Skip to main content

coven_protocol/membership/
chain.rs

1use super::entry::store_membership_anchor_stream;
2use super::*;
3
4impl MembershipChain {
5    pub fn from_entries_with_coords_and_heads_and_provider_admin(
6        entries: Vec<(MembershipCoord, MembershipEntry)>,
7        heads: Vec<(MembershipHeadRef, AuthorHead)>,
8        provider_admin: crate::provider::ProviderAdminState,
9    ) -> Result<Self, MembershipError> {
10        let expected_store = entries
11            .first()
12            .map(|(_, entry)| entry.store_id.as_str())
13            .ok_or(MembershipError::EmptyChain)?;
14        if heads.iter().any(|(reference, head)| {
15            reference.head_hash != head.head_hash()
16                || head.store_id != expected_store
17                || entries
18                    .iter()
19                    .find(|(coord, _)| *coord == head.entry_coord())
20                    .is_none()
21        }) {
22            return Err(MembershipError::MissingExactHeads);
23        }
24        Self::from_entries_with_coords_and_head_refs(
25            entries,
26            heads.into_iter().map(|(reference, _)| reference).collect(),
27            provider_admin,
28        )
29    }
30
31    fn from_entries_with_coords_and_head_refs(
32        entries: Vec<(MembershipCoord, MembershipEntry)>,
33        head_refs: Vec<MembershipHeadRef>,
34        provider_admin_genesis: crate::provider::ProviderAdminState,
35    ) -> Result<Self, MembershipError> {
36        if entries.is_empty() {
37            return Err(MembershipError::EmptyChain);
38        }
39        for (index, (_, entry)) in entries.iter().enumerate() {
40            Self::validate_entry_authenticity(index, entry)?;
41        }
42        let (coords, entries): (Vec<_>, Vec<_>) = entries.into_iter().unzip();
43        let (included, resolved) = Self::reduce(&entries, &coords, &provider_admin_genesis)?;
44        Ok(Self {
45            entries,
46            coords,
47            included,
48            resolved,
49            head_refs,
50            provider_admin_genesis,
51        })
52    }
53
54    pub fn entries(&self) -> &[MembershipEntry] {
55        &self.entries
56    }
57
58    pub fn resolved(&self) -> &ResolvedStoreMembership {
59        &self.resolved
60    }
61
62    pub fn head_refs(&self) -> &[MembershipHeadRef] {
63        &self.head_refs
64    }
65
66    pub fn head_ref_for_stream(
67        &self,
68        author: &str,
69        grant: &MembershipGrantId,
70        stream_id: AuthorStreamId,
71    ) -> Option<&MembershipHeadRef> {
72        self.head_refs.iter().find(|reference| {
73            reference.coord.author_pubkey == author
74                && reference.coord.author_owner_grant == *grant
75                && reference.coord.stream_id == stream_id
76        })
77    }
78
79    /// Whether this chain reaches at least as far as `heads` on every stream
80    /// they name.
81    ///
82    /// A membership stream is a hash-linked list walked from its founder anchor
83    /// forward: a walk cannot start in the middle, so two walks of the same
84    /// stream verify the same heads up to the shorter one's end. A chain that
85    /// covers a set of cursors therefore contains everything a walk anchored at
86    /// those cursors would have verified, and can stand in for it.
87    pub fn covers_heads(&self, heads: &[MembershipHeadRef]) -> bool {
88        heads.iter().all(|covered| {
89            self.head_ref_for_stream(
90                &covered.coord.author_pubkey,
91                &covered.coord.author_owner_grant,
92                covered.coord.stream_id,
93            )
94            .is_some_and(|current| {
95                current.coord.seq > covered.coord.seq
96                    || (current.coord.seq == covered.coord.seq && current == covered)
97            })
98        })
99    }
100
101    pub fn membership_anchor(&self, grant: &MembershipGrantId) -> Option<&GrantStreamAnchor> {
102        self.entries.iter().find_map(|entry| match &entry.change {
103            StoreAuthorityChange::Founder {
104                owner_grant_id,
105                membership,
106                ..
107            } if owner_grant_id == grant => Some(membership),
108            StoreAuthorityChange::SetMember {
109                grant_id,
110                membership: Some(membership),
111                ..
112            } if grant_id == grant => Some(membership),
113            _ => None,
114        })
115    }
116
117    pub fn membership_stream_id(&self, grant: &MembershipGrantId) -> Option<AuthorStreamId> {
118        let record = self.resolved().grants.get(grant)?.record();
119        let founder = self.founder_coord()?;
120        if founder.author_owner_grant == *grant {
121            // Creation owns the founder's first slot before grant-based paths
122            // exist; the verified founder entry owns its stream identity.
123            return Some(founder.stream_id);
124        }
125        store_membership_anchor_stream(&record.member_pubkey, grant, self.membership_anchor(grant)?)
126    }
127
128    pub fn activated_membership_streams(&self) -> Vec<(MembershipStreamKey, GrantStreamAnchor)> {
129        let mut streams = self
130            .resolved()
131            .grants
132            .iter()
133            .filter_map(|(grant, state)| {
134                let record = state.record();
135                let anchor = self.membership_anchor(grant)?.clone();
136                let stream_id = self.membership_stream_id(grant)?;
137                Some((
138                    MembershipStreamKey {
139                        author_pubkey: record.member_pubkey.clone(),
140                        author_owner_grant: grant.clone(),
141                        stream_id,
142                    },
143                    anchor,
144                ))
145            })
146            .collect::<BTreeMap<_, _>>();
147        let included = &self.included;
148        for (coord, entry) in self.entries_with_coords() {
149            if !included.contains(coord) {
150                continue;
151            }
152            let (owner_pubkey, grant, anchor, stream_id) = match &entry.change {
153                StoreAuthorityChange::Founder {
154                    owner_pubkey,
155                    owner_grant_id,
156                    membership,
157                    ..
158                } => (owner_pubkey, owner_grant_id, membership, entry.stream_id),
159                StoreAuthorityChange::SetMember {
160                    user_pubkey,
161                    role: StoreMembershipRoleGrant::Owner { .. },
162                    grant_id,
163                    membership: Some(membership),
164                    ..
165                } => (
166                    user_pubkey,
167                    grant_id,
168                    membership,
169                    store_membership_anchor_stream(user_pubkey, grant_id, membership)
170                        .expect("validated Owner grant has a Store membership stream anchor"),
171                ),
172                _ => continue,
173            };
174            streams.insert(
175                MembershipStreamKey {
176                    author_pubkey: owner_pubkey.clone(),
177                    author_owner_grant: grant.clone(),
178                    stream_id,
179                },
180                anchor.clone(),
181            );
182        }
183        streams.into_iter().collect()
184    }
185
186    pub fn activate_head_ref(
187        &mut self,
188        reference: MembershipHeadRef,
189    ) -> Result<(), MembershipError> {
190        if !self.coords.contains(&reference.coord) {
191            return Err(MembershipError::MissingExactHeads);
192        }
193        let stream = reference.coord.stream_key();
194        self.head_refs
195            .retain(|current| current.coord.stream_key() != stream);
196        self.head_refs.push(reference);
197        self.head_refs.sort();
198        Ok(())
199    }
200
201    pub(crate) fn entries_with_coords(
202        &self,
203    ) -> impl Iterator<Item = (&MembershipCoord, &MembershipEntry)> {
204        self.coords.iter().zip(self.entries.iter())
205    }
206
207    pub fn store_id(&self) -> Option<&str> {
208        self.entries.first().map(|entry| entry.store_id.as_str())
209    }
210
211    pub fn founder_coord(&self) -> Option<&MembershipCoord> {
212        self.entries_with_coords().find_map(|(coord, entry)| {
213            matches!(entry.change, StoreAuthorityChange::Founder { .. }).then_some(coord)
214        })
215    }
216
217    pub(crate) fn founder_entry(&self) -> Option<&MembershipEntry> {
218        self.entries
219            .iter()
220            .find(|entry| matches!(entry.change, StoreAuthorityChange::Founder { .. }))
221    }
222
223    pub fn founder_pubkey(&self) -> Option<&str> {
224        self.founder_entry().and_then(|entry| match &entry.change {
225            StoreAuthorityChange::Founder { owner_pubkey, .. } => Some(owner_pubkey.as_str()),
226            StoreAuthorityChange::SetMember { .. }
227            | StoreAuthorityChange::RemoveMember { .. }
228            | StoreAuthorityChange::DeviceRegistrationActivation { .. }
229            | StoreAuthorityChange::DeviceExclusionProposal { .. }
230            | StoreAuthorityChange::DeviceExclusionOutcome { .. }
231            | StoreAuthorityChange::ProviderAdmin => None,
232        })
233    }
234
235    pub fn is_founded_by(&self, owner_pubkey: &str) -> bool {
236        self.founder_pubkey() == Some(owner_pubkey)
237    }
238
239    pub fn add_entry(&mut self, entry: MembershipEntry) -> Result<(), MembershipError> {
240        self.add_entry_at(entry.coord(), entry)
241    }
242
243    pub fn add_entry_at(
244        &mut self,
245        coord: MembershipCoord,
246        entry: MembershipEntry,
247    ) -> Result<(), MembershipError> {
248        Self::validate_entry_authenticity(self.entries.len(), &entry)?;
249        self.entries.push(entry);
250        self.coords.push(coord);
251        match Self::reduce(&self.entries, &self.coords, &self.provider_admin_genesis) {
252            Ok((included, resolved)) => {
253                self.included = included;
254                self.resolved = resolved;
255            }
256            Err(error) => {
257                self.entries.pop();
258                self.coords.pop();
259                return Err(error);
260            }
261        }
262        Ok(())
263    }
264
265    pub fn with_exact_entry(&self, entry: &MembershipEntry) -> Result<Self, MembershipError> {
266        let coord = entry.coord();
267        if let Some((_, stored)) = self
268            .entries_with_coords()
269            .find(|(stored_coord, _)| **stored_coord == coord)
270        {
271            if stored != entry {
272                return Err(MembershipError::ExactEntryMismatch {
273                    coord: Box::new(coord),
274                });
275            }
276            return Ok(self.clone());
277        }
278        let mut chain = self.clone();
279        chain.add_entry_at(coord, entry.clone())?;
280        Ok(chain)
281    }
282
283    pub fn contains_coord(&self, expected: &MembershipCoord) -> bool {
284        self.coords.iter().any(|coord| coord == expected)
285    }
286
287    pub fn effectively_contains_coord(&self, expected: &MembershipCoord) -> bool {
288        self.included.contains(expected)
289    }
290
291    pub fn device_registration_activations(
292        &self,
293    ) -> impl Iterator<
294        Item = (
295            &MembershipCoord,
296            &crate::store_commit::ActivatedStoreDeviceRegistrationRef,
297        ),
298    > {
299        self.entries_with_coords().filter_map(|(coord, entry)| {
300            let StoreAuthorityChange::DeviceRegistrationActivation { registration } = &entry.change
301            else {
302                return None;
303            };
304            self.included
305                .contains(coord)
306                .then_some((coord, registration))
307        })
308    }
309
310    pub(crate) fn contains_member_history(&self, pubkey: &str) -> bool {
311        self.resolved()
312            .grants
313            .values()
314            .any(|state| state.record().member_pubkey == pubkey)
315    }
316
317    pub fn reusable_author_streams(
318        &self,
319        author_pubkey: &str,
320        grant: &MembershipGrantId,
321    ) -> BTreeSet<AuthorStreamId> {
322        self.effective_frontier()
323            .into_iter()
324            .filter(|coord| {
325                coord.author_pubkey == author_pubkey
326                    && coord.author_owner_grant == *grant
327                    && self.raw_stream_tip(author_pubkey, grant, coord.stream_id)
328                        == Some(coord.clone())
329            })
330            .map(|coord| coord.stream_id)
331            .collect()
332    }
333
334    /// Effective authoring frontier after causal pruning.
335    pub fn effective_frontier(&self) -> Vec<MembershipCoord> {
336        causal_grants::stream_frontier(
337            self.coords
338                .iter()
339                .filter(|coord| self.included.contains(*coord))
340                .cloned(),
341        )
342    }
343
344    pub fn causally_includes(&self, predecessor: &MembershipChain) -> bool {
345        predecessor.included.is_subset(&self.included)
346    }
347
348    pub(crate) fn stream_tip(
349        &self,
350        author_pubkey: &str,
351        grant: &MembershipGrantId,
352        stream_id: AuthorStreamId,
353    ) -> Option<MembershipCoord> {
354        self.effective_frontier().into_iter().find(|coord| {
355            coord.author_pubkey == author_pubkey
356                && coord.author_owner_grant == *grant
357                && coord.stream_id == stream_id
358        })
359    }
360
361    pub(crate) fn raw_stream_tip(
362        &self,
363        author_pubkey: &str,
364        grant: &MembershipGrantId,
365        stream_id: AuthorStreamId,
366    ) -> Option<MembershipCoord> {
367        self.coords
368            .iter()
369            .filter(|coord| {
370                coord.author_pubkey == author_pubkey
371                    && coord.author_owner_grant == *grant
372                    && coord.stream_id == stream_id
373            })
374            .max_by_key(|coord| coord.seq)
375            .cloned()
376    }
377
378    pub(crate) fn next_member_grant_id_in_stream(
379        &self,
380        signer: &UserKeypair,
381        stream_id: AuthorStreamId,
382        user_pubkey: &str,
383    ) -> Result<MembershipGrantId, MembershipError> {
384        let author = keys::public_key_hex(signer);
385        let author_grant = self
386            .active_owner_grant(&author)
387            .ok_or_else(|| MembershipError::SignerIsNotOwner(author.clone()))?;
388        let (seq, _) = self.next_stream_position(&author, &author_grant, stream_id)?;
389        Ok(derive_grant_id(
390            self.store_id().expect("validated chain has a store id"),
391            &author,
392            &author_grant,
393            stream_id,
394            seq,
395            user_pubkey,
396        ))
397    }
398
399    pub fn next_stream_position(
400        &self,
401        author: &str,
402        grant: &MembershipGrantId,
403        stream_id: AuthorStreamId,
404    ) -> Result<(u64, Option<ObjectHash>), MembershipError> {
405        let raw_tip = self.raw_stream_tip(author, grant, stream_id);
406        let effective_tip = self.stream_tip(author, grant, stream_id);
407        if raw_tip != effective_tip {
408            return Err(MembershipError::PrunedAuthorStream);
409        }
410        effective_tip.map_or(Ok((1, None)), |tip| {
411            tip.seq
412                .checked_add(1)
413                .map(|seq| (seq, Some(tip.entry_hash)))
414                .ok_or(MembershipError::SequenceExhausted)
415        })
416    }
417
418    fn validate_entry_authenticity(
419        index: usize,
420        entry: &MembershipEntry,
421    ) -> Result<(), MembershipError> {
422        if verify_membership_entry(entry) {
423            Ok(())
424        } else {
425            Err(MembershipError::InvalidSignature(index))
426        }
427    }
428
429    fn reduce(
430        entries: &[MembershipEntry],
431        coords: &[MembershipCoord],
432        provider_admin_genesis: &crate::provider::ProviderAdminState,
433    ) -> Result<(BTreeSet<MembershipCoord>, ResolvedStoreMembership), MembershipError> {
434        let expected_store = entries
435            .first()
436            .ok_or(MembershipError::EmptyChain)?
437            .store_id
438            .clone();
439        if expected_store.is_empty() {
440            return Err(MembershipError::InvalidFounder);
441        }
442
443        for (index, (coord, entry)) in coords.iter().zip(entries).enumerate() {
444            if entry.require_version().is_err() {
445                return Err(MembershipError::UnsupportedVersion(index));
446            }
447            if entry.store_id != expected_store {
448                return Err(MembershipError::StoreMismatch {
449                    index,
450                    expected: expected_store.clone(),
451                    actual: entry.store_id.clone(),
452                });
453            }
454            let actual = entry.coord();
455            if *coord != actual {
456                return Err(MembershipError::CoordinateMismatch {
457                    index,
458                    expected: Box::new(coord.clone()),
459                    actual: Box::new(actual),
460                });
461            }
462            if !entry
463                .dependencies
464                .windows(2)
465                .all(|pair| pair[0].stream_key() < pair[1].stream_key())
466            {
467                return Err(MembershipError::NonCanonicalDependencyFrontier { index });
468            }
469            let (barriers, retirement_device_state) = match &entry.change {
470                StoreAuthorityChange::SetMember {
471                    user_pubkey,
472                    role,
473                    grant_id,
474                    replaces,
475                    membership,
476                    retirement_barriers,
477                    retirement_device_state,
478                    ..
479                } => {
480                    let valid_owner_origin = match role {
481                        StoreMembershipRoleGrant::Owner {
482                            recovery: OwnerRecoveryAnchorRef::Promotion { acceptance },
483                        } => {
484                            let request = &acceptance.request;
485                            let anchors_match =
486                                Some(&acceptance.anchors.membership) == membership.as_ref();
487                            let finalization_matches = request.finalization.author_stream
488                                == entry.stream_id
489                                && request.finalization.seq == entry.seq
490                                && request.finalization.previous_hash == entry.previous_hash;
491                            request.member_pubkey == *user_pubkey
492                                && replaces.len() == 1
493                                && replaces.contains(&request.member_grant)
494                                && request.intended_owner_grant == *grant_id
495                                && request.promoter_owner_grant == entry.author_owner_grant
496                                && anchors_match
497                                && finalization_matches
498                        }
499                        StoreMembershipRoleGrant::Owner { .. } => false,
500                        StoreMembershipRoleGrant::Member | StoreMembershipRoleGrant::Follower => {
501                            membership.is_none()
502                        }
503                    };
504                    if role.is_owner()
505                        != membership.as_ref().is_some_and(|anchor| {
506                            store_membership_anchor_stream(user_pubkey, grant_id, anchor).is_some()
507                        })
508                        || !valid_owner_origin
509                    {
510                        return Err(MembershipError::InvalidOwnerMembershipAnchor(index));
511                    }
512                    (retirement_barriers, retirement_device_state)
513                }
514                StoreAuthorityChange::RemoveMember {
515                    retirement_barriers,
516                    retirement_device_state,
517                    ..
518                } => (retirement_barriers, retirement_device_state),
519                StoreAuthorityChange::ProviderAdmin => {
520                    let Some(crate::provider::ProviderAdminMembershipChange {
521                        owner_barriers, ..
522                    }) = &entry.provider_admin
523                    else {
524                        return Err(MembershipError::InvalidProviderAdminChange(index));
525                    };
526                    if owner_barriers.values().any(|barrier| {
527                        !barrier
528                            .observed_streams
529                            .windows(2)
530                            .all(|pair| pair[0].stream_key() < pair[1].stream_key())
531                    }) {
532                        return Err(MembershipError::InvalidProviderAdminChange(index));
533                    }
534                    continue;
535                }
536                StoreAuthorityChange::DeviceRegistrationActivation { .. }
537                | StoreAuthorityChange::DeviceExclusionProposal { .. }
538                | StoreAuthorityChange::DeviceExclusionOutcome { .. } => {
539                    if entry.provider_admin.is_some() {
540                        return Err(MembershipError::InvalidProviderAdminChange(index));
541                    }
542                    continue;
543                }
544                StoreAuthorityChange::Founder { .. } => continue,
545            };
546            if entry.provider_admin.is_some() {
547                return Err(MembershipError::InvalidProviderAdminChange(index));
548            }
549            let owner_recoveries = barriers
550                .values()
551                .filter_map(|barrier| match barrier {
552                    MergeMembershipGrantRetirementBarrier::Owner { barrier } => {
553                        Some(&barrier.recovery)
554                    }
555                    MergeMembershipGrantRetirementBarrier::NonOwner { .. } => None,
556                })
557                .collect::<Vec<_>>();
558            match (owner_recoveries.is_empty(), retirement_device_state) {
559                (true, None) => {}
560                (false, Some(state))
561                    if owner_recoveries
562                        .iter()
563                        .all(|cursor| state.recovery().binary_search(cursor).is_ok()) => {}
564                (true, Some(_)) => return Err(MembershipError::UnexpectedOwnerRecoveryState),
565                (false, None | Some(_)) => return Err(MembershipError::MissingOwnerRecoveryState),
566            }
567            if let Some((grant, _)) = barriers.iter().find(|(_, barrier)| {
568                !barrier
569                    .author_streams()
570                    .observed_streams
571                    .windows(2)
572                    .all(|pair| pair[0].stream_key() < pair[1].stream_key())
573            }) {
574                return Err(MembershipError::InvalidOwnerRevocationBarrier {
575                    index,
576                    grant: grant.clone(),
577                });
578            }
579        }
580
581        let founders = entries
582            .iter()
583            .filter_map(|entry| {
584                let StoreAuthorityChange::Founder {
585                    owner_pubkey,
586                    owner_grant_id,
587                    ..
588                } = &entry.change
589                else {
590                    return None;
591                };
592                Some((entry, owner_pubkey, owner_grant_id))
593            })
594            .collect::<Vec<_>>();
595        let [(founder, owner_pubkey, owner_grant_id)] = founders.as_slice() else {
596            return Err(MembershipError::InvalidFounder);
597        };
598        if founder.author_pubkey != **owner_pubkey
599            || founder.author_owner_grant != **owner_grant_id
600            || founder.stream_id != derive_founder_stream_id(&founder.store_id, owner_pubkey)
601            || founder.provider_admin.is_some()
602        {
603            return Err(MembershipError::InvalidFounder);
604        }
605
606        validate_provider_admin_controls(entries)?;
607        validate_membership_retirement_barriers(entries)?;
608        validate_membership_wrapped_keys(entries)?;
609        let reduced = reduce_store_membership(entries)?;
610        let provider_admin = crate::provider::ProviderAdminState::reduce_merge(
611            provider_admin_genesis,
612            entries,
613            &reduced.included,
614        )?;
615        let resolved = resolved_store_membership(&reduced, provider_admin, entries)?;
616        Ok((reduced.included, resolved))
617    }
618
619    #[cfg(any(test, feature = "test-utils"))]
620    pub fn from_entries(entries: Vec<MembershipEntry>) -> Result<Self, MembershipError> {
621        let provider_admin = test_provider_admin_genesis(&entries)?;
622        Self::from_entries_with_coords_and_provider_admin(
623            entries
624                .into_iter()
625                .map(|entry| (entry.coord(), entry))
626                .collect(),
627            provider_admin,
628        )
629    }
630
631    #[cfg(test)]
632    pub(crate) fn from_entries_with_coords_and_heads(
633        entries: Vec<(MembershipCoord, MembershipEntry)>,
634        heads: Vec<(MembershipHeadRef, AuthorHead)>,
635    ) -> Result<Self, MembershipError> {
636        let values = entries
637            .iter()
638            .map(|(_, entry)| entry.clone())
639            .collect::<Vec<_>>();
640        let provider_admin = test_provider_admin_genesis(&values)?;
641        Self::from_entries_with_coords_and_heads_and_provider_admin(entries, heads, provider_admin)
642    }
643
644    #[cfg(any(test, feature = "test-utils"))]
645    pub(crate) fn from_entries_with_coords_and_provider_admin(
646        entries: Vec<(MembershipCoord, MembershipEntry)>,
647        provider_admin: crate::provider::ProviderAdminState,
648    ) -> Result<Self, MembershipError> {
649        Self::from_entries_with_coords_and_head_refs(entries, Vec::new(), provider_admin)
650    }
651
652    /// Raw signed coverage: the greatest loaded coordinate in every stream,
653    /// including suffixes removed by causal pruning.
654    #[cfg(test)]
655    pub fn author_heads(&self) -> Vec<MembershipCoord> {
656        causal_grants::stream_frontier(self.coords.iter().cloned())
657    }
658}