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