Skip to main content

sim_lib_discrete_graph/
certificate.rs

1//! Verifiable certificates for MST and shortest-path results.
2//!
3//! Every certificate-producing algorithm ships a verifier that re-checks the
4//! result from scratch and never trusts the producer. `verify_mst` checks edge
5//! count, acyclicity, spanning connectivity, the recorded total weight, and
6//! optimality (the cycle property). `verify_shortest_paths` checks predecessor
7//! consistency and edge relaxation.
8
9use crate::error::GraphError;
10use crate::graph::Graph;
11use crate::mst::{MstWeight, checked_mst_add};
12use crate::unionfind::UnionFind;
13use core::fmt::Display;
14
15/// A spanning tree result: tree edge ids (ascending) and the total weight.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct SpanningTree<W> {
18    /// Edge ids forming the tree, sorted ascending.
19    pub edges: Vec<usize>,
20    /// Sum of the tree edge weights.
21    pub total_weight: W,
22}
23
24/// A compact, checkable MST witness.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct MstCertificate {
27    /// Edge ids claimed to form the minimum spanning tree.
28    pub edge_ids: Vec<usize>,
29    /// The claimed total weight, rendered via `Display`.
30    pub total_weight_repr: String,
31}
32
33impl<W: Display> SpanningTree<W> {
34    /// The certificate corresponding to this spanning tree.
35    pub fn certificate(&self) -> MstCertificate {
36        MstCertificate {
37            edge_ids: self.edges.clone(),
38            total_weight_repr: format!("{}", self.total_weight),
39        }
40    }
41}
42
43fn invalid(msg: &str) -> GraphError {
44    GraphError::CertificateInvalid(msg.to_string())
45}
46
47/// Maximum edge weight on the unique tree path from `u` to `v`, or `None` if
48/// `u == v` or they are not connected in the tree.
49fn tree_path_max<W: Ord + Clone>(
50    n: usize,
51    tree_adj: &[Vec<(usize, W)>],
52    u: usize,
53    v: usize,
54) -> Option<W> {
55    let mut max_w: Vec<Option<W>> = vec![None; n];
56    let mut visited = vec![false; n];
57    let mut queue = std::collections::VecDeque::new();
58    visited[u] = true;
59    queue.push_back(u);
60    while let Some(x) = queue.pop_front() {
61        if x == v {
62            return max_w[x].clone();
63        }
64        for (y, w) in &tree_adj[x] {
65            if !visited[*y] {
66                visited[*y] = true;
67                let cand = match &max_w[x] {
68                    Some(m) if m >= w => m.clone(),
69                    _ => w.clone(),
70                };
71                max_w[*y] = Some(cand);
72                queue.push_back(*y);
73            }
74        }
75    }
76    None
77}
78
79/// Verify that `cert` describes a minimum spanning tree of `graph`.
80pub fn verify_mst<N, W>(graph: &Graph<N, W>, cert: &MstCertificate) -> Result<(), GraphError>
81where
82    W: MstWeight + Display,
83{
84    graph.validate()?;
85    if graph.is_directed() {
86        return Err(GraphError::WrongGraphKind(
87            "MST certificate requires an undirected graph".to_string(),
88        ));
89    }
90    let n = graph.node_count();
91
92    // Resolve tree edges; reject unknown ids and self-loops.
93    let mut tree_edges = Vec::with_capacity(cert.edge_ids.len());
94    for &id in &cert.edge_ids {
95        let e = graph
96            .edges
97            .get(id)
98            .ok_or_else(|| invalid("unknown edge id"))?;
99        if e.is_self_loop() {
100            return Err(invalid("self-loop in tree"));
101        }
102        tree_edges.push(e);
103    }
104
105    // Edge count: a spanning tree of n nodes has exactly n-1 edges (0 for n<=1).
106    let expected = n.saturating_sub(1);
107    if tree_edges.len() != expected {
108        return Err(invalid("wrong edge count for a spanning tree"));
109    }
110
111    // Acyclic + spanning via union-find.
112    let mut uf = UnionFind::new(n);
113    for e in &tree_edges {
114        if !uf.union(e.source, e.target) {
115            return Err(invalid("tree contains a cycle"));
116        }
117    }
118    if n > 0 {
119        let root = uf.find(0);
120        for i in 1..n {
121            if uf.find(i) != root {
122                return Err(invalid("tree does not span the graph"));
123            }
124        }
125    }
126
127    // Recorded total weight must match the recomputed sum.
128    let mut total = W::default();
129    for e in &tree_edges {
130        total = checked_mst_add(&total, &e.weight)?;
131    }
132    if format!("{total}") != cert.total_weight_repr {
133        return Err(invalid("total weight mismatch"));
134    }
135
136    // Optimality (cycle property): no non-tree edge may be cheaper than the
137    // heaviest edge on the tree path between its endpoints.
138    let tree_ids: std::collections::HashSet<usize> = cert.edge_ids.iter().copied().collect();
139    let mut tree_adj: Vec<Vec<(usize, W)>> = vec![Vec::new(); n];
140    for e in &tree_edges {
141        tree_adj[e.source].push((e.target, e.weight.clone()));
142        tree_adj[e.target].push((e.source, e.weight.clone()));
143    }
144    for e in &graph.edges {
145        if tree_ids.contains(&e.id) || e.is_self_loop() {
146            continue;
147        }
148        if let Some(path_max) = tree_path_max(n, &tree_adj, e.source, e.target)
149            && e.weight < path_max
150        {
151            return Err(invalid("not minimal: a cheaper spanning tree exists"));
152        }
153    }
154    Ok(())
155}
156
157/// A shortest-path tree witness from a single source.
158#[derive(Debug, Clone, PartialEq, Eq)]
159pub struct ShortestPathCertificate {
160    /// The source node.
161    pub source: usize,
162    /// `predecessors[v]` is the node `v` was reached from (`None` for the source
163    /// and for unreachable nodes).
164    pub predecessors: Vec<Option<usize>>,
165}
166
167/// The minimum weight of a `from -> to` edge, respecting directedness.
168fn edge_weight<N>(graph: &Graph<N, i64>, from: usize, to: usize) -> Option<i64> {
169    let undirected = !graph.is_directed();
170    graph
171        .edges
172        .iter()
173        .filter(|e| {
174            (e.source == from && e.target == to)
175                || (undirected && e.source == to && e.target == from)
176        })
177        .map(|e| e.weight)
178        .min()
179}
180
181/// Recursively resolve the tree distance of `v` (memoized). `Ok(None)` means the
182/// node is not in the tree (no predecessor chain to the source).
183fn tree_dist<N>(
184    graph: &Graph<N, i64>,
185    cert: &ShortestPathCertificate,
186    v: usize,
187    memo: &mut [Option<Option<i64>>],
188    visiting: &mut [bool],
189) -> Result<Option<i64>, GraphError> {
190    if v == cert.source {
191        return Ok(Some(0));
192    }
193    if let Some(d) = memo[v] {
194        return Ok(d);
195    }
196    let result = match cert.predecessors[v] {
197        None => None,
198        Some(u) => {
199            if u >= cert.predecessors.len() {
200                return Err(invalid("predecessor out of range"));
201            }
202            if visiting[v] {
203                return Err(invalid("predecessor cycle"));
204            }
205            visiting[v] = true;
206            let du = tree_dist(graph, cert, u, memo, visiting)?;
207            visiting[v] = false;
208            match du {
209                None => return Err(invalid("predecessor points outside the tree")),
210                Some(d_u) => {
211                    let w = edge_weight(graph, u, v)
212                        .ok_or_else(|| invalid("predecessor edge missing"))?;
213                    Some(
214                        d_u.checked_add(w)
215                            .ok_or_else(|| invalid("shortest-path distance overflow"))?,
216                    )
217                }
218            }
219        }
220    };
221    memo[v] = Some(result);
222    Ok(result)
223}
224
225/// Verify that `cert` is a valid shortest-path tree of `graph` from its source.
226pub fn verify_shortest_paths<N>(
227    graph: &Graph<N, i64>,
228    cert: &ShortestPathCertificate,
229) -> Result<(), GraphError> {
230    graph.validate()?;
231    let n = graph.node_count();
232    if cert.source >= n {
233        return Err(invalid("source out of range"));
234    }
235    if cert.predecessors.len() != n {
236        return Err(invalid("predecessor length mismatch"));
237    }
238    if cert.predecessors[cert.source].is_some() {
239        return Err(invalid("source must have no predecessor"));
240    }
241    for (node, pred) in cert.predecessors.iter().enumerate() {
242        if let Some(parent) = pred
243            && *parent >= n
244        {
245            return Err(invalid(&format!(
246                "predecessor out of range for node {node}"
247            )));
248        }
249    }
250
251    let mut memo: Vec<Option<Option<i64>>> = vec![None; n];
252    let mut visiting = vec![false; n];
253    let mut dist = vec![None; n];
254    for (v, slot) in dist.iter_mut().enumerate() {
255        *slot = tree_dist(graph, cert, v, &mut memo, &mut visiting)?;
256    }
257
258    // Relaxation + completeness: every edge reachable from the source must keep
259    // dist[target] <= dist[source] + weight, and its target must be in the tree.
260    let undirected = !graph.is_directed();
261    for e in &graph.edges {
262        let mut arcs = vec![(e.source, e.target)];
263        if undirected {
264            arcs.push((e.target, e.source));
265        }
266        for (a, b) in arcs {
267            if let Some(da) = dist[a] {
268                match dist[b] {
269                    None => return Err(invalid("reachable node missing from tree")),
270                    Some(db) => {
271                        let nd = da
272                            .checked_add(e.weight)
273                            .ok_or_else(|| invalid("shortest-path relaxation overflow"))?;
274                        if db > nd {
275                            return Err(invalid("edge violates shortest-path relaxation"));
276                        }
277                    }
278                }
279            }
280        }
281    }
282    Ok(())
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use crate::edge::Directedness;
289    use crate::path::bellman_ford;
290
291    fn weighted() -> Graph<u8, i64> {
292        let mut g = Graph::with_nodes(vec![0, 1, 2, 3], Directedness::Directed);
293        g.add_edge(0, 1, 1).unwrap();
294        g.add_edge(1, 2, 2).unwrap();
295        g.add_edge(0, 2, 5).unwrap();
296        g.add_edge(2, 3, 1).unwrap();
297        g
298    }
299
300    #[test]
301    fn valid_shortest_path_cert_verifies() {
302        let g = weighted();
303        let (res, _) = bellman_ford(&g, 0).unwrap();
304        let cert = ShortestPathCertificate {
305            source: 0,
306            predecessors: res.predecessors,
307        };
308        assert!(verify_shortest_paths(&g, &cert).is_ok());
309    }
310
311    #[test]
312    fn tampered_shortest_path_cert_rejected() {
313        let g = weighted();
314        let (res, _) = bellman_ford(&g, 0).unwrap();
315        let mut preds = res.predecessors;
316        // Claim node 2 was reached directly from 0 (dist 5) -- but 0->1->2 (3)
317        // is shorter, so relaxation of edge 1->2 must fail.
318        preds[2] = Some(0);
319        let cert = ShortestPathCertificate {
320            source: 0,
321            predecessors: preds,
322        };
323        assert!(verify_shortest_paths(&g, &cert).is_err());
324    }
325
326    #[test]
327    fn predecessor_out_of_range_is_invalid_certificate() {
328        let mut g: Graph<u8, i64> = Graph::with_nodes(vec![0, 1], Directedness::Directed);
329        g.add_edge(0, 1, 1).unwrap();
330        let cert = ShortestPathCertificate {
331            source: 0,
332            predecessors: vec![None, Some(2)],
333        };
334
335        assert!(matches!(
336            verify_shortest_paths(&g, &cert),
337            Err(GraphError::CertificateInvalid(_))
338        ));
339    }
340
341    #[test]
342    fn shortest_path_certificate_rejects_distance_overflow() {
343        let mut max_graph: Graph<u8, i64> =
344            Graph::with_nodes(vec![0, 1, 2], Directedness::Directed);
345        max_graph.add_edge(0, 1, i64::MAX).unwrap();
346        max_graph.add_edge(1, 2, 1).unwrap();
347        let max_cert = ShortestPathCertificate {
348            source: 0,
349            predecessors: vec![None, Some(0), Some(1)],
350        };
351        assert!(matches!(
352            verify_shortest_paths(&max_graph, &max_cert),
353            Err(GraphError::CertificateInvalid(_))
354        ));
355
356        let mut min_graph: Graph<u8, i64> =
357            Graph::with_nodes(vec![0, 1, 2], Directedness::Directed);
358        min_graph.add_edge(0, 1, i64::MIN).unwrap();
359        min_graph.add_edge(1, 2, -1).unwrap();
360        let min_cert = ShortestPathCertificate {
361            source: 0,
362            predecessors: vec![None, Some(0), Some(1)],
363        };
364        assert!(matches!(
365            verify_shortest_paths(&min_graph, &min_cert),
366            Err(GraphError::CertificateInvalid(_))
367        ));
368    }
369
370    #[test]
371    fn mst_certificate_rejects_directed_graph() {
372        let mut g: Graph<u8, u64> = Graph::with_nodes(vec![0, 1], Directedness::Directed);
373        g.add_edge(0, 1, 1).unwrap();
374        let cert = MstCertificate {
375            edge_ids: vec![0],
376            total_weight_repr: "1".to_string(),
377        };
378
379        assert!(matches!(
380            verify_mst(&g, &cert),
381            Err(GraphError::WrongGraphKind(_))
382        ));
383    }
384
385    #[test]
386    fn mst_certificate_rejects_total_weight_overflow() {
387        let mut g: Graph<u8, u64> = Graph::with_nodes(vec![0, 1, 2], Directedness::Undirected);
388        g.add_edge(0, 1, u64::MAX).unwrap();
389        g.add_edge(1, 2, 1).unwrap();
390        let cert = MstCertificate {
391            edge_ids: vec![0, 1],
392            total_weight_repr: "0".to_string(),
393        };
394
395        assert!(matches!(
396            verify_mst(&g, &cert),
397            Err(GraphError::WeightOverflow(_))
398        ));
399    }
400}