Skip to main content

quorum_set/quorum/
quorum_intersection.rs

1use std::collections::BTreeSet;
2
3#[cfg(doc)]
4use crate::QuorumTree;
5use crate::quorum::QuorumSet;
6
7/// Relation between quorum sets whose quorums always intersect.
8///
9/// Quorum sets A and B have **quorum intersection**, written `A ~ B`, when:
10/// `∀ qᵢ ∈ A, ∀ qⱼ ∈ B: qᵢ ∩ qⱼ != ø`.
11/// In words, every quorum in A intersects every quorum in B. Consensus
12/// protocols use this relation to make membership changes without losing
13/// overlap between old and new decisions.
14///
15/// The relation is symmetric, and both universal quantifiers are load-bearing:
16/// weakening either one to "some quorum" (∃) breaks safety, because a reader
17/// or a candidate cannot know which quorum is "the right one" — the overlap
18/// must hold for every quorum it may legally assemble. E.g. for write quorums
19/// `{a,b}, {b,c}, {a,c}` and read quorums `{b,c}, {x,y}`: every write quorum
20/// intersects *some* read quorum, yet a read using `{x,y}` observes no
21/// committed write.
22///
23/// In a Raft-style membership change, quorum intersection is one safety
24/// requirement. The protocol also has to prevent an old, smaller candidate
25/// from being elected during the transition.
26pub trait QuorumIntersection<Other>
27where
28    Self: QuorumSet,
29    Other: QuorumSet<Id = Self::Id>,
30{
31    /// Return whether every quorum of this quorum set intersects every quorum
32    /// of the other quorum set.
33    ///
34    /// - `Some(true)`: the check proved that every quorum pair intersects.
35    /// - `Some(false)`: the check proved that some quorum pair is disjoint.
36    /// - `None`: the check proved neither. An implementation may use a condition that is sufficient
37    ///   but not necessary, so failing that condition tells the caller nothing about the true
38    ///   relation.
39    ///
40    /// Callers can act on `Some(true)`. On `Some(false)` and on `None` they
41    /// must take the unconditionally safe path, e.g. bridge through a joint
42    /// config built by [`QuorumBridge`]. [`verify_intersection`] computes the
43    /// exact relation in exponential time.
44    fn intersects_with(&self, other: &Other) -> Option<bool>;
45}
46
47/// Builds an intermediate quorum set that has [`QuorumIntersection`] with both
48/// the source and the target quorum set.
49pub trait QuorumBridge<Other>
50where
51    Self: QuorumSet,
52    Other: QuorumSet<Id = Self::Id>,
53{
54    /// Build a quorum set `X` so that `self ~ X ~ other`, where `~` is the
55    /// [`QuorumIntersection`] relation.
56    ///
57    /// Then `X` is the intermediate quorum set when changing membership from
58    /// `self` to `other`.
59    ///
60    /// E.g.(`cᵢcⱼ` is a joint of `cᵢ` and `cⱼ`):
61    /// - `c₁.bridge_to(c₁)`   returns `c₁`
62    /// - `c₁.bridge_to(c₂)`   returns `c₁c₂`
63    /// - `c₁c₂.bridge_to(c₂)` returns `c₂`
64    /// - `c₁c₂.bridge_to(c₁)` returns `c₁`
65    /// - `c₁c₂.bridge_to(c₃)` returns `c₂c₃`
66    fn bridge_to(&self, other: Other) -> Self;
67}
68
69/// Exhaustively check the [`QuorumIntersection`] relation between two quorum
70/// sets.
71///
72/// Returns `true` iff every quorum of `a` intersects every quorum of `b`.
73/// Unlike [`QuorumIntersection::intersects_with`], which may answer `None`,
74/// this check is exact for any two [`QuorumSet`] implementations, e.g. a read
75/// [`QuorumTree`] against a write [`QuorumTree`].
76///
77/// It tests every split of the combined voter IDs `U` into `(S, U ∖ S)`: a
78/// disjoint quorum pair exists iff for some split, `S` is a quorum of `a` and
79/// `U ∖ S` is a quorum of `b`. Quorum sets are upward-closed, so "some quorum
80/// of `b` fits inside `U ∖ S`" is the same as "`U ∖ S` is itself a quorum of
81/// `b`". This argument relies on two [`QuorumSet`] rules: implementations are
82/// upward-closed, and IDs outside [`QuorumSet::ids`] never affect
83/// [`QuorumSet::is_quorum`].
84///
85/// The check runs `2^n` quorum evaluations for `n` distinct IDs. It is meant
86/// for validating small configurations and as a test oracle.
87///
88/// # Panics
89///
90/// Panics if `a` and `b` together track 64 or more distinct IDs.
91///
92/// # Examples
93///
94/// ```
95/// use std::collections::BTreeSet;
96///
97/// use quorum_set::verify_intersection;
98///
99/// let abc = BTreeSet::from([1, 2, 3]);
100/// let de = BTreeSet::from([4, 5]);
101///
102/// // Majorities of one voter set always intersect each other.
103/// assert!(verify_intersection(&abc, &abc));
104/// // Majorities of disjoint voter sets never intersect.
105/// assert!(!verify_intersection(&abc, &de));
106/// ```
107pub fn verify_intersection<A, B>(a: &A, b: &B) -> bool
108where
109    A: QuorumSet,
110    B: QuorumSet<Id = A::Id>,
111    A::Id: Ord,
112{
113    let universe: BTreeSet<A::Id> = a.ids().chain(b.ids()).collect();
114    let universe: Vec<A::Id> = universe.into_iter().collect();
115    let n = universe.len();
116    assert!(
117        n < 64,
118        "verify_intersection enumerates 2^n subsets; {n} distinct ids do not fit a u64 mask"
119    );
120
121    for mask in 0u64..(1u64 << n) {
122        let selected = universe
123            .iter()
124            .enumerate()
125            .filter(move |&(i, _)| mask & (1u64 << i) != 0)
126            .map(|(_, id)| id);
127        let complement = universe
128            .iter()
129            .enumerate()
130            .filter(move |&(i, _)| mask & (1u64 << i) == 0)
131            .map(|(_, id)| id);
132        if a.is_quorum(selected) && b.is_quorum(complement) {
133            return false;
134        }
135    }
136    true
137}