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