Skip to main content

coven_protocol/membership/
conflict.rs

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/// The wire body of one membership conflict resolution. Every field here is
107/// signed.
108#[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
489/// The conflict-resolution retirements for `grant`, excluding the resolution
490/// whose member-assignment selection kept that grant.
491pub(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}