Skip to main content

coven_protocol/circle_control/
control.rs

1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4#[serde(deny_unknown_fields)]
5pub struct MergeCircleControlOrder {
6    pub device_id: String,
7    pub stream_id: AuthorStreamId,
8    pub author_owner_grant: MembershipGrantId,
9    pub seq: u64,
10    pub previous_control_hash: Option<ObjectHash>,
11    pub dependencies: Vec<CircleControlCoord>,
12}
13
14/// A terminal deletion. It freezes the epoch spine it terminated — the same
15/// `MergeActiveCircleEpoch` an `EpochClose` freezes — so historical package
16/// verification and exact reclamation keep the epoch, key fingerprint, and
17/// roster-head spine with no live access material.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19#[serde(deny_unknown_fields)]
20pub struct DeletedCircle {
21    pub frozen_epoch: MergeActiveCircleEpoch,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25#[serde(rename_all = "snake_case", deny_unknown_fields)]
26pub enum CircleControlState {
27    ActiveEpoch(MergeActiveCircleEpoch),
28    EpochClose(CircleEpochClose),
29    Deleted(DeletedCircle),
30}
31
32impl CircleControlState {
33    pub fn access_epoch(&self) -> &MergeActiveCircleEpoch {
34        match self {
35            Self::ActiveEpoch(active) => active,
36            Self::EpochClose(close) => &close.frozen_epoch,
37            Self::Deleted(deleted) => &deleted.frozen_epoch,
38        }
39    }
40
41    pub fn access_epoch_mut(&mut self) -> &mut MergeActiveCircleEpoch {
42        match self {
43            Self::ActiveEpoch(active) => active,
44            Self::EpochClose(close) => &mut close.frozen_epoch,
45            Self::Deleted(deleted) => &mut deleted.frozen_epoch,
46        }
47    }
48
49    pub fn active_epoch(&self) -> Option<&MergeActiveCircleEpoch> {
50        match self {
51            Self::ActiveEpoch(active) => Some(active),
52            Self::EpochClose(_) | Self::Deleted(_) => None,
53        }
54    }
55
56    pub fn active_epoch_mut(&mut self) -> Option<&mut MergeActiveCircleEpoch> {
57        match self {
58            Self::ActiveEpoch(active) => Some(active),
59            Self::EpochClose(_) | Self::Deleted(_) => None,
60        }
61    }
62
63    pub fn is_deleted(&self) -> bool {
64        matches!(self, Self::Deleted(_))
65    }
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
69#[serde(deny_unknown_fields)]
70pub struct MergeCircleControlHeadRef {
71    pub coord: CircleControlCoord,
72    pub head_hash: ObjectHash,
73    pub object: ExactObjectRef,
74}
75
76/// One losing branch of a resolved control conflict, carried so the resolution
77/// can cover every branch's frontier rather than only the chosen branch's: the
78/// branch's control head, its metadata and roster head frontiers, and the
79/// metadata entry that branch selected. The resolution unions these into its own
80/// frontier so no author-stream head is re-allocated once the conflict collapses,
81/// and re-derives its name as the deterministic metadata selection across the
82/// union.
83#[derive(Debug, Clone)]
84pub struct ResolvedConflictBranch {
85    pub control_head: MergeCircleControlHeadRef,
86    pub metadata_heads: Vec<CircleMetadataHeadRef>,
87    pub roster_heads: Vec<CircleRosterHeadRef>,
88    pub selected_metadata: CircleMetadata,
89}
90
91/// Insert `head` into a frontier keyed by author stream, keeping the deeper
92/// (higher-sequence) head when the stream already carries one. Merging every
93/// conflicting branch's heads this way yields the union frontier: each stream is
94/// covered at its deepest position across all branches, so a device that authored
95/// on that stream continues from its own head instead of re-allocating it.
96pub fn merge_frontier_head<H>(
97    frontier: &mut Vec<H>,
98    head: H,
99    stream_key: impl Fn(&H) -> CircleAuthorStreamKey,
100    seq: impl Fn(&H) -> u64,
101) {
102    let key = stream_key(&head);
103    match frontier
104        .iter_mut()
105        .find(|existing| stream_key(existing) == key)
106    {
107        Some(existing) if seq(&head) > seq(existing) => *existing = head,
108        Some(_) => {}
109        None => frontier.push(head),
110    }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114#[serde(rename_all = "snake_case", deny_unknown_fields)]
115pub enum MergeCircleOwnerAuthorityRef {
116    Roster {
117        roster: MergeCircleRosterStateRef,
118        grant_id: MembershipGrantId,
119        created_at: crate::circle_roster::CircleRosterCoord,
120    },
121    ConflictResolution {
122        conflict_hash: ObjectHash,
123        resolution_hash: ObjectHash,
124    },
125}
126
127impl MergeCircleOwnerAuthorityRef {
128    pub(crate) fn grant_id(&self, author_pubkey: &str) -> MembershipGrantId {
129        match self {
130            Self::Roster { grant_id, .. } => grant_id.clone(),
131            Self::ConflictResolution { conflict_hash, .. } => {
132                crate::circle_roster::derive_circle_resolution_grant(conflict_hash, author_pubkey)
133            }
134        }
135    }
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139#[serde(deny_unknown_fields)]
140pub struct CircleControlValue {
141    pub order: MergeCircleControlOrder,
142    pub state: CircleControlState,
143    pub author_authority: MergeCircleOwnerAuthorityRef,
144    pub membership_authority: MembershipGrantCreationAuthority,
145}
146
147/// The wire body of one Circle control. Every field here is signed.
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149#[serde(deny_unknown_fields)]
150pub struct CircleControlBody {
151    pub store_root_hash: ObjectHash,
152    pub circle_id: CircleId,
153    pub value: CircleControlValue,
154    pub author_pubkey: String,
155}
156
157impl SignedBody for CircleControlBody {
158    const DOMAIN: &'static [u8] = CONTROL_DOMAIN;
159}
160
161pub type CircleControl = Signed<CircleControlBody>;
162
163impl CircleControlBody {
164    pub fn state(&self) -> &CircleControlState {
165        &self.value.state
166    }
167
168    pub fn active_epoch(&self) -> Option<&MergeActiveCircleEpoch> {
169        self.value.state.active_epoch()
170    }
171
172    pub fn access_epoch(&self) -> &MergeActiveCircleEpoch {
173        self.value.state.access_epoch()
174    }
175
176    pub fn active_common(&self) -> &ActiveCircleEpochCore {
177        &self.access_epoch().common
178    }
179
180    pub fn epoch_id(&self) -> CircleEpochId {
181        self.active_common().epoch_id
182    }
183
184    pub fn key_fingerprint(&self) -> KeyFingerprint {
185        self.active_common().key_fingerprint
186    }
187
188    pub fn owners(&self) -> &[String] {
189        &self.active_common().owners
190    }
191
192    pub(crate) fn access_root(&self) -> ObjectHash {
193        self.active_common().access_root
194    }
195
196    pub fn roster_state_ref(&self) -> CircleRosterStateRef {
197        self.access_epoch().roster.clone()
198    }
199
200    pub fn metadata_state_ref(&self) -> CircleMetadataStateRef {
201        self.access_epoch().metadata.clone()
202    }
203
204    pub fn store_membership_state_ref(&self) -> StoreMembershipStateRef {
205        self.access_epoch().store_membership.clone()
206    }
207
208    pub fn previous_control_hash(&self) -> Option<ObjectHash> {
209        self.value.order.previous_control_hash
210    }
211
212    pub fn is_founder(&self) -> bool {
213        self.value.order.seq == 1
214            && self.value.order.previous_control_hash.is_none()
215            && self.value.order.dependencies.is_empty()
216    }
217
218    pub(crate) fn ordinal(&self) -> u64 {
219        self.value.order.seq
220    }
221
222    pub fn author_grant_id(&self) -> MembershipGrantId {
223        self.value.author_authority.grant_id(&self.author_pubkey)
224    }
225
226    #[cfg(any(test, feature = "test-utils"))]
227    pub fn membership_authority(&self) -> &MembershipGrantCreationAuthority {
228        &self.value.membership_authority
229    }
230}
231
232impl CircleControl {
233    pub fn control_hash(&self) -> ObjectHash {
234        self.hash()
235    }
236
237    pub fn causally_covers(&self, prior: &Self) -> bool {
238        if self.store_root_hash != prior.store_root_hash || self.circle_id != prior.circle_id {
239            return false;
240        }
241        self.value.order.previous_control_hash == Some(prior.control_hash())
242            || self
243                .value
244                .order
245                .dependencies
246                .binary_search(&prior.coord())
247                .is_ok()
248    }
249
250    pub fn verify(&self) -> bool {
251        let order = &self.value.order;
252        let access_epoch = self.access_epoch();
253        let author_authority = &self.value.author_authority;
254        let grant_id = author_authority.grant_id(&self.author_pubkey);
255        let stream_key = CircleAuthorStreamKey {
256            author_pubkey: self.author_pubkey.clone(),
257            device_id: order.device_id.clone(),
258            stream_id: order.stream_id,
259            author_owner_grant: order.author_owner_grant.clone(),
260        };
261        let covered_are_canonical = access_epoch
262            .covered_control_heads
263            .windows(2)
264            .all(|pair| pair[0].coord.stream_key() < pair[1].coord.stream_key());
265        let own_predecessor = access_epoch
266            .covered_control_heads
267            .iter()
268            .find(|head| head.coord.stream_key() == stream_key);
269        let expected_dependencies = access_epoch
270            .covered_control_heads
271            .iter()
272            .filter(|head| head.coord.stream_key() != stream_key)
273            .map(|head| head.coord.clone())
274            .collect::<Vec<_>>();
275        let order_is_valid = !order.device_id.is_empty()
276            && order.seq > 0
277            && order.author_owner_grant == grant_id
278            && covered_are_canonical
279            && order.dependencies == expected_dependencies;
280        let authority_is_founder_roster = matches!(
281            author_authority,
282            MergeCircleOwnerAuthorityRef::Roster { roster, .. }
283                if roster == &access_epoch.roster
284        );
285        let founder = order.seq == 1 && access_epoch.covered_control_heads.is_empty();
286        let continuity_is_valid = match (order.seq, own_predecessor) {
287            (1, None) => order.previous_control_hash.is_none(),
288            (seq, Some(predecessor)) if seq > 1 => {
289                predecessor.coord.seq.checked_add(1) == Some(seq)
290                    && order.previous_control_hash == Some(predecessor.coord.control_hash)
291            }
292            _ => false,
293        };
294        let founder_identity_is_valid = !founder
295            || (authority_is_founder_roster
296                && self.circle_id
297                    == CircleId::founder(self.store_root_hash, &self.author_pubkey, &grant_id));
298        let common = &access_epoch.common;
299        let owners_are_canonical =
300            !common.owners.is_empty() && common.owners.windows(2).all(|pair| pair[0] < pair[1]);
301        let origin_is_valid = match &common.origin {
302            CircleEpochOrigin::Founder => true,
303            CircleEpochOrigin::Closed { cutoff, .. } => {
304                crate::store_commit::validate_commit_frontier(cutoff).is_ok()
305            }
306        };
307        let state_is_valid = match &self.value.state {
308            CircleControlState::ActiveEpoch(_) => true,
309            CircleControlState::EpochClose(close) => !founder && close.verify_shape(self.circle_id),
310            // A deletion is always a successor of a live control; the frozen
311            // epoch it carries is validated by the shared access-epoch checks
312            // above.
313            CircleControlState::Deleted(_) => !founder,
314        };
315        owners_are_canonical
316            && origin_is_valid
317            && state_is_valid
318            && order_is_valid
319            && continuity_is_valid
320            && founder_identity_is_valid
321            && self.verify_by(&self.author_pubkey).is_ok()
322    }
323
324    pub fn coord(&self) -> CircleControlCoord {
325        let order = &self.value.order;
326        CircleControlCoord {
327            device_id: order.device_id.clone(),
328            stream_id: order.stream_id,
329            author_pubkey: self.author_pubkey.clone(),
330            author_owner_grant: order.author_owner_grant.clone(),
331            seq: order.seq,
332            control_hash: self.control_hash(),
333        }
334    }
335}
336
337/// The wire body of one Circle control head. Every field here is signed.
338#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
339#[serde(deny_unknown_fields)]
340pub struct CircleControlHeadBody {
341    pub store_root_hash: ObjectHash,
342    pub circle_id: CircleId,
343    pub control: CircleControlCoord,
344    pub entry: ExactObjectRef,
345    pub successor: SuccessorLink,
346}
347
348impl SignedBody for CircleControlHeadBody {
349    const DOMAIN: &'static [u8] = CONTROL_HEAD_DOMAIN;
350}
351
352pub type CircleControlHead = Signed<CircleControlHeadBody>;
353
354impl CircleControlHead {
355    pub fn signed(
356        control: &CircleControl,
357        entry: ExactObjectRef,
358        successor: SuccessorLink,
359        signer: &UserKeypair,
360    ) -> Self {
361        Signed::sign(
362            CircleControlHeadBody {
363                store_root_hash: control.store_root_hash,
364                circle_id: control.circle_id,
365                control: control.coord(),
366                entry,
367                successor,
368            },
369            signer,
370        )
371    }
372
373    pub fn head_hash(&self) -> ObjectHash {
374        self.hash()
375    }
376
377    pub fn verify(&self, registration: &StoreDeviceRegistration) -> bool {
378        self.control.validate().is_ok()
379            && self.control.device_id == registration.device_id.to_string()
380            && self.verify_by(&registration.device_signing_pubkey).is_ok()
381    }
382}
383
384/// The wire body of one recipient's access envelope. Every field here is
385/// signed.
386#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
387#[serde(deny_unknown_fields)]
388pub struct AccessEnvelopeBody {
389    pub store_root_hash: ObjectHash,
390    pub candidate_family: crate::store_commit::CandidateFamilyId,
391    pub circle_id: CircleId,
392    pub owner_pubkey: String,
393    pub recipient_slot: String,
394    pub control_hash: ObjectHash,
395    pub leaf_id: AccessLeafId,
396    pub leaf_hash: ObjectHash,
397    pub value_hash: ObjectHash,
398    pub proof: Vec<MerkleStep>,
399}
400
401impl SignedBody for AccessEnvelopeBody {
402    const DOMAIN: &'static [u8] = ENVELOPE_DOMAIN;
403}
404
405pub type AccessEnvelope = Signed<AccessEnvelopeBody>;
406
407impl AccessEnvelope {
408    pub fn verify(
409        &self,
410        control: &PreparedCircleControl,
411        candidate_family: crate::store_commit::CandidateFamilyId,
412    ) -> bool {
413        self.store_root_hash == control.value.store_root_hash
414            && self.candidate_family == candidate_family
415            && self.circle_id == control.value.circle_id
416            && self.owner_pubkey == control.value.author_pubkey
417            && self.control_hash == control.coord.control_hash()
418            && self.verify_by(&self.owner_pubkey).is_ok()
419            && verify_merkle_proof(self.leaf_hash, &self.proof, control.value.access_root())
420    }
421}