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, OwnerGrantBarrier,
15};
16pub use super::causal_grants::{AuthorStreamId, MembershipGrantId};
17use super::storage::ExactObjectRef;
18use super::store_commit::{
19    GrantStreamAnchor, ObjectHash, StoreBatchCommit, StoreControl, StoreDeviceRegistration,
20    StoreDeviceRegistrationOrigin, StoreDeviceRegistrationRef, StoreProtocolRoot, StoreRootRef,
21    SuccessorLink, STORE_PROTOCOL_VERSION,
22};
23use crate::keys::{self, UserKeypair};
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26pub enum MemberRole {
27    Owner,
28    Member,
29    Follower,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
33#[serde(deny_unknown_fields)]
34pub struct SerialMember {
35    pub member_pubkey: String,
36    pub role: MemberRole,
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub provider_account_email: Option<String>,
39    pub created_at_generation: u64,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
43#[serde(deny_unknown_fields)]
44pub struct SerialMembershipState {
45    store_root_hash: ObjectHash,
46    active_grants: BTreeMap<MembershipGrantId, SerialMember>,
47    current_generation: u64,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
51#[serde(deny_unknown_fields)]
52pub struct SerialAuthorizationState {
53    pub membership: SerialMembershipState,
54    pub provider_admin: super::provider::ProviderAdminState,
55    pub key_generation: u64,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
59#[serde(rename_all = "snake_case", deny_unknown_fields)]
60pub enum SerialMembershipChange {
61    SetMember {
62        user_pubkey: String,
63        #[serde(default, skip_serializing_if = "Option::is_none")]
64        provider_account_email: Option<String>,
65        role: MemberRole,
66        grant_id: MembershipGrantId,
67        replaces: BTreeSet<MembershipGrantId>,
68    },
69    RemoveMember {
70        user_pubkey: String,
71        removes: BTreeSet<MembershipGrantId>,
72    },
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
76#[serde(deny_unknown_fields)]
77pub struct SerialMembershipEntry {
78    pub version: u32,
79    pub store_root_hash: ObjectHash,
80    pub previous_state_hash: ObjectHash,
81    pub created_at_generation: u64,
82    pub author_pubkey: String,
83    pub created_at: String,
84    pub change: SerialMembershipChange,
85    pub signature: String,
86}
87
88#[derive(Debug, thiserror::Error, PartialEq, Eq)]
89pub enum SerialMembershipError {
90    #[error("Serial membership founder does not match the Store protocol root founder")]
91    InvalidFounder,
92    #[error("Serial membership entry has unsupported version {0}")]
93    UnsupportedVersion(u32),
94    #[error("Serial membership entry belongs to root {actual}, expected {expected}")]
95    StoreRootMismatch {
96        expected: ObjectHash,
97        actual: ObjectHash,
98    },
99    #[error("Serial membership entry has an invalid signature")]
100    InvalidSignature,
101    #[error("Serial membership entry names state {actual}, expected {expected}")]
102    StaleState {
103        expected: ObjectHash,
104        actual: ObjectHash,
105    },
106    #[error("Serial membership author {0} is not a current Owner")]
107    AuthorIsNotOwner(String),
108    #[error("Serial membership member {0} is absent")]
109    NotAMember(String),
110    #[error("Serial membership removal would leave no Owner")]
111    LastOwner,
112    #[error("Serial membership generation is {actual}, expected {expected}")]
113    MembershipGeneration { expected: u64, actual: u64 },
114    #[error("Serial commit carries a causal membership grant")]
115    CausalGrant,
116    #[error("Serial commit author {0} is not a current writer")]
117    AuthorIsNotWriter(String),
118    #[error("Serial device lifecycle commit also carries a Store or Circle package")]
119    LifecycleWithPackage,
120    #[error("Serial key rotation is not paired with a membership removal")]
121    RotationWithoutRemoval,
122    #[error("Serial key rotation names generation {actual}, expected {expected}")]
123    KeyGeneration { expected: u64, actual: u64 },
124    #[error("Serial commit reference does not authenticate the accepted commit")]
125    InvalidCommitRef,
126    #[error("Serial provider administrator history is invalid: {0}")]
127    ProviderAdmin(#[from] super::provider::ProviderAdminReducerError),
128}
129
130impl SerialAuthorizationState {
131    #[cfg(test)]
132    pub(crate) fn from_test_membership(
133        founder: &MembershipEntry,
134        membership: SerialMembershipState,
135    ) -> Result<Self, MembershipError> {
136        Ok(Self {
137            membership,
138            provider_admin: test_provider_admin_genesis(std::slice::from_ref(founder))?,
139            key_generation: 0,
140        })
141    }
142
143    pub fn membership_state_hash(&self) -> ObjectHash {
144        ObjectHash::digest(
145            &serde_json::to_vec(&(
146                "coven.serial-authorization-membership-state.v1",
147                self.membership.state_hash(),
148                self.provider_admin.state_hash(),
149            ))
150            .expect("Serial authorization membership state serialization cannot fail"),
151        )
152    }
153
154    pub fn from_founder(
155        root: &StoreRootRef,
156        root_value: &StoreProtocolRoot,
157        founder_ref: &StoreDeviceRegistrationRef,
158        founder: &StoreDeviceRegistration,
159    ) -> Result<Self, SerialMembershipError> {
160        if root_value.descriptor.write_policy != crate::WritePolicy::Serial
161            || root_value.descriptor.store_root_id() != root.store_root_id
162            || root_value.object_hash() != root.store_root_hash
163            || founder_ref.object.slot() != &root_value.descriptor.founder_registration
164            || founder_ref.verify_registration(founder).is_err()
165            || &founder.store_root != root
166            || founder.author_pubkey != root_value.descriptor.founder_pubkey
167            || founder.provider != root_value.descriptor.founder_provider_admin.provider
168            || !matches!(
169                founder.origin,
170                StoreDeviceRegistrationOrigin::Founder { creation_id }
171                    if creation_id == root_value.descriptor.creation_id
172            )
173        {
174            return Err(SerialMembershipError::InvalidFounder);
175        }
176        Ok(Self {
177            membership: SerialMembershipState::from_genesis(
178                root.store_root_hash,
179                root_value.descriptor.founder_pubkey.clone(),
180                root_value.descriptor.founder_grant.clone(),
181            ),
182            provider_admin: super::provider::ProviderAdminState::founder_from_root(
183                root.clone(),
184                founder_ref.clone(),
185                &root_value.descriptor.founder_provider_admin,
186            ),
187            key_generation: crate::encryption::INITIAL_KEY_GENERATION,
188        })
189    }
190
191    pub fn authorize_and_apply(
192        &self,
193        commit_ref: &super::store_commit::StoreBatchCommitRef,
194        commit: &StoreBatchCommit,
195        author: &StoreDeviceRegistration,
196    ) -> Result<Self, SerialMembershipError> {
197        commit_ref
198            .verify_commit(commit)
199            .map_err(|_| SerialMembershipError::InvalidCommitRef)?;
200        if commit.membership_authority.is_some() {
201            return Err(SerialMembershipError::CausalGrant);
202        }
203        if commit
204            .author_registration
205            .verify_registration(author)
206            .is_err()
207        {
208            return Err(SerialMembershipError::AuthorIsNotWriter(
209                author.author_pubkey.clone(),
210            ));
211        }
212        let carries_lifecycle =
213            !commit.device_registrations().is_empty() || !commit.device_retirements().is_empty();
214        let carries_package =
215            commit.store_package().is_some() || !commit.circle_packages().is_empty();
216        if carries_lifecycle && carries_package {
217            return Err(SerialMembershipError::LifecycleWithPackage);
218        }
219        let authorized = self.membership.can_write(&author.author_pubkey)
220            || (self.membership.contains(&author.author_pubkey)
221                && is_exact_self_retirement_only(commit));
222        if !authorized {
223            return Err(SerialMembershipError::AuthorIsNotWriter(
224                author.author_pubkey.clone(),
225            ));
226        }
227        let membership = match commit.control() {
228            Some(control) => match control.serial_membership_entry() {
229                Some(entry) => self.membership.apply_at(entry, commit.seq())?,
230                None => self.membership.advance_to(commit.seq())?,
231            },
232            None => self.membership.advance_to(commit.seq())?,
233        };
234        let Some(control) = commit.control() else {
235            return Ok(Self {
236                membership,
237                provider_admin: self.provider_admin.clone(),
238                key_generation: self.key_generation,
239            });
240        };
241        let key_generation = match control {
242            StoreControl::SerialMembership { .. } => self.key_generation,
243            StoreControl::SerialMembershipAndKeyRotation { entry, generation } => {
244                if !entry.change.is_removal() {
245                    return Err(SerialMembershipError::RotationWithoutRemoval);
246                }
247                let expected = self.key_generation.checked_add(1).ok_or(
248                    SerialMembershipError::KeyGeneration {
249                        expected: self.key_generation,
250                        actual: *generation,
251                    },
252                )?;
253                if *generation != expected {
254                    return Err(SerialMembershipError::KeyGeneration {
255                        expected,
256                        actual: *generation,
257                    });
258                }
259                *generation
260            }
261            StoreControl::ProviderAdmin { .. } => self.key_generation,
262        };
263        let mut provider_admin = self.provider_admin.clone();
264        if let StoreControl::ProviderAdmin { change } = control {
265            provider_admin.apply_membership_change(
266                super::provider::ProviderAdminMembershipChange::Serial {
267                    change: change.clone(),
268                },
269                super::provider::ProviderAdminGrantOrigin::SerialCommit {
270                    commit: commit_ref.clone(),
271                },
272            )?;
273        }
274        Ok(Self {
275            membership,
276            provider_admin,
277            key_generation,
278        })
279    }
280}
281
282fn is_exact_self_retirement_only(commit: &StoreBatchCommit) -> bool {
283    let [retirement] = commit.device_retirements() else {
284        return false;
285    };
286    retirement.target == commit.author_registration
287        && commit.control().is_none()
288        && commit.device_registrations().is_empty()
289        && commit.circle_controls().is_empty()
290        && commit.store_package().is_none()
291        && commit.circle_packages().is_empty()
292}
293
294impl SerialMembershipState {
295    fn from_genesis(
296        store_root_hash: ObjectHash,
297        founder_pubkey: String,
298        founder_grant: MembershipGrantId,
299    ) -> Self {
300        Self {
301            store_root_hash,
302            active_grants: BTreeMap::from([(
303                founder_grant,
304                SerialMember {
305                    member_pubkey: founder_pubkey,
306                    role: MemberRole::Owner,
307                    provider_account_email: None,
308                    created_at_generation: 0,
309                },
310            )]),
311            current_generation: 0,
312        }
313    }
314
315    pub fn from_founder(
316        store_root_hash: ObjectHash,
317        founder: &MembershipEntry,
318    ) -> Result<Self, SerialMembershipError> {
319        let MembershipChange::Founder {
320            owner_pubkey,
321            owner_grant_id,
322            ..
323        } = &founder.change
324        else {
325            return Err(SerialMembershipError::InvalidFounder);
326        };
327        if founder.author_pubkey != *owner_pubkey
328            || founder.author_owner_grant != *owner_grant_id
329            || founder.stream_id != derive_founder_stream_id(&founder.store_id, owner_pubkey)
330            || founder.seq != 1
331            || founder.previous_hash.is_some()
332            || !founder.dependencies.is_empty()
333            || !verify_membership_entry(founder)
334        {
335            return Err(SerialMembershipError::InvalidFounder);
336        }
337        Ok(Self {
338            store_root_hash,
339            active_grants: BTreeMap::from([(
340                owner_grant_id.clone(),
341                SerialMember {
342                    member_pubkey: owner_pubkey.clone(),
343                    role: MemberRole::Owner,
344                    provider_account_email: None,
345                    created_at_generation: 0,
346                },
347            )]),
348            current_generation: 0,
349        })
350    }
351
352    pub fn state_hash(&self) -> ObjectHash {
353        #[derive(Serialize)]
354        struct StateFields<'a> {
355            domain: &'static str,
356            store_root_hash: ObjectHash,
357            active_grants: &'a BTreeMap<MembershipGrantId, SerialMember>,
358        }
359        ObjectHash::digest(
360            &serde_json::to_vec(&StateFields {
361                domain: "coven.serial-membership-state.v1",
362                store_root_hash: self.store_root_hash,
363                active_grants: &self.active_grants,
364            })
365            .expect("Serial membership state serialization cannot fail"),
366        )
367    }
368
369    pub fn store_root_hash(&self) -> ObjectHash {
370        self.store_root_hash
371    }
372
373    pub fn current_members(&self) -> Vec<(String, MemberRole)> {
374        self.active_grants
375            .values()
376            .map(|member| (member.member_pubkey.clone(), member.role.clone()))
377            .collect::<BTreeMap<_, _>>()
378            .into_iter()
379            .collect()
380    }
381
382    pub fn current_member_provider_email(&self, pubkey: &str) -> Option<&str> {
383        self.active_grants
384            .values()
385            .find(|member| member.member_pubkey == pubkey)
386            .and_then(|member| member.provider_account_email.as_deref())
387    }
388
389    pub fn can_write(&self, pubkey: &str) -> bool {
390        self.active_grants
391            .values()
392            .any(|member| member.member_pubkey == pubkey && member.role.can_write())
393    }
394
395    fn contains(&self, pubkey: &str) -> bool {
396        self.active_grants
397            .values()
398            .any(|member| member.member_pubkey == pubkey)
399    }
400
401    pub fn is_owner(&self, pubkey: &str) -> bool {
402        self.active_grants
403            .values()
404            .any(|member| member.member_pubkey == pubkey && member.role == MemberRole::Owner)
405    }
406
407    pub fn active_owner_grant(&self, pubkey: &str) -> Option<MembershipGrantId> {
408        self.active_grants.iter().find_map(|(grant_id, member)| {
409            (member.member_pubkey == pubkey && member.role == MemberRole::Owner)
410                .then(|| grant_id.clone())
411        })
412    }
413
414    pub(crate) fn authorizes_owner_grant_id(
415        &self,
416        pubkey: &str,
417        grant_id: &MembershipGrantId,
418    ) -> bool {
419        self.active_grants.get(grant_id).is_some_and(|member| {
420            member.member_pubkey == pubkey && member.role == MemberRole::Owner
421        })
422    }
423
424    pub fn signed_set_member(
425        &self,
426        signer: &UserKeypair,
427        user_pubkey: String,
428        provider_account_email: Option<String>,
429        role: MemberRole,
430        created_at: String,
431    ) -> Result<SerialMembershipEntry, SerialMembershipError> {
432        let created_at_generation = self.next_generation()?;
433        let grant_id =
434            serial_membership_grant_id(self.store_root_hash, created_at_generation, &user_pubkey);
435        let replaces = self.active_grants_for(&user_pubkey);
436        self.signed_change(
437            signer,
438            SerialMembershipChange::SetMember {
439                user_pubkey,
440                provider_account_email,
441                role,
442                grant_id,
443                replaces,
444            },
445            created_at_generation,
446            created_at,
447        )
448    }
449
450    pub fn signed_remove_member(
451        &self,
452        signer: &UserKeypair,
453        user_pubkey: String,
454        created_at: String,
455    ) -> Result<SerialMembershipEntry, SerialMembershipError> {
456        let removes = self.active_grants_for(&user_pubkey);
457        if removes.is_empty() {
458            return Err(SerialMembershipError::NotAMember(user_pubkey));
459        }
460        let created_at_generation = self.next_generation()?;
461        self.signed_change(
462            signer,
463            SerialMembershipChange::RemoveMember {
464                user_pubkey,
465                removes,
466            },
467            created_at_generation,
468            created_at,
469        )
470    }
471
472    fn signed_change(
473        &self,
474        signer: &UserKeypair,
475        change: SerialMembershipChange,
476        created_at_generation: u64,
477        created_at: String,
478    ) -> Result<SerialMembershipEntry, SerialMembershipError> {
479        let author_pubkey = keys::public_key_hex(signer);
480        if !self.is_owner(&author_pubkey) {
481            return Err(SerialMembershipError::AuthorIsNotOwner(author_pubkey));
482        }
483        let mut entry = SerialMembershipEntry {
484            version: STORE_PROTOCOL_VERSION,
485            store_root_hash: self.store_root_hash,
486            previous_state_hash: self.state_hash(),
487            created_at_generation,
488            author_pubkey,
489            created_at,
490            change,
491            signature: String::new(),
492        };
493        let (_, signature) = keys::sign_hex(signer, &entry.canonical_bytes());
494        entry.signature = signature;
495        Ok(entry)
496    }
497
498    pub fn apply(&self, entry: &SerialMembershipEntry) -> Result<Self, SerialMembershipError> {
499        self.apply_at(entry, entry.created_at_generation)
500    }
501
502    fn apply_at(
503        &self,
504        entry: &SerialMembershipEntry,
505        generation: u64,
506    ) -> Result<Self, SerialMembershipError> {
507        if entry.version != STORE_PROTOCOL_VERSION {
508            return Err(SerialMembershipError::UnsupportedVersion(entry.version));
509        }
510        if entry.store_root_hash != self.store_root_hash {
511            return Err(SerialMembershipError::StoreRootMismatch {
512                expected: self.store_root_hash,
513                actual: entry.store_root_hash,
514            });
515        }
516        if !entry.verify() {
517            return Err(SerialMembershipError::InvalidSignature);
518        }
519        let expected = self.state_hash();
520        if entry.previous_state_hash != expected {
521            return Err(SerialMembershipError::StaleState {
522                expected,
523                actual: entry.previous_state_hash,
524            });
525        }
526        if !self.is_owner(&entry.author_pubkey) {
527            return Err(SerialMembershipError::AuthorIsNotOwner(
528                entry.author_pubkey.clone(),
529            ));
530        }
531        let expected_generation = self.next_generation()?;
532        if entry.created_at_generation != generation || generation != expected_generation {
533            return Err(SerialMembershipError::MembershipGeneration {
534                expected: expected_generation,
535                actual: entry.created_at_generation,
536            });
537        }
538        let mut next = self.clone();
539        match &entry.change {
540            SerialMembershipChange::SetMember {
541                user_pubkey,
542                provider_account_email,
543                role,
544                grant_id,
545                replaces,
546            } => {
547                if *replaces != self.active_grants_for(user_pubkey)
548                    || next.active_grants.contains_key(grant_id)
549                {
550                    return Err(SerialMembershipError::StaleState {
551                        expected,
552                        actual: entry.previous_state_hash,
553                    });
554                }
555                for replaced in replaces {
556                    next.active_grants.remove(replaced);
557                }
558                next.active_grants.insert(
559                    grant_id.clone(),
560                    SerialMember {
561                        member_pubkey: user_pubkey.clone(),
562                        role: role.clone(),
563                        provider_account_email: provider_account_email.clone(),
564                        created_at_generation: generation,
565                    },
566                );
567            }
568            SerialMembershipChange::RemoveMember {
569                user_pubkey,
570                removes,
571            } => {
572                if *removes != self.active_grants_for(user_pubkey) {
573                    return Err(SerialMembershipError::NotAMember(user_pubkey.clone()));
574                }
575                for removed in removes {
576                    next.active_grants.remove(removed);
577                }
578                if !next
579                    .active_grants
580                    .values()
581                    .any(|member| member.role == MemberRole::Owner)
582                {
583                    return Err(SerialMembershipError::LastOwner);
584                }
585            }
586        }
587        next.current_generation = generation;
588        Ok(next)
589    }
590
591    fn active_grants_for(&self, pubkey: &str) -> BTreeSet<MembershipGrantId> {
592        self.active_grants
593            .iter()
594            .filter_map(|(grant, member)| (member.member_pubkey == pubkey).then_some(grant.clone()))
595            .collect()
596    }
597
598    fn next_generation(&self) -> Result<u64, SerialMembershipError> {
599        self.current_generation
600            .checked_add(1)
601            .ok_or(SerialMembershipError::MembershipGeneration {
602                expected: self.current_generation,
603                actual: self.current_generation,
604            })
605    }
606
607    fn advance_to(&self, generation: u64) -> Result<Self, SerialMembershipError> {
608        let expected = self.next_generation()?;
609        if generation != expected {
610            return Err(SerialMembershipError::MembershipGeneration {
611                expected,
612                actual: generation,
613            });
614        }
615        let mut next = self.clone();
616        next.current_generation = generation;
617        Ok(next)
618    }
619}
620
621fn serial_membership_grant_id(
622    store_root_hash: ObjectHash,
623    created_at_generation: u64,
624    member_pubkey: &str,
625) -> MembershipGrantId {
626    MembershipGrantId(ObjectHash::digest(
627        format!(
628            "coven.serial-membership-grant.v1\0{store_root_hash}\0{created_at_generation}\0{member_pubkey}"
629        )
630        .as_bytes(),
631    ))
632}
633
634impl SerialMembershipChange {
635    pub fn user_pubkey(&self) -> &str {
636        match self {
637            Self::SetMember { user_pubkey, .. } | Self::RemoveMember { user_pubkey, .. } => {
638                user_pubkey
639            }
640        }
641    }
642
643    pub fn is_removal(&self) -> bool {
644        matches!(self, Self::RemoveMember { .. })
645    }
646}
647
648impl SerialMembershipEntry {
649    fn canonical_bytes(&self) -> Vec<u8> {
650        #[derive(Serialize)]
651        struct Signed<'a> {
652            domain: &'static str,
653            version: u32,
654            store_root_hash: ObjectHash,
655            previous_state_hash: ObjectHash,
656            created_at_generation: u64,
657            author_pubkey: &'a str,
658            created_at: &'a str,
659            change: &'a SerialMembershipChange,
660        }
661        serde_json::to_vec(&Signed {
662            domain: "coven.serial-membership-entry.v1",
663            version: self.version,
664            store_root_hash: self.store_root_hash,
665            previous_state_hash: self.previous_state_hash,
666            created_at_generation: self.created_at_generation,
667            author_pubkey: &self.author_pubkey,
668            created_at: &self.created_at,
669            change: &self.change,
670        })
671        .expect("Serial membership entry serialization cannot fail")
672    }
673
674    pub fn verify(&self) -> bool {
675        keys::verify_signature_hex(
676            &self.author_pubkey,
677            &self.signature,
678            &self.canonical_bytes(),
679        )
680    }
681}
682
683impl MemberRole {
684    pub fn can_write(&self) -> bool {
685        matches!(self, Self::Owner | Self::Member)
686    }
687}
688
689#[derive(Debug, Clone)]
690pub struct MemberInfo {
691    pub pubkey: String,
692    pub role: MemberRole,
693    pub is_self: bool,
694}
695
696#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
697#[serde(deny_unknown_fields)]
698pub enum MembershipChange {
699    Founder {
700        owner_pubkey: String,
701        owner_grant_id: MembershipGrantId,
702        membership: GrantStreamAnchor,
703        provider_admin: super::provider::FounderProviderAdminGrant,
704    },
705    SetMember {
706        user_pubkey: String,
707        #[serde(default, skip_serializing_if = "Option::is_none")]
708        provider_account_email: Option<String>,
709        role: MemberRole,
710        grant_id: MembershipGrantId,
711        membership: Option<GrantStreamAnchor>,
712        replaces: BTreeSet<MembershipGrantId>,
713        owner_barriers: BTreeMap<MembershipGrantId, OwnerStreamBarrier>,
714    },
715    RemoveMember {
716        user_pubkey: String,
717        removes: BTreeSet<MembershipGrantId>,
718        owner_barriers: BTreeMap<MembershipGrantId, OwnerStreamBarrier>,
719    },
720    ProviderAdmin,
721    ResolutionActivation {
722        resolution: StoreMembershipConflictResolutionRef,
723    },
724}
725
726impl MembershipChange {
727    pub(crate) fn membership_anchor(&self) -> Option<GrantStreamAnchor> {
728        match self {
729            Self::Founder { membership, .. } => Some(membership.clone()),
730            Self::SetMember { membership, .. } => membership.clone(),
731            Self::RemoveMember { .. } | Self::ProviderAdmin | Self::ResolutionActivation { .. } => {
732                None
733            }
734        }
735    }
736}
737
738#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
739#[serde(deny_unknown_fields)]
740pub struct MembershipCoord {
741    pub author_pubkey: String,
742    pub author_owner_grant: MembershipGrantId,
743    pub stream_id: AuthorStreamId,
744    pub seq: u64,
745    pub entry_hash: ObjectHash,
746}
747
748impl MembershipCoord {
749    pub(crate) fn stream_key(&self) -> MembershipStreamKey {
750        MembershipStreamKey {
751            author_pubkey: self.author_pubkey.clone(),
752            author_owner_grant: self.author_owner_grant.clone(),
753            stream_id: self.stream_id,
754        }
755    }
756}
757
758#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
759pub(crate) struct MembershipStreamKey {
760    pub(crate) author_pubkey: String,
761    pub(crate) author_owner_grant: MembershipGrantId,
762    pub(crate) stream_id: AuthorStreamId,
763}
764
765impl CausalCoordinate for MembershipCoord {
766    type StreamKey = MembershipStreamKey;
767
768    fn stream_key(&self) -> Self::StreamKey {
769        MembershipCoord::stream_key(self)
770    }
771
772    fn author_pubkey(&self) -> &str {
773        &self.author_pubkey
774    }
775
776    fn author_owner_grant(&self) -> &MembershipGrantId {
777        &self.author_owner_grant
778    }
779
780    fn seq(&self) -> u64 {
781        self.seq
782    }
783
784    fn entry_hash(&self) -> ObjectHash {
785        self.entry_hash
786    }
787}
788
789#[derive(Debug, Clone, PartialEq, Eq)]
790struct StoreAssignment {
791    role: MemberRole,
792    provider_account_email: Option<String>,
793}
794
795impl CausalAssignment for StoreAssignment {
796    fn is_owner(&self) -> bool {
797        self.role == MemberRole::Owner
798    }
799}
800
801#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
802#[serde(deny_unknown_fields)]
803pub struct OwnerStreamBarrier {
804    pub observed_streams: Vec<MembershipCoord>,
805}
806
807#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
808#[serde(deny_unknown_fields)]
809pub struct MembershipEntry {
810    pub version: u32,
811    pub store_id: String,
812    pub author_pubkey: String,
813    pub author_owner_grant: MembershipGrantId,
814    pub stream_id: AuthorStreamId,
815    pub seq: u64,
816    pub previous_hash: Option<ObjectHash>,
817    pub dependencies: Vec<MembershipCoord>,
818    pub resolution_dependencies: Vec<StoreMembershipConflictResolutionRef>,
819    pub created_at: String,
820    pub change: MembershipChange,
821    #[serde(skip_serializing_if = "Option::is_none")]
822    pub provider_admin: Option<super::provider::ProviderAdminMembershipChange>,
823    pub signature: String,
824}
825
826#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
827#[serde(deny_unknown_fields)]
828pub struct MembershipEntryRef {
829    pub coord: MembershipCoord,
830    pub object: ExactObjectRef,
831}
832
833impl MembershipEntry {
834    pub fn coord(&self) -> MembershipCoord {
835        MembershipCoord {
836            author_pubkey: self.author_pubkey.clone(),
837            author_owner_grant: self.author_owner_grant.clone(),
838            stream_id: self.stream_id,
839            seq: self.seq,
840            entry_hash: entry_hash(self),
841        }
842    }
843
844    pub fn provider_account_email(&self) -> Option<&str> {
845        match &self.change {
846            MembershipChange::SetMember {
847                provider_account_email,
848                ..
849            } => provider_account_email.as_deref(),
850            MembershipChange::Founder { .. }
851            | MembershipChange::RemoveMember { .. }
852            | MembershipChange::ProviderAdmin
853            | MembershipChange::ResolutionActivation { .. } => None,
854        }
855    }
856}
857
858#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
859#[serde(deny_unknown_fields)]
860pub struct AuthorHead {
861    pub version: u32,
862    pub store_id: String,
863    pub author_registration: StoreDeviceRegistrationRef,
864    pub entry: MembershipEntryRef,
865    pub predecessor: Option<MembershipHeadRef>,
866    pub resolutions: Vec<StoreMembershipConflictResolutionRef>,
867    pub successor: SuccessorLink,
868    pub signature: String,
869}
870
871#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
872#[serde(deny_unknown_fields)]
873pub struct MembershipHeadRef {
874    pub coord: MembershipCoord,
875    pub head_hash: ObjectHash,
876    pub object: ExactObjectRef,
877}
878
879#[derive(Debug, thiserror::Error, PartialEq, Eq)]
880pub enum MembershipError {
881    #[error("membership chain is empty")]
882    EmptyChain,
883    #[error("membership entry {0} has unsupported version")]
884    UnsupportedVersion(usize),
885    #[error("membership entry {index} belongs to store {actual:?}, expected {expected:?}")]
886    StoreMismatch {
887        index: usize,
888        expected: String,
889        actual: String,
890    },
891    #[error("membership entry {0} has an invalid signature")]
892    InvalidSignature(usize),
893    #[error("membership entry {index} is in coordinate {actual:?}, expected {expected:?}")]
894    CoordinateMismatch {
895        index: usize,
896        expected: Box<MembershipCoord>,
897        actual: Box<MembershipCoord>,
898    },
899    #[error("membership stream {author}/{grant} is missing sequence {seq}")]
900    MissingSequence {
901        author: String,
902        grant: MembershipGrantId,
903        seq: u64,
904    },
905    #[error("membership stream {author}/{grant} has conflicting entries at sequence {seq}")]
906    ConflictingSequence {
907        author: String,
908        grant: MembershipGrantId,
909        seq: u64,
910    },
911    #[error("membership entry {index} has predecessor {actual:?}, expected {expected:?}")]
912    BrokenStreamLink {
913        index: usize,
914        expected: Option<ObjectHash>,
915        actual: Option<ObjectHash>,
916    },
917    #[error("membership entry {index} does not carry its complete own-stream dependency")]
918    MissingOwnDependency { index: usize },
919    #[error("membership entry {index} depends on missing coordinate {dependency:?}")]
920    MissingDependency {
921        index: usize,
922        dependency: Box<MembershipCoord>,
923    },
924    #[error(
925        "membership entry {index} dependency frontier is not strictly ordered by author stream"
926    )]
927    NonCanonicalDependencyFrontier { index: usize },
928    #[error("membership dependency graph contains a cycle")]
929    DependencyCycle,
930    #[error("membership founder entry is invalid")]
931    InvalidFounder,
932    #[error("membership entry {index} author is not active under Owner grant {grant}")]
933    AuthorGrantInactive {
934        index: usize,
935        grant: MembershipGrantId,
936    },
937    #[error("membership entry {index} creates an already-defined grant {grant}")]
938    DuplicateGrant {
939        index: usize,
940        grant: MembershipGrantId,
941    },
942    #[error("membership entry {index} replaces or removes grant {grant} owned by another member")]
943    GrantOwnerMismatch {
944        index: usize,
945        grant: MembershipGrantId,
946    },
947    #[error("membership entry {index} does not name the exact active grants for member {pubkey}")]
948    GrantSetMismatch { index: usize, pubkey: String },
949    #[error("membership entry {index} removes no exact grants")]
950    EmptyRemoval { index: usize },
951    #[error("membership entry {index} removes Owner grant {grant} without its exact observed-through coordinate")]
952    MissingOwnerRevocationBarrier {
953        index: usize,
954        grant: MembershipGrantId,
955    },
956    #[error(
957        "membership entry {index} carries an invalid revocation barrier for Owner grant {grant}"
958    )]
959    InvalidOwnerRevocationBarrier {
960        index: usize,
961        grant: MembershipGrantId,
962    },
963    #[error("membership entry {0} carries an invalid Owner membership stream anchor")]
964    InvalidOwnerMembershipAnchor(usize),
965    #[error("membership history leaves no active Owner")]
966    NoActiveOwner,
967    #[error(
968        "membership revocation cycle has {sources} sources, exceeding the protocol limit of {maximum}"
969    )]
970    RevocationCycleTooWide { sources: usize, maximum: usize },
971    #[error("signer {0} has no active Owner grant")]
972    SignerIsNotOwner(String),
973    #[error("member {0} has no active grants")]
974    NotAMember(String),
975    #[error("membership author stream contains a pruned suffix and cannot be extended")]
976    PrunedAuthorStream,
977    #[error("membership author has no reusable stream; a fresh persisted stream is required")]
978    MissingAuthorStream,
979    #[error("membership resolution activation entry {0} is invalid")]
980    InvalidResolutionActivation(usize),
981    #[error("membership resolution activation requires a fresh persisted author stream")]
982    ResolutionActivationRequiresFreshStream,
983    #[error("provider administrator control entry {0} is invalid")]
984    InvalidProviderAdminChange(usize),
985    #[error("membership has an unresolved semantic conflict")]
986    Conflict,
987    #[error("membership conflict is missing its exact signed raw heads")]
988    MissingConflictHeads,
989    #[error("membership conflict resolution does not name exact validated conflict evidence")]
990    InvalidConflictResolution,
991    #[error("provider administrator history is invalid: {0}")]
992    ProviderAdmin(#[from] super::provider::ProviderAdminReducerError),
993}
994
995#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
996#[serde(deny_unknown_fields)]
997pub struct MembershipGrantRecord {
998    pub member_pubkey: String,
999    pub role: MemberRole,
1000    #[serde(default, skip_serializing_if = "Option::is_none")]
1001    pub provider_account_email: Option<String>,
1002    pub creation_authority: MembershipGrantCreationAuthority,
1003}
1004
1005#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1006#[serde(rename_all = "snake_case", deny_unknown_fields)]
1007pub enum MembershipGrantCreationAuthority {
1008    Entry(MembershipCoord),
1009    ConflictResolution(StoreMembershipConflictResolutionRef),
1010}
1011
1012#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1013#[serde(deny_unknown_fields)]
1014pub struct ResolvedStoreMembership {
1015    pub active_grants: BTreeMap<MembershipGrantId, MembershipGrantRecord>,
1016    pub provider_admin: super::provider::ProviderAdminResolution,
1017    pub state_hash: ObjectHash,
1018}
1019
1020#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1021#[serde(deny_unknown_fields)]
1022pub struct StoreMembershipBranch {
1023    pub heads: Vec<MembershipHeadRef>,
1024    pub effective_frontier: Vec<MembershipCoord>,
1025    pub active_grants: BTreeMap<MembershipGrantId, MembershipGrantRecord>,
1026    pub provider_admin: super::provider::ProviderAdminResolution,
1027    pub state_hash: ObjectHash,
1028}
1029
1030#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1031#[serde(rename_all = "snake_case", deny_unknown_fields)]
1032pub enum MembershipConflict {
1033    ConcurrentMemberAssignments {
1034        conflict_hash: ObjectHash,
1035        heads: Vec<MembershipHeadRef>,
1036        effective_frontier: Vec<MembershipCoord>,
1037        member_pubkey: String,
1038        conflicting_grants: BTreeMap<MembershipGrantId, MembershipGrantRecord>,
1039        uncontested_grants: BTreeMap<MembershipGrantId, MembershipGrantRecord>,
1040    },
1041    RevocationCycle {
1042        conflict_hash: ObjectHash,
1043        heads: Vec<MembershipHeadRef>,
1044        cyclic_sources: Vec<MembershipCoord>,
1045        involved_owner_grants: BTreeSet<MembershipGrantId>,
1046        maximal_valid_branches: Vec<StoreMembershipBranch>,
1047    },
1048}
1049
1050#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1051#[serde(rename_all = "snake_case", deny_unknown_fields)]
1052pub enum MembershipStatus {
1053    Resolved(ResolvedStoreMembership),
1054    Conflict(MembershipConflict),
1055}
1056
1057#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1058#[serde(deny_unknown_fields)]
1059pub struct StoreMembershipConflictResolutionRef {
1060    pub conflict_hash: ObjectHash,
1061    pub resolver_pubkey: String,
1062    pub resolution_hash: ObjectHash,
1063    pub object: ExactObjectRef,
1064}
1065
1066#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1067#[serde(deny_unknown_fields)]
1068pub struct StoreMembershipConflictResolution {
1069    pub version: u32,
1070    pub store_root_hash: ObjectHash,
1071    pub conflict_hash: ObjectHash,
1072    pub conflicting_heads: Vec<MembershipHeadRef>,
1073    pub retired_owner_grants: BTreeSet<MembershipGrantId>,
1074    pub resolver_pubkey: String,
1075    pub resolver_branch_heads: Vec<MembershipHeadRef>,
1076    pub replacement_grant: MembershipGrantId,
1077    pub replacement_membership: GrantStreamAnchor,
1078    pub signature: String,
1079}
1080
1081impl StoreMembershipConflictResolution {
1082    fn canonical_bytes(&self) -> Vec<u8> {
1083        #[derive(Serialize)]
1084        struct Signed<'a> {
1085            domain: &'static str,
1086            version: u32,
1087            store_root_hash: ObjectHash,
1088            conflict_hash: ObjectHash,
1089            conflicting_heads: &'a [MembershipHeadRef],
1090            retired_owner_grants: &'a BTreeSet<MembershipGrantId>,
1091            resolver_pubkey: &'a str,
1092            resolver_branch_heads: &'a [MembershipHeadRef],
1093            replacement_grant: &'a MembershipGrantId,
1094            replacement_membership: &'a GrantStreamAnchor,
1095        }
1096        serde_json::to_vec(&Signed {
1097            domain: "coven.store-membership-conflict-resolution.v1",
1098            version: self.version,
1099            store_root_hash: self.store_root_hash,
1100            conflict_hash: self.conflict_hash,
1101            conflicting_heads: &self.conflicting_heads,
1102            retired_owner_grants: &self.retired_owner_grants,
1103            resolver_pubkey: &self.resolver_pubkey,
1104            resolver_branch_heads: &self.resolver_branch_heads,
1105            replacement_grant: &self.replacement_grant,
1106            replacement_membership: &self.replacement_membership,
1107        })
1108        .expect("Store membership resolution serialization cannot fail")
1109    }
1110
1111    pub fn resolution_hash(&self) -> ObjectHash {
1112        ObjectHash::digest(
1113            &serde_json::to_vec(self)
1114                .expect("Store membership resolution serialization cannot fail"),
1115        )
1116    }
1117
1118    pub fn resolution_ref(&self, object: ExactObjectRef) -> StoreMembershipConflictResolutionRef {
1119        StoreMembershipConflictResolutionRef {
1120            conflict_hash: self.conflict_hash,
1121            resolver_pubkey: self.resolver_pubkey.clone(),
1122            resolution_hash: self.resolution_hash(),
1123            object,
1124        }
1125    }
1126
1127    pub fn verify_signature(&self) -> bool {
1128        self.version == STORE_PROTOCOL_VERSION
1129            && self.replacement_grant
1130                == derive_store_resolution_grant(&self.conflict_hash, &self.resolver_pubkey)
1131            && keys::verify_signature_hex(
1132                &self.resolver_pubkey,
1133                &self.signature,
1134                &self.canonical_bytes(),
1135            )
1136    }
1137
1138    pub fn verify_against(
1139        &self,
1140        store_root_hash: ObjectHash,
1141        conflict: &MembershipConflict,
1142    ) -> bool {
1143        let MembershipConflict::RevocationCycle {
1144            conflict_hash,
1145            heads,
1146            involved_owner_grants,
1147            maximal_valid_branches,
1148            ..
1149        } = conflict
1150        else {
1151            return false;
1152        };
1153        let Some(branch) = maximal_valid_branches
1154            .iter()
1155            .find(|branch| branch.heads == self.resolver_branch_heads)
1156        else {
1157            return false;
1158        };
1159        let mut expected_retired = involved_owner_grants.clone();
1160        expected_retired.extend(branch.active_grants.iter().filter_map(|(grant, record)| {
1161            (record.member_pubkey == self.resolver_pubkey && record.role == MemberRole::Owner)
1162                .then_some(grant.clone())
1163        }));
1164        self.version == STORE_PROTOCOL_VERSION
1165            && self.store_root_hash == store_root_hash
1166            && self.conflict_hash == *conflict_hash
1167            && self.conflicting_heads == *heads
1168            && self.retired_owner_grants == expected_retired
1169            && self.replacement_grant
1170                == derive_store_resolution_grant(conflict_hash, &self.resolver_pubkey)
1171            && branch.active_grants.values().any(|record| {
1172                record.member_pubkey == self.resolver_pubkey && record.role == MemberRole::Owner
1173            })
1174            && self.verify_signature()
1175    }
1176}
1177
1178pub fn derive_store_resolution_grant(
1179    conflict_hash: &ObjectHash,
1180    resolver_pubkey: &str,
1181) -> MembershipGrantId {
1182    MembershipGrantId(ObjectHash::digest(
1183        format!("coven.store-membership-resolution-grant.v1\0{conflict_hash}\0{resolver_pubkey}")
1184            .as_bytes(),
1185    ))
1186}
1187
1188pub fn resolve_store_membership_conflict(
1189    store_root_hash: ObjectHash,
1190    conflict: &MembershipConflict,
1191    resolutions: &[(
1192        StoreMembershipConflictResolutionRef,
1193        StoreMembershipConflictResolution,
1194    )],
1195) -> Result<ResolvedStoreMembership, MembershipError> {
1196    let MembershipConflict::RevocationCycle {
1197        maximal_valid_branches,
1198        ..
1199    } = conflict
1200    else {
1201        return Err(MembershipError::InvalidConflictResolution);
1202    };
1203    if resolutions.is_empty() {
1204        return Err(MembershipError::InvalidConflictResolution);
1205    }
1206    let mut by_resolver = BTreeMap::new();
1207    let mut selected_branches = Vec::new();
1208    let mut retired_owner_grants = BTreeSet::new();
1209    for (_, resolution) in resolutions {
1210        if !resolution.verify_against(store_root_hash, conflict) {
1211            return Err(MembershipError::InvalidConflictResolution);
1212        }
1213        if let Some(existing) = by_resolver.insert(
1214            resolution.resolver_pubkey.clone(),
1215            resolution.resolution_hash(),
1216        ) {
1217            if existing != resolution.resolution_hash() {
1218                return Err(MembershipError::InvalidConflictResolution);
1219            }
1220            continue;
1221        }
1222        let branch = maximal_valid_branches
1223            .iter()
1224            .find(|branch| branch.heads == resolution.resolver_branch_heads)
1225            .ok_or(MembershipError::InvalidConflictResolution)?;
1226        if !selected_branches
1227            .iter()
1228            .any(|selected: &&StoreMembershipBranch| selected.heads == branch.heads)
1229        {
1230            selected_branches.push(branch);
1231        }
1232        retired_owner_grants.extend(resolution.retired_owner_grants.iter().cloned());
1233    }
1234    let (first_branch, other_branches) = selected_branches
1235        .split_first()
1236        .ok_or(MembershipError::InvalidConflictResolution)?;
1237    let mut active_grants = first_branch
1238        .active_grants
1239        .iter()
1240        .filter(|(grant, _)| !retired_owner_grants.contains(*grant))
1241        .map(|(grant, record)| (grant.clone(), record.clone()))
1242        .collect::<BTreeMap<_, _>>();
1243    active_grants.retain(|grant, record| {
1244        other_branches
1245            .iter()
1246            .all(|branch| branch.active_grants.get(grant) == Some(record))
1247    });
1248    for (reference, resolution) in resolutions {
1249        let record = MembershipGrantRecord {
1250            member_pubkey: resolution.resolver_pubkey.clone(),
1251            role: MemberRole::Owner,
1252            provider_account_email: None,
1253            creation_authority: MembershipGrantCreationAuthority::ConflictResolution(
1254                reference.clone(),
1255            ),
1256        };
1257        if active_grants
1258            .insert(resolution.replacement_grant.clone(), record.clone())
1259            .is_some_and(|current| current != record)
1260        {
1261            return Err(MembershipError::InvalidConflictResolution);
1262        }
1263    }
1264    let mut members = BTreeSet::new();
1265    if !active_grants
1266        .values()
1267        .any(|record| record.role == MemberRole::Owner)
1268        || active_grants
1269            .values()
1270            .any(|record| !members.insert(record.member_pubkey.clone()))
1271    {
1272        return Err(MembershipError::InvalidConflictResolution);
1273    }
1274    let provider_admin = super::provider::ProviderAdminResolution::Resolved(
1275        super::provider::ProviderAdminState::merge(
1276            selected_branches
1277                .iter()
1278                .map(|branch| branch.provider_admin.combined_state().clone()),
1279        )?,
1280    );
1281    Ok(ResolvedStoreMembership {
1282        state_hash: store_membership_state_hash(&active_grants, &provider_admin),
1283        active_grants,
1284        provider_admin,
1285    })
1286}
1287
1288#[derive(Debug, Clone)]
1289struct GrantRecord {
1290    pubkey: String,
1291    role: MemberRole,
1292    provider_account_email: Option<String>,
1293    creation_authority: MembershipGrantCreationAuthority,
1294}
1295
1296#[derive(Debug, Clone, Default)]
1297struct CausalState {
1298    grants: BTreeMap<MembershipGrantId, GrantRecord>,
1299    removed: BTreeSet<MembershipGrantId>,
1300}
1301
1302#[derive(Debug, Clone)]
1303pub struct MembershipChain {
1304    entries: Vec<MembershipEntry>,
1305    coords: Vec<MembershipCoord>,
1306    state: CausalState,
1307    included: BTreeSet<MembershipCoord>,
1308    status: Option<MembershipStatus>,
1309    head_refs: Vec<MembershipHeadRef>,
1310    resolution_checkpoint: Option<MembershipResolutionCheckpoint>,
1311    provider_admin_genesis: super::provider::ProviderAdminState,
1312}
1313
1314#[derive(Debug, Clone)]
1315struct MembershipResolutionCheckpoint {
1316    raw_heads: Vec<MembershipCoord>,
1317    effective_frontier: Vec<MembershipCoord>,
1318    grants: BTreeMap<MembershipGrantId, MembershipGrantRecord>,
1319    grant_anchors: BTreeMap<MembershipGrantId, GrantStreamAnchor>,
1320    removed: BTreeSet<MembershipGrantId>,
1321    included: BTreeSet<MembershipCoord>,
1322    resolutions: Vec<StoreMembershipConflictResolutionRef>,
1323    provider_admin: super::provider::ProviderAdminState,
1324}
1325
1326#[cfg(any(test, feature = "test-utils"))]
1327fn test_provider_admin_genesis(
1328    entries: &[MembershipEntry],
1329) -> Result<super::provider::ProviderAdminState, MembershipError> {
1330    let founder = entries
1331        .iter()
1332        .find_map(|entry| match &entry.change {
1333            MembershipChange::Founder { provider_admin, .. } => Some((entry, provider_admin)),
1334            _ => None,
1335        })
1336        .ok_or(MembershipError::InvalidFounder)?;
1337    let root_bytes = founder.0.store_id.as_bytes();
1338    let root = StoreRootRef {
1339        store_root_id: ObjectHash::digest(
1340            format!("{} test root id", founder.0.store_id).as_bytes(),
1341        ),
1342        store_root_hash: ObjectHash::digest(root_bytes),
1343        object: ExactObjectRef::new(
1344            crate::storage::cloud::ObjectSlot::logical(format!(
1345                "store-v1/test/{}/root.json",
1346                founder.0.store_id
1347            ))
1348            .expect("valid test root slot"),
1349            root_bytes.len() as u64,
1350            ObjectHash::digest(root_bytes),
1351        ),
1352    };
1353    let registration: StoreDeviceRegistrationRef =
1354        serde_json::from_value(serde_json::json!({
1355            "device_id": ObjectHash::digest(format!("{} founder device", founder.0.store_id).as_bytes()),
1356            "registration_hash": ObjectHash::digest(format!("{} founder registration", founder.0.store_id).as_bytes()),
1357            "object": {
1358                "slot": {"logical_key": format!("store-v1/test/{}/registration.json", founder.0.store_id), "physical": {"kind": "logical_key"}},
1359                "stored_size": 1,
1360                "stored_hash": ObjectHash::digest(format!("{} founder registration object", founder.0.store_id).as_bytes()),
1361            }
1362        }))
1363        .expect("valid test founder registration reference");
1364    Ok(super::provider::ProviderAdminState::founder_from_root(
1365        root,
1366        registration,
1367        founder.1,
1368    ))
1369}
1370
1371impl MembershipChain {
1372    #[cfg(any(test, feature = "test-utils"))]
1373    pub fn from_entries(entries: Vec<MembershipEntry>) -> Result<Self, MembershipError> {
1374        let provider_admin = test_provider_admin_genesis(&entries)?;
1375        Self::from_entries_with_coords_and_provider_admin(
1376            entries
1377                .into_iter()
1378                .map(|entry| (entry.coord(), entry))
1379                .collect(),
1380            provider_admin,
1381        )
1382    }
1383
1384    #[cfg(any(test, feature = "test-utils"))]
1385    pub fn from_entries_with_coords(
1386        entries: Vec<(MembershipCoord, MembershipEntry)>,
1387    ) -> Result<Self, MembershipError> {
1388        let values = entries
1389            .iter()
1390            .map(|(_, entry)| entry.clone())
1391            .collect::<Vec<_>>();
1392        let provider_admin = test_provider_admin_genesis(&values)?;
1393        Self::from_entries_with_coords_and_provider_admin(entries, provider_admin)
1394    }
1395
1396    #[cfg(any(test, feature = "test-utils"))]
1397    pub fn from_entries_with_coords_and_heads(
1398        entries: Vec<(MembershipCoord, MembershipEntry)>,
1399        heads: Vec<(MembershipHeadRef, AuthorHead)>,
1400    ) -> Result<Self, MembershipError> {
1401        let values = entries
1402            .iter()
1403            .map(|(_, entry)| entry.clone())
1404            .collect::<Vec<_>>();
1405        let provider_admin = test_provider_admin_genesis(&values)?;
1406        Self::from_entries_with_coords_and_heads_and_provider_admin(entries, heads, provider_admin)
1407    }
1408
1409    pub fn from_entries_with_coords_and_provider_admin(
1410        entries: Vec<(MembershipCoord, MembershipEntry)>,
1411        provider_admin: super::provider::ProviderAdminState,
1412    ) -> Result<Self, MembershipError> {
1413        Self::from_entries_with_coords_and_head_refs(entries, Vec::new(), provider_admin)
1414    }
1415
1416    pub fn from_entries_with_coords_and_heads_and_provider_admin(
1417        entries: Vec<(MembershipCoord, MembershipEntry)>,
1418        heads: Vec<(MembershipHeadRef, AuthorHead)>,
1419        provider_admin: super::provider::ProviderAdminState,
1420    ) -> Result<Self, MembershipError> {
1421        let expected_store = entries
1422            .first()
1423            .map(|(_, entry)| entry.store_id.as_str())
1424            .ok_or(MembershipError::EmptyChain)?;
1425        if heads.iter().any(|(reference, head)| {
1426            reference.head_hash != head.head_hash()
1427                || head.store_id != expected_store
1428                || entries
1429                    .iter()
1430                    .find(|(coord, _)| *coord == head.entry_coord())
1431                    .is_none_or(|(_, entry)| head.resolutions != entry.resolution_dependencies)
1432        }) {
1433            return Err(MembershipError::MissingConflictHeads);
1434        }
1435        Self::from_entries_with_coords_and_head_refs(
1436            entries,
1437            heads.into_iter().map(|(reference, _)| reference).collect(),
1438            provider_admin,
1439        )
1440    }
1441
1442    fn from_entries_with_coords_and_head_refs(
1443        entries: Vec<(MembershipCoord, MembershipEntry)>,
1444        head_refs: Vec<MembershipHeadRef>,
1445        provider_admin_genesis: super::provider::ProviderAdminState,
1446    ) -> Result<Self, MembershipError> {
1447        if entries.is_empty() {
1448            return Err(MembershipError::EmptyChain);
1449        }
1450        let (coords, entries): (Vec<_>, Vec<_>) = entries.into_iter().unzip();
1451        let mut chain = Self {
1452            entries,
1453            coords,
1454            state: CausalState::default(),
1455            included: BTreeSet::new(),
1456            status: None,
1457            head_refs,
1458            resolution_checkpoint: None,
1459            provider_admin_genesis,
1460        };
1461        chain.rebuild()?;
1462        Ok(chain)
1463    }
1464
1465    pub fn entries(&self) -> &[MembershipEntry] {
1466        &self.entries
1467    }
1468
1469    pub fn status(&self) -> &MembershipStatus {
1470        self.status
1471            .as_ref()
1472            .expect("a loaded membership chain always has status")
1473    }
1474
1475    pub fn head_refs(&self) -> &[MembershipHeadRef] {
1476        &self.head_refs
1477    }
1478
1479    pub(crate) fn head_ref_for_stream(
1480        &self,
1481        author: &str,
1482        grant: &MembershipGrantId,
1483        stream_id: AuthorStreamId,
1484    ) -> Option<&MembershipHeadRef> {
1485        self.head_refs.iter().find(|reference| {
1486            reference.coord.author_pubkey == author
1487                && reference.coord.author_owner_grant == *grant
1488                && reference.coord.stream_id == stream_id
1489        })
1490    }
1491
1492    pub(crate) fn membership_anchor(
1493        &self,
1494        grant: &MembershipGrantId,
1495    ) -> Option<&GrantStreamAnchor> {
1496        self.entries
1497            .iter()
1498            .find_map(|entry| match &entry.change {
1499                MembershipChange::Founder {
1500                    owner_grant_id,
1501                    membership,
1502                    ..
1503                } if owner_grant_id == grant => Some(membership),
1504                MembershipChange::SetMember {
1505                    grant_id,
1506                    membership: Some(membership),
1507                    ..
1508                } if grant_id == grant => Some(membership),
1509                _ => None,
1510            })
1511            .or_else(|| {
1512                self.resolution_checkpoint
1513                    .as_ref()?
1514                    .grant_anchors
1515                    .get(grant)
1516            })
1517    }
1518
1519    pub(crate) fn membership_stream_id(&self, grant: &MembershipGrantId) -> Option<AuthorStreamId> {
1520        let record = self.state.grants.get(grant)?;
1521        store_membership_anchor_stream(&record.pubkey, grant, self.membership_anchor(grant)?)
1522    }
1523
1524    pub(crate) fn activated_membership_streams(
1525        &self,
1526    ) -> Vec<(MembershipStreamKey, GrantStreamAnchor)> {
1527        let mut streams = self
1528            .state
1529            .grants
1530            .iter()
1531            .filter_map(|(grant, record)| {
1532                let anchor = self.membership_anchor(grant)?.clone();
1533                let stream_id = self.membership_stream_id(grant)?;
1534                Some((
1535                    MembershipStreamKey {
1536                        author_pubkey: record.pubkey.clone(),
1537                        author_owner_grant: grant.clone(),
1538                        stream_id,
1539                    },
1540                    anchor,
1541                ))
1542            })
1543            .collect::<BTreeMap<_, _>>();
1544        let mut included = self.included.clone();
1545        if let MembershipStatus::Conflict(MembershipConflict::RevocationCycle {
1546            maximal_valid_branches,
1547            ..
1548        }) = self.status()
1549        {
1550            for branch in maximal_valid_branches {
1551                included.extend(membership_history_closure(
1552                    &self.entries,
1553                    &branch.effective_frontier,
1554                ));
1555            }
1556        }
1557        for (coord, entry) in self.entries_with_coords() {
1558            if !included.contains(coord) {
1559                continue;
1560            }
1561            let (owner_pubkey, grant, anchor) = match &entry.change {
1562                MembershipChange::SetMember {
1563                    user_pubkey,
1564                    role: MemberRole::Owner,
1565                    grant_id,
1566                    membership: Some(membership),
1567                    ..
1568                } => (user_pubkey, grant_id, membership),
1569                _ => continue,
1570            };
1571            let stream_id = store_membership_anchor_stream(owner_pubkey, grant, anchor)
1572                .expect("validated Owner grant has a Store membership stream anchor");
1573            streams.insert(
1574                MembershipStreamKey {
1575                    author_pubkey: owner_pubkey.clone(),
1576                    author_owner_grant: grant.clone(),
1577                    stream_id,
1578                },
1579                anchor.clone(),
1580            );
1581        }
1582        streams.into_iter().collect()
1583    }
1584
1585    pub(crate) fn activate_head_ref(
1586        &mut self,
1587        reference: MembershipHeadRef,
1588    ) -> Result<(), MembershipError> {
1589        if !self.coords.contains(&reference.coord) {
1590            return Err(MembershipError::MissingConflictHeads);
1591        }
1592        let stream = reference.coord.stream_key();
1593        self.head_refs
1594            .retain(|current| current.coord.stream_key() != stream);
1595        self.head_refs.push(reference);
1596        self.head_refs.sort();
1597        self.rebuild()
1598    }
1599
1600    pub fn resolution_refs(&self) -> &[StoreMembershipConflictResolutionRef] {
1601        self.resolution_checkpoint
1602            .as_ref()
1603            .map_or(&[], |checkpoint| checkpoint.resolutions.as_slice())
1604    }
1605
1606    pub fn conflict(&self) -> Option<&MembershipConflict> {
1607        match self.status() {
1608            MembershipStatus::Resolved(_) => None,
1609            MembershipStatus::Conflict(conflict) => Some(conflict),
1610        }
1611    }
1612
1613    pub fn ensure_resolved(&self) -> Result<(), MembershipError> {
1614        match self.status() {
1615            MembershipStatus::Resolved(_) => Ok(()),
1616            MembershipStatus::Conflict(_) => Err(MembershipError::Conflict),
1617        }
1618    }
1619
1620    pub fn resolved_with(
1621        &self,
1622        store_root_hash: ObjectHash,
1623        resolutions: &[(
1624            StoreMembershipConflictResolutionRef,
1625            StoreMembershipConflictResolution,
1626        )],
1627    ) -> Result<ResolvedStoreMembership, MembershipError> {
1628        match self.status() {
1629            MembershipStatus::Resolved(resolved) if resolutions.is_empty() => Ok(resolved.clone()),
1630            MembershipStatus::Conflict(conflict) => {
1631                resolve_store_membership_conflict(store_root_hash, conflict, resolutions)
1632            }
1633            MembershipStatus::Resolved(_) => Err(MembershipError::InvalidConflictResolution),
1634        }
1635    }
1636
1637    pub fn signed_cycle_resolution(
1638        &self,
1639        store_root_hash: ObjectHash,
1640        resolver_branch_heads: Vec<MembershipHeadRef>,
1641        replacement_membership: GrantStreamAnchor,
1642        signer: &UserKeypair,
1643    ) -> Result<StoreMembershipConflictResolution, MembershipError> {
1644        let MembershipStatus::Conflict(MembershipConflict::RevocationCycle {
1645            conflict_hash,
1646            heads,
1647            involved_owner_grants,
1648            maximal_valid_branches,
1649            ..
1650        }) = self.status()
1651        else {
1652            return Err(MembershipError::Conflict);
1653        };
1654        let resolver_pubkey = keys::public_key_hex(signer);
1655        let branch = maximal_valid_branches
1656            .iter()
1657            .find(|branch| branch.heads == resolver_branch_heads)
1658            .ok_or(MembershipError::InvalidConflictResolution)?;
1659        if !branch.active_grants.values().any(|record| {
1660            record.member_pubkey == resolver_pubkey && record.role == MemberRole::Owner
1661        }) {
1662            return Err(MembershipError::SignerIsNotOwner(resolver_pubkey));
1663        }
1664        let replacement_grant = derive_store_resolution_grant(conflict_hash, &resolver_pubkey);
1665        let mut retired_owner_grants = involved_owner_grants.clone();
1666        retired_owner_grants.extend(branch.active_grants.iter().filter_map(|(grant, record)| {
1667            (record.member_pubkey == resolver_pubkey && record.role == MemberRole::Owner)
1668                .then_some(grant.clone())
1669        }));
1670        let mut resolution = StoreMembershipConflictResolution {
1671            version: STORE_PROTOCOL_VERSION,
1672            store_root_hash,
1673            conflict_hash: *conflict_hash,
1674            conflicting_heads: heads.clone(),
1675            retired_owner_grants,
1676            resolver_pubkey,
1677            resolver_branch_heads,
1678            replacement_grant,
1679            replacement_membership,
1680            signature: String::new(),
1681        };
1682        resolution.signature = keys::sign_hex(signer, &resolution.canonical_bytes()).1;
1683        Ok(resolution)
1684    }
1685
1686    pub fn entries_with_coords(
1687        &self,
1688    ) -> impl Iterator<Item = (&MembershipCoord, &MembershipEntry)> {
1689        self.coords.iter().zip(self.entries.iter())
1690    }
1691
1692    pub fn store_id(&self) -> Option<&str> {
1693        self.entries.first().map(|entry| entry.store_id.as_str())
1694    }
1695
1696    pub fn founder_coord(&self) -> Option<&MembershipCoord> {
1697        self.entries_with_coords().find_map(|(coord, entry)| {
1698            matches!(entry.change, MembershipChange::Founder { .. }).then_some(coord)
1699        })
1700    }
1701
1702    pub fn founder_pubkey(&self) -> Option<&str> {
1703        self.entries.iter().find_map(|entry| match &entry.change {
1704            MembershipChange::Founder { owner_pubkey, .. } => Some(owner_pubkey.as_str()),
1705            MembershipChange::SetMember { .. }
1706            | MembershipChange::RemoveMember { .. }
1707            | MembershipChange::ProviderAdmin
1708            | MembershipChange::ResolutionActivation { .. } => None,
1709        })
1710    }
1711
1712    pub fn is_founded_by(&self, owner_pubkey: &str) -> bool {
1713        self.founder_pubkey() == Some(owner_pubkey)
1714    }
1715
1716    pub fn validate(&self) -> Result<(), MembershipError> {
1717        let mut rebuilt = self.clone();
1718        rebuilt.rebuild()
1719    }
1720
1721    pub fn add_entry(&mut self, entry: MembershipEntry) -> Result<(), MembershipError> {
1722        self.add_entry_at(entry.coord(), entry)
1723    }
1724
1725    pub fn add_entry_at(
1726        &mut self,
1727        coord: MembershipCoord,
1728        entry: MembershipEntry,
1729    ) -> Result<(), MembershipError> {
1730        self.entries.push(entry);
1731        self.coords.push(coord);
1732        if let Err(error) = self.rebuild() {
1733            self.entries.pop();
1734            self.coords.pop();
1735            self.rebuild().expect("previous membership chain validated");
1736            return Err(error);
1737        }
1738        Ok(())
1739    }
1740
1741    pub fn can_write_now(&self, pubkey: &str) -> bool {
1742        if self.conflict().is_some() {
1743            return false;
1744        }
1745        self.active_grants_for(pubkey)
1746            .iter()
1747            .any(|(_, record)| record.role.can_write())
1748    }
1749
1750    pub(crate) fn contains_member_now(&self, pubkey: &str) -> bool {
1751        if self.conflict().is_some() {
1752            return false;
1753        }
1754        !self.active_grants_for(pubkey).is_empty()
1755    }
1756
1757    pub fn is_owner_now(&self, pubkey: &str) -> bool {
1758        if self.conflict().is_some() {
1759            return false;
1760        }
1761        self.active_grants_for(pubkey)
1762            .iter()
1763            .any(|(_, record)| record.role == MemberRole::Owner)
1764    }
1765
1766    pub fn authorizes_write_at(&self, coord: &MembershipCoord, pubkey: &str) -> bool {
1767        self.active_grants_for(pubkey).iter().any(|(_, record)| {
1768            record.role.can_write()
1769                && record.creation_authority
1770                    == MembershipGrantCreationAuthority::Entry(coord.clone())
1771        })
1772    }
1773
1774    pub fn authorizes_write_authority(
1775        &self,
1776        authority: &MembershipGrantCreationAuthority,
1777        pubkey: &str,
1778    ) -> bool {
1779        self.active_grants_for(pubkey)
1780            .iter()
1781            .any(|(_, record)| record.role.can_write() && &record.creation_authority == authority)
1782    }
1783
1784    pub fn contains_coord(&self, expected: &MembershipCoord) -> bool {
1785        self.coords.iter().any(|coord| coord == expected)
1786    }
1787
1788    pub fn current_members(&self) -> Vec<(String, MemberRole)> {
1789        let mut members = BTreeMap::new();
1790        for (grant, record) in &self.state.grants {
1791            if !self.state.removed.contains(grant) {
1792                members.insert(record.pubkey.clone(), record.role.clone());
1793            }
1794        }
1795        members.into_iter().collect()
1796    }
1797
1798    pub fn current_member_provider_email(&self, pubkey: &str) -> Option<&str> {
1799        self.active_grants_for(pubkey)
1800            .into_iter()
1801            .next()
1802            .and_then(|(_, record)| record.provider_account_email.as_deref())
1803    }
1804
1805    pub fn write_grant_coord(&self, pubkey: &str) -> Option<MembershipCoord> {
1806        self.active_grants_for(pubkey)
1807            .into_iter()
1808            .find(|(_, record)| record.role.can_write())
1809            .and_then(|(_, record)| match &record.creation_authority {
1810                MembershipGrantCreationAuthority::Entry(coord) => Some(coord.clone()),
1811                MembershipGrantCreationAuthority::ConflictResolution(_) => None,
1812            })
1813    }
1814
1815    pub fn write_grant_authority(&self, pubkey: &str) -> Option<MembershipGrantCreationAuthority> {
1816        self.active_grants_for(pubkey)
1817            .into_iter()
1818            .find(|(_, record)| record.role.can_write())
1819            .map(|(_, record)| record.creation_authority.clone())
1820    }
1821
1822    pub fn active_grant_ids(&self, pubkey: &str) -> BTreeSet<MembershipGrantId> {
1823        self.active_grants_for(pubkey)
1824            .into_iter()
1825            .map(|(grant, _)| grant.clone())
1826            .collect()
1827    }
1828
1829    pub fn active_owner_grant(&self, pubkey: &str) -> Option<MembershipGrantId> {
1830        self.active_grants_for(pubkey)
1831            .into_iter()
1832            .find(|(_, record)| record.role == MemberRole::Owner)
1833            .map(|(grant, _)| grant.clone())
1834    }
1835
1836    pub(crate) fn reusable_author_streams(
1837        &self,
1838        author_pubkey: &str,
1839        grant: &MembershipGrantId,
1840    ) -> BTreeSet<AuthorStreamId> {
1841        self.effective_frontier()
1842            .into_iter()
1843            .filter(|coord| {
1844                coord.author_pubkey == author_pubkey
1845                    && coord.author_owner_grant == *grant
1846                    && self.raw_stream_tip(author_pubkey, grant, coord.stream_id)
1847                        == Some(coord.clone())
1848            })
1849            .map(|coord| coord.stream_id)
1850            .collect()
1851    }
1852
1853    #[cfg(test)]
1854    pub(crate) fn preferred_author_stream(
1855        &self,
1856        author_pubkey: &str,
1857        grant: &MembershipGrantId,
1858    ) -> Option<AuthorStreamId> {
1859        self.reusable_author_streams(author_pubkey, grant)
1860            .into_iter()
1861            .next_back()
1862    }
1863
1864    /// Raw signed coverage: the greatest loaded coordinate in every stream,
1865    /// including suffixes removed by causal pruning.
1866    pub fn author_heads(&self) -> Vec<MembershipCoord> {
1867        self.frontier_from_coords(self.coords.iter())
1868    }
1869
1870    /// Effective authoring frontier after causal pruning.
1871    pub fn effective_frontier(&self) -> Vec<MembershipCoord> {
1872        self.frontier_from_coords(
1873            self.coords
1874                .iter()
1875                .filter(|coord| self.included.contains(*coord)),
1876        )
1877    }
1878
1879    fn frontier_from_coords<'a>(
1880        &self,
1881        coords: impl Iterator<Item = &'a MembershipCoord>,
1882    ) -> Vec<MembershipCoord> {
1883        let mut heads = BTreeMap::<MembershipStreamKey, MembershipCoord>::new();
1884        for coord in coords {
1885            heads
1886                .entry(coord.stream_key())
1887                .and_modify(|current| {
1888                    if coord.seq > current.seq {
1889                        *current = coord.clone();
1890                    }
1891                })
1892                .or_insert_with(|| coord.clone());
1893        }
1894        heads.into_values().collect()
1895    }
1896
1897    pub fn stream_tip(
1898        &self,
1899        author_pubkey: &str,
1900        grant: &MembershipGrantId,
1901        stream_id: AuthorStreamId,
1902    ) -> Option<MembershipCoord> {
1903        self.effective_frontier().into_iter().find(|coord| {
1904            coord.author_pubkey == author_pubkey
1905                && coord.author_owner_grant == *grant
1906                && coord.stream_id == stream_id
1907        })
1908    }
1909
1910    pub fn raw_stream_tip(
1911        &self,
1912        author_pubkey: &str,
1913        grant: &MembershipGrantId,
1914        stream_id: AuthorStreamId,
1915    ) -> Option<MembershipCoord> {
1916        self.coords
1917            .iter()
1918            .filter(|coord| {
1919                coord.author_pubkey == author_pubkey
1920                    && coord.author_owner_grant == *grant
1921                    && coord.stream_id == stream_id
1922            })
1923            .max_by_key(|coord| coord.seq)
1924            .cloned()
1925    }
1926
1927    pub(crate) fn next_member_grant_id_in_stream(
1928        &self,
1929        signer: &UserKeypair,
1930        stream_id: AuthorStreamId,
1931        user_pubkey: &str,
1932    ) -> Result<MembershipGrantId, MembershipError> {
1933        let author = keys::public_key_hex(signer);
1934        let author_grant = self
1935            .active_owner_grant(&author)
1936            .ok_or_else(|| MembershipError::SignerIsNotOwner(author.clone()))?;
1937        let (seq, _) = self.next_stream_position(&author, &author_grant, stream_id)?;
1938        Ok(derive_grant_id(
1939            self.store_id().expect("validated chain has a store id"),
1940            &author,
1941            &author_grant,
1942            stream_id,
1943            seq,
1944            user_pubkey,
1945        ))
1946    }
1947
1948    pub(crate) fn signed_set_member_with_anchor_in_stream(
1949        &self,
1950        signer: &UserKeypair,
1951        stream_id: AuthorStreamId,
1952        user_pubkey: String,
1953        provider_account_email: Option<String>,
1954        role: MemberRole,
1955        membership: Option<GrantStreamAnchor>,
1956        created_at: String,
1957    ) -> Result<MembershipEntry, MembershipError> {
1958        let author = keys::public_key_hex(signer);
1959        let author_grant = self
1960            .active_owner_grant(&author)
1961            .ok_or_else(|| MembershipError::SignerIsNotOwner(author.clone()))?;
1962        let (seq, previous_hash) = self.next_stream_position(&author, &author_grant, stream_id)?;
1963        let grant_id = derive_grant_id(
1964            self.store_id().expect("validated chain has a store id"),
1965            &author,
1966            &author_grant,
1967            stream_id,
1968            seq,
1969            &user_pubkey,
1970        );
1971        let replaces = self.active_grant_ids(&user_pubkey);
1972        let owner_barriers = self.owner_barriers(&replaces);
1973        if (role == MemberRole::Owner) != membership.is_some() {
1974            return Err(MembershipError::InvalidOwnerMembershipAnchor(
1975                self.entries.len(),
1976            ));
1977        }
1978        let mut entry = MembershipEntry {
1979            version: STORE_PROTOCOL_VERSION,
1980            store_id: self
1981                .store_id()
1982                .expect("validated chain has a store id")
1983                .to_string(),
1984            author_pubkey: author,
1985            author_owner_grant: author_grant,
1986            stream_id,
1987            seq,
1988            previous_hash,
1989            dependencies: self.frontier(),
1990            resolution_dependencies: self.resolution_refs().to_vec(),
1991            created_at,
1992            change: MembershipChange::SetMember {
1993                user_pubkey: user_pubkey.clone(),
1994                provider_account_email,
1995                role,
1996                grant_id,
1997                membership,
1998                replaces,
1999                owner_barriers,
2000            },
2001            provider_admin: None,
2002            signature: String::new(),
2003        };
2004        sign_membership_entry(&mut entry, signer);
2005        let mut candidate = self.clone();
2006        candidate.add_entry(entry.clone())?;
2007        Ok(entry)
2008    }
2009
2010    #[cfg(any(test, feature = "test-utils"))]
2011    pub fn signed_set_member_in_stream(
2012        &self,
2013        signer: &UserKeypair,
2014        stream_id: AuthorStreamId,
2015        user_pubkey: String,
2016        provider_account_email: Option<String>,
2017        role: MemberRole,
2018        created_at: String,
2019    ) -> Result<MembershipEntry, MembershipError> {
2020        let grant_id = self.next_member_grant_id_in_stream(signer, stream_id, &user_pubkey)?;
2021        let membership = (role == MemberRole::Owner).then(|| GrantStreamAnchor::StoreMembership {
2022            first_slot: crate::storage::cloud::ObjectSlot::logical(format!(
2023                "{}.json",
2024                super::store_commit::membership_head_slot_prefix(
2025                    &user_pubkey,
2026                    &grant_id,
2027                    stream_id,
2028                    1,
2029                )
2030            ))
2031            .expect("test membership head slot is a valid logical key"),
2032        });
2033        self.signed_set_member_with_anchor_in_stream(
2034            signer,
2035            stream_id,
2036            user_pubkey,
2037            provider_account_email,
2038            role,
2039            membership,
2040            created_at,
2041        )
2042    }
2043
2044    pub fn signed_remove_member_in_stream(
2045        &self,
2046        signer: &UserKeypair,
2047        stream_id: AuthorStreamId,
2048        user_pubkey: String,
2049        created_at: String,
2050    ) -> Result<MembershipEntry, MembershipError> {
2051        let removes = self.active_grant_ids(&user_pubkey);
2052        if removes.is_empty() {
2053            return Err(MembershipError::NotAMember(user_pubkey));
2054        }
2055        let author = keys::public_key_hex(signer);
2056        let author_grant = self
2057            .active_owner_grant(&author)
2058            .ok_or_else(|| MembershipError::SignerIsNotOwner(author.clone()))?;
2059        let (seq, previous_hash) = self.next_stream_position(&author, &author_grant, stream_id)?;
2060        let owner_barriers = self.owner_barriers(&removes);
2061        let mut entry = MembershipEntry {
2062            version: STORE_PROTOCOL_VERSION,
2063            store_id: self
2064                .store_id()
2065                .expect("validated chain has a store id")
2066                .to_string(),
2067            author_pubkey: author,
2068            author_owner_grant: author_grant,
2069            stream_id,
2070            seq,
2071            previous_hash,
2072            dependencies: self.frontier(),
2073            resolution_dependencies: self.resolution_refs().to_vec(),
2074            created_at,
2075            change: MembershipChange::RemoveMember {
2076                user_pubkey,
2077                removes,
2078                owner_barriers,
2079            },
2080            provider_admin: None,
2081            signature: String::new(),
2082        };
2083        sign_membership_entry(&mut entry, signer);
2084        let mut candidate = self.clone();
2085        candidate.add_entry(entry.clone())?;
2086        Ok(entry)
2087    }
2088
2089    pub fn signed_provider_admin_change_in_stream(
2090        &self,
2091        signer: &UserKeypair,
2092        stream_id: AuthorStreamId,
2093        change: super::provider::ProviderAdminChange,
2094        created_at: String,
2095    ) -> Result<MembershipEntry, MembershipError> {
2096        let author = keys::public_key_hex(signer);
2097        let author_grant = self
2098            .active_owner_grant(&author)
2099            .ok_or_else(|| MembershipError::SignerIsNotOwner(author.clone()))?;
2100        let (seq, previous_hash) = self.next_stream_position(&author, &author_grant, stream_id)?;
2101        let owner_grants = self
2102            .state
2103            .grants
2104            .iter()
2105            .filter(|(grant_id, record)| {
2106                record.role == MemberRole::Owner && !self.state.removed.contains(*grant_id)
2107            })
2108            .map(|(grant_id, _)| grant_id.clone())
2109            .collect();
2110        let mut entry = MembershipEntry {
2111            version: STORE_PROTOCOL_VERSION,
2112            store_id: self
2113                .store_id()
2114                .expect("validated chain has a store id")
2115                .to_string(),
2116            author_pubkey: author,
2117            author_owner_grant: author_grant,
2118            stream_id,
2119            seq,
2120            previous_hash,
2121            dependencies: self.frontier(),
2122            resolution_dependencies: Vec::new(),
2123            created_at,
2124            change: MembershipChange::ProviderAdmin,
2125            provider_admin: Some(
2126                super::provider::ProviderAdminMembershipChange::MergeConcurrent {
2127                    change,
2128                    owner_barriers: self.owner_barriers(&owner_grants),
2129                },
2130            ),
2131            signature: String::new(),
2132        };
2133        sign_membership_entry(&mut entry, signer);
2134        let mut candidate = self.clone();
2135        candidate.add_entry(entry.clone())?;
2136        Ok(entry)
2137    }
2138
2139    pub fn signed_resolution_activation_in_stream(
2140        &self,
2141        store_root_hash: ObjectHash,
2142        signer: &UserKeypair,
2143        stream_id: AuthorStreamId,
2144        reference: StoreMembershipConflictResolutionRef,
2145        resolution: &StoreMembershipConflictResolution,
2146        created_at: String,
2147    ) -> Result<MembershipEntry, MembershipError> {
2148        self.ensure_resolved()?;
2149        let MembershipStatus::Resolved(resolved_before) = self.status() else {
2150            unreachable!("ensure_resolved accepted a conflict")
2151        };
2152        let author = keys::public_key_hex(signer);
2153        if !resolution.verify_signature()
2154            || resolution.store_root_hash != store_root_hash
2155            || reference.resolver_pubkey != author
2156            || !self.resolution_refs().contains(&reference)
2157            || self.active_owner_grant(&author) != Some(resolution.replacement_grant.clone())
2158        {
2159            return Err(MembershipError::InvalidConflictResolution);
2160        }
2161        let author_grant = resolution.replacement_grant.clone();
2162        if self
2163            .raw_stream_tip(&author, &author_grant, stream_id)
2164            .is_some()
2165        {
2166            return Err(MembershipError::ResolutionActivationRequiresFreshStream);
2167        }
2168        let mut entry = MembershipEntry {
2169            version: STORE_PROTOCOL_VERSION,
2170            store_id: self
2171                .store_id()
2172                .expect("validated chain has a store id")
2173                .to_string(),
2174            author_pubkey: author,
2175            author_owner_grant: author_grant,
2176            stream_id,
2177            seq: 1,
2178            previous_hash: None,
2179            dependencies: self.effective_frontier(),
2180            resolution_dependencies: self.resolution_refs().to_vec(),
2181            created_at,
2182            change: MembershipChange::ResolutionActivation {
2183                resolution: reference,
2184            },
2185            provider_admin: None,
2186            signature: String::new(),
2187        };
2188        sign_membership_entry(&mut entry, signer);
2189        let mut candidate = self.clone();
2190        candidate.add_entry(entry.clone())?;
2191        let MembershipStatus::Resolved(resolved_after) = candidate.status() else {
2192            return Err(MembershipError::InvalidConflictResolution);
2193        };
2194        if resolved_after.state_hash != resolved_before.state_hash {
2195            return Err(MembershipError::InvalidConflictResolution);
2196        }
2197        Ok(entry)
2198    }
2199
2200    fn next_stream_position(
2201        &self,
2202        author: &str,
2203        grant: &MembershipGrantId,
2204        stream_id: AuthorStreamId,
2205    ) -> Result<(u64, Option<ObjectHash>), MembershipError> {
2206        let raw_tip = self.raw_stream_tip(author, grant, stream_id);
2207        let effective_tip = self.stream_tip(author, grant, stream_id);
2208        if raw_tip != effective_tip {
2209            return Err(MembershipError::PrunedAuthorStream);
2210        }
2211        Ok(effective_tip.map_or((1, None), |tip| (tip.seq + 1, Some(tip.entry_hash))))
2212    }
2213
2214    fn frontier(&self) -> Vec<MembershipCoord> {
2215        self.effective_frontier()
2216    }
2217
2218    fn owner_barriers(
2219        &self,
2220        grants: &BTreeSet<MembershipGrantId>,
2221    ) -> BTreeMap<MembershipGrantId, OwnerStreamBarrier> {
2222        grants
2223            .iter()
2224            .filter_map(|grant| {
2225                let record = self.state.grants.get(grant)?;
2226                (record.role == MemberRole::Owner).then(|| {
2227                    let observed_streams = self
2228                        .effective_frontier()
2229                        .into_iter()
2230                        .filter(|coord| coord.author_owner_grant == *grant)
2231                        .collect();
2232                    (grant.clone(), OwnerStreamBarrier { observed_streams })
2233                })
2234            })
2235            .collect()
2236    }
2237
2238    fn active_grants_for(&self, pubkey: &str) -> Vec<(&MembershipGrantId, &GrantRecord)> {
2239        self.state
2240            .grants
2241            .iter()
2242            .filter(|(grant, record)| {
2243                record.pubkey == pubkey && !self.state.removed.contains(*grant)
2244            })
2245            .collect()
2246    }
2247
2248    fn rebuild(&mut self) -> Result<(), MembershipError> {
2249        let expected_store = self
2250            .entries
2251            .first()
2252            .ok_or(MembershipError::EmptyChain)?
2253            .store_id
2254            .clone();
2255        if expected_store.is_empty() {
2256            return Err(MembershipError::InvalidFounder);
2257        }
2258
2259        for (index, (coord, entry)) in self.entries_with_coords().enumerate() {
2260            if entry.version != STORE_PROTOCOL_VERSION {
2261                return Err(MembershipError::UnsupportedVersion(index));
2262            }
2263            if entry.store_id != expected_store {
2264                return Err(MembershipError::StoreMismatch {
2265                    index,
2266                    expected: expected_store.clone(),
2267                    actual: entry.store_id.clone(),
2268                });
2269            }
2270            if !verify_membership_entry(entry) {
2271                return Err(MembershipError::InvalidSignature(index));
2272            }
2273            let actual = entry.coord();
2274            if *coord != actual {
2275                return Err(MembershipError::CoordinateMismatch {
2276                    index,
2277                    expected: Box::new(coord.clone()),
2278                    actual: Box::new(actual),
2279                });
2280            }
2281            if !entry
2282                .dependencies
2283                .windows(2)
2284                .all(|pair| pair[0].stream_key() < pair[1].stream_key())
2285            {
2286                return Err(MembershipError::NonCanonicalDependencyFrontier { index });
2287            }
2288            let barriers = match &entry.change {
2289                MembershipChange::SetMember {
2290                    user_pubkey,
2291                    role,
2292                    grant_id,
2293                    membership,
2294                    owner_barriers,
2295                    ..
2296                } => {
2297                    if (role == &MemberRole::Owner)
2298                        != membership.as_ref().is_some_and(|anchor| {
2299                            store_membership_anchor_stream(user_pubkey, grant_id, anchor).is_some()
2300                        })
2301                    {
2302                        return Err(MembershipError::InvalidOwnerMembershipAnchor(index));
2303                    }
2304                    owner_barriers
2305                }
2306                MembershipChange::RemoveMember { owner_barriers, .. } => owner_barriers,
2307                MembershipChange::ResolutionActivation { resolution } => {
2308                    if resolution.resolver_pubkey != entry.author_pubkey
2309                        || entry.seq != 1
2310                        || entry.previous_hash.is_some()
2311                        || entry
2312                            .dependencies
2313                            .iter()
2314                            .any(|dependency| dependency.stream_key() == entry.coord().stream_key())
2315                        || entry.author_owner_grant
2316                            != derive_store_resolution_grant(
2317                                &resolution.conflict_hash,
2318                                &resolution.resolver_pubkey,
2319                            )
2320                        || entry
2321                            .resolution_dependencies
2322                            .binary_search(resolution)
2323                            .is_err()
2324                        || self
2325                            .resolution_checkpoint
2326                            .as_ref()
2327                            .is_none_or(|checkpoint| {
2328                                let already_checkpointed =
2329                                    checkpoint.included.contains(&entry.coord())
2330                                        || checkpoint.raw_heads.contains(&entry.coord());
2331                                !already_checkpointed
2332                                    && (entry.dependencies != checkpoint.effective_frontier
2333                                        || entry.resolution_dependencies != checkpoint.resolutions)
2334                            })
2335                    {
2336                        return Err(MembershipError::InvalidResolutionActivation(index));
2337                    }
2338                    continue;
2339                }
2340                MembershipChange::ProviderAdmin => {
2341                    let Some(super::provider::ProviderAdminMembershipChange::MergeConcurrent {
2342                        owner_barriers,
2343                        ..
2344                    }) = &entry.provider_admin
2345                    else {
2346                        return Err(MembershipError::InvalidProviderAdminChange(index));
2347                    };
2348                    if !entry.resolution_dependencies.is_empty()
2349                        || owner_barriers.values().any(|barrier| {
2350                            !barrier
2351                                .observed_streams
2352                                .windows(2)
2353                                .all(|pair| pair[0].stream_key() < pair[1].stream_key())
2354                        })
2355                    {
2356                        return Err(MembershipError::InvalidProviderAdminChange(index));
2357                    }
2358                    continue;
2359                }
2360                MembershipChange::Founder { .. } => continue,
2361            };
2362            if entry.provider_admin.is_some() {
2363                return Err(MembershipError::InvalidProviderAdminChange(index));
2364            }
2365            if let Some((grant, _)) = barriers.iter().find(|(_, barrier)| {
2366                !barrier
2367                    .observed_streams
2368                    .windows(2)
2369                    .all(|pair| pair[0].stream_key() < pair[1].stream_key())
2370            }) {
2371                return Err(MembershipError::InvalidOwnerRevocationBarrier {
2372                    index,
2373                    grant: grant.clone(),
2374                });
2375            }
2376        }
2377
2378        let founders = self
2379            .entries
2380            .iter()
2381            .filter_map(|entry| {
2382                let MembershipChange::Founder {
2383                    owner_pubkey,
2384                    owner_grant_id,
2385                    ..
2386                } = &entry.change
2387                else {
2388                    return None;
2389                };
2390                Some((entry, owner_pubkey, owner_grant_id))
2391            })
2392            .collect::<Vec<_>>();
2393        let [(founder, owner_pubkey, owner_grant_id)] = founders.as_slice() else {
2394            return Err(MembershipError::InvalidFounder);
2395        };
2396        if founder.author_pubkey != **owner_pubkey
2397            || founder.author_owner_grant != **owner_grant_id
2398            || founder.stream_id != derive_founder_stream_id(&founder.store_id, owner_pubkey)
2399            || founder.provider_admin.is_some()
2400        {
2401            return Err(MembershipError::InvalidFounder);
2402        }
2403
2404        validate_provider_admin_controls(&self.entries, self.resolution_checkpoint.as_ref())?;
2405
2406        let reduced = match &self.resolution_checkpoint {
2407            Some(checkpoint) => reduce_store_membership_from_checkpoint(&self.entries, checkpoint)?,
2408            None => reduce_store_membership(&self.entries)?,
2409        };
2410        let checkpoint_grants = self
2411            .resolution_checkpoint
2412            .as_ref()
2413            .map(|checkpoint| &checkpoint.grants);
2414        let provider_admin_seed = self
2415            .resolution_checkpoint
2416            .as_ref()
2417            .map_or(&self.provider_admin_genesis, |checkpoint| {
2418                &checkpoint.provider_admin
2419            });
2420        let (state_source, status) = match reduced {
2421            CausalGrantStatus::Resolved(reduced) => {
2422                let provider_admin = super::provider::ProviderAdminState::reduce_merge(
2423                    provider_admin_seed,
2424                    &self.entries,
2425                    &reduced.included,
2426                )?;
2427                let resolved =
2428                    resolved_store_membership(&reduced, checkpoint_grants, provider_admin);
2429                (Some(reduced), MembershipStatus::Resolved(resolved))
2430            }
2431            CausalGrantStatus::Conflict(CausalGrantConflict::ConcurrentMemberAssignments {
2432                raw_heads,
2433                effective_frontier,
2434                member_pubkey,
2435                conflicting_grants,
2436                uncontested_grants,
2437                reduced,
2438            }) => {
2439                let heads = self.exact_head_refs(&raw_heads)?;
2440                let conflict = MembershipConflict::ConcurrentMemberAssignments {
2441                    conflict_hash: membership_assignment_conflict_hash(
2442                        &heads,
2443                        &member_pubkey,
2444                        &conflicting_grants,
2445                    ),
2446                    heads,
2447                    effective_frontier,
2448                    member_pubkey,
2449                    conflicting_grants: map_store_grants(conflicting_grants, checkpoint_grants),
2450                    uncontested_grants: map_store_grants(uncontested_grants, checkpoint_grants),
2451                };
2452                (Some(reduced), MembershipStatus::Conflict(conflict))
2453            }
2454            CausalGrantStatus::Conflict(CausalGrantConflict::RevocationCycle {
2455                raw_heads,
2456                cyclic_sources,
2457                involved_owner_grants,
2458                maximal_valid_branches,
2459            }) => {
2460                let heads = self.exact_head_refs(&raw_heads)?;
2461                let branches = maximal_valid_branches
2462                    .into_iter()
2463                    .map(|branch| -> Result<StoreMembershipBranch, MembershipError> {
2464                        let resolved = resolved_store_membership(
2465                            &branch.reduced,
2466                            checkpoint_grants,
2467                            super::provider::ProviderAdminState::reduce_merge(
2468                                provider_admin_seed,
2469                                &self.entries,
2470                                &branch.reduced.included,
2471                            )?,
2472                        );
2473                        Ok(StoreMembershipBranch {
2474                            heads: self.branch_head_refs(&branch.raw_heads)?,
2475                            effective_frontier: branch.effective_frontier,
2476                            active_grants: resolved.active_grants,
2477                            provider_admin: resolved.provider_admin,
2478                            state_hash: resolved.state_hash,
2479                        })
2480                    })
2481                    .collect::<Result<Vec<_>, _>>()?;
2482                let conflict_hash = membership_revocation_conflict_hash(
2483                    &heads,
2484                    &cyclic_sources,
2485                    &involved_owner_grants,
2486                );
2487                (
2488                    None,
2489                    MembershipStatus::Conflict(MembershipConflict::RevocationCycle {
2490                        conflict_hash,
2491                        heads,
2492                        cyclic_sources,
2493                        involved_owner_grants,
2494                        maximal_valid_branches: branches,
2495                    }),
2496                )
2497            }
2498        };
2499        if let Some(reduced) = state_source {
2500            self.state = CausalState {
2501                grants: reduced
2502                    .grants
2503                    .into_iter()
2504                    .map(|(grant, record)| {
2505                        let creation_authority = membership_creation_authority(
2506                            &grant,
2507                            record.creation,
2508                            checkpoint_grants,
2509                        );
2510                        (
2511                            grant,
2512                            GrantRecord {
2513                                pubkey: record.member_pubkey,
2514                                role: record.assignment.role,
2515                                provider_account_email: record.assignment.provider_account_email,
2516                                creation_authority,
2517                            },
2518                        )
2519                    })
2520                    .collect(),
2521                removed: reduced.removed,
2522            };
2523            self.included = reduced.included;
2524        } else {
2525            self.state = CausalState::default();
2526            self.included.clear();
2527        }
2528        self.status = Some(status);
2529        Ok(())
2530    }
2531
2532    pub fn apply_resolutions(
2533        &mut self,
2534        store_root_hash: ObjectHash,
2535        resolutions: &[(
2536            StoreMembershipConflictResolutionRef,
2537            StoreMembershipConflictResolution,
2538        )],
2539    ) -> Result<(), MembershipError> {
2540        let (raw_heads, effective_frontier) = match self.conflict() {
2541            Some(MembershipConflict::RevocationCycle {
2542                heads,
2543                maximal_valid_branches,
2544                ..
2545            }) => {
2546                let selected = resolutions
2547                    .iter()
2548                    .map(|(_, resolution)| {
2549                        maximal_valid_branches
2550                            .iter()
2551                            .find(|branch| branch.heads == resolution.resolver_branch_heads)
2552                            .map(|branch| branch.effective_frontier.as_slice())
2553                            .ok_or(MembershipError::InvalidConflictResolution)
2554                    })
2555                    .collect::<Result<Vec<_>, _>>()?;
2556                (
2557                    heads
2558                        .iter()
2559                        .map(|reference| reference.coord.clone())
2560                        .collect(),
2561                    causal_grants::common_frontier(&selected),
2562                )
2563            }
2564            _ => return Err(MembershipError::InvalidConflictResolution),
2565        };
2566        let resolved = self.resolved_with(store_root_hash, resolutions)?;
2567        let mut grants = self
2568            .resolution_checkpoint
2569            .as_ref()
2570            .map_or_else(BTreeMap::new, |checkpoint| checkpoint.grants.clone());
2571        let mut grant_anchors = self
2572            .resolution_checkpoint
2573            .as_ref()
2574            .map_or_else(BTreeMap::new, |checkpoint| checkpoint.grant_anchors.clone());
2575        for entry in &self.entries {
2576            let (grant, record) = match &entry.change {
2577                MembershipChange::Founder {
2578                    owner_pubkey,
2579                    owner_grant_id,
2580                    ..
2581                } => (
2582                    owner_grant_id.clone(),
2583                    MembershipGrantRecord {
2584                        member_pubkey: owner_pubkey.clone(),
2585                        role: MemberRole::Owner,
2586                        provider_account_email: None,
2587                        creation_authority: MembershipGrantCreationAuthority::Entry(entry.coord()),
2588                    },
2589                ),
2590                MembershipChange::SetMember {
2591                    user_pubkey,
2592                    provider_account_email,
2593                    role,
2594                    grant_id,
2595                    ..
2596                } => (
2597                    grant_id.clone(),
2598                    MembershipGrantRecord {
2599                        member_pubkey: user_pubkey.clone(),
2600                        role: role.clone(),
2601                        provider_account_email: provider_account_email.clone(),
2602                        creation_authority: MembershipGrantCreationAuthority::Entry(entry.coord()),
2603                    },
2604                ),
2605                MembershipChange::RemoveMember { .. }
2606                | MembershipChange::ProviderAdmin
2607                | MembershipChange::ResolutionActivation { .. } => continue,
2608            };
2609            grants.insert(grant, record);
2610            match &entry.change {
2611                MembershipChange::Founder {
2612                    owner_grant_id,
2613                    membership,
2614                    ..
2615                } => {
2616                    grant_anchors.insert(owner_grant_id.clone(), membership.clone());
2617                }
2618                MembershipChange::SetMember {
2619                    grant_id,
2620                    membership: Some(membership),
2621                    ..
2622                } => {
2623                    grant_anchors.insert(grant_id.clone(), membership.clone());
2624                }
2625                _ => {}
2626            }
2627        }
2628        grants.extend(resolved.active_grants.clone());
2629        for (_, resolution) in resolutions {
2630            grant_anchors.insert(
2631                resolution.replacement_grant.clone(),
2632                resolution.replacement_membership.clone(),
2633            );
2634        }
2635        let removed: BTreeSet<_> = grants
2636            .keys()
2637            .filter(|grant| !resolved.active_grants.contains_key(*grant))
2638            .cloned()
2639            .collect();
2640        let included = membership_history_closure(&self.entries, &effective_frontier);
2641        let mut resolution_refs = self
2642            .resolution_checkpoint
2643            .as_ref()
2644            .map_or_else(Vec::new, |checkpoint| checkpoint.resolutions.clone());
2645        resolution_refs.extend(resolutions.iter().map(|(reference, _)| reference.clone()));
2646        resolution_refs.sort();
2647        resolution_refs.dedup();
2648        self.resolution_checkpoint = Some(MembershipResolutionCheckpoint {
2649            raw_heads,
2650            effective_frontier: effective_frontier.clone(),
2651            grants: grants.clone(),
2652            grant_anchors,
2653            removed: removed.clone(),
2654            included: included.clone(),
2655            resolutions: resolution_refs,
2656            provider_admin: resolved.provider_admin.combined_state().clone(),
2657        });
2658        self.state = CausalState {
2659            grants: grants
2660                .iter()
2661                .map(|(grant, record)| {
2662                    (
2663                        grant.clone(),
2664                        GrantRecord {
2665                            pubkey: record.member_pubkey.clone(),
2666                            role: record.role.clone(),
2667                            provider_account_email: record.provider_account_email.clone(),
2668                            creation_authority: record.creation_authority.clone(),
2669                        },
2670                    )
2671                })
2672                .collect(),
2673            removed,
2674        };
2675        self.included = included;
2676        self.status = Some(MembershipStatus::Resolved(resolved));
2677        Ok(())
2678    }
2679
2680    fn exact_head_refs(
2681        &self,
2682        raw_heads: &[MembershipCoord],
2683    ) -> Result<Vec<MembershipHeadRef>, MembershipError> {
2684        let expected = raw_heads.iter().cloned().collect::<BTreeSet<_>>();
2685        let mut references = self
2686            .head_refs
2687            .iter()
2688            .filter(|reference| expected.contains(&reference.coord))
2689            .cloned()
2690            .collect::<Vec<_>>();
2691        let actual = references
2692            .iter()
2693            .map(|reference| reference.coord.clone())
2694            .collect::<BTreeSet<_>>();
2695        if expected != actual || references.len() != expected.len() {
2696            return Err(MembershipError::MissingConflictHeads);
2697        }
2698        references.sort();
2699        Ok(references)
2700    }
2701
2702    fn branch_head_refs(
2703        &self,
2704        branch_heads: &[MembershipCoord],
2705    ) -> Result<Vec<MembershipHeadRef>, MembershipError> {
2706        let by_coord = self
2707            .head_refs
2708            .iter()
2709            .map(|reference| (reference.coord.clone(), reference.clone()))
2710            .collect::<BTreeMap<_, _>>();
2711        let mut references = branch_heads
2712            .iter()
2713            .map(|coord| {
2714                by_coord
2715                    .get(coord)
2716                    .cloned()
2717                    .ok_or(MembershipError::MissingConflictHeads)
2718            })
2719            .collect::<Result<Vec<_>, _>>()?;
2720        references.sort();
2721        Ok(references)
2722    }
2723}
2724
2725fn reduce_store_membership(
2726    entries: &[MembershipEntry],
2727) -> Result<CausalGrantStatus<MembershipCoord, StoreAssignment>, MembershipError> {
2728    let normalized = normalize_store_membership(entries);
2729    causal_grants::reduce(&normalized).map_err(map_store_causal_error)
2730}
2731
2732fn reduce_store_membership_from_checkpoint(
2733    entries: &[MembershipEntry],
2734    checkpoint: &MembershipResolutionCheckpoint,
2735) -> Result<CausalGrantStatus<MembershipCoord, StoreAssignment>, MembershipError> {
2736    let checkpoint_by_stream = checkpoint
2737        .raw_heads
2738        .iter()
2739        .map(|coord| (coord.stream_key(), coord))
2740        .collect::<BTreeMap<_, _>>();
2741    let suffix = entries
2742        .iter()
2743        .filter(|entry| {
2744            checkpoint_by_stream
2745                .get(&entry.coord().stream_key())
2746                .is_none_or(|head| entry.seq > head.seq)
2747        })
2748        .cloned()
2749        .collect::<Vec<_>>();
2750    let normalized = normalize_store_membership(&suffix);
2751    let seeds = checkpoint
2752        .grants
2753        .iter()
2754        .map(|(grant, record)| {
2755            (
2756                grant.clone(),
2757                causal_grants::CausalSeedGrant {
2758                    member_pubkey: record.member_pubkey.clone(),
2759                    assignment: StoreAssignment {
2760                        role: record.role.clone(),
2761                        provider_account_email: record.provider_account_email.clone(),
2762                    },
2763                },
2764            )
2765        })
2766        .collect();
2767    causal_grants::reduce_from_checkpoint(
2768        &normalized,
2769        &checkpoint.raw_heads,
2770        &checkpoint.effective_frontier,
2771        &seeds,
2772        &checkpoint.removed,
2773        &checkpoint.included,
2774    )
2775    .map_err(map_store_causal_error)
2776}
2777
2778fn validate_provider_admin_controls(
2779    entries: &[MembershipEntry],
2780    checkpoint: Option<&MembershipResolutionCheckpoint>,
2781) -> Result<(), MembershipError> {
2782    for (index, entry) in entries.iter().enumerate() {
2783        let Some(super::provider::ProviderAdminMembershipChange::MergeConcurrent {
2784            owner_barriers,
2785            ..
2786        }) = &entry.provider_admin
2787        else {
2788            continue;
2789        };
2790        let included = membership_history_closure(entries, &entry.dependencies);
2791        let causal_past = entries
2792            .iter()
2793            .filter(|candidate| included.contains(&candidate.coord()))
2794            .cloned()
2795            .collect::<Vec<_>>();
2796        let reduced = match checkpoint {
2797            Some(checkpoint) => reduce_store_membership_from_checkpoint(&causal_past, checkpoint)?,
2798            None => reduce_store_membership(&causal_past)?,
2799        };
2800        let CausalGrantStatus::Resolved(reduced) = reduced else {
2801            return Err(MembershipError::InvalidProviderAdminChange(index));
2802        };
2803        let expected = reduced
2804            .grants
2805            .iter()
2806            .filter(|(grant_id, record)| {
2807                !reduced.removed.contains(*grant_id) && record.assignment.is_owner()
2808            })
2809            .map(|(grant_id, _)| {
2810                let observed_streams = entry
2811                    .dependencies
2812                    .iter()
2813                    .filter(|coord| coord.author_owner_grant == *grant_id)
2814                    .cloned()
2815                    .collect();
2816                (grant_id.clone(), OwnerStreamBarrier { observed_streams })
2817            })
2818            .collect::<BTreeMap<_, _>>();
2819        if *owner_barriers != expected {
2820            return Err(MembershipError::InvalidProviderAdminChange(index));
2821        }
2822    }
2823    Ok(())
2824}
2825
2826fn membership_history_closure(
2827    entries: &[MembershipEntry],
2828    frontier: &[MembershipCoord],
2829) -> BTreeSet<MembershipCoord> {
2830    let by_coord = entries
2831        .iter()
2832        .map(|entry| (entry.coord(), entry))
2833        .collect::<BTreeMap<_, _>>();
2834    let mut pending = frontier.iter().cloned().collect::<BTreeSet<_>>();
2835    let mut included = BTreeSet::new();
2836    while let Some(coord) = pending.pop_first() {
2837        if !included.insert(coord.clone()) {
2838            continue;
2839        }
2840        if let Some(entry) = by_coord.get(&coord) {
2841            pending.extend(entry.dependencies.iter().cloned());
2842        }
2843    }
2844    included
2845}
2846
2847fn normalize_store_membership(
2848    entries: &[MembershipEntry],
2849) -> Vec<CausalEntry<MembershipCoord, StoreAssignment>> {
2850    entries
2851        .iter()
2852        .map(|entry| {
2853            let dependencies = entry
2854                .dependencies
2855                .iter()
2856                .cloned()
2857                .map(|coord| (coord.stream_key(), coord))
2858                .collect();
2859            let change = match &entry.change {
2860                MembershipChange::Founder {
2861                    owner_pubkey,
2862                    owner_grant_id,
2863                    ..
2864                } => CausalChange::Founder {
2865                    member_pubkey: owner_pubkey.clone(),
2866                    grant_id: owner_grant_id.clone(),
2867                    assignment: StoreAssignment {
2868                        role: MemberRole::Owner,
2869                        provider_account_email: None,
2870                    },
2871                },
2872                MembershipChange::SetMember {
2873                    user_pubkey,
2874                    provider_account_email,
2875                    role,
2876                    grant_id,
2877                    membership: _,
2878                    replaces,
2879                    owner_barriers,
2880                } => CausalChange::SetMember {
2881                    member_pubkey: user_pubkey.clone(),
2882                    assignment: StoreAssignment {
2883                        role: role.clone(),
2884                        provider_account_email: provider_account_email.clone(),
2885                    },
2886                    grant_id: grant_id.clone(),
2887                    replaces: replaces.clone(),
2888                    owner_barriers: owner_barriers
2889                        .iter()
2890                        .map(|(grant, barrier)| (grant.clone(), shared_store_barrier(barrier)))
2891                        .collect(),
2892                },
2893                MembershipChange::RemoveMember {
2894                    user_pubkey,
2895                    removes,
2896                    owner_barriers,
2897                } => CausalChange::RemoveMember {
2898                    member_pubkey: user_pubkey.clone(),
2899                    removes: removes.clone(),
2900                    owner_barriers: owner_barriers
2901                        .iter()
2902                        .map(|(grant, barrier)| (grant.clone(), shared_store_barrier(barrier)))
2903                        .collect(),
2904                },
2905                MembershipChange::ProviderAdmin => CausalChange::Control,
2906                MembershipChange::ResolutionActivation { .. } => CausalChange::ResolutionActivation,
2907            };
2908            CausalEntry {
2909                coord: entry.coord(),
2910                previous_hash: entry.previous_hash,
2911                dependencies,
2912                change,
2913            }
2914        })
2915        .collect()
2916}
2917
2918fn map_store_grants(
2919    grants: BTreeMap<
2920        MembershipGrantId,
2921        causal_grants::GrantRecord<MembershipCoord, StoreAssignment>,
2922    >,
2923    checkpoint: Option<&BTreeMap<MembershipGrantId, MembershipGrantRecord>>,
2924) -> BTreeMap<MembershipGrantId, MembershipGrantRecord> {
2925    grants
2926        .into_iter()
2927        .map(|(grant, record)| {
2928            let creation_authority =
2929                membership_creation_authority(&grant, record.creation, checkpoint);
2930            (
2931                grant,
2932                MembershipGrantRecord {
2933                    member_pubkey: record.member_pubkey,
2934                    role: record.assignment.role,
2935                    provider_account_email: record.assignment.provider_account_email,
2936                    creation_authority,
2937                },
2938            )
2939        })
2940        .collect()
2941}
2942
2943fn resolved_store_membership(
2944    reduced: &causal_grants::ReducedGrants<MembershipCoord, StoreAssignment>,
2945    checkpoint: Option<&BTreeMap<MembershipGrantId, MembershipGrantRecord>>,
2946    provider_admin: super::provider::ProviderAdminResolution,
2947) -> ResolvedStoreMembership {
2948    let active_grants = reduced
2949        .grants
2950        .iter()
2951        .filter(|(grant, _)| !reduced.removed.contains(*grant))
2952        .map(|(grant, record)| {
2953            (
2954                grant.clone(),
2955                MembershipGrantRecord {
2956                    member_pubkey: record.member_pubkey.clone(),
2957                    role: record.assignment.role.clone(),
2958                    provider_account_email: record.assignment.provider_account_email.clone(),
2959                    creation_authority: membership_creation_authority(
2960                        grant,
2961                        record.creation.clone(),
2962                        checkpoint,
2963                    ),
2964                },
2965            )
2966        })
2967        .collect::<BTreeMap<_, _>>();
2968    let state_hash = store_membership_state_hash(&active_grants, &provider_admin);
2969    ResolvedStoreMembership {
2970        active_grants,
2971        provider_admin,
2972        state_hash,
2973    }
2974}
2975
2976fn membership_creation_authority(
2977    grant: &MembershipGrantId,
2978    creation: causal_grants::CausalGrantCreation<MembershipCoord>,
2979    checkpoint: Option<&BTreeMap<MembershipGrantId, MembershipGrantRecord>>,
2980) -> MembershipGrantCreationAuthority {
2981    match creation {
2982        causal_grants::CausalGrantCreation::Entry(coord) => {
2983            MembershipGrantCreationAuthority::Entry(coord)
2984        }
2985        causal_grants::CausalGrantCreation::Checkpoint => checkpoint
2986            .and_then(|grants| grants.get(grant))
2987            .expect("checkpoint reducer seed has exact domain grant record")
2988            .creation_authority
2989            .clone(),
2990    }
2991}
2992
2993fn store_membership_state_hash(
2994    active_grants: &BTreeMap<MembershipGrantId, MembershipGrantRecord>,
2995    provider_admin: &super::provider::ProviderAdminResolution,
2996) -> ObjectHash {
2997    #[derive(Serialize)]
2998    struct State<'a> {
2999        domain: &'static str,
3000        active_grants: &'a BTreeMap<MembershipGrantId, MembershipGrantRecord>,
3001        provider_admin: &'a super::provider::ProviderAdminResolution,
3002    }
3003    ObjectHash::digest(
3004        &serde_json::to_vec(&State {
3005            domain: "coven.store-membership-state.v1",
3006            active_grants,
3007            provider_admin,
3008        })
3009        .expect("Store membership state serialization cannot fail"),
3010    )
3011}
3012
3013fn membership_assignment_conflict_hash(
3014    heads: &[MembershipHeadRef],
3015    member_pubkey: &str,
3016    conflicting_grants: &BTreeMap<
3017        MembershipGrantId,
3018        causal_grants::GrantRecord<MembershipCoord, StoreAssignment>,
3019    >,
3020) -> ObjectHash {
3021    #[derive(Serialize)]
3022    struct Conflict<'a> {
3023        domain: &'static str,
3024        heads: &'a [MembershipHeadRef],
3025        member_pubkey: &'a str,
3026        conflicting_grant_ids: Vec<&'a MembershipGrantId>,
3027    }
3028    ObjectHash::digest(
3029        &serde_json::to_vec(&Conflict {
3030            domain: "coven.store-membership-assignment-conflict.v1",
3031            heads,
3032            member_pubkey,
3033            conflicting_grant_ids: conflicting_grants.keys().collect(),
3034        })
3035        .expect("Store membership conflict serialization cannot fail"),
3036    )
3037}
3038
3039fn membership_revocation_conflict_hash(
3040    heads: &[MembershipHeadRef],
3041    cyclic_sources: &[MembershipCoord],
3042    involved_owner_grants: &BTreeSet<MembershipGrantId>,
3043) -> ObjectHash {
3044    #[derive(Serialize)]
3045    struct Conflict<'a> {
3046        domain: &'static str,
3047        heads: &'a [MembershipHeadRef],
3048        cyclic_sources: &'a [MembershipCoord],
3049        involved_owner_grants: &'a BTreeSet<MembershipGrantId>,
3050    }
3051    ObjectHash::digest(
3052        &serde_json::to_vec(&Conflict {
3053            domain: "coven.store-membership-revocation-conflict.v1",
3054            heads,
3055            cyclic_sources,
3056            involved_owner_grants,
3057        })
3058        .expect("Store membership revocation conflict serialization cannot fail"),
3059    )
3060}
3061
3062fn shared_store_barrier(barrier: &OwnerStreamBarrier) -> OwnerGrantBarrier<MembershipCoord> {
3063    let observed_streams = barrier
3064        .observed_streams
3065        .iter()
3066        .cloned()
3067        .map(|coord| (coord.stream_key(), coord))
3068        .collect();
3069    OwnerGrantBarrier { observed_streams }
3070}
3071
3072fn map_store_causal_error(error: CausalGrantError<MembershipCoord>) -> MembershipError {
3073    match error {
3074        CausalGrantError::Empty => MembershipError::EmptyChain,
3075        CausalGrantError::ConflictingSequence { stream, seq } => {
3076            MembershipError::ConflictingSequence {
3077                author: stream.author_pubkey,
3078                grant: stream.author_owner_grant,
3079                seq,
3080            }
3081        }
3082        CausalGrantError::MissingSequence { stream, seq } => MembershipError::MissingSequence {
3083            author: stream.author_pubkey,
3084            grant: stream.author_owner_grant,
3085            seq,
3086        },
3087        CausalGrantError::BrokenStreamLink {
3088            index,
3089            expected,
3090            actual,
3091        } => MembershipError::BrokenStreamLink {
3092            index,
3093            expected,
3094            actual,
3095        },
3096        CausalGrantError::MissingOwnDependency { index } => {
3097            MembershipError::MissingOwnDependency { index }
3098        }
3099        CausalGrantError::DependencyStreamMismatch { .. } => {
3100            unreachable!("Store dependencies are normalized from their signed coordinates")
3101        }
3102        CausalGrantError::MissingDependency { index, dependency } => {
3103            MembershipError::MissingDependency {
3104                index,
3105                dependency: Box::new(dependency),
3106            }
3107        }
3108        CausalGrantError::DependencyCycle => MembershipError::DependencyCycle,
3109        CausalGrantError::InvalidFounder => MembershipError::InvalidFounder,
3110        CausalGrantError::AuthorGrantInactive { index, grant } => {
3111            MembershipError::AuthorGrantInactive { index, grant }
3112        }
3113        CausalGrantError::DuplicateGrant { index, grant } => {
3114            MembershipError::DuplicateGrant { index, grant }
3115        }
3116        CausalGrantError::GrantOwnerMismatch { index, grant } => {
3117            MembershipError::GrantOwnerMismatch { index, grant }
3118        }
3119        CausalGrantError::GrantSetMismatch {
3120            index,
3121            member_pubkey,
3122        } => MembershipError::GrantSetMismatch {
3123            index,
3124            pubkey: member_pubkey,
3125        },
3126        CausalGrantError::EmptyRemoval { index } => MembershipError::EmptyRemoval { index },
3127        CausalGrantError::MissingOwnerRevocationBarrier { index, grant } => {
3128            MembershipError::MissingOwnerRevocationBarrier { index, grant }
3129        }
3130        CausalGrantError::InvalidOwnerRevocationBarrier { index, grant } => {
3131            MembershipError::InvalidOwnerRevocationBarrier { index, grant }
3132        }
3133        CausalGrantError::NoActiveOwner => MembershipError::NoActiveOwner,
3134        CausalGrantError::RevocationCycleTooWide { sources, maximum } => {
3135            MembershipError::RevocationCycleTooWide { sources, maximum }
3136        }
3137    }
3138}
3139
3140pub fn derive_founder_grant_id(store_id: &str, owner_pubkey: &str) -> MembershipGrantId {
3141    MembershipGrantId(ObjectHash::digest(
3142        format!("coven.membership-founder-grant.v1\0{store_id}\0{owner_pubkey}").as_bytes(),
3143    ))
3144}
3145
3146pub(crate) fn derive_founder_stream_id(store_id: &str, owner_pubkey: &str) -> AuthorStreamId {
3147    AuthorStreamId::from_digest(ObjectHash::digest(
3148        format!("coven.membership-founder-stream.v1\0{store_id}\0{owner_pubkey}").as_bytes(),
3149    ))
3150}
3151
3152fn store_membership_anchor_stream(
3153    owner_pubkey: &str,
3154    owner_grant: &MembershipGrantId,
3155    anchor: &GrantStreamAnchor,
3156) -> Option<AuthorStreamId> {
3157    let GrantStreamAnchor::StoreMembership { first_slot } = anchor else {
3158        return None;
3159    };
3160    let prefix = format!(
3161        "{}{owner_pubkey}/{owner_grant}/",
3162        super::store_commit::STORE_MEMBERSHIP_HEAD_PREFIX,
3163    );
3164    first_slot
3165        .logical_key()
3166        .strip_prefix(&prefix)?
3167        .strip_suffix("/1.json")?
3168        .parse()
3169        .ok()
3170}
3171
3172pub fn derive_grant_id(
3173    store_id: &str,
3174    author_pubkey: &str,
3175    author_grant: &MembershipGrantId,
3176    stream_id: AuthorStreamId,
3177    seq: u64,
3178    user_pubkey: &str,
3179) -> MembershipGrantId {
3180    MembershipGrantId(ObjectHash::digest(
3181        format!(
3182            "coven.membership-grant.v1\0{store_id}\0{author_pubkey}\0{author_grant}\0{stream_id}\0{seq}\0{user_pubkey}"
3183        )
3184        .as_bytes(),
3185    ))
3186}
3187
3188pub fn founder_entry(
3189    store_id: &str,
3190    owner: &UserKeypair,
3191    owner_grant_id: MembershipGrantId,
3192    created_at: &str,
3193    membership: GrantStreamAnchor,
3194    provider_admin: super::provider::FounderProviderAdminGrant,
3195) -> MembershipEntry {
3196    let owner_pubkey = keys::public_key_hex(owner);
3197    let stream_id = derive_founder_stream_id(store_id, &owner_pubkey);
3198    let mut entry = MembershipEntry {
3199        version: STORE_PROTOCOL_VERSION,
3200        store_id: store_id.to_string(),
3201        author_pubkey: owner_pubkey.clone(),
3202        author_owner_grant: owner_grant_id.clone(),
3203        stream_id,
3204        seq: 1,
3205        previous_hash: None,
3206        dependencies: Vec::new(),
3207        resolution_dependencies: Vec::new(),
3208        created_at: created_at.to_string(),
3209        change: MembershipChange::Founder {
3210            owner_pubkey,
3211            owner_grant_id,
3212            membership,
3213            provider_admin,
3214        },
3215        provider_admin: None,
3216        signature: String::new(),
3217    };
3218    sign_membership_entry(&mut entry, owner);
3219    entry
3220}
3221
3222pub fn canonical_bytes(entry: &MembershipEntry) -> Vec<u8> {
3223    #[derive(Serialize)]
3224    struct Signed<'a> {
3225        version: u32,
3226        store_id: &'a str,
3227        author_pubkey: &'a str,
3228        author_owner_grant: &'a MembershipGrantId,
3229        stream_id: AuthorStreamId,
3230        seq: u64,
3231        #[serde(skip_serializing_if = "Option::is_none")]
3232        previous_hash: Option<ObjectHash>,
3233        dependencies: &'a [MembershipCoord],
3234        resolution_dependencies: &'a [StoreMembershipConflictResolutionRef],
3235        created_at: &'a str,
3236        change: &'a MembershipChange,
3237        #[serde(skip_serializing_if = "Option::is_none")]
3238        provider_admin: Option<&'a super::provider::ProviderAdminMembershipChange>,
3239    }
3240    serde_json::to_vec(&Signed {
3241        version: entry.version,
3242        store_id: &entry.store_id,
3243        author_pubkey: &entry.author_pubkey,
3244        author_owner_grant: &entry.author_owner_grant,
3245        stream_id: entry.stream_id,
3246        seq: entry.seq,
3247        previous_hash: entry.previous_hash,
3248        dependencies: &entry.dependencies,
3249        resolution_dependencies: &entry.resolution_dependencies,
3250        created_at: &entry.created_at,
3251        change: &entry.change,
3252        provider_admin: entry.provider_admin.as_ref(),
3253    })
3254    .expect("membership signed fields serialize")
3255}
3256
3257pub fn entry_hash(entry: &MembershipEntry) -> ObjectHash {
3258    ObjectHash::digest(
3259        &serde_json::to_vec(entry).expect("membership entry serialization cannot fail"),
3260    )
3261}
3262
3263pub fn sign_membership_entry(entry: &mut MembershipEntry, keypair: &UserKeypair) {
3264    entry.author_pubkey = keys::public_key_hex(keypair);
3265    let (_, signature) = keys::sign_hex(keypair, &canonical_bytes(entry));
3266    entry.signature = signature;
3267}
3268
3269pub fn verify_membership_entry(entry: &MembershipEntry) -> bool {
3270    let activation_position_is_valid = match &entry.change {
3271        MembershipChange::ResolutionActivation { .. } => {
3272            entry.seq == 1
3273                && entry.previous_hash.is_none()
3274                && entry
3275                    .dependencies
3276                    .iter()
3277                    .all(|dependency| dependency.stream_key() != entry.coord().stream_key())
3278        }
3279        _ => true,
3280    };
3281    activation_position_is_valid
3282        && entry
3283            .resolution_dependencies
3284            .windows(2)
3285            .all(|pair| pair[0] < pair[1])
3286        && keys::verify_signature_hex(
3287            &entry.author_pubkey,
3288            &entry.signature,
3289            &canonical_bytes(entry),
3290        )
3291}
3292
3293impl AuthorHead {
3294    #[allow(clippy::too_many_arguments)]
3295    pub fn signed(
3296        store_id: String,
3297        author_registration: StoreDeviceRegistrationRef,
3298        entry: MembershipEntryRef,
3299        predecessor: Option<MembershipHeadRef>,
3300        mut resolutions: Vec<StoreMembershipConflictResolutionRef>,
3301        successor: SuccessorLink,
3302        device_signer: &UserKeypair,
3303    ) -> Self {
3304        resolutions.sort();
3305        resolutions.dedup();
3306        let mut head = Self {
3307            version: STORE_PROTOCOL_VERSION,
3308            store_id,
3309            author_registration,
3310            entry,
3311            predecessor,
3312            resolutions,
3313            successor,
3314            signature: String::new(),
3315        };
3316        let (_, signature) = keys::sign_hex(device_signer, &head.canonical_bytes());
3317        head.signature = signature;
3318        head
3319    }
3320
3321    pub fn verify(&self, registration: &StoreDeviceRegistration) -> bool {
3322        self.version == STORE_PROTOCOL_VERSION
3323            && self.resolutions.windows(2).all(|pair| pair[0] < pair[1])
3324            && self
3325                .author_registration
3326                .verify_registration(registration)
3327                .is_ok()
3328            && registration.author_pubkey == self.entry.coord.author_pubkey
3329            && self.successor.predecessor
3330                == self
3331                    .predecessor
3332                    .as_ref()
3333                    .map(|reference| reference.object.clone())
3334            && keys::verify_signature_hex(
3335                &registration.device_signing_pubkey,
3336                &self.signature,
3337                &self.canonical_bytes(),
3338            )
3339    }
3340
3341    pub fn entry_coord(&self) -> MembershipCoord {
3342        self.entry.coord.clone()
3343    }
3344
3345    pub fn head_hash(&self) -> ObjectHash {
3346        ObjectHash::digest(
3347            &serde_json::to_vec(self).expect("membership head serialization cannot fail"),
3348        )
3349    }
3350
3351    fn canonical_bytes(&self) -> Vec<u8> {
3352        #[derive(Serialize)]
3353        struct Signed<'a> {
3354            version: u32,
3355            store_id: &'a str,
3356            author_registration: &'a StoreDeviceRegistrationRef,
3357            entry: &'a MembershipEntryRef,
3358            #[serde(skip_serializing_if = "Option::is_none")]
3359            predecessor: Option<&'a MembershipHeadRef>,
3360            resolutions: &'a [StoreMembershipConflictResolutionRef],
3361            successor: &'a SuccessorLink,
3362        }
3363        serde_json::to_vec(&Signed {
3364            version: self.version,
3365            store_id: &self.store_id,
3366            author_registration: &self.author_registration,
3367            entry: &self.entry,
3368            predecessor: self.predecessor.as_ref(),
3369            resolutions: &self.resolutions,
3370            successor: &self.successor,
3371        })
3372        .expect("membership head signed fields serialize")
3373    }
3374}
3375
3376#[cfg(test)]
3377mod tests {
3378    use super::*;
3379    use crate::storage::cloud::ObjectSlot;
3380    use crate::sync::circle_control::StoreMembershipStateRef;
3381    use crate::sync::storage::{ProviderDeviceBinding, ProviderPrincipalId};
3382    use crate::sync::store_commit::{
3383        commit_semantic_prefix, device_self_retirement_semantic_prefix,
3384        membership_entry_semantic_prefix, membership_head_semantic_prefix,
3385        membership_resolution_semantic_prefix, registration_semantic_prefix, CandidateFamilyId,
3386        DeviceJoinAttemptId, DeviceStreamAnchor, GrantStreamAnchor, ResolvedStoreDeviceState,
3387        StoreBatchCommitRef, StoreCommitAnchor, StoreCommitCoord, StoreCommitOrder,
3388        StoreCreationId, StoreDeviceRegistrationOrigin, StoreDeviceRegistrationRef,
3389        StoreDeviceSelfRetirement, StoreDeviceSelfRetirementRef, StoreDeviceStateRef,
3390        StoreHistoryCut, StoreRootRef, StoreSerialPredecessor, StreamActivationId,
3391        SERIAL_STREAM_ID,
3392    };
3393
3394    fn key() -> UserKeypair {
3395        UserKeypair::generate()
3396    }
3397
3398    fn stream(byte: u8) -> AuthorStreamId {
3399        AuthorStreamId::from_bytes([byte; 32])
3400    }
3401
3402    fn slot(key: impl Into<String>) -> ObjectSlot {
3403        ObjectSlot::logical(key.into()).expect("valid test object slot")
3404    }
3405
3406    fn exact(key: impl Into<String>, bytes: &[u8]) -> ExactObjectRef {
3407        ExactObjectRef::new(slot(key), bytes.len() as u64, ObjectHash::digest(bytes))
3408    }
3409
3410    fn membership_anchor(store_id: &str) -> GrantStreamAnchor {
3411        GrantStreamAnchor::StoreMembership {
3412            first_slot: slot(format!("test/{store_id}/membership/1.json")),
3413        }
3414    }
3415
3416    fn recovery_anchor(store_id: &str) -> GrantStreamAnchor {
3417        GrantStreamAnchor::OwnerRecovery {
3418            first_slot: slot(format!("test/{store_id}/recovery/1.json")),
3419        }
3420    }
3421
3422    fn test_founder_entry(
3423        store_id: &str,
3424        owner: &UserKeypair,
3425        created_at: &str,
3426        membership: GrantStreamAnchor,
3427    ) -> MembershipEntry {
3428        founder_entry(
3429            store_id,
3430            owner,
3431            crate::sync::test_helpers::test_membership_grant_id(store_id),
3432            created_at,
3433            membership,
3434            crate::sync::test_helpers::test_founder_provider_admin(store_id),
3435        )
3436    }
3437
3438    fn test_root(store_id: &str) -> StoreRootRef {
3439        let bytes = store_id.as_bytes();
3440        StoreRootRef {
3441            store_root_id: ObjectHash::digest(format!("{store_id} identity").as_bytes()),
3442            store_root_hash: ObjectHash::digest(bytes),
3443            object: exact(format!("test/{store_id}/root.json"), bytes),
3444        }
3445    }
3446
3447    fn registration(
3448        root: &StoreRootRef,
3449        label: &str,
3450        signer: &UserKeypair,
3451    ) -> (StoreDeviceRegistration, StoreDeviceRegistrationRef) {
3452        let registration = StoreDeviceRegistration::signed(
3453            root.clone(),
3454            StoreDeviceRegistrationOrigin::Founder {
3455                creation_id: StoreCreationId::from_nonce(label),
3456            },
3457            ProviderDeviceBinding {
3458                principal: ProviderPrincipalId::CustomS3Credential {
3459                    access_key_id_hash: ObjectHash::digest(label.as_bytes()),
3460                },
3461            },
3462            StoreCommitAnchor::Serial,
3463            DeviceStreamAnchor::StoreAcknowledgements {
3464                first_slot: slot(format!("test/{label}/acks/1.json")),
3465            },
3466            DeviceStreamAnchor::StoreSnapshots {
3467                first_slot: slot(format!("test/{label}/snapshots/1.json")),
3468            },
3469            signer,
3470        )
3471        .expect("sign test registration");
3472        let bytes = registration.to_bytes();
3473        let reference = StoreDeviceRegistrationRef::from_registration(
3474            &registration,
3475            exact(
3476                format!(
3477                    "{}.json",
3478                    registration_semantic_prefix(&registration.device_id.to_string())
3479                ),
3480                &bytes,
3481            ),
3482        );
3483        (registration, reference)
3484    }
3485
3486    fn exact_head(
3487        entry: &MembershipEntry,
3488        signer: &UserKeypair,
3489    ) -> (MembershipHeadRef, AuthorHead) {
3490        exact_head_with_resolutions(entry, signer, entry.resolution_dependencies.clone())
3491    }
3492
3493    fn exact_head_with_resolutions(
3494        entry: &MembershipEntry,
3495        signer: &UserKeypair,
3496        resolutions: Vec<StoreMembershipConflictResolutionRef>,
3497    ) -> (MembershipHeadRef, AuthorHead) {
3498        let root = test_root(&entry.store_id);
3499        let (registration, registration_ref) = registration(
3500            &root,
3501            &format!("{}-{}", entry.store_id, entry.author_pubkey),
3502            signer,
3503        );
3504        let entry_bytes = serde_json::to_vec(entry).expect("serialize membership entry");
3505        let coord = entry.coord();
3506        let entry_ref = MembershipEntryRef {
3507            coord: coord.clone(),
3508            object: exact(
3509                format!(
3510                    "{}.json",
3511                    membership_entry_semantic_prefix(
3512                        &coord.author_pubkey,
3513                        &coord.author_owner_grant,
3514                        coord.stream_id,
3515                        coord.seq,
3516                        coord.entry_hash,
3517                    )
3518                ),
3519                &entry_bytes,
3520            ),
3521        };
3522        let anchor = membership_anchor(&entry.store_id);
3523        let successor = SuccessorLink {
3524            activation: StreamActivationId::store_membership(
3525                &root,
3526                &registration_ref,
3527                &entry.author_owner_grant,
3528                &anchor,
3529            ),
3530            predecessor: None,
3531            next_slot: slot(format!(
3532                "test/{}/membership-heads/{}/next.json",
3533                entry.store_id, coord.entry_hash
3534            )),
3535        };
3536        let device_signer = registration.device_signer(signer).unwrap();
3537        let head = AuthorHead::signed(
3538            entry.store_id.clone(),
3539            registration_ref,
3540            entry_ref,
3541            None,
3542            resolutions,
3543            successor,
3544            &device_signer,
3545        );
3546        let head_bytes = serde_json::to_vec(&head).expect("serialize membership head");
3547        let reference = MembershipHeadRef {
3548            coord: coord.clone(),
3549            head_hash: head.head_hash(),
3550            object: exact(
3551                format!(
3552                    "{}.json",
3553                    membership_head_semantic_prefix(
3554                        &coord.author_pubkey,
3555                        &coord.author_owner_grant,
3556                        coord.stream_id,
3557                        coord.seq,
3558                        head.head_hash(),
3559                    )
3560                ),
3561                &head_bytes,
3562            ),
3563        };
3564        (reference, head)
3565    }
3566
3567    fn exact_resolution(
3568        resolution: StoreMembershipConflictResolution,
3569    ) -> (
3570        StoreMembershipConflictResolutionRef,
3571        StoreMembershipConflictResolution,
3572    ) {
3573        let bytes = serde_json::to_vec(&resolution).expect("serialize membership resolution");
3574        let reference = resolution.resolution_ref(exact(
3575            format!(
3576                "{}.json",
3577                membership_resolution_semantic_prefix(
3578                    resolution.conflict_hash,
3579                    &resolution.resolver_pubkey,
3580                    resolution.resolution_hash(),
3581                )
3582            ),
3583            &bytes,
3584        ));
3585        (reference, resolution)
3586    }
3587
3588    fn join_registration(
3589        root: &StoreRootRef,
3590        label: &str,
3591        signer: &UserKeypair,
3592    ) -> (StoreDeviceRegistration, StoreDeviceRegistrationRef) {
3593        let attempt_id = DeviceJoinAttemptId::from_hash(ObjectHash::digest(label.as_bytes()));
3594        let registration = StoreDeviceRegistration::signed(
3595            root.clone(),
3596            StoreDeviceRegistrationOrigin::Join {
3597                attempt_id,
3598                attempt_slot: slot(format!("test/{label}/join/attempt.json")),
3599                outcome_slot: slot(format!("test/{label}/join/outcome.json")),
3600            },
3601            ProviderDeviceBinding {
3602                principal: ProviderPrincipalId::CustomS3Credential {
3603                    access_key_id_hash: ObjectHash::digest(
3604                        format!("{label} access key").as_bytes(),
3605                    ),
3606                },
3607            },
3608            StoreCommitAnchor::Serial,
3609            DeviceStreamAnchor::StoreAcknowledgements {
3610                first_slot: slot(format!("test/{label}/acks/1.json")),
3611            },
3612            DeviceStreamAnchor::StoreSnapshots {
3613                first_slot: slot(format!("test/{label}/snapshots/1.json")),
3614            },
3615            signer,
3616        )
3617        .expect("sign test join registration");
3618        let bytes = registration.to_bytes();
3619        let reference = StoreDeviceRegistrationRef::from_registration(
3620            &registration,
3621            exact(
3622                format!(
3623                    "{}.json",
3624                    registration_semantic_prefix(&registration.device_id.to_string())
3625                ),
3626                &bytes,
3627            ),
3628        );
3629        (registration, reference)
3630    }
3631
3632    fn serial_retirement_commit(
3633        role: MemberRole,
3634    ) -> (
3635        SerialAuthorizationState,
3636        StoreBatchCommitRef,
3637        StoreBatchCommit,
3638        StoreDeviceRegistration,
3639    ) {
3640        let store_id = "serial-follower-retirement";
3641        let owner = key();
3642        let follower = key();
3643        let root = test_root(store_id);
3644        let founder = test_founder_entry(store_id, &owner, "founder", membership_anchor(store_id));
3645        let founder_recovery = recovery_anchor(store_id);
3646        let (owner_registration, owner_registration_ref) =
3647            registration(&root, "serial-retirement-owner", &owner);
3648        let founder_devices = ResolvedStoreDeviceState::founder(
3649            &root,
3650            owner_registration_ref.clone(),
3651            &founder.author_pubkey,
3652            founder.author_owner_grant.clone(),
3653            &founder_recovery,
3654        )
3655        .unwrap();
3656        let genesis = StoreSerialPredecessor::Genesis {
3657            root: root.clone(),
3658            founder_registration: owner_registration_ref.clone(),
3659        };
3660        let membership = SerialMembershipState::from_founder(root.store_root_hash, &founder)
3661            .expect("founder membership");
3662        let authorization = SerialAuthorizationState::from_test_membership(&founder, membership)
3663            .expect("founder authorization");
3664        let add_follower = authorization
3665            .membership
3666            .signed_set_member(
3667                &owner,
3668                keys::public_key_hex(&follower),
3669                None,
3670                role,
3671                "add retirement author".to_string(),
3672            )
3673            .unwrap();
3674        let add_order = StoreCommitOrder::Serial {
3675            seq: 1,
3676            predecessor: genesis.clone(),
3677        };
3678        let add_membership = StoreMembershipStateRef::serial(
3679            genesis.clone(),
3680            founder_devices.recovery.clone(),
3681            &authorization,
3682        )
3683        .unwrap();
3684        let add_devices = StoreDeviceStateRef::serial(genesis, &founder_devices).unwrap();
3685        let add_commit = StoreBatchCommit::signed_with_control(
3686            root.store_root_hash,
3687            crate::WriteId::from_generated("add-retirement-follower".to_string()),
3688            StoreCommitCoord::Serial { sequence: 1 },
3689            owner_registration_ref,
3690            &owner_registration,
3691            add_order,
3692            add_membership,
3693            add_devices,
3694            None,
3695            Some(StoreControl::SerialMembership {
3696                entry: add_follower,
3697            }),
3698            None,
3699            &owner_registration.device_signer(&owner).unwrap(),
3700        )
3701        .unwrap();
3702        let add_bytes = add_commit.to_bytes();
3703        let add_ref = StoreBatchCommitRef::from_commit(
3704            &add_commit,
3705            StoreCommitCoord::Serial { sequence: 1 },
3706            exact(
3707                format!(
3708                    "{}.json",
3709                    commit_semantic_prefix(
3710                        add_commit.candidate_family(),
3711                        SERIAL_STREAM_ID,
3712                        1,
3713                        add_commit.commit_hash(),
3714                    )
3715                ),
3716                &add_bytes,
3717            ),
3718        )
3719        .unwrap();
3720        let authorization = authorization
3721            .authorize_and_apply(&add_ref, &add_commit, &owner_registration)
3722            .unwrap();
3723        let (follower_registration, follower_ref) =
3724            join_registration(&root, "serial-retirement-follower", &follower);
3725        let active_devices = founder_devices
3726            .activate_registration(follower_ref.clone(), None)
3727            .unwrap();
3728        let predecessor = StoreSerialPredecessor::Commit(add_ref);
3729        let order = StoreCommitOrder::Serial {
3730            seq: 2,
3731            predecessor: predecessor.clone(),
3732        };
3733        let write_id = crate::WriteId::from_generated("retire-follower".to_string());
3734        let candidate_family =
3735            CandidateFamilyId::derive(root.store_root_hash, &follower_ref, &write_id, &order);
3736        let retirement = StoreDeviceSelfRetirement::signed(
3737            root.store_root_hash,
3738            candidate_family,
3739            follower_ref.clone(),
3740            StoreHistoryCut::Serial(predecessor.clone()),
3741            &follower_registration.device_signer(&follower).unwrap(),
3742        )
3743        .unwrap();
3744        let retirement_bytes = retirement.to_bytes();
3745        let retirement_ref = StoreDeviceSelfRetirementRef::from_retirement(
3746            &retirement,
3747            exact(
3748                format!(
3749                    "{}.json",
3750                    device_self_retirement_semantic_prefix(
3751                        candidate_family,
3752                        &follower_registration.device_id,
3753                        retirement.retirement_hash(),
3754                    )
3755                ),
3756                &retirement_bytes,
3757            ),
3758        );
3759        StoreDeviceSelfRetirement::parse_at(
3760            &retirement_bytes,
3761            &retirement_ref,
3762            &follower_registration,
3763        )
3764        .expect("verify exact retirement object");
3765        let membership_state = StoreMembershipStateRef::serial(
3766            predecessor.clone(),
3767            active_devices.recovery.clone(),
3768            &authorization,
3769        )
3770        .unwrap();
3771        let device_state = StoreDeviceStateRef::serial(predecessor, &active_devices).unwrap();
3772        let signer = follower_registration.device_signer(&follower).unwrap();
3773        let commit = StoreBatchCommit::signed_with_self_retirement(
3774            root.store_root_hash,
3775            write_id,
3776            StoreCommitCoord::Serial { sequence: 2 },
3777            follower_ref,
3778            &follower_registration,
3779            order,
3780            membership_state,
3781            device_state,
3782            None,
3783            retirement_ref,
3784            &signer,
3785        )
3786        .unwrap();
3787        let commit_bytes = commit.to_bytes();
3788        let commit_ref = StoreBatchCommitRef::from_commit(
3789            &commit,
3790            StoreCommitCoord::Serial { sequence: 2 },
3791            exact(
3792                format!(
3793                    "{}.json",
3794                    commit_semantic_prefix(
3795                        commit.candidate_family(),
3796                        SERIAL_STREAM_ID,
3797                        2,
3798                        commit.commit_hash(),
3799                    )
3800                ),
3801                &commit_bytes,
3802            ),
3803        )
3804        .unwrap();
3805        (authorization, commit_ref, commit, follower_registration)
3806    }
3807
3808    fn founded(store_id: &str, owner: &UserKeypair) -> MembershipChain {
3809        MembershipChain::from_entries(vec![test_founder_entry(
3810            store_id,
3811            owner,
3812            "founder",
3813            membership_anchor(store_id),
3814        )])
3815        .unwrap()
3816    }
3817
3818    fn three_owner_store_cycle() -> (UserKeypair, UserKeypair, UserKeypair, MembershipChain) {
3819        let first = key();
3820        let second = key();
3821        let third = key();
3822        let first_pubkey = keys::public_key_hex(&first);
3823        let second_pubkey = keys::public_key_hex(&second);
3824        let third_pubkey = keys::public_key_hex(&third);
3825        let mut base = founded("three-owner-store", &first);
3826        let add_second = base
3827            .signed_set_member_in_stream(
3828                &first,
3829                stream(1),
3830                second_pubkey.clone(),
3831                None,
3832                MemberRole::Owner,
3833                "add second Owner".to_string(),
3834            )
3835            .expect("add second Owner");
3836        base.add_entry(add_second).expect("apply second Owner");
3837        let add_third = base
3838            .signed_set_member_in_stream(
3839                &first,
3840                stream(1),
3841                third_pubkey,
3842                None,
3843                MemberRole::Owner,
3844                "add third Owner".to_string(),
3845            )
3846            .expect("add third Owner");
3847        base.add_entry(add_third).expect("apply third Owner");
3848        let remove_second = base
3849            .signed_remove_member_in_stream(
3850                &first,
3851                stream(1),
3852                second_pubkey,
3853                "first branch".to_string(),
3854            )
3855            .expect("first branch");
3856        let remove_first = base
3857            .signed_remove_member_in_stream(
3858                &second,
3859                stream(92),
3860                first_pubkey,
3861                "second branch".to_string(),
3862            )
3863            .expect("second branch");
3864        let mut entries = base.entries().to_vec();
3865        entries.extend([remove_second.clone(), remove_first.clone()]);
3866        let heads = vec![
3867            exact_head(
3868                base.entries().first().expect("founder membership entry"),
3869                &first,
3870            ),
3871            exact_head(&remove_second, &first),
3872            exact_head(&remove_first, &second),
3873        ];
3874        let conflict = MembershipChain::from_entries_with_coords_and_heads(
3875            entries
3876                .into_iter()
3877                .map(|entry| (entry.coord(), entry))
3878                .collect(),
3879            heads,
3880        )
3881        .expect("three-Owner Store conflict");
3882        (first, second, third, conflict)
3883    }
3884
3885    #[test]
3886    fn unaffected_store_owner_resolution_retires_its_selected_branch_grant() {
3887        let (_first, _second, third, conflicted) = three_owner_store_cycle();
3888        let third_pubkey = keys::public_key_hex(&third);
3889        let (branch, old_grant) = match conflicted.conflict().expect("conflict") {
3890            MembershipConflict::RevocationCycle {
3891                maximal_valid_branches,
3892                ..
3893            } => {
3894                let branch = maximal_valid_branches
3895                    .iter()
3896                    .find(|branch| {
3897                        branch.active_grants.values().any(|record| {
3898                            record.member_pubkey == third_pubkey && record.role == MemberRole::Owner
3899                        })
3900                    })
3901                    .expect("unaffected Owner branch");
3902                let old_grant = branch
3903                    .active_grants
3904                    .iter()
3905                    .find_map(|(grant, record)| {
3906                        (record.member_pubkey == third_pubkey).then_some(grant.clone())
3907                    })
3908                    .expect("unaffected Owner grant");
3909                (branch.heads.clone(), old_grant)
3910            }
3911            _ => panic!("expected revocation conflict"),
3912        };
3913        let store_root_hash = ObjectHash::digest(b"unaffected Store resolver root");
3914        let resolution = conflicted
3915            .signed_cycle_resolution(
3916                store_root_hash,
3917                branch,
3918                membership_anchor("unaffected-store-resolver"),
3919                &third,
3920            )
3921            .expect("unaffected Owner resolution");
3922        let resolution = exact_resolution(resolution);
3923        let resolved = conflicted
3924            .resolved_with(store_root_hash, std::slice::from_ref(&resolution))
3925            .expect("unaffected Owner resolution is valid");
3926
3927        assert!(resolution.1.retired_owner_grants.contains(&old_grant));
3928        assert!(!resolved.active_grants.contains_key(&old_grant));
3929        assert!(resolved
3930            .active_grants
3931            .contains_key(&resolution.1.replacement_grant));
3932    }
3933
3934    #[test]
3935    fn store_revocation_cycle_over_protocol_bound_is_typed() {
3936        let owners = (0..13).map(|_| key()).collect::<Vec<_>>();
3937        let pubkeys = owners.iter().map(keys::public_key_hex).collect::<Vec<_>>();
3938        let mut base = founded("bounded-store-cycle", &owners[0]);
3939        for pubkey in pubkeys.iter().skip(1) {
3940            let add = base
3941                .signed_set_member_in_stream(
3942                    &owners[0],
3943                    stream(1),
3944                    pubkey.clone(),
3945                    None,
3946                    MemberRole::Owner,
3947                    format!("add {pubkey}"),
3948                )
3949                .expect("add ring Owner");
3950            base.add_entry(add).expect("apply ring Owner");
3951        }
3952        let removals = owners
3953            .iter()
3954            .enumerate()
3955            .map(|(index, owner)| {
3956                base.signed_remove_member_in_stream(
3957                    owner,
3958                    stream(index as u8 + 101),
3959                    pubkeys[(index + 1) % pubkeys.len()].clone(),
3960                    format!("remove ring successor {index}"),
3961                )
3962                .expect("sign ring removal")
3963            })
3964            .collect::<Vec<_>>();
3965        let mut entries = base.entries().to_vec();
3966        entries.extend(removals.iter().cloned());
3967        let heads = removals
3968            .iter()
3969            .zip(&owners)
3970            .map(|(entry, owner)| exact_head(entry, owner))
3971            .collect();
3972
3973        assert!(matches!(
3974            MembershipChain::from_entries_with_coords_and_heads(
3975                entries
3976                    .into_iter()
3977                    .map(|entry| (entry.coord(), entry))
3978                    .collect(),
3979                heads,
3980            ),
3981            Err(MembershipError::RevocationCycleTooWide {
3982                sources: 13,
3983                maximum: 12,
3984            })
3985        ));
3986    }
3987
3988    #[test]
3989    fn serial_follower_can_author_its_exact_self_retirement() {
3990        let (authorization, commit_ref, commit, follower_registration) =
3991            serial_retirement_commit(MemberRole::Follower);
3992
3993        authorization
3994            .authorize_and_apply(&commit_ref, &commit, &follower_registration)
3995            .expect("Follower exact self-retirement is authorized");
3996    }
3997
3998    #[test]
3999    fn serial_device_state_activates_and_retires_an_exact_registration() {
4000        let store_id = "serial-registration";
4001        let owner = key();
4002        let follower = key();
4003        let root = test_root(store_id);
4004        let founder = test_founder_entry(store_id, &owner, "founder", membership_anchor(store_id));
4005        let founder_recovery = recovery_anchor(store_id);
4006        let (_owner_registration, owner_registration_ref) =
4007            registration(&root, "serial-registration-owner", &owner);
4008        let founder_state = ResolvedStoreDeviceState::founder(
4009            &root,
4010            owner_registration_ref.clone(),
4011            &founder.author_pubkey,
4012            founder.author_owner_grant.clone(),
4013            &founder_recovery,
4014        )
4015        .unwrap();
4016        let follower_registration = StoreDeviceRegistration::signed(
4017            root.clone(),
4018            StoreDeviceRegistrationOrigin::Join {
4019                attempt_id: DeviceJoinAttemptId::from_hash(ObjectHash::digest(
4020                    b"follower join attempt",
4021                )),
4022                attempt_slot: slot("test/serial-registration/join/attempt.json"),
4023                outcome_slot: slot("test/serial-registration/join/outcome.json"),
4024            },
4025            ProviderDeviceBinding {
4026                principal: ProviderPrincipalId::CustomS3Credential {
4027                    access_key_id_hash: ObjectHash::digest(b"follower access key"),
4028                },
4029            },
4030            StoreCommitAnchor::Serial,
4031            DeviceStreamAnchor::StoreAcknowledgements {
4032                first_slot: slot("test/serial-registration/follower/acks/1.json"),
4033            },
4034            DeviceStreamAnchor::StoreSnapshots {
4035                first_slot: slot("test/serial-registration/follower/snapshots/1.json"),
4036            },
4037            &follower,
4038        )
4039        .unwrap();
4040        let follower_bytes = follower_registration.to_bytes();
4041        let follower_ref = StoreDeviceRegistrationRef::from_registration(
4042            &follower_registration,
4043            exact(
4044                format!(
4045                    "{}.json",
4046                    registration_semantic_prefix(&follower_registration.device_id.to_string())
4047                ),
4048                &follower_bytes,
4049            ),
4050        );
4051        let active_state = founder_state
4052            .activate_registration(follower_ref.clone(), None)
4053            .expect("activate exact follower registration");
4054        let predecessor = StoreSerialPredecessor::Genesis {
4055            root: root.clone(),
4056            founder_registration: owner_registration_ref,
4057        };
4058        let order = StoreCommitOrder::Serial {
4059            seq: 1,
4060            predecessor: predecessor.clone(),
4061        };
4062        let write_id = crate::WriteId::from_generated("follower-retirement".to_string());
4063        let family =
4064            CandidateFamilyId::derive(root.store_root_hash, &follower_ref, &write_id, &order);
4065        let retirement = StoreDeviceSelfRetirement::signed(
4066            root.store_root_hash,
4067            family,
4068            follower_ref,
4069            StoreHistoryCut::Serial(predecessor),
4070            &follower_registration.device_signer(&follower).unwrap(),
4071        )
4072        .unwrap();
4073        let retirement_bytes = retirement.to_bytes();
4074        let retirement_ref = StoreDeviceSelfRetirementRef::from_retirement(
4075            &retirement,
4076            exact(
4077                format!(
4078                    "{}.json",
4079                    device_self_retirement_semantic_prefix(
4080                        family,
4081                        &follower_registration.device_id,
4082                        retirement.retirement_hash(),
4083                    )
4084                ),
4085                &retirement_bytes,
4086            ),
4087        );
4088        StoreDeviceSelfRetirement::parse_at(
4089            &retirement_bytes,
4090            &retirement_ref,
4091            &follower_registration,
4092        )
4093        .expect("verify exact self-retirement");
4094        let retired_state = active_state.self_retire(retirement_ref).unwrap();
4095        assert!(matches!(
4096            retired_state
4097                .devices
4098                .get(&follower_registration.device_id)
4099                .expect("follower device state")
4100                .status,
4101            crate::sync::store_commit::StoreDeviceStatus::Inactive { .. }
4102        ));
4103    }
4104
4105    #[test]
4106    fn self_retirement_signature_cannot_retire_another_identity_registration() {
4107        let root = test_root("follower-registration-negatives");
4108        let follower = key();
4109        let outsider = key();
4110        let (follower_registration, follower_ref) =
4111            registration(&root, "negative-follower", &follower);
4112        let (outsider_registration, outsider_ref) =
4113            registration(&root, "negative-outsider", &outsider);
4114        let predecessor = StoreSerialPredecessor::Genesis {
4115            root: root.clone(),
4116            founder_registration: follower_ref,
4117        };
4118        let order = StoreCommitOrder::Serial {
4119            seq: 1,
4120            predecessor: predecessor.clone(),
4121        };
4122        let write_id = crate::WriteId::from_generated("foreign-retirement".to_string());
4123        let family =
4124            CandidateFamilyId::derive(root.store_root_hash, &outsider_ref, &write_id, &order);
4125        let retirement = StoreDeviceSelfRetirement::signed(
4126            root.store_root_hash,
4127            family,
4128            outsider_ref,
4129            StoreHistoryCut::Serial(predecessor),
4130            &follower_registration.device_signer(&follower).unwrap(),
4131        )
4132        .unwrap();
4133        let bytes = retirement.to_bytes();
4134        let reference = StoreDeviceSelfRetirementRef::from_retirement(
4135            &retirement,
4136            exact(
4137                format!(
4138                    "{}.json",
4139                    device_self_retirement_semantic_prefix(
4140                        family,
4141                        &outsider_registration.device_id,
4142                        retirement.retirement_hash(),
4143                    )
4144                ),
4145                &bytes,
4146            ),
4147        );
4148        assert!(matches!(
4149            StoreDeviceSelfRetirement::parse_at(&bytes, &reference, &outsider_registration),
4150            Err(crate::sync::store_commit::StoreProtocolError::InvalidSignature)
4151        ));
4152    }
4153
4154    #[test]
4155    fn timestamp_does_not_change_causal_authorization() {
4156        let owner = key();
4157        let member = key();
4158        let mut chain = founded("store", &owner);
4159        let add = chain
4160            .signed_set_member_in_stream(
4161                &owner,
4162                stream(1),
4163                keys::public_key_hex(&member),
4164                None,
4165                MemberRole::Member,
4166                "9999".to_string(),
4167            )
4168            .unwrap();
4169        chain.add_entry(add).unwrap();
4170        let remove = chain
4171            .signed_remove_member_in_stream(
4172                &owner,
4173                stream(1),
4174                keys::public_key_hex(&member),
4175                "0000".to_string(),
4176            )
4177            .unwrap();
4178        chain.add_entry(remove).unwrap();
4179        assert!(!chain.can_write_now(&keys::public_key_hex(&member)));
4180    }
4181
4182    #[test]
4183    fn signed_candidate_is_validated_before_it_is_returned() {
4184        let owner = key();
4185        let chain = founded("store", &owner);
4186
4187        assert!(matches!(
4188            chain.signed_remove_member_in_stream(
4189                &owner,
4190                stream(1),
4191                keys::public_key_hex(&owner),
4192                "remove last owner".to_string(),
4193            ),
4194            Err(MembershipError::NoActiveOwner)
4195        ));
4196    }
4197
4198    #[test]
4199    fn concurrent_member_assignments_are_validated_conflict_state() {
4200        let owner = key();
4201        let target = key();
4202        let chain = founded("store", &owner);
4203        let first = chain
4204            .signed_set_member_in_stream(
4205                &owner,
4206                stream(21),
4207                keys::public_key_hex(&target),
4208                None,
4209                MemberRole::Member,
4210                "first".to_string(),
4211            )
4212            .unwrap();
4213        let second = chain
4214            .signed_set_member_in_stream(
4215                &owner,
4216                stream(22),
4217                keys::public_key_hex(&target),
4218                None,
4219                MemberRole::Owner,
4220                "second".to_string(),
4221            )
4222            .unwrap();
4223        let mut entries = chain.entries().to_vec();
4224        entries.extend([first.clone(), second.clone()]);
4225        let heads = entries
4226            .iter()
4227            .filter(|entry| {
4228                !entries.iter().any(|candidate| {
4229                    candidate
4230                        .dependencies
4231                        .iter()
4232                        .any(|dependency| dependency == &entry.coord())
4233                        && candidate.stream_id == entry.stream_id
4234                })
4235            })
4236            .map(|entry| exact_head(entry, &owner))
4237            .collect();
4238
4239        let conflicted = MembershipChain::from_entries_with_coords_and_heads(
4240            entries
4241                .into_iter()
4242                .map(|entry| (entry.coord(), entry))
4243                .collect(),
4244            heads,
4245        )
4246        .expect("well-formed conflict");
4247        assert!(matches!(
4248            conflicted.status(),
4249            MembershipStatus::Conflict(MembershipConflict::ConcurrentMemberAssignments {
4250                member_pubkey,
4251                conflicting_grants,
4252                ..
4253            }) if member_pubkey == &keys::public_key_hex(&target)
4254                && conflicting_grants.len() == 2
4255        ));
4256    }
4257
4258    #[test]
4259    fn concurrent_cross_revocation_is_a_validated_cycle_conflict() {
4260        let first_owner = key();
4261        let second_owner = key();
4262        let first_pubkey = keys::public_key_hex(&first_owner);
4263        let second_pubkey = keys::public_key_hex(&second_owner);
4264        let mut base = founded("store", &first_owner);
4265        let add_second = base
4266            .signed_set_member_in_stream(
4267                &first_owner,
4268                stream(1),
4269                second_pubkey.clone(),
4270                None,
4271                MemberRole::Owner,
4272                "add second".to_string(),
4273            )
4274            .unwrap();
4275        base.add_entry(add_second).unwrap();
4276        let remove_second = base
4277            .signed_remove_member_in_stream(
4278                &first_owner,
4279                stream(1),
4280                second_pubkey.clone(),
4281                "remove second".to_string(),
4282            )
4283            .unwrap();
4284        let remove_first = base
4285            .signed_remove_member_in_stream(
4286                &second_owner,
4287                stream(23),
4288                first_pubkey.clone(),
4289                "remove first".to_string(),
4290            )
4291            .unwrap();
4292        let mut entries = base.entries().to_vec();
4293        entries.extend([remove_second.clone(), remove_first.clone()]);
4294        let heads = vec![
4295            exact_head(
4296                base.entries().first().expect("founder membership entry"),
4297                &first_owner,
4298            ),
4299            exact_head(&remove_second, &first_owner),
4300            exact_head(&remove_first, &second_owner),
4301        ];
4302
4303        let conflicted = MembershipChain::from_entries_with_coords_and_heads(
4304            entries
4305                .into_iter()
4306                .map(|entry| (entry.coord(), entry))
4307                .collect(),
4308            heads,
4309        )
4310        .expect("well-formed conflict");
4311        assert!(matches!(
4312            conflicted.status(),
4313            MembershipStatus::Conflict(MembershipConflict::RevocationCycle {
4314                cyclic_sources,
4315                involved_owner_grants,
4316                maximal_valid_branches,
4317                ..
4318
4319            }) if cyclic_sources.len() == 2
4320                && involved_owner_grants.len() == 2
4321                && maximal_valid_branches.len() == 2
4322        ));
4323
4324        let MembershipConflict::RevocationCycle {
4325            maximal_valid_branches,
4326            ..
4327        } = conflicted.conflict().expect("cycle conflict")
4328        else {
4329            unreachable!();
4330        };
4331        let resolver_branch_state = maximal_valid_branches
4332            .iter()
4333            .find(|branch| {
4334                branch.active_grants.values().any(|record| {
4335                    record.member_pubkey == first_pubkey && record.role == MemberRole::Owner
4336                })
4337            })
4338            .expect("first Owner branch")
4339            .clone();
4340        let resolver_branch = resolver_branch_state.heads.clone();
4341        let second_resolver_branch = maximal_valid_branches
4342            .iter()
4343            .find(|branch| {
4344                branch.active_grants.values().any(|record| {
4345                    record.member_pubkey == second_pubkey && record.role == MemberRole::Owner
4346                })
4347            })
4348            .expect("second Owner branch")
4349            .heads
4350            .clone();
4351        let store_root_hash = ObjectHash::digest(b"resolution Store root");
4352        let resolution_value = conflicted
4353            .signed_cycle_resolution(
4354                store_root_hash,
4355                resolver_branch.clone(),
4356                membership_anchor("first-cycle-resolution"),
4357                &first_owner,
4358            )
4359            .expect("branch Owner resolves the conflict");
4360        let second_resolution_value = conflicted
4361            .signed_cycle_resolution(
4362                store_root_hash,
4363                second_resolver_branch,
4364                membership_anchor("second-cycle-resolution"),
4365                &second_owner,
4366            )
4367            .expect("other branch Owner resolves the conflict");
4368        let retried = conflicted
4369            .signed_cycle_resolution(
4370                store_root_hash,
4371                resolver_branch,
4372                membership_anchor("first-cycle-resolution"),
4373                &first_owner,
4374            )
4375            .expect("same resolver retry");
4376        assert_eq!(resolution_value, retried);
4377        assert!(resolution_value.verify_against(
4378            store_root_hash,
4379            conflicted.conflict().expect("cycle conflict"),
4380        ));
4381        let resolution = exact_resolution(resolution_value);
4382        let second_resolution = exact_resolution(second_resolution_value);
4383        let resolved_once = conflicted
4384            .resolved_with(store_root_hash, std::slice::from_ref(&resolution))
4385            .expect("one resolution applies");
4386        let resolved_duplicate = conflicted
4387            .resolved_with(store_root_hash, &[resolution.clone(), resolution.clone()])
4388            .expect("an exact retry is idempotent");
4389        assert_eq!(resolved_once, resolved_duplicate);
4390        assert!(resolved_once
4391            .active_grants
4392            .contains_key(&resolution.1.replacement_grant));
4393        assert!(resolution
4394            .1
4395            .retired_owner_grants
4396            .iter()
4397            .all(|grant| !resolved_once.active_grants.contains_key(grant)));
4398
4399        let resolved_union = conflicted
4400            .resolved_with(
4401                store_root_hash,
4402                &[resolution.clone(), second_resolution.clone()],
4403            )
4404            .expect("distinct resolvers are unioned");
4405        assert!(resolved_union
4406            .active_grants
4407            .contains_key(&resolution.1.replacement_grant));
4408        assert!(resolved_union
4409            .active_grants
4410            .contains_key(&second_resolution.1.replacement_grant));
4411
4412        let mut branch_specific = conflicted.conflict().expect("cycle conflict").clone();
4413        let MembershipConflict::RevocationCycle {
4414            maximal_valid_branches,
4415            ..
4416        } = &mut branch_specific
4417        else {
4418            unreachable!()
4419        };
4420        let branch_only_grant = MembershipGrantId(ObjectHash::digest(b"branch-only grant"));
4421        let branch_only_creation = maximal_valid_branches[0].effective_frontier[0].clone();
4422        maximal_valid_branches[0].active_grants.insert(
4423            branch_only_grant.clone(),
4424            MembershipGrantRecord {
4425                member_pubkey: keys::public_key_hex(&key()),
4426                role: MemberRole::Member,
4427                provider_account_email: None,
4428                creation_authority: MembershipGrantCreationAuthority::Entry(branch_only_creation),
4429            },
4430        );
4431        let composed = resolve_store_membership_conflict(
4432            store_root_hash,
4433            &branch_specific,
4434            &[resolution.clone(), second_resolution.clone()],
4435        )
4436        .expect("compose only grants agreed by every valid branch");
4437        assert!(!composed.active_grants.contains_key(&branch_only_grant));
4438
4439        let mut duplicate_member = branch_specific;
4440        let MembershipConflict::RevocationCycle {
4441            maximal_valid_branches,
4442            ..
4443        } = &mut duplicate_member
4444        else {
4445            unreachable!()
4446        };
4447        let duplicate_pubkey = keys::public_key_hex(&key());
4448        let duplicate_creation = resolution.1.conflicting_heads[0].coord.clone();
4449        for branch in maximal_valid_branches {
4450            for suffix in [b'a', b'b'] {
4451                branch.active_grants.insert(
4452                    MembershipGrantId(ObjectHash::digest(&[suffix])),
4453                    MembershipGrantRecord {
4454                        member_pubkey: duplicate_pubkey.clone(),
4455                        role: MemberRole::Member,
4456                        provider_account_email: None,
4457                        creation_authority: MembershipGrantCreationAuthority::Entry(
4458                            duplicate_creation.clone(),
4459                        ),
4460                    },
4461                );
4462            }
4463        }
4464        assert!(matches!(
4465            resolve_store_membership_conflict(
4466                store_root_hash,
4467                &duplicate_member,
4468                &[resolution.clone(), second_resolution.clone()],
4469            ),
4470            Err(MembershipError::InvalidConflictResolution)
4471        ));
4472
4473        let mut resumed = conflicted.clone();
4474        let raw_heads = resumed.author_heads();
4475        resumed
4476            .apply_resolutions(store_root_hash, std::slice::from_ref(&resolution))
4477            .expect("resolution activates replacement Owner grant");
4478        assert_eq!(resumed.author_heads(), raw_heads);
4479        assert_eq!(
4480            resumed.effective_frontier(),
4481            resolver_branch_state.effective_frontier
4482        );
4483        assert_eq!(
4484            resumed.resolution_refs(),
4485            std::slice::from_ref(&resolution.0)
4486        );
4487        let after_resolution = resumed
4488            .signed_set_member_in_stream(
4489                &first_owner,
4490                stream(37),
4491                keys::public_key_hex(&key()),
4492                None,
4493                MemberRole::Member,
4494                "write after resolution".to_string(),
4495            )
4496            .expect("replacement Owner can author from a fresh stream");
4497        assert_eq!(
4498            after_resolution.author_owner_grant,
4499            resolution.1.replacement_grant
4500        );
4501        let activated_head = exact_head(&after_resolution, &first_owner).1;
4502        resumed
4503            .add_entry(after_resolution)
4504            .expect("future authoring validates from the resolved checkpoint");
4505        assert_eq!(activated_head.resolutions, vec![resolution.0.clone()]);
4506        let authority = MembershipGrantCreationAuthority::ConflictResolution(resolution.0.clone());
4507        assert!(resumed.authorizes_write_authority(&authority, &first_pubkey));
4508        assert!(matches!(
4509            conflicted.signed_cycle_resolution(
4510                store_root_hash,
4511                resolution.1.resolver_branch_heads.clone(),
4512                membership_anchor("non-owner-cycle-resolution"),
4513                &key(),
4514            ),
4515            Err(MembershipError::SignerIsNotOwner(_))
4516        ));
4517    }
4518
4519    #[test]
4520    fn dependency_frontier_must_be_strictly_ordered_by_author_stream() {
4521        let founder = key();
4522        let second_owner = key();
4523        let mut chain = founded("store", &founder);
4524        let add_owner = chain
4525            .signed_set_member_in_stream(
4526                &founder,
4527                stream(1),
4528                keys::public_key_hex(&second_owner),
4529                None,
4530                MemberRole::Owner,
4531                "add owner".to_string(),
4532            )
4533            .unwrap();
4534        chain.add_entry(add_owner).unwrap();
4535        let second_stream = chain
4536            .signed_set_member_in_stream(
4537                &second_owner,
4538                stream(31),
4539                keys::public_key_hex(&key()),
4540                None,
4541                MemberRole::Member,
4542                "second stream".to_string(),
4543            )
4544            .unwrap();
4545        chain.add_entry(second_stream).unwrap();
4546        let mut unsorted = chain
4547            .signed_set_member_in_stream(
4548                &founder,
4549                stream(1),
4550                keys::public_key_hex(&key()),
4551                None,
4552                MemberRole::Member,
4553                "candidate".to_string(),
4554            )
4555            .unwrap();
4556        assert!(unsorted.dependencies.len() > 1);
4557        unsorted.dependencies.reverse();
4558        sign_membership_entry(&mut unsorted, &founder);
4559
4560        assert!(matches!(
4561            chain.add_entry(unsorted),
4562            Err(MembershipError::NonCanonicalDependencyFrontier { .. })
4563        ));
4564    }
4565
4566    #[test]
4567    fn owner_barrier_must_be_strictly_ordered_by_author_stream() {
4568        let founder = key();
4569        let second_owner = key();
4570        let second_owner_pubkey = keys::public_key_hex(&second_owner);
4571        let mut chain = founded("store", &founder);
4572        let add_owner = chain
4573            .signed_set_member_in_stream(
4574                &founder,
4575                stream(1),
4576                second_owner_pubkey.clone(),
4577                None,
4578                MemberRole::Owner,
4579                "add owner".to_string(),
4580            )
4581            .unwrap();
4582        chain.add_entry(add_owner).unwrap();
4583        for (stream_id, timestamp) in [(stream(41), "first stream"), (stream(42), "second stream")]
4584        {
4585            let authored = chain
4586                .signed_set_member_in_stream(
4587                    &second_owner,
4588                    stream_id,
4589                    keys::public_key_hex(&key()),
4590                    None,
4591                    MemberRole::Member,
4592                    timestamp.to_string(),
4593                )
4594                .unwrap();
4595            chain.add_entry(authored).unwrap();
4596        }
4597        let mut removal = chain
4598            .signed_remove_member_in_stream(
4599                &founder,
4600                stream(1),
4601                second_owner_pubkey,
4602                "remove owner".to_string(),
4603            )
4604            .unwrap();
4605        let MembershipChange::RemoveMember { owner_barriers, .. } = &mut removal.change else {
4606            unreachable!();
4607        };
4608        let observed = &mut owner_barriers
4609            .values_mut()
4610            .next()
4611            .expect("owner removal barrier")
4612            .observed_streams;
4613        assert!(observed.len() > 1);
4614        observed.reverse();
4615        sign_membership_entry(&mut removal, &founder);
4616
4617        assert!(matches!(
4618            chain.add_entry(removal),
4619            Err(MembershipError::InvalidOwnerRevocationBarrier { .. })
4620        ));
4621    }
4622
4623    #[test]
4624    fn owner_readd_uses_a_new_sequence_one_stream() {
4625        let owner = key();
4626        let second = key();
4627        let mut chain = founded("store", &owner);
4628        let first = chain
4629            .signed_set_member_in_stream(
4630                &owner,
4631                stream(1),
4632                keys::public_key_hex(&second),
4633                None,
4634                MemberRole::Owner,
4635                "add".to_string(),
4636            )
4637            .unwrap();
4638        chain.add_entry(first).unwrap();
4639        let old_grant = chain
4640            .active_owner_grant(&keys::public_key_hex(&second))
4641            .unwrap();
4642        let remove = chain
4643            .signed_remove_member_in_stream(
4644                &owner,
4645                stream(1),
4646                keys::public_key_hex(&second),
4647                "remove".to_string(),
4648            )
4649            .unwrap();
4650        chain.add_entry(remove).unwrap();
4651        let readd = chain
4652            .signed_set_member_in_stream(
4653                &owner,
4654                stream(1),
4655                keys::public_key_hex(&second),
4656                None,
4657                MemberRole::Owner,
4658                "readd".to_string(),
4659            )
4660            .unwrap();
4661        chain.add_entry(readd).unwrap();
4662        let new_grant = chain
4663            .active_owner_grant(&keys::public_key_hex(&second))
4664            .unwrap();
4665        assert_ne!(old_grant, new_grant);
4666        let authored = chain
4667            .signed_set_member_in_stream(
4668                &second,
4669                stream(32),
4670                keys::public_key_hex(&key()),
4671                None,
4672                MemberRole::Member,
4673                "authored".to_string(),
4674            )
4675            .unwrap();
4676        assert_eq!(authored.seq, 1);
4677        assert_eq!(authored.author_owner_grant, new_grant);
4678    }
4679
4680    #[test]
4681    fn owner_self_removal_remains_effective_when_its_grant_is_capped_before_first() {
4682        let founder = key();
4683        let departing_owner = key();
4684        let departing_pubkey = keys::public_key_hex(&departing_owner);
4685        let mut chain = founded("store", &founder);
4686        let add_owner = chain
4687            .signed_set_member_in_stream(
4688                &founder,
4689                stream(1),
4690                departing_pubkey.clone(),
4691                None,
4692                MemberRole::Owner,
4693                "add owner".to_string(),
4694            )
4695            .unwrap();
4696        chain.add_entry(add_owner).unwrap();
4697
4698        let self_removal = chain
4699            .signed_remove_member_in_stream(
4700                &departing_owner,
4701                stream(33),
4702                departing_pubkey.clone(),
4703                "self removal".to_string(),
4704            )
4705            .unwrap();
4706        assert!(matches!(
4707            &self_removal.change,
4708            MembershipChange::RemoveMember { owner_barriers, .. }
4709                if owner_barriers.values().all(|barrier| barrier.observed_streams.is_empty())
4710        ));
4711        chain.add_entry(self_removal).unwrap();
4712
4713        assert!(!chain.is_owner_now(&departing_pubkey));
4714    }
4715
4716    #[test]
4717    fn before_first_barrier_excludes_every_entry_from_the_revoked_owner_stream() {
4718        let founder = key();
4719        let second_owner = key();
4720        let target = key();
4721        let mut observed = founded("store", &founder);
4722        let add_owner = observed
4723            .signed_set_member_in_stream(
4724                &founder,
4725                stream(1),
4726                keys::public_key_hex(&second_owner),
4727                None,
4728                MemberRole::Owner,
4729                "add owner".to_string(),
4730            )
4731            .unwrap();
4732        observed.add_entry(add_owner).unwrap();
4733
4734        let stale_entry = observed
4735            .signed_set_member_in_stream(
4736                &second_owner,
4737                stream(34),
4738                keys::public_key_hex(&target),
4739                None,
4740                MemberRole::Member,
4741                "stale entry".to_string(),
4742            )
4743            .unwrap();
4744        let removal = observed
4745            .signed_remove_member_in_stream(
4746                &founder,
4747                stream(1),
4748                keys::public_key_hex(&second_owner),
4749                "remove owner".to_string(),
4750            )
4751            .unwrap();
4752        assert!(matches!(
4753            &removal.change,
4754            MembershipChange::RemoveMember { owner_barriers, .. }
4755                if owner_barriers.values().all(|barrier| barrier.observed_streams.is_empty())
4756        ));
4757
4758        let mut entries = observed.entries().to_vec();
4759        entries.extend([removal, stale_entry]);
4760        let chain = MembershipChain::from_entries(entries).unwrap();
4761        assert!(!chain.can_write_now(&keys::public_key_hex(&target)));
4762        assert!(chain
4763            .author_heads()
4764            .iter()
4765            .any(|coord| coord.author_pubkey == keys::public_key_hex(&second_owner)));
4766        assert!(chain
4767            .effective_frontier()
4768            .iter()
4769            .all(|coord| coord.author_pubkey != keys::public_key_hex(&second_owner)));
4770    }
4771
4772    #[test]
4773    fn through_barrier_keeps_its_exact_prefix_and_prunes_the_stale_suffix() {
4774        let founder = key();
4775        let second_owner = key();
4776        let first_target = key();
4777        let second_target = key();
4778        let third_target = key();
4779        let mut observed = founded("store", &founder);
4780        let add_owner = observed
4781            .signed_set_member_in_stream(
4782                &founder,
4783                stream(1),
4784                keys::public_key_hex(&second_owner),
4785                None,
4786                MemberRole::Owner,
4787                "add owner".to_string(),
4788            )
4789            .unwrap();
4790        observed.add_entry(add_owner).unwrap();
4791        let first = observed
4792            .signed_set_member_in_stream(
4793                &second_owner,
4794                stream(35),
4795                keys::public_key_hex(&first_target),
4796                None,
4797                MemberRole::Member,
4798                "first".to_string(),
4799            )
4800            .unwrap();
4801        observed.add_entry(first.clone()).unwrap();
4802
4803        let removal = observed
4804            .signed_remove_member_in_stream(
4805                &founder,
4806                stream(1),
4807                keys::public_key_hex(&second_owner),
4808                "remove owner".to_string(),
4809            )
4810            .unwrap();
4811        assert!(matches!(
4812            &removal.change,
4813            MembershipChange::RemoveMember { owner_barriers, .. }
4814                if owner_barriers.values().any(|barrier| barrier.observed_streams == vec![first.coord()])
4815        ));
4816
4817        let second = observed
4818            .signed_set_member_in_stream(
4819                &second_owner,
4820                stream(35),
4821                keys::public_key_hex(&second_target),
4822                None,
4823                MemberRole::Member,
4824                "second".to_string(),
4825            )
4826            .unwrap();
4827        let mut exact_entries = observed.entries().to_vec();
4828        exact_entries.extend([removal.clone(), second.clone()]);
4829        let exact = MembershipChain::from_entries(exact_entries).unwrap();
4830        assert!(exact.can_write_now(&keys::public_key_hex(&first_target)));
4831        assert!(!exact.can_write_now(&keys::public_key_hex(&second_target)));
4832
4833        let mut stale = observed;
4834        stale.add_entry(second).unwrap();
4835        let third = stale
4836            .signed_set_member_in_stream(
4837                &second_owner,
4838                stream(35),
4839                keys::public_key_hex(&third_target),
4840                None,
4841                MemberRole::Member,
4842                "third".to_string(),
4843            )
4844            .unwrap();
4845        stale.add_entry(third.clone()).unwrap();
4846        let mut beyond_entries = stale.entries().to_vec();
4847        beyond_entries.push(removal);
4848        let pruned = MembershipChain::from_entries(beyond_entries).unwrap();
4849        assert!(pruned.can_write_now(&keys::public_key_hex(&first_target)));
4850        assert!(!pruned.can_write_now(&keys::public_key_hex(&second_target)));
4851        assert!(!pruned.can_write_now(&keys::public_key_hex(&third_target)));
4852    }
4853
4854    #[test]
4855    fn through_barrier_rejects_a_coordinate_hash_that_is_not_its_dependency() {
4856        let founder = key();
4857        let second_owner = key();
4858        let mut chain = founded("store", &founder);
4859        let add_owner = chain
4860            .signed_set_member_in_stream(
4861                &founder,
4862                stream(1),
4863                keys::public_key_hex(&second_owner),
4864                None,
4865                MemberRole::Owner,
4866                "add owner".to_string(),
4867            )
4868            .unwrap();
4869        chain.add_entry(add_owner).unwrap();
4870        let authored = chain
4871            .signed_set_member_in_stream(
4872                &second_owner,
4873                stream(36),
4874                keys::public_key_hex(&key()),
4875                None,
4876                MemberRole::Member,
4877                "authored".to_string(),
4878            )
4879            .unwrap();
4880        chain.add_entry(authored).unwrap();
4881        let mut removal = chain
4882            .signed_remove_member_in_stream(
4883                &founder,
4884                stream(1),
4885                keys::public_key_hex(&second_owner),
4886                "remove owner".to_string(),
4887            )
4888            .unwrap();
4889        let MembershipChange::RemoveMember { owner_barriers, .. } = &mut removal.change else {
4890            unreachable!();
4891        };
4892        let barrier = owner_barriers
4893            .values_mut()
4894            .next()
4895            .expect("owner removal barrier")
4896            .observed_streams
4897            .first_mut()
4898            .expect("observed owner stream");
4899        barrier.entry_hash = ObjectHash::digest(b"wrong barrier hash");
4900        sign_membership_entry(&mut removal, &founder);
4901        assert!(matches!(
4902            chain.add_entry(removal),
4903            Err(MembershipError::InvalidOwnerRevocationBarrier { .. })
4904        ));
4905    }
4906
4907    #[test]
4908    fn cross_store_replay_fails_even_with_the_same_founder_key() {
4909        let owner = key();
4910        let from_a = test_founder_entry("store-a", &owner, "founder", membership_anchor("store-a"));
4911        let mut replayed = from_a.clone();
4912        replayed.store_id = "store-b".to_string();
4913        assert!(!verify_membership_entry(&replayed));
4914        assert!(MembershipChain::from_entries(vec![from_a])
4915            .unwrap()
4916            .is_founded_by(&keys::public_key_hex(&owner)));
4917    }
4918
4919    #[test]
4920    fn created_at_is_signed_but_never_orders_entries() {
4921        let owner = key();
4922        let entry = test_founder_entry("store", &owner, "display-time", membership_anchor("store"));
4923        let mut tampered = entry.clone();
4924        tampered.created_at = "other".to_string();
4925        assert!(!verify_membership_entry(&tampered));
4926    }
4927
4928    #[test]
4929    fn serial_membership_applies_only_against_its_exact_previous_state() {
4930        let owner = key();
4931        let first_member = key();
4932        let second_member = key();
4933        let root = ObjectHash::digest(b"Serial membership root");
4934        let state = SerialMembershipState::from_founder(
4935            root,
4936            &test_founder_entry(
4937                "serial-store",
4938                &owner,
4939                "founder",
4940                membership_anchor("serial-store"),
4941            ),
4942        )
4943        .unwrap();
4944        let first = state
4945            .signed_set_member(
4946                &owner,
4947                keys::public_key_hex(&first_member),
4948                None,
4949                MemberRole::Member,
4950                "first".to_string(),
4951            )
4952            .unwrap();
4953        let stale = state
4954            .signed_set_member(
4955                &owner,
4956                keys::public_key_hex(&second_member),
4957                None,
4958                MemberRole::Member,
4959                "stale".to_string(),
4960            )
4961            .unwrap();
4962        let after_first = state.apply(&first).unwrap();
4963        assert!(matches!(
4964            after_first.apply(&stale),
4965            Err(SerialMembershipError::StaleState { .. })
4966        ));
4967
4968        let removal = after_first
4969            .signed_remove_member(
4970                &owner,
4971                keys::public_key_hex(&first_member),
4972                "remove".to_string(),
4973            )
4974            .unwrap();
4975        let after_removal = after_first.apply(&removal).unwrap();
4976        assert!(!after_removal.can_write(&keys::public_key_hex(&first_member)));
4977        assert_eq!(
4978            removal.previous_state_hash,
4979            after_first.state_hash(),
4980            "removal names the exact globally preceding membership state"
4981        );
4982    }
4983
4984    #[test]
4985    fn serial_membership_hash_changes_when_an_assignment_is_recreated() {
4986        let owner = key();
4987        let member = key();
4988        let root = ObjectHash::digest(b"Serial grant-bearing membership root");
4989        let state = SerialMembershipState::from_founder(
4990            root,
4991            &test_founder_entry(
4992                "serial-grant-store",
4993                &owner,
4994                "founder",
4995                membership_anchor("serial-grant-store"),
4996            ),
4997        )
4998        .unwrap();
4999        let first = state
5000            .signed_set_member(
5001                &owner,
5002                keys::public_key_hex(&member),
5003                None,
5004                MemberRole::Member,
5005                "first assignment".to_string(),
5006            )
5007            .unwrap();
5008        let first_state = state.apply(&first).unwrap();
5009        let replacement = first_state
5010            .signed_set_member(
5011                &owner,
5012                keys::public_key_hex(&member),
5013                None,
5014                MemberRole::Member,
5015                "replacement assignment".to_string(),
5016            )
5017            .unwrap();
5018        let replacement_state = first_state.apply(&replacement).unwrap();
5019
5020        assert_ne!(first_state.state_hash(), replacement_state.state_hash());
5021    }
5022
5023    #[test]
5024    fn membership_head_resolution_cut_must_equal_its_tip_entry_cut() {
5025        let owner = UserKeypair::generate();
5026        let entry = test_founder_entry(
5027            "head-tip-resolution-cut",
5028            &owner,
5029            "founder",
5030            membership_anchor("head-tip-resolution-cut"),
5031        );
5032        let fake = StoreMembershipConflictResolutionRef {
5033            conflict_hash: ObjectHash::digest(b"head-tip conflict"),
5034            resolver_pubkey: keys::public_key_hex(&owner),
5035            resolution_hash: ObjectHash::digest(b"head-tip resolution"),
5036            object: exact(
5037                "test/head-tip-resolution-cut/resolution.json",
5038                b"head-tip resolution",
5039            ),
5040        };
5041        let head = exact_head_with_resolutions(&entry, &owner, vec![fake]);
5042
5043        assert!(matches!(
5044            MembershipChain::from_entries_with_coords_and_heads(
5045                vec![(entry.coord(), entry)],
5046                vec![head],
5047            ),
5048            Err(MembershipError::MissingConflictHeads)
5049        ));
5050    }
5051
5052    #[test]
5053    fn membership_entry_rejects_unsorted_or_duplicate_resolution_dependencies() {
5054        let owner = UserKeypair::generate();
5055        let founder = test_founder_entry(
5056            "entry-resolution-cut",
5057            &owner,
5058            "founder",
5059            membership_anchor("entry-resolution-cut"),
5060        );
5061        let chain = MembershipChain::from_entries(vec![founder]).unwrap();
5062        let entry = chain
5063            .signed_set_member_in_stream(
5064                &owner,
5065                stream(1),
5066                keys::public_key_hex(&UserKeypair::generate()),
5067                None,
5068                MemberRole::Member,
5069                "member".to_string(),
5070            )
5071            .unwrap();
5072        let mut refs = [b"first".as_slice(), b"second".as_slice()]
5073            .into_iter()
5074            .map(|label| StoreMembershipConflictResolutionRef {
5075                conflict_hash: ObjectHash::digest(label),
5076                resolver_pubkey: keys::public_key_hex(&owner),
5077                resolution_hash: ObjectHash::digest(&[label, b" resolution"].concat()),
5078                object: exact(
5079                    format!(
5080                        "test/entry-resolution-cut/{}.json",
5081                        String::from_utf8_lossy(label)
5082                    ),
5083                    label,
5084                ),
5085            })
5086            .collect::<Vec<_>>();
5087        refs.sort();
5088
5089        let mut unsorted = entry.clone();
5090        unsorted.resolution_dependencies = refs.iter().rev().cloned().collect();
5091        sign_membership_entry(&mut unsorted, &owner);
5092        assert!(!verify_membership_entry(&unsorted));
5093
5094        let mut duplicate = entry;
5095        duplicate.resolution_dependencies = vec![refs[0].clone(), refs[0].clone()];
5096        sign_membership_entry(&mut duplicate, &owner);
5097        assert!(!verify_membership_entry(&duplicate));
5098    }
5099}