1use super::*;
2
3#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
4#[serde(deny_unknown_fields)]
5pub struct PreparedCircleControl {
6 pub coord: CircleControlCoord,
7 pub bytes: Vec<u8>,
8 pub value: CircleControl,
9}
10
11impl PreparedCircleControl {
12 pub fn verify(&self) -> bool {
13 self.bytes
14 == serde_json::to_vec(&self.value).expect("circle control serialization cannot fail")
15 && self.value.verify()
16 && self.coord == self.value.coord()
17 }
18}
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22pub struct PreparedAccessLeaf {
23 pub bytes: Vec<u8>,
24 pub value: CircleAccessLeaf,
25 pub leaf_hash: ObjectHash,
26}
27
28impl PreparedAccessLeaf {
29 pub fn verify(
30 &self,
31 control: &PreparedCircleControl,
32 candidate_family: crate::store_commit::CandidateFamilyId,
33 ) -> bool {
34 self.value.verify_for_control(control, candidate_family)
35 && ObjectHash::digest(&self.bytes) == self.leaf_hash
36 }
37
38 pub fn verify_envelope(
39 &self,
40 control: &PreparedCircleControl,
41 envelope: &AccessEnvelope,
42 candidate_family: crate::store_commit::CandidateFamilyId,
43 ) -> bool {
44 self.verify(control, candidate_family)
45 && envelope.verify(control, candidate_family)
46 && self.leaf_hash == envelope.leaf_hash
47 && envelope.value_hash
48 == ObjectHash::digest(
49 &serde_json::to_vec(&self.value)
50 .expect("circle access leaf serialization cannot fail"),
51 )
52 && self.value.leaf_id == envelope.leaf_id
53 && self.value.owner_pubkey == envelope.owner_pubkey
54 && self.value.recipient_slot == envelope.recipient_slot
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(deny_unknown_fields)]
60pub struct PreparedCircleAccess {
61 pub leaf: PreparedAccessLeaf,
62 pub envelope: AccessEnvelope,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
66#[serde(deny_unknown_fields)]
67pub struct CircleRosterPolicyObjects {
68 pub entry: CircleRosterEntry,
69 pub head: CircleRosterHead,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(deny_unknown_fields)]
74pub struct CircleTransitionPolicyObjects {
75 pub roster: Option<CircleRosterPolicyObjects>,
76 pub metadata_head: Option<CircleMetadataHead>,
77 pub control_head: CircleControlHead,
78}
79
80#[derive(Debug, Clone)]
81pub enum CircleRosterDraftPolicy {
82 Inherited,
83 Founder {
84 entry: CircleRosterEntry,
85 },
86 Successor {
87 predecessor: CircleRosterChain,
88 entry: CircleRosterEntry,
89 },
90}
91
92#[derive(Debug, Clone)]
93pub struct CircleTransitionDraftPolicy {
94 pub roster: CircleRosterDraftPolicy,
95 pub metadata_successor: bool,
96}
97
98#[derive(Debug, Clone)]
99pub struct CircleTransitionDraft {
100 pub circle_id: CircleId,
101 pub epoch_id: CircleEpochId,
102 pub keyring: String,
103 pub roster: CircleMaterializedRoster,
104 pub policy: CircleTransitionDraftPolicy,
105 pub metadata: CircleMetadata,
106 pub close_intent: Option<CircleEpochCloseIntent>,
107 pub close_finalization: Option<CircleEpochCloseFinalizationDraft>,
108 pub close_cancellation: Option<CircleEpochCloseCancellationDraft>,
109 pub access: Vec<PreparedCircleAccess>,
110 pub control: PreparedCircleControl,
111}
112
113#[derive(Debug, Clone)]
114pub struct CircleEpochCloseFinalizationDraft {
115 pub close_control: PreparedCircleControl,
116 pub intent: CircleEpochCloseIntent,
117 pub responses: Vec<CircleEpochCloseSettlement>,
118 pub outcome_slot: ObjectSlot,
119}
120
121#[derive(Debug, Clone)]
122pub struct CircleEpochCloseCancellationDraft {
123 pub close_control: PreparedCircleControl,
124 pub outcome_slot: ObjectSlot,
125}
126
127#[derive(Debug, Clone)]
128pub(super) struct FounderRosterObjects {
129 pub(super) entry: CircleRosterEntry,
130 pub(super) resolved: ResolvedCircleRoster,
131}
132
133pub(super) struct CircleAccessDraft<'identity> {
134 store_root_hash: ObjectHash,
135 candidate_family: crate::store_commit::CandidateFamilyId,
136 circle_id: CircleId,
137 access_root: ObjectHash,
138 leaves: Vec<PreparedAccessLeaf>,
139 proofs: Vec<Vec<MerkleStep>>,
140 signer: &'identity dyn coven_keys::keys::IdentityKeyAuthority,
141}
142
143impl<'identity> CircleAccessDraft<'identity> {
144 #[allow(clippy::too_many_arguments)]
145 pub(super) fn prepare(
146 store_root_hash: ObjectHash,
147 candidate_family: crate::store_commit::CandidateFamilyId,
148 circle_id: CircleId,
149 epoch_id: CircleEpochId,
150 keyring: &str,
151 key_fingerprint: KeyFingerprint,
152 roster_state: &CircleRosterStateRef,
153 roster_members: &std::collections::BTreeMap<String, crate::circle::CircleRole>,
154 store_membership: &StoreMembershipStateRef,
155 store_members: &[(String, MemberRole)],
156 bootstraps: &std::collections::BTreeMap<String, CircleBootstrapRef>,
157 ids: &dyn coven_foundation::id_provider::IdProvider,
158 signer: &'identity dyn coven_keys::keys::IdentityKeyAuthority,
159 ) -> Result<Self, CircleTransitionError> {
160 let author_pubkey = keys::public_key_hex(signer);
161 let leaves = store_members
162 .iter()
163 .map(|(recipient_pubkey, _)| {
164 let recipient_slot = recipient_slot(signer, recipient_pubkey, circle_id)?;
165 let disposition = if roster_members.contains_key(recipient_pubkey) {
166 CircleAccessDisposition::Active {
167 keyring: keyring.to_string(),
168 key_fingerprint,
169 roster: roster_state.clone(),
170 bootstrap: bootstraps.get(recipient_pubkey).cloned(),
171 }
172 } else {
173 CircleAccessDisposition::Inactive
174 };
175 let value = CircleAccessLeafBody {
176 store_root_hash,
177 candidate_family,
178 circle_id,
179 epoch_id,
180 leaf_id: AccessLeafId::generate(ids),
181 owner_pubkey: author_pubkey.clone(),
182 recipient_pubkey: recipient_pubkey.clone(),
183 recipient_slot,
184 disposition,
185 store_membership: store_membership.clone(),
186 };
187 let value = Signed::sign(value, signer);
188 let recipient_x25519 = keys::ed25519_hex_to_x25519_public_key(recipient_pubkey)
189 .map_err(|_| {
190 CircleTransitionError::InvalidRecipient(recipient_pubkey.clone())
191 })?;
192 let plaintext =
193 serde_json::to_vec(&value).expect("circle access serialization cannot fail");
194 let bytes = keys::seal_box_encrypt(&plaintext, &recipient_x25519);
195 let leaf_hash = ObjectHash::digest(&bytes);
196 Ok::<PreparedAccessLeaf, CircleTransitionError>(PreparedAccessLeaf {
197 bytes,
198 value,
199 leaf_hash,
200 })
201 })
202 .collect::<Result<Vec<_>, _>>()?;
203 let leaf_hashes = leaves.iter().map(|leaf| leaf.leaf_hash).collect::<Vec<_>>();
204 let (access_root, proofs) = merkle_root_and_proofs(&leaf_hashes);
205 Ok(Self {
206 store_root_hash,
207 candidate_family,
208 circle_id,
209 access_root,
210 leaves,
211 proofs,
212 signer,
213 })
214 }
215
216 pub(super) fn access_root(&self) -> ObjectHash {
217 self.access_root
218 }
219
220 pub(super) fn finish(
221 self,
222 control: &PreparedCircleControl,
223 ) -> Result<Vec<PreparedCircleAccess>, CircleTransitionError> {
224 let author_pubkey = keys::public_key_hex(self.signer);
225 if control.value.store_root_hash != self.store_root_hash
226 || control.value.circle_id != self.circle_id
227 || control.value.author_pubkey != author_pubkey
228 || control.value.access_root() != self.access_root
229 {
230 return Err(CircleTransitionError::InvalidCurrentState);
231 }
232 Ok(self
233 .leaves
234 .into_iter()
235 .zip(self.proofs)
236 .map(|(leaf, proof)| {
237 let envelope = AccessEnvelopeBody {
238 store_root_hash: self.store_root_hash,
239 candidate_family: self.candidate_family,
240 circle_id: self.circle_id,
241 owner_pubkey: author_pubkey.clone(),
242 recipient_slot: leaf.value.recipient_slot.clone(),
243 control_hash: control.coord.control_hash(),
244 leaf_id: leaf.value.leaf_id,
245 leaf_hash: leaf.leaf_hash,
246 value_hash: ObjectHash::digest(
247 &serde_json::to_vec(&leaf.value)
248 .expect("circle access leaf serialization cannot fail"),
249 ),
250 proof,
251 };
252 PreparedCircleAccess {
253 leaf,
254 envelope: Signed::sign(envelope, self.signer),
255 }
256 })
257 .collect())
258 }
259}
260
261#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
262#[serde(deny_unknown_fields)]
263pub struct PreparedCircleTransition {
264 pub circle_id: CircleId,
265 pub epoch_id: CircleEpochId,
266 pub keyring: String,
267 pub roster: CircleMaterializedRoster,
268 pub policy_objects: CircleTransitionPolicyObjects,
269 pub metadata: CircleMetadata,
270 pub close_intent: Option<CircleEpochCloseIntent>,
271 pub close_outcome: Option<CircleEpochCloseOutcome>,
272 pub close_cancellation: Option<CircleEpochCloseCancellation>,
273 pub access: Vec<PreparedCircleAccess>,
274 pub control: PreparedCircleControl,
275}
276
277impl PreparedCircleTransition {
278 pub fn resolved_roster(&self) -> CircleMaterializedRoster {
279 self.roster.clone()
280 }
281
282 pub fn control_ref(
283 &self,
284 objects: crate::store_commit::CircleActivationObjects,
285 head_object: Option<ExactObjectRef>,
286 ) -> crate::store_commit::CircleControlRef {
287 let head_object =
288 head_object.expect("prepared Circle transition must contain its stored head");
289 crate::store_commit::CircleControlRef {
290 circle_id: self.circle_id,
291 control: self.control.coord.clone(),
292 head_hash: self.policy_objects.control_head.head_hash(),
293 head_object,
294 objects,
295 }
296 }
297}
298
299pub(super) struct CircleSuccessorContext<'a> {
300 pub(super) store_members: Vec<(String, MemberRole)>,
301 pub(super) author_pubkey: String,
302 pub(super) epoch: &'a MergeActiveCircleEpoch,
303 pub(super) grant_id: MembershipGrantId,
304 pub(super) author_authority: MergeCircleOwnerAuthorityRef,
305 pub(super) key_fingerprint: KeyFingerprint,
306}
307
308pub(super) fn circle_successor_context<'a>(
312 store_members: Vec<(String, MemberRole)>,
313 current_control: &'a PreparedCircleControl,
314 current_roster: &CircleMaterializedRoster,
315 current_metadata: &CircleMetadata,
316 keyring: &str,
317 signer: &dyn coven_keys::keys::IdentityKeyAuthority,
318) -> Result<CircleSuccessorContext<'a>, CircleTransitionError> {
319 let epoch = current_control
320 .value
321 .active_epoch()
322 .ok_or(CircleTransitionError::InvalidCurrentState)?;
323 circle_authored_successor_context(
324 store_members,
325 current_control,
326 current_roster,
327 current_metadata,
328 keyring,
329 signer,
330 epoch,
331 )
332}
333
334pub(super) fn circle_delete_successor_context<'a>(
340 store_members: Vec<(String, MemberRole)>,
341 current_control: &'a PreparedCircleControl,
342 current_roster: &CircleMaterializedRoster,
343 current_metadata: &CircleMetadata,
344 keyring: &str,
345 signer: &dyn coven_keys::keys::IdentityKeyAuthority,
346) -> Result<CircleSuccessorContext<'a>, CircleTransitionError> {
347 let epoch = current_control.value.access_epoch();
348 circle_authored_successor_context(
349 store_members,
350 current_control,
351 current_roster,
352 current_metadata,
353 keyring,
354 signer,
355 epoch,
356 )
357}
358
359pub(super) fn circle_authored_successor_context<'a>(
360 mut store_members: Vec<(String, MemberRole)>,
361 current_control: &PreparedCircleControl,
362 current_roster: &CircleMaterializedRoster,
363 current_metadata: &CircleMetadata,
364 keyring: &str,
365 signer: &dyn coven_keys::keys::IdentityKeyAuthority,
366 epoch: &'a MergeActiveCircleEpoch,
367) -> Result<CircleSuccessorContext<'a>, CircleTransitionError> {
368 if !current_control.verify()
369 || !current_roster.verify()
370 || !current_metadata.verify()
371 || current_control.value.circle_id != current_metadata.circle_id
372 || current_control.value.epoch_id() != current_metadata.epoch_id
373 {
374 return Err(CircleTransitionError::InvalidCurrentState);
375 }
376 let author_pubkey = keys::public_key_hex(signer);
377 store_members.sort_by(|left, right| left.0.cmp(&right.0));
378 store_members.dedup_by(|left, right| left.0 == right.0);
379 if !store_members
380 .iter()
381 .any(|(pubkey, role)| pubkey == &author_pubkey && role.can_write())
382 {
383 return Err(CircleTransitionError::AuthorNotStoreWriter);
384 }
385 if current_roster.members().get(&author_pubkey) != Some(&crate::circle::CircleRole::Owner) {
386 return Err(CircleTransitionError::AuthorNotCircleOwner);
387 }
388 let key_fingerprint = EncryptionService::from(
389 MasterKeyring::from_serialized(keyring)
390 .map_err(|_| CircleTransitionError::InvalidCurrentState)?,
391 )
392 .seal_key_fingerprint();
393 if key_fingerprint != current_control.value.key_fingerprint()
394 || current_metadata.key_fingerprint != key_fingerprint
395 {
396 return Err(CircleTransitionError::InvalidCurrentState);
397 }
398 let (grant_id, record) = current_roster
399 .active_grants()
400 .find(|(_, record)| {
401 record.member_pubkey == author_pubkey && record.role == crate::circle::CircleRole::Owner
402 })
403 .ok_or(CircleTransitionError::AuthorNotCircleOwner)?;
404 let author_authority = match &record.creation_authority {
405 CircleGrantCreationAuthority::Entry(created_at) => MergeCircleOwnerAuthorityRef::Roster {
406 roster: epoch.roster.clone(),
407 grant_id: grant_id.clone(),
408 created_at: created_at.clone(),
409 },
410 CircleGrantCreationAuthority::ConflictResolution(resolution) => {
411 MergeCircleOwnerAuthorityRef::ConflictResolution {
412 conflict_hash: resolution.conflict_hash,
413 resolution_hash: resolution.resolution_hash,
414 }
415 }
416 };
417 Ok(CircleSuccessorContext {
418 store_members,
419 author_pubkey,
420 epoch,
421 grant_id: grant_id.clone(),
422 author_authority,
423 key_fingerprint,
424 })
425}