Skip to main content

coven_protocol/
circle_control.rs

1//! Circle metadata, access records, controls, and creation objects.
2
3use hmac::{Hmac, KeyInit, Mac};
4use serde::{Deserialize, Serialize};
5use sha2::Sha256;
6
7use super::causal_grants::AuthorStreamId;
8use super::circle::CircleEpochCloseId;
9use super::circle::{generated_id_digest, AccessLeafId, CircleEpochId, CircleId};
10use super::circle_roster::{
11    CircleAuthorStreamKey, CircleGrantCreationAuthority, CircleMaterializedRoster,
12    CircleRosterChain, CircleRosterEntry, CircleRosterError, CircleRosterHead, CircleRosterHeadRef,
13    CircleRosterStateRef, MergeCircleRosterStateRef, ResolvedCircleRoster,
14};
15use super::membership::{MemberRole, MembershipGrantCreationAuthority, MembershipGrantId};
16use super::membership::{MembershipHeadRef, StoreMembershipConflictResolutionRef};
17use super::store_commit::{
18    CommitFrontier, ObjectHash, OwnerRecoveryCursor, Signed, SignedBody, SnapshotImageRef,
19    StoreBatchCommitRef, StoreDeviceRegistration, StoreDeviceRegistrationRef, StoreDeviceStateRef,
20    SuccessorLink,
21};
22use crate::objects::ExactObjectRef;
23use crate::objects::ObjectSlot;
24use coven_keys::encryption::{EncryptionService, KeyFingerprint, MasterKeyring};
25use coven_keys::keys::{self, UserKeypair};
26
27const RECIPIENT_SLOT_DOMAIN: &[u8] = b"coven.circle-recipient-slot.v1\0";
28const METADATA_DOMAIN: &[u8] = b"coven.circle-metadata.v1\0";
29const METADATA_HEAD_DOMAIN: &[u8] = b"coven.circle-metadata-head.v1\0";
30const ACCESS_DOMAIN: &[u8] = b"coven.circle-access-leaf.v1\0";
31const CONTROL_DOMAIN: &[u8] = b"coven.circle-control.v1\0";
32const CONTROL_HEAD_DOMAIN: &[u8] = b"coven.circle-control-head.v1\0";
33const CLOSE_INTENT_DOMAIN: &[u8] = b"coven.circle-epoch-close-intent.v1\0";
34const CLOSE_RESPONSE_DOMAIN: &[u8] = b"coven.circle-epoch-close-response.v1\0";
35const CLOSE_EXCLUSION_DOMAIN: &[u8] = b"coven.circle-epoch-close-exclusion.v1\0";
36const CLOSE_OUTCOME_DOMAIN: &[u8] = b"coven.circle-epoch-close-outcome.v1\0";
37const CLOSE_CANCELLATION_DOMAIN: &[u8] = b"coven.circle-epoch-close-cancellation.v1\0";
38const ENVELOPE_DOMAIN: &[u8] = b"coven.circle-access-envelope.v1\0";
39const OWNER_GRANT_ID_GENERATION_DOMAIN: &[u8] = b"coven.circle-owner-grant-id-generation.v1\0";
40
41mod access;
42mod control;
43mod drafts;
44mod epoch_close;
45mod epoch_transition;
46mod metadata;
47mod semantic_path;
48mod transition;
49
50pub use access::{
51    merkle_root_and_proofs, verify_merkle_proof, CircleAccessDisposition, CircleAccessLeaf,
52    CircleAccessLeafBody, MerkleStep,
53};
54pub use access::{CircleBootstrapCoverageRef, CircleBootstrapRef};
55pub use control::{
56    merge_frontier_head, AccessEnvelope, AccessEnvelopeBody, CircleControl, CircleControlBody,
57    CircleControlHead, CircleControlState, CircleControlValue, DeletedCircle,
58    MergeCircleControlHeadRef, MergeCircleControlOrder, MergeCircleOwnerAuthorityRef,
59    ResolvedConflictBranch,
60};
61#[cfg(any(test, feature = "test-utils"))]
62pub use drafts::CircleTransitionDraftPolicy;
63pub use drafts::{
64    CircleRosterDraftPolicy, CircleRosterPolicyObjects, CircleTransitionDraft,
65    CircleTransitionPolicyObjects, PreparedAccessLeaf, PreparedCircleAccess, PreparedCircleControl,
66    PreparedCircleTransition,
67};
68pub use epoch_close::{
69    ActiveCircleEpochCore, CircleEpochClose, CircleEpochCloseCancellation,
70    CircleEpochCloseExclusion, CircleEpochCloseExclusionRef, CircleEpochCloseIntent,
71    CircleEpochCloseOutcome, CircleEpochCloseParticipant, CircleEpochCloseResponse,
72    CircleEpochCloseResponseRef, CircleEpochCloseResponseSlotValue, CircleEpochCloseSettlement,
73    CircleEpochCloseSlotValue, CircleEpochOrigin, CircleEpochSuccessor, MergeActiveCircleEpoch,
74};
75pub use epoch_close::{
76    CircleEpochCloseCancellationRef, CircleEpochCloseIntentRef, CircleEpochCloseOutcomeRef,
77};
78pub use metadata::CircleMetadataHeadRef;
79pub use metadata::{
80    CircleMetadata, CircleMetadataBody, CircleMetadataCoord, CircleMetadataHead,
81    CircleMetadataStateRef, MergeCircleMetadataStateRef,
82};
83pub use semantic_path::{
84    circle_control_head_prefix, circle_epoch_close_intent_semantic_prefix,
85    circle_epoch_close_outcome_semantic_prefix, circle_epoch_close_response_semantic_prefix,
86    circle_metadata_head_prefix, circle_roster_head_prefix, circle_semantic_prefix, recipient_slot,
87    recipient_slot_with_peer, verify_circle_semantic_prefix, CircleSemanticSlot,
88};
89
90/// Exact coordinate of one signed circle control entry.
91#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
92#[serde(deny_unknown_fields)]
93pub struct CircleControlCoord {
94    pub device_id: String,
95    pub stream_id: AuthorStreamId,
96    pub author_pubkey: String,
97    pub author_owner_grant: MembershipGrantId,
98    pub seq: u64,
99    pub control_hash: ObjectHash,
100}
101
102impl CircleControlCoord {
103    pub fn control_hash(&self) -> ObjectHash {
104        self.control_hash
105    }
106
107    pub fn validate(&self) -> Result<(), CircleControlCoordError> {
108        if self.device_id.is_empty() || self.author_pubkey.is_empty() || self.seq == 0 {
109            Err(CircleControlCoordError)
110        } else {
111            Ok(())
112        }
113    }
114
115    pub fn stream_key(&self) -> CircleAuthorStreamKey {
116        CircleAuthorStreamKey {
117            author_pubkey: self.author_pubkey.clone(),
118            device_id: self.device_id.clone(),
119            stream_id: self.stream_id,
120            author_owner_grant: self.author_owner_grant.clone(),
121        }
122    }
123
124    /// A well-formed coordinate that names no real control, for API dispatch tests
125    /// that only need a value to send through the command channel.
126    #[cfg(any(test, feature = "test-utils"))]
127    pub fn placeholder(seed: u8) -> Self {
128        let hash = ObjectHash::digest(&[seed]);
129        Self {
130            device_id: format!("device-{seed}"),
131            stream_id: AuthorStreamId::from_digest(hash),
132            author_pubkey: format!("pubkey-{seed}"),
133            author_owner_grant: MembershipGrantId(hash),
134            seq: 1,
135            control_hash: hash,
136        }
137    }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
141#[error("circle control coordinate has an empty device/author or zero sequence/generation")]
142pub struct CircleControlCoordError;
143
144/// The exact Store membership state whose identities require access dispositions.
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(deny_unknown_fields)]
147pub struct StoreMembershipStateRef {
148    pub heads: Vec<MembershipHeadRef>,
149    pub resolutions: Vec<StoreMembershipConflictResolutionRef>,
150    pub recovery: Vec<OwnerRecoveryCursor>,
151    pub state_hash: ObjectHash,
152}
153
154impl StoreMembershipStateRef {
155    pub fn from_membership(
156        membership: &crate::membership::MembershipChain,
157        recovery: Vec<OwnerRecoveryCursor>,
158    ) -> Result<Self, super::store_commit::StoreProtocolError> {
159        let crate::membership::MembershipStatus::Resolved(resolved) = membership.status() else {
160            return Err(super::store_commit::StoreProtocolError::Malformed(
161                "Store membership state is conflicted".to_string(),
162            ));
163        };
164        Self::from_parts(
165            membership.head_refs().to_vec(),
166            membership.resolution_refs().to_vec(),
167            recovery,
168            resolved.state_hash,
169        )
170    }
171
172    pub fn from_parts(
173        mut heads: Vec<MembershipHeadRef>,
174        mut resolutions: Vec<StoreMembershipConflictResolutionRef>,
175        recovery: Vec<OwnerRecoveryCursor>,
176        membership_state_hash: ObjectHash,
177    ) -> Result<Self, super::store_commit::StoreProtocolError> {
178        heads.sort();
179        resolutions.sort();
180        let recovery = super::store_commit::canonical_recovery_cursors(recovery)?;
181        Ok(Self {
182            heads,
183            resolutions,
184            state_hash: membership_state_ref_hash(membership_state_hash, &recovery),
185            recovery,
186        })
187    }
188
189    pub fn state_hash(&self) -> ObjectHash {
190        self.state_hash
191    }
192
193    pub fn recovery(&self) -> &[OwnerRecoveryCursor] {
194        &self.recovery
195    }
196
197    pub fn validate_shape(&self) -> Result<(), super::store_commit::StoreProtocolError> {
198        super::store_commit::validate_recovery_cursors(self.recovery())?;
199        if self.heads.windows(2).any(|pair| pair[0] >= pair[1])
200            || self.resolutions.windows(2).any(|pair| pair[0] >= pair[1])
201        {
202            return Err(super::store_commit::StoreProtocolError::Malformed(
203                "Store membership state reference is not canonical".to_string(),
204            ));
205        }
206        Ok(())
207    }
208}
209
210fn membership_state_ref_hash(
211    membership_state_hash: ObjectHash,
212    recovery: &[OwnerRecoveryCursor],
213) -> ObjectHash {
214    ObjectHash::digest(
215        &serde_json::to_vec(&(
216            "coven.store-membership-state-ref.v1",
217            membership_state_hash,
218            recovery,
219        ))
220        .expect("Store membership state hash serialization cannot fail"),
221    )
222}
223
224impl CircleTransitionDraft {}
225
226#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
227pub enum CircleTransitionError {
228    #[error("circle name cannot be empty")]
229    EmptyName,
230    #[error("circle creator is not a current Store writer")]
231    AuthorNotStoreWriter,
232    #[error("circle operation author is not a current Circle Owner")]
233    AuthorNotCircleOwner,
234    #[error("circle operation current state is invalid")]
235    InvalidCurrentState,
236    #[error("circle transition sequence overflow")]
237    SequenceOverflow,
238    #[error("circle recipient has an invalid Ed25519 public key: {0}")]
239    InvalidRecipient(String),
240    #[error("circle member is not a current Store member: {0}")]
241    MemberNotInStore(String),
242    #[error("circle roster: {0}")]
243    Roster(#[from] CircleRosterError),
244}
245
246#[cfg(test)]
247mod tests;