coven_database/store/store_session/
membership_request_completion.rs1use 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}