1use crate::membership::{
6 self, AuthorHead, MembershipEntry, MembershipEntryRef, MembershipHeadRef,
7 MergeMembershipHeadTransition,
8};
9use crate::objects::{ExactObjectRef, PreparedExactObject};
10use crate::store_commit::ObjectHash;
11
12pub fn prepare_exact_object(
21 object: &ExactObjectRef,
22 value: &impl serde::Serialize,
23) -> Result<PreparedExactObject, MembershipPreparationError> {
24 let bytes = serde_json::to_vec(value).map_err(MembershipPreparationError::Json)?;
25 PreparedExactObject::new(object.clone(), bytes).map_err(MembershipPreparationError::ExactObject)
26}
27
28fn binds_exact_object(object: &ExactObjectRef, value: &impl serde::Serialize) -> bool {
29 prepare_exact_object(object, value).is_ok()
30}
31
32#[derive(Debug, thiserror::Error)]
35pub enum MembershipPreparationError {
36 #[error("invalid prepared membership mutation: {0}")]
37 Invariant(String),
38 #[error("serialize prepared membership mutation: {0}")]
39 Json(#[source] serde_json::Error),
40 #[error("prepared membership exact object: {0}")]
41 ExactObject(#[source] crate::objects::StorageError),
42}
43
44#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
52#[serde(deny_unknown_fields)]
53pub struct PreparedMembershipPublication {
54 pub entry: MembershipEntry,
55 pub entry_ref: MembershipEntryRef,
56 pub head: AuthorHead,
57 pub head_ref: MembershipHeadRef,
58}
59
60impl PreparedMembershipPublication {
61 pub fn validate(&self) -> Result<(), MembershipPreparationError> {
62 self.transition().validate()?;
63 let coord = self.entry.coord();
64 if self.entry_ref.coord != coord
65 || self.head.body.entry != self.entry_ref
66 || self.head.entry_coord() != coord
67 || self.head_ref.coord != coord
68 || self.head_ref.head_hash != self.head.head_hash()
69 || !binds_exact_object(&self.head_ref.object, &self.head)
70 {
71 return Err(MembershipPreparationError::Invariant(
72 "prepared membership publication does not bind one exact entry and head"
73 .to_string(),
74 ));
75 }
76 Ok(())
77 }
78
79 pub fn transition(&self) -> PreparedMembershipTransition {
80 PreparedMembershipTransition {
81 entry: self.entry.clone(),
82 entry_ref: self.entry_ref.clone(),
83 transition: membership::MergeMembershipHeadTransition {
84 body: self.head.body.clone(),
85 head_slot: self.head_ref.object.slot().clone(),
86 },
87 }
88 }
89
90 pub fn candidate_remote_objects(
91 &self,
92 commit: &crate::store_commit::StoreBatchCommit,
93 reference: &crate::store_commit::StoreBatchCommitRef,
94 ) -> Result<
95 Vec<crate::remote_object::ClosedRemoteObject>,
96 crate::prepared_commit::PreparedCommitError,
97 > {
98 self.validate()?;
99 reference.verify_commit(commit)?;
100 if !commit
101 .control()
102 .is_some_and(|control| control.transition.matches_head(&self.head, &self.head_ref))
103 || !matches!(&self.head.activation, membership::MembershipHeadActivation::StoreCommit { commit, .. } if commit == reference)
104 {
105 return Err(crate::prepared_commit::PreparedCommitError::Invariant(
106 "membership objects differ from their activating Store commit".into(),
107 ));
108 }
109 let entry = self.prepared_entry()?;
110 let head = self.prepared_head()?;
111 Ok(vec![
112 crate::remote_object::RemoteObjectRecord::candidate_exclusive_merge_membership_entry(
113 commit.candidate_family(),
114 self.entry_ref.clone(),
115 entry.stored_bytes(),
116 entry.stored_bytes(),
117 reference.clone(),
118 )?,
119 crate::remote_object::RemoteObjectRecord::candidate_exclusive_merge_membership_head(
120 commit.candidate_family(),
121 self.head_ref.clone(),
122 head.stored_bytes(),
123 head.stored_bytes(),
124 reference.clone(),
125 )?,
126 ])
127 }
128
129 pub fn candidate_object_refs(
130 &self,
131 commit: &crate::store_commit::StoreBatchCommit,
132 reference: &crate::store_commit::StoreBatchCommitRef,
133 ) -> Result<Vec<ExactObjectRef>, crate::prepared_commit::PreparedCommitError> {
134 let mut objects = self
135 .candidate_remote_objects(commit, reference)?
136 .into_iter()
137 .map(|remote| remote.object().clone())
138 .collect::<Vec<_>>();
139 objects.push(reference.object.clone());
140 match &self.entry.change {
141 membership::StoreAuthorityChange::SetMember { wrapped_key, .. } => {
142 objects.push(wrapped_key.object.clone());
143 }
144 membership::StoreAuthorityChange::RemoveMember { wrapped_keys, .. } => {
145 objects.extend(wrapped_keys.iter().map(|key| key.object.clone()));
146 }
147 membership::StoreAuthorityChange::Founder { .. }
148 | membership::StoreAuthorityChange::DeviceRegistrationActivation { .. }
149 | membership::StoreAuthorityChange::DeviceExclusionProposal { .. }
150 | membership::StoreAuthorityChange::DeviceExclusionOutcome { .. }
151 | membership::StoreAuthorityChange::ProviderAdmin => {}
152 }
153 objects.sort();
154 if objects.windows(2).any(|pair| pair[0] == pair[1]) {
155 return Err(crate::prepared_commit::PreparedCommitError::Invariant(
156 "membership candidate repeats an exact object".into(),
157 ));
158 }
159 Ok(objects)
160 }
161
162 pub fn prepared_entry(&self) -> Result<PreparedExactObject, MembershipPreparationError> {
164 prepare_exact_object(&self.entry_ref.object, &self.entry)
165 }
166
167 pub fn prepared_head(&self) -> Result<PreparedExactObject, MembershipPreparationError> {
169 prepare_exact_object(&self.head_ref.object, &self.head)
170 }
171}
172
173#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
174#[serde(deny_unknown_fields)]
175pub struct PreparedMembershipTransition {
176 pub entry: MembershipEntry,
177 pub entry_ref: MembershipEntryRef,
178 pub transition: MergeMembershipHeadTransition,
179}
180
181impl PreparedMembershipTransition {
182 pub fn validate(&self) -> Result<(), MembershipPreparationError> {
183 let coord = self.entry.coord();
184 let next_sequence = coord.seq.checked_add(1).ok_or_else(|| {
185 MembershipPreparationError::Invariant("membership sequence is exhausted".to_string())
186 })?;
187 let entry_key = format!(
188 "{}.json",
189 crate::store_commit::membership_entry_semantic_prefix(
190 &coord.author_pubkey,
191 &coord.author_owner_grant,
192 coord.stream_id,
193 coord.seq,
194 coord.entry_hash,
195 )
196 );
197 let head_key = format!(
198 "{}.json",
199 crate::store_commit::membership_head_slot_prefix(
200 &coord.author_pubkey,
201 &coord.author_owner_grant,
202 coord.stream_id,
203 coord.seq,
204 )
205 );
206 let successor_key = format!(
207 "{}.json",
208 crate::store_commit::membership_head_slot_prefix(
209 &coord.author_pubkey,
210 &coord.author_owner_grant,
211 coord.stream_id,
212 next_sequence,
213 )
214 );
215 if self.entry_ref.coord != self.entry.coord()
216 || !binds_exact_object(&self.entry_ref.object, &self.entry)
217 || self.entry_ref.object.slot().logical_key() != entry_key
218 || self.transition.body.entry != self.entry_ref
219 || self.transition.head_slot.logical_key() != head_key
220 || self.transition.body.successor.next_slot.logical_key() != successor_key
221 {
222 return Err(MembershipPreparationError::Invariant(
223 "prepared membership transition does not bind its exact entry".to_string(),
224 ));
225 }
226 Ok(())
227 }
228}
229
230pub enum StoreMembershipJournalCompletion {
231 MembershipCandidateAbandoned {
232 intent_hash: ObjectHash,
233 original: Box<crate::prepared_commit::PreparedStoreOperationCommit>,
234 remote_objects: Vec<crate::remote_object::RemoteObjectRecord>,
235 },
236 DeviceJoin {
237 remote_objects: Vec<crate::remote_object::RemoteObjectRecord>,
238 },
239 DeviceExclusion {
240 operation: Box<crate::device_exclusion_journal::DurableStoreDeviceExclusionOperation>,
241 remote_objects: Vec<crate::remote_object::RemoteObjectRecord>,
242 },
243 Mutation {
244 intent_hash: ObjectHash,
245 progress_bytes: Vec<u8>,
246 remote_objects: Vec<crate::remote_object::RemoteObjectRecord>,
247 },
248 OwnerPromotion {
249 transition: crate::owner_promotion_journal::OwnerPromotionJournalTransition,
250 remote_objects: Vec<crate::remote_object::RemoteObjectRecord>,
251 },
252}
253
254impl StoreMembershipJournalCompletion {
255 pub fn retain_acceptance_result(&mut self, remote: crate::remote_object::RemoteObjectRecord) {
256 let remote_objects = match self {
257 Self::MembershipCandidateAbandoned { remote_objects, .. }
258 | Self::DeviceJoin { remote_objects }
259 | Self::DeviceExclusion { remote_objects, .. }
260 | Self::Mutation { remote_objects, .. }
261 | Self::OwnerPromotion { remote_objects, .. } => remote_objects,
262 };
263 remote_objects.push(remote);
264 }
265
266 pub fn object_refs(&self) -> Vec<ExactObjectRef> {
267 let remote_objects = match self {
268 Self::MembershipCandidateAbandoned { remote_objects, .. }
269 | Self::DeviceJoin { remote_objects }
270 | Self::DeviceExclusion { remote_objects, .. }
271 | Self::Mutation { remote_objects, .. }
272 | Self::OwnerPromotion { remote_objects, .. } => remote_objects,
273 };
274 remote_objects
275 .iter()
276 .map(|remote| remote.object().clone())
277 .collect()
278 }
279
280 pub fn remote_object(
281 &self,
282 object: &ExactObjectRef,
283 ) -> Result<crate::remote_object::RemoteObjectRecord, MembershipPreparationError> {
284 let remote_objects = match self {
285 Self::MembershipCandidateAbandoned { remote_objects, .. }
286 | Self::DeviceJoin { remote_objects }
287 | Self::DeviceExclusion { remote_objects, .. }
288 | Self::Mutation { remote_objects, .. }
289 | Self::OwnerPromotion { remote_objects, .. } => remote_objects,
290 };
291 remote_objects
292 .iter()
293 .find(|remote| remote.object() == object)
294 .cloned()
295 .ok_or_else(|| {
296 MembershipPreparationError::Invariant(
297 "membership completion omits an exact activated object".to_string(),
298 )
299 })
300 }
301}