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::{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#[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 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;