1use std::collections::{BTreeMap, BTreeSet};
4use std::fmt::{self, Debug};
5use std::str::FromStr;
6
7use serde::{Deserialize, Serialize};
8
9use super::store_commit::ObjectHash;
10
11const MAX_CYCLIC_REVOCATION_SOURCES: usize = 12;
12
13mod fixed_sets;
14mod reduction;
15
16pub(crate) use reduction::{reduce, reduce_from_checkpoint};
17
18pub fn canonical_ready_checkpoint<'a, K: Clone + Ord + 'a>(
19 mut dependencies: impl Iterator<Item = (&'a K, &'a BTreeSet<K>)>,
20 applied: &BTreeSet<K>,
21) -> Option<K> {
22 dependencies
23 .find(|(_, required)| required.is_subset(applied))
24 .map(|(checkpoint, _)| checkpoint.clone())
25}
26
27pub(crate) fn merge_checkpoint_evidence<K, V, T, C>(
28 merged_grants: &mut BTreeMap<K, GrantState<V, T>>,
29 merged_included: &mut BTreeSet<C>,
30 grants: &BTreeMap<K, GrantState<V, T>>,
31 included: &BTreeSet<C>,
32) -> bool
33where
34 K: Clone + Ord,
35 V: Clone + Eq,
36 T: Clone + Ord,
37 C: Clone + Ord,
38{
39 for (grant, state) in grants {
40 match merged_grants.entry(grant.clone()) {
41 std::collections::btree_map::Entry::Vacant(entry) => {
42 entry.insert(state.clone());
43 }
44 std::collections::btree_map::Entry::Occupied(mut entry) => {
45 if !entry.get_mut().merge(state) {
46 return false;
47 }
48 }
49 }
50 }
51 merged_included.extend(included.iter().cloned());
52 true
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
56#[serde(transparent)]
57pub struct GrantRetirements<T: Ord>(BTreeSet<T>);
58
59impl<T: Ord> GrantRetirements<T> {
60 pub fn new(retirement: T) -> Self {
61 Self(BTreeSet::from([retirement]))
62 }
63
64 pub fn insert(&mut self, retirement: T) -> bool {
65 self.0.insert(retirement)
66 }
67
68 pub fn extend(&mut self, retirements: impl IntoIterator<Item = T>) {
69 self.0.extend(retirements);
70 }
71
72 pub fn iter(&self) -> impl Iterator<Item = &T> {
73 self.0.iter()
74 }
75
76 pub fn contains(&self, retirement: &T) -> bool {
77 self.0.contains(retirement)
78 }
79
80 pub fn as_set(&self) -> &BTreeSet<T> {
81 &self.0
82 }
83}
84
85impl<'de, T> Deserialize<'de> for GrantRetirements<T>
86where
87 T: Deserialize<'de> + Ord,
88{
89 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
90 where
91 D: serde::Deserializer<'de>,
92 {
93 let retirements = BTreeSet::deserialize(deserializer)?;
94 if retirements.is_empty() {
95 return Err(serde::de::Error::custom(
96 "grant retirement set cannot be empty",
97 ));
98 }
99 Ok(Self(retirements))
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(rename_all = "snake_case", deny_unknown_fields)]
105pub enum GrantState<R, T: Ord> {
106 Active {
107 record: R,
108 },
109 Tombstoned {
110 record: R,
111 retirements: GrantRetirements<T>,
112 },
113}
114
115impl<R, T: Ord> GrantState<R, T> {
116 pub fn record(&self) -> &R {
117 match self {
118 Self::Active { record } | Self::Tombstoned { record, .. } => record,
119 }
120 }
121
122 pub fn active(&self) -> Option<&R> {
123 match self {
124 Self::Active { record } => Some(record),
125 Self::Tombstoned { .. } => None,
126 }
127 }
128
129 pub fn retirements(&self) -> Option<&GrantRetirements<T>> {
130 match self {
131 Self::Active { .. } => None,
132 Self::Tombstoned { retirements, .. } => Some(retirements),
133 }
134 }
135}
136
137impl<R: Clone + Eq, T: Clone + Ord> GrantState<R, T> {
138 fn merge(&mut self, other: &Self) -> bool {
139 if self.record() != other.record() {
140 return false;
141 }
142 let retirements = match (&*self, other) {
143 (Self::Active { .. }, Self::Active { .. }) => return true,
144 (Self::Tombstoned { retirements, .. }, Self::Active { .. }) => retirements.clone(),
145 (Self::Active { .. }, Self::Tombstoned { retirements, .. }) => retirements.clone(),
146 (
147 Self::Tombstoned {
148 retirements: current,
149 ..
150 },
151 Self::Tombstoned { retirements, .. },
152 ) => {
153 let mut merged = current.clone();
154 merged.extend(retirements.iter().cloned());
155 merged
156 }
157 };
158 *self = Self::Tombstoned {
159 record: self.record().clone(),
160 retirements,
161 };
162 true
163 }
164}
165
166pub(crate) type CausalGrants<R, T> = BTreeMap<MembershipGrantId, GrantState<R, T>>;
168
169pub fn active_grants<R, T: Ord>(
171 grants: &CausalGrants<R, T>,
172) -> impl Iterator<Item = (&MembershipGrantId, &R)> {
173 grants
174 .iter()
175 .filter_map(|(grant, state)| state.active().map(|record| (grant, record)))
176}
177
178pub(crate) fn has_active_owner<R, T: Ord>(
181 grants: &CausalGrants<R, T>,
182 is_owner: impl Fn(&R) -> bool,
183) -> bool {
184 active_grants(grants).any(|(_, record)| is_owner(record))
185}
186
187pub(crate) fn has_concurrent_assignments<R, T: Ord>(
190 grants: &CausalGrants<R, T>,
191 member_pubkey: impl Fn(&R) -> &str,
192) -> bool {
193 let mut members = BTreeSet::new();
194 active_grants(grants).any(|(_, record)| !members.insert(member_pubkey(record).to_string()))
195}
196
197pub(crate) fn merge_conflict_grant_state<R: Clone + Eq, T: Clone + Ord>(
201 grants: &mut CausalGrants<R, T>,
202 grant: MembershipGrantId,
203 state: &GrantState<R, T>,
204) -> Result<(), ()> {
205 match grants.entry(grant) {
206 std::collections::btree_map::Entry::Vacant(entry) => {
207 entry.insert(state.clone());
208 Ok(())
209 }
210 std::collections::btree_map::Entry::Occupied(mut entry) => {
211 if entry.get().record() != state.record() {
212 return Err(());
213 }
214 if !entry.get_mut().merge(state) {
215 return Err(());
216 }
217 Ok(())
218 }
219 }
220}
221
222pub(crate) fn tombstone_conflict_grant<R: Clone + Eq, T: Clone + Ord>(
226 grants: &mut CausalGrants<R, T>,
227 grant: &MembershipGrantId,
228 record: &R,
229 retirements: &GrantRetirements<T>,
230) -> Result<(), ()> {
231 match grants.entry(grant.clone()) {
232 std::collections::btree_map::Entry::Vacant(entry) => {
233 entry.insert(GrantState::Tombstoned {
234 record: record.clone(),
235 retirements: retirements.clone(),
236 });
237 }
238 std::collections::btree_map::Entry::Occupied(mut entry) => {
239 if entry.get().record() != record {
240 return Err(());
241 }
242 let mut merged = entry
243 .get()
244 .retirements()
245 .cloned()
246 .unwrap_or_else(|| retirements.clone());
247 merged.extend(retirements.iter().cloned());
248 *entry.get_mut() = GrantState::Tombstoned {
249 record: record.clone(),
250 retirements: merged,
251 };
252 }
253 }
254 Ok(())
255}
256
257pub(crate) fn resolve_conflict_grants<'branch, R, T, E>(
267 branches: impl Iterator<Item = &'branch CausalGrants<R, T>> + Clone,
268 selected: impl Iterator<Item = &'branch CausalGrants<R, T>> + Clone,
269 retired_owner_grants: &BTreeSet<MembershipGrantId>,
270 resolution_retirements: impl Fn(&MembershipGrantId) -> Result<GrantRetirements<T>, E>,
271 invalid: impl Fn() -> E,
272) -> Result<CausalGrants<R, T>, E>
273where
274 R: Clone + Eq + 'branch,
275 T: Clone + Ord + 'branch,
276{
277 let mut selected = selected;
278 let first = selected.next().ok_or_else(&invalid)?;
279 let others = selected;
280 let mut resolved = active_grants(first)
281 .filter(|(grant, _)| !retired_owner_grants.contains(*grant))
282 .filter(|(grant, record)| {
283 others
284 .clone()
285 .all(|branch| branch.get(*grant).and_then(GrantState::active) == Some(*record))
286 })
287 .map(|(grant, record)| {
288 (
289 grant.clone(),
290 GrantState::Active {
291 record: record.clone(),
292 },
293 )
294 })
295 .collect::<CausalGrants<R, T>>();
296 for branch in branches.clone() {
297 for (grant, state) in branch {
298 if state.retirements().is_some() {
299 merge_conflict_grant_state(&mut resolved, grant.clone(), state)
300 .map_err(|()| invalid())?;
301 }
302 }
303 }
304 for branch in branches {
305 for (grant, record) in active_grants(branch) {
306 if resolved.get(grant).and_then(GrantState::active).is_some() {
307 continue;
308 }
309 let retirements = resolution_retirements(grant)?;
310 tombstone_conflict_grant(&mut resolved, grant, record, &retirements)
311 .map_err(|()| invalid())?;
312 }
313 }
314 Ok(resolved)
315}
316
317pub(crate) fn try_map_grant_state<C, A, R, T, E>(
318 state: &GrantState<GrantRecord<C, A>, CausalGrantRetirement<C>>,
319 record: R,
320 checkpoint_retirements: Option<&GrantRetirements<T>>,
321 missing_checkpoint_retirements: impl Fn() -> E,
322 map_entry: impl Fn(&C, Option<&OwnerGrantBarrier<C>>) -> Result<T, E>,
323) -> Result<GrantState<R, T>, E>
324where
325 C: CausalCoordinate,
326 A: CausalAssignment,
327 T: Clone + Ord,
328{
329 let GrantState::Tombstoned { retirements, .. } = state else {
330 return Ok(GrantState::Active { record });
331 };
332 let mut mapped: Option<GrantRetirements<T>> = None;
333 let mut add = |retirement| match &mut mapped {
334 Some(mapped) => {
335 mapped.insert(retirement);
336 }
337 None => mapped = Some(GrantRetirements::new(retirement)),
338 };
339 for retirement in retirements.iter() {
340 match retirement {
341 CausalGrantRetirement::Entry {
342 coord,
343 owner_barrier,
344 } => add(map_entry(coord, owner_barrier.as_ref())?),
345 CausalGrantRetirement::Checkpoint => {
346 let checkpoint_retirements =
347 checkpoint_retirements.ok_or_else(&missing_checkpoint_retirements)?;
348 for retirement in checkpoint_retirements.iter().cloned() {
349 add(retirement);
350 }
351 }
352 }
353 }
354 Ok(GrantState::Tombstoned {
355 record,
356 retirements: mapped.expect("causal tombstone has retirement evidence"),
357 })
358}
359
360pub(crate) fn merge_checkpoint_frontier<C: CausalCoordinate>(
361 merged: &mut BTreeMap<C::StreamKey, C>,
362 frontier: &[C],
363) -> bool {
364 for coord in frontier {
365 let stream = coord.stream_key();
366 match merged.get(&stream) {
367 Some(existing) if existing.seq() == coord.seq() && existing != coord => return false,
368 Some(existing) if existing.seq() >= coord.seq() => {}
369 _ => {
370 merged.insert(stream, coord.clone());
371 }
372 }
373 }
374 true
375}
376
377#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
379pub struct AuthorStreamId([u8; 32]);
380
381#[derive(Debug, thiserror::Error)]
382pub enum AuthorStreamIdParseError {
383 #[error("author stream id must be exactly 64 lowercase hexadecimal characters: {value:?}")]
384 InvalidFormat { value: String },
385 #[error("decode author stream id: {0}")]
386 Hex(#[source] hex::FromHexError),
387 #[error("author stream id has the wrong byte length")]
388 WrongLength,
389}
390
391impl PartialEq for AuthorStreamIdParseError {
392 fn eq(&self, other: &Self) -> bool {
393 match (self, other) {
394 (Self::InvalidFormat { value: left }, Self::InvalidFormat { value: right }) => {
395 left == right
396 }
397 (Self::Hex(left), Self::Hex(right)) => left.to_string() == right.to_string(),
398 (Self::WrongLength, Self::WrongLength) => true,
399 _ => false,
400 }
401 }
402}
403
404impl Eq for AuthorStreamIdParseError {}
405
406impl AuthorStreamId {
407 pub fn from_digest(digest: ObjectHash) -> Self {
408 Self(*digest.as_bytes())
409 }
410
411 #[cfg(any(test, feature = "test-utils"))]
412 pub fn from_bytes(bytes: [u8; 32]) -> Self {
413 Self(bytes)
414 }
415}
416
417impl fmt::Display for AuthorStreamId {
418 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
419 formatter.write_str(&hex::encode(self.0))
420 }
421}
422
423impl FromStr for AuthorStreamId {
424 type Err = AuthorStreamIdParseError;
425
426 fn from_str(value: &str) -> Result<Self, Self::Err> {
427 if value.len() != 64
428 || value
429 .bytes()
430 .any(|byte| !byte.is_ascii_digit() && !(b'a'..=b'f').contains(&byte))
431 {
432 return Err(AuthorStreamIdParseError::InvalidFormat {
433 value: value.to_string(),
434 });
435 }
436 let bytes = hex::decode(value)
437 .map_err(AuthorStreamIdParseError::Hex)?
438 .try_into()
439 .map_err(|_| AuthorStreamIdParseError::WrongLength)?;
440 Ok(Self(bytes))
441 }
442}
443
444impl Serialize for AuthorStreamId {
445 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
446 where
447 S: serde::Serializer,
448 {
449 serializer.serialize_str(&self.to_string())
450 }
451}
452
453impl<'de> Deserialize<'de> for AuthorStreamId {
454 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
455 where
456 D: serde::Deserializer<'de>,
457 {
458 String::deserialize(deserializer)?
459 .parse()
460 .map_err(serde::de::Error::custom)
461 }
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
465#[serde(transparent)]
466pub struct MembershipGrantId(pub ObjectHash);
467
468impl MembershipGrantId {
469 #[cfg(any(test, feature = "test-utils"))]
470 pub fn from_test_label(label: &str) -> Self {
471 Self(ObjectHash::digest(label.as_bytes()))
472 }
473}
474
475impl fmt::Display for MembershipGrantId {
476 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
477 fmt::Display::fmt(&self.0, formatter)
478 }
479}
480
481pub(crate) trait CausalCoordinate: Clone + Debug + Eq + Ord {
482 type StreamKey: Clone + Debug + Eq + Ord;
483
484 fn stream_key(&self) -> Self::StreamKey;
485 fn author_pubkey(&self) -> &str;
486 fn author_owner_grant(&self) -> &MembershipGrantId;
487 fn seq(&self) -> u64;
488 fn entry_hash(&self) -> ObjectHash;
489}
490
491pub(crate) fn stream_frontier<C: CausalCoordinate>(coords: impl IntoIterator<Item = C>) -> Vec<C> {
496 let mut heads = BTreeMap::<C::StreamKey, C>::new();
497 for coord in coords {
498 match heads.entry(coord.stream_key()) {
499 std::collections::btree_map::Entry::Vacant(slot) => {
500 slot.insert(coord);
501 }
502 std::collections::btree_map::Entry::Occupied(mut slot) => {
503 if coord.seq() > slot.get().seq() {
504 slot.insert(coord);
505 }
506 }
507 }
508 }
509 heads.into_values().collect()
510}
511
512pub(crate) fn common_frontier<C: CausalCoordinate>(frontiers: &[&[C]]) -> Vec<C> {
513 let Some(first) = frontiers.first() else {
514 return Vec::new();
515 };
516 let others = frontiers[1..]
517 .iter()
518 .map(|frontier| {
519 frontier
520 .iter()
521 .map(|coord| (coord.stream_key(), coord))
522 .collect::<BTreeMap<_, _>>()
523 })
524 .collect::<Vec<_>>();
525 first
526 .iter()
527 .filter_map(|coord| {
528 let stream = coord.stream_key();
529 let mut common = coord.clone();
530 for frontier in &others {
531 let candidate = frontier.get(&stream)?;
532 if candidate.seq() < common.seq() {
533 common = (*candidate).clone();
534 }
535 }
536 Some(common)
537 })
538 .collect()
539}
540
541pub(crate) fn selected_branch_frontier<'branch, C, R, E>(
547 resolutions: &[R],
548 branch_frontier: impl Fn(&R) -> Result<&'branch [C], E>,
549) -> Result<Vec<C>, E>
550where
551 C: CausalCoordinate + 'branch,
552{
553 let selected = resolutions
554 .iter()
555 .map(branch_frontier)
556 .collect::<Result<Vec<_>, _>>()?;
557 Ok(common_frontier(&selected))
558}
559
560pub(crate) fn starts_author_stream<K: Eq>(
563 seq: u64,
564 previous_hash: Option<ObjectHash>,
565 own_stream: &K,
566 dependency_streams: impl IntoIterator<Item = K>,
567) -> bool {
568 seq == 1
569 && previous_hash.is_none()
570 && dependency_streams
571 .into_iter()
572 .all(|stream| stream != *own_stream)
573}
574
575pub(crate) fn author_stream_position_is_valid<K: Eq>(
578 seq: u64,
579 previous_hash: Option<ObjectHash>,
580 own_stream: &K,
581 dependency_streams: impl IntoIterator<Item = K>,
582) -> bool {
583 match (seq, previous_hash) {
584 (1, _) => starts_author_stream(seq, previous_hash, own_stream, dependency_streams),
585 (0, _) | (_, None) => false,
586 (_, Some(_)) => true,
587 }
588}
589
590pub(crate) trait CausalAssignment: Clone + Debug + Eq {
591 fn is_owner(&self) -> bool;
592}
593
594pub(crate) trait CausalHistoryEntry {
597 type Coord: CausalCoordinate;
598
599 fn coord(&self) -> Self::Coord;
600 fn dependencies(&self) -> &[Self::Coord];
601}
602
603pub(crate) fn entries_beyond_checkpoint<'a, E: CausalHistoryEntry>(
609 entries: &'a [E],
610 raw_heads: &[E::Coord],
611) -> impl Iterator<Item = &'a E> {
612 let heads = raw_heads
613 .iter()
614 .map(|coord| (coord.stream_key(), coord.seq()))
615 .collect::<BTreeMap<_, _>>();
616 entries.iter().filter(move |entry| {
617 let coord = entry.coord();
618 heads
619 .get(&coord.stream_key())
620 .is_none_or(|head| coord.seq() > *head)
621 })
622}
623
624pub(crate) fn map_checkpoint_grants<R, T: Ord, MappedRecord, MappedRetirement: Ord>(
631 grants: &CausalGrants<R, T>,
632 record: impl Fn(&R) -> MappedRecord,
633 retirement: impl Fn() -> MappedRetirement,
634) -> CausalGrants<MappedRecord, MappedRetirement> {
635 grants
636 .iter()
637 .map(|(grant, state)| {
638 let record = record(state.record());
639 (
640 grant.clone(),
641 match state {
642 GrantState::Active { .. } => GrantState::Active { record },
643 GrantState::Tombstoned { .. } => GrantState::Tombstoned {
644 record,
645 retirements: GrantRetirements::new(retirement()),
646 },
647 },
648 )
649 })
650 .collect()
651}
652
653pub(crate) fn checkpoint_resolution_refs<R: Clone + Ord>(
657 previous: Option<&[R]>,
658 applied: impl IntoIterator<Item = R>,
659) -> Vec<R> {
660 let mut references = previous.map_or_else(Vec::new, <[R]>::to_vec);
661 references.extend(applied);
662 references.sort();
663 references.dedup();
664 references
665}
666
667pub(crate) fn history_closure<E: CausalHistoryEntry>(
672 entries: &[E],
673 frontier: &[E::Coord],
674) -> BTreeSet<E::Coord> {
675 let by_coord = entries
676 .iter()
677 .map(|entry| (entry.coord(), entry))
678 .collect::<BTreeMap<_, _>>();
679 let mut pending = frontier.iter().cloned().collect::<BTreeSet<_>>();
680 let mut included = BTreeSet::new();
681 while let Some(coord) = pending.pop_first() {
682 if !included.insert(coord.clone()) {
683 continue;
684 }
685 if let Some(entry) = by_coord.get(&coord) {
686 pending.extend(entry.dependencies().iter().cloned());
687 }
688 }
689 included
690}
691
692#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
693pub(crate) struct OwnerGrantBarrier<C: CausalCoordinate> {
694 pub observed_streams: BTreeMap<C::StreamKey, C>,
695}
696
697impl<C: CausalCoordinate> OwnerGrantBarrier<C> {
698 pub(crate) fn from_observed(coords: impl IntoIterator<Item = C>) -> Self {
700 Self {
701 observed_streams: coords
702 .into_iter()
703 .map(|coord| (coord.stream_key(), coord))
704 .collect(),
705 }
706 }
707
708 fn includes(&self, coord: &C) -> bool {
709 self.observed_streams
710 .get(&coord.stream_key())
711 .is_some_and(|barrier| coord.seq() <= barrier.seq())
712 }
713}
714
715#[derive(Debug, Clone, PartialEq, Eq)]
716pub(crate) enum CausalChange<C: CausalCoordinate, A: CausalAssignment> {
717 Founder {
718 member_pubkey: String,
719 grant_id: MembershipGrantId,
720 assignment: A,
721 },
722 SetMember {
723 member_pubkey: String,
724 assignment: A,
725 grant_id: MembershipGrantId,
726 replaces: BTreeSet<MembershipGrantId>,
727 owner_barriers: BTreeMap<MembershipGrantId, OwnerGrantBarrier<C>>,
728 },
729 RemoveMember {
730 member_pubkey: String,
731 removes: BTreeSet<MembershipGrantId>,
732 owner_barriers: BTreeMap<MembershipGrantId, OwnerGrantBarrier<C>>,
733 },
734 Control,
735 ResolutionActivation,
736}
737
738type RemovedGrants<'a, C> = (
739 &'a BTreeSet<MembershipGrantId>,
740 &'a BTreeMap<MembershipGrantId, OwnerGrantBarrier<C>>,
741);
742
743impl<C: CausalCoordinate, A: CausalAssignment> CausalChange<C, A> {
744 fn removed(&self) -> Option<RemovedGrants<'_, C>> {
745 match self {
746 Self::SetMember {
747 replaces,
748 owner_barriers,
749 ..
750 } => Some((replaces, owner_barriers)),
751 Self::RemoveMember {
752 removes,
753 owner_barriers,
754 ..
755 } => Some((removes, owner_barriers)),
756 Self::Founder { .. } | Self::Control | Self::ResolutionActivation => None,
757 }
758 }
759
760 fn removes_grant(&self, grant: &MembershipGrantId) -> bool {
761 self.removed()
762 .is_some_and(|(removed, _)| removed.contains(grant))
763 }
764}
765
766#[derive(Debug, Clone)]
767pub(crate) struct CausalEntry<C: CausalCoordinate, A: CausalAssignment> {
768 pub coord: C,
769 pub previous_hash: Option<ObjectHash>,
770 pub dependencies: BTreeMap<C::StreamKey, C>,
771 pub change: CausalChange<C, A>,
772}
773
774#[derive(Debug, Clone, PartialEq, Eq)]
775pub(crate) struct GrantRecord<C: CausalCoordinate, A: CausalAssignment> {
776 pub member_pubkey: String,
777 pub assignment: A,
778 pub creation: CausalGrantCreation<C>,
779}
780
781#[derive(Debug, Clone, PartialEq, Eq)]
782pub(crate) enum CausalGrantCreation<C: CausalCoordinate> {
783 Entry(C),
784 Checkpoint,
785}
786
787#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
788pub(crate) enum CausalGrantRetirement<C: CausalCoordinate> {
789 Entry {
790 coord: C,
791 owner_barrier: Option<OwnerGrantBarrier<C>>,
792 },
793 Checkpoint,
794}
795
796#[derive(Debug, Clone, PartialEq, Eq)]
797pub(crate) struct CausalSeedGrant<A: CausalAssignment> {
798 pub member_pubkey: String,
799 pub assignment: A,
800}
801
802#[derive(Debug, Clone, PartialEq, Eq)]
803pub(crate) struct ReducedGrants<C: CausalCoordinate, A: CausalAssignment> {
804 pub grants:
805 BTreeMap<MembershipGrantId, GrantState<GrantRecord<C, A>, CausalGrantRetirement<C>>>,
806 pub included: BTreeSet<C>,
807}
808
809#[derive(Debug, Clone, PartialEq, Eq)]
810pub(crate) struct CausalGrantBranch<C: CausalCoordinate, A: CausalAssignment> {
811 pub raw_heads: Vec<C>,
812 pub effective_frontier: Vec<C>,
813 pub reduced: ReducedGrants<C, A>,
814}
815
816#[derive(Debug, Clone, PartialEq, Eq)]
817pub(crate) enum CausalGrantConflict<C: CausalCoordinate, A: CausalAssignment> {
818 ConcurrentMemberAssignments {
819 raw_heads: Vec<C>,
820 effective_frontier: Vec<C>,
821 member_pubkey: String,
822 conflicting_grants: BTreeMap<MembershipGrantId, GrantRecord<C, A>>,
823 uncontested_grants: BTreeMap<MembershipGrantId, GrantRecord<C, A>>,
824 reduced: ReducedGrants<C, A>,
825 },
826 RevocationCycle {
827 raw_heads: Vec<C>,
828 cyclic_sources: Vec<C>,
829 involved_owner_grants: BTreeSet<MembershipGrantId>,
830 maximal_valid_branches: Vec<CausalGrantBranch<C, A>>,
831 },
832}
833
834#[derive(Debug, Clone, PartialEq, Eq)]
835pub(crate) enum CausalGrantStatus<C: CausalCoordinate, A: CausalAssignment> {
836 Resolved(ReducedGrants<C, A>),
837 Conflict(CausalGrantConflict<C, A>),
838}
839
840impl<C: CausalCoordinate, A: CausalAssignment> ReducedGrants<C, A> {
841 pub(crate) fn active_grant(&self, grant: &MembershipGrantId) -> Option<&GrantRecord<C, A>> {
842 self.grants.get(grant).and_then(GrantState::active)
843 }
844
845 pub(crate) fn includes_coord(&self, coord: &C) -> bool {
846 self.included.contains(coord)
847 }
848}
849
850#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
851pub(crate) enum CausalGrantError<C: CausalCoordinate> {
852 #[error("causal assignment history is empty")]
853 Empty,
854 #[error("stream {stream:?} has conflicting entries at sequence {seq}")]
855 ConflictingSequence { stream: C::StreamKey, seq: u64 },
856 #[error("stream {stream:?} is missing sequence {seq}")]
857 MissingSequence { stream: C::StreamKey, seq: u64 },
858 #[error("entry {index} has predecessor {actual:?}, expected {expected:?}")]
859 BrokenStreamLink {
860 index: usize,
861 expected: Option<ObjectHash>,
862 actual: Option<ObjectHash>,
863 },
864 #[error("entry {index} does not carry its exact own-stream dependency")]
865 MissingOwnDependency { index: usize },
866 #[error("entry {index} has a dependency under the wrong stream key")]
867 DependencyStreamMismatch { index: usize },
868 #[error("entry {index} depends on missing coordinate {dependency:?}")]
869 MissingDependency { index: usize, dependency: C },
870 #[error("causal assignment dependency graph contains a cycle")]
871 DependencyCycle,
872 #[error("causal assignment founder is invalid")]
873 InvalidFounder,
874 #[error("entry {index} author is not active under Owner grant {grant}")]
875 AuthorGrantInactive {
876 index: usize,
877 grant: MembershipGrantId,
878 },
879 #[error("entry {index} creates already-defined grant {grant}")]
880 DuplicateGrant {
881 index: usize,
882 grant: MembershipGrantId,
883 },
884 #[error("entry {index} replaces or removes grant {grant} owned by another member")]
885 GrantOwnerMismatch {
886 index: usize,
887 grant: MembershipGrantId,
888 },
889 #[error("entry {index} does not name the exact active grants for member {member_pubkey}")]
890 GrantSetMismatch { index: usize, member_pubkey: String },
891 #[error("entry {index} removes no exact grants")]
892 EmptyRemoval { index: usize },
893 #[error("entry {index} removes Owner grant {grant} without its exact observed frontier")]
894 MissingOwnerRevocationBarrier {
895 index: usize,
896 grant: MembershipGrantId,
897 },
898 #[error("entry {index} carries an invalid frontier for Owner grant {grant}")]
899 InvalidOwnerRevocationBarrier {
900 index: usize,
901 grant: MembershipGrantId,
902 },
903 #[error("causal assignment history leaves no active Owner")]
904 NoActiveOwner,
905 #[error(
906 "causal assignment revocation cycle has {sources} sources, exceeding the protocol limit of {maximum}"
907 )]
908 RevocationCycleTooWide { sources: usize, maximum: usize },
909}
910
911#[cfg(test)]
912mod tests;
913
914pub(crate) fn exact_head_refs<H, C>(
918 head_refs: &[H],
919 coords: &[C],
920 coord_of: impl Fn(&H) -> &C,
921) -> Option<Vec<H>>
922where
923 H: Clone + Ord,
924 C: Clone + Ord,
925{
926 let expected = coords
927 .iter()
928 .cloned()
929 .collect::<std::collections::BTreeSet<_>>();
930 let mut references = head_refs
931 .iter()
932 .filter(|reference| expected.contains(coord_of(reference)))
933 .cloned()
934 .collect::<Vec<_>>();
935 let actual = references
936 .iter()
937 .map(|reference| coord_of(reference).clone())
938 .collect::<std::collections::BTreeSet<_>>();
939 if expected != actual || references.len() != expected.len() {
940 return None;
941 }
942 references.sort();
943 Some(references)
944}