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::unionfind::UnionFind;
12use core::fmt::Display;
13use core::ops::Add;
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: Ord + Clone + Default + Add<Output = W> + Display,
83{
84    graph.validate()?;
85    let n = graph.node_count();
86
87    // Resolve tree edges; reject unknown ids and self-loops.
88    let mut tree_edges = Vec::with_capacity(cert.edge_ids.len());
89    for &id in &cert.edge_ids {
90        let e = graph
91            .edges
92            .get(id)
93            .ok_or_else(|| invalid("unknown edge id"))?;
94        if e.is_self_loop() {
95            return Err(invalid("self-loop in tree"));
96        }
97        tree_edges.push(e);
98    }
99
100    // Edge count: a spanning tree of n nodes has exactly n-1 edges (0 for n<=1).
101    let expected = n.saturating_sub(1);
102    if tree_edges.len() != expected {
103        return Err(invalid("wrong edge count for a spanning tree"));
104    }
105
106    // Acyclic + spanning via union-find.
107    let mut uf = UnionFind::new(n);
108    for e in &tree_edges {
109        if !uf.union(e.source, e.target) {
110            return Err(invalid("tree contains a cycle"));
111        }
112    }
113    if n > 0 {
114        let root = uf.find(0);
115        for i in 1..n {
116            if uf.find(i) != root {
117                return Err(invalid("tree does not span the graph"));
118            }
119        }
120    }
121
122    // Recorded total weight must match the recomputed sum.
123    let mut total = W::default();
124    for e in &tree_edges {
125        total = total + e.weight.clone();
126    }
127    if format!("{total}") != cert.total_weight_repr {
128        return Err(invalid("total weight mismatch"));
129    }
130
131    // Optimality (cycle property): no non-tree edge may be cheaper than the
132    // heaviest edge on the tree path between its endpoints.
133    let tree_ids: std::collections::HashSet<usize> = cert.edge_ids.iter().copied().collect();
134    let mut tree_adj: Vec<Vec<(usize, W)>> = vec![Vec::new(); n];
135    for e in &tree_edges {
136        tree_adj[e.source].push((e.target, e.weight.clone()));
137        tree_adj[e.target].push((e.source, e.weight.clone()));
138    }
139    for e in &graph.edges {
140        if tree_ids.contains(&e.id) || e.is_self_loop() {
141            continue;
142        }
143        if let Some(path_max) = tree_path_max(n, &tree_adj, e.source, e.target)
144            && e.weight < path_max
145        {
146            return Err(invalid("not minimal: a cheaper spanning tree exists"));
147        }
148    }
149    Ok(())
150}
151
152/// A shortest-path tree witness from a single source.
153#[derive(Debug, Clone, PartialEq, Eq)]
154pub struct ShortestPathCertificate {
155    /// The source node.
156    pub source: usize,
157    /// `predecessors[v]` is the node `v` was reached from (`None` for the source
158    /// and for unreachable nodes).
159    pub predecessors: Vec<Option<usize>>,
160}
161
162/// The minimum weight of a `from -> to` edge, respecting directedness.
163fn edge_weight<N>(graph: &Graph<N, i64>, from: usize, to: usize) -> Option<i64> {
164    let undirected = !graph.is_directed();
165    graph
166        .edges
167        .iter()
168        .filter(|e| {
169            (e.source == from && e.target == to)
170                || (undirected && e.source == to && e.target == from)
171        })
172        .map(|e| e.weight)
173        .min()
174}
175
176/// Recursively resolve the tree distance of `v` (memoized). `Ok(None)` means the
177/// node is not in the tree (no predecessor chain to the source).
178fn tree_dist<N>(
179    graph: &Graph<N, i64>,
180    cert: &ShortestPathCertificate,
181    v: usize,
182    memo: &mut [Option<Option<i64>>],
183    visiting: &mut [bool],
184) -> Result<Option<i64>, GraphError> {
185    if v == cert.source {
186        return Ok(Some(0));
187    }
188    if let Some(d) = memo[v] {
189        return Ok(d);
190    }
191    let result = match cert.predecessors[v] {
192        None => None,
193        Some(u) => {
194            if visiting[v] {
195                return Err(invalid("predecessor cycle"));
196            }
197            visiting[v] = true;
198            let du = tree_dist(graph, cert, u, memo, visiting)?;
199            visiting[v] = false;
200            match du {
201                None => return Err(invalid("predecessor points outside the tree")),
202                Some(d_u) => {
203                    let w = edge_weight(graph, u, v)
204                        .ok_or_else(|| invalid("predecessor edge missing"))?;
205                    Some(d_u + w)
206                }
207            }
208        }
209    };
210    memo[v] = Some(result);
211    Ok(result)
212}
213
214/// Verify that `cert` is a valid shortest-path tree of `graph` from its source.
215pub fn verify_shortest_paths<N>(
216    graph: &Graph<N, i64>,
217    cert: &ShortestPathCertificate,
218) -> Result<(), GraphError> {
219    graph.validate()?;
220    let n = graph.node_count();
221    if cert.source >= n {
222        return Err(invalid("source out of range"));
223    }
224    if cert.predecessors.len() != n {
225        return Err(invalid("predecessor length mismatch"));
226    }
227    if cert.predecessors[cert.source].is_some() {
228        return Err(invalid("source must have no predecessor"));
229    }
230
231    let mut memo: Vec<Option<Option<i64>>> = vec![None; n];
232    let mut visiting = vec![false; n];
233    let mut dist = vec![None; n];
234    for (v, slot) in dist.iter_mut().enumerate() {
235        *slot = tree_dist(graph, cert, v, &mut memo, &mut visiting)?;
236    }
237
238    // Relaxation + completeness: every edge reachable from the source must keep
239    // dist[target] <= dist[source] + weight, and its target must be in the tree.
240    let undirected = !graph.is_directed();
241    for e in &graph.edges {
242        let mut arcs = vec![(e.source, e.target)];
243        if undirected {
244            arcs.push((e.target, e.source));
245        }
246        for (a, b) in arcs {
247            if let Some(da) = dist[a] {
248                match dist[b] {
249                    None => return Err(invalid("reachable node missing from tree")),
250                    Some(db) => {
251                        if db > da + e.weight {
252                            return Err(invalid("edge violates shortest-path relaxation"));
253                        }
254                    }
255                }
256            }
257        }
258    }
259    Ok(())
260}
261
262#[cfg(test)]
263mod tests {
264    use super::*;
265    use crate::edge::Directedness;
266    use crate::path::bellman_ford;
267
268    fn weighted() -> Graph<u8, i64> {
269        let mut g = Graph::with_nodes(vec![0, 1, 2, 3], Directedness::Directed);
270        g.add_edge(0, 1, 1).unwrap();
271        g.add_edge(1, 2, 2).unwrap();
272        g.add_edge(0, 2, 5).unwrap();
273        g.add_edge(2, 3, 1).unwrap();
274        g
275    }
276
277    #[test]
278    fn valid_shortest_path_cert_verifies() {
279        let g = weighted();
280        let (res, _) = bellman_ford(&g, 0).unwrap();
281        let cert = ShortestPathCertificate {
282            source: 0,
283            predecessors: res.predecessors,
284        };
285        assert!(verify_shortest_paths(&g, &cert).is_ok());
286    }
287
288    #[test]
289    fn tampered_shortest_path_cert_rejected() {
290        let g = weighted();
291        let (res, _) = bellman_ford(&g, 0).unwrap();
292        let mut preds = res.predecessors;
293        // Claim node 2 was reached directly from 0 (dist 5) -- but 0->1->2 (3)
294        // is shorter, so relaxation of edge 1->2 must fail.
295        preds[2] = Some(0);
296        let cert = ShortestPathCertificate {
297            source: 0,
298            predecessors: preds,
299        };
300        assert!(verify_shortest_paths(&g, &cert).is_err());
301    }
302}