Skip to main content

coven_protocol/
membership.rs

1//! Store-bound causal membership protocol.
2//!
3//! Every causal author stream is identified by its author, the Owner grant that
4//! authorizes it, and an independently generated stream id. Entries carry the
5//! complete observed stream frontier; authorization is derived from that causal
6//! past, never from `created_at`.
7
8use std::collections::{BTreeMap, BTreeSet};
9
10use serde::{Deserialize, Serialize};
11
12use super::causal_grants::{
13    self, CausalAssignment, CausalChange, CausalCoordinate, CausalEntry, CausalGrantConflict,
14    CausalGrantError, CausalGrantStatus, GrantRetirements, GrantState, OwnerGrantBarrier,
15};
16pub use super::causal_grants::{AuthorStreamId, MembershipGrantId};
17use super::store_commit::{
18    GrantStreamAnchor, ObjectHash, OwnerConflictResolutionAcceptance, OwnerPromotionAcceptance,
19    OwnerPromotionFinalization, OwnerRecoveryCursor, Signed, SignedBody, StoreBatchCommitRef,
20    StoreCreationId, StoreDeviceRegistration, StoreDeviceRegistrationRef, StoreDeviceStateRef,
21    StoreRootRef, SuccessorLink,
22};
23#[cfg(any(test, feature = "test-utils"))]
24use super::store_commit::{
25    OwnerPromotionAcceptanceBody, OwnerPromotionAnchors, OwnerPromotionId, OwnerPromotionRequest,
26    OwnerPromotionRequestActivation, OwnerPromotionRequestBody, OwnerRecoveryNodeRef,
27    OwnerRecoveryPosition,
28};
29// Only this module's own tests build a conflict-resolution acceptance body; the
30// `test-utils` fixtures above do not.
31#[cfg(test)]
32use super::store_commit::OwnerConflictResolutionAcceptanceBody;
33use super::wrapped_store_key::WrappedStoreKeyRef;
34use crate::objects::ExactObjectRef;
35use coven_keys::keys::{self, UserKeypair};
36
37mod authoring;
38mod authority;
39mod chain;
40mod conflict;
41mod entry;
42mod reduction;
43
44pub use conflict::{
45    derive_store_resolution_grant, resolve_store_membership_conflict, MembershipConflict,
46    MembershipConflictSelection, MembershipStatus, StoreMembershipConflictResolution,
47    StoreMembershipConflictResolutionBody, StoreMembershipConflictResolutionRef,
48};
49pub use conflict::{MembershipConflictChoice, MembershipConflictInfo};
50pub use entry::{
51    derive_founder_stream_id, derive_grant_id, founder_entry_for_creation, verify_membership_entry,
52};
53#[cfg(any(test, feature = "test-utils"))]
54pub use entry::{founder_entry, test_wrapped_key_ref};
55use reduction::*;
56
57const MEMBERSHIP_ENTRY_DOMAIN: &[u8] = b"coven.store-membership-entry.v1\0";
58const MEMBERSHIP_HEAD_DOMAIN: &[u8] = b"coven.store-membership-head.v1\0";
59const MEMBERSHIP_RESOLUTION_DOMAIN: &[u8] = b"coven.store-membership-conflict-resolution.v1\0";
60
61#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
62pub enum MemberRole {
63    Owner,
64    Member,
65    Follower,
66}
67
68impl MemberRole {
69    pub fn can_write(&self) -> bool {
70        matches!(self, Self::Owner | Self::Member)
71    }
72}
73
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
75#[serde(rename_all = "snake_case", deny_unknown_fields)]
76pub enum OwnerRecoveryAnchorRef {
77    Founder {
78        creation_id: StoreCreationId,
79    },
80    Promotion {
81        acceptance: Box<OwnerPromotionAcceptance>,
82    },
83    ConflictResolution {
84        acceptance: Box<OwnerConflictResolutionAcceptance>,
85    },
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
89#[serde(rename_all = "snake_case", deny_unknown_fields)]
90pub enum StoreMembershipRoleGrant {
91    Owner { recovery: OwnerRecoveryAnchorRef },
92    Member,
93    Follower,
94}
95
96impl StoreMembershipRoleGrant {
97    pub fn role(&self) -> MemberRole {
98        match self {
99            Self::Owner { .. } => MemberRole::Owner,
100            Self::Member => MemberRole::Member,
101            Self::Follower => MemberRole::Follower,
102        }
103    }
104
105    fn from_direct_assignment(role: MemberRole) -> Result<Self, MembershipError> {
106        match role {
107            MemberRole::Owner => Err(MembershipError::OwnerPromotionRequired),
108            MemberRole::Member => Ok(Self::Member),
109            MemberRole::Follower => Ok(Self::Follower),
110        }
111    }
112
113    fn can_write(&self) -> bool {
114        matches!(self, Self::Owner { .. } | Self::Member)
115    }
116
117    fn is_owner(&self) -> bool {
118        matches!(self, Self::Owner { .. })
119    }
120}
121
122#[derive(Debug, Clone, PartialEq, Eq)]
123pub struct MemberInfo {
124    pub pubkey: String,
125    pub role: MemberRole,
126    pub is_self: bool,
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
130#[serde(deny_unknown_fields)]
131pub enum MembershipChange {
132    Founder {
133        creation_id: StoreCreationId,
134        owner_pubkey: String,
135        owner_grant_id: MembershipGrantId,
136        membership: GrantStreamAnchor,
137        provider_admin: super::provider::FounderProviderAdminGrant,
138    },
139    SetMember {
140        user_pubkey: String,
141        #[serde(skip_serializing_if = "Option::is_none")]
142        provider_account_email: Option<String>,
143        role: StoreMembershipRoleGrant,
144        grant_id: MembershipGrantId,
145        membership: Option<GrantStreamAnchor>,
146        replaces: BTreeSet<MembershipGrantId>,
147        retirement_barriers: BTreeMap<MembershipGrantId, MergeMembershipGrantRetirementBarrier>,
148        #[serde(skip_serializing_if = "Option::is_none")]
149        retirement_device_state: Option<StoreDeviceStateRef>,
150        wrapped_key: WrappedStoreKeyRef,
151    },
152    RemoveMember {
153        user_pubkey: String,
154        removes: BTreeSet<MembershipGrantId>,
155        retirement_barriers: BTreeMap<MembershipGrantId, MergeMembershipGrantRetirementBarrier>,
156        #[serde(skip_serializing_if = "Option::is_none")]
157        retirement_device_state: Option<StoreDeviceStateRef>,
158        wrapped_keys: Vec<WrappedStoreKeyRef>,
159    },
160    ProviderAdmin,
161    ResolutionActivation {
162        resolution: StoreMembershipConflictResolutionRef,
163    },
164}
165
166impl MembershipChange {
167    pub fn membership_anchor(&self) -> Option<GrantStreamAnchor> {
168        match self {
169            Self::Founder { membership, .. } => Some(membership.clone()),
170            Self::SetMember { membership, .. } => membership.clone(),
171            Self::RemoveMember { .. } | Self::ProviderAdmin | Self::ResolutionActivation { .. } => {
172                None
173            }
174        }
175    }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
179#[serde(deny_unknown_fields)]
180pub struct MembershipCoord {
181    pub author_pubkey: String,
182    pub author_owner_grant: MembershipGrantId,
183    pub stream_id: AuthorStreamId,
184    pub seq: u64,
185    pub entry_hash: ObjectHash,
186}
187
188impl MembershipCoord {
189    pub fn stream_key(&self) -> MembershipStreamKey {
190        MembershipStreamKey {
191            author_pubkey: self.author_pubkey.clone(),
192            author_owner_grant: self.author_owner_grant.clone(),
193            stream_id: self.stream_id,
194        }
195    }
196}
197
198#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
199pub struct MembershipStreamKey {
200    pub author_pubkey: String,
201    pub author_owner_grant: MembershipGrantId,
202    pub stream_id: AuthorStreamId,
203}
204
205impl CausalCoordinate for MembershipCoord {
206    type StreamKey = MembershipStreamKey;
207
208    fn stream_key(&self) -> Self::StreamKey {
209        MembershipCoord::stream_key(self)
210    }
211
212    fn author_pubkey(&self) -> &str {
213        &self.author_pubkey
214    }
215
216    fn author_owner_grant(&self) -> &MembershipGrantId {
217        &self.author_owner_grant
218    }
219
220    fn seq(&self) -> u64 {
221        self.seq
222    }
223
224    fn entry_hash(&self) -> ObjectHash {
225        self.entry_hash
226    }
227}
228
229#[derive(Debug, Clone, PartialEq, Eq)]
230struct StoreAssignment {
231    role: StoreMembershipRoleGrant,
232    provider_account_email: Option<String>,
233}
234
235impl causal_grants::CausalHistoryEntry for MembershipEntry {
236    type Coord = MembershipCoord;
237
238    fn coord(&self) -> Self::Coord {
239        self.coord()
240    }
241
242    fn dependencies(&self) -> &[Self::Coord] {
243        &self.dependencies
244    }
245}
246
247impl CausalAssignment for StoreAssignment {
248    fn is_owner(&self) -> bool {
249        self.role.is_owner()
250    }
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
254#[serde(deny_unknown_fields)]
255pub struct OwnerStreamBarrier {
256    pub observed_streams: Vec<MembershipCoord>,
257}
258
259#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
260#[serde(deny_unknown_fields)]
261pub struct StoreGrantStreamBarrier {
262    pub observed_streams: Vec<MembershipCoord>,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
266#[serde(deny_unknown_fields)]
267pub struct MergeStoreOwnerGrantBarrier {
268    pub author_streams: StoreGrantStreamBarrier,
269    pub recovery: OwnerRecoveryCursor,
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
273#[serde(rename_all = "snake_case", deny_unknown_fields)]
274pub enum MergeMembershipGrantRetirementBarrier {
275    NonOwner {
276        author_streams: StoreGrantStreamBarrier,
277    },
278    Owner {
279        barrier: MergeStoreOwnerGrantBarrier,
280    },
281}
282
283impl MergeMembershipGrantRetirementBarrier {
284    fn author_streams(&self) -> &StoreGrantStreamBarrier {
285        match self {
286            Self::NonOwner { author_streams } => author_streams,
287            Self::Owner { barrier } => &barrier.author_streams,
288        }
289    }
290
291    fn owner_stream_barrier(&self) -> Option<OwnerGrantBarrier<MembershipCoord>> {
292        match self {
293            Self::NonOwner { .. } => None,
294            Self::Owner { barrier } => Some(shared_store_barrier(&barrier.author_streams)),
295        }
296    }
297}
298
299/// The wire body of one membership entry. Every field here is signed.
300#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
301#[serde(deny_unknown_fields)]
302pub struct MembershipEntryBody {
303    pub store_id: String,
304    pub author_pubkey: String,
305    pub author_owner_grant: MembershipGrantId,
306    pub stream_id: AuthorStreamId,
307    pub seq: u64,
308    pub previous_hash: Option<ObjectHash>,
309    pub dependencies: Vec<MembershipCoord>,
310    pub resolution_dependencies: Vec<StoreMembershipConflictResolutionRef>,
311    pub created_at: String,
312    pub change: MembershipChange,
313    #[serde(skip_serializing_if = "Option::is_none")]
314    pub provider_admin: Option<super::provider::ProviderAdminMembershipChange>,
315}
316
317impl SignedBody for MembershipEntryBody {
318    const DOMAIN: &'static [u8] = MEMBERSHIP_ENTRY_DOMAIN;
319}
320
321pub type MembershipEntry = Signed<MembershipEntryBody>;
322
323#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
324#[serde(deny_unknown_fields)]
325pub struct MembershipEntryRef {
326    pub coord: MembershipCoord,
327    pub object: ExactObjectRef,
328}
329
330impl MembershipEntry {
331    pub fn coord(&self) -> MembershipCoord {
332        MembershipCoord {
333            author_pubkey: self.author_pubkey.clone(),
334            author_owner_grant: self.author_owner_grant.clone(),
335            stream_id: self.stream_id,
336            seq: self.seq,
337            entry_hash: self.hash(),
338        }
339    }
340}
341
342/// The wire body of one membership author head. Every field here is signed.
343#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
344#[serde(deny_unknown_fields)]
345pub struct AuthorHeadBody {
346    pub store_id: String,
347    pub body: MembershipHeadBody,
348    pub activation: MembershipHeadActivation,
349}
350
351impl SignedBody for AuthorHeadBody {
352    const DOMAIN: &'static [u8] = MEMBERSHIP_HEAD_DOMAIN;
353}
354
355pub type AuthorHead = Signed<AuthorHeadBody>;
356
357#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
358#[serde(deny_unknown_fields)]
359pub struct MembershipHeadBody {
360    pub author_registration: StoreDeviceRegistrationRef,
361    pub entry: MembershipEntryRef,
362    pub predecessor: Option<MembershipHeadRef>,
363    pub resolutions: Vec<StoreMembershipConflictResolutionRef>,
364    pub successor: SuccessorLink,
365}
366
367#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
368#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
369pub enum MembershipHeadActivation {
370    Direct,
371    StoreCommit { commit: StoreBatchCommitRef },
372}
373
374#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
375#[serde(deny_unknown_fields)]
376pub struct MergeMembershipHeadTransition {
377    pub body: MembershipHeadBody,
378    pub head_slot: crate::objects::ObjectSlot,
379}
380
381impl MergeMembershipHeadTransition {
382    pub fn matches_head(&self, head: &AuthorHead, reference: &MembershipHeadRef) -> bool {
383        self.body == head.body
384            && self.head_slot == *reference.object.slot()
385            && self.body.entry.coord == reference.coord
386            && head.entry_coord() == reference.coord
387            && head.head_hash() == reference.head_hash
388    }
389}
390
391#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
392#[serde(deny_unknown_fields)]
393pub struct MembershipHeadRef {
394    pub coord: MembershipCoord,
395    pub head_hash: ObjectHash,
396    pub object: ExactObjectRef,
397}
398
399#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
400pub enum MembershipFloorError {
401    #[error("membership floor is empty")]
402    Empty,
403    #[error("membership floor contains sequence zero")]
404    SequenceZero,
405    #[error("membership floor is not strictly ordered by author stream")]
406    NotStrictlyOrdered,
407}
408
409pub fn validate_membership_floor(floor: &[MembershipHeadRef]) -> Result<(), MembershipFloorError> {
410    if floor.is_empty() {
411        return Err(MembershipFloorError::Empty);
412    }
413    for (index, reference) in floor.iter().enumerate() {
414        if reference.coord.seq == 0 {
415            return Err(MembershipFloorError::SequenceZero);
416        }
417        if index > 0 && floor[index - 1].coord.stream_key() >= reference.coord.stream_key() {
418            return Err(MembershipFloorError::NotStrictlyOrdered);
419        }
420    }
421    Ok(())
422}
423
424#[derive(Debug, thiserror::Error, PartialEq, Eq)]
425pub enum MembershipError {
426    #[error("membership chain is empty")]
427    EmptyChain,
428    #[error("membership entry {0} has unsupported version")]
429    UnsupportedVersion(usize),
430    #[error("membership entry {index} belongs to store {actual:?}, expected {expected:?}")]
431    StoreMismatch {
432        index: usize,
433        expected: String,
434        actual: String,
435    },
436    #[error("membership entry {0} has an invalid signature")]
437    InvalidSignature(usize),
438    #[error("membership entry {index} is in coordinate {actual:?}, expected {expected:?}")]
439    CoordinateMismatch {
440        index: usize,
441        expected: Box<MembershipCoord>,
442        actual: Box<MembershipCoord>,
443    },
444    #[error("membership stream {author}/{grant} is missing sequence {seq}")]
445    MissingSequence {
446        author: String,
447        grant: MembershipGrantId,
448        seq: u64,
449    },
450    #[error("membership stream {author}/{grant} has conflicting entries at sequence {seq}")]
451    ConflictingSequence {
452        author: String,
453        grant: MembershipGrantId,
454        seq: u64,
455    },
456    #[error("membership entry at {coord:?} differs from the expected exact entry")]
457    ExactEntryMismatch { coord: Box<MembershipCoord> },
458    #[error("membership entry {index} has predecessor {actual:?}, expected {expected:?}")]
459    BrokenStreamLink {
460        index: usize,
461        expected: Option<ObjectHash>,
462        actual: Option<ObjectHash>,
463    },
464    #[error("membership entry {index} does not carry its complete own-stream dependency")]
465    MissingOwnDependency { index: usize },
466    #[error("membership entry {index} depends on missing coordinate {dependency:?}")]
467    MissingDependency {
468        index: usize,
469        dependency: Box<MembershipCoord>,
470    },
471    #[error(
472        "membership entry {index} dependency frontier is not strictly ordered by author stream"
473    )]
474    NonCanonicalDependencyFrontier { index: usize },
475    #[error("membership dependency graph contains a cycle")]
476    DependencyCycle,
477    #[error("membership founder entry is invalid")]
478    InvalidFounder,
479    #[error("membership entry {index} author is not active under Owner grant {grant}")]
480    AuthorGrantInactive {
481        index: usize,
482        grant: MembershipGrantId,
483    },
484    #[error("membership entry {index} creates an already-defined grant {grant}")]
485    DuplicateGrant {
486        index: usize,
487        grant: MembershipGrantId,
488    },
489    #[error("membership entry {index} replaces or removes grant {grant} owned by another member")]
490    GrantOwnerMismatch {
491        index: usize,
492        grant: MembershipGrantId,
493    },
494    #[error("membership entry {index} does not name the exact active grants for member {pubkey}")]
495    GrantSetMismatch { index: usize, pubkey: String },
496    #[error("membership entry {index} removes no exact grants")]
497    EmptyRemoval { index: usize },
498    #[error("membership entry {index} removes Owner grant {grant} without its exact observed-through coordinate")]
499    MissingOwnerRevocationBarrier {
500        index: usize,
501        grant: MembershipGrantId,
502    },
503    #[error(
504        "membership entry {index} carries an invalid revocation barrier for Owner grant {grant}"
505    )]
506    InvalidOwnerRevocationBarrier {
507        index: usize,
508        grant: MembershipGrantId,
509    },
510    #[error("membership change retiring Owner grants lacks the exact Merge device state")]
511    MissingOwnerRecoveryState,
512    #[error("membership change without an Owner retirement carries a device state")]
513    UnexpectedOwnerRecoveryState,
514    #[error("membership entry {0} carries an invalid Owner membership stream anchor")]
515    InvalidOwnerMembershipAnchor(usize),
516    #[error("membership entry {0} carries invalid wrapped Store-key authority")]
517    InvalidWrappedKeys(usize),
518    #[error("checkpoint lacks the exact record for membership grant {grant}")]
519    MissingCheckpointGrant { grant: MembershipGrantId },
520    #[error("checkpoint lacks retirement evidence for membership grant {grant}")]
521    MissingCheckpointRetirementEvidence { grant: MembershipGrantId },
522    #[error("membership grant {grant} retirement at {authority:?} lacks its exact signed barrier")]
523    MissingRetirementBarrier {
524        grant: MembershipGrantId,
525        authority: Box<MembershipCoord>,
526    },
527    #[error(
528        "current member {recipient_pubkey} lacks wrapped Store-key coverage for rotation {rotation:?}"
529    )]
530    MissingWrappedKeyCoverage {
531        recipient_pubkey: String,
532        rotation: Box<MembershipCoord>,
533    },
534    #[error("membership history leaves no active Owner")]
535    NoActiveOwner,
536    #[error(
537        "membership revocation cycle has {sources} sources, exceeding the protocol limit of {maximum}"
538    )]
539    RevocationCycleTooWide { sources: usize, maximum: usize },
540    #[error("signer {0} has no active Owner grant")]
541    SignerIsNotOwner(String),
542    #[error("member {0} has no active grants")]
543    NotAMember(String),
544    #[error("non-founder Owner grants require an accepted Owner promotion")]
545    OwnerPromotionRequired,
546    #[error("Owner promotion does not match the exact current membership state")]
547    InvalidOwnerPromotion,
548    #[error("membership author stream contains a pruned suffix and cannot be extended")]
549    PrunedAuthorStream,
550    #[error("membership author stream exhausted its sequence space")]
551    SequenceExhausted,
552    #[error("membership resolution activation entry {0} is invalid")]
553    InvalidResolutionActivation(usize),
554    #[error("membership resolution activation requires a fresh persisted author stream")]
555    ResolutionActivationRequiresFreshStream,
556    #[error("provider administrator control entry {0} is invalid")]
557    InvalidProviderAdminChange(usize),
558    #[error("membership has an unresolved semantic conflict")]
559    Conflict,
560    #[error("membership conflict is missing its exact signed raw heads")]
561    MissingConflictHeads,
562    #[error("membership conflict resolution does not name exact validated conflict evidence")]
563    InvalidConflictResolution,
564    #[error("provider administrator history is invalid: {0}")]
565    ProviderAdmin(#[from] super::provider::ProviderAdminReducerError),
566}
567
568#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
569#[serde(deny_unknown_fields)]
570pub struct MembershipGrantRecord {
571    pub member_pubkey: String,
572    pub role: StoreMembershipRoleGrant,
573    #[serde(skip_serializing_if = "Option::is_none")]
574    pub provider_account_email: Option<String>,
575    pub creation_authority: MembershipGrantCreationAuthority,
576}
577
578#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
579#[serde(rename_all = "snake_case", deny_unknown_fields)]
580pub enum MembershipGrantCreationAuthority {
581    Entry(MembershipCoord),
582    ConflictResolution(StoreMembershipConflictResolutionRef),
583}
584
585#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
586#[serde(rename_all = "snake_case", deny_unknown_fields)]
587pub enum MembershipGrantRetirement {
588    Entry {
589        authority: MembershipCoord,
590        barrier: MergeMembershipGrantRetirementBarrier,
591    },
592    ConflictResolution {
593        authority: StoreMembershipConflictResolutionRef,
594        barrier: MergeMembershipGrantRetirementBarrier,
595    },
596}
597
598#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
599#[serde(deny_unknown_fields)]
600pub struct ResolvedStoreMembership {
601    pub grants:
602        BTreeMap<MembershipGrantId, GrantState<MembershipGrantRecord, MembershipGrantRetirement>>,
603    pub provider_admin: super::provider::ProviderAdminResolution,
604    pub state_hash: ObjectHash,
605}
606
607#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
608#[serde(deny_unknown_fields)]
609pub struct StoreMembershipBranch {
610    pub heads: Vec<MembershipHeadRef>,
611    pub effective_frontier: Vec<MembershipCoord>,
612    pub grants:
613        BTreeMap<MembershipGrantId, GrantState<MembershipGrantRecord, MembershipGrantRetirement>>,
614    pub provider_admin: super::provider::ProviderAdminResolution,
615    pub state_hash: ObjectHash,
616}
617
618impl ResolvedStoreMembership {
619    pub fn active_grants(
620        &self,
621    ) -> impl Iterator<Item = (&MembershipGrantId, &MembershipGrantRecord)> {
622        causal_grants::active_grants(&self.grants)
623    }
624
625    pub fn active_grant(&self, grant: &MembershipGrantId) -> Option<&MembershipGrantRecord> {
626        self.grants.get(grant).and_then(GrantState::active)
627    }
628}
629
630impl StoreMembershipBranch {
631    pub fn active_grants(
632        &self,
633    ) -> impl Iterator<Item = (&MembershipGrantId, &MembershipGrantRecord)> {
634        causal_grants::active_grants(&self.grants)
635    }
636}
637
638#[derive(Debug, Clone, Default)]
639struct CausalState {
640    grants:
641        BTreeMap<MembershipGrantId, GrantState<MembershipGrantRecord, MembershipGrantRetirement>>,
642}
643
644#[derive(Debug, Clone)]
645pub struct MembershipChain {
646    entries: Vec<MembershipEntry>,
647    coords: Vec<MembershipCoord>,
648    state: CausalState,
649    included: BTreeSet<MembershipCoord>,
650    status: Option<MembershipStatus>,
651    head_refs: Vec<MembershipHeadRef>,
652    resolution_checkpoint: Option<MembershipResolutionCheckpoint>,
653    provider_admin_genesis: super::provider::ProviderAdminState,
654}
655
656#[derive(Debug, Clone)]
657struct MembershipResolutionCheckpoint {
658    raw_heads: Vec<MembershipCoord>,
659    effective_frontier: Vec<MembershipCoord>,
660    grants:
661        BTreeMap<MembershipGrantId, GrantState<MembershipGrantRecord, MembershipGrantRetirement>>,
662    grant_anchors: BTreeMap<MembershipGrantId, GrantStreamAnchor>,
663    included: BTreeSet<MembershipCoord>,
664    resolutions: Vec<StoreMembershipConflictResolutionRef>,
665    provider_admin: super::provider::ProviderAdminState,
666}
667
668#[cfg(any(test, feature = "test-utils"))]
669fn test_provider_admin_genesis(
670    entries: &[MembershipEntry],
671) -> Result<super::provider::ProviderAdminState, MembershipError> {
672    let founder = entries
673        .iter()
674        .find_map(|entry| match &entry.change {
675            MembershipChange::Founder { provider_admin, .. } => Some((entry, provider_admin)),
676            _ => None,
677        })
678        .ok_or(MembershipError::InvalidFounder)?;
679    let root_bytes = founder.0.store_id.as_bytes();
680    let root = StoreRootRef {
681        store_root_id: ObjectHash::digest(
682            format!("{} test root id", founder.0.store_id).as_bytes(),
683        ),
684        store_root_hash: ObjectHash::digest(root_bytes),
685        object: ExactObjectRef::new(
686            crate::objects::ObjectSlot::logical(format!(
687                "store-v1/test/{}/root.json",
688                founder.0.store_id
689            ))
690            .expect("valid test root slot"),
691            root_bytes.len() as u64,
692            ObjectHash::digest(root_bytes),
693        ),
694    };
695    let registration: StoreDeviceRegistrationRef =
696        serde_json::from_value(serde_json::json!({
697            "device_id": ObjectHash::digest(format!("{} founder device", founder.0.store_id).as_bytes()),
698            "registration_hash": ObjectHash::digest(format!("{} founder registration", founder.0.store_id).as_bytes()),
699            "object": {
700                "slot": {"logical_key": format!("store-v1/test/{}/registration.json", founder.0.store_id), "physical": {"kind": "logical_key"}},
701                "stored_size": 1,
702                "stored_hash": ObjectHash::digest(format!("{} founder registration object", founder.0.store_id).as_bytes()),
703            }
704        }))
705        .expect("valid test founder registration reference");
706    Ok(super::provider::ProviderAdminState::founder_from_root(
707        root,
708        registration,
709        founder.1,
710    ))
711}
712
713impl MembershipChain {}
714
715impl AuthorHead {
716    pub fn signed(
717        store_id: String,
718        mut body: MembershipHeadBody,
719        activation: MembershipHeadActivation,
720        device_signer: &UserKeypair,
721    ) -> Self {
722        body.resolutions.sort();
723        body.resolutions.dedup();
724        Signed::sign(
725            AuthorHeadBody {
726                store_id,
727                body,
728                activation,
729            },
730            device_signer,
731        )
732    }
733
734    pub fn verify(&self, registration: &StoreDeviceRegistration) -> bool {
735        self.body
736            .resolutions
737            .windows(2)
738            .all(|pair| pair[0] < pair[1])
739            && self
740                .body
741                .author_registration
742                .verify_registration(registration)
743                .is_ok()
744            && registration.author_pubkey == self.body.entry.coord.author_pubkey
745            && self.body.successor.predecessor
746                == self
747                    .body
748                    .predecessor
749                    .as_ref()
750                    .map(|reference| reference.object.clone())
751            && self.verify_by(&registration.device_signing_pubkey).is_ok()
752    }
753
754    pub fn entry_coord(&self) -> MembershipCoord {
755        self.body.entry.coord.clone()
756    }
757
758    pub fn head_hash(&self) -> ObjectHash {
759        self.hash()
760    }
761}
762
763#[cfg(test)]
764mod tests;
765
766pub const OWNER_PUBKEY_STATE_KEY: &str = "owner_pubkey";
767
768#[derive(Clone, Copy, Debug, PartialEq, Eq)]
769pub enum LocalStoreMembership {
770    Current,
771    NotYetMember,
772    Removed,
773    IdentityNotSupplied,
774}
775
776impl LocalStoreMembership {
777    pub fn from_membership(
778        membership: &MembershipChain,
779        identity: Option<&coven_keys::keys::UserKeypair>,
780    ) -> Result<Self, crate::membership::MembershipError> {
781        membership.ensure_resolved()?;
782        let Some(identity) = identity else {
783            return Ok(Self::IdentityNotSupplied);
784        };
785        let identity = coven_keys::keys::public_key_hex(identity);
786        if membership
787            .current_members()
788            .iter()
789            .any(|(member, _)| member == &identity)
790        {
791            Ok(Self::Current)
792        } else if membership.contains_member_history(&identity) {
793            Ok(Self::Removed)
794        } else {
795            Ok(Self::NotYetMember)
796        }
797    }
798
799    pub fn allows_circle_access(self) -> bool {
800        matches!(self, Self::Current)
801    }
802
803    pub fn retains_circle_rows(self) -> bool {
804        !matches!(self, Self::Removed)
805    }
806}
807
808pub enum ApplyOutcome<R> {
809    Applied(Vec<coven_foundation::changeset::RowChange>),
810    Held(R),
811}
812
813#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
814#[serde(transparent)]
815pub struct MembershipFloor(pub Vec<MembershipHeadRef>);
816
817impl MembershipFloor {
818    pub fn validate(&self) -> Result<(), MembershipFloorError> {
819        crate::membership::validate_membership_floor(&self.0)
820    }
821}