1use 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::MembershipHeadRef;
16use super::membership::{MemberRole, MembershipCoord, MembershipGrantId};
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#[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 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
146#[serde(deny_unknown_fields)]
147pub struct StoreMembershipStateRef {
148 pub heads: Vec<MembershipHeadRef>,
149 pub recovery: Vec<OwnerRecoveryCursor>,
150 pub state_hash: ObjectHash,
151}
152
153impl StoreMembershipStateRef {
154 pub fn from_membership(
155 membership: &crate::membership::MembershipChain,
156 recovery: Vec<OwnerRecoveryCursor>,
157 ) -> Result<Self, super::store_commit::StoreProtocolError> {
158 let resolved = membership.resolved();
159 Self::from_parts(
160 membership.head_refs().to_vec(),
161 recovery,
162 resolved.state_hash,
163 )
164 }
165
166 pub fn from_parts(
167 mut heads: Vec<MembershipHeadRef>,
168 recovery: Vec<OwnerRecoveryCursor>,
169 membership_state_hash: ObjectHash,
170 ) -> Result<Self, super::store_commit::StoreProtocolError> {
171 heads.sort();
172 let recovery = super::store_commit::canonical_recovery_cursors(recovery)?;
173 Ok(Self {
174 heads,
175 state_hash: membership_state_ref_hash(membership_state_hash, &recovery),
176 recovery,
177 })
178 }
179
180 pub fn state_hash(&self) -> ObjectHash {
181 self.state_hash
182 }
183
184 pub fn recovery(&self) -> &[OwnerRecoveryCursor] {
185 &self.recovery
186 }
187
188 pub fn validate_shape(&self) -> Result<(), super::store_commit::StoreProtocolError> {
189 super::store_commit::validate_recovery_cursors(self.recovery())?;
190 if self.heads.windows(2).any(|pair| pair[0] >= pair[1]) {
191 return Err(super::store_commit::StoreProtocolError::Malformed(
192 "Store membership state reference is not canonical".to_string(),
193 ));
194 }
195 Ok(())
196 }
197}
198
199fn membership_state_ref_hash(
200 membership_state_hash: ObjectHash,
201 recovery: &[OwnerRecoveryCursor],
202) -> ObjectHash {
203 ObjectHash::digest(
204 &serde_json::to_vec(&(
205 "coven.store-membership-state-ref.v1",
206 membership_state_hash,
207 recovery,
208 ))
209 .expect("Store membership state hash serialization cannot fail"),
210 )
211}
212
213impl CircleTransitionDraft {}
214
215#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
216pub enum CircleTransitionError {
217 #[error("circle name cannot be empty")]
218 EmptyName,
219 #[error("circle creator is not a current Store writer")]
220 AuthorNotStoreWriter,
221 #[error("circle operation author is not a current Circle Owner")]
222 AuthorNotCircleOwner,
223 #[error("circle operation current state is invalid")]
224 InvalidCurrentState,
225 #[error("circle transition sequence overflow")]
226 SequenceOverflow,
227 #[error("circle recipient has an invalid Ed25519 public key: {0}")]
228 InvalidRecipient(String),
229 #[error("circle member is not a current Store member: {0}")]
230 MemberNotInStore(String),
231 #[error("circle roster: {0}")]
232 Roster(#[from] CircleRosterError),
233}
234
235#[cfg(test)]
236mod tests;