Skip to main content

coven_database/store/store_session/
membership_request_completion.rs

1use super::membership_mutations::{
2    consume_retired_membership_candidate_on, membership_rotation_generation,
3    require_membership_mutation_on, require_retired_membership_candidate_on,
4};
5use super::*;
6use crate::*;
7use coven_protocol::membership::{MembershipChain, StoreAuthorityChange};
8use coven_protocol::store_commit::ObjectHash;
9
10enum MembershipRequestCompletion {
11    Satisfied,
12    RejectedAdmission,
13    AuthorityRetired,
14}
15
16impl MembershipRequestCompletion {
17    fn require(
18        &self,
19        retired: &RetiredStoreCandidate,
20        membership: &MembershipChain,
21    ) -> Result<(), DbError> {
22        let RetiredStoreCandidateInputs::Membership(original) = &retired.inputs else {
23            return Err(DbError::Message(
24                "request completion has another candidate domain".into(),
25            ));
26        };
27        let (resolved, failure) = match (self, &original.entry.change) {
28            (Self::AuthorityRetired, _) => {
29                let commit: coven_protocol::store_commit::StoreBatchCommit =
30                    serde_json::from_slice(
31                        &retired.nonactivation.candidate().canonical_signed_bytes,
32                    )
33                    .map_err(|error| DbError::context("retired membership candidate", error))?;
34                let creation = commit.membership_authority.as_ref().ok_or_else(|| {
35                    DbError::Message(
36                        "retired membership candidate has no initiating authority".into(),
37                    )
38                })?;
39                (
40                    membership
41                        .write_authority_retirement(creation, &original.entry.author_pubkey)
42                        .is_some(),
43                    "membership request initiating authority remains active",
44                )
45            }
46            (Self::Satisfied, StoreAuthorityChange::RemoveMember { user_pubkey, .. }) => (
47                !membership.is_member_now(user_pubkey),
48                "retained membership removal is not satisfied",
49            ),
50            (
51                Self::Satisfied | Self::RejectedAdmission,
52                StoreAuthorityChange::SetMember {
53                    user_pubkey,
54                    provider_account_email,
55                    role,
56                    ..
57                },
58            ) => {
59                let grants = membership.active_grant_ids(user_pubkey);
60                let matching = grants.iter().try_fold(true, |matching, grant| {
61                    let record = membership.active_grant(grant).ok_or_else(|| {
62                        DbError::Message("accepted membership grant is unresolved".into())
63                    })?;
64                    Ok::<_, DbError>(
65                        matching
66                            && &record.role == role
67                            && &record.provider_account_email == provider_account_email,
68                    )
69                })?;
70                match self {
71                    Self::Satisfied => (
72                        grants.len() == 1 && matching,
73                        "retained membership admission is not satisfied",
74                    ),
75                    Self::RejectedAdmission => (
76                        grants.len() == 1 && !matching,
77                        "retained membership admission is not rejected by an accepted grant",
78                    ),
79                    Self::AuthorityRetired => unreachable!("authority retirement is handled above"),
80                }
81            }
82            _ => {
83                return Err(DbError::Message(
84                    "membership completion does not match its retained request".into(),
85                ))
86            }
87        };
88        if !resolved {
89            return Err(DbError::Message(failure.into()));
90        }
91        Ok(())
92    }
93}
94
95impl StoreSession<'_> {
96    fn retire_membership_candidate_authority(
97        &mut self,
98        intent_hash: ObjectHash,
99        expected: ActiveStorePublication,
100        candidate: coven_protocol::prepared_commit::PreparedStoreOperationCommit,
101        membership: MembershipChain,
102        publication: coven_protocol::store_commit::StorePublicationRef,
103    ) -> Result<ActiveStorePublication, DbError> {
104        candidate.validate_closed_shape()?;
105        if expected.owner() != &ActiveStorePublicationOwner::MembershipMutation
106            || expected.commit_reservation()
107                != Some((
108                    &candidate.commit.write_id,
109                    &candidate.commit.author_registration,
110                    &candidate.reference.coord,
111                ))
112            || expected.attempt()?.entry.payload
113                != coven_protocol::store_commit::StorePublicationPayload::Commit(
114                    candidate.reference.clone(),
115                )
116            || !expected.retired_candidates().is_empty()
117        {
118            return Err(DbError::Message(
119                "membership retirement differs from its reserved candidate".into(),
120            ));
121        }
122        let original = candidate.prepared_membership_publication()?;
123        let tx = self.conn.unchecked_transaction()?;
124        require_membership_mutation_on(&tx, intent_hash)?;
125        crate::remote_object_records::validate_remote_object_on(
126            &tx,
127            coven_protocol::remote_object::remote_object_id(&candidate.reference.object),
128            &candidate.reference.object,
129            &candidate.commit.to_bytes(),
130        )?;
131        let nonactivation = StoreTransaction::new(&tx, self.store_dir)
132            .candidate_grant_nonactivation(
133                self.verified_store_authority,
134                &membership,
135                &candidate.reference,
136                &candidate.commit,
137                &publication,
138            )?;
139        let mut publications = vec![expected.attempt()?.reference()?];
140        if let Some(previous) = expected.superseded_entry() {
141            publications.push(previous.clone());
142        }
143        publications.sort();
144        publications.dedup();
145        let retired = RetiredStoreCandidate {
146            nonactivation,
147            inputs: RetiredStoreCandidateInputs::Membership(original),
148            publications,
149        };
150        super::candidate_records::begin_candidate_nonactivation_targets_on(
151            &tx,
152            &candidate.reference,
153            &retired.objects()?,
154            &retired.nonactivation,
155        )?;
156        let replacement = expected.await_preparation(retired)?;
157        super::active_store_publication::update_active_store_publication_on(
158            &tx,
159            &expected,
160            &replacement,
161        )?;
162        tx.commit()?;
163        Ok(replacement)
164    }
165
166    fn complete_retired_membership_request(
167        &mut self,
168        completion: MembershipRequestCompletion,
169        intent_hash: ObjectHash,
170        expected: ActiveStorePublication,
171        accepted: coven_protocol::store_commit::StoreCurrentPublicationRecord,
172        membership: coven_protocol::membership::MembershipChain,
173    ) -> Result<(), DbError> {
174        if expected.owner() != &ActiveStorePublicationOwner::MembershipMutation
175            || !expected.is_awaiting_preparation()
176        {
177            return Err(DbError::Message(
178                "retained membership request still owns a prepared candidate".into(),
179            ));
180        }
181        let tx = self.conn.unchecked_transaction()?;
182        require_membership_mutation_on(&tx, intent_hash)?;
183        if super::observed_store_publication::load_store_current_publication_on(&tx)?.record()
184            != &accepted
185        {
186            return Err(DbError::Message(
187                "accepted membership changed before completing the retained request".into(),
188            ));
189        }
190        let retired = require_retired_membership_candidate_on(&tx, &expected)?;
191        let RetiredStoreCandidateInputs::Membership(original) = &retired.inputs else {
192            unreachable!("retired membership candidate is validated")
193        };
194        let rotation_generation = membership_rotation_generation(&original.entry)?;
195        StoreTransaction::new(&tx, self.store_dir).require_accepted_membership(
196            self.verified_store_authority,
197            &membership,
198            accepted.accepted().ok_or_else(|| {
199                DbError::Message("retained membership request has no accepted publication".into())
200            })?,
201        )?;
202        completion.require(retired, &membership)?;
203        let completed = consume_retired_membership_candidate_on(&tx, &expected)?;
204        super::active_store_publication::update_active_store_publication_on(
205            &tx, &expected, &completed,
206        )?;
207        super::active_store_publication::clear_active_store_publication_on(&tx, &completed)?;
208        if let Some(generation) = rotation_generation {
209            super::membership_rotation::remove_rotation_candidate_on(&tx, intent_hash, generation)?;
210        }
211        if tx.execute(
212            "DELETE FROM outbound_membership_mutation WHERE singleton = 1 AND intent_hash = ?1",
213            [intent_hash.to_string()],
214        )? != 1
215        {
216            return Err(DbError::Message(
217                "membership mutation changed before completing the retained request".into(),
218            ));
219        }
220        tx.commit()?;
221        Ok(())
222    }
223}
224
225impl StoreDatabase {
226    pub async fn retire_membership_candidate_authority(
227        &self,
228        intent_hash: ObjectHash,
229        expected: ActiveStorePublication,
230        candidate: coven_protocol::prepared_commit::PreparedStoreOperationCommit,
231        membership: MembershipChain,
232        publication: coven_protocol::store_commit::StorePublicationRef,
233    ) -> Result<ActiveStorePublication, DbError> {
234        self.call_store(move |session| {
235            session.retire_membership_candidate_authority(
236                intent_hash,
237                expected,
238                candidate,
239                membership,
240                publication,
241            )
242        })
243        .await
244    }
245
246    pub async fn complete_retired_membership_authority(
247        &self,
248        intent_hash: ObjectHash,
249        expected: ActiveStorePublication,
250        accepted: coven_protocol::store_commit::StoreCurrentPublicationRecord,
251        membership: MembershipChain,
252    ) -> Result<(), DbError> {
253        self.call_store(move |session| {
254            session.complete_retired_membership_request(
255                MembershipRequestCompletion::AuthorityRetired,
256                intent_hash,
257                expected,
258                accepted,
259                membership,
260            )
261        })
262        .await
263    }
264
265    pub async fn complete_satisfied_membership_mutation(
266        &self,
267        intent_hash: ObjectHash,
268        expected: ActiveStorePublication,
269        accepted: coven_protocol::store_commit::StoreCurrentPublicationRecord,
270        membership: coven_protocol::membership::MembershipChain,
271    ) -> Result<(), DbError> {
272        self.call_store(move |session| {
273            session.complete_retired_membership_request(
274                MembershipRequestCompletion::Satisfied,
275                intent_hash,
276                expected,
277                accepted,
278                membership,
279            )
280        })
281        .await
282    }
283
284    pub async fn complete_rejected_membership_admission(
285        &self,
286        intent_hash: ObjectHash,
287        expected: ActiveStorePublication,
288        accepted: coven_protocol::store_commit::StoreCurrentPublicationRecord,
289        membership: coven_protocol::membership::MembershipChain,
290    ) -> Result<(), DbError> {
291        self.call_store(move |session| {
292            session.complete_retired_membership_request(
293                MembershipRequestCompletion::RejectedAdmission,
294                intent_hash,
295                expected,
296                accepted,
297                membership,
298            )
299        })
300        .await
301    }
302}