1use super::*;
2
3#[derive(Clone, PartialEq, Eq)]
4pub struct MembershipConflictChoice {
5 pub id: String,
6 pub members: Vec<MemberInfo>,
7 conflict_hash: ObjectHash,
8 selection: MembershipConflictSelection,
9}
10
11impl std::fmt::Debug for MembershipConflictChoice {
12 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13 formatter
14 .debug_struct("MembershipConflictChoice")
15 .field("id", &self.id)
16 .field("members", &self.members)
17 .finish()
18 }
19}
20
21impl MembershipConflictChoice {
22 pub fn new(
23 id: String,
24 members: Vec<MemberInfo>,
25 conflict_hash: ObjectHash,
26 selection: MembershipConflictSelection,
27 ) -> Self {
28 Self {
29 id,
30 members,
31 conflict_hash,
32 selection,
33 }
34 }
35
36 pub fn conflict_hash(&self) -> ObjectHash {
37 self.conflict_hash
38 }
39
40 pub fn selection(&self) -> &MembershipConflictSelection {
41 &self.selection
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub enum MembershipConflictInfo {
47 ConcurrentMemberAssignments {
48 id: String,
49 member_pubkey: String,
50 choices: Vec<MembershipConflictChoice>,
51 },
52 RevocationCycle {
53 id: String,
54 choices: Vec<MembershipConflictChoice>,
55 },
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(rename_all = "snake_case", deny_unknown_fields)]
60pub enum MembershipConflict {
61 ConcurrentMemberAssignments {
62 conflict_hash: ObjectHash,
63 heads: Vec<MembershipHeadRef>,
64 effective_frontier: Vec<MembershipCoord>,
65 member_pubkey: String,
66 conflicting_grants: BTreeMap<MembershipGrantId, MembershipGrantRecord>,
67 uncontested_grants: BTreeMap<MembershipGrantId, MembershipGrantRecord>,
68 grants: BTreeMap<
69 MembershipGrantId,
70 GrantState<MembershipGrantRecord, MembershipGrantRetirement>,
71 >,
72 provider_admin: crate::provider::ProviderAdminResolution,
73 },
74 RevocationCycle {
75 conflict_hash: ObjectHash,
76 heads: Vec<MembershipHeadRef>,
77 cyclic_sources: Vec<MembershipCoord>,
78 involved_owner_grants: BTreeSet<MembershipGrantId>,
79 maximal_valid_branches: Vec<StoreMembershipBranch>,
80 },
81}
82
83#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
84#[serde(rename_all = "snake_case", deny_unknown_fields)]
85pub enum MembershipStatus {
86 Resolved(ResolvedStoreMembership),
87 Conflict(MembershipConflict),
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
91#[serde(deny_unknown_fields)]
92pub struct StoreMembershipConflictResolutionRef {
93 pub conflict_hash: ObjectHash,
94 pub resolver_pubkey: String,
95 pub resolution_hash: ObjectHash,
96 pub object: ExactObjectRef,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100#[serde(deny_unknown_fields)]
101pub enum MembershipConflictSelection {
102 MemberAssignment { grant: MembershipGrantId },
103 RevocationBranch { heads: Vec<MembershipHeadRef> },
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(deny_unknown_fields)]
110pub struct StoreMembershipConflictResolutionBody {
111 pub store_root_hash: ObjectHash,
112 pub conflict_hash: ObjectHash,
113 pub conflicting_heads: Vec<MembershipHeadRef>,
114 pub retired_owner_grants: BTreeSet<MembershipGrantId>,
115 pub retirement_barriers: BTreeMap<MembershipGrantId, MergeMembershipGrantRetirementBarrier>,
116 pub resolver_pubkey: String,
117 pub selection: MembershipConflictSelection,
118 pub replacement_grant: MembershipGrantId,
119 pub replacement_membership: GrantStreamAnchor,
120 pub replacement_acceptance: OwnerConflictResolutionAcceptance,
121}
122
123impl SignedBody for StoreMembershipConflictResolutionBody {
124 const DOMAIN: &'static [u8] = MEMBERSHIP_RESOLUTION_DOMAIN;
125}
126
127pub type StoreMembershipConflictResolution = Signed<StoreMembershipConflictResolutionBody>;
128
129impl StoreMembershipConflictResolution {
130 pub fn resolution_hash(&self) -> ObjectHash {
131 self.hash()
132 }
133
134 pub fn resolution_ref(&self, object: ExactObjectRef) -> StoreMembershipConflictResolutionRef {
135 StoreMembershipConflictResolutionRef {
136 conflict_hash: self.conflict_hash,
137 resolver_pubkey: self.resolver_pubkey.clone(),
138 resolution_hash: self.resolution_hash(),
139 object,
140 }
141 }
142
143 pub fn verify_signature(&self) -> bool {
144 self.replacement_grant
145 == derive_store_resolution_grant(&self.conflict_hash, &self.resolver_pubkey)
146 && self.replacement_acceptance.store_root_hash == self.store_root_hash
147 && self.replacement_acceptance.owner_grant == self.replacement_grant
148 && self.replacement_acceptance.membership == self.replacement_membership
149 && self.verify_by(&self.resolver_pubkey).is_ok()
150 }
151
152 pub(crate) fn verify_against(
153 &self,
154 store_root_hash: ObjectHash,
155 conflict: &MembershipConflict,
156 ) -> bool {
157 let (conflict_hash, heads, expected_retired, known_grants, resolver_is_owner) =
158 match (conflict, &self.selection) {
159 (
160 MembershipConflict::ConcurrentMemberAssignments {
161 conflict_hash,
162 heads,
163 conflicting_grants,
164 uncontested_grants,
165 grants,
166 ..
167 },
168 MembershipConflictSelection::MemberAssignment { grant },
169 ) => (
170 conflict_hash,
171 heads,
172 uncontested_grants
173 .iter()
174 .filter_map(|(grant, record)| {
175 (record.member_pubkey == self.resolver_pubkey && record.role.is_owner())
176 .then_some(grant.clone())
177 })
178 .collect(),
179 grants.keys().cloned().collect::<BTreeSet<_>>(),
180 conflicting_grants.contains_key(grant)
181 && uncontested_grants.values().any(|record| {
182 record.member_pubkey == self.resolver_pubkey && record.role.is_owner()
183 }),
184 ),
185 (
186 MembershipConflict::RevocationCycle {
187 conflict_hash,
188 heads,
189 involved_owner_grants,
190 maximal_valid_branches,
191 ..
192 },
193 MembershipConflictSelection::RevocationBranch {
194 heads: selected_heads,
195 },
196 ) => {
197 let Some(branch) = maximal_valid_branches
198 .iter()
199 .find(|branch| branch.heads == *selected_heads)
200 else {
201 return false;
202 };
203 let mut retired = involved_owner_grants.clone();
204 retired.extend(branch.active_grants().filter_map(|(grant, record)| {
205 (record.member_pubkey == self.resolver_pubkey && record.role.is_owner())
206 .then_some(grant.clone())
207 }));
208 (
209 conflict_hash,
210 heads,
211 retired,
212 maximal_valid_branches
213 .iter()
214 .flat_map(|branch| branch.grants.keys().cloned())
215 .collect(),
216 branch.active_grants().any(|(_, record)| {
217 record.member_pubkey == self.resolver_pubkey && record.role.is_owner()
218 }),
219 )
220 }
221 _ => return false,
222 };
223 self.store_root_hash == store_root_hash
224 && self.conflict_hash == *conflict_hash
225 && self.conflicting_heads == *heads
226 && self.retired_owner_grants == expected_retired
227 && self.retirement_barriers.len() == known_grants.len()
228 && self
229 .retirement_barriers
230 .keys()
231 .all(|grant| known_grants.contains(grant))
232 && self.replacement_grant
233 == derive_store_resolution_grant(conflict_hash, &self.resolver_pubkey)
234 && resolver_is_owner
235 && self.verify_signature()
236 }
237}
238
239pub fn derive_store_resolution_grant(
240 conflict_hash: &ObjectHash,
241 resolver_pubkey: &str,
242) -> MembershipGrantId {
243 MembershipGrantId(ObjectHash::digest(
244 format!("coven.store-membership-resolution-grant.v1\0{conflict_hash}\0{resolver_pubkey}")
245 .as_bytes(),
246 ))
247}
248
249pub(super) fn conflict_retirement_barriers(
250 records: BTreeMap<MembershipGrantId, MembershipGrantRecord>,
251 effective_frontier: Vec<MembershipCoord>,
252 device_state: &StoreDeviceStateRef,
253) -> Result<BTreeMap<MembershipGrantId, MergeMembershipGrantRetirementBarrier>, MembershipError> {
254 let recovery = device_state.recovery();
255 records
256 .into_iter()
257 .map(|(grant, record)| {
258 let mut observed_streams = effective_frontier
259 .iter()
260 .filter(|coord| coord.author_owner_grant == grant)
261 .cloned()
262 .collect::<Vec<_>>();
263 observed_streams.sort_by_key(MembershipCoord::stream_key);
264 observed_streams.dedup_by_key(|coord| coord.stream_key());
265 let author_streams = StoreGrantStreamBarrier { observed_streams };
266 let barrier = if record.role.is_owner() {
267 let cursor = recovery
268 .iter()
269 .find(|cursor| cursor.owner_grant == grant)
270 .cloned()
271 .ok_or(MembershipError::MissingOwnerRecoveryState)?;
272 MergeMembershipGrantRetirementBarrier::Owner {
273 barrier: MergeStoreOwnerGrantBarrier {
274 author_streams,
275 recovery: cursor,
276 },
277 }
278 } else {
279 MergeMembershipGrantRetirementBarrier::NonOwner { author_streams }
280 };
281 Ok((grant, barrier))
282 })
283 .collect()
284}
285
286pub fn resolve_store_membership_conflict(
287 store_root_hash: ObjectHash,
288 conflict: &MembershipConflict,
289 resolutions: &[(
290 StoreMembershipConflictResolutionRef,
291 StoreMembershipConflictResolution,
292 )],
293) -> Result<ResolvedStoreMembership, MembershipError> {
294 if resolutions.is_empty() {
295 return Err(MembershipError::InvalidConflictResolution);
296 }
297 let mut by_resolver = BTreeMap::new();
298 let mut retired_owner_grants = BTreeSet::new();
299 for (_, resolution) in resolutions {
300 if !resolution.verify_against(store_root_hash, conflict) {
301 return Err(MembershipError::InvalidConflictResolution);
302 }
303 if let Some(existing) = by_resolver.insert(
304 resolution.resolver_pubkey.clone(),
305 resolution.resolution_hash(),
306 ) {
307 if existing != resolution.resolution_hash() {
308 return Err(MembershipError::InvalidConflictResolution);
309 }
310 continue;
311 }
312 retired_owner_grants.extend(resolution.retired_owner_grants.iter().cloned());
313 }
314 let (mut grants, known_records, provider_admin) = match conflict {
315 MembershipConflict::ConcurrentMemberAssignments {
316 conflicting_grants,
317 grants,
318 provider_admin,
319 ..
320 } => {
321 let selected = resolutions
322 .iter()
323 .filter_map(|(_, resolution)| match &resolution.selection {
324 MembershipConflictSelection::MemberAssignment { grant } => Some(grant.clone()),
325 MembershipConflictSelection::RevocationBranch { .. } => None,
326 })
327 .collect::<BTreeSet<_>>();
328 let retained = (selected.len() == 1)
329 .then(|| selected.first().cloned())
330 .flatten();
331 let mut resolved = grants.clone();
332 for (grant, record) in conflicting_grants {
333 if retained.as_ref() == Some(grant) {
334 continue;
335 }
336 let retirements = assignment_conflict_retirements(resolutions, grant)?;
337 resolved.insert(
338 grant.clone(),
339 GrantState::Tombstoned {
340 record: record.clone(),
341 retirements,
342 },
343 );
344 }
345 (
346 resolved,
347 grants
348 .iter()
349 .map(|(grant, state)| (grant.clone(), state.record().clone()))
350 .collect::<BTreeMap<_, _>>(),
351 provider_admin.clone(),
352 )
353 }
354 MembershipConflict::RevocationCycle {
355 maximal_valid_branches,
356 ..
357 } => {
358 let mut selected_branches = Vec::new();
359 for (_, resolution) in resolutions {
360 let MembershipConflictSelection::RevocationBranch {
361 heads: selected_heads,
362 } = &resolution.selection
363 else {
364 return Err(MembershipError::InvalidConflictResolution);
365 };
366 let branch = maximal_valid_branches
367 .iter()
368 .find(|branch| branch.heads == *selected_heads)
369 .ok_or(MembershipError::InvalidConflictResolution)?;
370 if !selected_branches
371 .iter()
372 .any(|selected: &&StoreMembershipBranch| selected.heads == branch.heads)
373 {
374 selected_branches.push(branch);
375 }
376 }
377 let resolved = causal_grants::resolve_conflict_grants(
378 maximal_valid_branches.iter().map(|branch| &branch.grants),
379 selected_branches
380 .iter()
381 .copied()
382 .map(|branch| &branch.grants),
383 &retired_owner_grants,
384 |grant| conflict_resolution_retirements(resolutions, grant),
385 || MembershipError::InvalidConflictResolution,
386 )?;
387 let known_records = maximal_valid_branches
388 .iter()
389 .flat_map(|branch| branch.grants.iter())
390 .map(|(grant, state)| (grant.clone(), state.record().clone()))
391 .collect::<BTreeMap<_, _>>();
392 let provider_admin = crate::provider::ProviderAdminResolution::Resolved(
393 crate::provider::ProviderAdminState::merge(
394 selected_branches
395 .iter()
396 .map(|branch| branch.provider_admin.combined_state().clone()),
397 )?,
398 );
399 (resolved, known_records, provider_admin)
400 }
401 };
402 for (reference, resolution) in resolutions {
403 for retired in &resolution.retired_owner_grants {
404 let record = known_records
405 .get(retired)
406 .ok_or(MembershipError::InvalidConflictResolution)?
407 .clone();
408 let barrier = resolution
409 .retirement_barriers
410 .get(retired)
411 .cloned()
412 .ok_or(MembershipError::InvalidConflictResolution)?;
413 let retirements =
414 GrantRetirements::new(MembershipGrantRetirement::ConflictResolution {
415 authority: reference.clone(),
416 barrier,
417 });
418 causal_grants::tombstone_conflict_grant(&mut grants, retired, &record, &retirements)
419 .map_err(|()| MembershipError::InvalidConflictResolution)?;
420 }
421 }
422 for (reference, resolution) in resolutions {
423 let record = MembershipGrantRecord {
424 member_pubkey: resolution.resolver_pubkey.clone(),
425 role: StoreMembershipRoleGrant::Owner {
426 recovery: OwnerRecoveryAnchorRef::ConflictResolution {
427 acceptance: Box::new(resolution.replacement_acceptance.clone()),
428 },
429 },
430 provider_account_email: None,
431 creation_authority: MembershipGrantCreationAuthority::ConflictResolution(
432 reference.clone(),
433 ),
434 };
435 if grants
436 .insert(
437 resolution.replacement_grant.clone(),
438 GrantState::Active {
439 record: record.clone(),
440 },
441 )
442 .is_some_and(|current| current.active() != Some(&record))
443 {
444 return Err(MembershipError::InvalidConflictResolution);
445 }
446 }
447 if !causal_grants::has_active_owner(&grants, |record| record.role.is_owner())
448 || causal_grants::has_concurrent_assignments(&grants, |record| &record.member_pubkey)
449 {
450 return Err(MembershipError::InvalidConflictResolution);
451 }
452 Ok(ResolvedStoreMembership {
453 state_hash: store_membership_state_hash(&grants, &provider_admin),
454 grants,
455 provider_admin,
456 })
457}
458
459pub(super) fn conflict_resolution_retirements<'resolution>(
460 resolutions: impl IntoIterator<
461 Item = &'resolution (
462 StoreMembershipConflictResolutionRef,
463 StoreMembershipConflictResolution,
464 ),
465 >,
466 grant: &MembershipGrantId,
467) -> Result<GrantRetirements<MembershipGrantRetirement>, MembershipError> {
468 let mut retirements = resolutions.into_iter().map(|(reference, resolution)| {
469 resolution
470 .retirement_barriers
471 .get(grant)
472 .cloned()
473 .map(|barrier| MembershipGrantRetirement::ConflictResolution {
474 authority: reference.clone(),
475 barrier,
476 })
477 .ok_or(MembershipError::InvalidConflictResolution)
478 });
479 let first = retirements
480 .next()
481 .ok_or(MembershipError::InvalidConflictResolution)??;
482 let mut result = GrantRetirements::new(first);
483 for retirement in retirements {
484 result.insert(retirement?);
485 }
486 Ok(result)
487}
488
489pub(super) fn assignment_conflict_retirements(
492 resolutions: &[(
493 StoreMembershipConflictResolutionRef,
494 StoreMembershipConflictResolution,
495 )],
496 grant: &MembershipGrantId,
497) -> Result<GrantRetirements<MembershipGrantRetirement>, MembershipError> {
498 conflict_resolution_retirements(
499 resolutions.iter().filter(|(_, resolution)| {
500 !matches!(
501 &resolution.selection,
502 MembershipConflictSelection::MemberAssignment { grant: selected }
503 if selected == grant
504 )
505 }),
506 grant,
507 )
508}