Skip to main content

vyre_libs/security/
path_reconstruct.rs

1//! `path_reconstruct`  -  Tier-3 shim over
2//! [`vyre_primitives::graph::path_reconstruct`].
3
4use vyre::ir::Program;
5use vyre_primitives::graph::path_reconstruct::path_reconstruct as primitive_path_reconstruct;
6
7const OP_ID: &str = "vyre-libs::security::path_reconstruct";
8
9/// Signature retained for ABI compatibility.
10#[must_use]
11pub fn path_reconstruct(
12    parent: &str,
13    target: &str,
14    path_out: &str,
15    path_len: &str,
16    max_depth: u32,
17) -> Program {
18    // path_reconstruct does not consume a node count; max_depth is
19    // the structural sizing parameter. Use it as the non-degenerate
20    // sentinel  -  a zero-depth reconstruction has no useful output.
21    crate::security::assert_security_inputs(
22        OP_ID,
23        max_depth,
24        &[
25            ("parent", parent),
26            ("target", target),
27            ("path_out", path_out),
28            ("path_len", path_len),
29        ],
30    );
31    crate::region::tag_program(
32        OP_ID,
33        primitive_path_reconstruct(parent, target, path_out, path_len, max_depth),
34    )
35}
36
37inventory::submit! {
38    crate::harness::OpEntry {
39        id: OP_ID,
40        build: || path_reconstruct("parent", "target", "path_out", "path_len", 4),
41        test_inputs: Some(|| {
42            let to_bytes = |w: &[u32]| vyre_primitives::wire::pack_u32_slice(w);
43            vec![vec![
44                to_bytes(&[0, 0, 1, 2]),
45                to_bytes(&[3]),
46                to_bytes(&[0, 0, 0, 0]),
47                to_bytes(&[0]),
48            ]]
49        }),
50        expected_output: Some(|| {
51            let to_bytes = |w: &[u32]| vyre_primitives::wire::pack_u32_slice(w);
52            vec![vec![to_bytes(&[3, 2, 1, 0]), to_bytes(&[4])]]
53        }),
54        category: Some("security"),
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use vyre_primitives::graph::path_reconstruct::cpu_ref;
62
63    #[test]
64    fn path_reconstruct_program_emits_buffers() {
65        let p = path_reconstruct("parent", "target", "path_out", "path_len", 4);
66        let names: Vec<&str> = p.buffers().iter().map(|b| b.name()).collect();
67        assert!(names.contains(&"parent"));
68        assert!(names.contains(&"target"));
69        assert!(names.contains(&"path_out"));
70        assert!(names.contains(&"path_len"));
71    }
72
73    #[test]
74    fn path_reconstruct_respects_max_depth() {
75        let p = path_reconstruct("parent", "target", "path_out", "path_len", 8);
76        let path_out_buf = p
77            .buffers()
78            .iter()
79            .find(|b| b.name() == "path_out")
80            .expect("Fix: path_out buffer");
81        assert_eq!(path_out_buf.count, 8);
82    }
83
84    #[test]
85    fn path_reconstruct_cpu_ref_happy_path() {
86        let parent = [0, 0, 1, 2];
87        let mut scratch = Vec::new();
88        let len = cpu_ref(&parent, 3, 4, &mut scratch);
89        assert_eq!(len, 4);
90        assert_eq!(scratch, vec![3, 2, 1, 0]);
91    }
92
93    #[test]
94    fn path_reconstruct_cpu_ref_oob_target_returns_self() {
95        // Target >= parent.len() => get returns None => current stays unchanged,
96        // loop breaks immediately, path = [target].
97        let parent = [0, 0, 1, 2];
98        let mut scratch = Vec::new();
99        let len = cpu_ref(&parent, 10, 4, &mut scratch);
100        assert_eq!(len, 1);
101        assert_eq!(scratch[0], 10);
102    }
103
104    #[test]
105    fn path_reconstruct_cpu_ref_cycle_terminates_at_max_depth() {
106        // 2-cycle: 1->2, 2->1. Starting at 1, the walk alternates 1,2,1,2...
107        // It must stop at max_depth.
108        let parent = [0, 2, 1, 3];
109        let mut scratch = Vec::new();
110        let len = cpu_ref(&parent, 1, 4, &mut scratch);
111        assert_eq!(len, 4);
112        assert_eq!(scratch, vec![1, 2, 1, 2]);
113    }
114
115    #[test]
116    fn path_reconstruct_gpu_matches_cpu_reference_on_cycle() {
117        let parent = [0u32, 2, 1, 3];
118        let target = 1u32;
119        let max_depth = 4u32;
120        let p = path_reconstruct("parent", "target", "path_out", "path_len", max_depth);
121        let to_bytes = |w: &[u32]| vyre_primitives::wire::pack_u32_slice(w);
122        let inputs = vec![
123            to_bytes(&parent),
124            to_bytes(&[target]),
125            to_bytes(&[0, 0, 0, 0]),
126            to_bytes(&[0]),
127        ];
128        let values: Vec<vyre_reference::value::Value> = inputs
129            .into_iter()
130            .map(vyre_reference::value::Value::from)
131            .collect();
132        let outputs = vyre_reference::reference_eval(&p, &values).unwrap();
133        let gpu_path_bytes = outputs[0].to_bytes();
134        let gpu_len = u32::from_le_bytes(outputs[1].to_bytes()[0..4].try_into().unwrap());
135
136        let mut cpu_scratch = Vec::new();
137        let cpu_len = cpu_ref(&parent, target, max_depth, &mut cpu_scratch);
138
139        assert_eq!(
140            gpu_len, cpu_len,
141            "GPU path length must match CPU reference on cycle"
142        );
143        let gpu_path: Vec<u32> = gpu_path_bytes
144            .chunks_exact(4)
145            .map(|c| u32::from_le_bytes(c.try_into().unwrap()))
146            .collect();
147        assert_eq!(
148            &gpu_path[..cpu_len as usize],
149            &cpu_scratch[..cpu_len as usize],
150            "GPU path must match CPU reference up to max_depth on cyclic parent array"
151        );
152    }
153
154    #[test]
155    #[should_panic(expected = "empty buffer name")]
156    fn path_reconstruct_empty_buffer_name_should_panic() {
157        let _ = path_reconstruct("", "target", "path_out", "path_len", 4);
158    }
159}