p2panda_auth/
graph.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! Graph functions for identifying related sets of concurrent operations.
4
5use std::collections::HashSet;
6
7use petgraph::graphmap::DiGraphMap;
8use petgraph::visit::{Dfs, Reversed};
9
10use crate::traits::OperationId;
11
12/// Recursively identify all operations concurrent with the given target operation.
13fn concurrent_bubble<OP>(
14    graph: &DiGraphMap<OP, ()>,
15    target: OP,
16    processed: &mut HashSet<OP>,
17) -> HashSet<OP>
18where
19    OP: OperationId + Ord,
20{
21    let mut bubble = HashSet::new();
22    bubble.insert(target);
23
24    concurrent_operations(graph, target)
25        .into_iter()
26        .for_each(|op| {
27            if processed.insert(op) {
28                bubble.extend(concurrent_bubble(graph, op, processed).iter())
29            }
30        });
31
32    bubble
33}
34
35/// Walk the graph and identify all sets of concurrent operations.
36pub fn concurrent_bubbles<OP>(graph: &DiGraphMap<OP, ()>) -> Vec<HashSet<OP>>
37where
38    OP: OperationId + Ord,
39{
40    let mut processed: HashSet<OP> = HashSet::new();
41    let mut bubbles = Vec::new();
42
43    graph.nodes().for_each(|target| {
44        if processed.insert(target) {
45            let bubble = concurrent_bubble(graph, target, &mut processed);
46            if bubble.len() > 1 {
47                bubbles.push(bubble)
48            }
49        }
50    });
51
52    bubbles
53}
54
55/// Return any operations concurrent with the given target operation.
56///
57/// Operations are considered concurrent if they are neither predecessors nor successors of the
58/// target operation.
59fn concurrent_operations<OP>(graph: &DiGraphMap<OP, ()>, target: OP) -> HashSet<OP>
60where
61    OP: OperationId + Ord,
62{
63    // Get all successors.
64    let mut successors = HashSet::new();
65    let mut dfs = Dfs::new(&graph, target);
66    while let Some(nx) = dfs.next(&graph) {
67        successors.insert(nx);
68    }
69
70    // Get all predecessors.
71    let mut predecessors = HashSet::new();
72    let reversed = Reversed(graph);
73    let mut dfs_rev = Dfs::new(&reversed, target);
74    while let Some(nx) = dfs_rev.next(&reversed) {
75        predecessors.insert(nx);
76    }
77
78    let relatives: HashSet<_> = successors.union(&predecessors).cloned().collect();
79
80    // Collect all operations which are not successors or predecessors.
81    graph.nodes().filter(|n| !relatives.contains(n)).collect()
82}
83
84/// Return `true` if a linear path exists in the graph between `from` and `to`.
85///
86/// This indicates whether or not the given operations occurred concurrently.
87pub fn has_path<OP>(graph: &DiGraphMap<OP, ()>, from: OP, to: OP) -> bool
88where
89    OP: OperationId + Ord,
90{
91    let mut dfs = Dfs::new(graph, from);
92    while let Some(node) = dfs.next(graph) {
93        if node == to {
94            return true;
95        }
96    }
97    false
98}
99
100#[cfg(test)]
101mod tests {
102    use std::collections::HashSet;
103
104    use petgraph::{graph::DiGraph, prelude::DiGraphMap};
105
106    use crate::graph::concurrent_bubbles;
107
108    #[test]
109    fn test_linear_chain_no_concurrency() {
110        let mut graph = DiGraphMap::new();
111        graph.add_edge(1, 2, ());
112        graph.add_edge(2, 3, ());
113        graph.add_edge(3, 4, ());
114
115        let bubbles = concurrent_bubbles(&graph);
116        assert!(bubbles.is_empty());
117    }
118
119    #[test]
120    fn test_bubble() {
121        let mut graph = DiGraphMap::new();
122        graph.add_edge(1, 2, ());
123        graph.add_edge(1, 3, ());
124        graph.add_edge(2, 4, ());
125        graph.add_edge(3, 4, ());
126
127        let bubbles = concurrent_bubbles(&graph);
128
129        // 2 and 3 are concurrent.
130        assert_eq!(bubbles.len(), 1);
131        let expected: HashSet<_> = [2, 3].into_iter().collect();
132        assert_eq!(bubbles[0], expected);
133    }
134
135    #[test]
136    fn test_two_bubbles() {
137        let mut graph = DiGraphMap::new();
138        // Bubble 1: 1 → 2, 1 → 3, 2 → 4, 3 → 4
139        graph.add_edge(1, 2, ());
140        graph.add_edge(1, 3, ());
141        graph.add_edge(2, 4, ());
142        graph.add_edge(3, 4, ());
143        // Bubble 2: 4 → 5, 4 → 6, 5 → 7, 6 → 7
144        graph.add_edge(4, 5, ());
145        graph.add_edge(4, 6, ());
146        graph.add_edge(5, 7, ());
147        graph.add_edge(6, 7, ());
148
149        let bubbles = concurrent_bubbles(&graph);
150        assert_eq!(bubbles.len(), 2);
151
152        let b1: HashSet<_> = [2, 3].into_iter().collect();
153        let b2: HashSet<_> = [5, 6].into_iter().collect();
154
155        assert!(bubbles.contains(&b1));
156        assert!(bubbles.contains(&b2));
157    }
158
159    #[test]
160    fn complex_bubble() {
161        //       A
162        //     /   \
163        //    B     C
164        //   / \     \
165        //  D   E     F
166        //   \ /     /
167        //    G     H
168        //     \   /
169        //       I
170        //       |
171        //       J
172
173        let mut graph = DiGraph::new();
174
175        // Add nodes A–M.
176        let a = graph.add_node("A"); // 0
177        let b = graph.add_node("B"); // 1
178        let c = graph.add_node("C"); // 2
179        let d = graph.add_node("D"); // 3
180        let e = graph.add_node("E"); // 4
181        let f = graph.add_node("F"); // 5
182        let g = graph.add_node("G"); // 6
183        let h = graph.add_node("H"); // 7
184        let i = graph.add_node("I"); // 8
185        let j = graph.add_node("J"); // 9
186
187        // Add edges.
188        graph.extend_with_edges(&[
189            (a, b),
190            (a, c),
191            (b, d),
192            (b, e),
193            (d, g),
194            (e, g),
195            (c, f),
196            (f, h),
197            (h, i),
198            (g, i),
199            (i, j),
200        ]);
201
202        let graph_map = DiGraphMap::from_graph(graph);
203        let concurrent_bubbles = concurrent_bubbles(&graph_map);
204
205        assert_eq!(concurrent_bubbles.len(), 1);
206        let bubble = concurrent_bubbles.first().unwrap();
207        for id in &["B", "C", "D", "E", "F", "G", "H"] {
208            assert!(bubble.contains(id));
209        }
210    }
211}