Skip to main content

coven_core/sync/
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::storage::ExactObjectRef;
18use super::store_commit::{
19    GrantStreamAnchor, ObjectHash, OwnerConflictResolutionAcceptance, OwnerPromotionAcceptance,
20    OwnerPromotionFinalization, OwnerRecoveryCursor, StoreBatchCommitRef, StoreCreationId,
21    StoreDeviceRegistration, StoreDeviceRegistrationRef, StoreDeviceStateRef, StoreRootRef,
22    SuccessorLink, STORE_PROTOCOL_VERSION,
23};
24#[cfg(any(test, feature = "test-utils"))]
25use super::store_commit::{
26    OwnerPromotionAnchors, OwnerPromotionId, OwnerPromotionRequest,
27    OwnerPromotionRequestActivation, OwnerRecoveryNodeRef, OwnerRecoveryPosition,
28};
29use super::wrapped_store_key::WrappedStoreKeyRef;
30use crate::keys::{self, UserKeypair};
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33pub enum MemberRole {
34    Owner,
35    Member,
36    Follower,
37}
38
39impl MemberRole {
40    pub(crate) fn can_write(&self) -> bool {
41        matches!(self, Self::Owner | Self::Member)
42    }
43}
44
45#[cfg(any(test, feature = "test-utils"))]
46pub(crate) fn test_wrapped_key_ref(
47    owner_pubkey: &str,
48    recipient_pubkey: &str,
49    generation: u64,
50    label: &[u8],
51) -> WrappedStoreKeyRef {
52    let wrap_hash = ObjectHash::digest(
53        &[
54            label,
55            owner_pubkey.as_bytes(),
56            recipient_pubkey.as_bytes(),
57            &generation.to_le_bytes(),
58        ]
59        .concat(),
60    );
61    let logical_key =
62        format!("keys/{owner_pubkey}/{recipient_pubkey}/{generation}/{wrap_hash}.json");
63    WrappedStoreKeyRef {
64        owner_pubkey: owner_pubkey.to_string(),
65        recipient_pubkey: recipient_pubkey.to_string(),
66        generation,
67        wrap_hash,
68        object: ExactObjectRef::new(
69            crate::storage::cloud::ObjectSlot::logical(logical_key)
70                .expect("test wrapped-key slot is valid"),
71            label.len() as u64,
72            ObjectHash::digest(label),
73        ),
74    }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
78#[serde(rename_all = "snake_case", deny_unknown_fields)]
79pub enum OwnerRecoveryAnchorRef {
80    Founder {
81        creation_id: StoreCreationId,
82    },
83    Promotion {
84        acceptance: Box<OwnerPromotionAcceptance>,
85    },
86    ConflictResolution {
87        acceptance: Box<OwnerConflictResolutionAcceptance>,
88    },
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
92#[serde(rename_all = "snake_case", deny_unknown_fields)]
93pub enum StoreMembershipRoleGrant {
94    Owner { recovery: OwnerRecoveryAnchorRef },
95    Member,
96    Follower,
97}
98
99impl StoreMembershipRoleGrant {
100    pub fn role(&self) -> MemberRole {
101        match self {
102            Self::Owner { .. } => MemberRole::Owner,
103            Self::Member => MemberRole::Member,
104            Self::Follower => MemberRole::Follower,
105        }
106    }
107
108    fn from_direct_assignment(role: MemberRole) -> Result<Self, MembershipError> {
109        match role {
110            MemberRole::Owner => Err(MembershipError::OwnerPromotionRequired),
111            MemberRole::Member => Ok(Self::Member),
112            MemberRole::Follower => Ok(Self::Follower),
113        }
114    }
115
116    fn can_write(&self) -> bool {
117        matches!(self, Self::Owner { .. } | Self::Member)
118    }
119
120    fn is_owner(&self) -> bool {
121        matches!(self, Self::Owner { .. })
122    }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
126pub struct MemberInfo {
127    pub pubkey: String,
128    pub role: MemberRole,
129    pub is_self: bool,
130}
131
132#[derive(Clone, PartialEq, Eq)]
133pub struct MembershipConflictChoice {
134    pub id: String,
135    pub members: Vec<MemberInfo>,
136    conflict_hash: ObjectHash,
137    selection: MembershipConflictSelection,
138}
139
140impl std::fmt::Debug for MembershipConflictChoice {
141    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        formatter
143            .debug_struct("MembershipConflictChoice")
144            .field("id", &self.id)
145            .field("members", &self.members)
146            .finish()
147    }
148}
149
150impl MembershipConflictChoice {
151    pub(crate) fn new(
152        id: String,
153        members: Vec<MemberInfo>,
154        conflict_hash: ObjectHash,
155        selection: MembershipConflictSelection,
156    ) -> Self {
157        Self {
158            id,
159            members,
160            conflict_hash,
161            selection,
162        }
163    }
164
165    pub(crate) fn conflict_hash(&self) -> ObjectHash {
166        self.conflict_hash
167    }
168
169    pub(crate) fn selection(&self) -> &MembershipConflictSelection {
170        &self.selection
171    }
172}
173
174#[derive(Debug, Clone, PartialEq, Eq)]
175pub enum MembershipConflictInfo {
176    ConcurrentMemberAssignments {
177        id: String,
178        member_pubkey: String,
179        choices: Vec<MembershipConflictChoice>,
180    },
181    RevocationCycle {
182        id: String,
183        choices: Vec<MembershipConflictChoice>,
184    },
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
188#[serde(deny_unknown_fields)]
189pub enum MembershipChange {
190    Founder {
191        creation_id: StoreCreationId,
192        owner_pubkey: String,
193        owner_grant_id: MembershipGrantId,
194        membership: GrantStreamAnchor,
195        provider_admin: super::provider::FounderProviderAdminGrant,
196    },
197    SetMember {
198        user_pubkey: String,
199        #[serde(default, skip_serializing_if = "Option::is_none")]
200        provider_account_email: Option<String>,
201        role: StoreMembershipRoleGrant,
202        grant_id: MembershipGrantId,
203        membership: Option<GrantStreamAnchor>,
204        replaces: BTreeSet<MembershipGrantId>,
205        retirement_barriers: BTreeMap<MembershipGrantId, MergeMembershipGrantRetirementBarrier>,
206        #[serde(skip_serializing_if = "Option::is_none")]
207        retirement_device_state: Option<StoreDeviceStateRef>,
208        wrapped_key: WrappedStoreKeyRef,
209    },
210    RemoveMember {
211        user_pubkey: String,
212        removes: BTreeSet<MembershipGrantId>,
213        retirement_barriers: BTreeMap<MembershipGrantId, MergeMembershipGrantRetirementBarrier>,
214        #[serde(skip_serializing_if = "Option::is_none")]
215        retirement_device_state: Option<StoreDeviceStateRef>,
216        wrapped_keys: Vec<WrappedStoreKeyRef>,
217    },
218    ProviderAdmin,
219    ResolutionActivation {
220        resolution: StoreMembershipConflictResolutionRef,
221    },
222}
223
224impl MembershipChange {
225    pub(crate) fn membership_anchor(&self) -> Option<GrantStreamAnchor> {
226        match self {
227            Self::Founder { membership, .. } => Some(membership.clone()),
228            Self::SetMember { membership, .. } => membership.clone(),
229            Self::RemoveMember { .. } | Self::ProviderAdmin | Self::ResolutionActivation { .. } => {
230                None
231            }
232        }
233    }
234}
235
236#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
237#[serde(deny_unknown_fields)]
238pub struct MembershipCoord {
239    pub author_pubkey: String,
240    pub author_owner_grant: MembershipGrantId,
241    pub stream_id: AuthorStreamId,
242    pub seq: u64,
243    pub entry_hash: ObjectHash,
244}
245
246impl MembershipCoord {
247    pub(crate) fn stream_key(&self) -> MembershipStreamKey {
248        MembershipStreamKey {
249            author_pubkey: self.author_pubkey.clone(),
250            author_owner_grant: self.author_owner_grant.clone(),
251            stream_id: self.stream_id,
252        }
253    }
254}
255
256#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
257pub(crate) struct MembershipStreamKey {
258    pub(crate) author_pubkey: String,
259    pub(crate) author_owner_grant: MembershipGrantId,
260    pub(crate) stream_id: AuthorStreamId,
261}
262
263impl CausalCoordinate for MembershipCoord {
264    type StreamKey = MembershipStreamKey;
265
266    fn stream_key(&self) -> Self::StreamKey {
267        MembershipCoord::stream_key(self)
268    }
269
270    fn author_pubkey(&self) -> &str {
271        &self.author_pubkey
272    }
273
274    fn author_owner_grant(&self) -> &MembershipGrantId {
275        &self.author_owner_grant
276    }
277
278    fn seq(&self) -> u64 {
279        self.seq
280    }
281
282    fn entry_hash(&self) -> ObjectHash {
283        self.entry_hash
284    }
285}
286
287#[derive(Debug, Clone, PartialEq, Eq)]
288struct StoreAssignment {
289    role: StoreMembershipRoleGrant,
290    provider_account_email: Option<String>,
291}
292
293impl CausalAssignment for StoreAssignment {
294    fn is_owner(&self) -> bool {
295        self.role.is_owner()
296    }
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
300#[serde(deny_unknown_fields)]
301pub struct OwnerStreamBarrier {
302    pub observed_streams: Vec<MembershipCoord>,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
306#[serde(deny_unknown_fields)]
307pub struct StoreGrantStreamBarrier {
308    pub observed_streams: Vec<MembershipCoord>,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
312#[serde(deny_unknown_fields)]
313pub struct MergeStoreOwnerGrantBarrier {
314    pub author_streams: StoreGrantStreamBarrier,
315    pub recovery: OwnerRecoveryCursor,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
319#[serde(rename_all = "snake_case", deny_unknown_fields)]
320pub enum MergeMembershipGrantRetirementBarrier {
321    NonOwner {
322        author_streams: StoreGrantStreamBarrier,
323    },
324    Owner {
325        barrier: MergeStoreOwnerGrantBarrier,
326    },
327}
328
329impl MergeMembershipGrantRetirementBarrier {
330    fn author_streams(&self) -> &StoreGrantStreamBarrier {
331        match self {
332            Self::NonOwner { author_streams } => author_streams,
333            Self::Owner { barrier } => &barrier.author_streams,
334        }
335    }
336
337    fn owner_stream_barrier(&self) -> Option<OwnerGrantBarrier<MembershipCoord>> {
338        match self {
339            Self::NonOwner { .. } => None,
340            Self::Owner { barrier } => Some(shared_store_barrier(&barrier.author_streams)),
341        }
342    }
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
346#[serde(deny_unknown_fields)]
347pub struct MembershipEntry {
348    pub version: u32,
349    pub store_id: String,
350    pub author_pubkey: String,
351    pub author_owner_grant: MembershipGrantId,
352    pub stream_id: AuthorStreamId,
353    pub seq: u64,
354    pub previous_hash: Option<ObjectHash>,
355    pub dependencies: Vec<MembershipCoord>,
356    pub resolution_dependencies: Vec<StoreMembershipConflictResolutionRef>,
357    pub created_at: String,
358    pub change: MembershipChange,
359    #[serde(skip_serializing_if = "Option::is_none")]
360    pub provider_admin: Option<super::provider::ProviderAdminMembershipChange>,
361    pub signature: String,
362}
363
364#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
365#[serde(deny_unknown_fields)]
366pub struct MembershipEntryRef {
367    pub coord: MembershipCoord,
368    pub object: ExactObjectRef,
369}
370
371impl MembershipEntry {
372    pub fn coord(&self) -> MembershipCoord {
373        MembershipCoord {
374            author_pubkey: self.author_pubkey.clone(),
375            author_owner_grant: self.author_owner_grant.clone(),
376            stream_id: self.stream_id,
377            seq: self.seq,
378            entry_hash: entry_hash(self),
379        }
380    }
381
382    pub fn provider_account_email(&self) -> Option<&str> {
383        match &self.change {
384            MembershipChange::SetMember {
385                provider_account_email,
386                ..
387            } => provider_account_email.as_deref(),
388            MembershipChange::Founder { .. }
389            | MembershipChange::RemoveMember { .. }
390            | MembershipChange::ProviderAdmin
391            | MembershipChange::ResolutionActivation { .. } => None,
392        }
393    }
394}
395
396#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
397#[serde(deny_unknown_fields)]
398pub struct AuthorHead {
399    pub version: u32,
400    pub store_id: String,
401    pub body: MembershipHeadBody,
402    pub activation: MembershipHeadActivation,
403    pub signature: String,
404}
405
406#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
407#[serde(deny_unknown_fields)]
408pub struct MembershipHeadBody {
409    pub author_registration: StoreDeviceRegistrationRef,
410    pub entry: MembershipEntryRef,
411    pub predecessor: Option<MembershipHeadRef>,
412    pub resolutions: Vec<StoreMembershipConflictResolutionRef>,
413    pub successor: SuccessorLink,
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
417#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
418pub enum MembershipHeadActivation {
419    Direct,
420    StoreCommit { commit: StoreBatchCommitRef },
421}
422
423#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
424#[serde(deny_unknown_fields)]
425pub struct MergeMembershipHeadTransition {
426    pub body: MembershipHeadBody,
427    pub head_slot: crate::storage::cloud::ObjectSlot,
428}
429
430impl MergeMembershipHeadTransition {
431    pub fn matches_head(&self, head: &AuthorHead, reference: &MembershipHeadRef) -> bool {
432        self.body == head.body
433            && self.head_slot == *reference.object.slot()
434            && self.body.entry.coord == reference.coord
435            && head.entry_coord() == reference.coord
436            && head.head_hash() == reference.head_hash
437    }
438}
439
440#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
441#[serde(deny_unknown_fields)]
442pub struct MembershipHeadRef {
443    pub coord: MembershipCoord,
444    pub head_hash: ObjectHash,
445    pub object: ExactObjectRef,
446}
447
448pub(crate) fn validate_membership_floor(floor: &[MembershipHeadRef]) -> Result<(), String> {
449    if floor.is_empty() {
450        return Err("membership floor is empty".to_string());
451    }
452    for (index, reference) in floor.iter().enumerate() {
453        if reference.coord.seq == 0 {
454            return Err("membership floor contains sequence zero".to_string());
455        }
456        if index > 0 && floor[index - 1].coord.stream_key() >= reference.coord.stream_key() {
457            return Err("membership floor is not strictly ordered by author stream".to_string());
458        }
459    }
460    Ok(())
461}
462
463#[derive(Debug, thiserror::Error, PartialEq, Eq)]
464pub enum MembershipError {
465    #[error("membership chain is empty")]
466    EmptyChain,
467    #[error("membership entry {0} has unsupported version")]
468    UnsupportedVersion(usize),
469    #[error("membership entry {index} belongs to store {actual:?}, expected {expected:?}")]
470    StoreMismatch {
471        index: usize,
472        expected: String,
473        actual: String,
474    },
475    #[error("membership entry {0} has an invalid signature")]
476    InvalidSignature(usize),
477    #[error("membership entry {index} is in coordinate {actual:?}, expected {expected:?}")]
478    CoordinateMismatch {
479        index: usize,
480        expected: Box<MembershipCoord>,
481        actual: Box<MembershipCoord>,
482    },
483    #[error("membership stream {author}/{grant} is missing sequence {seq}")]
484    MissingSequence {
485        author: String,
486        grant: MembershipGrantId,
487        seq: u64,
488    },
489    #[error("membership stream {author}/{grant} has conflicting entries at sequence {seq}")]
490    ConflictingSequence {
491        author: String,
492        grant: MembershipGrantId,
493        seq: u64,
494    },
495    #[error("membership entry {index} has predecessor {actual:?}, expected {expected:?}")]
496    BrokenStreamLink {
497        index: usize,
498        expected: Option<ObjectHash>,
499        actual: Option<ObjectHash>,
500    },
501    #[error("membership entry {index} does not carry its complete own-stream dependency")]
502    MissingOwnDependency { index: usize },
503    #[error("membership entry {index} depends on missing coordinate {dependency:?}")]
504    MissingDependency {
505        index: usize,
506        dependency: Box<MembershipCoord>,
507    },
508    #[error(
509        "membership entry {index} dependency frontier is not strictly ordered by author stream"
510    )]
511    NonCanonicalDependencyFrontier { index: usize },
512    #[error("membership dependency graph contains a cycle")]
513    DependencyCycle,
514    #[error("membership founder entry is invalid")]
515    InvalidFounder,
516    #[error("membership entry {index} author is not active under Owner grant {grant}")]
517    AuthorGrantInactive {
518        index: usize,
519        grant: MembershipGrantId,
520    },
521    #[error("membership entry {index} creates an already-defined grant {grant}")]
522    DuplicateGrant {
523        index: usize,
524        grant: MembershipGrantId,
525    },
526    #[error("membership entry {index} replaces or removes grant {grant} owned by another member")]
527    GrantOwnerMismatch {
528        index: usize,
529        grant: MembershipGrantId,
530    },
531    #[error("membership entry {index} does not name the exact active grants for member {pubkey}")]
532    GrantSetMismatch { index: usize, pubkey: String },
533    #[error("membership entry {index} removes no exact grants")]
534    EmptyRemoval { index: usize },
535    #[error("membership entry {index} removes Owner grant {grant} without its exact observed-through coordinate")]
536    MissingOwnerRevocationBarrier {
537        index: usize,
538        grant: MembershipGrantId,
539    },
540    #[error(
541        "membership entry {index} carries an invalid revocation barrier for Owner grant {grant}"
542    )]
543    InvalidOwnerRevocationBarrier {
544        index: usize,
545        grant: MembershipGrantId,
546    },
547    #[error("membership change retiring Owner grants lacks the exact Merge device state")]
548    MissingOwnerRecoveryState,
549    #[error("membership change without an Owner retirement carries a device state")]
550    UnexpectedOwnerRecoveryState,
551    #[error("membership entry {0} carries an invalid Owner membership stream anchor")]
552    InvalidOwnerMembershipAnchor(usize),
553    #[error("membership entry {0} carries invalid wrapped Store-key authority")]
554    InvalidWrappedKeys(usize),
555    #[error("checkpoint lacks the exact record for membership grant {grant}")]
556    MissingCheckpointGrant { grant: MembershipGrantId },
557    #[error("checkpoint lacks retirement evidence for membership grant {grant}")]
558    MissingCheckpointRetirementEvidence { grant: MembershipGrantId },
559    #[error("membership grant {grant} retirement at {authority:?} lacks its exact signed barrier")]
560    MissingRetirementBarrier {
561        grant: MembershipGrantId,
562        authority: Box<MembershipCoord>,
563    },
564    #[error(
565        "current member {recipient_pubkey} lacks wrapped Store-key coverage for rotation {rotation:?}"
566    )]
567    MissingWrappedKeyCoverage {
568        recipient_pubkey: String,
569        rotation: Box<MembershipCoord>,
570    },
571    #[error("membership history leaves no active Owner")]
572    NoActiveOwner,
573    #[error(
574        "membership revocation cycle has {sources} sources, exceeding the protocol limit of {maximum}"
575    )]
576    RevocationCycleTooWide { sources: usize, maximum: usize },
577    #[error("signer {0} has no active Owner grant")]
578    SignerIsNotOwner(String),
579    #[error("member {0} has no active grants")]
580    NotAMember(String),
581    #[error("non-founder Owner grants require an accepted Owner promotion")]
582    OwnerPromotionRequired,
583    #[error("Owner promotion does not match the exact current membership state")]
584    InvalidOwnerPromotion,
585    #[error("membership author stream contains a pruned suffix and cannot be extended")]
586    PrunedAuthorStream,
587    #[error("membership author stream exhausted its sequence space")]
588    SequenceExhausted,
589    #[error("membership author has no reusable stream; a fresh persisted stream is required")]
590    MissingAuthorStream,
591    #[error("membership resolution activation entry {0} is invalid")]
592    InvalidResolutionActivation(usize),
593    #[error("membership resolution activation requires a fresh persisted author stream")]
594    ResolutionActivationRequiresFreshStream,
595    #[error("provider administrator control entry {0} is invalid")]
596    InvalidProviderAdminChange(usize),
597    #[error("membership has an unresolved semantic conflict")]
598    Conflict,
599    #[error("membership conflict is missing its exact signed raw heads")]
600    MissingConflictHeads,
601    #[error("membership conflict resolution does not name exact validated conflict evidence")]
602    InvalidConflictResolution,
603    #[error("provider administrator history is invalid: {0}")]
604    ProviderAdmin(#[from] super::provider::ProviderAdminReducerError),
605}
606
607#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
608#[serde(deny_unknown_fields)]
609pub struct MembershipGrantRecord {
610    pub member_pubkey: String,
611    pub role: StoreMembershipRoleGrant,
612    #[serde(default, skip_serializing_if = "Option::is_none")]
613    pub provider_account_email: Option<String>,
614    pub creation_authority: MembershipGrantCreationAuthority,
615}
616
617#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
618#[serde(rename_all = "snake_case", deny_unknown_fields)]
619pub enum MembershipGrantCreationAuthority {
620    Entry(MembershipCoord),
621    ConflictResolution(StoreMembershipConflictResolutionRef),
622}
623
624#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
625#[serde(rename_all = "snake_case", deny_unknown_fields)]
626pub enum MembershipGrantRetirement {
627    Entry {
628        authority: MembershipCoord,
629        barrier: MergeMembershipGrantRetirementBarrier,
630    },
631    ConflictResolution {
632        authority: StoreMembershipConflictResolutionRef,
633        barrier: MergeMembershipGrantRetirementBarrier,
634    },
635}
636
637#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
638#[serde(deny_unknown_fields)]
639pub struct ResolvedStoreMembership {
640    pub grants:
641        BTreeMap<MembershipGrantId, GrantState<MembershipGrantRecord, MembershipGrantRetirement>>,
642    pub provider_admin: super::provider::ProviderAdminResolution,
643    pub state_hash: ObjectHash,
644}
645
646#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
647#[serde(deny_unknown_fields)]
648pub struct StoreMembershipBranch {
649    pub heads: Vec<MembershipHeadRef>,
650    pub effective_frontier: Vec<MembershipCoord>,
651    pub grants:
652        BTreeMap<MembershipGrantId, GrantState<MembershipGrantRecord, MembershipGrantRetirement>>,
653    pub provider_admin: super::provider::ProviderAdminResolution,
654    pub state_hash: ObjectHash,
655}
656
657fn active_membership_grants(
658    grants: &BTreeMap<
659        MembershipGrantId,
660        GrantState<MembershipGrantRecord, MembershipGrantRetirement>,
661    >,
662) -> impl Iterator<Item = (&MembershipGrantId, &MembershipGrantRecord)> {
663    grants
664        .iter()
665        .filter_map(|(grant, state)| state.active().map(|record| (grant, record)))
666}
667
668impl ResolvedStoreMembership {
669    pub fn active_grants(
670        &self,
671    ) -> impl Iterator<Item = (&MembershipGrantId, &MembershipGrantRecord)> {
672        active_membership_grants(&self.grants)
673    }
674
675    pub fn active_grant(&self, grant: &MembershipGrantId) -> Option<&MembershipGrantRecord> {
676        self.grants.get(grant).and_then(GrantState::active)
677    }
678}
679
680impl StoreMembershipBranch {
681    pub fn active_grants(
682        &self,
683    ) -> impl Iterator<Item = (&MembershipGrantId, &MembershipGrantRecord)> {
684        active_membership_grants(&self.grants)
685    }
686}
687
688#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
689#[serde(rename_all = "snake_case", deny_unknown_fields)]
690pub enum MembershipConflict {
691    ConcurrentMemberAssignments {
692        conflict_hash: ObjectHash,
693        heads: Vec<MembershipHeadRef>,
694        effective_frontier: Vec<MembershipCoord>,
695        member_pubkey: String,
696        conflicting_grants: BTreeMap<MembershipGrantId, MembershipGrantRecord>,
697        uncontested_grants: BTreeMap<MembershipGrantId, MembershipGrantRecord>,
698        grants: BTreeMap<
699            MembershipGrantId,
700            GrantState<MembershipGrantRecord, MembershipGrantRetirement>,
701        >,
702        provider_admin: super::provider::ProviderAdminResolution,
703    },
704    RevocationCycle {
705        conflict_hash: ObjectHash,
706        heads: Vec<MembershipHeadRef>,
707        cyclic_sources: Vec<MembershipCoord>,
708        involved_owner_grants: BTreeSet<MembershipGrantId>,
709        maximal_valid_branches: Vec<StoreMembershipBranch>,
710    },
711}
712
713#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
714#[serde(rename_all = "snake_case", deny_unknown_fields)]
715pub enum MembershipStatus {
716    Resolved(ResolvedStoreMembership),
717    Conflict(MembershipConflict),
718}
719
720#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
721#[serde(deny_unknown_fields)]
722pub struct StoreMembershipConflictResolutionRef {
723    pub conflict_hash: ObjectHash,
724    pub resolver_pubkey: String,
725    pub resolution_hash: ObjectHash,
726    pub object: ExactObjectRef,
727}
728
729#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
730#[serde(deny_unknown_fields)]
731pub enum MembershipConflictSelection {
732    MemberAssignment { grant: MembershipGrantId },
733    RevocationBranch { heads: Vec<MembershipHeadRef> },
734}
735
736#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
737#[serde(deny_unknown_fields)]
738pub struct StoreMembershipConflictResolution {
739    pub version: u32,
740    pub store_root_hash: ObjectHash,
741    pub conflict_hash: ObjectHash,
742    pub conflicting_heads: Vec<MembershipHeadRef>,
743    pub retired_owner_grants: BTreeSet<MembershipGrantId>,
744    pub retirement_barriers: BTreeMap<MembershipGrantId, MergeMembershipGrantRetirementBarrier>,
745    pub resolver_pubkey: String,
746    pub selection: MembershipConflictSelection,
747    pub replacement_grant: MembershipGrantId,
748    pub replacement_membership: GrantStreamAnchor,
749    pub replacement_acceptance: OwnerConflictResolutionAcceptance,
750    pub signature: String,
751}
752
753impl StoreMembershipConflictResolution {
754    fn canonical_bytes(&self) -> Vec<u8> {
755        #[derive(Serialize)]
756        struct Signed<'a> {
757            domain: &'static str,
758            version: u32,
759            store_root_hash: ObjectHash,
760            conflict_hash: ObjectHash,
761            conflicting_heads: &'a [MembershipHeadRef],
762            retired_owner_grants: &'a BTreeSet<MembershipGrantId>,
763            retirement_barriers:
764                &'a BTreeMap<MembershipGrantId, MergeMembershipGrantRetirementBarrier>,
765            resolver_pubkey: &'a str,
766            selection: &'a MembershipConflictSelection,
767            replacement_grant: &'a MembershipGrantId,
768            replacement_membership: &'a GrantStreamAnchor,
769            replacement_acceptance: &'a OwnerConflictResolutionAcceptance,
770        }
771        serde_json::to_vec(&Signed {
772            domain: "coven.store-membership-conflict-resolution.v1",
773            version: self.version,
774            store_root_hash: self.store_root_hash,
775            conflict_hash: self.conflict_hash,
776            conflicting_heads: &self.conflicting_heads,
777            retired_owner_grants: &self.retired_owner_grants,
778            retirement_barriers: &self.retirement_barriers,
779            resolver_pubkey: &self.resolver_pubkey,
780            selection: &self.selection,
781            replacement_grant: &self.replacement_grant,
782            replacement_membership: &self.replacement_membership,
783            replacement_acceptance: &self.replacement_acceptance,
784        })
785        .expect("Store membership resolution serialization cannot fail")
786    }
787
788    pub fn resolution_hash(&self) -> ObjectHash {
789        ObjectHash::digest(
790            &serde_json::to_vec(self)
791                .expect("Store membership resolution serialization cannot fail"),
792        )
793    }
794
795    pub fn resolution_ref(&self, object: ExactObjectRef) -> StoreMembershipConflictResolutionRef {
796        StoreMembershipConflictResolutionRef {
797            conflict_hash: self.conflict_hash,
798            resolver_pubkey: self.resolver_pubkey.clone(),
799            resolution_hash: self.resolution_hash(),
800            object,
801        }
802    }
803
804    pub fn verify_signature(&self) -> bool {
805        self.version == STORE_PROTOCOL_VERSION
806            && self.replacement_grant
807                == derive_store_resolution_grant(&self.conflict_hash, &self.resolver_pubkey)
808            && self.replacement_acceptance.store_root_hash == self.store_root_hash
809            && self.replacement_acceptance.owner_grant == self.replacement_grant
810            && self.replacement_acceptance.membership == self.replacement_membership
811            && keys::verify_signature_hex(
812                &self.resolver_pubkey,
813                &self.signature,
814                &self.canonical_bytes(),
815            )
816    }
817
818    pub fn verify_against(
819        &self,
820        store_root_hash: ObjectHash,
821        conflict: &MembershipConflict,
822    ) -> bool {
823        let (conflict_hash, heads, expected_retired, known_grants, resolver_is_owner) =
824            match (conflict, &self.selection) {
825                (
826                    MembershipConflict::ConcurrentMemberAssignments {
827                        conflict_hash,
828                        heads,
829                        conflicting_grants,
830                        uncontested_grants,
831                        grants,
832                        ..
833                    },
834                    MembershipConflictSelection::MemberAssignment { grant },
835                ) => (
836                    conflict_hash,
837                    heads,
838                    uncontested_grants
839                        .iter()
840                        .filter_map(|(grant, record)| {
841                            (record.member_pubkey == self.resolver_pubkey && record.role.is_owner())
842                                .then_some(grant.clone())
843                        })
844                        .collect(),
845                    grants.keys().cloned().collect::<BTreeSet<_>>(),
846                    conflicting_grants.contains_key(grant)
847                        && uncontested_grants.values().any(|record| {
848                            record.member_pubkey == self.resolver_pubkey && record.role.is_owner()
849                        }),
850                ),
851                (
852                    MembershipConflict::RevocationCycle {
853                        conflict_hash,
854                        heads,
855                        involved_owner_grants,
856                        maximal_valid_branches,
857                        ..
858                    },
859                    MembershipConflictSelection::RevocationBranch {
860                        heads: selected_heads,
861                    },
862                ) => {
863                    let Some(branch) = maximal_valid_branches
864                        .iter()
865                        .find(|branch| branch.heads == *selected_heads)
866                    else {
867                        return false;
868                    };
869                    let mut retired = involved_owner_grants.clone();
870                    retired.extend(branch.active_grants().filter_map(|(grant, record)| {
871                        (record.member_pubkey == self.resolver_pubkey && record.role.is_owner())
872                            .then_some(grant.clone())
873                    }));
874                    (
875                        conflict_hash,
876                        heads,
877                        retired,
878                        maximal_valid_branches
879                            .iter()
880                            .flat_map(|branch| branch.grants.keys().cloned())
881                            .collect(),
882                        branch.active_grants().any(|(_, record)| {
883                            record.member_pubkey == self.resolver_pubkey && record.role.is_owner()
884                        }),
885                    )
886                }
887                _ => return false,
888            };
889        self.version == STORE_PROTOCOL_VERSION
890            && self.store_root_hash == store_root_hash
891            && self.conflict_hash == *conflict_hash
892            && self.conflicting_heads == *heads
893            && self.retired_owner_grants == expected_retired
894            && self.retirement_barriers.len() == known_grants.len()
895            && self
896                .retirement_barriers
897                .keys()
898                .all(|grant| known_grants.contains(grant))
899            && self.replacement_grant
900                == derive_store_resolution_grant(conflict_hash, &self.resolver_pubkey)
901            && resolver_is_owner
902            && self.verify_signature()
903    }
904}
905
906pub fn derive_store_resolution_grant(
907    conflict_hash: &ObjectHash,
908    resolver_pubkey: &str,
909) -> MembershipGrantId {
910    MembershipGrantId(ObjectHash::digest(
911        format!("coven.store-membership-resolution-grant.v1\0{conflict_hash}\0{resolver_pubkey}")
912            .as_bytes(),
913    ))
914}
915
916fn conflict_retirement_barriers(
917    records: BTreeMap<MembershipGrantId, MembershipGrantRecord>,
918    effective_frontier: Vec<MembershipCoord>,
919    device_state: &StoreDeviceStateRef,
920) -> Result<BTreeMap<MembershipGrantId, MergeMembershipGrantRetirementBarrier>, MembershipError> {
921    let recovery = device_state.recovery();
922    records
923        .into_iter()
924        .map(|(grant, record)| {
925            let mut observed_streams = effective_frontier
926                .iter()
927                .filter(|coord| coord.author_owner_grant == grant)
928                .cloned()
929                .collect::<Vec<_>>();
930            observed_streams.sort_by_key(MembershipCoord::stream_key);
931            observed_streams.dedup_by_key(|coord| coord.stream_key());
932            let author_streams = StoreGrantStreamBarrier { observed_streams };
933            let barrier = if record.role.is_owner() {
934                let cursor = recovery
935                    .iter()
936                    .find(|cursor| cursor.owner_grant == grant)
937                    .cloned()
938                    .ok_or(MembershipError::MissingOwnerRecoveryState)?;
939                MergeMembershipGrantRetirementBarrier::Owner {
940                    barrier: MergeStoreOwnerGrantBarrier {
941                        author_streams,
942                        recovery: cursor,
943                    },
944                }
945            } else {
946                MergeMembershipGrantRetirementBarrier::NonOwner { author_streams }
947            };
948            Ok((grant, barrier))
949        })
950        .collect()
951}
952
953pub fn resolve_store_membership_conflict(
954    store_root_hash: ObjectHash,
955    conflict: &MembershipConflict,
956    resolutions: &[(
957        StoreMembershipConflictResolutionRef,
958        StoreMembershipConflictResolution,
959    )],
960) -> Result<ResolvedStoreMembership, MembershipError> {
961    if resolutions.is_empty() {
962        return Err(MembershipError::InvalidConflictResolution);
963    }
964    let mut by_resolver = BTreeMap::new();
965    let mut retired_owner_grants = BTreeSet::new();
966    for (_, resolution) in resolutions {
967        if !resolution.verify_against(store_root_hash, conflict) {
968            return Err(MembershipError::InvalidConflictResolution);
969        }
970        if let Some(existing) = by_resolver.insert(
971            resolution.resolver_pubkey.clone(),
972            resolution.resolution_hash(),
973        ) {
974            if existing != resolution.resolution_hash() {
975                return Err(MembershipError::InvalidConflictResolution);
976            }
977            continue;
978        }
979        retired_owner_grants.extend(resolution.retired_owner_grants.iter().cloned());
980    }
981    let (mut grants, known_records, provider_admin) = match conflict {
982        MembershipConflict::ConcurrentMemberAssignments {
983            conflicting_grants,
984            grants,
985            provider_admin,
986            ..
987        } => {
988            let selected = resolutions
989                .iter()
990                .filter_map(|(_, resolution)| match &resolution.selection {
991                    MembershipConflictSelection::MemberAssignment { grant } => Some(grant.clone()),
992                    MembershipConflictSelection::RevocationBranch { .. } => None,
993                })
994                .collect::<BTreeSet<_>>();
995            let retained = (selected.len() == 1)
996                .then(|| selected.first().cloned())
997                .flatten();
998            let mut resolved = grants.clone();
999            for (grant, record) in conflicting_grants {
1000                if retained.as_ref() == Some(grant) {
1001                    continue;
1002                }
1003                let retirements = assignment_conflict_retirements(resolutions, grant)?;
1004                resolved.insert(
1005                    grant.clone(),
1006                    GrantState::Tombstoned {
1007                        record: record.clone(),
1008                        retirements,
1009                    },
1010                );
1011            }
1012            (
1013                resolved,
1014                grants
1015                    .iter()
1016                    .map(|(grant, state)| (grant.clone(), state.record().clone()))
1017                    .collect::<BTreeMap<_, _>>(),
1018                provider_admin.clone(),
1019            )
1020        }
1021        MembershipConflict::RevocationCycle {
1022            maximal_valid_branches,
1023            ..
1024        } => {
1025            let mut selected_branches = Vec::new();
1026            for (_, resolution) in resolutions {
1027                let MembershipConflictSelection::RevocationBranch {
1028                    heads: selected_heads,
1029                } = &resolution.selection
1030                else {
1031                    return Err(MembershipError::InvalidConflictResolution);
1032                };
1033                let branch = maximal_valid_branches
1034                    .iter()
1035                    .find(|branch| branch.heads == *selected_heads)
1036                    .ok_or(MembershipError::InvalidConflictResolution)?;
1037                if !selected_branches
1038                    .iter()
1039                    .any(|selected: &&StoreMembershipBranch| selected.heads == branch.heads)
1040                {
1041                    selected_branches.push(branch);
1042                }
1043            }
1044            let (first_branch, other_branches) = selected_branches
1045                .split_first()
1046                .ok_or(MembershipError::InvalidConflictResolution)?;
1047            let mut resolved = first_branch
1048                .active_grants()
1049                .filter(|(grant, _)| !retired_owner_grants.contains(*grant))
1050                .filter(|(grant, record)| {
1051                    other_branches.iter().all(|branch| {
1052                        branch.grants.get(*grant).and_then(GrantState::active) == Some(*record)
1053                    })
1054                })
1055                .map(|(grant, record)| {
1056                    (
1057                        grant.clone(),
1058                        GrantState::Active {
1059                            record: record.clone(),
1060                        },
1061                    )
1062                })
1063                .collect::<BTreeMap<_, _>>();
1064            let known_records = maximal_valid_branches
1065                .iter()
1066                .flat_map(|branch| branch.grants.iter())
1067                .map(|(grant, state)| (grant.clone(), state.record().clone()))
1068                .collect::<BTreeMap<_, _>>();
1069            for branch in maximal_valid_branches {
1070                for (grant, state) in &branch.grants {
1071                    let GrantState::Tombstoned {
1072                        record,
1073                        retirements,
1074                    } = state
1075                    else {
1076                        continue;
1077                    };
1078                    match resolved.entry(grant.clone()) {
1079                        std::collections::btree_map::Entry::Vacant(entry) => {
1080                            entry.insert(state.clone());
1081                        }
1082                        std::collections::btree_map::Entry::Occupied(mut entry) => {
1083                            if entry.get().record() != record {
1084                                return Err(MembershipError::InvalidConflictResolution);
1085                            }
1086                            let current = entry.get_mut();
1087                            let mut merged = retirements.clone();
1088                            if let Some(current_retirements) = current.retirements() {
1089                                merged.extend(current_retirements.iter().cloned());
1090                            }
1091                            *current = GrantState::Tombstoned {
1092                                record: record.clone(),
1093                                retirements: merged,
1094                            };
1095                        }
1096                    }
1097                }
1098            }
1099            for branch in maximal_valid_branches {
1100                for (grant, record) in branch.active_grants() {
1101                    if resolved.get(grant).and_then(GrantState::active).is_some() {
1102                        continue;
1103                    }
1104                    let resolution_retirements =
1105                        conflict_resolution_retirements(resolutions, grant)?;
1106                    match resolved.entry(grant.clone()) {
1107                        std::collections::btree_map::Entry::Vacant(entry) => {
1108                            entry.insert(GrantState::Tombstoned {
1109                                record: record.clone(),
1110                                retirements: resolution_retirements.clone(),
1111                            });
1112                        }
1113                        std::collections::btree_map::Entry::Occupied(mut entry) => {
1114                            if entry.get().record() != record {
1115                                return Err(MembershipError::InvalidConflictResolution);
1116                            }
1117                            let GrantState::Tombstoned { retirements, .. } = entry.get_mut() else {
1118                                unreachable!("active conflict grant was handled above")
1119                            };
1120                            retirements.extend(resolution_retirements.iter().cloned());
1121                        }
1122                    }
1123                }
1124            }
1125            let provider_admin = super::provider::ProviderAdminResolution::Resolved(
1126                super::provider::ProviderAdminState::merge(
1127                    selected_branches
1128                        .iter()
1129                        .map(|branch| branch.provider_admin.combined_state().clone()),
1130                )?,
1131            );
1132            (resolved, known_records, provider_admin)
1133        }
1134    };
1135    for (reference, resolution) in resolutions {
1136        for retired in &resolution.retired_owner_grants {
1137            let record = known_records
1138                .get(retired)
1139                .ok_or(MembershipError::InvalidConflictResolution)?
1140                .clone();
1141            let barrier = resolution
1142                .retirement_barriers
1143                .get(retired)
1144                .cloned()
1145                .ok_or(MembershipError::InvalidConflictResolution)?;
1146            let retirement = MembershipGrantRetirement::ConflictResolution {
1147                authority: reference.clone(),
1148                barrier,
1149            };
1150            match grants.entry(retired.clone()) {
1151                std::collections::btree_map::Entry::Vacant(entry) => {
1152                    entry.insert(GrantState::Tombstoned {
1153                        record,
1154                        retirements: GrantRetirements::new(retirement),
1155                    });
1156                }
1157                std::collections::btree_map::Entry::Occupied(mut entry) => {
1158                    if entry.get().record() != &record {
1159                        return Err(MembershipError::InvalidConflictResolution);
1160                    }
1161                    let mut retirements = match entry.get().retirements() {
1162                        Some(retirements) => retirements.clone(),
1163                        None => GrantRetirements::new(retirement.clone()),
1164                    };
1165                    retirements.insert(retirement);
1166                    *entry.get_mut() = GrantState::Tombstoned {
1167                        record,
1168                        retirements,
1169                    };
1170                }
1171            }
1172        }
1173    }
1174    for (reference, resolution) in resolutions {
1175        let record = MembershipGrantRecord {
1176            member_pubkey: resolution.resolver_pubkey.clone(),
1177            role: StoreMembershipRoleGrant::Owner {
1178                recovery: OwnerRecoveryAnchorRef::ConflictResolution {
1179                    acceptance: Box::new(resolution.replacement_acceptance.clone()),
1180                },
1181            },
1182            provider_account_email: None,
1183            creation_authority: MembershipGrantCreationAuthority::ConflictResolution(
1184                reference.clone(),
1185            ),
1186        };
1187        if grants
1188            .insert(
1189                resolution.replacement_grant.clone(),
1190                GrantState::Active {
1191                    record: record.clone(),
1192                },
1193            )
1194            .is_some_and(|current| current.active() != Some(&record))
1195        {
1196            return Err(MembershipError::InvalidConflictResolution);
1197        }
1198    }
1199    let mut members = BTreeSet::new();
1200    if !active_membership_grants(&grants).any(|(_, record)| record.role.is_owner())
1201        || active_membership_grants(&grants)
1202            .any(|(_, record)| !members.insert(record.member_pubkey.clone()))
1203    {
1204        return Err(MembershipError::InvalidConflictResolution);
1205    }
1206    Ok(ResolvedStoreMembership {
1207        state_hash: store_membership_state_hash(&grants, &provider_admin),
1208        grants,
1209        provider_admin,
1210    })
1211}
1212
1213fn conflict_resolution_retirements(
1214    resolutions: &[(
1215        StoreMembershipConflictResolutionRef,
1216        StoreMembershipConflictResolution,
1217    )],
1218    grant: &MembershipGrantId,
1219) -> Result<GrantRetirements<MembershipGrantRetirement>, MembershipError> {
1220    let mut retirements = resolutions.iter().map(|(reference, resolution)| {
1221        resolution
1222            .retirement_barriers
1223            .get(grant)
1224            .cloned()
1225            .map(|barrier| MembershipGrantRetirement::ConflictResolution {
1226                authority: reference.clone(),
1227                barrier,
1228            })
1229            .ok_or(MembershipError::InvalidConflictResolution)
1230    });
1231    let first = retirements
1232        .next()
1233        .ok_or(MembershipError::InvalidConflictResolution)??;
1234    let mut result = GrantRetirements::new(first);
1235    for retirement in retirements {
1236        result.insert(retirement?);
1237    }
1238    Ok(result)
1239}
1240
1241fn assignment_conflict_retirements(
1242    resolutions: &[(
1243        StoreMembershipConflictResolutionRef,
1244        StoreMembershipConflictResolution,
1245    )],
1246    grant: &MembershipGrantId,
1247) -> Result<GrantRetirements<MembershipGrantRetirement>, MembershipError> {
1248    let mut retirements = resolutions
1249        .iter()
1250        .filter(|(_, resolution)| {
1251            !matches!(
1252                &resolution.selection,
1253                MembershipConflictSelection::MemberAssignment { grant: selected }
1254                    if selected == grant
1255            )
1256        })
1257        .map(|(reference, resolution)| {
1258            resolution
1259                .retirement_barriers
1260                .get(grant)
1261                .cloned()
1262                .map(|barrier| MembershipGrantRetirement::ConflictResolution {
1263                    authority: reference.clone(),
1264                    barrier,
1265                })
1266                .ok_or(MembershipError::InvalidConflictResolution)
1267        });
1268    let first = retirements
1269        .next()
1270        .ok_or(MembershipError::InvalidConflictResolution)??;
1271    let mut result = GrantRetirements::new(first);
1272    for retirement in retirements {
1273        result.insert(retirement?);
1274    }
1275    Ok(result)
1276}
1277
1278#[derive(Debug, Clone, Default)]
1279struct CausalState {
1280    grants:
1281        BTreeMap<MembershipGrantId, GrantState<MembershipGrantRecord, MembershipGrantRetirement>>,
1282}
1283
1284#[derive(Debug, Clone)]
1285pub struct MembershipChain {
1286    entries: Vec<MembershipEntry>,
1287    coords: Vec<MembershipCoord>,
1288    state: CausalState,
1289    included: BTreeSet<MembershipCoord>,
1290    status: Option<MembershipStatus>,
1291    head_refs: Vec<MembershipHeadRef>,
1292    resolution_checkpoint: Option<MembershipResolutionCheckpoint>,
1293    provider_admin_genesis: super::provider::ProviderAdminState,
1294}
1295
1296#[derive(Debug, Clone)]
1297struct MembershipResolutionCheckpoint {
1298    raw_heads: Vec<MembershipCoord>,
1299    effective_frontier: Vec<MembershipCoord>,
1300    grants:
1301        BTreeMap<MembershipGrantId, GrantState<MembershipGrantRecord, MembershipGrantRetirement>>,
1302    grant_anchors: BTreeMap<MembershipGrantId, GrantStreamAnchor>,
1303    included: BTreeSet<MembershipCoord>,
1304    resolutions: Vec<StoreMembershipConflictResolutionRef>,
1305    provider_admin: super::provider::ProviderAdminState,
1306}
1307
1308#[cfg(any(test, feature = "test-utils"))]
1309fn test_provider_admin_genesis(
1310    entries: &[MembershipEntry],
1311) -> Result<super::provider::ProviderAdminState, MembershipError> {
1312    let founder = entries
1313        .iter()
1314        .find_map(|entry| match &entry.change {
1315            MembershipChange::Founder { provider_admin, .. } => Some((entry, provider_admin)),
1316            _ => None,
1317        })
1318        .ok_or(MembershipError::InvalidFounder)?;
1319    let root_bytes = founder.0.store_id.as_bytes();
1320    let root = StoreRootRef {
1321        store_root_id: ObjectHash::digest(
1322            format!("{} test root id", founder.0.store_id).as_bytes(),
1323        ),
1324        store_root_hash: ObjectHash::digest(root_bytes),
1325        object: ExactObjectRef::new(
1326            crate::storage::cloud::ObjectSlot::logical(format!(
1327                "store-v1/test/{}/root.json",
1328                founder.0.store_id
1329            ))
1330            .expect("valid test root slot"),
1331            root_bytes.len() as u64,
1332            ObjectHash::digest(root_bytes),
1333        ),
1334    };
1335    let registration: StoreDeviceRegistrationRef =
1336        serde_json::from_value(serde_json::json!({
1337            "device_id": ObjectHash::digest(format!("{} founder device", founder.0.store_id).as_bytes()),
1338            "registration_hash": ObjectHash::digest(format!("{} founder registration", founder.0.store_id).as_bytes()),
1339            "object": {
1340                "slot": {"logical_key": format!("store-v1/test/{}/registration.json", founder.0.store_id), "physical": {"kind": "logical_key"}},
1341                "stored_size": 1,
1342                "stored_hash": ObjectHash::digest(format!("{} founder registration object", founder.0.store_id).as_bytes()),
1343            }
1344        }))
1345        .expect("valid test founder registration reference");
1346    Ok(super::provider::ProviderAdminState::founder_from_root(
1347        root,
1348        registration,
1349        founder.1,
1350    ))
1351}
1352
1353impl MembershipChain {
1354    #[cfg(any(test, feature = "test-utils"))]
1355    pub fn from_entries(entries: Vec<MembershipEntry>) -> Result<Self, MembershipError> {
1356        let provider_admin = test_provider_admin_genesis(&entries)?;
1357        Self::from_entries_with_coords_and_provider_admin(
1358            entries
1359                .into_iter()
1360                .map(|entry| (entry.coord(), entry))
1361                .collect(),
1362            provider_admin,
1363        )
1364    }
1365
1366    #[cfg(any(test, feature = "test-utils"))]
1367    pub fn from_entries_with_coords(
1368        entries: Vec<(MembershipCoord, MembershipEntry)>,
1369    ) -> Result<Self, MembershipError> {
1370        let values = entries
1371            .iter()
1372            .map(|(_, entry)| entry.clone())
1373            .collect::<Vec<_>>();
1374        let provider_admin = test_provider_admin_genesis(&values)?;
1375        Self::from_entries_with_coords_and_provider_admin(entries, provider_admin)
1376    }
1377
1378    #[cfg(any(test, feature = "test-utils"))]
1379    pub fn from_entries_with_coords_and_heads(
1380        entries: Vec<(MembershipCoord, MembershipEntry)>,
1381        heads: Vec<(MembershipHeadRef, AuthorHead)>,
1382    ) -> Result<Self, MembershipError> {
1383        let values = entries
1384            .iter()
1385            .map(|(_, entry)| entry.clone())
1386            .collect::<Vec<_>>();
1387        let provider_admin = test_provider_admin_genesis(&values)?;
1388        Self::from_entries_with_coords_and_heads_and_provider_admin(entries, heads, provider_admin)
1389    }
1390
1391    pub fn from_entries_with_coords_and_provider_admin(
1392        entries: Vec<(MembershipCoord, MembershipEntry)>,
1393        provider_admin: super::provider::ProviderAdminState,
1394    ) -> Result<Self, MembershipError> {
1395        Self::from_entries_with_coords_and_head_refs(entries, Vec::new(), provider_admin)
1396    }
1397
1398    pub fn from_entries_with_coords_and_heads_and_provider_admin(
1399        entries: Vec<(MembershipCoord, MembershipEntry)>,
1400        heads: Vec<(MembershipHeadRef, AuthorHead)>,
1401        provider_admin: super::provider::ProviderAdminState,
1402    ) -> Result<Self, MembershipError> {
1403        let expected_store = entries
1404            .first()
1405            .map(|(_, entry)| entry.store_id.as_str())
1406            .ok_or(MembershipError::EmptyChain)?;
1407        if heads.iter().any(|(reference, head)| {
1408            reference.head_hash != head.head_hash()
1409                || head.store_id != expected_store
1410                || entries
1411                    .iter()
1412                    .find(|(coord, _)| *coord == head.entry_coord())
1413                    .is_none_or(|(_, entry)| head.body.resolutions != entry.resolution_dependencies)
1414        }) {
1415            return Err(MembershipError::MissingConflictHeads);
1416        }
1417        Self::from_entries_with_coords_and_head_refs(
1418            entries,
1419            heads.into_iter().map(|(reference, _)| reference).collect(),
1420            provider_admin,
1421        )
1422    }
1423
1424    fn from_entries_with_coords_and_head_refs(
1425        entries: Vec<(MembershipCoord, MembershipEntry)>,
1426        head_refs: Vec<MembershipHeadRef>,
1427        provider_admin_genesis: super::provider::ProviderAdminState,
1428    ) -> Result<Self, MembershipError> {
1429        if entries.is_empty() {
1430            return Err(MembershipError::EmptyChain);
1431        }
1432        let (coords, entries): (Vec<_>, Vec<_>) = entries.into_iter().unzip();
1433        let mut chain = Self {
1434            entries,
1435            coords,
1436            state: CausalState::default(),
1437            included: BTreeSet::new(),
1438            status: None,
1439            head_refs,
1440            resolution_checkpoint: None,
1441            provider_admin_genesis,
1442        };
1443        chain.rebuild()?;
1444        Ok(chain)
1445    }
1446
1447    pub fn entries(&self) -> &[MembershipEntry] {
1448        &self.entries
1449    }
1450
1451    pub fn status(&self) -> &MembershipStatus {
1452        self.status
1453            .as_ref()
1454            .expect("a loaded membership chain always has status")
1455    }
1456
1457    pub fn head_refs(&self) -> &[MembershipHeadRef] {
1458        &self.head_refs
1459    }
1460
1461    pub(crate) fn head_ref_for_stream(
1462        &self,
1463        author: &str,
1464        grant: &MembershipGrantId,
1465        stream_id: AuthorStreamId,
1466    ) -> Option<&MembershipHeadRef> {
1467        self.head_refs.iter().find(|reference| {
1468            reference.coord.author_pubkey == author
1469                && reference.coord.author_owner_grant == *grant
1470                && reference.coord.stream_id == stream_id
1471        })
1472    }
1473
1474    pub(crate) fn membership_anchor(
1475        &self,
1476        grant: &MembershipGrantId,
1477    ) -> Option<&GrantStreamAnchor> {
1478        self.entries
1479            .iter()
1480            .find_map(|entry| match &entry.change {
1481                MembershipChange::Founder {
1482                    owner_grant_id,
1483                    membership,
1484                    ..
1485                } if owner_grant_id == grant => Some(membership),
1486                MembershipChange::SetMember {
1487                    grant_id,
1488                    membership: Some(membership),
1489                    ..
1490                } if grant_id == grant => Some(membership),
1491                _ => None,
1492            })
1493            .or_else(|| {
1494                self.resolution_checkpoint
1495                    .as_ref()?
1496                    .grant_anchors
1497                    .get(grant)
1498            })
1499    }
1500
1501    pub(crate) fn membership_stream_id(&self, grant: &MembershipGrantId) -> Option<AuthorStreamId> {
1502        let record = self.state.grants.get(grant)?.record();
1503        store_membership_anchor_stream(&record.member_pubkey, grant, self.membership_anchor(grant)?)
1504    }
1505
1506    pub(crate) fn activated_membership_streams(
1507        &self,
1508    ) -> Vec<(MembershipStreamKey, GrantStreamAnchor)> {
1509        let mut streams = self
1510            .state
1511            .grants
1512            .iter()
1513            .filter_map(|(grant, state)| {
1514                let record = state.record();
1515                let anchor = self.membership_anchor(grant)?.clone();
1516                let stream_id = self.membership_stream_id(grant)?;
1517                Some((
1518                    MembershipStreamKey {
1519                        author_pubkey: record.member_pubkey.clone(),
1520                        author_owner_grant: grant.clone(),
1521                        stream_id,
1522                    },
1523                    anchor,
1524                ))
1525            })
1526            .collect::<BTreeMap<_, _>>();
1527        let mut included = self.included.clone();
1528        if let MembershipStatus::Conflict(MembershipConflict::RevocationCycle {
1529            maximal_valid_branches,
1530            ..
1531        }) = self.status()
1532        {
1533            for branch in maximal_valid_branches {
1534                included.extend(membership_history_closure(
1535                    &self.entries,
1536                    &branch.effective_frontier,
1537                ));
1538            }
1539        }
1540        for (coord, entry) in self.entries_with_coords() {
1541            if !included.contains(coord) {
1542                continue;
1543            }
1544            let (owner_pubkey, grant, anchor) = match &entry.change {
1545                MembershipChange::SetMember {
1546                    user_pubkey,
1547                    role: StoreMembershipRoleGrant::Owner { .. },
1548                    grant_id,
1549                    membership: Some(membership),
1550                    ..
1551                } => (user_pubkey, grant_id, membership),
1552                _ => continue,
1553            };
1554            let stream_id = store_membership_anchor_stream(owner_pubkey, grant, anchor)
1555                .expect("validated Owner grant has a Store membership stream anchor");
1556            streams.insert(
1557                MembershipStreamKey {
1558                    author_pubkey: owner_pubkey.clone(),
1559                    author_owner_grant: grant.clone(),
1560                    stream_id,
1561                },
1562                anchor.clone(),
1563            );
1564        }
1565        streams.into_iter().collect()
1566    }
1567
1568    pub(crate) fn activate_head_ref(
1569        &mut self,
1570        reference: MembershipHeadRef,
1571    ) -> Result<(), MembershipError> {
1572        if !self.coords.contains(&reference.coord) {
1573            return Err(MembershipError::MissingConflictHeads);
1574        }
1575        let stream = reference.coord.stream_key();
1576        self.head_refs
1577            .retain(|current| current.coord.stream_key() != stream);
1578        self.head_refs.push(reference);
1579        self.head_refs.sort();
1580        self.rebuild()
1581    }
1582
1583    pub fn resolution_refs(&self) -> &[StoreMembershipConflictResolutionRef] {
1584        self.resolution_checkpoint
1585            .as_ref()
1586            .map_or(&[], |checkpoint| checkpoint.resolutions.as_slice())
1587    }
1588
1589    pub fn conflict(&self) -> Option<&MembershipConflict> {
1590        match self.status() {
1591            MembershipStatus::Resolved(_) => None,
1592            MembershipStatus::Conflict(conflict) => Some(conflict),
1593        }
1594    }
1595
1596    pub fn ensure_resolved(&self) -> Result<(), MembershipError> {
1597        match self.status() {
1598            MembershipStatus::Resolved(_) => Ok(()),
1599            MembershipStatus::Conflict(_) => Err(MembershipError::Conflict),
1600        }
1601    }
1602
1603    pub fn resolved_with(
1604        &self,
1605        store_root_hash: ObjectHash,
1606        resolutions: &[(
1607            StoreMembershipConflictResolutionRef,
1608            StoreMembershipConflictResolution,
1609        )],
1610    ) -> Result<ResolvedStoreMembership, MembershipError> {
1611        match self.status() {
1612            MembershipStatus::Resolved(resolved) if resolutions.is_empty() => Ok(resolved.clone()),
1613            MembershipStatus::Conflict(conflict) => {
1614                resolve_store_membership_conflict(store_root_hash, conflict, resolutions)
1615            }
1616            MembershipStatus::Resolved(_) => Err(MembershipError::InvalidConflictResolution),
1617        }
1618    }
1619
1620    pub fn signed_conflict_resolution(
1621        &self,
1622        store_root_hash: ObjectHash,
1623        selection: MembershipConflictSelection,
1624        replacement_membership: GrantStreamAnchor,
1625        replacement_acceptance: OwnerConflictResolutionAcceptance,
1626        signer: &UserKeypair,
1627    ) -> Result<StoreMembershipConflictResolution, MembershipError> {
1628        let MembershipStatus::Conflict(conflict) = self.status() else {
1629            return Err(MembershipError::Conflict);
1630        };
1631        let resolver_pubkey = keys::public_key_hex(signer);
1632        let (conflict_hash, heads, retired_owner_grants, records, effective_frontier) =
1633            match (conflict, &selection) {
1634                (
1635                    MembershipConflict::ConcurrentMemberAssignments {
1636                        conflict_hash,
1637                        heads,
1638                        effective_frontier,
1639                        conflicting_grants,
1640                        uncontested_grants,
1641                        grants,
1642                        ..
1643                    },
1644                    MembershipConflictSelection::MemberAssignment { grant },
1645                ) => {
1646                    if !conflicting_grants.contains_key(grant) {
1647                        return Err(MembershipError::InvalidConflictResolution);
1648                    }
1649                    let retired = uncontested_grants
1650                        .iter()
1651                        .filter_map(|(grant, record)| {
1652                            (record.member_pubkey == resolver_pubkey && record.role.is_owner())
1653                                .then_some(grant.clone())
1654                        })
1655                        .collect::<BTreeSet<_>>();
1656                    if retired.is_empty() {
1657                        return Err(MembershipError::SignerIsNotOwner(resolver_pubkey));
1658                    }
1659                    (
1660                        conflict_hash,
1661                        heads,
1662                        retired,
1663                        grants
1664                            .iter()
1665                            .map(|(grant, state)| (grant.clone(), state.record().clone()))
1666                            .collect(),
1667                        effective_frontier.clone(),
1668                    )
1669                }
1670                (
1671                    MembershipConflict::RevocationCycle {
1672                        conflict_hash,
1673                        heads,
1674                        involved_owner_grants,
1675                        maximal_valid_branches,
1676                        ..
1677                    },
1678                    MembershipConflictSelection::RevocationBranch {
1679                        heads: selected_heads,
1680                    },
1681                ) => {
1682                    let branch = maximal_valid_branches
1683                        .iter()
1684                        .find(|branch| branch.heads == *selected_heads)
1685                        .ok_or(MembershipError::InvalidConflictResolution)?;
1686                    let resolver_grants = branch
1687                        .active_grants()
1688                        .filter_map(|(grant, record)| {
1689                            (record.member_pubkey == resolver_pubkey && record.role.is_owner())
1690                                .then_some(grant.clone())
1691                        })
1692                        .collect::<BTreeSet<_>>();
1693                    if resolver_grants.is_empty() {
1694                        return Err(MembershipError::SignerIsNotOwner(resolver_pubkey));
1695                    }
1696                    let mut retired = involved_owner_grants.clone();
1697                    retired.extend(resolver_grants);
1698                    let records = maximal_valid_branches
1699                        .iter()
1700                        .flat_map(|branch| branch.grants.iter())
1701                        .map(|(grant, state)| (grant.clone(), state.record().clone()))
1702                        .collect();
1703                    let mut frontier = maximal_valid_branches
1704                        .iter()
1705                        .flat_map(|branch| branch.effective_frontier.iter().cloned())
1706                        .collect::<Vec<_>>();
1707                    frontier.sort();
1708                    frontier.dedup();
1709                    (conflict_hash, heads, retired, records, frontier)
1710                }
1711                _ => return Err(MembershipError::InvalidConflictResolution),
1712            };
1713        let replacement_grant = derive_store_resolution_grant(conflict_hash, &resolver_pubkey);
1714        let retirement_barriers = conflict_retirement_barriers(
1715            records,
1716            effective_frontier,
1717            &replacement_acceptance.device_state,
1718        )?;
1719        let mut resolution = StoreMembershipConflictResolution {
1720            version: STORE_PROTOCOL_VERSION,
1721            store_root_hash,
1722            conflict_hash: *conflict_hash,
1723            conflicting_heads: heads.clone(),
1724            retired_owner_grants,
1725            retirement_barriers,
1726            resolver_pubkey,
1727            selection,
1728            replacement_grant,
1729            replacement_membership,
1730            replacement_acceptance,
1731            signature: String::new(),
1732        };
1733        resolution.signature = keys::sign_hex(signer, &resolution.canonical_bytes()).1;
1734        Ok(resolution)
1735    }
1736
1737    pub fn entries_with_coords(
1738        &self,
1739    ) -> impl Iterator<Item = (&MembershipCoord, &MembershipEntry)> {
1740        self.coords.iter().zip(self.entries.iter())
1741    }
1742
1743    pub fn store_id(&self) -> Option<&str> {
1744        self.entries.first().map(|entry| entry.store_id.as_str())
1745    }
1746
1747    pub fn founder_coord(&self) -> Option<&MembershipCoord> {
1748        self.entries_with_coords().find_map(|(coord, entry)| {
1749            matches!(entry.change, MembershipChange::Founder { .. }).then_some(coord)
1750        })
1751    }
1752
1753    pub fn founder_entry(&self) -> Option<&MembershipEntry> {
1754        self.entries
1755            .iter()
1756            .find(|entry| matches!(entry.change, MembershipChange::Founder { .. }))
1757    }
1758
1759    pub fn founder_pubkey(&self) -> Option<&str> {
1760        self.founder_entry().and_then(|entry| match &entry.change {
1761            MembershipChange::Founder { owner_pubkey, .. } => Some(owner_pubkey.as_str()),
1762            MembershipChange::SetMember { .. }
1763            | MembershipChange::RemoveMember { .. }
1764            | MembershipChange::ProviderAdmin
1765            | MembershipChange::ResolutionActivation { .. } => None,
1766        })
1767    }
1768
1769    pub fn is_founded_by(&self, owner_pubkey: &str) -> bool {
1770        self.founder_pubkey() == Some(owner_pubkey)
1771    }
1772
1773    pub fn validate(&self) -> Result<(), MembershipError> {
1774        let mut rebuilt = self.clone();
1775        rebuilt.rebuild()
1776    }
1777
1778    pub fn add_entry(&mut self, entry: MembershipEntry) -> Result<(), MembershipError> {
1779        self.add_entry_at(entry.coord(), entry)
1780    }
1781
1782    pub fn add_entry_at(
1783        &mut self,
1784        coord: MembershipCoord,
1785        entry: MembershipEntry,
1786    ) -> Result<(), MembershipError> {
1787        self.entries.push(entry);
1788        self.coords.push(coord);
1789        if let Err(error) = self.rebuild() {
1790            self.entries.pop();
1791            self.coords.pop();
1792            self.rebuild().expect("previous membership chain validated");
1793            return Err(error);
1794        }
1795        Ok(())
1796    }
1797
1798    pub fn can_write_now(&self, pubkey: &str) -> bool {
1799        if self.conflict().is_some() {
1800            return false;
1801        }
1802        self.active_grants_for(pubkey)
1803            .iter()
1804            .any(|(_, record)| record.role.can_write())
1805    }
1806
1807    pub fn is_owner_now(&self, pubkey: &str) -> bool {
1808        if self.conflict().is_some() {
1809            return false;
1810        }
1811        self.active_grants_for(pubkey)
1812            .iter()
1813            .any(|(_, record)| record.role.is_owner())
1814    }
1815
1816    pub fn authorizes_write_at(&self, coord: &MembershipCoord, pubkey: &str) -> bool {
1817        self.active_grants_for(pubkey).iter().any(|(_, record)| {
1818            record.role.can_write()
1819                && record.creation_authority
1820                    == MembershipGrantCreationAuthority::Entry(coord.clone())
1821        })
1822    }
1823
1824    pub fn authorizes_write_authority(
1825        &self,
1826        authority: &MembershipGrantCreationAuthority,
1827        pubkey: &str,
1828    ) -> bool {
1829        let MembershipStatus::Resolved(resolved) = self.status() else {
1830            return false;
1831        };
1832        resolved.active_grants().any(|(_, record)| {
1833            record.member_pubkey == pubkey
1834                && record.role.can_write()
1835                && &record.creation_authority == authority
1836        })
1837    }
1838
1839    pub fn active_grant(&self, grant_id: &MembershipGrantId) -> Option<&MembershipGrantRecord> {
1840        let MembershipStatus::Resolved(resolved) = self.status() else {
1841            return None;
1842        };
1843        resolved.active_grant(grant_id)
1844    }
1845
1846    pub fn contains_coord(&self, expected: &MembershipCoord) -> bool {
1847        self.coords.iter().any(|coord| coord == expected)
1848    }
1849
1850    pub(crate) fn effectively_contains_coord(&self, expected: &MembershipCoord) -> bool {
1851        self.included.contains(expected)
1852    }
1853
1854    pub fn current_members(&self) -> Vec<(String, MemberRole)> {
1855        let mut members = BTreeMap::new();
1856        for state in self.state.grants.values() {
1857            let Some(record) = state.active() else {
1858                continue;
1859            };
1860            members.insert(record.member_pubkey.clone(), record.role.role());
1861        }
1862        members.into_iter().collect()
1863    }
1864
1865    pub(crate) fn contains_member_history(&self, pubkey: &str) -> bool {
1866        self.state
1867            .grants
1868            .values()
1869            .any(|state| state.record().member_pubkey == pubkey)
1870    }
1871
1872    pub fn active_wrapped_keys_for(&self, recipient_pubkey: &str) -> Vec<WrappedStoreKeyRef> {
1873        let active_grants = self.active_grant_ids(recipient_pubkey);
1874        self.entries_with_coords()
1875            .filter(|(coord, _)| self.included.contains(*coord))
1876            .flat_map(|(_, entry)| match &entry.change {
1877                MembershipChange::SetMember {
1878                    grant_id,
1879                    wrapped_key,
1880                    ..
1881                } if active_grants.contains(grant_id) => std::slice::from_ref(wrapped_key),
1882                MembershipChange::RemoveMember { wrapped_keys, .. } => wrapped_keys.as_slice(),
1883                MembershipChange::Founder { .. }
1884                | MembershipChange::SetMember { .. }
1885                | MembershipChange::ProviderAdmin
1886                | MembershipChange::ResolutionActivation { .. } => &[],
1887            })
1888            .filter(|reference| reference.recipient_pubkey == recipient_pubkey)
1889            .cloned()
1890            .collect::<BTreeSet<_>>()
1891            .into_iter()
1892            .collect()
1893    }
1894
1895    pub fn wrapped_key_authority_for(
1896        &self,
1897        recipient_pubkey: &str,
1898    ) -> Result<Vec<WrappedStoreKeyRef>, MembershipError> {
1899        let active_grants = self.active_grants_for(recipient_pubkey);
1900        for (index, (rotation_coord, entry)) in self
1901            .entries_with_coords()
1902            .enumerate()
1903            .filter(|(_, (coord, _))| self.included.contains(*coord))
1904        {
1905            let MembershipChange::RemoveMember { wrapped_keys, .. } = &entry.change else {
1906                continue;
1907            };
1908            if wrapped_keys
1909                .iter()
1910                .any(|reference| reference.recipient_pubkey == recipient_pubkey)
1911            {
1912                continue;
1913            }
1914            let rotation_generation = wrapped_keys
1915                .first()
1916                .ok_or(MembershipError::InvalidWrappedKeys(index))?
1917                .generation;
1918            let covered_by_later_grant = !active_grants.is_empty()
1919                && active_grants.iter().all(|(active_grant, _)| {
1920                    let Some((_, creation)) = self.entries_with_coords().find(|(_, entry)| {
1921                        matches!(
1922                            &entry.change,
1923                            MembershipChange::SetMember { grant_id, .. }
1924                                if grant_id == *active_grant
1925                        )
1926                    }) else {
1927                        return false;
1928                    };
1929                    let MembershipChange::SetMember { wrapped_key, .. } = &creation.change else {
1930                        return false;
1931                    };
1932                    wrapped_key.generation >= rotation_generation
1933                        && membership_history_closure(&self.entries, &creation.dependencies)
1934                            .contains(rotation_coord)
1935                });
1936            if !covered_by_later_grant {
1937                return Err(MembershipError::MissingWrappedKeyCoverage {
1938                    recipient_pubkey: recipient_pubkey.to_string(),
1939                    rotation: Box::new(rotation_coord.clone()),
1940                });
1941            }
1942        }
1943        Ok(self.active_wrapped_keys_for(recipient_pubkey))
1944    }
1945
1946    pub fn current_member_provider_email(&self, pubkey: &str) -> Option<&str> {
1947        self.active_grants_for(pubkey)
1948            .into_iter()
1949            .next()
1950            .and_then(|(_, record)| record.provider_account_email.as_deref())
1951    }
1952
1953    pub fn write_grant_coord(&self, pubkey: &str) -> Option<MembershipCoord> {
1954        self.active_grants_for(pubkey)
1955            .into_iter()
1956            .find(|(_, record)| record.role.can_write())
1957            .and_then(|(_, record)| match &record.creation_authority {
1958                MembershipGrantCreationAuthority::Entry(coord) => Some(coord.clone()),
1959                MembershipGrantCreationAuthority::ConflictResolution(_) => None,
1960            })
1961    }
1962
1963    pub fn write_grant_authority(&self, pubkey: &str) -> Option<MembershipGrantCreationAuthority> {
1964        self.active_grants_for(pubkey)
1965            .into_iter()
1966            .find(|(_, record)| record.role.can_write())
1967            .map(|(_, record)| record.creation_authority.clone())
1968    }
1969
1970    pub fn active_grant_ids(&self, pubkey: &str) -> BTreeSet<MembershipGrantId> {
1971        self.active_grants_for(pubkey)
1972            .into_iter()
1973            .map(|(grant, _)| grant.clone())
1974            .collect()
1975    }
1976
1977    pub fn active_owner_grant(&self, pubkey: &str) -> Option<MembershipGrantId> {
1978        self.active_grants_for(pubkey)
1979            .into_iter()
1980            .find(|(_, record)| record.role.is_owner())
1981            .map(|(grant, _)| grant.clone())
1982    }
1983
1984    pub(crate) fn reusable_author_streams(
1985        &self,
1986        author_pubkey: &str,
1987        grant: &MembershipGrantId,
1988    ) -> BTreeSet<AuthorStreamId> {
1989        self.effective_frontier()
1990            .into_iter()
1991            .filter(|coord| {
1992                coord.author_pubkey == author_pubkey
1993                    && coord.author_owner_grant == *grant
1994                    && self.raw_stream_tip(author_pubkey, grant, coord.stream_id)
1995                        == Some(coord.clone())
1996            })
1997            .map(|coord| coord.stream_id)
1998            .collect()
1999    }
2000
2001    /// Raw signed coverage: the greatest loaded coordinate in every stream,
2002    /// including suffixes removed by causal pruning.
2003    pub fn author_heads(&self) -> Vec<MembershipCoord> {
2004        self.frontier_from_coords(self.coords.iter())
2005    }
2006
2007    /// Effective authoring frontier after causal pruning.
2008    pub fn effective_frontier(&self) -> Vec<MembershipCoord> {
2009        self.frontier_from_coords(
2010            self.coords
2011                .iter()
2012                .filter(|coord| self.included.contains(*coord)),
2013        )
2014    }
2015
2016    pub(crate) fn causally_includes(&self, predecessor: &MembershipChain) -> bool {
2017        predecessor.included.is_subset(&self.included)
2018            && predecessor
2019                .resolution_refs()
2020                .iter()
2021                .all(|reference| self.resolution_refs().binary_search(reference).is_ok())
2022    }
2023
2024    fn frontier_from_coords<'a>(
2025        &self,
2026        coords: impl Iterator<Item = &'a MembershipCoord>,
2027    ) -> Vec<MembershipCoord> {
2028        let mut heads = BTreeMap::<MembershipStreamKey, MembershipCoord>::new();
2029        for coord in coords {
2030            heads
2031                .entry(coord.stream_key())
2032                .and_modify(|current| {
2033                    if coord.seq > current.seq {
2034                        *current = coord.clone();
2035                    }
2036                })
2037                .or_insert_with(|| coord.clone());
2038        }
2039        heads.into_values().collect()
2040    }
2041
2042    pub fn stream_tip(
2043        &self,
2044        author_pubkey: &str,
2045        grant: &MembershipGrantId,
2046        stream_id: AuthorStreamId,
2047    ) -> Option<MembershipCoord> {
2048        self.effective_frontier().into_iter().find(|coord| {
2049            coord.author_pubkey == author_pubkey
2050                && coord.author_owner_grant == *grant
2051                && coord.stream_id == stream_id
2052        })
2053    }
2054
2055    pub fn raw_stream_tip(
2056        &self,
2057        author_pubkey: &str,
2058        grant: &MembershipGrantId,
2059        stream_id: AuthorStreamId,
2060    ) -> Option<MembershipCoord> {
2061        self.coords
2062            .iter()
2063            .filter(|coord| {
2064                coord.author_pubkey == author_pubkey
2065                    && coord.author_owner_grant == *grant
2066                    && coord.stream_id == stream_id
2067            })
2068            .max_by_key(|coord| coord.seq)
2069            .cloned()
2070    }
2071
2072    pub(crate) fn next_member_grant_id_in_stream(
2073        &self,
2074        signer: &UserKeypair,
2075        stream_id: AuthorStreamId,
2076        user_pubkey: &str,
2077    ) -> Result<MembershipGrantId, MembershipError> {
2078        let author = keys::public_key_hex(signer);
2079        let author_grant = self
2080            .active_owner_grant(&author)
2081            .ok_or_else(|| MembershipError::SignerIsNotOwner(author.clone()))?;
2082        let (seq, _) = self.next_stream_position(&author, &author_grant, stream_id)?;
2083        Ok(derive_grant_id(
2084            self.store_id().expect("validated chain has a store id"),
2085            &author,
2086            &author_grant,
2087            stream_id,
2088            seq,
2089            user_pubkey,
2090        ))
2091    }
2092
2093    pub(crate) fn signed_set_member_with_anchor_and_wrapped_key_in_stream(
2094        &self,
2095        signer: &UserKeypair,
2096        stream_id: AuthorStreamId,
2097        user_pubkey: String,
2098        provider_account_email: Option<String>,
2099        role: MemberRole,
2100        membership: Option<GrantStreamAnchor>,
2101        wrapped_key: WrappedStoreKeyRef,
2102        created_at: String,
2103    ) -> Result<MembershipEntry, MembershipError> {
2104        let role = StoreMembershipRoleGrant::from_direct_assignment(role)?;
2105        let grant_id = self.next_member_grant_id_in_stream(signer, stream_id, &user_pubkey)?;
2106        self.signed_set_role_grant_with_anchor_and_wrapped_key_in_stream(
2107            signer,
2108            stream_id,
2109            user_pubkey,
2110            provider_account_email,
2111            role,
2112            grant_id,
2113            membership,
2114            wrapped_key,
2115            created_at,
2116        )
2117    }
2118
2119    #[allow(clippy::too_many_arguments)]
2120    fn signed_set_role_grant_with_anchor_and_wrapped_key_in_stream(
2121        &self,
2122        signer: &UserKeypair,
2123        stream_id: AuthorStreamId,
2124        user_pubkey: String,
2125        provider_account_email: Option<String>,
2126        role: StoreMembershipRoleGrant,
2127        grant_id: MembershipGrantId,
2128        membership: Option<GrantStreamAnchor>,
2129        wrapped_key: WrappedStoreKeyRef,
2130        created_at: String,
2131    ) -> Result<MembershipEntry, MembershipError> {
2132        let author = keys::public_key_hex(signer);
2133        let author_grant = self
2134            .active_owner_grant(&author)
2135            .ok_or_else(|| MembershipError::SignerIsNotOwner(author.clone()))?;
2136        let (seq, previous_hash) = self.next_stream_position(&author, &author_grant, stream_id)?;
2137        let replaces = self.active_grant_ids(&user_pubkey);
2138        let retirement_barriers = self.membership_retirement_barriers(&replaces, None)?;
2139        if role.is_owner() != membership.is_some() {
2140            return Err(MembershipError::InvalidOwnerMembershipAnchor(
2141                self.entries.len(),
2142            ));
2143        }
2144        let mut entry = MembershipEntry {
2145            version: STORE_PROTOCOL_VERSION,
2146            store_id: self
2147                .store_id()
2148                .expect("validated chain has a store id")
2149                .to_string(),
2150            author_pubkey: author,
2151            author_owner_grant: author_grant,
2152            stream_id,
2153            seq,
2154            previous_hash,
2155            dependencies: self.frontier(),
2156            resolution_dependencies: self.resolution_refs().to_vec(),
2157            created_at,
2158            change: MembershipChange::SetMember {
2159                user_pubkey: user_pubkey.clone(),
2160                provider_account_email,
2161                role,
2162                grant_id,
2163                membership,
2164                replaces,
2165                retirement_barriers,
2166                retirement_device_state: None,
2167                wrapped_key,
2168            },
2169            provider_admin: None,
2170            signature: String::new(),
2171        };
2172        sign_membership_entry(&mut entry, signer);
2173        let mut candidate = self.clone();
2174        candidate.add_entry(entry.clone())?;
2175        Ok(entry)
2176    }
2177
2178    #[allow(clippy::too_many_arguments)]
2179    pub fn signed_finalize_owner_promotion_in_stream(
2180        &self,
2181        root: &StoreRootRef,
2182        promoter: &StoreDeviceRegistration,
2183        candidate: &StoreDeviceRegistration,
2184        acceptance: OwnerPromotionAcceptance,
2185        signer: &UserKeypair,
2186        wrapped_key: WrappedStoreKeyRef,
2187        created_at: String,
2188    ) -> Result<MembershipEntry, MembershipError> {
2189        acceptance
2190            .request
2191            .verify(root, promoter)
2192            .map_err(|_| MembershipError::InvalidOwnerPromotion)?;
2193        acceptance
2194            .verify(candidate)
2195            .map_err(|_| MembershipError::InvalidOwnerPromotion)?;
2196        let request = &acceptance.request;
2197        let author = keys::public_key_hex(signer);
2198        let OwnerPromotionFinalization {
2199            author_stream,
2200            seq: requested_seq,
2201            previous_hash: requested_previous_hash,
2202        } = request.finalization;
2203        let (expected_seq, expected_previous_hash) =
2204            self.next_stream_position(&author, &request.promoter_owner_grant, author_stream)?;
2205        let Some(member) = self.active_grant(&request.member_grant) else {
2206            return Err(MembershipError::InvalidOwnerPromotion);
2207        };
2208        let membership = &acceptance.anchors.membership;
2209        let root_id = root.store_root_id.to_string();
2210        if author != promoter.author_pubkey
2211            || self.store_id() != Some(root_id.as_str())
2212            || self.active_owner_grant(&author) != Some(request.promoter_owner_grant.clone())
2213            || member.member_pubkey != request.member_pubkey
2214            || member.role != StoreMembershipRoleGrant::Member
2215            || self.active_grant_ids(&request.member_pubkey)
2216                != BTreeSet::from([request.member_grant.clone()])
2217            || expected_seq != requested_seq
2218            || expected_previous_hash != requested_previous_hash
2219            || self
2220                .state
2221                .grants
2222                .contains_key(&request.intended_owner_grant)
2223        {
2224            return Err(MembershipError::InvalidOwnerPromotion);
2225        }
2226        self.signed_set_role_grant_with_anchor_and_wrapped_key_in_stream(
2227            signer,
2228            author_stream,
2229            request.member_pubkey.clone(),
2230            member.provider_account_email.clone(),
2231            StoreMembershipRoleGrant::Owner {
2232                recovery: OwnerRecoveryAnchorRef::Promotion {
2233                    acceptance: Box::new(acceptance.clone()),
2234                },
2235            },
2236            request.intended_owner_grant.clone(),
2237            Some(membership.clone()),
2238            wrapped_key,
2239            created_at,
2240        )
2241    }
2242
2243    #[cfg(any(test, feature = "test-utils"))]
2244    pub fn signed_set_member_in_stream(
2245        &self,
2246        signer: &UserKeypair,
2247        stream_id: AuthorStreamId,
2248        user_pubkey: String,
2249        provider_account_email: Option<String>,
2250        role: MemberRole,
2251        created_at: String,
2252    ) -> Result<MembershipEntry, MembershipError> {
2253        let role = StoreMembershipRoleGrant::from_direct_assignment(role)?;
2254        let grant_id = self.next_member_grant_id_in_stream(signer, stream_id, &user_pubkey)?;
2255        let dependencies = self.frontier();
2256        let wrapped_key = test_wrapped_key_ref(
2257            &keys::public_key_hex(signer),
2258            &user_pubkey,
2259            membership_causal_generation(&self.entries, &dependencies),
2260            b"Merge membership test wrap",
2261        );
2262        self.signed_set_role_grant_with_anchor_and_wrapped_key_in_stream(
2263            signer,
2264            stream_id,
2265            user_pubkey,
2266            provider_account_email,
2267            role,
2268            grant_id,
2269            None,
2270            wrapped_key,
2271            created_at,
2272        )
2273    }
2274
2275    #[cfg(any(test, feature = "test-utils"))]
2276    pub fn signed_promote_member_in_stream_for_test(
2277        &self,
2278        signer: &UserKeypair,
2279        stream_id: AuthorStreamId,
2280        user_pubkey: String,
2281        created_at: String,
2282    ) -> Result<MembershipEntry, MembershipError> {
2283        let author_pubkey = keys::public_key_hex(signer);
2284        let dependencies = self.frontier();
2285        let wrapped_key = test_wrapped_key_ref(
2286            &author_pubkey,
2287            &user_pubkey,
2288            membership_causal_generation(&self.entries, &dependencies),
2289            b"Merge Owner-promotion test wrap",
2290        );
2291        self.signed_promote_member_in_stream_with_wrapped_key_for_test(
2292            signer,
2293            stream_id,
2294            user_pubkey,
2295            wrapped_key,
2296            created_at,
2297        )
2298    }
2299
2300    #[cfg(any(test, feature = "test-utils"))]
2301    pub fn signed_promote_member_in_stream_with_wrapped_key_for_test(
2302        &self,
2303        signer: &UserKeypair,
2304        stream_id: AuthorStreamId,
2305        user_pubkey: String,
2306        wrapped_key: WrappedStoreKeyRef,
2307        created_at: String,
2308    ) -> Result<MembershipEntry, MembershipError> {
2309        let author_pubkey = keys::public_key_hex(signer);
2310        let promoter_owner_grant = self
2311            .active_owner_grant(&author_pubkey)
2312            .ok_or_else(|| MembershipError::SignerIsNotOwner(author_pubkey.clone()))?;
2313        let member_grants = self.active_grant_ids(&user_pubkey);
2314        let Some(member_grant) = member_grants.iter().next().cloned() else {
2315            return Err(MembershipError::InvalidOwnerPromotion);
2316        };
2317        if member_grants.len() != 1
2318            || self
2319                .active_grant(&member_grant)
2320                .is_none_or(|record| record.role != StoreMembershipRoleGrant::Member)
2321        {
2322            return Err(MembershipError::InvalidOwnerPromotion);
2323        }
2324        let (seq, previous_hash) =
2325            self.next_stream_position(&author_pubkey, &promoter_owner_grant, stream_id)?;
2326        let promotion_id = OwnerPromotionId::from_generated(format!(
2327            "test promotion {author_pubkey} {user_pubkey} {stream_id:?} {seq}"
2328        ));
2329        let store_root_hash = ObjectHash::digest(
2330            self.store_id()
2331                .expect("validated membership chain has a Store id")
2332                .as_bytes(),
2333        );
2334        let intended_owner_grant = super::store_commit::derive_owner_promotion_grant(
2335            store_root_hash,
2336            promotion_id,
2337            &user_pubkey,
2338        );
2339        let membership_state_hash = match self.status() {
2340            MembershipStatus::Resolved(state) => state.state_hash,
2341            MembershipStatus::Conflict(_) => return Err(MembershipError::InvalidOwnerPromotion),
2342        };
2343        let object = |name: &str| {
2344            let slot = crate::storage::cloud::ObjectSlot::logical(format!(
2345                "test/owner-promotion/{promotion_id:?}/{name}.json"
2346            ))
2347            .expect("test Owner-promotion slot is valid");
2348            ExactObjectRef::new(slot, 1, ObjectHash::digest(name.as_bytes()))
2349        };
2350        let registration = |name: &str| StoreDeviceRegistrationRef {
2351            device_id: ObjectHash::digest(name.as_bytes())
2352                .to_string()
2353                .parse()
2354                .expect("digest is a valid Store device id"),
2355            registration_hash: ObjectHash::digest(format!("{name} registration").as_bytes()),
2356            object: object(&format!("{name}-registration")),
2357        };
2358        let candidate_stream = AuthorStreamId::from_bytes([0xA5; 32]);
2359        let activation_commit = super::store_commit::StoreBatchCommitRef {
2360            coord: super::store_commit::StoreCommitCoord {
2361                stream_id: candidate_stream,
2362                sequence: 1,
2363            },
2364            commit_hash: ObjectHash::digest(b"test Owner-promotion activation commit"),
2365            object: object("activation-commit"),
2366        };
2367        let membership = GrantStreamAnchor::StoreMembership {
2368            first_slot: crate::storage::cloud::ObjectSlot::logical(format!(
2369                "{}.json",
2370                super::store_commit::membership_head_slot_prefix(
2371                    &user_pubkey,
2372                    &intended_owner_grant,
2373                    stream_id,
2374                    1,
2375                )
2376            ))
2377            .expect("test membership head slot is valid"),
2378        };
2379        let request = OwnerPromotionRequest {
2380            version: STORE_PROTOCOL_VERSION,
2381            promotion_id,
2382            store_root_hash,
2383            promoter_registration: registration("promoter"),
2384            promoter_owner_grant: promoter_owner_grant.clone(),
2385            member_pubkey: user_pubkey.clone(),
2386            member_grant,
2387            member_registration: registration("member"),
2388            intended_owner_grant: intended_owner_grant.clone(),
2389            predecessor_membership: super::circle_control::StoreMembershipStateRef::from_parts(
2390                Vec::new(),
2391                Vec::new(),
2392                Vec::new(),
2393                membership_state_hash,
2394            )
2395            .expect("construct test predecessor membership"),
2396            predecessor_devices: StoreDeviceStateRef::from_resolved(
2397                super::store_commit::CommitFrontier(BTreeMap::new()),
2398                &super::store_commit::ResolvedStoreDeviceState {
2399                    devices: BTreeMap::new(),
2400                    recovery: Vec::new(),
2401                    state_hash: ObjectHash::digest(b"test Owner-promotion device state"),
2402                },
2403            )
2404            .expect("construct test predecessor device state"),
2405            finalization: OwnerPromotionFinalization {
2406                author_stream: stream_id,
2407                seq,
2408                previous_hash,
2409            },
2410            signature: String::new(),
2411        };
2412        let acceptance = OwnerPromotionAcceptance {
2413            request: Box::new(request),
2414            activation: OwnerPromotionRequestActivation {
2415                commit: activation_commit,
2416                head: super::store_commit::StoreDeviceHeadRef {
2417                    head_hash: ObjectHash::digest(b"test Owner-promotion activation head"),
2418                    object: object("activation-head"),
2419                },
2420            },
2421            anchors: OwnerPromotionAnchors {
2422                membership: membership.clone(),
2423                recovery: GrantStreamAnchor::OwnerRecovery {
2424                    first_slot: crate::storage::cloud::ObjectSlot::logical(format!(
2425                        "test/owner-promotion/{promotion_id:?}/recovery/1.json"
2426                    ))
2427                    .expect("test recovery slot is valid"),
2428                },
2429            },
2430            signature: String::new(),
2431        };
2432        self.signed_set_role_grant_with_anchor_and_wrapped_key_in_stream(
2433            signer,
2434            stream_id,
2435            user_pubkey,
2436            None,
2437            StoreMembershipRoleGrant::Owner {
2438                recovery: OwnerRecoveryAnchorRef::Promotion {
2439                    acceptance: Box::new(acceptance),
2440                },
2441            },
2442            intended_owner_grant,
2443            Some(membership),
2444            wrapped_key,
2445            created_at,
2446        )
2447    }
2448
2449    #[cfg(any(test, feature = "test-utils"))]
2450    pub fn add_owner_for_test(
2451        &mut self,
2452        signer: &UserKeypair,
2453        stream_id: AuthorStreamId,
2454        user_pubkey: String,
2455        created_at: String,
2456    ) -> Result<(), MembershipError> {
2457        let member = self.signed_set_member_in_stream(
2458            signer,
2459            stream_id,
2460            user_pubkey.clone(),
2461            None,
2462            MemberRole::Member,
2463            format!("{created_at}: Member grant"),
2464        )?;
2465        self.add_entry(member)?;
2466        let promotion = self.signed_promote_member_in_stream_for_test(
2467            signer,
2468            stream_id,
2469            user_pubkey,
2470            created_at,
2471        )?;
2472        self.add_entry(promotion)
2473    }
2474
2475    pub fn signed_remove_member_with_wrapped_keys_in_stream(
2476        &self,
2477        signer: &UserKeypair,
2478        stream_id: AuthorStreamId,
2479        user_pubkey: String,
2480        wrapped_keys: Vec<WrappedStoreKeyRef>,
2481        created_at: String,
2482    ) -> Result<MembershipEntry, MembershipError> {
2483        self.signed_remove_member_with_barrier_state(
2484            signer,
2485            stream_id,
2486            user_pubkey,
2487            wrapped_keys,
2488            None,
2489            created_at,
2490        )
2491    }
2492
2493    pub fn signed_remove_member_with_owner_barrier_state(
2494        &self,
2495        signer: &UserKeypair,
2496        stream_id: AuthorStreamId,
2497        user_pubkey: String,
2498        wrapped_keys: Vec<WrappedStoreKeyRef>,
2499        device_state: StoreDeviceStateRef,
2500        created_at: String,
2501    ) -> Result<MembershipEntry, MembershipError> {
2502        self.signed_remove_member_with_barrier_state(
2503            signer,
2504            stream_id,
2505            user_pubkey,
2506            wrapped_keys,
2507            Some(device_state),
2508            created_at,
2509        )
2510    }
2511
2512    fn signed_remove_member_with_barrier_state(
2513        &self,
2514        signer: &UserKeypair,
2515        stream_id: AuthorStreamId,
2516        user_pubkey: String,
2517        wrapped_keys: Vec<WrappedStoreKeyRef>,
2518        retirement_device_state: Option<StoreDeviceStateRef>,
2519        created_at: String,
2520    ) -> Result<MembershipEntry, MembershipError> {
2521        let removes = self.active_grant_ids(&user_pubkey);
2522        if removes.is_empty() {
2523            return Err(MembershipError::NotAMember(user_pubkey));
2524        }
2525        let retains_owner = self.state.grants.iter().any(|(grant, state)| {
2526            !removes.contains(grant) && state.active().is_some_and(|record| record.role.is_owner())
2527        });
2528        if !retains_owner {
2529            return Err(MembershipError::NoActiveOwner);
2530        }
2531        let author = keys::public_key_hex(signer);
2532        let author_grant = self
2533            .active_owner_grant(&author)
2534            .ok_or_else(|| MembershipError::SignerIsNotOwner(author.clone()))?;
2535        let (seq, previous_hash) = self.next_stream_position(&author, &author_grant, stream_id)?;
2536        let retirement_barriers =
2537            self.membership_retirement_barriers(&removes, retirement_device_state.as_ref())?;
2538        let mut entry = MembershipEntry {
2539            version: STORE_PROTOCOL_VERSION,
2540            store_id: self
2541                .store_id()
2542                .expect("validated chain has a store id")
2543                .to_string(),
2544            author_pubkey: author,
2545            author_owner_grant: author_grant,
2546            stream_id,
2547            seq,
2548            previous_hash,
2549            dependencies: self.frontier(),
2550            resolution_dependencies: self.resolution_refs().to_vec(),
2551            created_at,
2552            change: MembershipChange::RemoveMember {
2553                user_pubkey,
2554                removes,
2555                retirement_barriers,
2556                retirement_device_state,
2557                wrapped_keys,
2558            },
2559            provider_admin: None,
2560            signature: String::new(),
2561        };
2562        sign_membership_entry(&mut entry, signer);
2563        let mut candidate = self.clone();
2564        candidate.add_entry(entry.clone())?;
2565        Ok(entry)
2566    }
2567
2568    #[cfg(any(test, feature = "test-utils"))]
2569    pub fn signed_remove_member_in_stream(
2570        &self,
2571        signer: &UserKeypair,
2572        stream_id: AuthorStreamId,
2573        user_pubkey: String,
2574        created_at: String,
2575    ) -> Result<MembershipEntry, MembershipError> {
2576        let owner = keys::public_key_hex(signer);
2577        let dependencies = self.frontier();
2578        let generation = membership_causal_generation(&self.entries, &dependencies)
2579            .checked_add(1)
2580            .ok_or(MembershipError::InvalidWrappedKeys(self.entries.len()))?;
2581        let wrapped_keys = self
2582            .current_members()
2583            .into_iter()
2584            .filter(|(member, _)| member != &user_pubkey)
2585            .map(|(member, _)| {
2586                test_wrapped_key_ref(&owner, &member, generation, b"Merge removal test wrap")
2587            })
2588            .collect();
2589        let removes = self.active_grant_ids(&user_pubkey);
2590        let mut recovery = removes
2591            .iter()
2592            .filter_map(|grant| {
2593                self.state
2594                    .grants
2595                    .get(grant)
2596                    .and_then(GrantState::active)
2597                    .filter(|record| record.role.is_owner())
2598                    .map(|record| OwnerRecoveryCursor {
2599                        owner_grant: grant.clone(),
2600                        position: OwnerRecoveryPosition::At {
2601                            node: OwnerRecoveryNodeRef {
2602                                owner_pubkey: record.member_pubkey.clone(),
2603                                owner_grant: grant.clone(),
2604                                sequence: 1,
2605                                node_hash: ObjectHash::digest(
2606                                    format!("test recovery node {grant}").as_bytes(),
2607                                ),
2608                                object: ExactObjectRef::new(
2609                                    crate::storage::cloud::ObjectSlot::logical(format!(
2610                                        "test/recovery/{grant}/1.json"
2611                                    ))
2612                                    .expect("test recovery node slot is valid"),
2613                                    1,
2614                                    ObjectHash::digest(format!("test recovery {grant}").as_bytes()),
2615                                ),
2616                            },
2617                        },
2618                    })
2619            })
2620            .collect::<Vec<_>>();
2621        recovery.sort();
2622        let device_state = (!recovery.is_empty()).then(|| {
2623            StoreDeviceStateRef::from_resolved(
2624                super::store_commit::CommitFrontier(BTreeMap::new()),
2625                &super::store_commit::ResolvedStoreDeviceState {
2626                    devices: BTreeMap::new(),
2627                    recovery,
2628                    state_hash: ObjectHash::digest(b"test membership retirement device state"),
2629                },
2630            )
2631            .expect("construct test membership retirement device state")
2632        });
2633        self.signed_remove_member_with_barrier_state(
2634            signer,
2635            stream_id,
2636            user_pubkey,
2637            wrapped_keys,
2638            device_state,
2639            created_at,
2640        )
2641    }
2642
2643    pub fn signed_provider_admin_change_in_stream(
2644        &self,
2645        signer: &UserKeypair,
2646        stream_id: AuthorStreamId,
2647        change: super::provider::ProviderAdminChange,
2648        created_at: String,
2649    ) -> Result<MembershipEntry, MembershipError> {
2650        let author = keys::public_key_hex(signer);
2651        let author_grant = self
2652            .active_owner_grant(&author)
2653            .ok_or_else(|| MembershipError::SignerIsNotOwner(author.clone()))?;
2654        let (seq, previous_hash) = self.next_stream_position(&author, &author_grant, stream_id)?;
2655        let owner_grants = self
2656            .state
2657            .grants
2658            .iter()
2659            .filter(|(_, state)| state.active().is_some_and(|record| record.role.is_owner()))
2660            .map(|(grant_id, _)| grant_id.clone())
2661            .collect();
2662        let mut entry = MembershipEntry {
2663            version: STORE_PROTOCOL_VERSION,
2664            store_id: self
2665                .store_id()
2666                .expect("validated chain has a store id")
2667                .to_string(),
2668            author_pubkey: author,
2669            author_owner_grant: author_grant,
2670            stream_id,
2671            seq,
2672            previous_hash,
2673            dependencies: self.frontier(),
2674            resolution_dependencies: Vec::new(),
2675            created_at,
2676            change: MembershipChange::ProviderAdmin,
2677            provider_admin: Some(super::provider::ProviderAdminMembershipChange {
2678                change,
2679                owner_barriers: self.owner_barriers(&owner_grants),
2680            }),
2681            signature: String::new(),
2682        };
2683        sign_membership_entry(&mut entry, signer);
2684        let mut candidate = self.clone();
2685        candidate.add_entry(entry.clone())?;
2686        Ok(entry)
2687    }
2688
2689    pub fn signed_resolution_activation_in_stream(
2690        &self,
2691        store_root_hash: ObjectHash,
2692        signer: &UserKeypair,
2693        stream_id: AuthorStreamId,
2694        reference: StoreMembershipConflictResolutionRef,
2695        resolution: &StoreMembershipConflictResolution,
2696        created_at: String,
2697    ) -> Result<MembershipEntry, MembershipError> {
2698        self.ensure_resolved()?;
2699        let MembershipStatus::Resolved(resolved_before) = self.status() else {
2700            unreachable!("ensure_resolved accepted a conflict")
2701        };
2702        let author = keys::public_key_hex(signer);
2703        if !resolution.verify_signature()
2704            || resolution.store_root_hash != store_root_hash
2705            || reference.resolver_pubkey != author
2706            || !self.resolution_refs().contains(&reference)
2707            || self.active_owner_grant(&author) != Some(resolution.replacement_grant.clone())
2708        {
2709            return Err(MembershipError::InvalidConflictResolution);
2710        }
2711        let author_grant = resolution.replacement_grant.clone();
2712        if self
2713            .raw_stream_tip(&author, &author_grant, stream_id)
2714            .is_some()
2715        {
2716            return Err(MembershipError::ResolutionActivationRequiresFreshStream);
2717        }
2718        let mut entry = MembershipEntry {
2719            version: STORE_PROTOCOL_VERSION,
2720            store_id: self
2721                .store_id()
2722                .expect("validated chain has a store id")
2723                .to_string(),
2724            author_pubkey: author,
2725            author_owner_grant: author_grant,
2726            stream_id,
2727            seq: 1,
2728            previous_hash: None,
2729            dependencies: self.effective_frontier(),
2730            resolution_dependencies: self.resolution_refs().to_vec(),
2731            created_at,
2732            change: MembershipChange::ResolutionActivation {
2733                resolution: reference,
2734            },
2735            provider_admin: None,
2736            signature: String::new(),
2737        };
2738        sign_membership_entry(&mut entry, signer);
2739        let mut candidate = self.clone();
2740        candidate.add_entry(entry.clone())?;
2741        let MembershipStatus::Resolved(resolved_after) = candidate.status() else {
2742            return Err(MembershipError::InvalidConflictResolution);
2743        };
2744        if resolved_after.state_hash != resolved_before.state_hash {
2745            return Err(MembershipError::InvalidConflictResolution);
2746        }
2747        Ok(entry)
2748    }
2749
2750    pub(crate) fn next_stream_position(
2751        &self,
2752        author: &str,
2753        grant: &MembershipGrantId,
2754        stream_id: AuthorStreamId,
2755    ) -> Result<(u64, Option<ObjectHash>), MembershipError> {
2756        let raw_tip = self.raw_stream_tip(author, grant, stream_id);
2757        let effective_tip = self.stream_tip(author, grant, stream_id);
2758        if raw_tip != effective_tip {
2759            return Err(MembershipError::PrunedAuthorStream);
2760        }
2761        effective_tip.map_or(Ok((1, None)), |tip| {
2762            tip.seq
2763                .checked_add(1)
2764                .map(|seq| (seq, Some(tip.entry_hash)))
2765                .ok_or(MembershipError::SequenceExhausted)
2766        })
2767    }
2768
2769    fn frontier(&self) -> Vec<MembershipCoord> {
2770        self.effective_frontier()
2771    }
2772
2773    fn owner_barriers(
2774        &self,
2775        grants: &BTreeSet<MembershipGrantId>,
2776    ) -> BTreeMap<MembershipGrantId, OwnerStreamBarrier> {
2777        grants
2778            .iter()
2779            .filter_map(|grant| {
2780                let record = self.state.grants.get(grant)?.active()?;
2781                record.role.is_owner().then(|| {
2782                    let observed_streams = self
2783                        .effective_frontier()
2784                        .into_iter()
2785                        .filter(|coord| coord.author_owner_grant == *grant)
2786                        .collect();
2787                    (grant.clone(), OwnerStreamBarrier { observed_streams })
2788                })
2789            })
2790            .collect()
2791    }
2792
2793    fn membership_retirement_barriers(
2794        &self,
2795        grants: &BTreeSet<MembershipGrantId>,
2796        device_state: Option<&StoreDeviceStateRef>,
2797    ) -> Result<BTreeMap<MembershipGrantId, MergeMembershipGrantRetirementBarrier>, MembershipError>
2798    {
2799        let retires_owner = grants.iter().any(|grant| {
2800            self.state
2801                .grants
2802                .get(grant)
2803                .and_then(GrantState::active)
2804                .is_some_and(|record| record.role.is_owner())
2805        });
2806        if retires_owner && device_state.is_none() {
2807            return Err(MembershipError::MissingOwnerRecoveryState);
2808        }
2809        if !retires_owner && device_state.is_some() {
2810            return Err(MembershipError::UnexpectedOwnerRecoveryState);
2811        }
2812        let recovery = match device_state {
2813            Some(state) => state.recovery(),
2814            None => &[],
2815        };
2816        grants
2817            .iter()
2818            .map(|grant| {
2819                let record = self
2820                    .state
2821                    .grants
2822                    .get(grant)
2823                    .and_then(GrantState::active)
2824                    .ok_or_else(|| MembershipError::NotAMember(grant.to_string()))?;
2825                let author_streams = StoreGrantStreamBarrier {
2826                    observed_streams: self
2827                        .effective_frontier()
2828                        .into_iter()
2829                        .filter(|coord| coord.author_owner_grant == *grant)
2830                        .collect(),
2831                };
2832                let barrier = if record.role.is_owner() {
2833                    let cursor = recovery
2834                        .iter()
2835                        .find(|cursor| cursor.owner_grant == *grant)
2836                        .cloned()
2837                        .ok_or(MembershipError::MissingOwnerRecoveryState)?;
2838                    MergeMembershipGrantRetirementBarrier::Owner {
2839                        barrier: MergeStoreOwnerGrantBarrier {
2840                            author_streams,
2841                            recovery: cursor,
2842                        },
2843                    }
2844                } else {
2845                    MergeMembershipGrantRetirementBarrier::NonOwner { author_streams }
2846                };
2847                Ok((grant.clone(), barrier))
2848            })
2849            .collect()
2850    }
2851
2852    fn active_grants_for(&self, pubkey: &str) -> Vec<(&MembershipGrantId, &MembershipGrantRecord)> {
2853        self.state
2854            .grants
2855            .iter()
2856            .filter_map(|(grant, state)| {
2857                state
2858                    .active()
2859                    .filter(|record| record.member_pubkey == pubkey)
2860                    .map(|record| (grant, record))
2861            })
2862            .collect()
2863    }
2864
2865    fn rebuild(&mut self) -> Result<(), MembershipError> {
2866        let expected_store = self
2867            .entries
2868            .first()
2869            .ok_or(MembershipError::EmptyChain)?
2870            .store_id
2871            .clone();
2872        if expected_store.is_empty() {
2873            return Err(MembershipError::InvalidFounder);
2874        }
2875
2876        for (index, (coord, entry)) in self.entries_with_coords().enumerate() {
2877            if entry.version != STORE_PROTOCOL_VERSION {
2878                return Err(MembershipError::UnsupportedVersion(index));
2879            }
2880            if entry.store_id != expected_store {
2881                return Err(MembershipError::StoreMismatch {
2882                    index,
2883                    expected: expected_store.clone(),
2884                    actual: entry.store_id.clone(),
2885                });
2886            }
2887            if !verify_membership_entry(entry) {
2888                return Err(MembershipError::InvalidSignature(index));
2889            }
2890            let actual = entry.coord();
2891            if *coord != actual {
2892                return Err(MembershipError::CoordinateMismatch {
2893                    index,
2894                    expected: Box::new(coord.clone()),
2895                    actual: Box::new(actual),
2896                });
2897            }
2898            if !entry
2899                .dependencies
2900                .windows(2)
2901                .all(|pair| pair[0].stream_key() < pair[1].stream_key())
2902            {
2903                return Err(MembershipError::NonCanonicalDependencyFrontier { index });
2904            }
2905            let (barriers, retirement_device_state) = match &entry.change {
2906                MembershipChange::SetMember {
2907                    user_pubkey,
2908                    role,
2909                    grant_id,
2910                    replaces,
2911                    membership,
2912                    retirement_barriers,
2913                    retirement_device_state,
2914                    ..
2915                } => {
2916                    let valid_owner_origin = match role {
2917                        StoreMembershipRoleGrant::Owner {
2918                            recovery: OwnerRecoveryAnchorRef::Promotion { acceptance },
2919                        } => {
2920                            let request = &acceptance.request;
2921                            let anchors_match =
2922                                Some(&acceptance.anchors.membership) == membership.as_ref();
2923                            let finalization_matches = request.finalization.author_stream
2924                                == entry.stream_id
2925                                && request.finalization.seq == entry.seq
2926                                && request.finalization.previous_hash == entry.previous_hash;
2927                            request.member_pubkey == *user_pubkey
2928                                && replaces.len() == 1
2929                                && replaces.contains(&request.member_grant)
2930                                && request.intended_owner_grant == *grant_id
2931                                && request.promoter_owner_grant == entry.author_owner_grant
2932                                && anchors_match
2933                                && finalization_matches
2934                        }
2935                        StoreMembershipRoleGrant::Owner { .. } => false,
2936                        StoreMembershipRoleGrant::Member | StoreMembershipRoleGrant::Follower => {
2937                            membership.is_none()
2938                        }
2939                    };
2940                    if role.is_owner()
2941                        != membership.as_ref().is_some_and(|anchor| {
2942                            store_membership_anchor_stream(user_pubkey, grant_id, anchor).is_some()
2943                        })
2944                        || !valid_owner_origin
2945                    {
2946                        return Err(MembershipError::InvalidOwnerMembershipAnchor(index));
2947                    }
2948                    (retirement_barriers, retirement_device_state)
2949                }
2950                MembershipChange::RemoveMember {
2951                    retirement_barriers,
2952                    retirement_device_state,
2953                    ..
2954                } => (retirement_barriers, retirement_device_state),
2955                MembershipChange::ResolutionActivation { resolution } => {
2956                    if resolution.resolver_pubkey != entry.author_pubkey
2957                        || entry.seq != 1
2958                        || entry.previous_hash.is_some()
2959                        || entry
2960                            .dependencies
2961                            .iter()
2962                            .any(|dependency| dependency.stream_key() == entry.coord().stream_key())
2963                        || entry.author_owner_grant
2964                            != derive_store_resolution_grant(
2965                                &resolution.conflict_hash,
2966                                &resolution.resolver_pubkey,
2967                            )
2968                        || entry
2969                            .resolution_dependencies
2970                            .binary_search(resolution)
2971                            .is_err()
2972                        || self
2973                            .resolution_checkpoint
2974                            .as_ref()
2975                            .is_none_or(|checkpoint| {
2976                                let already_checkpointed =
2977                                    checkpoint.included.contains(&entry.coord())
2978                                        || checkpoint.raw_heads.contains(&entry.coord());
2979                                !already_checkpointed
2980                                    && (entry.dependencies != checkpoint.effective_frontier
2981                                        || entry.resolution_dependencies != checkpoint.resolutions)
2982                            })
2983                    {
2984                        return Err(MembershipError::InvalidResolutionActivation(index));
2985                    }
2986                    continue;
2987                }
2988                MembershipChange::ProviderAdmin => {
2989                    let Some(super::provider::ProviderAdminMembershipChange {
2990                        owner_barriers, ..
2991                    }) = &entry.provider_admin
2992                    else {
2993                        return Err(MembershipError::InvalidProviderAdminChange(index));
2994                    };
2995                    if !entry.resolution_dependencies.is_empty()
2996                        || owner_barriers.values().any(|barrier| {
2997                            !barrier
2998                                .observed_streams
2999                                .windows(2)
3000                                .all(|pair| pair[0].stream_key() < pair[1].stream_key())
3001                        })
3002                    {
3003                        return Err(MembershipError::InvalidProviderAdminChange(index));
3004                    }
3005                    continue;
3006                }
3007                MembershipChange::Founder { .. } => continue,
3008            };
3009            if entry.provider_admin.is_some() {
3010                return Err(MembershipError::InvalidProviderAdminChange(index));
3011            }
3012            let owner_recoveries = barriers
3013                .values()
3014                .filter_map(|barrier| match barrier {
3015                    MergeMembershipGrantRetirementBarrier::Owner { barrier } => {
3016                        Some(&barrier.recovery)
3017                    }
3018                    MergeMembershipGrantRetirementBarrier::NonOwner { .. } => None,
3019                })
3020                .collect::<Vec<_>>();
3021            match (owner_recoveries.is_empty(), retirement_device_state) {
3022                (true, None) => {}
3023                (false, Some(state))
3024                    if owner_recoveries
3025                        .iter()
3026                        .all(|cursor| state.recovery().binary_search(cursor).is_ok()) => {}
3027                (true, Some(_)) => return Err(MembershipError::UnexpectedOwnerRecoveryState),
3028                (false, None | Some(_)) => return Err(MembershipError::MissingOwnerRecoveryState),
3029            }
3030            if let Some((grant, _)) = barriers.iter().find(|(_, barrier)| {
3031                !barrier
3032                    .author_streams()
3033                    .observed_streams
3034                    .windows(2)
3035                    .all(|pair| pair[0].stream_key() < pair[1].stream_key())
3036            }) {
3037                return Err(MembershipError::InvalidOwnerRevocationBarrier {
3038                    index,
3039                    grant: grant.clone(),
3040                });
3041            }
3042        }
3043
3044        let founders = self
3045            .entries
3046            .iter()
3047            .filter_map(|entry| {
3048                let MembershipChange::Founder {
3049                    owner_pubkey,
3050                    owner_grant_id,
3051                    ..
3052                } = &entry.change
3053                else {
3054                    return None;
3055                };
3056                Some((entry, owner_pubkey, owner_grant_id))
3057            })
3058            .collect::<Vec<_>>();
3059        let [(founder, owner_pubkey, owner_grant_id)] = founders.as_slice() else {
3060            return Err(MembershipError::InvalidFounder);
3061        };
3062        if founder.author_pubkey != **owner_pubkey
3063            || founder.author_owner_grant != **owner_grant_id
3064            || founder.stream_id != derive_founder_stream_id(&founder.store_id, owner_pubkey)
3065            || founder.provider_admin.is_some()
3066        {
3067            return Err(MembershipError::InvalidFounder);
3068        }
3069
3070        validate_provider_admin_controls(&self.entries, self.resolution_checkpoint.as_ref())?;
3071        validate_membership_retirement_barriers(
3072            &self.entries,
3073            self.resolution_checkpoint.as_ref(),
3074        )?;
3075        validate_membership_wrapped_keys(&self.entries, self.resolution_checkpoint.as_ref())?;
3076
3077        let reduced = match &self.resolution_checkpoint {
3078            Some(checkpoint) => reduce_store_membership_from_checkpoint(&self.entries, checkpoint)?,
3079            None => reduce_store_membership(&self.entries)?,
3080        };
3081        let checkpoint_grants = self
3082            .resolution_checkpoint
3083            .as_ref()
3084            .map(|checkpoint| &checkpoint.grants);
3085        let provider_admin_seed = self
3086            .resolution_checkpoint
3087            .as_ref()
3088            .map_or(&self.provider_admin_genesis, |checkpoint| {
3089                &checkpoint.provider_admin
3090            });
3091        let (state_source, status) = match reduced {
3092            CausalGrantStatus::Resolved(reduced) => {
3093                let provider_admin = super::provider::ProviderAdminState::reduce_merge(
3094                    provider_admin_seed,
3095                    &self.entries,
3096                    &reduced.included,
3097                )?;
3098                let resolved = resolved_store_membership(
3099                    &reduced,
3100                    checkpoint_grants,
3101                    provider_admin,
3102                    &self.entries,
3103                )?;
3104                (Some(reduced), MembershipStatus::Resolved(resolved))
3105            }
3106            CausalGrantStatus::Conflict(CausalGrantConflict::ConcurrentMemberAssignments {
3107                raw_heads,
3108                effective_frontier,
3109                member_pubkey,
3110                conflicting_grants,
3111                uncontested_grants,
3112                reduced,
3113            }) => {
3114                let heads = self.exact_head_refs(&raw_heads)?;
3115                let provider_admin = super::provider::ProviderAdminState::reduce_merge(
3116                    provider_admin_seed,
3117                    &self.entries,
3118                    &reduced.included,
3119                )?;
3120                let grants = reduced
3121                    .grants
3122                    .iter()
3123                    .map(|(grant, state)| {
3124                        Ok((
3125                            grant.clone(),
3126                            map_store_grant_state(grant, state, checkpoint_grants, &self.entries)?,
3127                        ))
3128                    })
3129                    .collect::<Result<_, MembershipError>>()?;
3130                let conflict = MembershipConflict::ConcurrentMemberAssignments {
3131                    conflict_hash: membership_assignment_conflict_hash(
3132                        &heads,
3133                        &member_pubkey,
3134                        &conflicting_grants,
3135                    ),
3136                    heads,
3137                    effective_frontier,
3138                    member_pubkey,
3139                    conflicting_grants: map_store_grants(conflicting_grants, checkpoint_grants)?,
3140                    uncontested_grants: map_store_grants(uncontested_grants, checkpoint_grants)?,
3141                    grants,
3142                    provider_admin,
3143                };
3144                (Some(reduced), MembershipStatus::Conflict(conflict))
3145            }
3146            CausalGrantStatus::Conflict(CausalGrantConflict::RevocationCycle {
3147                raw_heads,
3148                cyclic_sources,
3149                involved_owner_grants,
3150                maximal_valid_branches,
3151            }) => {
3152                let heads = self.exact_head_refs(&raw_heads)?;
3153                let branches = maximal_valid_branches
3154                    .into_iter()
3155                    .map(|branch| -> Result<StoreMembershipBranch, MembershipError> {
3156                        let resolved = resolved_store_membership(
3157                            &branch.reduced,
3158                            checkpoint_grants,
3159                            super::provider::ProviderAdminState::reduce_merge(
3160                                provider_admin_seed,
3161                                &self.entries,
3162                                &branch.reduced.included,
3163                            )?,
3164                            &self.entries,
3165                        )?;
3166                        Ok(StoreMembershipBranch {
3167                            heads: self.branch_head_refs(&branch.raw_heads)?,
3168                            effective_frontier: branch.effective_frontier,
3169                            grants: resolved.grants,
3170                            provider_admin: resolved.provider_admin,
3171                            state_hash: resolved.state_hash,
3172                        })
3173                    })
3174                    .collect::<Result<Vec<_>, _>>()?;
3175                let conflict_hash = membership_revocation_conflict_hash(
3176                    &heads,
3177                    &cyclic_sources,
3178                    &involved_owner_grants,
3179                );
3180                (
3181                    None,
3182                    MembershipStatus::Conflict(MembershipConflict::RevocationCycle {
3183                        conflict_hash,
3184                        heads,
3185                        cyclic_sources,
3186                        involved_owner_grants,
3187                        maximal_valid_branches: branches,
3188                    }),
3189                )
3190            }
3191        };
3192        if let Some(reduced) = state_source {
3193            self.state = CausalState {
3194                grants: reduced
3195                    .grants
3196                    .iter()
3197                    .map(|(grant, state)| {
3198                        Ok((
3199                            grant.clone(),
3200                            map_store_grant_state(grant, state, checkpoint_grants, &self.entries)?,
3201                        ))
3202                    })
3203                    .collect::<Result<_, MembershipError>>()?,
3204            };
3205            self.included = reduced.included;
3206        } else {
3207            self.state = CausalState::default();
3208            self.included.clear();
3209        }
3210        self.status = Some(status);
3211        Ok(())
3212    }
3213
3214    pub fn apply_resolutions(
3215        &mut self,
3216        store_root_hash: ObjectHash,
3217        resolutions: &[(
3218            StoreMembershipConflictResolutionRef,
3219            StoreMembershipConflictResolution,
3220        )],
3221    ) -> Result<(), MembershipError> {
3222        let (raw_heads, effective_frontier) = match self.conflict() {
3223            Some(MembershipConflict::ConcurrentMemberAssignments {
3224                heads,
3225                effective_frontier,
3226                ..
3227            }) => (
3228                heads
3229                    .iter()
3230                    .map(|reference| reference.coord.clone())
3231                    .collect(),
3232                effective_frontier.clone(),
3233            ),
3234            Some(MembershipConflict::RevocationCycle {
3235                heads,
3236                maximal_valid_branches,
3237                ..
3238            }) => {
3239                let selected = resolutions
3240                    .iter()
3241                    .map(|(_, resolution)| {
3242                        let MembershipConflictSelection::RevocationBranch {
3243                            heads: selected_heads,
3244                        } = &resolution.selection
3245                        else {
3246                            return Err(MembershipError::InvalidConflictResolution);
3247                        };
3248                        maximal_valid_branches
3249                            .iter()
3250                            .find(|branch| branch.heads == *selected_heads)
3251                            .map(|branch| branch.effective_frontier.as_slice())
3252                            .ok_or(MembershipError::InvalidConflictResolution)
3253                    })
3254                    .collect::<Result<Vec<_>, _>>()?;
3255                (
3256                    heads
3257                        .iter()
3258                        .map(|reference| reference.coord.clone())
3259                        .collect(),
3260                    causal_grants::common_frontier(&selected),
3261                )
3262            }
3263            _ => return Err(MembershipError::InvalidConflictResolution),
3264        };
3265        let resolved = self.resolved_with(store_root_hash, resolutions)?;
3266        let grants = resolved.grants.clone();
3267        let mut grant_anchors = self
3268            .resolution_checkpoint
3269            .as_ref()
3270            .map_or_else(BTreeMap::new, |checkpoint| checkpoint.grant_anchors.clone());
3271        for entry in &self.entries {
3272            match &entry.change {
3273                MembershipChange::Founder {
3274                    owner_grant_id,
3275                    membership,
3276                    ..
3277                } => {
3278                    grant_anchors.insert(owner_grant_id.clone(), membership.clone());
3279                }
3280                MembershipChange::SetMember {
3281                    grant_id,
3282                    membership: Some(membership),
3283                    ..
3284                } => {
3285                    grant_anchors.insert(grant_id.clone(), membership.clone());
3286                }
3287                _ => {}
3288            }
3289        }
3290        for (_, resolution) in resolutions {
3291            grant_anchors.insert(
3292                resolution.replacement_grant.clone(),
3293                resolution.replacement_membership.clone(),
3294            );
3295        }
3296        let included = membership_history_closure(&self.entries, &effective_frontier);
3297        let mut resolution_refs = self
3298            .resolution_checkpoint
3299            .as_ref()
3300            .map_or_else(Vec::new, |checkpoint| checkpoint.resolutions.clone());
3301        resolution_refs.extend(resolutions.iter().map(|(reference, _)| reference.clone()));
3302        resolution_refs.sort();
3303        resolution_refs.dedup();
3304        self.resolution_checkpoint = Some(MembershipResolutionCheckpoint {
3305            raw_heads,
3306            effective_frontier: effective_frontier.clone(),
3307            grants: grants.clone(),
3308            grant_anchors,
3309            included: included.clone(),
3310            resolutions: resolution_refs,
3311            provider_admin: resolved.provider_admin.combined_state().clone(),
3312        });
3313        self.state = CausalState { grants };
3314        self.included = included;
3315        self.status = Some(MembershipStatus::Resolved(resolved));
3316        Ok(())
3317    }
3318
3319    fn exact_head_refs(
3320        &self,
3321        raw_heads: &[MembershipCoord],
3322    ) -> Result<Vec<MembershipHeadRef>, MembershipError> {
3323        let expected = raw_heads.iter().cloned().collect::<BTreeSet<_>>();
3324        let mut references = self
3325            .head_refs
3326            .iter()
3327            .filter(|reference| expected.contains(&reference.coord))
3328            .cloned()
3329            .collect::<Vec<_>>();
3330        let actual = references
3331            .iter()
3332            .map(|reference| reference.coord.clone())
3333            .collect::<BTreeSet<_>>();
3334        if expected != actual || references.len() != expected.len() {
3335            return Err(MembershipError::MissingConflictHeads);
3336        }
3337        references.sort();
3338        Ok(references)
3339    }
3340
3341    fn branch_head_refs(
3342        &self,
3343        branch_heads: &[MembershipCoord],
3344    ) -> Result<Vec<MembershipHeadRef>, MembershipError> {
3345        let by_coord = self
3346            .head_refs
3347            .iter()
3348            .map(|reference| (reference.coord.clone(), reference.clone()))
3349            .collect::<BTreeMap<_, _>>();
3350        let mut references = branch_heads
3351            .iter()
3352            .map(|coord| {
3353                by_coord
3354                    .get(coord)
3355                    .cloned()
3356                    .ok_or(MembershipError::MissingConflictHeads)
3357            })
3358            .collect::<Result<Vec<_>, _>>()?;
3359        references.sort();
3360        Ok(references)
3361    }
3362}
3363
3364fn validate_membership_retirement_barriers(
3365    entries: &[MembershipEntry],
3366    checkpoint: Option<&MembershipResolutionCheckpoint>,
3367) -> Result<(), MembershipError> {
3368    for (index, entry) in entries.iter().enumerate() {
3369        if checkpoint.is_some_and(|checkpoint| {
3370            checkpoint.raw_heads.iter().any(|head| {
3371                head.stream_key() == entry.coord().stream_key() && entry.seq <= head.seq
3372            })
3373        }) {
3374            continue;
3375        }
3376        let (retired, barriers) = match &entry.change {
3377            MembershipChange::SetMember {
3378                replaces,
3379                retirement_barriers,
3380                ..
3381            } => (replaces, retirement_barriers),
3382            MembershipChange::RemoveMember {
3383                removes,
3384                retirement_barriers,
3385                ..
3386            } => (removes, retirement_barriers),
3387            MembershipChange::Founder { .. }
3388            | MembershipChange::ProviderAdmin
3389            | MembershipChange::ResolutionActivation { .. } => continue,
3390        };
3391        if retired != &barriers.keys().cloned().collect::<BTreeSet<_>>() {
3392            let barrier_grants = barriers.keys().cloned().collect::<BTreeSet<_>>();
3393            let grant = retired
3394                .symmetric_difference(&barrier_grants)
3395                .next()
3396                .cloned()
3397                .expect("unequal retirement and barrier grant sets have a difference");
3398            return Err(MembershipError::InvalidOwnerRevocationBarrier { index, grant });
3399        }
3400        let included = membership_history_closure(entries, &entry.dependencies);
3401        let causal_past = entries
3402            .iter()
3403            .filter(|candidate| included.contains(&candidate.coord()))
3404            .cloned()
3405            .collect::<Vec<_>>();
3406        let reduced = match checkpoint {
3407            Some(checkpoint) => reduce_store_membership_from_checkpoint(&causal_past, checkpoint)?,
3408            None => reduce_store_membership(&causal_past)?,
3409        };
3410        let CausalGrantStatus::Resolved(reduced) = reduced else {
3411            return Err(MembershipError::Conflict);
3412        };
3413        for (grant, barrier) in barriers {
3414            let Some(record) = reduced.grants.get(grant).and_then(GrantState::active) else {
3415                return Err(MembershipError::InvalidOwnerRevocationBarrier {
3416                    index,
3417                    grant: grant.clone(),
3418                });
3419            };
3420            let expected_streams = entry
3421                .dependencies
3422                .iter()
3423                .filter(|coord| coord.author_owner_grant == *grant)
3424                .cloned()
3425                .collect::<Vec<_>>();
3426            let shape_matches = matches!(
3427                (record.assignment.is_owner(), barrier),
3428                (true, MergeMembershipGrantRetirementBarrier::Owner { .. })
3429                    | (
3430                        false,
3431                        MergeMembershipGrantRetirementBarrier::NonOwner { .. }
3432                    )
3433            );
3434            if !shape_matches || barrier.author_streams().observed_streams != expected_streams {
3435                return Err(MembershipError::InvalidOwnerRevocationBarrier {
3436                    index,
3437                    grant: grant.clone(),
3438                });
3439            }
3440        }
3441    }
3442    Ok(())
3443}
3444
3445fn validate_membership_wrapped_keys(
3446    entries: &[MembershipEntry],
3447    checkpoint: Option<&MembershipResolutionCheckpoint>,
3448) -> Result<(), MembershipError> {
3449    for (index, entry) in entries.iter().enumerate() {
3450        let included = membership_history_closure(entries, &entry.dependencies);
3451        let causal_generation = membership_causal_generation(entries, &entry.dependencies);
3452        let references = match &entry.change {
3453            MembershipChange::SetMember {
3454                user_pubkey,
3455                wrapped_key,
3456                ..
3457            } => {
3458                if wrapped_key.owner_pubkey != entry.author_pubkey
3459                    || wrapped_key.recipient_pubkey != *user_pubkey
3460                    || wrapped_key.generation != causal_generation
3461                    || wrapped_key.validate_identity().is_err()
3462                {
3463                    return Err(MembershipError::InvalidWrappedKeys(index));
3464                }
3465                continue;
3466            }
3467            MembershipChange::RemoveMember {
3468                user_pubkey,
3469                wrapped_keys,
3470                ..
3471            } => (user_pubkey, wrapped_keys),
3472            MembershipChange::Founder { .. }
3473            | MembershipChange::ProviderAdmin
3474            | MembershipChange::ResolutionActivation { .. } => continue,
3475        };
3476        let (removed_pubkey, wrapped_keys) = references;
3477        let rotation_generation = wrapped_keys.first().map(|reference| reference.generation);
3478        if causal_generation.checked_add(1) != rotation_generation
3479            || !wrapped_keys.windows(2).all(|pair| pair[0] < pair[1])
3480            || wrapped_keys.iter().any(|reference| {
3481                reference.owner_pubkey != entry.author_pubkey
3482                    || reference.recipient_pubkey == *removed_pubkey
3483                    || Some(reference.generation) != rotation_generation
3484                    || reference.validate_identity().is_err()
3485            })
3486        {
3487            return Err(MembershipError::InvalidWrappedKeys(index));
3488        }
3489        let causal_past = entries
3490            .iter()
3491            .filter(|candidate| included.contains(&candidate.coord()))
3492            .cloned()
3493            .collect::<Vec<_>>();
3494        let precedes_checkpoint = checkpoint.is_some_and(|checkpoint| {
3495            checkpoint.raw_heads.iter().any(|head| {
3496                head.stream_key() == entry.coord().stream_key() && entry.seq <= head.seq
3497            })
3498        });
3499        let reduced = match (checkpoint, precedes_checkpoint) {
3500            (Some(checkpoint), false) => {
3501                reduce_store_membership_from_checkpoint(&causal_past, checkpoint)?
3502            }
3503            (None, _) | (Some(_), true) => reduce_store_membership(&causal_past)?,
3504        };
3505        let CausalGrantStatus::Resolved(reduced) = reduced else {
3506            return Err(MembershipError::InvalidWrappedKeys(index));
3507        };
3508        let expected_recipients = reduced
3509            .grants
3510            .values()
3511            .filter_map(GrantState::active)
3512            .filter(|record| record.member_pubkey != *removed_pubkey)
3513            .map(|record| record.member_pubkey.clone())
3514            .collect::<BTreeSet<_>>();
3515        let actual_recipients = wrapped_keys
3516            .iter()
3517            .map(|reference| reference.recipient_pubkey.clone())
3518            .collect::<BTreeSet<_>>();
3519        if expected_recipients != actual_recipients || actual_recipients.len() != wrapped_keys.len()
3520        {
3521            return Err(MembershipError::InvalidWrappedKeys(index));
3522        }
3523    }
3524    Ok(())
3525}
3526
3527fn membership_causal_generation(
3528    entries: &[MembershipEntry],
3529    dependencies: &[MembershipCoord],
3530) -> u64 {
3531    let included = membership_history_closure(entries, dependencies);
3532    entries
3533        .iter()
3534        .filter(|candidate| included.contains(&candidate.coord()))
3535        .flat_map(|candidate| match &candidate.change {
3536            MembershipChange::SetMember { wrapped_key, .. } => std::slice::from_ref(wrapped_key),
3537            MembershipChange::RemoveMember { wrapped_keys, .. } => wrapped_keys.as_slice(),
3538            MembershipChange::Founder { .. }
3539            | MembershipChange::ProviderAdmin
3540            | MembershipChange::ResolutionActivation { .. } => &[],
3541        })
3542        .map(|reference| reference.generation)
3543        .max()
3544        .unwrap_or(crate::encryption::INITIAL_KEY_GENERATION)
3545}
3546
3547fn reduce_store_membership(
3548    entries: &[MembershipEntry],
3549) -> Result<CausalGrantStatus<MembershipCoord, StoreAssignment>, MembershipError> {
3550    let normalized = normalize_store_membership(entries);
3551    causal_grants::reduce(&normalized).map_err(map_store_causal_error)
3552}
3553
3554fn reduce_store_membership_from_checkpoint(
3555    entries: &[MembershipEntry],
3556    checkpoint: &MembershipResolutionCheckpoint,
3557) -> Result<CausalGrantStatus<MembershipCoord, StoreAssignment>, MembershipError> {
3558    let checkpoint_by_stream = checkpoint
3559        .raw_heads
3560        .iter()
3561        .map(|coord| (coord.stream_key(), coord))
3562        .collect::<BTreeMap<_, _>>();
3563    let suffix = entries
3564        .iter()
3565        .filter(|entry| {
3566            checkpoint_by_stream
3567                .get(&entry.coord().stream_key())
3568                .is_none_or(|head| entry.seq > head.seq)
3569        })
3570        .cloned()
3571        .collect::<Vec<_>>();
3572    let normalized = normalize_store_membership(&suffix);
3573    let seeds = checkpoint
3574        .grants
3575        .iter()
3576        .map(|(grant, state)| {
3577            let record = state.record();
3578            let record = causal_grants::CausalSeedGrant {
3579                member_pubkey: record.member_pubkey.clone(),
3580                assignment: StoreAssignment {
3581                    role: record.role.clone(),
3582                    provider_account_email: record.provider_account_email.clone(),
3583                },
3584            };
3585            (
3586                grant.clone(),
3587                match state {
3588                    GrantState::Active { .. } => GrantState::Active { record },
3589                    GrantState::Tombstoned { .. } => GrantState::Tombstoned {
3590                        record,
3591                        retirements: GrantRetirements::new(()),
3592                    },
3593                },
3594            )
3595        })
3596        .collect();
3597    causal_grants::reduce_from_checkpoint(
3598        &normalized,
3599        &checkpoint.raw_heads,
3600        &checkpoint.effective_frontier,
3601        &seeds,
3602        &checkpoint.included,
3603    )
3604    .map_err(map_store_causal_error)
3605}
3606
3607fn validate_provider_admin_controls(
3608    entries: &[MembershipEntry],
3609    checkpoint: Option<&MembershipResolutionCheckpoint>,
3610) -> Result<(), MembershipError> {
3611    for (index, entry) in entries.iter().enumerate() {
3612        let Some(super::provider::ProviderAdminMembershipChange { owner_barriers, .. }) =
3613            &entry.provider_admin
3614        else {
3615            continue;
3616        };
3617        let included = membership_history_closure(entries, &entry.dependencies);
3618        let causal_past = entries
3619            .iter()
3620            .filter(|candidate| included.contains(&candidate.coord()))
3621            .cloned()
3622            .collect::<Vec<_>>();
3623        let reduced = match checkpoint {
3624            Some(checkpoint) => reduce_store_membership_from_checkpoint(&causal_past, checkpoint)?,
3625            None => reduce_store_membership(&causal_past)?,
3626        };
3627        let CausalGrantStatus::Resolved(reduced) = reduced else {
3628            return Err(MembershipError::InvalidProviderAdminChange(index));
3629        };
3630        let expected = reduced
3631            .grants
3632            .iter()
3633            .filter(|(_, state)| {
3634                state
3635                    .active()
3636                    .is_some_and(|record| record.assignment.is_owner())
3637            })
3638            .map(|(grant_id, _)| {
3639                let observed_streams = entry
3640                    .dependencies
3641                    .iter()
3642                    .filter(|coord| coord.author_owner_grant == *grant_id)
3643                    .cloned()
3644                    .collect();
3645                (grant_id.clone(), OwnerStreamBarrier { observed_streams })
3646            })
3647            .collect::<BTreeMap<_, _>>();
3648        if *owner_barriers != expected {
3649            return Err(MembershipError::InvalidProviderAdminChange(index));
3650        }
3651    }
3652    Ok(())
3653}
3654
3655fn membership_history_closure(
3656    entries: &[MembershipEntry],
3657    frontier: &[MembershipCoord],
3658) -> BTreeSet<MembershipCoord> {
3659    let by_coord = entries
3660        .iter()
3661        .map(|entry| (entry.coord(), entry))
3662        .collect::<BTreeMap<_, _>>();
3663    let mut pending = frontier.iter().cloned().collect::<BTreeSet<_>>();
3664    let mut included = BTreeSet::new();
3665    while let Some(coord) = pending.pop_first() {
3666        if !included.insert(coord.clone()) {
3667            continue;
3668        }
3669        if let Some(entry) = by_coord.get(&coord) {
3670            pending.extend(entry.dependencies.iter().cloned());
3671        }
3672    }
3673    included
3674}
3675
3676fn normalize_store_membership(
3677    entries: &[MembershipEntry],
3678) -> Vec<CausalEntry<MembershipCoord, StoreAssignment>> {
3679    entries
3680        .iter()
3681        .map(|entry| {
3682            let dependencies = entry
3683                .dependencies
3684                .iter()
3685                .cloned()
3686                .map(|coord| (coord.stream_key(), coord))
3687                .collect();
3688            let change = match &entry.change {
3689                MembershipChange::Founder {
3690                    creation_id,
3691                    owner_pubkey,
3692                    owner_grant_id,
3693                    ..
3694                } => CausalChange::Founder {
3695                    member_pubkey: owner_pubkey.clone(),
3696                    grant_id: owner_grant_id.clone(),
3697                    assignment: StoreAssignment {
3698                        role: StoreMembershipRoleGrant::Owner {
3699                            recovery: OwnerRecoveryAnchorRef::Founder {
3700                                creation_id: *creation_id,
3701                            },
3702                        },
3703                        provider_account_email: None,
3704                    },
3705                },
3706                MembershipChange::SetMember {
3707                    user_pubkey,
3708                    provider_account_email,
3709                    role,
3710                    grant_id,
3711                    membership: _,
3712                    replaces,
3713                    retirement_barriers,
3714                    ..
3715                } => CausalChange::SetMember {
3716                    member_pubkey: user_pubkey.clone(),
3717                    assignment: StoreAssignment {
3718                        role: role.clone(),
3719                        provider_account_email: provider_account_email.clone(),
3720                    },
3721                    grant_id: grant_id.clone(),
3722                    replaces: replaces.clone(),
3723                    owner_barriers: retirement_barriers
3724                        .iter()
3725                        .filter_map(|(grant, barrier)| {
3726                            barrier
3727                                .owner_stream_barrier()
3728                                .map(|barrier| (grant.clone(), barrier))
3729                        })
3730                        .collect(),
3731                },
3732                MembershipChange::RemoveMember {
3733                    user_pubkey,
3734                    removes,
3735                    retirement_barriers,
3736                    ..
3737                } => CausalChange::RemoveMember {
3738                    member_pubkey: user_pubkey.clone(),
3739                    removes: removes.clone(),
3740                    owner_barriers: retirement_barriers
3741                        .iter()
3742                        .filter_map(|(grant, barrier)| {
3743                            barrier
3744                                .owner_stream_barrier()
3745                                .map(|barrier| (grant.clone(), barrier))
3746                        })
3747                        .collect(),
3748                },
3749                MembershipChange::ProviderAdmin => CausalChange::Control,
3750                MembershipChange::ResolutionActivation { .. } => CausalChange::ResolutionActivation,
3751            };
3752            CausalEntry {
3753                coord: entry.coord(),
3754                previous_hash: entry.previous_hash,
3755                dependencies,
3756                change,
3757            }
3758        })
3759        .collect()
3760}
3761
3762fn map_store_grants(
3763    grants: BTreeMap<
3764        MembershipGrantId,
3765        causal_grants::GrantRecord<MembershipCoord, StoreAssignment>,
3766    >,
3767    checkpoint: Option<
3768        &BTreeMap<MembershipGrantId, GrantState<MembershipGrantRecord, MembershipGrantRetirement>>,
3769    >,
3770) -> Result<BTreeMap<MembershipGrantId, MembershipGrantRecord>, MembershipError> {
3771    grants
3772        .into_iter()
3773        .map(|(grant, record)| -> Result<_, MembershipError> {
3774            let creation_authority =
3775                membership_creation_authority(&grant, record.creation, checkpoint)?;
3776            Ok((
3777                grant,
3778                MembershipGrantRecord {
3779                    member_pubkey: record.member_pubkey,
3780                    role: record.assignment.role,
3781                    provider_account_email: record.assignment.provider_account_email,
3782                    creation_authority,
3783                },
3784            ))
3785        })
3786        .collect()
3787}
3788
3789fn resolved_store_membership(
3790    reduced: &causal_grants::ReducedGrants<MembershipCoord, StoreAssignment>,
3791    checkpoint: Option<
3792        &BTreeMap<MembershipGrantId, GrantState<MembershipGrantRecord, MembershipGrantRetirement>>,
3793    >,
3794    provider_admin: super::provider::ProviderAdminResolution,
3795    entries: &[MembershipEntry],
3796) -> Result<ResolvedStoreMembership, MembershipError> {
3797    let grants = reduced
3798        .grants
3799        .iter()
3800        .map(|(grant, state)| -> Result<_, MembershipError> {
3801            Ok((
3802                grant.clone(),
3803                map_store_grant_state(grant, state, checkpoint, entries)?,
3804            ))
3805        })
3806        .collect::<Result<BTreeMap<_, _>, _>>()?;
3807    let state_hash = store_membership_state_hash(&grants, &provider_admin);
3808    Ok(ResolvedStoreMembership {
3809        grants,
3810        provider_admin,
3811        state_hash,
3812    })
3813}
3814
3815fn map_store_grant_state(
3816    grant: &MembershipGrantId,
3817    state: &GrantState<
3818        causal_grants::GrantRecord<MembershipCoord, StoreAssignment>,
3819        causal_grants::CausalGrantRetirement<MembershipCoord>,
3820    >,
3821    checkpoint: Option<
3822        &BTreeMap<MembershipGrantId, GrantState<MembershipGrantRecord, MembershipGrantRetirement>>,
3823    >,
3824    entries: &[MembershipEntry],
3825) -> Result<GrantState<MembershipGrantRecord, MembershipGrantRetirement>, MembershipError> {
3826    let causal_record = state.record();
3827    let record = MembershipGrantRecord {
3828        member_pubkey: causal_record.member_pubkey.clone(),
3829        role: causal_record.assignment.role.clone(),
3830        provider_account_email: causal_record.assignment.provider_account_email.clone(),
3831        creation_authority: membership_creation_authority(
3832            grant,
3833            causal_record.creation.clone(),
3834            checkpoint,
3835        )?,
3836    };
3837    causal_grants::try_map_grant_state(
3838        state,
3839        record,
3840        checkpoint
3841            .and_then(|grants| grants.get(grant))
3842            .and_then(GrantState::retirements),
3843        || MembershipError::MissingCheckpointRetirementEvidence {
3844            grant: grant.clone(),
3845        },
3846        |coord, _owner_barrier| {
3847            Ok(MembershipGrantRetirement::Entry {
3848                authority: coord.clone(),
3849                barrier: membership_retirement_barrier(entries, coord, grant).ok_or_else(|| {
3850                    MembershipError::MissingRetirementBarrier {
3851                        grant: grant.clone(),
3852                        authority: Box::new(coord.clone()),
3853                    }
3854                })?,
3855            })
3856        },
3857    )
3858}
3859
3860fn membership_retirement_barrier(
3861    entries: &[MembershipEntry],
3862    authority: &MembershipCoord,
3863    grant: &MembershipGrantId,
3864) -> Option<MergeMembershipGrantRetirementBarrier> {
3865    let entry = entries.iter().find(|entry| entry.coord() == *authority)?;
3866    let barriers = match &entry.change {
3867        MembershipChange::SetMember {
3868            retirement_barriers,
3869            ..
3870        }
3871        | MembershipChange::RemoveMember {
3872            retirement_barriers,
3873            ..
3874        } => retirement_barriers,
3875        MembershipChange::Founder { .. }
3876        | MembershipChange::ProviderAdmin
3877        | MembershipChange::ResolutionActivation { .. } => return None,
3878    };
3879    barriers.get(grant).cloned()
3880}
3881
3882fn membership_creation_authority(
3883    grant: &MembershipGrantId,
3884    creation: causal_grants::CausalGrantCreation<MembershipCoord>,
3885    checkpoint: Option<
3886        &BTreeMap<MembershipGrantId, GrantState<MembershipGrantRecord, MembershipGrantRetirement>>,
3887    >,
3888) -> Result<MembershipGrantCreationAuthority, MembershipError> {
3889    match creation {
3890        causal_grants::CausalGrantCreation::Entry(coord) => {
3891            Ok(MembershipGrantCreationAuthority::Entry(coord))
3892        }
3893        causal_grants::CausalGrantCreation::Checkpoint => checkpoint
3894            .and_then(|grants| grants.get(grant))
3895            .ok_or_else(|| MembershipError::MissingCheckpointGrant {
3896                grant: grant.clone(),
3897            })
3898            .map(|state| state.record().creation_authority.clone()),
3899    }
3900}
3901
3902fn store_membership_state_hash(
3903    grants: &BTreeMap<
3904        MembershipGrantId,
3905        GrantState<MembershipGrantRecord, MembershipGrantRetirement>,
3906    >,
3907    provider_admin: &super::provider::ProviderAdminResolution,
3908) -> ObjectHash {
3909    #[derive(Serialize)]
3910    struct State<'a> {
3911        domain: &'static str,
3912        grants: &'a BTreeMap<
3913            MembershipGrantId,
3914            GrantState<MembershipGrantRecord, MembershipGrantRetirement>,
3915        >,
3916        provider_admin: &'a super::provider::ProviderAdminResolution,
3917    }
3918    ObjectHash::digest(
3919        &serde_json::to_vec(&State {
3920            domain: "coven.store-membership-state.v2",
3921            grants,
3922            provider_admin,
3923        })
3924        .expect("Store membership state serialization cannot fail"),
3925    )
3926}
3927
3928fn membership_assignment_conflict_hash(
3929    heads: &[MembershipHeadRef],
3930    member_pubkey: &str,
3931    conflicting_grants: &BTreeMap<
3932        MembershipGrantId,
3933        causal_grants::GrantRecord<MembershipCoord, StoreAssignment>,
3934    >,
3935) -> ObjectHash {
3936    #[derive(Serialize)]
3937    struct Conflict<'a> {
3938        domain: &'static str,
3939        heads: &'a [MembershipHeadRef],
3940        member_pubkey: &'a str,
3941        conflicting_grant_ids: Vec<&'a MembershipGrantId>,
3942    }
3943    ObjectHash::digest(
3944        &serde_json::to_vec(&Conflict {
3945            domain: "coven.store-membership-assignment-conflict.v1",
3946            heads,
3947            member_pubkey,
3948            conflicting_grant_ids: conflicting_grants.keys().collect(),
3949        })
3950        .expect("Store membership conflict serialization cannot fail"),
3951    )
3952}
3953
3954fn membership_revocation_conflict_hash(
3955    heads: &[MembershipHeadRef],
3956    cyclic_sources: &[MembershipCoord],
3957    involved_owner_grants: &BTreeSet<MembershipGrantId>,
3958) -> ObjectHash {
3959    #[derive(Serialize)]
3960    struct Conflict<'a> {
3961        domain: &'static str,
3962        heads: &'a [MembershipHeadRef],
3963        cyclic_sources: &'a [MembershipCoord],
3964        involved_owner_grants: &'a BTreeSet<MembershipGrantId>,
3965    }
3966    ObjectHash::digest(
3967        &serde_json::to_vec(&Conflict {
3968            domain: "coven.store-membership-revocation-conflict.v1",
3969            heads,
3970            cyclic_sources,
3971            involved_owner_grants,
3972        })
3973        .expect("Store membership revocation conflict serialization cannot fail"),
3974    )
3975}
3976
3977fn shared_store_barrier(barrier: &StoreGrantStreamBarrier) -> OwnerGrantBarrier<MembershipCoord> {
3978    let observed_streams = barrier
3979        .observed_streams
3980        .iter()
3981        .cloned()
3982        .map(|coord| (coord.stream_key(), coord))
3983        .collect();
3984    OwnerGrantBarrier { observed_streams }
3985}
3986
3987fn map_store_causal_error(error: CausalGrantError<MembershipCoord>) -> MembershipError {
3988    match error {
3989        CausalGrantError::Empty => MembershipError::EmptyChain,
3990        CausalGrantError::ConflictingSequence { stream, seq } => {
3991            MembershipError::ConflictingSequence {
3992                author: stream.author_pubkey,
3993                grant: stream.author_owner_grant,
3994                seq,
3995            }
3996        }
3997        CausalGrantError::MissingSequence { stream, seq } => MembershipError::MissingSequence {
3998            author: stream.author_pubkey,
3999            grant: stream.author_owner_grant,
4000            seq,
4001        },
4002        CausalGrantError::BrokenStreamLink {
4003            index,
4004            expected,
4005            actual,
4006        } => MembershipError::BrokenStreamLink {
4007            index,
4008            expected,
4009            actual,
4010        },
4011        CausalGrantError::MissingOwnDependency { index } => {
4012            MembershipError::MissingOwnDependency { index }
4013        }
4014        CausalGrantError::DependencyStreamMismatch { .. } => {
4015            unreachable!("Store dependencies are normalized from their signed coordinates")
4016        }
4017        CausalGrantError::MissingDependency { index, dependency } => {
4018            MembershipError::MissingDependency {
4019                index,
4020                dependency: Box::new(dependency),
4021            }
4022        }
4023        CausalGrantError::DependencyCycle => MembershipError::DependencyCycle,
4024        CausalGrantError::InvalidFounder => MembershipError::InvalidFounder,
4025        CausalGrantError::AuthorGrantInactive { index, grant } => {
4026            MembershipError::AuthorGrantInactive { index, grant }
4027        }
4028        CausalGrantError::DuplicateGrant { index, grant } => {
4029            MembershipError::DuplicateGrant { index, grant }
4030        }
4031        CausalGrantError::GrantOwnerMismatch { index, grant } => {
4032            MembershipError::GrantOwnerMismatch { index, grant }
4033        }
4034        CausalGrantError::GrantSetMismatch {
4035            index,
4036            member_pubkey,
4037        } => MembershipError::GrantSetMismatch {
4038            index,
4039            pubkey: member_pubkey,
4040        },
4041        CausalGrantError::EmptyRemoval { index } => MembershipError::EmptyRemoval { index },
4042        CausalGrantError::MissingOwnerRevocationBarrier { index, grant } => {
4043            MembershipError::MissingOwnerRevocationBarrier { index, grant }
4044        }
4045        CausalGrantError::InvalidOwnerRevocationBarrier { index, grant } => {
4046            MembershipError::InvalidOwnerRevocationBarrier { index, grant }
4047        }
4048        CausalGrantError::NoActiveOwner => MembershipError::NoActiveOwner,
4049        CausalGrantError::RevocationCycleTooWide { sources, maximum } => {
4050            MembershipError::RevocationCycleTooWide { sources, maximum }
4051        }
4052    }
4053}
4054
4055pub fn derive_founder_grant_id(store_id: &str, owner_pubkey: &str) -> MembershipGrantId {
4056    MembershipGrantId(ObjectHash::digest(
4057        format!("coven.membership-founder-grant.v1\0{store_id}\0{owner_pubkey}").as_bytes(),
4058    ))
4059}
4060
4061pub(crate) fn derive_founder_stream_id(store_id: &str, owner_pubkey: &str) -> AuthorStreamId {
4062    AuthorStreamId::from_digest(ObjectHash::digest(
4063        format!("coven.membership-founder-stream.v1\0{store_id}\0{owner_pubkey}").as_bytes(),
4064    ))
4065}
4066
4067fn store_membership_anchor_stream(
4068    owner_pubkey: &str,
4069    owner_grant: &MembershipGrantId,
4070    anchor: &GrantStreamAnchor,
4071) -> Option<AuthorStreamId> {
4072    let GrantStreamAnchor::StoreMembership { first_slot } = anchor else {
4073        return None;
4074    };
4075    let prefix = format!(
4076        "{}{owner_pubkey}/{owner_grant}/",
4077        super::store_commit::STORE_MEMBERSHIP_HEAD_PREFIX,
4078    );
4079    first_slot
4080        .logical_key()
4081        .strip_prefix(&prefix)?
4082        .strip_suffix("/1.json")?
4083        .parse()
4084        .ok()
4085}
4086
4087pub fn derive_grant_id(
4088    store_id: &str,
4089    author_pubkey: &str,
4090    author_grant: &MembershipGrantId,
4091    stream_id: AuthorStreamId,
4092    seq: u64,
4093    user_pubkey: &str,
4094) -> MembershipGrantId {
4095    MembershipGrantId(ObjectHash::digest(
4096        format!(
4097            "coven.membership-grant.v1\0{store_id}\0{author_pubkey}\0{author_grant}\0{stream_id}\0{seq}\0{user_pubkey}"
4098        )
4099        .as_bytes(),
4100    ))
4101}
4102
4103pub fn founder_entry_for_creation(
4104    store_id: &str,
4105    creation_id: StoreCreationId,
4106    owner: &UserKeypair,
4107    owner_grant_id: MembershipGrantId,
4108    created_at: &str,
4109    membership: GrantStreamAnchor,
4110    provider_admin: super::provider::FounderProviderAdminGrant,
4111) -> MembershipEntry {
4112    let owner_pubkey = keys::public_key_hex(owner);
4113    let stream_id = derive_founder_stream_id(store_id, &owner_pubkey);
4114    let mut entry = MembershipEntry {
4115        version: STORE_PROTOCOL_VERSION,
4116        store_id: store_id.to_string(),
4117        author_pubkey: owner_pubkey.clone(),
4118        author_owner_grant: owner_grant_id.clone(),
4119        stream_id,
4120        seq: 1,
4121        previous_hash: None,
4122        dependencies: Vec::new(),
4123        resolution_dependencies: Vec::new(),
4124        created_at: created_at.to_string(),
4125        change: MembershipChange::Founder {
4126            creation_id,
4127            owner_pubkey,
4128            owner_grant_id,
4129            membership,
4130            provider_admin,
4131        },
4132        provider_admin: None,
4133        signature: String::new(),
4134    };
4135    sign_membership_entry(&mut entry, owner);
4136    entry
4137}
4138
4139#[cfg(any(test, feature = "test-utils"))]
4140pub fn founder_entry(
4141    store_id: &str,
4142    owner: &UserKeypair,
4143    owner_grant_id: MembershipGrantId,
4144    created_at: &str,
4145    membership: GrantStreamAnchor,
4146    provider_admin: super::provider::FounderProviderAdminGrant,
4147) -> MembershipEntry {
4148    founder_entry_for_creation(
4149        store_id,
4150        StoreCreationId::from_nonce(store_id),
4151        owner,
4152        owner_grant_id,
4153        created_at,
4154        membership,
4155        provider_admin,
4156    )
4157}
4158
4159pub fn canonical_bytes(entry: &MembershipEntry) -> Vec<u8> {
4160    #[derive(Serialize)]
4161    struct Signed<'a> {
4162        version: u32,
4163        store_id: &'a str,
4164        author_pubkey: &'a str,
4165        author_owner_grant: &'a MembershipGrantId,
4166        stream_id: AuthorStreamId,
4167        seq: u64,
4168        #[serde(skip_serializing_if = "Option::is_none")]
4169        previous_hash: Option<ObjectHash>,
4170        dependencies: &'a [MembershipCoord],
4171        resolution_dependencies: &'a [StoreMembershipConflictResolutionRef],
4172        created_at: &'a str,
4173        change: &'a MembershipChange,
4174        #[serde(skip_serializing_if = "Option::is_none")]
4175        provider_admin: Option<&'a super::provider::ProviderAdminMembershipChange>,
4176    }
4177    serde_json::to_vec(&Signed {
4178        version: entry.version,
4179        store_id: &entry.store_id,
4180        author_pubkey: &entry.author_pubkey,
4181        author_owner_grant: &entry.author_owner_grant,
4182        stream_id: entry.stream_id,
4183        seq: entry.seq,
4184        previous_hash: entry.previous_hash,
4185        dependencies: &entry.dependencies,
4186        resolution_dependencies: &entry.resolution_dependencies,
4187        created_at: &entry.created_at,
4188        change: &entry.change,
4189        provider_admin: entry.provider_admin.as_ref(),
4190    })
4191    .expect("membership signed fields serialize")
4192}
4193
4194pub fn entry_hash(entry: &MembershipEntry) -> ObjectHash {
4195    ObjectHash::digest(
4196        &serde_json::to_vec(entry).expect("membership entry serialization cannot fail"),
4197    )
4198}
4199
4200pub fn sign_membership_entry(entry: &mut MembershipEntry, keypair: &UserKeypair) {
4201    entry.author_pubkey = keys::public_key_hex(keypair);
4202    let (_, signature) = keys::sign_hex(keypair, &canonical_bytes(entry));
4203    entry.signature = signature;
4204}
4205
4206pub fn verify_membership_entry(entry: &MembershipEntry) -> bool {
4207    let activation_position_is_valid = match &entry.change {
4208        MembershipChange::ResolutionActivation { .. } => {
4209            entry.seq == 1
4210                && entry.previous_hash.is_none()
4211                && entry
4212                    .dependencies
4213                    .iter()
4214                    .all(|dependency| dependency.stream_key() != entry.coord().stream_key())
4215        }
4216        _ => true,
4217    };
4218    activation_position_is_valid
4219        && entry
4220            .resolution_dependencies
4221            .windows(2)
4222            .all(|pair| pair[0] < pair[1])
4223        && keys::verify_signature_hex(
4224            &entry.author_pubkey,
4225            &entry.signature,
4226            &canonical_bytes(entry),
4227        )
4228}
4229
4230impl AuthorHead {
4231    pub fn signed(
4232        store_id: String,
4233        mut body: MembershipHeadBody,
4234        activation: MembershipHeadActivation,
4235        device_signer: &UserKeypair,
4236    ) -> Self {
4237        body.resolutions.sort();
4238        body.resolutions.dedup();
4239        let mut head = Self {
4240            version: STORE_PROTOCOL_VERSION,
4241            store_id,
4242            body,
4243            activation,
4244            signature: String::new(),
4245        };
4246        let (_, signature) = keys::sign_hex(device_signer, &head.canonical_bytes());
4247        head.signature = signature;
4248        head
4249    }
4250
4251    pub fn verify(&self, registration: &StoreDeviceRegistration) -> bool {
4252        self.version == STORE_PROTOCOL_VERSION
4253            && self
4254                .body
4255                .resolutions
4256                .windows(2)
4257                .all(|pair| pair[0] < pair[1])
4258            && self
4259                .body
4260                .author_registration
4261                .verify_registration(registration)
4262                .is_ok()
4263            && registration.author_pubkey == self.body.entry.coord.author_pubkey
4264            && self.body.successor.predecessor
4265                == self
4266                    .body
4267                    .predecessor
4268                    .as_ref()
4269                    .map(|reference| reference.object.clone())
4270            && keys::verify_signature_hex(
4271                &registration.device_signing_pubkey,
4272                &self.signature,
4273                &self.canonical_bytes(),
4274            )
4275    }
4276
4277    pub fn entry_coord(&self) -> MembershipCoord {
4278        self.body.entry.coord.clone()
4279    }
4280
4281    pub fn head_hash(&self) -> ObjectHash {
4282        ObjectHash::digest(
4283            &serde_json::to_vec(self).expect("membership head serialization cannot fail"),
4284        )
4285    }
4286
4287    fn canonical_bytes(&self) -> Vec<u8> {
4288        #[derive(Serialize)]
4289        struct Signed<'a> {
4290            version: u32,
4291            store_id: &'a str,
4292            body: &'a MembershipHeadBody,
4293            activation: &'a MembershipHeadActivation,
4294        }
4295        serde_json::to_vec(&Signed {
4296            version: self.version,
4297            store_id: &self.store_id,
4298            body: &self.body,
4299            activation: &self.activation,
4300        })
4301        .expect("membership head signed fields serialize")
4302    }
4303}
4304
4305#[cfg(test)]
4306mod tests {
4307    use super::*;
4308    use crate::storage::cloud::ObjectSlot;
4309    use crate::sync::storage::{ProviderDeviceBinding, ProviderPrincipalId};
4310    use crate::sync::store_commit::{
4311        membership_entry_semantic_prefix, membership_head_semantic_prefix,
4312        membership_resolution_semantic_prefix, registration_semantic_prefix, CommitFrontier,
4313        DeviceStreamAnchor, GrantStreamAnchor, ResolvedStoreDeviceState, StoreCreationId,
4314        StoreDeviceRegistrationOrigin, StoreDeviceRegistrationRef, StoreDeviceStateRef,
4315        StoreRootRef, StreamActivation,
4316    };
4317
4318    fn key() -> UserKeypair {
4319        UserKeypair::generate()
4320    }
4321
4322    fn stream(byte: u8) -> AuthorStreamId {
4323        AuthorStreamId::from_bytes([byte; 32])
4324    }
4325
4326    fn slot(key: impl Into<String>) -> ObjectSlot {
4327        ObjectSlot::logical(key.into()).expect("valid test object slot")
4328    }
4329
4330    fn exact(key: impl Into<String>, bytes: &[u8]) -> ExactObjectRef {
4331        ExactObjectRef::new(slot(key), bytes.len() as u64, ObjectHash::digest(bytes))
4332    }
4333
4334    fn membership_anchor(store_id: &str) -> GrantStreamAnchor {
4335        GrantStreamAnchor::StoreMembership {
4336            first_slot: slot(format!("test/{store_id}/membership/1.json")),
4337        }
4338    }
4339
4340    fn recovery_anchor(store_id: &str) -> GrantStreamAnchor {
4341        GrantStreamAnchor::OwnerRecovery {
4342            first_slot: slot(format!("test/{store_id}/recovery/1.json")),
4343        }
4344    }
4345
4346    fn test_founder_entry(
4347        store_id: &str,
4348        owner: &UserKeypair,
4349        created_at: &str,
4350        membership: GrantStreamAnchor,
4351    ) -> MembershipEntry {
4352        founder_entry(
4353            store_id,
4354            owner,
4355            crate::sync::test_helpers::test_membership_grant_id(store_id),
4356            created_at,
4357            membership,
4358            crate::sync::test_helpers::test_founder_provider_admin(store_id),
4359        )
4360    }
4361
4362    fn test_root(store_id: &str) -> StoreRootRef {
4363        let bytes = store_id.as_bytes();
4364        StoreRootRef {
4365            store_root_id: ObjectHash::digest(format!("{store_id} identity").as_bytes()),
4366            store_root_hash: ObjectHash::digest(bytes),
4367            object: exact(format!("test/{store_id}/root.json"), bytes),
4368        }
4369    }
4370
4371    fn registration(
4372        root: &StoreRootRef,
4373        label: &str,
4374        signer: &UserKeypair,
4375    ) -> (StoreDeviceRegistration, StoreDeviceRegistrationRef) {
4376        let registration = StoreDeviceRegistration::signed(
4377            root.clone(),
4378            StoreDeviceRegistrationOrigin::Founder {
4379                creation_id: StoreCreationId::from_nonce(label),
4380            },
4381            ProviderDeviceBinding {
4382                principal: ProviderPrincipalId::CustomS3Credential {
4383                    access_key_id_hash: ObjectHash::digest(label.as_bytes()),
4384                },
4385            },
4386            DeviceStreamAnchor::StoreAnnouncements {
4387                first_slot: slot(format!("test/{label}/announcements/1.json")),
4388            },
4389            DeviceStreamAnchor::StoreAcknowledgements {
4390                first_slot: slot(format!("test/{label}/acks/1.json")),
4391            },
4392            DeviceStreamAnchor::StoreSnapshots {
4393                first_slot: slot(format!("test/{label}/snapshots/1.json")),
4394            },
4395            signer,
4396        )
4397        .expect("sign test registration");
4398        let bytes = registration.to_bytes();
4399        let reference = StoreDeviceRegistrationRef::from_registration(
4400            &registration,
4401            exact(
4402                format!(
4403                    "{}.json",
4404                    registration_semantic_prefix(&registration.device_id.to_string())
4405                ),
4406                &bytes,
4407            ),
4408        );
4409        (registration, reference)
4410    }
4411
4412    fn conflict_acceptance(
4413        chain: &MembershipChain,
4414        store_root_hash: ObjectHash,
4415        membership: GrantStreamAnchor,
4416        signer: &UserKeypair,
4417    ) -> OwnerConflictResolutionAcceptance {
4418        let (conflict_hash, owner_grants) = match chain
4419            .conflict()
4420            .expect("test chain has a membership conflict")
4421        {
4422            MembershipConflict::ConcurrentMemberAssignments {
4423                conflict_hash,
4424                grants,
4425                ..
4426            } => (
4427                conflict_hash,
4428                grants
4429                    .iter()
4430                    .filter_map(|(grant, state)| {
4431                        state
4432                            .active()
4433                            .filter(|record| record.role.is_owner())
4434                            .map(|record| (grant.clone(), record.clone()))
4435                    })
4436                    .collect::<Vec<_>>(),
4437            ),
4438            MembershipConflict::RevocationCycle {
4439                conflict_hash,
4440                maximal_valid_branches,
4441                ..
4442            } => (
4443                conflict_hash,
4444                maximal_valid_branches
4445                    .iter()
4446                    .flat_map(StoreMembershipBranch::active_grants)
4447                    .filter(|(_, record)| record.role.is_owner())
4448                    .map(|(grant, record)| (grant.clone(), record.clone()))
4449                    .collect::<Vec<_>>(),
4450            ),
4451        };
4452        let resolver_pubkey = keys::public_key_hex(signer);
4453        let root = StoreRootRef {
4454            store_root_id: ObjectHash::digest(b"test conflict-resolution root id"),
4455            store_root_hash,
4456            object: exact(
4457                "test/conflict-resolution/root.json",
4458                b"conflict resolution root",
4459            ),
4460        };
4461        let (registration, owner_registration) = registration(
4462            &root,
4463            &format!("conflict-resolution-{resolver_pubkey}"),
4464            signer,
4465        );
4466        let mut recovery = owner_grants
4467            .into_iter()
4468            .map(|(grant, record)| OwnerRecoveryCursor {
4469                owner_grant: grant.clone(),
4470                position: OwnerRecoveryPosition::At {
4471                    node: OwnerRecoveryNodeRef {
4472                        owner_pubkey: record.member_pubkey,
4473                        owner_grant: grant.clone(),
4474                        sequence: 1,
4475                        node_hash: ObjectHash::digest(
4476                            format!("conflict recovery {grant}").as_bytes(),
4477                        ),
4478                        object: exact(
4479                            format!("test/conflict-recovery/{grant}/1.json"),
4480                            format!("conflict recovery {grant}").as_bytes(),
4481                        ),
4482                    },
4483                },
4484            })
4485            .collect::<Vec<_>>();
4486        recovery.sort();
4487        recovery.dedup_by(|left, right| left.owner_grant == right.owner_grant);
4488        OwnerConflictResolutionAcceptance {
4489            store_root_hash,
4490            owner_grant: derive_store_resolution_grant(conflict_hash, &resolver_pubkey),
4491            owner_registration,
4492            provider: registration.provider,
4493            membership,
4494            recovery: recovery_anchor(&format!("conflict-resolution-{resolver_pubkey}")),
4495            device_state: StoreDeviceStateRef::from_resolved(
4496                CommitFrontier(BTreeMap::new()),
4497                &ResolvedStoreDeviceState {
4498                    devices: BTreeMap::new(),
4499                    recovery,
4500                    state_hash: ObjectHash::digest(b"test conflict-resolution device state"),
4501                },
4502            )
4503            .expect("construct conflict-resolution device state"),
4504            signature: String::new(),
4505        }
4506    }
4507
4508    fn exact_head(
4509        entry: &MembershipEntry,
4510        signer: &UserKeypair,
4511    ) -> (MembershipHeadRef, AuthorHead) {
4512        exact_head_with_resolutions(entry, signer, entry.resolution_dependencies.clone())
4513    }
4514
4515    fn exact_head_with_resolutions(
4516        entry: &MembershipEntry,
4517        signer: &UserKeypair,
4518        resolutions: Vec<StoreMembershipConflictResolutionRef>,
4519    ) -> (MembershipHeadRef, AuthorHead) {
4520        let root = test_root(&entry.store_id);
4521        let (registration, registration_ref) = registration(
4522            &root,
4523            &format!("{}-{}", entry.store_id, entry.author_pubkey),
4524            signer,
4525        );
4526        let entry_bytes = serde_json::to_vec(entry).expect("serialize membership entry");
4527        let coord = entry.coord();
4528        let entry_ref = MembershipEntryRef {
4529            coord: coord.clone(),
4530            object: exact(
4531                format!(
4532                    "{}.json",
4533                    membership_entry_semantic_prefix(
4534                        &coord.author_pubkey,
4535                        &coord.author_owner_grant,
4536                        coord.stream_id,
4537                        coord.seq,
4538                        coord.entry_hash,
4539                    )
4540                ),
4541                &entry_bytes,
4542            ),
4543        };
4544        let anchor = membership_anchor(&entry.store_id);
4545        let successor = SuccessorLink {
4546            activation: StreamActivation::grant_authorized(
4547                root.store_root_hash,
4548                registration_ref.clone(),
4549                entry.author_owner_grant.clone(),
4550                anchor,
4551            )
4552            .activation_id(),
4553            predecessor: None,
4554            next_slot: slot(format!(
4555                "test/{}/membership-heads/{}/next.json",
4556                entry.store_id, coord.entry_hash
4557            )),
4558        };
4559        let device_signer = registration.device_signer(signer).unwrap();
4560        let head = AuthorHead::signed(
4561            entry.store_id.clone(),
4562            MembershipHeadBody {
4563                author_registration: registration_ref,
4564                entry: entry_ref,
4565                predecessor: None,
4566                resolutions,
4567                successor,
4568            },
4569            MembershipHeadActivation::Direct,
4570            &device_signer,
4571        );
4572        let head_bytes = serde_json::to_vec(&head).expect("serialize membership head");
4573        let reference = MembershipHeadRef {
4574            coord: coord.clone(),
4575            head_hash: head.head_hash(),
4576            object: exact(
4577                format!(
4578                    "{}.json",
4579                    membership_head_semantic_prefix(
4580                        &coord.author_pubkey,
4581                        &coord.author_owner_grant,
4582                        coord.stream_id,
4583                        coord.seq,
4584                        head.head_hash(),
4585                    )
4586                ),
4587                &head_bytes,
4588            ),
4589        };
4590        (reference, head)
4591    }
4592
4593    fn exact_resolution(
4594        resolution: StoreMembershipConflictResolution,
4595    ) -> (
4596        StoreMembershipConflictResolutionRef,
4597        StoreMembershipConflictResolution,
4598    ) {
4599        let bytes = serde_json::to_vec(&resolution).expect("serialize membership resolution");
4600        let reference = resolution.resolution_ref(exact(
4601            format!(
4602                "{}.json",
4603                membership_resolution_semantic_prefix(
4604                    resolution.conflict_hash,
4605                    &resolution.resolver_pubkey,
4606                    resolution.resolution_hash(),
4607                )
4608            ),
4609            &bytes,
4610        ));
4611        (reference, resolution)
4612    }
4613
4614    fn founded(store_id: &str, owner: &UserKeypair) -> MembershipChain {
4615        MembershipChain::from_entries(vec![test_founder_entry(
4616            store_id,
4617            owner,
4618            "founder",
4619            membership_anchor(store_id),
4620        )])
4621        .unwrap()
4622    }
4623
4624    #[test]
4625    fn membership_head_requires_an_explicit_activation_rule() {
4626        let owner = key();
4627        let entry = test_founder_entry(
4628            "required-head-activation",
4629            &owner,
4630            "founder",
4631            membership_anchor("required-head-activation"),
4632        );
4633        let (_, head) = exact_head(&entry, &owner);
4634        let mut encoded = serde_json::to_value(head).expect("serialize membership head");
4635        encoded
4636            .as_object_mut()
4637            .expect("membership head object")
4638            .remove("activation");
4639        assert!(serde_json::from_value::<AuthorHead>(encoded).is_err());
4640    }
4641
4642    #[test]
4643    fn reserved_membership_transition_and_published_head_share_one_body() {
4644        let owner = key();
4645        let entry = test_founder_entry(
4646            "shared-head-body",
4647            &owner,
4648            "founder",
4649            membership_anchor("shared-head-body"),
4650        );
4651        let (reference, head) = exact_head(&entry, &owner);
4652        let transition = MergeMembershipHeadTransition {
4653            body: head.body.clone(),
4654            head_slot: reference.object.slot().clone(),
4655        };
4656        let encoded = serde_json::to_vec(&transition).expect("serialize reserved transition");
4657        let decoded: MergeMembershipHeadTransition =
4658            serde_json::from_slice(&encoded).expect("parse reserved transition");
4659        assert_eq!(decoded, transition);
4660        assert!(decoded.matches_head(&head, &reference));
4661
4662        let mut mismatched = decoded;
4663        mismatched.body.successor.next_slot = slot("test/shared-head-body/another-next.json");
4664        assert!(!mismatched.matches_head(&head, &reference));
4665    }
4666
4667    #[test]
4668    fn merge_active_grant_lookup_returns_only_the_exact_live_record() {
4669        let owner = key();
4670        let member = key();
4671        let member_pubkey = keys::public_key_hex(&member);
4672        let mut chain = founded("exact-live-merge-grant", &owner);
4673        let addition = chain
4674            .signed_set_member_in_stream(
4675                &owner,
4676                stream(1),
4677                member_pubkey.clone(),
4678                None,
4679                MemberRole::Member,
4680                "add member".to_string(),
4681            )
4682            .unwrap();
4683        let MembershipChange::SetMember { grant_id, .. } = &addition.change else {
4684            unreachable!()
4685        };
4686        let grant_id = grant_id.clone();
4687        chain.add_entry(addition).unwrap();
4688        let MembershipStatus::Resolved(resolved) = chain.status() else {
4689            panic!("membership must resolve")
4690        };
4691        assert_eq!(
4692            chain.active_grant(&grant_id),
4693            resolved.active_grant(&grant_id)
4694        );
4695        assert!(chain
4696            .active_grant(&MembershipGrantId(ObjectHash::digest(b"absent grant")))
4697            .is_none());
4698
4699        let removal = chain
4700            .signed_remove_member_in_stream(
4701                &owner,
4702                stream(1),
4703                member_pubkey.clone(),
4704                "remove member".to_string(),
4705            )
4706            .unwrap();
4707        let retirement_authority = removal.coord();
4708        chain.add_entry(removal).unwrap();
4709        assert!(chain.active_grant(&grant_id).is_none());
4710        let MembershipStatus::Resolved(resolved) = chain.status() else {
4711            panic!("membership must resolve")
4712        };
4713        assert!(matches!(
4714            &resolved.grants[&grant_id],
4715            GrantState::Tombstoned { record, retirements }
4716                if record.member_pubkey == member_pubkey
4717                    && retirements.as_set() == &BTreeSet::from([MembershipGrantRetirement::Entry {
4718                        authority: retirement_authority.clone(),
4719                        barrier: MergeMembershipGrantRetirementBarrier::NonOwner {
4720                            author_streams: StoreGrantStreamBarrier {
4721                                observed_streams: Vec::new(),
4722                            },
4723                        },
4724                    }])
4725        ));
4726        let mut altered = resolved.grants.clone();
4727        let GrantState::Tombstoned { retirements, .. } = altered
4728            .get_mut(&grant_id)
4729            .expect("retired Merge grant remains present")
4730        else {
4731            unreachable!()
4732        };
4733        retirements.insert(MembershipGrantRetirement::Entry {
4734            authority: MembershipCoord {
4735                entry_hash: ObjectHash::digest(b"different retirement entry"),
4736                ..retirement_authority.clone()
4737            },
4738            barrier: MergeMembershipGrantRetirementBarrier::NonOwner {
4739                author_streams: StoreGrantStreamBarrier {
4740                    observed_streams: Vec::new(),
4741                },
4742            },
4743        });
4744        assert_ne!(
4745            resolved.state_hash,
4746            store_membership_state_hash(&altered, &resolved.provider_admin)
4747        );
4748
4749        let mut reuse = chain
4750            .signed_set_member_in_stream(
4751                &owner,
4752                stream(1),
4753                member_pubkey,
4754                None,
4755                MemberRole::Member,
4756                "reuse retired grant".to_string(),
4757            )
4758            .unwrap();
4759        let MembershipChange::SetMember {
4760            grant_id: candidate,
4761            ..
4762        } = &mut reuse.change
4763        else {
4764            unreachable!()
4765        };
4766        *candidate = grant_id.clone();
4767        sign_membership_entry(&mut reuse, &owner);
4768        assert!(matches!(
4769            chain.add_entry(reuse),
4770            Err(MembershipError::DuplicateGrant {
4771                grant,
4772                ..
4773            }) if grant == grant_id
4774        ));
4775    }
4776
4777    #[test]
4778    fn grant_mapping_returns_an_error_when_signed_retirement_evidence_is_absent() {
4779        let owner = key();
4780        let founder = test_founder_entry(
4781            "missing-retirement-evidence",
4782            &owner,
4783            "founder",
4784            membership_anchor("missing-retirement-evidence"),
4785        );
4786        let MembershipChange::Founder { owner_grant_id, .. } = &founder.change else {
4787            panic!("test entry is the founder")
4788        };
4789        let owner_grant_id = owner_grant_id.clone();
4790        let authority = MembershipCoord {
4791            author_pubkey: keys::public_key_hex(&owner),
4792            author_owner_grant: owner_grant_id.clone(),
4793            stream_id: stream(77),
4794            seq: 1,
4795            entry_hash: ObjectHash::digest(b"missing retirement authority"),
4796        };
4797        let state = GrantState::Tombstoned {
4798            record: causal_grants::GrantRecord {
4799                member_pubkey: keys::public_key_hex(&owner),
4800                assignment: StoreAssignment {
4801                    role: StoreMembershipRoleGrant::Member,
4802                    provider_account_email: None,
4803                },
4804                creation: causal_grants::CausalGrantCreation::Entry(founder.coord()),
4805            },
4806            retirements: GrantRetirements::new(causal_grants::CausalGrantRetirement::Entry {
4807                coord: authority.clone(),
4808                owner_barrier: None,
4809            }),
4810        };
4811
4812        assert!(matches!(
4813            map_store_grant_state(&owner_grant_id, &state, None, &[founder]),
4814            Err(MembershipError::MissingRetirementBarrier {
4815                grant,
4816                authority: missing,
4817            }) if grant == owner_grant_id && *missing == authority
4818        ));
4819    }
4820
4821    #[test]
4822    fn concurrent_effective_removals_union_exact_retirement_entries() {
4823        let first_owner = key();
4824        let second_owner = key();
4825        let member = key();
4826        let member_pubkey = keys::public_key_hex(&member);
4827        let mut base = founded("concurrent-retirement-evidence", &first_owner);
4828        base.add_owner_for_test(
4829            &first_owner,
4830            stream(1),
4831            keys::public_key_hex(&second_owner),
4832            "add second Owner".to_string(),
4833        )
4834        .unwrap();
4835        let add_member = base
4836            .signed_set_member_in_stream(
4837                &first_owner,
4838                stream(1),
4839                member_pubkey.clone(),
4840                None,
4841                MemberRole::Member,
4842                "add member".to_string(),
4843            )
4844            .unwrap();
4845        let member_grant = match &add_member.change {
4846            MembershipChange::SetMember { grant_id, .. } => grant_id.clone(),
4847            _ => unreachable!(),
4848        };
4849        base.add_entry(add_member).unwrap();
4850
4851        let first_removal = base
4852            .signed_remove_member_in_stream(
4853                &first_owner,
4854                stream(1),
4855                member_pubkey.clone(),
4856                "first removal".to_string(),
4857            )
4858            .unwrap();
4859        let second_removal = base
4860            .signed_remove_member_in_stream(
4861                &second_owner,
4862                stream(2),
4863                member_pubkey,
4864                "second removal".to_string(),
4865            )
4866            .unwrap();
4867        let expected = GrantRetirements::new(MembershipGrantRetirement::Entry {
4868            authority: first_removal.coord(),
4869            barrier: MergeMembershipGrantRetirementBarrier::NonOwner {
4870                author_streams: StoreGrantStreamBarrier {
4871                    observed_streams: Vec::new(),
4872                },
4873            },
4874        });
4875        let mut expected = expected;
4876        expected.insert(MembershipGrantRetirement::Entry {
4877            authority: second_removal.coord(),
4878            barrier: MergeMembershipGrantRetirementBarrier::NonOwner {
4879                author_streams: StoreGrantStreamBarrier {
4880                    observed_streams: Vec::new(),
4881                },
4882            },
4883        });
4884        let mut entries = base.entries().to_vec();
4885        entries.extend([first_removal, second_removal]);
4886        let chain = MembershipChain::from_entries(entries).unwrap();
4887        let MembershipStatus::Resolved(resolved) = chain.status() else {
4888            panic!("concurrent non-Owner removals must resolve")
4889        };
4890
4891        assert!(matches!(
4892            &resolved.grants[&member_grant],
4893            GrantState::Tombstoned { retirements, .. }
4894                if retirements.as_set() == expected.as_set()
4895        ));
4896    }
4897
4898    fn three_owner_store_cycle() -> (UserKeypair, UserKeypair, UserKeypair, MembershipChain) {
4899        let first = key();
4900        let second = key();
4901        let third = key();
4902        let first_pubkey = keys::public_key_hex(&first);
4903        let second_pubkey = keys::public_key_hex(&second);
4904        let third_pubkey = keys::public_key_hex(&third);
4905        let mut base = founded("three-owner-store", &first);
4906        base.add_owner_for_test(
4907            &first,
4908            stream(1),
4909            second_pubkey.clone(),
4910            "add second Owner".to_string(),
4911        )
4912        .expect("add second Owner");
4913        base.add_owner_for_test(
4914            &first,
4915            stream(1),
4916            third_pubkey,
4917            "add third Owner".to_string(),
4918        )
4919        .expect("add third Owner");
4920        let remove_second = base
4921            .signed_remove_member_in_stream(
4922                &first,
4923                stream(1),
4924                second_pubkey,
4925                "first branch".to_string(),
4926            )
4927            .expect("first branch");
4928        let remove_first = base
4929            .signed_remove_member_in_stream(
4930                &second,
4931                stream(92),
4932                first_pubkey,
4933                "second branch".to_string(),
4934            )
4935            .expect("second branch");
4936        let mut entries = base.entries().to_vec();
4937        entries.extend([remove_second.clone(), remove_first.clone()]);
4938        let heads = vec![
4939            exact_head(
4940                base.entries().first().expect("founder membership entry"),
4941                &first,
4942            ),
4943            exact_head(&remove_second, &first),
4944            exact_head(&remove_first, &second),
4945        ];
4946        let conflict = MembershipChain::from_entries_with_coords_and_heads(
4947            entries
4948                .into_iter()
4949                .map(|entry| (entry.coord(), entry))
4950                .collect(),
4951            heads,
4952        )
4953        .expect("three-Owner Store conflict");
4954        (first, second, third, conflict)
4955    }
4956
4957    #[test]
4958    fn unaffected_store_owner_resolution_retires_its_selected_branch_grant() {
4959        let (_first, _second, third, conflicted) = three_owner_store_cycle();
4960        let third_pubkey = keys::public_key_hex(&third);
4961        let (branch, old_grant) = match conflicted.conflict().expect("conflict") {
4962            MembershipConflict::RevocationCycle {
4963                maximal_valid_branches,
4964                ..
4965            } => {
4966                let branch = maximal_valid_branches
4967                    .iter()
4968                    .find(|branch| {
4969                        branch.active_grants().any(|(_, record)| {
4970                            record.member_pubkey == third_pubkey && record.role.is_owner()
4971                        })
4972                    })
4973                    .expect("unaffected Owner branch");
4974                let old_grant = branch
4975                    .active_grants()
4976                    .find_map(|(grant, record)| {
4977                        (record.member_pubkey == third_pubkey).then_some(grant.clone())
4978                    })
4979                    .expect("unaffected Owner grant");
4980                (branch.heads.clone(), old_grant)
4981            }
4982            _ => panic!("expected revocation conflict"),
4983        };
4984        let store_root_hash = ObjectHash::digest(b"unaffected Store resolver root");
4985        let replacement_membership = membership_anchor("unaffected-store-resolver");
4986        let acceptance = conflict_acceptance(
4987            &conflicted,
4988            store_root_hash,
4989            replacement_membership.clone(),
4990            &third,
4991        );
4992        let resolution = conflicted
4993            .signed_conflict_resolution(
4994                store_root_hash,
4995                MembershipConflictSelection::RevocationBranch { heads: branch },
4996                replacement_membership,
4997                acceptance,
4998                &third,
4999            )
5000            .expect("unaffected Owner resolution");
5001        let resolution = exact_resolution(resolution);
5002        let resolved = conflicted
5003            .resolved_with(store_root_hash, std::slice::from_ref(&resolution))
5004            .expect("unaffected Owner resolution is valid");
5005
5006        assert!(resolution.1.retired_owner_grants.contains(&old_grant));
5007        assert!(resolved.grants[&old_grant].active().is_none());
5008        assert!(resolved
5009            .grants
5010            .get(&resolution.1.replacement_grant)
5011            .and_then(GrantState::active)
5012            .is_some());
5013        assert!(matches!(
5014            &resolved.grants[&old_grant],
5015            GrantState::Tombstoned { retirements, .. }
5016                if retirements.iter().any(|retirement| matches!(
5017                    retirement,
5018                    MembershipGrantRetirement::ConflictResolution { authority, .. }
5019                        if authority == &resolution.0
5020                ))
5021        ));
5022    }
5023
5024    #[test]
5025    fn store_revocation_cycle_over_protocol_bound_is_typed() {
5026        let owners = (0..13).map(|_| key()).collect::<Vec<_>>();
5027        let pubkeys = owners.iter().map(keys::public_key_hex).collect::<Vec<_>>();
5028        let mut base = founded("bounded-store-cycle", &owners[0]);
5029        for pubkey in pubkeys.iter().skip(1) {
5030            base.add_owner_for_test(
5031                &owners[0],
5032                stream(1),
5033                pubkey.clone(),
5034                format!("add {pubkey}"),
5035            )
5036            .expect("add ring Owner");
5037        }
5038        let removals = owners
5039            .iter()
5040            .enumerate()
5041            .map(|(index, owner)| {
5042                base.signed_remove_member_in_stream(
5043                    owner,
5044                    stream(index as u8 + 101),
5045                    pubkeys[(index + 1) % pubkeys.len()].clone(),
5046                    format!("remove ring successor {index}"),
5047                )
5048                .expect("sign ring removal")
5049            })
5050            .collect::<Vec<_>>();
5051        let mut entries = base.entries().to_vec();
5052        entries.extend(removals.iter().cloned());
5053        let heads = removals
5054            .iter()
5055            .zip(&owners)
5056            .map(|(entry, owner)| exact_head(entry, owner))
5057            .collect();
5058
5059        assert!(matches!(
5060            MembershipChain::from_entries_with_coords_and_heads(
5061                entries
5062                    .into_iter()
5063                    .map(|entry| (entry.coord(), entry))
5064                    .collect(),
5065                heads,
5066            ),
5067            Err(MembershipError::RevocationCycleTooWide {
5068                sources: 13,
5069                maximum: 12,
5070            })
5071        ));
5072    }
5073
5074    #[test]
5075    fn timestamp_does_not_change_causal_authorization() {
5076        let owner = key();
5077        let member = key();
5078        let mut chain = founded("store", &owner);
5079        let add = chain
5080            .signed_set_member_in_stream(
5081                &owner,
5082                stream(1),
5083                keys::public_key_hex(&member),
5084                None,
5085                MemberRole::Member,
5086                "9999".to_string(),
5087            )
5088            .unwrap();
5089        chain.add_entry(add).unwrap();
5090        let remove = chain
5091            .signed_remove_member_in_stream(
5092                &owner,
5093                stream(1),
5094                keys::public_key_hex(&member),
5095                "0000".to_string(),
5096            )
5097            .unwrap();
5098        chain.add_entry(remove).unwrap();
5099        assert!(!chain.can_write_now(&keys::public_key_hex(&member)));
5100    }
5101
5102    #[test]
5103    fn signed_candidate_is_validated_before_it_is_returned() {
5104        let owner = key();
5105        let chain = founded("store", &owner);
5106
5107        assert!(matches!(
5108            chain.signed_remove_member_in_stream(
5109                &owner,
5110                stream(1),
5111                keys::public_key_hex(&owner),
5112                "remove last owner".to_string(),
5113            ),
5114            Err(MembershipError::NoActiveOwner)
5115        ));
5116    }
5117
5118    #[test]
5119    fn direct_owner_assignment_is_rejected() {
5120        let founder = key();
5121        let candidate = key();
5122        let chain = founded("owner-promotion-required", &founder);
5123        let candidate_pubkey = keys::public_key_hex(&candidate);
5124
5125        assert!(matches!(
5126            chain.signed_set_member_with_anchor_and_wrapped_key_in_stream(
5127                &founder,
5128                stream(1),
5129                candidate_pubkey.clone(),
5130                None,
5131                MemberRole::Owner,
5132                Some(membership_anchor("direct-owner-assignment")),
5133                test_wrapped_key_ref(
5134                    &keys::public_key_hex(&founder),
5135                    &candidate_pubkey,
5136                    crate::encryption::INITIAL_KEY_GENERATION,
5137                    b"direct Owner assignment",
5138                ),
5139                "direct Owner assignment".to_string(),
5140            ),
5141            Err(MembershipError::OwnerPromotionRequired)
5142        ));
5143    }
5144
5145    #[test]
5146    fn membership_candidates_require_exact_wrapped_key_recipient_coverage() {
5147        let owner = key();
5148        let member = key();
5149        let owner_pubkey = keys::public_key_hex(&owner);
5150        let member_pubkey = keys::public_key_hex(&member);
5151        let mut chain = founded("store", &owner);
5152        let wrong_recipient = test_wrapped_key_ref(
5153            &owner_pubkey,
5154            &owner_pubkey,
5155            crate::encryption::INITIAL_KEY_GENERATION,
5156            b"wrong invitation recipient",
5157        );
5158        assert!(matches!(
5159            chain.signed_set_member_with_anchor_and_wrapped_key_in_stream(
5160                &owner,
5161                stream(1),
5162                member_pubkey.clone(),
5163                None,
5164                MemberRole::Member,
5165                None,
5166                wrong_recipient,
5167                "invalid invitation".to_string(),
5168            ),
5169            Err(MembershipError::InvalidWrappedKeys(_))
5170        ));
5171
5172        let add = chain
5173            .signed_set_member_in_stream(
5174                &owner,
5175                stream(1),
5176                member_pubkey.clone(),
5177                None,
5178                MemberRole::Member,
5179                "add member".to_string(),
5180            )
5181            .unwrap();
5182        chain.add_entry(add).unwrap();
5183        assert!(matches!(
5184            chain.signed_remove_member_with_wrapped_keys_in_stream(
5185                &owner,
5186                stream(1),
5187                member_pubkey,
5188                Vec::new(),
5189                "missing owner wrap".to_string(),
5190            ),
5191            Err(MembershipError::InvalidWrappedKeys(_))
5192        ));
5193    }
5194
5195    #[test]
5196    fn wrapped_key_generations_follow_the_causal_membership_history() {
5197        let owner = key();
5198        let first_member = key();
5199        let second_member = key();
5200        let later_member = key();
5201        let owner_pubkey = keys::public_key_hex(&owner);
5202        let first_pubkey = keys::public_key_hex(&first_member);
5203        let second_pubkey = keys::public_key_hex(&second_member);
5204        let later_pubkey = keys::public_key_hex(&later_member);
5205        let mut chain = founded("wrapped-generation-history", &owner);
5206        for member in [&first_pubkey, &second_pubkey] {
5207            let add = chain
5208                .signed_set_member_in_stream(
5209                    &owner,
5210                    stream(1),
5211                    member.clone(),
5212                    None,
5213                    MemberRole::Member,
5214                    format!("add {member}"),
5215                )
5216                .unwrap();
5217            chain.add_entry(add).unwrap();
5218        }
5219        let mut first_rotation_wraps = vec![
5220            test_wrapped_key_ref(&owner_pubkey, &owner_pubkey, 2, b"first owner rotation"),
5221            test_wrapped_key_ref(&owner_pubkey, &second_pubkey, 2, b"first member rotation"),
5222        ];
5223        first_rotation_wraps.sort();
5224        let first_rotation = chain
5225            .signed_remove_member_with_wrapped_keys_in_stream(
5226                &owner,
5227                stream(1),
5228                first_pubkey,
5229                first_rotation_wraps,
5230                "first rotation".to_string(),
5231            )
5232            .unwrap();
5233        chain.add_entry(first_rotation).unwrap();
5234
5235        assert!(matches!(
5236            chain.signed_set_member_with_anchor_and_wrapped_key_in_stream(
5237                &owner,
5238                stream(1),
5239                later_pubkey.clone(),
5240                None,
5241                MemberRole::Member,
5242                None,
5243                test_wrapped_key_ref(&owner_pubkey, &later_pubkey, 1, b"stale later invitation",),
5244                "stale later invitation".to_string(),
5245            ),
5246            Err(MembershipError::InvalidWrappedKeys(_))
5247        ));
5248        assert!(matches!(
5249            chain.signed_remove_member_with_wrapped_keys_in_stream(
5250                &owner,
5251                stream(1),
5252                second_pubkey,
5253                vec![test_wrapped_key_ref(
5254                    &owner_pubkey,
5255                    &owner_pubkey,
5256                    2,
5257                    b"reused rotation generation",
5258                )],
5259                "reused rotation generation".to_string(),
5260            ),
5261            Err(MembershipError::InvalidWrappedKeys(_))
5262        ));
5263    }
5264
5265    #[test]
5266    fn concurrent_add_and_rotation_has_incomplete_wrapped_key_authority() {
5267        let owner = key();
5268        let removed = key();
5269        let concurrent_member = key();
5270        let owner_pubkey = keys::public_key_hex(&owner);
5271        let removed_pubkey = keys::public_key_hex(&removed);
5272        let concurrent_pubkey = keys::public_key_hex(&concurrent_member);
5273        let mut chain = founded("concurrent-add-rotation", &owner);
5274        let add_removed = chain
5275            .signed_set_member_in_stream(
5276                &owner,
5277                stream(1),
5278                removed_pubkey.clone(),
5279                None,
5280                MemberRole::Member,
5281                "add member that will be removed".to_string(),
5282            )
5283            .unwrap();
5284        chain.add_entry(add_removed).unwrap();
5285
5286        let add_concurrent = chain
5287            .signed_set_member_in_stream(
5288                &owner,
5289                stream(2),
5290                concurrent_pubkey.clone(),
5291                None,
5292                MemberRole::Member,
5293                "concurrent add".to_string(),
5294            )
5295            .unwrap();
5296        let owner_rotation = test_wrapped_key_ref(
5297            &owner_pubkey,
5298            &owner_pubkey,
5299            2,
5300            b"rotation missing concurrent member",
5301        );
5302        let remove = chain
5303            .signed_remove_member_with_wrapped_keys_in_stream(
5304                &owner,
5305                stream(3),
5306                removed_pubkey,
5307                vec![owner_rotation],
5308                "concurrent removal".to_string(),
5309            )
5310            .unwrap();
5311        chain.add_entry(add_concurrent).unwrap();
5312        chain.add_entry(remove).unwrap();
5313
5314        assert!(matches!(
5315            chain.wrapped_key_authority_for(&concurrent_pubkey),
5316            Err(MembershipError::MissingWrappedKeyCoverage { .. })
5317        ));
5318
5319        let replacement_wrap = test_wrapped_key_ref(
5320            &owner_pubkey,
5321            &concurrent_pubkey,
5322            2,
5323            b"post-rotation replacement invitation",
5324        );
5325        let replacement = chain
5326            .signed_set_member_with_anchor_and_wrapped_key_in_stream(
5327                &owner,
5328                stream(4),
5329                concurrent_pubkey.clone(),
5330                None,
5331                MemberRole::Member,
5332                None,
5333                replacement_wrap.clone(),
5334                "replace concurrent invitation after rotation".to_string(),
5335            )
5336            .unwrap();
5337        chain.add_entry(replacement).unwrap();
5338        assert_eq!(
5339            chain.wrapped_key_authority_for(&concurrent_pubkey).unwrap(),
5340            vec![replacement_wrap],
5341        );
5342    }
5343
5344    #[test]
5345    fn concurrent_member_assignments_are_validated_conflict_state() {
5346        let owner = key();
5347        let target = key();
5348        let target_pubkey = keys::public_key_hex(&target);
5349        let mut chain = founded("store", &owner);
5350        let member = chain
5351            .signed_set_member_in_stream(
5352                &owner,
5353                stream(1),
5354                target_pubkey.clone(),
5355                None,
5356                MemberRole::Member,
5357                "initial Member".to_string(),
5358            )
5359            .unwrap();
5360        chain.add_entry(member).unwrap();
5361        let first = chain
5362            .signed_set_member_in_stream(
5363                &owner,
5364                stream(21),
5365                target_pubkey.clone(),
5366                None,
5367                MemberRole::Follower,
5368                "first".to_string(),
5369            )
5370            .unwrap();
5371        let second = chain
5372            .signed_promote_member_in_stream_for_test(
5373                &owner,
5374                stream(22),
5375                target_pubkey.clone(),
5376                "second".to_string(),
5377            )
5378            .unwrap();
5379        let mut entries = chain.entries().to_vec();
5380        entries.extend([first.clone(), second.clone()]);
5381        let heads = entries
5382            .iter()
5383            .filter(|entry| {
5384                !entries.iter().any(|candidate| {
5385                    candidate
5386                        .dependencies
5387                        .iter()
5388                        .any(|dependency| dependency == &entry.coord())
5389                        && candidate.stream_id == entry.stream_id
5390                })
5391            })
5392            .map(|entry| exact_head(entry, &owner))
5393            .collect();
5394
5395        let conflicted = MembershipChain::from_entries_with_coords_and_heads(
5396            entries
5397                .into_iter()
5398                .map(|entry| (entry.coord(), entry))
5399                .collect(),
5400            heads,
5401        )
5402        .expect("well-formed conflict");
5403        let MembershipConflict::ConcurrentMemberAssignments {
5404            member_pubkey,
5405            conflicting_grants,
5406            ..
5407        } = conflicted.conflict().expect("assignment conflict")
5408        else {
5409            panic!("concurrent assignments must produce an assignment conflict")
5410        };
5411        assert_eq!(member_pubkey, &target_pubkey);
5412        assert_eq!(conflicting_grants.len(), 2);
5413
5414        let selected_grant = conflicting_grants
5415            .iter()
5416            .find_map(|(grant, record)| {
5417                (record.role.role() == MemberRole::Follower).then(|| grant.clone())
5418            })
5419            .expect("Follower assignment");
5420        let retired_grant = conflicting_grants
5421            .keys()
5422            .find(|grant| **grant != selected_grant)
5423            .expect("other assignment")
5424            .clone();
5425        let opaque_choice = MembershipConflictChoice::new(
5426            "opaque-choice".to_string(),
5427            Vec::new(),
5428            ObjectHash::digest(b"hidden conflict"),
5429            MembershipConflictSelection::MemberAssignment {
5430                grant: selected_grant.clone(),
5431            },
5432        );
5433        assert_eq!(
5434            format!("{opaque_choice:?}"),
5435            "MembershipConflictChoice { id: \"opaque-choice\", members: [] }",
5436        );
5437        let store_root_hash = ObjectHash::digest(b"assignment-resolution Store root");
5438        let replacement_membership = membership_anchor("assignment-resolution");
5439        let acceptance = conflict_acceptance(
5440            &conflicted,
5441            store_root_hash,
5442            replacement_membership.clone(),
5443            &owner,
5444        );
5445        let resolution_value = conflicted
5446            .signed_conflict_resolution(
5447                store_root_hash,
5448                MembershipConflictSelection::MemberAssignment {
5449                    grant: selected_grant.clone(),
5450                },
5451                replacement_membership,
5452                acceptance,
5453                &owner,
5454            )
5455            .expect("Owner selects an assignment");
5456        let mut incomplete_resolution = resolution_value.clone();
5457        incomplete_resolution
5458            .retirement_barriers
5459            .remove(&retired_grant);
5460        incomplete_resolution.signature =
5461            keys::sign_hex(&owner, &incomplete_resolution.canonical_bytes()).1;
5462        assert!(!incomplete_resolution.verify_against(
5463            store_root_hash,
5464            conflicted.conflict().expect("assignment conflict"),
5465        ));
5466        let resolution = exact_resolution(resolution_value);
5467        let resolved_once = conflicted
5468            .resolved_with(store_root_hash, std::slice::from_ref(&resolution))
5469            .expect("assignment resolution applies");
5470        let resolved_retry = conflicted
5471            .resolved_with(store_root_hash, &[resolution.clone(), resolution.clone()])
5472            .expect("exact assignment resolution retry is idempotent");
5473
5474        assert_eq!(resolved_once, resolved_retry);
5475        assert_eq!(
5476            resolved_once
5477                .grants
5478                .get(&selected_grant)
5479                .and_then(GrantState::active)
5480                .map(|record| record.role.role()),
5481            Some(MemberRole::Follower),
5482        );
5483        assert!(matches!(
5484            resolved_once.grants.get(&retired_grant),
5485            Some(GrantState::Tombstoned { .. })
5486        ));
5487        assert!(resolution
5488            .1
5489            .retired_owner_grants
5490            .iter()
5491            .all(|grant| resolved_once
5492                .grants
5493                .get(grant)
5494                .and_then(GrantState::active)
5495                .is_none()));
5496        assert!(resolved_once
5497            .grants
5498            .get(&resolution.1.replacement_grant)
5499            .and_then(GrantState::active)
5500            .is_some());
5501    }
5502
5503    #[test]
5504    fn assignment_resolvers_keep_only_a_choice_they_all_selected() {
5505        let first_owner = key();
5506        let second_owner = key();
5507        let target = key();
5508        let first_owner_pubkey = keys::public_key_hex(&first_owner);
5509        let second_owner_pubkey = keys::public_key_hex(&second_owner);
5510        let target_pubkey = keys::public_key_hex(&target);
5511        let mut base = founded("assignment-consensus", &first_owner);
5512        base.add_owner_for_test(
5513            &first_owner,
5514            stream(1),
5515            second_owner_pubkey.clone(),
5516            "add second Owner".to_string(),
5517        )
5518        .unwrap();
5519        let initial = base
5520            .signed_set_member_in_stream(
5521                &first_owner,
5522                stream(1),
5523                target_pubkey.clone(),
5524                None,
5525                MemberRole::Member,
5526                "initial target assignment".to_string(),
5527            )
5528            .unwrap();
5529        base.add_entry(initial).unwrap();
5530        let follower_assignment = base
5531            .signed_set_member_in_stream(
5532                &first_owner,
5533                stream(21),
5534                target_pubkey.clone(),
5535                None,
5536                MemberRole::Follower,
5537                "Follower assignment".to_string(),
5538            )
5539            .unwrap();
5540        let member_assignment = base
5541            .signed_set_member_in_stream(
5542                &second_owner,
5543                stream(22),
5544                target_pubkey.clone(),
5545                None,
5546                MemberRole::Member,
5547                "Member assignment".to_string(),
5548            )
5549            .unwrap();
5550        let mut entries = base.entries().to_vec();
5551        entries.extend([follower_assignment, member_assignment]);
5552        let heads = entries
5553            .iter()
5554            .filter(|entry| {
5555                !entries.iter().any(|candidate| {
5556                    candidate
5557                        .dependencies
5558                        .iter()
5559                        .any(|dependency| dependency == &entry.coord())
5560                        && candidate.stream_id == entry.stream_id
5561                })
5562            })
5563            .map(|entry| {
5564                let signer = if entry.author_pubkey == first_owner_pubkey {
5565                    &first_owner
5566                } else {
5567                    assert_eq!(entry.author_pubkey, second_owner_pubkey);
5568                    &second_owner
5569                };
5570                exact_head(entry, signer)
5571            })
5572            .collect();
5573        let conflicted = MembershipChain::from_entries_with_coords_and_heads(
5574            entries
5575                .into_iter()
5576                .map(|entry| (entry.coord(), entry))
5577                .collect(),
5578            heads,
5579        )
5580        .expect("well-formed assignment conflict");
5581        let MembershipConflict::ConcurrentMemberAssignments {
5582            conflicting_grants, ..
5583        } = conflicted.conflict().expect("assignment conflict")
5584        else {
5585            panic!("concurrent assignments must produce an assignment conflict")
5586        };
5587        let follower_grant = conflicting_grants
5588            .iter()
5589            .find_map(|(grant, record)| {
5590                (record.role.role() == MemberRole::Follower).then(|| grant.clone())
5591            })
5592            .expect("Follower assignment");
5593        let member_grant = conflicting_grants
5594            .iter()
5595            .find_map(|(grant, record)| {
5596                (record.role.role() == MemberRole::Member).then(|| grant.clone())
5597            })
5598            .expect("Member assignment");
5599        let store_root_hash = ObjectHash::digest(b"assignment consensus Store root");
5600
5601        let first_membership = membership_anchor("first-assignment-resolution");
5602        let first_acceptance = conflict_acceptance(
5603            &conflicted,
5604            store_root_hash,
5605            first_membership.clone(),
5606            &first_owner,
5607        );
5608        let first_resolution = exact_resolution(
5609            conflicted
5610                .signed_conflict_resolution(
5611                    store_root_hash,
5612                    MembershipConflictSelection::MemberAssignment {
5613                        grant: follower_grant.clone(),
5614                    },
5615                    first_membership,
5616                    first_acceptance,
5617                    &first_owner,
5618                )
5619                .expect("first Owner selects the Follower assignment"),
5620        );
5621        let second_membership = membership_anchor("second-assignment-resolution");
5622        let second_acceptance = conflict_acceptance(
5623            &conflicted,
5624            store_root_hash,
5625            second_membership.clone(),
5626            &second_owner,
5627        );
5628        let second_resolution = exact_resolution(
5629            conflicted
5630                .signed_conflict_resolution(
5631                    store_root_hash,
5632                    MembershipConflictSelection::MemberAssignment {
5633                        grant: member_grant.clone(),
5634                    },
5635                    second_membership,
5636                    second_acceptance,
5637                    &second_owner,
5638                )
5639                .expect("second Owner selects the Member assignment"),
5640        );
5641
5642        let resolved = conflicted
5643            .resolved_with(
5644                store_root_hash,
5645                &[first_resolution.clone(), second_resolution.clone()],
5646            )
5647            .expect("disagreeing assignment resolutions converge");
5648
5649        assert!(matches!(
5650            resolved.grants.get(&follower_grant),
5651            Some(GrantState::Tombstoned { .. })
5652        ));
5653        assert!(matches!(
5654            resolved.grants.get(&member_grant),
5655            Some(GrantState::Tombstoned { .. })
5656        ));
5657        assert!(!resolved
5658            .grants
5659            .values()
5660            .filter_map(GrantState::active)
5661            .any(|record| record.member_pubkey == target_pubkey));
5662        for resolution in [&first_resolution, &second_resolution] {
5663            assert!(resolved
5664                .grants
5665                .get(&resolution.1.replacement_grant)
5666                .and_then(GrantState::active)
5667                .is_some());
5668            assert!(resolution
5669                .1
5670                .retired_owner_grants
5671                .iter()
5672                .all(|grant| resolved
5673                    .grants
5674                    .get(grant)
5675                    .and_then(GrantState::active)
5676                    .is_none()));
5677        }
5678    }
5679
5680    #[test]
5681    fn concurrent_cross_revocation_is_a_validated_cycle_conflict() {
5682        let first_owner = key();
5683        let second_owner = key();
5684        let first_pubkey = keys::public_key_hex(&first_owner);
5685        let second_pubkey = keys::public_key_hex(&second_owner);
5686        let mut base = founded("store", &first_owner);
5687        base.add_owner_for_test(
5688            &first_owner,
5689            stream(1),
5690            second_pubkey.clone(),
5691            "add second".to_string(),
5692        )
5693        .unwrap();
5694        let remove_second = base
5695            .signed_remove_member_in_stream(
5696                &first_owner,
5697                stream(1),
5698                second_pubkey.clone(),
5699                "remove second".to_string(),
5700            )
5701            .unwrap();
5702        let remove_first = base
5703            .signed_remove_member_in_stream(
5704                &second_owner,
5705                stream(23),
5706                first_pubkey.clone(),
5707                "remove first".to_string(),
5708            )
5709            .unwrap();
5710        let mut entries = base.entries().to_vec();
5711        entries.extend([remove_second.clone(), remove_first.clone()]);
5712        let heads = vec![
5713            exact_head(
5714                base.entries().first().expect("founder membership entry"),
5715                &first_owner,
5716            ),
5717            exact_head(&remove_second, &first_owner),
5718            exact_head(&remove_first, &second_owner),
5719        ];
5720
5721        let conflicted = MembershipChain::from_entries_with_coords_and_heads(
5722            entries
5723                .into_iter()
5724                .map(|entry| (entry.coord(), entry))
5725                .collect(),
5726            heads,
5727        )
5728        .expect("well-formed conflict");
5729        assert!(matches!(
5730            conflicted.status(),
5731            MembershipStatus::Conflict(MembershipConflict::RevocationCycle {
5732                cyclic_sources,
5733                involved_owner_grants,
5734                maximal_valid_branches,
5735                ..
5736
5737            }) if cyclic_sources.len() == 2
5738                && involved_owner_grants.len() == 2
5739                && maximal_valid_branches.len() == 2
5740        ));
5741
5742        let MembershipConflict::RevocationCycle {
5743            maximal_valid_branches,
5744            ..
5745        } = conflicted.conflict().expect("cycle conflict")
5746        else {
5747            unreachable!();
5748        };
5749        let resolver_branch_state = maximal_valid_branches
5750            .iter()
5751            .find(|branch| {
5752                branch.active_grants().any(|(_, record)| {
5753                    record.member_pubkey == first_pubkey && record.role.is_owner()
5754                })
5755            })
5756            .expect("first Owner branch")
5757            .clone();
5758        let resolver_branch = resolver_branch_state.heads.clone();
5759        let second_resolver_branch = maximal_valid_branches
5760            .iter()
5761            .find(|branch| {
5762                branch.active_grants().any(|(_, record)| {
5763                    record.member_pubkey == second_pubkey && record.role.is_owner()
5764                })
5765            })
5766            .expect("second Owner branch")
5767            .heads
5768            .clone();
5769        let store_root_hash = ObjectHash::digest(b"resolution Store root");
5770        let first_membership = membership_anchor("first-cycle-resolution");
5771        let first_acceptance = conflict_acceptance(
5772            &conflicted,
5773            store_root_hash,
5774            first_membership.clone(),
5775            &first_owner,
5776        );
5777        let resolution_value = conflicted
5778            .signed_conflict_resolution(
5779                store_root_hash,
5780                MembershipConflictSelection::RevocationBranch {
5781                    heads: resolver_branch.clone(),
5782                },
5783                first_membership.clone(),
5784                first_acceptance.clone(),
5785                &first_owner,
5786            )
5787            .expect("branch Owner resolves the conflict");
5788        let mut forged_resolution = resolution_value.clone();
5789        forged_resolution.signature =
5790            keys::sign_hex(&second_owner, &forged_resolution.canonical_bytes()).1;
5791        assert!(!forged_resolution.verify_signature());
5792        assert!(!forged_resolution.verify_against(
5793            store_root_hash,
5794            conflicted.conflict().expect("cycle conflict"),
5795        ));
5796        let second_membership = membership_anchor("second-cycle-resolution");
5797        let second_acceptance = conflict_acceptance(
5798            &conflicted,
5799            store_root_hash,
5800            second_membership.clone(),
5801            &second_owner,
5802        );
5803        let second_resolution_value = conflicted
5804            .signed_conflict_resolution(
5805                store_root_hash,
5806                MembershipConflictSelection::RevocationBranch {
5807                    heads: second_resolver_branch,
5808                },
5809                second_membership,
5810                second_acceptance,
5811                &second_owner,
5812            )
5813            .expect("other branch Owner resolves the conflict");
5814        let retried = conflicted
5815            .signed_conflict_resolution(
5816                store_root_hash,
5817                MembershipConflictSelection::RevocationBranch {
5818                    heads: resolver_branch,
5819                },
5820                first_membership,
5821                first_acceptance,
5822                &first_owner,
5823            )
5824            .expect("same resolver retry");
5825        assert_eq!(resolution_value, retried);
5826        assert!(resolution_value.verify_against(
5827            store_root_hash,
5828            conflicted.conflict().expect("cycle conflict"),
5829        ));
5830        let resolution = exact_resolution(resolution_value);
5831        let second_resolution = exact_resolution(second_resolution_value);
5832        let resolved_once = conflicted
5833            .resolved_with(store_root_hash, std::slice::from_ref(&resolution))
5834            .expect("one resolution applies");
5835        let resolved_duplicate = conflicted
5836            .resolved_with(store_root_hash, &[resolution.clone(), resolution.clone()])
5837            .expect("an exact retry is idempotent");
5838        assert_eq!(resolved_once, resolved_duplicate);
5839        assert!(resolved_once
5840            .grants
5841            .get(&resolution.1.replacement_grant)
5842            .and_then(GrantState::active)
5843            .is_some());
5844        assert!(resolution
5845            .1
5846            .retired_owner_grants
5847            .iter()
5848            .all(|grant| resolved_once
5849                .grants
5850                .get(grant)
5851                .and_then(GrantState::active)
5852                .is_none()));
5853
5854        let resolved_union = conflicted
5855            .resolved_with(
5856                store_root_hash,
5857                &[resolution.clone(), second_resolution.clone()],
5858            )
5859            .expect("distinct resolvers are unioned");
5860        assert!(resolved_union
5861            .grants
5862            .get(&resolution.1.replacement_grant)
5863            .and_then(GrantState::active)
5864            .is_some());
5865        assert!(resolved_union
5866            .grants
5867            .get(&second_resolution.1.replacement_grant)
5868            .and_then(GrantState::active)
5869            .is_some());
5870
5871        let mut branch_specific = conflicted.conflict().expect("cycle conflict").clone();
5872        let MembershipConflict::RevocationCycle {
5873            maximal_valid_branches,
5874            ..
5875        } = &mut branch_specific
5876        else {
5877            unreachable!()
5878        };
5879        let branch_only_grant = MembershipGrantId(ObjectHash::digest(b"branch-only grant"));
5880        let branch_only_creation = maximal_valid_branches[0].effective_frontier[0].clone();
5881        maximal_valid_branches[0].grants.insert(
5882            branch_only_grant.clone(),
5883            GrantState::Active {
5884                record: MembershipGrantRecord {
5885                    member_pubkey: keys::public_key_hex(&key()),
5886                    role: StoreMembershipRoleGrant::Member,
5887                    provider_account_email: None,
5888                    creation_authority: MembershipGrantCreationAuthority::Entry(
5889                        branch_only_creation,
5890                    ),
5891                },
5892            },
5893        );
5894        let branch_barrier = MergeMembershipGrantRetirementBarrier::NonOwner {
5895            author_streams: StoreGrantStreamBarrier {
5896                observed_streams: Vec::new(),
5897            },
5898        };
5899        let mut branch_resolution_value = resolution.1.clone();
5900        branch_resolution_value
5901            .retirement_barriers
5902            .insert(branch_only_grant.clone(), branch_barrier.clone());
5903        branch_resolution_value.signature =
5904            keys::sign_hex(&first_owner, &branch_resolution_value.canonical_bytes()).1;
5905        let branch_resolution = exact_resolution(branch_resolution_value);
5906        let mut branch_second_resolution_value = second_resolution.1.clone();
5907        branch_second_resolution_value
5908            .retirement_barriers
5909            .insert(branch_only_grant.clone(), branch_barrier);
5910        branch_second_resolution_value.signature = keys::sign_hex(
5911            &second_owner,
5912            &branch_second_resolution_value.canonical_bytes(),
5913        )
5914        .1;
5915        let branch_second_resolution = exact_resolution(branch_second_resolution_value);
5916        let composed = resolve_store_membership_conflict(
5917            store_root_hash,
5918            &branch_specific,
5919            &[branch_resolution.clone(), branch_second_resolution.clone()],
5920        )
5921        .expect("retire grants not agreed by every valid branch");
5922        let branch_only_retirements = composed
5923            .grants
5924            .get(&branch_only_grant)
5925            .and_then(GrantState::retirements)
5926            .expect("branch-only grant is retained as retired");
5927        assert!(branch_only_retirements.iter().any(|retirement| matches!(
5928            retirement,
5929            MembershipGrantRetirement::ConflictResolution { authority, .. }
5930                if authority == &branch_resolution.0
5931        )));
5932        assert!(branch_only_retirements.iter().any(|retirement| matches!(
5933            retirement,
5934            MembershipGrantRetirement::ConflictResolution { authority, .. }
5935                if authority == &branch_second_resolution.0
5936        )));
5937
5938        let mut duplicate_member = branch_specific;
5939        let MembershipConflict::RevocationCycle {
5940            maximal_valid_branches,
5941            ..
5942        } = &mut duplicate_member
5943        else {
5944            unreachable!()
5945        };
5946        let duplicate_pubkey = keys::public_key_hex(&key());
5947        let duplicate_creation = resolution.1.conflicting_heads[0].coord.clone();
5948        for branch in maximal_valid_branches {
5949            for suffix in [b'a', b'b'] {
5950                branch.grants.insert(
5951                    MembershipGrantId(ObjectHash::digest(&[suffix])),
5952                    GrantState::Active {
5953                        record: MembershipGrantRecord {
5954                            member_pubkey: duplicate_pubkey.clone(),
5955                            role: StoreMembershipRoleGrant::Member,
5956                            provider_account_email: None,
5957                            creation_authority: MembershipGrantCreationAuthority::Entry(
5958                                duplicate_creation.clone(),
5959                            ),
5960                        },
5961                    },
5962                );
5963            }
5964        }
5965        assert!(matches!(
5966            resolve_store_membership_conflict(
5967                store_root_hash,
5968                &duplicate_member,
5969                &[resolution.clone(), second_resolution.clone()],
5970            ),
5971            Err(MembershipError::InvalidConflictResolution)
5972        ));
5973
5974        let mut resumed = conflicted.clone();
5975        let raw_heads = resumed.author_heads();
5976        resumed
5977            .apply_resolutions(store_root_hash, std::slice::from_ref(&resolution))
5978            .expect("resolution activates replacement Owner grant");
5979        assert_eq!(resumed.author_heads(), raw_heads);
5980        let accepted_controls = [remove_second.coord(), remove_first.coord()];
5981        assert!(accepted_controls
5982            .iter()
5983            .all(|coord| resumed.contains_coord(coord)));
5984        assert!(accepted_controls
5985            .iter()
5986            .any(|coord| !resumed.included.contains(coord)));
5987        let raw_losing_control = accepted_controls
5988            .iter()
5989            .find(|coord| !resumed.included.contains(*coord))
5990            .expect("resolved history retains one raw losing control")
5991            .clone();
5992        let checkpoint_floor = crate::sync::store_commit::MembershipCausalFloor {
5993            effective_coordinates: vec![raw_losing_control],
5994            resolutions: resumed.resolution_refs().to_vec(),
5995        };
5996        assert!(
5997            !crate::sync::store::retained_membership_floor_is_included(
5998                &checkpoint_floor,
5999                &resumed,
6000            ),
6001            "a coordinate present only in the raw losing branch cannot satisfy a retained effective checkpoint floor",
6002        );
6003        assert_eq!(
6004            resumed.effective_frontier(),
6005            resolver_branch_state.effective_frontier
6006        );
6007        assert_eq!(
6008            resumed.resolution_refs(),
6009            std::slice::from_ref(&resolution.0)
6010        );
6011        let after_resolution = resumed
6012            .signed_set_member_in_stream(
6013                &first_owner,
6014                stream(37),
6015                keys::public_key_hex(&key()),
6016                None,
6017                MemberRole::Member,
6018                "write after resolution".to_string(),
6019            )
6020            .expect("replacement Owner can author from a fresh stream");
6021        assert_eq!(
6022            after_resolution.author_owner_grant,
6023            resolution.1.replacement_grant
6024        );
6025        let activated_head = exact_head(&after_resolution, &first_owner).1;
6026        resumed
6027            .add_entry(after_resolution)
6028            .expect("future authoring validates from the resolved checkpoint");
6029        assert_eq!(activated_head.body.resolutions, vec![resolution.0.clone()]);
6030        let authority = MembershipGrantCreationAuthority::ConflictResolution(resolution.0.clone());
6031        assert!(resumed.authorizes_write_authority(&authority, &first_pubkey));
6032        let outsider = key();
6033        let outsider_membership = membership_anchor("non-owner-cycle-resolution");
6034        let outsider_acceptance = conflict_acceptance(
6035            &conflicted,
6036            store_root_hash,
6037            outsider_membership.clone(),
6038            &outsider,
6039        );
6040        assert!(matches!(
6041            conflicted.signed_conflict_resolution(
6042                store_root_hash,
6043                resolution.1.selection.clone(),
6044                outsider_membership,
6045                outsider_acceptance,
6046                &outsider,
6047            ),
6048            Err(MembershipError::SignerIsNotOwner(_))
6049        ));
6050    }
6051
6052    #[test]
6053    fn dependency_frontier_must_be_strictly_ordered_by_author_stream() {
6054        let founder = key();
6055        let second_owner = key();
6056        let mut chain = founded("store", &founder);
6057        chain
6058            .add_owner_for_test(
6059                &founder,
6060                stream(1),
6061                keys::public_key_hex(&second_owner),
6062                "add owner".to_string(),
6063            )
6064            .unwrap();
6065        let second_stream = chain
6066            .signed_set_member_in_stream(
6067                &second_owner,
6068                stream(31),
6069                keys::public_key_hex(&key()),
6070                None,
6071                MemberRole::Member,
6072                "second stream".to_string(),
6073            )
6074            .unwrap();
6075        chain.add_entry(second_stream).unwrap();
6076        let mut unsorted = chain
6077            .signed_set_member_in_stream(
6078                &founder,
6079                stream(1),
6080                keys::public_key_hex(&key()),
6081                None,
6082                MemberRole::Member,
6083                "candidate".to_string(),
6084            )
6085            .unwrap();
6086        assert!(unsorted.dependencies.len() > 1);
6087        unsorted.dependencies.reverse();
6088        sign_membership_entry(&mut unsorted, &founder);
6089
6090        assert!(matches!(
6091            chain.add_entry(unsorted),
6092            Err(MembershipError::NonCanonicalDependencyFrontier { .. })
6093        ));
6094    }
6095
6096    #[test]
6097    fn owner_barrier_must_be_strictly_ordered_by_author_stream() {
6098        let founder = key();
6099        let second_owner = key();
6100        let second_owner_pubkey = keys::public_key_hex(&second_owner);
6101        let mut chain = founded("store", &founder);
6102        chain
6103            .add_owner_for_test(
6104                &founder,
6105                stream(1),
6106                second_owner_pubkey.clone(),
6107                "add owner".to_string(),
6108            )
6109            .unwrap();
6110        for (stream_id, timestamp) in [(stream(41), "first stream"), (stream(42), "second stream")]
6111        {
6112            let authored = chain
6113                .signed_set_member_in_stream(
6114                    &second_owner,
6115                    stream_id,
6116                    keys::public_key_hex(&key()),
6117                    None,
6118                    MemberRole::Member,
6119                    timestamp.to_string(),
6120                )
6121                .unwrap();
6122            chain.add_entry(authored).unwrap();
6123        }
6124        let mut removal = chain
6125            .signed_remove_member_in_stream(
6126                &founder,
6127                stream(1),
6128                second_owner_pubkey,
6129                "remove owner".to_string(),
6130            )
6131            .unwrap();
6132        let MembershipChange::RemoveMember {
6133            retirement_barriers,
6134            ..
6135        } = &mut removal.change
6136        else {
6137            unreachable!();
6138        };
6139        let observed = &mut retirement_barriers
6140            .values_mut()
6141            .next()
6142            .expect("owner removal barrier")
6143            .author_streams()
6144            .observed_streams
6145            .clone();
6146        assert!(observed.len() > 1);
6147        let barrier = retirement_barriers
6148            .values_mut()
6149            .next()
6150            .expect("owner removal barrier");
6151        match barrier {
6152            MergeMembershipGrantRetirementBarrier::Owner { barrier } => {
6153                barrier.author_streams.observed_streams.reverse();
6154            }
6155            MergeMembershipGrantRetirementBarrier::NonOwner { .. } => {
6156                panic!("Owner removal carries non-Owner barrier")
6157            }
6158        }
6159        sign_membership_entry(&mut removal, &founder);
6160
6161        assert!(matches!(
6162            chain.add_entry(removal),
6163            Err(MembershipError::InvalidOwnerRevocationBarrier { .. })
6164        ));
6165    }
6166
6167    #[test]
6168    fn owner_readd_uses_a_new_sequence_one_stream() {
6169        let owner = key();
6170        let second = key();
6171        let mut chain = founded("store", &owner);
6172        chain
6173            .add_owner_for_test(
6174                &owner,
6175                stream(1),
6176                keys::public_key_hex(&second),
6177                "add".to_string(),
6178            )
6179            .unwrap();
6180        let old_grant = chain
6181            .active_owner_grant(&keys::public_key_hex(&second))
6182            .unwrap();
6183        let remove = chain
6184            .signed_remove_member_in_stream(
6185                &owner,
6186                stream(1),
6187                keys::public_key_hex(&second),
6188                "remove".to_string(),
6189            )
6190            .unwrap();
6191        chain.add_entry(remove).unwrap();
6192        chain
6193            .add_owner_for_test(
6194                &owner,
6195                stream(1),
6196                keys::public_key_hex(&second),
6197                "readd".to_string(),
6198            )
6199            .unwrap();
6200        let new_grant = chain
6201            .active_owner_grant(&keys::public_key_hex(&second))
6202            .unwrap();
6203        assert_ne!(old_grant, new_grant);
6204        let authored = chain
6205            .signed_set_member_in_stream(
6206                &second,
6207                stream(32),
6208                keys::public_key_hex(&key()),
6209                None,
6210                MemberRole::Member,
6211                "authored".to_string(),
6212            )
6213            .unwrap();
6214        assert_eq!(authored.seq, 1);
6215        assert_eq!(authored.author_owner_grant, new_grant);
6216    }
6217
6218    #[test]
6219    fn owner_self_removal_remains_effective_when_its_grant_is_capped_before_first() {
6220        let founder = key();
6221        let departing_owner = key();
6222        let departing_pubkey = keys::public_key_hex(&departing_owner);
6223        let mut chain = founded("store", &founder);
6224        chain
6225            .add_owner_for_test(
6226                &founder,
6227                stream(1),
6228                departing_pubkey.clone(),
6229                "add owner".to_string(),
6230            )
6231            .unwrap();
6232
6233        let self_removal = chain
6234            .signed_remove_member_in_stream(
6235                &departing_owner,
6236                stream(33),
6237                departing_pubkey.clone(),
6238                "self removal".to_string(),
6239            )
6240            .unwrap();
6241        assert!(matches!(
6242            &self_removal.change,
6243            MembershipChange::RemoveMember { retirement_barriers, .. }
6244                if retirement_barriers.values().all(|barrier| barrier.author_streams().observed_streams.is_empty())
6245        ));
6246        chain.add_entry(self_removal).unwrap();
6247
6248        assert!(!chain.is_owner_now(&departing_pubkey));
6249    }
6250
6251    #[test]
6252    fn before_first_barrier_excludes_every_entry_from_the_revoked_owner_stream() {
6253        let founder = key();
6254        let second_owner = key();
6255        let target = key();
6256        let mut observed = founded("store", &founder);
6257        observed
6258            .add_owner_for_test(
6259                &founder,
6260                stream(1),
6261                keys::public_key_hex(&second_owner),
6262                "add owner".to_string(),
6263            )
6264            .unwrap();
6265
6266        let stale_entry = observed
6267            .signed_set_member_in_stream(
6268                &second_owner,
6269                stream(34),
6270                keys::public_key_hex(&target),
6271                None,
6272                MemberRole::Member,
6273                "stale entry".to_string(),
6274            )
6275            .unwrap();
6276        let removal = observed
6277            .signed_remove_member_in_stream(
6278                &founder,
6279                stream(1),
6280                keys::public_key_hex(&second_owner),
6281                "remove owner".to_string(),
6282            )
6283            .unwrap();
6284        assert!(matches!(
6285            &removal.change,
6286            MembershipChange::RemoveMember { retirement_barriers, .. }
6287                if retirement_barriers.values().all(|barrier| barrier.author_streams().observed_streams.is_empty())
6288        ));
6289
6290        let mut entries = observed.entries().to_vec();
6291        entries.extend([removal, stale_entry]);
6292        let chain = MembershipChain::from_entries(entries).unwrap();
6293        assert!(!chain.can_write_now(&keys::public_key_hex(&target)));
6294        assert!(chain
6295            .author_heads()
6296            .iter()
6297            .any(|coord| coord.author_pubkey == keys::public_key_hex(&second_owner)));
6298        assert!(chain
6299            .effective_frontier()
6300            .iter()
6301            .all(|coord| coord.author_pubkey != keys::public_key_hex(&second_owner)));
6302    }
6303
6304    #[test]
6305    fn through_barrier_keeps_its_exact_prefix_and_prunes_the_stale_suffix() {
6306        let founder = key();
6307        let second_owner = key();
6308        let first_target = key();
6309        let second_target = key();
6310        let third_target = key();
6311        let mut observed = founded("store", &founder);
6312        observed
6313            .add_owner_for_test(
6314                &founder,
6315                stream(1),
6316                keys::public_key_hex(&second_owner),
6317                "add owner".to_string(),
6318            )
6319            .unwrap();
6320        let first = observed
6321            .signed_set_member_in_stream(
6322                &second_owner,
6323                stream(35),
6324                keys::public_key_hex(&first_target),
6325                None,
6326                MemberRole::Member,
6327                "first".to_string(),
6328            )
6329            .unwrap();
6330        observed.add_entry(first.clone()).unwrap();
6331
6332        let removal = observed
6333            .signed_remove_member_in_stream(
6334                &founder,
6335                stream(1),
6336                keys::public_key_hex(&second_owner),
6337                "remove owner".to_string(),
6338            )
6339            .unwrap();
6340        assert!(matches!(
6341            &removal.change,
6342            MembershipChange::RemoveMember { retirement_barriers, .. }
6343                if retirement_barriers.values().any(|barrier| barrier.author_streams().observed_streams == vec![first.coord()])
6344        ));
6345
6346        let second = observed
6347            .signed_set_member_in_stream(
6348                &second_owner,
6349                stream(35),
6350                keys::public_key_hex(&second_target),
6351                None,
6352                MemberRole::Member,
6353                "second".to_string(),
6354            )
6355            .unwrap();
6356        let mut exact_entries = observed.entries().to_vec();
6357        exact_entries.extend([removal.clone(), second.clone()]);
6358        let exact = MembershipChain::from_entries(exact_entries).unwrap();
6359        assert!(exact.can_write_now(&keys::public_key_hex(&first_target)));
6360        assert!(!exact.can_write_now(&keys::public_key_hex(&second_target)));
6361
6362        let mut stale = observed;
6363        stale.add_entry(second).unwrap();
6364        let third = stale
6365            .signed_set_member_in_stream(
6366                &second_owner,
6367                stream(35),
6368                keys::public_key_hex(&third_target),
6369                None,
6370                MemberRole::Member,
6371                "third".to_string(),
6372            )
6373            .unwrap();
6374        stale.add_entry(third.clone()).unwrap();
6375        let mut beyond_entries = stale.entries().to_vec();
6376        beyond_entries.push(removal);
6377        let pruned = MembershipChain::from_entries(beyond_entries).unwrap();
6378        assert!(pruned.can_write_now(&keys::public_key_hex(&first_target)));
6379        assert!(!pruned.can_write_now(&keys::public_key_hex(&second_target)));
6380        assert!(!pruned.can_write_now(&keys::public_key_hex(&third_target)));
6381    }
6382
6383    #[test]
6384    fn through_barrier_rejects_a_coordinate_hash_that_is_not_its_dependency() {
6385        let founder = key();
6386        let second_owner = key();
6387        let mut chain = founded("store", &founder);
6388        chain
6389            .add_owner_for_test(
6390                &founder,
6391                stream(1),
6392                keys::public_key_hex(&second_owner),
6393                "add owner".to_string(),
6394            )
6395            .unwrap();
6396        let authored = chain
6397            .signed_set_member_in_stream(
6398                &second_owner,
6399                stream(36),
6400                keys::public_key_hex(&key()),
6401                None,
6402                MemberRole::Member,
6403                "authored".to_string(),
6404            )
6405            .unwrap();
6406        chain.add_entry(authored).unwrap();
6407        let mut removal = chain
6408            .signed_remove_member_in_stream(
6409                &founder,
6410                stream(1),
6411                keys::public_key_hex(&second_owner),
6412                "remove owner".to_string(),
6413            )
6414            .unwrap();
6415        let MembershipChange::RemoveMember {
6416            retirement_barriers,
6417            ..
6418        } = &mut removal.change
6419        else {
6420            unreachable!();
6421        };
6422        let barrier = retirement_barriers
6423            .values_mut()
6424            .next()
6425            .expect("owner removal barrier");
6426        let MergeMembershipGrantRetirementBarrier::Owner { barrier } = barrier else {
6427            panic!("Owner removal carries non-Owner barrier")
6428        };
6429        let barrier = barrier
6430            .author_streams
6431            .observed_streams
6432            .first_mut()
6433            .expect("observed owner stream");
6434        barrier.entry_hash = ObjectHash::digest(b"wrong barrier hash");
6435        sign_membership_entry(&mut removal, &founder);
6436        assert!(matches!(
6437            chain.add_entry(removal),
6438            Err(MembershipError::InvalidOwnerRevocationBarrier { .. })
6439        ));
6440    }
6441
6442    #[test]
6443    fn cross_store_replay_fails_even_with_the_same_founder_key() {
6444        let owner = key();
6445        let from_a = test_founder_entry("store-a", &owner, "founder", membership_anchor("store-a"));
6446        let mut replayed = from_a.clone();
6447        replayed.store_id = "store-b".to_string();
6448        assert!(!verify_membership_entry(&replayed));
6449        assert!(MembershipChain::from_entries(vec![from_a])
6450            .unwrap()
6451            .is_founded_by(&keys::public_key_hex(&owner)));
6452    }
6453
6454    #[test]
6455    fn created_at_is_signed_but_never_orders_entries() {
6456        let owner = key();
6457        let entry = test_founder_entry("store", &owner, "display-time", membership_anchor("store"));
6458        let mut tampered = entry.clone();
6459        tampered.created_at = "other".to_string();
6460        assert!(!verify_membership_entry(&tampered));
6461    }
6462
6463    #[test]
6464    fn membership_head_resolution_cut_must_equal_its_tip_entry_cut() {
6465        let owner = UserKeypair::generate();
6466        let entry = test_founder_entry(
6467            "head-tip-resolution-cut",
6468            &owner,
6469            "founder",
6470            membership_anchor("head-tip-resolution-cut"),
6471        );
6472        let fake = StoreMembershipConflictResolutionRef {
6473            conflict_hash: ObjectHash::digest(b"head-tip conflict"),
6474            resolver_pubkey: keys::public_key_hex(&owner),
6475            resolution_hash: ObjectHash::digest(b"head-tip resolution"),
6476            object: exact(
6477                "test/head-tip-resolution-cut/resolution.json",
6478                b"head-tip resolution",
6479            ),
6480        };
6481        let head = exact_head_with_resolutions(&entry, &owner, vec![fake]);
6482
6483        assert!(matches!(
6484            MembershipChain::from_entries_with_coords_and_heads(
6485                vec![(entry.coord(), entry)],
6486                vec![head],
6487            ),
6488            Err(MembershipError::MissingConflictHeads)
6489        ));
6490    }
6491
6492    #[test]
6493    fn membership_entry_rejects_unsorted_or_duplicate_resolution_dependencies() {
6494        let owner = UserKeypair::generate();
6495        let founder = test_founder_entry(
6496            "entry-resolution-cut",
6497            &owner,
6498            "founder",
6499            membership_anchor("entry-resolution-cut"),
6500        );
6501        let chain = MembershipChain::from_entries(vec![founder]).unwrap();
6502        let entry = chain
6503            .signed_set_member_in_stream(
6504                &owner,
6505                stream(1),
6506                keys::public_key_hex(&UserKeypair::generate()),
6507                None,
6508                MemberRole::Member,
6509                "member".to_string(),
6510            )
6511            .unwrap();
6512        let mut refs = [b"first".as_slice(), b"second".as_slice()]
6513            .into_iter()
6514            .map(|label| StoreMembershipConflictResolutionRef {
6515                conflict_hash: ObjectHash::digest(label),
6516                resolver_pubkey: keys::public_key_hex(&owner),
6517                resolution_hash: ObjectHash::digest(&[label, b" resolution"].concat()),
6518                object: exact(
6519                    format!(
6520                        "test/entry-resolution-cut/{}.json",
6521                        String::from_utf8_lossy(label)
6522                    ),
6523                    label,
6524                ),
6525            })
6526            .collect::<Vec<_>>();
6527        refs.sort();
6528
6529        let mut unsorted = entry.clone();
6530        unsorted.resolution_dependencies = refs.iter().rev().cloned().collect();
6531        sign_membership_entry(&mut unsorted, &owner);
6532        assert!(!verify_membership_entry(&unsorted));
6533
6534        let mut duplicate = entry;
6535        duplicate.resolution_dependencies = vec![refs[0].clone(), refs[0].clone()];
6536        sign_membership_entry(&mut duplicate, &owner);
6537        assert!(!verify_membership_entry(&duplicate));
6538    }
6539}