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