Skip to main content

coven_protocol/circle_control/
drafts.rs

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
308/// The successor context for a command that publishes a new active epoch: the
309/// current control must be `ActiveEpoch`, so a closing or deleted control is
310/// refused.
311pub(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
334/// The successor context for a terminal deletion, which supersedes an in-flight
335/// close. It authors over the control's access epoch — the active epoch itself,
336/// or a close's frozen epoch — so a `Closing` control resolves to the frozen
337/// spine the deletion freezes, rather than being refused for lacking an active
338/// epoch.
339pub(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}