Skip to main content

coven_database/store/store_session/
circle_authority.rs

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
9/// Package keys resolved against the installed state and verified prepared controls.
10/// Successor keys still require the historical roster to authorize the package author.
11pub enum CirclePackageAccess {
12    Exact(coven_protocol::circle_activation::CircleEpochAccess),
13    Historical(String),
14}
15
16/// The three states a Circle control's activating commit can be in when resolved
17/// from the retained authority: not an activation at all, a known activation whose
18/// materialization has been reclaimed, or a retained activation with its commit.
19enum CircleActivationCommitLookup {
20    Absent,
21    Reclaimed { stream_id: String, sequence: u64 },
22    Retained(StoreBatchCommitRef),
23}
24
25/// Resolve a Circle control's activating commit reference from retained
26/// authority. A known activation whose materialization was reclaimed is an
27/// error because strict callers require the current control to stay retained.
28pub(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
45/// Resolve a Circle control's activating commit, reading a reclaimed
46/// materialization as absence because its standalone snapshot is superseded.
47pub(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            // Exact access remains valid for packages within the accepted epoch
191            // cutoff even after a successor removes the local member.
192            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            &current.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            &current.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                // Snapshot recipient access can enrich the installed public
352                // control without publishing that control a second time.
353                state = Some(if activation.local_access.is_some() {
354                    next
355                } else {
356                    current
357                });
358                continue;
359            }
360            let heads = match &current {
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    /// Verify bootstrap blob authority against retained controls and the caller's
608    /// verified candidate predecessor controls in one ancestry traversal owner.
609    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, &current, &blobs, &activations)
618        })
619        .await
620    }
621
622    /// Whether one activated Circle control strictly covers another in the retained
623    /// control lineage — `covering` is a proper successor of `covered`. Bootstrap
624    /// reclamation uses this to prove a removed recipient lost authority under a
625    /// successor control that supersedes its seed's control. `false` when the
626    /// controls are equal or `covering` is not retained.
627    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    /// The head control of a Circle: the retained control whose lineage no other
766    /// retained control covers. Restore resolves the restoring identity's current
767    /// access at the head control's activating commit, so a member removed by a
768    /// later epoch close resolves against the successor control that excludes them
769    /// — never against a stale predecessor that still lists them active. A Circle
770    /// with two uncovered controls is a forked lineage and fails loud.
771    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        // A control whose activating commit was reclaimed is superseded by a later
779        // epoch and cannot be head; keep only controls whose commit is retained.
780        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, &current, &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}