Skip to main content

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 p2panda_core::traits::OperationId;
8use petgraph::algo::has_path_connecting;
9use petgraph::graphmap::DiGraphMap;
10use petgraph::visit::{Dfs, Reversed};
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/// Split a set of operations into concurrent, predecessors and successors.
85pub fn split_bubble<OP>(
86    graph: &DiGraphMap<OP, ()>,
87    bubble: &HashSet<OP>,
88    target: OP,
89) -> (HashSet<OP>, HashSet<OP>, Vec<OP>)
90where
91    OP: OperationId + Ord,
92{
93    // Get all successors.
94    let mut concurrent = bubble.clone();
95    let mut successors = Vec::new();
96    let mut dfs = Dfs::new(&graph, target);
97    while let Some(id) = dfs.next(&graph) {
98        concurrent.remove(&id);
99        successors.push(id);
100    }
101
102    // Get all predecessors.
103    let mut predecessors = HashSet::new();
104    let reversed = Reversed(graph);
105    let mut dfs_rev = Dfs::new(&reversed, target);
106    while let Some(id) = dfs_rev.next(&reversed) {
107        concurrent.remove(&id);
108        predecessors.insert(id);
109    }
110
111    (concurrent, predecessors, successors)
112}
113
114/// Return `true` if a linear path exists in the graph between `from` and `to`.
115///
116/// This indicates whether `to` is a successor of `from`.
117pub fn has_path<OP>(graph: &DiGraphMap<OP, ()>, from: OP, to: OP) -> bool
118where
119    OP: OperationId + Ord,
120{
121    from != to && has_path_connecting(graph, from, to, None)
122}
123
124/// Return `true` if no linear path exists between `a` and `b` in either direction.
125///
126/// This indicates whether or not the given operations occurred concurrently.
127pub fn is_concurrent<OP>(graph: &DiGraphMap<OP, ()>, a: OP, b: OP) -> bool
128where
129    OP: OperationId + Ord,
130{
131    a != b && !has_path(graph, a, b) && !has_path(graph, b, a)
132}
133
134#[cfg(test)]
135mod tests {
136    use std::collections::HashSet;
137
138    use petgraph::{graph::DiGraph, prelude::DiGraphMap};
139
140    use crate::graph::concurrent_bubbles;
141
142    #[test]
143    fn test_linear_chain_no_concurrency() {
144        let mut graph = DiGraphMap::new();
145        graph.add_edge(1, 2, ());
146        graph.add_edge(2, 3, ());
147        graph.add_edge(3, 4, ());
148
149        let bubbles = concurrent_bubbles(&graph);
150        assert!(bubbles.is_empty());
151    }
152
153    #[test]
154    fn test_bubble() {
155        let mut graph = DiGraphMap::new();
156        graph.add_edge(1, 2, ());
157        graph.add_edge(1, 3, ());
158        graph.add_edge(2, 4, ());
159        graph.add_edge(3, 4, ());
160
161        let bubbles = concurrent_bubbles(&graph);
162
163        // 2 and 3 are concurrent.
164        assert_eq!(bubbles.len(), 1);
165        let expected: HashSet<_> = [2, 3].into_iter().collect();
166        assert_eq!(bubbles[0], expected);
167    }
168
169    #[test]
170    fn test_two_bubbles() {
171        let mut graph = DiGraphMap::new();
172        // Bubble 1: 1 → 2, 1 → 3, 2 → 4, 3 → 4
173        graph.add_edge(1, 2, ());
174        graph.add_edge(1, 3, ());
175        graph.add_edge(2, 4, ());
176        graph.add_edge(3, 4, ());
177        // Bubble 2: 4 → 5, 4 → 6, 5 → 7, 6 → 7
178        graph.add_edge(4, 5, ());
179        graph.add_edge(4, 6, ());
180        graph.add_edge(5, 7, ());
181        graph.add_edge(6, 7, ());
182
183        let bubbles = concurrent_bubbles(&graph);
184        assert_eq!(bubbles.len(), 2);
185
186        let b1: HashSet<_> = [2, 3].into_iter().collect();
187        let b2: HashSet<_> = [5, 6].into_iter().collect();
188
189        assert!(bubbles.contains(&b1));
190        assert!(bubbles.contains(&b2));
191    }
192
193    #[test]
194    fn complex_bubble() {
195        //       A
196        //     /   \
197        //    B     C
198        //   / \     \
199        //  D   E     F
200        //   \ /     /
201        //    G     H
202        //     \   /
203        //       I
204        //       |
205        //       J
206
207        let mut graph = DiGraph::new();
208
209        // Add nodes A–M.
210        let a = graph.add_node("A"); // 0
211        let b = graph.add_node("B"); // 1
212        let c = graph.add_node("C"); // 2
213        let d = graph.add_node("D"); // 3
214        let e = graph.add_node("E"); // 4
215        let f = graph.add_node("F"); // 5
216        let g = graph.add_node("G"); // 6
217        let h = graph.add_node("H"); // 7
218        let i = graph.add_node("I"); // 8
219        let j = graph.add_node("J"); // 9
220
221        // Add edges.
222        graph.extend_with_edges([
223            (a, b),
224            (a, c),
225            (b, d),
226            (b, e),
227            (d, g),
228            (e, g),
229            (c, f),
230            (f, h),
231            (h, i),
232            (g, i),
233            (i, j),
234        ]);
235
236        let graph_map = DiGraphMap::from_graph(graph);
237        let concurrent_bubbles = concurrent_bubbles(&graph_map);
238
239        assert_eq!(concurrent_bubbles.len(), 1);
240        let bubble = concurrent_bubbles.first().unwrap();
241        for id in &["B", "C", "D", "E", "F", "G", "H"] {
242            assert!(bubble.contains(id));
243        }
244    }
245}