1use super::entry::store_membership_anchor_stream;
2use super::*;
3
4impl MembershipChain {
5 pub fn from_entries_with_coords_and_heads_and_provider_admin(
6 entries: Vec<(MembershipCoord, MembershipEntry)>,
7 heads: Vec<(MembershipHeadRef, AuthorHead)>,
8 provider_admin: crate::provider::ProviderAdminState,
9 ) -> Result<Self, MembershipError> {
10 let expected_store = entries
11 .first()
12 .map(|(_, entry)| entry.store_id.as_str())
13 .ok_or(MembershipError::EmptyChain)?;
14 if heads.iter().any(|(reference, head)| {
15 reference.head_hash != head.head_hash()
16 || head.store_id != expected_store
17 || entries
18 .iter()
19 .find(|(coord, _)| *coord == head.entry_coord())
20 .is_none()
21 }) {
22 return Err(MembershipError::MissingExactHeads);
23 }
24 Self::from_entries_with_coords_and_head_refs(
25 entries,
26 heads.into_iter().map(|(reference, _)| reference).collect(),
27 provider_admin,
28 )
29 }
30
31 fn from_entries_with_coords_and_head_refs(
32 entries: Vec<(MembershipCoord, MembershipEntry)>,
33 head_refs: Vec<MembershipHeadRef>,
34 provider_admin_genesis: crate::provider::ProviderAdminState,
35 ) -> Result<Self, MembershipError> {
36 if entries.is_empty() {
37 return Err(MembershipError::EmptyChain);
38 }
39 for (index, (_, entry)) in entries.iter().enumerate() {
40 Self::validate_entry_authenticity(index, entry)?;
41 }
42 let (coords, entries): (Vec<_>, Vec<_>) = entries.into_iter().unzip();
43 let (included, resolved) = Self::reduce(&entries, &coords, &provider_admin_genesis)?;
44 Ok(Self {
45 entries,
46 coords,
47 included,
48 resolved,
49 head_refs,
50 provider_admin_genesis,
51 })
52 }
53
54 pub fn entries(&self) -> &[MembershipEntry] {
55 &self.entries
56 }
57
58 pub fn resolved(&self) -> &ResolvedStoreMembership {
59 &self.resolved
60 }
61
62 pub fn head_refs(&self) -> &[MembershipHeadRef] {
63 &self.head_refs
64 }
65
66 pub fn head_ref_for_stream(
67 &self,
68 author: &str,
69 grant: &MembershipGrantId,
70 stream_id: AuthorStreamId,
71 ) -> Option<&MembershipHeadRef> {
72 self.head_refs.iter().find(|reference| {
73 reference.coord.author_pubkey == author
74 && reference.coord.author_owner_grant == *grant
75 && reference.coord.stream_id == stream_id
76 })
77 }
78
79 pub fn covers_heads(&self, heads: &[MembershipHeadRef]) -> bool {
88 heads.iter().all(|covered| {
89 self.head_ref_for_stream(
90 &covered.coord.author_pubkey,
91 &covered.coord.author_owner_grant,
92 covered.coord.stream_id,
93 )
94 .is_some_and(|current| {
95 current.coord.seq > covered.coord.seq
96 || (current.coord.seq == covered.coord.seq && current == covered)
97 })
98 })
99 }
100
101 pub fn membership_anchor(&self, grant: &MembershipGrantId) -> Option<&GrantStreamAnchor> {
102 self.entries.iter().find_map(|entry| match &entry.change {
103 StoreAuthorityChange::Founder {
104 owner_grant_id,
105 membership,
106 ..
107 } if owner_grant_id == grant => Some(membership),
108 StoreAuthorityChange::SetMember {
109 grant_id,
110 membership: Some(membership),
111 ..
112 } if grant_id == grant => Some(membership),
113 _ => None,
114 })
115 }
116
117 pub fn membership_stream_id(&self, grant: &MembershipGrantId) -> Option<AuthorStreamId> {
118 let record = self.resolved().grants.get(grant)?.record();
119 let founder = self.founder_coord()?;
120 if founder.author_owner_grant == *grant {
121 return Some(founder.stream_id);
124 }
125 store_membership_anchor_stream(&record.member_pubkey, grant, self.membership_anchor(grant)?)
126 }
127
128 pub fn activated_membership_streams(&self) -> Vec<(MembershipStreamKey, GrantStreamAnchor)> {
129 let mut streams = self
130 .resolved()
131 .grants
132 .iter()
133 .filter_map(|(grant, state)| {
134 let record = state.record();
135 let anchor = self.membership_anchor(grant)?.clone();
136 let stream_id = self.membership_stream_id(grant)?;
137 Some((
138 MembershipStreamKey {
139 author_pubkey: record.member_pubkey.clone(),
140 author_owner_grant: grant.clone(),
141 stream_id,
142 },
143 anchor,
144 ))
145 })
146 .collect::<BTreeMap<_, _>>();
147 let included = &self.included;
148 for (coord, entry) in self.entries_with_coords() {
149 if !included.contains(coord) {
150 continue;
151 }
152 let (owner_pubkey, grant, anchor, stream_id) = match &entry.change {
153 StoreAuthorityChange::Founder {
154 owner_pubkey,
155 owner_grant_id,
156 membership,
157 ..
158 } => (owner_pubkey, owner_grant_id, membership, entry.stream_id),
159 StoreAuthorityChange::SetMember {
160 user_pubkey,
161 role: StoreMembershipRoleGrant::Owner { .. },
162 grant_id,
163 membership: Some(membership),
164 ..
165 } => (
166 user_pubkey,
167 grant_id,
168 membership,
169 store_membership_anchor_stream(user_pubkey, grant_id, membership)
170 .expect("validated Owner grant has a Store membership stream anchor"),
171 ),
172 _ => continue,
173 };
174 streams.insert(
175 MembershipStreamKey {
176 author_pubkey: owner_pubkey.clone(),
177 author_owner_grant: grant.clone(),
178 stream_id,
179 },
180 anchor.clone(),
181 );
182 }
183 streams.into_iter().collect()
184 }
185
186 pub fn activate_head_ref(
187 &mut self,
188 reference: MembershipHeadRef,
189 ) -> Result<(), MembershipError> {
190 if !self.coords.contains(&reference.coord) {
191 return Err(MembershipError::MissingExactHeads);
192 }
193 let stream = reference.coord.stream_key();
194 self.head_refs
195 .retain(|current| current.coord.stream_key() != stream);
196 self.head_refs.push(reference);
197 self.head_refs.sort();
198 Ok(())
199 }
200
201 pub(crate) fn entries_with_coords(
202 &self,
203 ) -> impl Iterator<Item = (&MembershipCoord, &MembershipEntry)> {
204 self.coords.iter().zip(self.entries.iter())
205 }
206
207 pub fn store_id(&self) -> Option<&str> {
208 self.entries.first().map(|entry| entry.store_id.as_str())
209 }
210
211 pub fn founder_coord(&self) -> Option<&MembershipCoord> {
212 self.entries_with_coords().find_map(|(coord, entry)| {
213 matches!(entry.change, StoreAuthorityChange::Founder { .. }).then_some(coord)
214 })
215 }
216
217 pub(crate) fn founder_entry(&self) -> Option<&MembershipEntry> {
218 self.entries
219 .iter()
220 .find(|entry| matches!(entry.change, StoreAuthorityChange::Founder { .. }))
221 }
222
223 pub fn founder_pubkey(&self) -> Option<&str> {
224 self.founder_entry().and_then(|entry| match &entry.change {
225 StoreAuthorityChange::Founder { owner_pubkey, .. } => Some(owner_pubkey.as_str()),
226 StoreAuthorityChange::SetMember { .. }
227 | StoreAuthorityChange::RemoveMember { .. }
228 | StoreAuthorityChange::DeviceRegistrationActivation { .. }
229 | StoreAuthorityChange::DeviceExclusionProposal { .. }
230 | StoreAuthorityChange::DeviceExclusionOutcome { .. }
231 | StoreAuthorityChange::ProviderAdmin => None,
232 })
233 }
234
235 pub fn is_founded_by(&self, owner_pubkey: &str) -> bool {
236 self.founder_pubkey() == Some(owner_pubkey)
237 }
238
239 pub fn add_entry(&mut self, entry: MembershipEntry) -> Result<(), MembershipError> {
240 self.add_entry_at(entry.coord(), entry)
241 }
242
243 pub fn add_entry_at(
244 &mut self,
245 coord: MembershipCoord,
246 entry: MembershipEntry,
247 ) -> Result<(), MembershipError> {
248 Self::validate_entry_authenticity(self.entries.len(), &entry)?;
249 self.entries.push(entry);
250 self.coords.push(coord);
251 match Self::reduce(&self.entries, &self.coords, &self.provider_admin_genesis) {
252 Ok((included, resolved)) => {
253 self.included = included;
254 self.resolved = resolved;
255 }
256 Err(error) => {
257 self.entries.pop();
258 self.coords.pop();
259 return Err(error);
260 }
261 }
262 Ok(())
263 }
264
265 pub fn with_exact_entry(&self, entry: &MembershipEntry) -> Result<Self, MembershipError> {
266 let coord = entry.coord();
267 if let Some((_, stored)) = self
268 .entries_with_coords()
269 .find(|(stored_coord, _)| **stored_coord == coord)
270 {
271 if stored != entry {
272 return Err(MembershipError::ExactEntryMismatch {
273 coord: Box::new(coord),
274 });
275 }
276 return Ok(self.clone());
277 }
278 let mut chain = self.clone();
279 chain.add_entry_at(coord, entry.clone())?;
280 Ok(chain)
281 }
282
283 pub fn contains_coord(&self, expected: &MembershipCoord) -> bool {
284 self.coords.iter().any(|coord| coord == expected)
285 }
286
287 pub fn effectively_contains_coord(&self, expected: &MembershipCoord) -> bool {
288 self.included.contains(expected)
289 }
290
291 pub fn device_registration_activations(
292 &self,
293 ) -> impl Iterator<
294 Item = (
295 &MembershipCoord,
296 &crate::store_commit::ActivatedStoreDeviceRegistrationRef,
297 ),
298 > {
299 self.entries_with_coords().filter_map(|(coord, entry)| {
300 let StoreAuthorityChange::DeviceRegistrationActivation { registration } = &entry.change
301 else {
302 return None;
303 };
304 self.included
305 .contains(coord)
306 .then_some((coord, registration))
307 })
308 }
309
310 pub(crate) fn contains_member_history(&self, pubkey: &str) -> bool {
311 self.resolved()
312 .grants
313 .values()
314 .any(|state| state.record().member_pubkey == pubkey)
315 }
316
317 pub fn reusable_author_streams(
318 &self,
319 author_pubkey: &str,
320 grant: &MembershipGrantId,
321 ) -> BTreeSet<AuthorStreamId> {
322 self.effective_frontier()
323 .into_iter()
324 .filter(|coord| {
325 coord.author_pubkey == author_pubkey
326 && coord.author_owner_grant == *grant
327 && self.raw_stream_tip(author_pubkey, grant, coord.stream_id)
328 == Some(coord.clone())
329 })
330 .map(|coord| coord.stream_id)
331 .collect()
332 }
333
334 pub fn effective_frontier(&self) -> Vec<MembershipCoord> {
336 causal_grants::stream_frontier(
337 self.coords
338 .iter()
339 .filter(|coord| self.included.contains(*coord))
340 .cloned(),
341 )
342 }
343
344 pub fn causally_includes(&self, predecessor: &MembershipChain) -> bool {
345 predecessor.included.is_subset(&self.included)
346 }
347
348 pub(crate) fn stream_tip(
349 &self,
350 author_pubkey: &str,
351 grant: &MembershipGrantId,
352 stream_id: AuthorStreamId,
353 ) -> Option<MembershipCoord> {
354 self.effective_frontier().into_iter().find(|coord| {
355 coord.author_pubkey == author_pubkey
356 && coord.author_owner_grant == *grant
357 && coord.stream_id == stream_id
358 })
359 }
360
361 pub(crate) fn raw_stream_tip(
362 &self,
363 author_pubkey: &str,
364 grant: &MembershipGrantId,
365 stream_id: AuthorStreamId,
366 ) -> Option<MembershipCoord> {
367 self.coords
368 .iter()
369 .filter(|coord| {
370 coord.author_pubkey == author_pubkey
371 && coord.author_owner_grant == *grant
372 && coord.stream_id == stream_id
373 })
374 .max_by_key(|coord| coord.seq)
375 .cloned()
376 }
377
378 pub(crate) fn next_member_grant_id_in_stream(
379 &self,
380 signer: &UserKeypair,
381 stream_id: AuthorStreamId,
382 user_pubkey: &str,
383 ) -> Result<MembershipGrantId, MembershipError> {
384 let author = keys::public_key_hex(signer);
385 let author_grant = self
386 .active_owner_grant(&author)
387 .ok_or_else(|| MembershipError::SignerIsNotOwner(author.clone()))?;
388 let (seq, _) = self.next_stream_position(&author, &author_grant, stream_id)?;
389 Ok(derive_grant_id(
390 self.store_id().expect("validated chain has a store id"),
391 &author,
392 &author_grant,
393 stream_id,
394 seq,
395 user_pubkey,
396 ))
397 }
398
399 pub fn next_stream_position(
400 &self,
401 author: &str,
402 grant: &MembershipGrantId,
403 stream_id: AuthorStreamId,
404 ) -> Result<(u64, Option<ObjectHash>), MembershipError> {
405 let raw_tip = self.raw_stream_tip(author, grant, stream_id);
406 let effective_tip = self.stream_tip(author, grant, stream_id);
407 if raw_tip != effective_tip {
408 return Err(MembershipError::PrunedAuthorStream);
409 }
410 effective_tip.map_or(Ok((1, None)), |tip| {
411 tip.seq
412 .checked_add(1)
413 .map(|seq| (seq, Some(tip.entry_hash)))
414 .ok_or(MembershipError::SequenceExhausted)
415 })
416 }
417
418 fn validate_entry_authenticity(
419 index: usize,
420 entry: &MembershipEntry,
421 ) -> Result<(), MembershipError> {
422 if verify_membership_entry(entry) {
423 Ok(())
424 } else {
425 Err(MembershipError::InvalidSignature(index))
426 }
427 }
428
429 fn reduce(
430 entries: &[MembershipEntry],
431 coords: &[MembershipCoord],
432 provider_admin_genesis: &crate::provider::ProviderAdminState,
433 ) -> Result<(BTreeSet<MembershipCoord>, ResolvedStoreMembership), MembershipError> {
434 let expected_store = entries
435 .first()
436 .ok_or(MembershipError::EmptyChain)?
437 .store_id
438 .clone();
439 if expected_store.is_empty() {
440 return Err(MembershipError::InvalidFounder);
441 }
442
443 for (index, (coord, entry)) in coords.iter().zip(entries).enumerate() {
444 if entry.require_version().is_err() {
445 return Err(MembershipError::UnsupportedVersion(index));
446 }
447 if entry.store_id != expected_store {
448 return Err(MembershipError::StoreMismatch {
449 index,
450 expected: expected_store.clone(),
451 actual: entry.store_id.clone(),
452 });
453 }
454 let actual = entry.coord();
455 if *coord != actual {
456 return Err(MembershipError::CoordinateMismatch {
457 index,
458 expected: Box::new(coord.clone()),
459 actual: Box::new(actual),
460 });
461 }
462 if !entry
463 .dependencies
464 .windows(2)
465 .all(|pair| pair[0].stream_key() < pair[1].stream_key())
466 {
467 return Err(MembershipError::NonCanonicalDependencyFrontier { index });
468 }
469 let (barriers, retirement_device_state) = match &entry.change {
470 StoreAuthorityChange::SetMember {
471 user_pubkey,
472 role,
473 grant_id,
474 replaces,
475 membership,
476 retirement_barriers,
477 retirement_device_state,
478 ..
479 } => {
480 let valid_owner_origin = match role {
481 StoreMembershipRoleGrant::Owner {
482 recovery: OwnerRecoveryAnchorRef::Promotion { acceptance },
483 } => {
484 let request = &acceptance.request;
485 let anchors_match =
486 Some(&acceptance.anchors.membership) == membership.as_ref();
487 let finalization_matches = request.finalization.author_stream
488 == entry.stream_id
489 && request.finalization.seq == entry.seq
490 && request.finalization.previous_hash == entry.previous_hash;
491 request.member_pubkey == *user_pubkey
492 && replaces.len() == 1
493 && replaces.contains(&request.member_grant)
494 && request.intended_owner_grant == *grant_id
495 && request.promoter_owner_grant == entry.author_owner_grant
496 && anchors_match
497 && finalization_matches
498 }
499 StoreMembershipRoleGrant::Owner { .. } => false,
500 StoreMembershipRoleGrant::Member | StoreMembershipRoleGrant::Follower => {
501 membership.is_none()
502 }
503 };
504 if role.is_owner()
505 != membership.as_ref().is_some_and(|anchor| {
506 store_membership_anchor_stream(user_pubkey, grant_id, anchor).is_some()
507 })
508 || !valid_owner_origin
509 {
510 return Err(MembershipError::InvalidOwnerMembershipAnchor(index));
511 }
512 (retirement_barriers, retirement_device_state)
513 }
514 StoreAuthorityChange::RemoveMember {
515 retirement_barriers,
516 retirement_device_state,
517 ..
518 } => (retirement_barriers, retirement_device_state),
519 StoreAuthorityChange::ProviderAdmin => {
520 let Some(crate::provider::ProviderAdminMembershipChange {
521 owner_barriers, ..
522 }) = &entry.provider_admin
523 else {
524 return Err(MembershipError::InvalidProviderAdminChange(index));
525 };
526 if owner_barriers.values().any(|barrier| {
527 !barrier
528 .observed_streams
529 .windows(2)
530 .all(|pair| pair[0].stream_key() < pair[1].stream_key())
531 }) {
532 return Err(MembershipError::InvalidProviderAdminChange(index));
533 }
534 continue;
535 }
536 StoreAuthorityChange::DeviceRegistrationActivation { .. }
537 | StoreAuthorityChange::DeviceExclusionProposal { .. }
538 | StoreAuthorityChange::DeviceExclusionOutcome { .. } => {
539 if entry.provider_admin.is_some() {
540 return Err(MembershipError::InvalidProviderAdminChange(index));
541 }
542 continue;
543 }
544 StoreAuthorityChange::Founder { .. } => continue,
545 };
546 if entry.provider_admin.is_some() {
547 return Err(MembershipError::InvalidProviderAdminChange(index));
548 }
549 let owner_recoveries = barriers
550 .values()
551 .filter_map(|barrier| match barrier {
552 MergeMembershipGrantRetirementBarrier::Owner { barrier } => {
553 Some(&barrier.recovery)
554 }
555 MergeMembershipGrantRetirementBarrier::NonOwner { .. } => None,
556 })
557 .collect::<Vec<_>>();
558 match (owner_recoveries.is_empty(), retirement_device_state) {
559 (true, None) => {}
560 (false, Some(state))
561 if owner_recoveries
562 .iter()
563 .all(|cursor| state.recovery().binary_search(cursor).is_ok()) => {}
564 (true, Some(_)) => return Err(MembershipError::UnexpectedOwnerRecoveryState),
565 (false, None | Some(_)) => return Err(MembershipError::MissingOwnerRecoveryState),
566 }
567 if let Some((grant, _)) = barriers.iter().find(|(_, barrier)| {
568 !barrier
569 .author_streams()
570 .observed_streams
571 .windows(2)
572 .all(|pair| pair[0].stream_key() < pair[1].stream_key())
573 }) {
574 return Err(MembershipError::InvalidOwnerRevocationBarrier {
575 index,
576 grant: grant.clone(),
577 });
578 }
579 }
580
581 let founders = entries
582 .iter()
583 .filter_map(|entry| {
584 let StoreAuthorityChange::Founder {
585 owner_pubkey,
586 owner_grant_id,
587 ..
588 } = &entry.change
589 else {
590 return None;
591 };
592 Some((entry, owner_pubkey, owner_grant_id))
593 })
594 .collect::<Vec<_>>();
595 let [(founder, owner_pubkey, owner_grant_id)] = founders.as_slice() else {
596 return Err(MembershipError::InvalidFounder);
597 };
598 if founder.author_pubkey != **owner_pubkey
599 || founder.author_owner_grant != **owner_grant_id
600 || founder.stream_id != derive_founder_stream_id(&founder.store_id, owner_pubkey)
601 || founder.provider_admin.is_some()
602 {
603 return Err(MembershipError::InvalidFounder);
604 }
605
606 validate_provider_admin_controls(entries)?;
607 validate_membership_retirement_barriers(entries)?;
608 validate_membership_wrapped_keys(entries)?;
609 let reduced = reduce_store_membership(entries)?;
610 let provider_admin = crate::provider::ProviderAdminState::reduce_merge(
611 provider_admin_genesis,
612 entries,
613 &reduced.included,
614 )?;
615 let resolved = resolved_store_membership(&reduced, provider_admin, entries)?;
616 Ok((reduced.included, resolved))
617 }
618
619 #[cfg(any(test, feature = "test-utils"))]
620 pub fn from_entries(entries: Vec<MembershipEntry>) -> Result<Self, MembershipError> {
621 let provider_admin = test_provider_admin_genesis(&entries)?;
622 Self::from_entries_with_coords_and_provider_admin(
623 entries
624 .into_iter()
625 .map(|entry| (entry.coord(), entry))
626 .collect(),
627 provider_admin,
628 )
629 }
630
631 #[cfg(test)]
632 pub(crate) fn from_entries_with_coords_and_heads(
633 entries: Vec<(MembershipCoord, MembershipEntry)>,
634 heads: Vec<(MembershipHeadRef, AuthorHead)>,
635 ) -> Result<Self, MembershipError> {
636 let values = entries
637 .iter()
638 .map(|(_, entry)| entry.clone())
639 .collect::<Vec<_>>();
640 let provider_admin = test_provider_admin_genesis(&values)?;
641 Self::from_entries_with_coords_and_heads_and_provider_admin(entries, heads, provider_admin)
642 }
643
644 #[cfg(any(test, feature = "test-utils"))]
645 pub(crate) fn from_entries_with_coords_and_provider_admin(
646 entries: Vec<(MembershipCoord, MembershipEntry)>,
647 provider_admin: crate::provider::ProviderAdminState,
648 ) -> Result<Self, MembershipError> {
649 Self::from_entries_with_coords_and_head_refs(entries, Vec::new(), provider_admin)
650 }
651
652 #[cfg(test)]
655 pub fn author_heads(&self) -> Vec<MembershipCoord> {
656 causal_grants::stream_frontier(self.coords.iter().cloned())
657 }
658}