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