Skip to main content

quorum_set/tree/
quorum_tree.rs

1use std::cmp::Ordering;
2use std::fmt;
3use std::hash::Hash;
4use std::hash::Hasher;
5
6use super::tree_spec::QuorumTreeSpec;
7use crate::CanonicalId;
8use crate::Node;
9use crate::QuorumTreeError;
10
11mod impl_display;
12
13/// A single quorum rule represented as a tree.
14///
15/// `QuorumTree` models one side of a quorum configuration. A consensus
16/// implementation should usually build one tree for read quorums and another
17/// tree for write quorums, then ensure every read quorum intersects every write
18/// quorum.
19///
20/// A child node is selected when it is either an included node ID or a nested
21/// quorum tree whose own quorum rule is satisfied.
22///
23/// # Invariants
24///
25/// - A [`QuorumTree`] represents exactly one quorum rule.
26/// - Child nodes are unique and traversal order is deterministic according to [`Node`] ordering.
27/// - `quorum_size` is at least `1` and never exceeds the number of child nodes: construction
28///   rejects duplicate children, a zero quorum size, and unsatisfiable quorum sizes.
29/// - Equality and ordering for [`QuorumTree`] are based only on its canonical ID.
30/// - Canonical IDs are stable identifiers. [`std::fmt::Display`] is human-readable output and is
31///   not a serialization format.
32#[derive(Clone, Debug)]
33pub struct QuorumTree<ID>
34where ID: Ord
35{
36    pub(crate) spec: QuorumTreeSpec<ID>,
37
38    canonical_id: String,
39}
40
41impl<ID> QuorumTree<ID>
42where ID: Ord
43{
44    /// Builds a quorum tree from a quorum size and child nodes.
45    ///
46    /// `quorum_size` is the number of child nodes that must be selected for this
47    /// tree to be selected. A child can be a single ID or another quorum tree.
48    ///
49    /// # Errors
50    ///
51    /// - [`QuorumTreeError::DuplicateChild`]: a child was given more than once. A duplicate child
52    ///   is always an upstream bug; a tree never contains the same child twice.
53    /// - [`QuorumTreeError::UnsatisfiableQuorum`]: `quorum_size` exceeds the number of child nodes,
54    ///   so no input could satisfy the tree.
55    /// - [`QuorumTreeError::ZeroQuorumSize`]: `quorum_size` is `0`, so every input, even an empty
56    ///   one, would satisfy the tree.
57    ///
58    /// # Examples
59    ///
60    /// ```
61    /// use quorum_set::{Node, QuorumSet, QuorumTree, QuorumTreeError};
62    ///
63    /// let tree = QuorumTree::new(2, [
64    ///     Node::Id(1),
65    ///     Node::Id(2),
66    ///     Node::Id(3),
67    /// ]).unwrap();
68    ///
69    /// assert!(tree.is_quorum([1, 2].iter()));
70    /// assert!(!tree.is_quorum([1].iter()));
71    ///
72    /// let err = QuorumTree::new(1, [Node::Id(1), Node::Id(1)]).unwrap_err();
73    /// assert_eq!(
74    ///     QuorumTreeError::DuplicateChild { canonical_id: "Id=1".to_string() },
75    ///     err
76    /// );
77    /// ```
78    pub fn new(
79        quorum_size: u64,
80        nodes: impl IntoIterator<Item = Node<ID>>,
81    ) -> Result<Self, QuorumTreeError>
82    where
83        ID: CanonicalId,
84    {
85        let spec = QuorumTreeSpec::new(quorum_size, nodes)?;
86        let canonical_id = spec.canonical_id();
87        Ok(Self { spec, canonical_id })
88    }
89
90    /// Returns the number of selected child nodes required to satisfy this
91    /// tree.
92    pub fn quorum_size(&self) -> u64 {
93        self.spec.quorum_size()
94    }
95
96    /// Returns this tree's child nodes in canonical order.
97    ///
98    /// Children are unique: construction rejects duplicate child nodes.
99    pub fn children(&self) -> impl Iterator<Item = &Node<ID>> {
100        self.spec.children()
101    }
102
103    /// Returns the canonical ID of this tree.
104    ///
105    /// The canonical ID decides equality and ordering. It is computed once at
106    /// construction, so this accessor does not allocate.
107    pub fn canonical_id(&self) -> &str {
108        &self.canonical_id
109    }
110}
111
112/// Equality and ordering are decided solely by `canonical_id`.
113///
114/// `new()` is the sole constructor and `canonical_id` is the cached canonical
115/// representation of the quorum tree spec. A single string comparison keeps
116/// `BTreeSet` operations cheap; no recursive structural comparison is needed.
117impl<ID> PartialEq for QuorumTree<ID>
118where ID: Ord
119{
120    fn eq(&self, other: &Self) -> bool {
121        self.canonical_id == other.canonical_id
122    }
123}
124
125impl<ID> Eq for QuorumTree<ID> where ID: Ord {}
126
127impl<ID> PartialOrd for QuorumTree<ID>
128where ID: Ord
129{
130    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
131        Some(self.cmp(other))
132    }
133}
134
135impl<ID> Ord for QuorumTree<ID>
136where ID: Ord
137{
138    fn cmp(&self, other: &Self) -> Ordering {
139        self.canonical_id.cmp(&other.canonical_id)
140    }
141}
142
143/// Hashing is based solely on `canonical_id`, consistent with equality.
144impl<ID> Hash for QuorumTree<ID>
145where ID: Ord
146{
147    fn hash<H>(&self, state: &mut H)
148    where H: Hasher {
149        self.canonical_id.hash(state);
150    }
151}
152
153impl<ID> CanonicalId for QuorumTree<ID>
154where ID: Ord + CanonicalId
155{
156    fn fmt_canonical_id<W>(&self, f: &mut W) -> fmt::Result
157    where W: fmt::Write + ?Sized {
158        write!(f, "{}", self.canonical_id)
159    }
160}
161
162#[cfg(test)]
163mod tests {
164    use std::cmp::Ordering;
165    use std::collections::HashSet;
166
167    use crate::Node;
168    use crate::QuorumTree;
169    use crate::QuorumTreeError;
170
171    fn id(i: u64) -> Node<u64> {
172        Node::Id(i)
173    }
174
175    #[test]
176    fn test_eq_ignores_construction_order() {
177        let a = QuorumTree::new(2, [id(1), id(2), id(3)]).unwrap();
178        let b = QuorumTree::new(2, [id(3), id(1), id(2)]).unwrap();
179        let c = QuorumTree::new(3, [id(1), id(2), id(3)]).unwrap();
180
181        assert_eq!(a, b);
182        assert_eq!(a.canonical_id(), b.canonical_id());
183        assert_ne!(a, c);
184    }
185
186    #[test]
187    fn test_hash_is_consistent_with_eq() {
188        let a = QuorumTree::new(2, [id(1), id(2), id(3)]).unwrap();
189        let b = QuorumTree::new(2, [id(3), id(2), id(1)]).unwrap();
190        let c = QuorumTree::new(3, [id(1), id(2), id(3)]).unwrap();
191
192        let set: HashSet<QuorumTree<u64>> = [a.clone(), b.clone(), c.clone()].into_iter().collect();
193
194        assert_eq!(HashSet::from([a, c]), set);
195    }
196
197    #[test]
198    fn test_canonical_id_accessor() {
199        let tree = QuorumTree::new(2, [id(3), id(1), id(2)]).unwrap();
200
201        assert_eq!("2/(Id=1,Id=2,Id=3)", tree.canonical_id());
202    }
203
204    #[test]
205    fn test_quorum_size_accessor() {
206        let tree = QuorumTree::new(2, [id(1), id(2), id(3)]).unwrap();
207
208        assert_eq!(2, tree.quorum_size());
209    }
210
211    #[test]
212    fn test_ord_is_consistent_with_canonical_id() {
213        let a = QuorumTree::new(1, [id(1), id(2)]).unwrap();
214        let b = QuorumTree::new(2, [id(1), id(2)]).unwrap();
215
216        assert_eq!(Ordering::Less, a.cmp(&b));
217        assert_eq!(Some(Ordering::Less), a.partial_cmp(&b));
218        assert_eq!(
219            a.canonical_id().cmp(b.canonical_id()),
220            a.cmp(&b),
221            "ordering follows the canonical ID"
222        );
223        assert_eq!(Ordering::Equal, a.cmp(&a.clone()));
224    }
225
226    #[test]
227    fn test_children_are_sorted() {
228        let tree = QuorumTree::new(2, [id(3), id(1), id(2)]).unwrap();
229
230        assert_eq!(
231            vec![id(1), id(2), id(3)],
232            tree.children().cloned().collect::<Vec<_>>()
233        );
234    }
235
236    #[test]
237    fn test_new_rejects_duplicate_child() {
238        let err = QuorumTree::new(2, [id(1), id(2), id(2)]).unwrap_err();
239
240        assert_eq!(
241            QuorumTreeError::DuplicateChild {
242                canonical_id: "Id=2".to_string()
243            },
244            err
245        );
246
247        let sub = QuorumTree::new(1, [id(1), id(2)]).unwrap();
248        let err = QuorumTree::new(2, [Node::Subtree(sub.clone()), Node::Subtree(sub)]).unwrap_err();
249
250        assert_eq!(
251            QuorumTreeError::DuplicateChild {
252                canonical_id: "Subtree=1/(Id=1,Id=2)".to_string(),
253            },
254            err
255        );
256    }
257
258    #[test]
259    fn test_new_rejects_unsatisfiable_quorum() {
260        let err = QuorumTree::new(3, [id(1), id(2)]).unwrap_err();
261
262        assert_eq!(
263            QuorumTreeError::UnsatisfiableQuorum {
264                quorum_size: 3,
265                num_children: 2
266            },
267            err
268        );
269    }
270
271    #[test]
272    fn test_new_rejects_zero_quorum_size() {
273        let err = QuorumTree::new(0, [id(1), id(2)]).unwrap_err();
274
275        assert_eq!(QuorumTreeError::ZeroQuorumSize, err);
276    }
277}