Skip to main content

confium_privacy/
differential.rs

1//! Differential testing framework.
2//!
3//! Compares outputs of two implementations against the same inputs
4//! to detect discrepancies. Used to verify cross-implementation
5//! compatibility (e.g., our FROST vs. reference FROST).
6
7/// Result of a differential test comparison.
8#[derive(Debug, Clone)]
9pub struct DiffResult<T: Clone + PartialEq> {
10    pub input_label: String,
11    pub implementation_a: T,
12    pub implementation_b: T,
13    pub matches: bool,
14}
15
16/// Run a differential comparison between two functions over a set
17/// of byte-vector inputs.
18pub fn differential_test<A, B, T>(
19    inputs: &[(String, Vec<u8>)],
20    impl_a: A,
21    impl_b: B,
22) -> Vec<DiffResult<T>>
23where
24    A: Fn(&[u8]) -> T,
25    B: Fn(&[u8]) -> T,
26    T: Clone + PartialEq,
27{
28    inputs
29        .iter()
30        .map(|(label, input)| {
31            let result_a = impl_a(input);
32            let result_b = impl_b(input);
33            DiffResult {
34                input_label: label.clone(),
35                matches: result_a == result_b,
36                implementation_a: result_a,
37                implementation_b: result_b,
38            }
39        })
40        .collect()
41}
42
43/// Summary of a differential test run.
44#[derive(Debug, Clone)]
45pub struct DiffSummary {
46    pub total: usize,
47    pub matching: usize,
48    pub mismatching: usize,
49    pub mismatches: Vec<String>,
50}
51
52impl DiffSummary {
53    pub fn from_results<T: Clone + PartialEq>(results: &[DiffResult<T>]) -> Self {
54        let total = results.len();
55        let matching = results.iter().filter(|r| r.matches).count();
56        let mismatches: Vec<String> = results
57            .iter()
58            .filter(|r| !r.matches)
59            .map(|r| r.input_label.clone())
60            .collect();
61        Self {
62            total,
63            matching,
64            mismatching: total - matching,
65            mismatches,
66        }
67    }
68
69    pub fn all_match(&self) -> bool {
70        self.mismatching == 0
71    }
72
73    pub fn match_rate(&self) -> f64 {
74        if self.total == 0 {
75            1.0
76        } else {
77            self.matching as f64 / self.total as f64
78        }
79    }
80}
81
82/// Compare hash function outputs for consistency.
83pub fn compare_hashes(
84    inputs: &[Vec<u8>],
85    hash_a: impl Fn(&[u8]) -> Vec<u8>,
86    hash_b: impl Fn(&[u8]) -> Vec<u8>,
87) -> DiffSummary {
88    let results: Vec<DiffResult<Vec<u8>>> = inputs
89        .iter()
90        .enumerate()
91        .map(|(i, input)| {
92            let ra = hash_a(input);
93            let rb = hash_b(input);
94            DiffResult {
95                input_label: format!("input-{i}"),
96                matches: ra == rb,
97                implementation_a: ra,
98                implementation_b: rb,
99            }
100        })
101        .collect();
102    DiffSummary::from_results(&results)
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn matching_implementations() {
111        let inputs: Vec<(String, Vec<u8>)> =
112            vec![("a".into(), vec![1, 2, 3]), ("b".into(), vec![4, 5, 6])];
113        let results = differential_test(
114            &inputs,
115            |input: &[u8]| input.len(),
116            |input: &[u8]| input.len(),
117        );
118        let summary = DiffSummary::from_results(&results);
119        assert!(summary.all_match());
120        assert_eq!(summary.match_rate(), 1.0);
121    }
122
123    #[test]
124    fn mismatching_implementations() {
125        let inputs: Vec<(String, Vec<u8>)> = vec![("a".into(), vec![1]), ("b".into(), vec![2])];
126        let results = differential_test(
127            &inputs,
128            |input: &[u8]| input[0] as u32 * 2,
129            |input: &[u8]| input[0] as u32 * 3,
130        );
131        let summary = DiffSummary::from_results(&results);
132        assert!(!summary.all_match());
133        assert_eq!(summary.mismatching, 2);
134    }
135
136    #[test]
137    fn empty_inputs() {
138        let inputs: Vec<(String, Vec<u8>)> = vec![];
139        let results = differential_test(&inputs, |_: &[u8]| 0u32, |_: &[u8]| 0u32);
140        let summary = DiffSummary::from_results(&results);
141        assert_eq!(summary.total, 0);
142        assert_eq!(summary.match_rate(), 1.0);
143    }
144
145    #[test]
146    fn partial_match() {
147        let inputs: Vec<(String, Vec<u8>)> = vec![("a".into(), vec![0]), ("b".into(), vec![1])];
148        let results = differential_test(
149            &inputs,
150            |input: &[u8]| input[0],
151            |input: &[u8]| if input[0] == 0 { 0 } else { 99 },
152        );
153        let summary = DiffSummary::from_results(&results);
154        assert_eq!(summary.matching, 1);
155        assert_eq!(summary.mismatching, 1);
156    }
157
158    #[test]
159    fn compare_hashes_identical() {
160        let inputs = vec![vec![1, 2, 3], vec![4, 5, 6]];
161        let summary = compare_hashes(&inputs, |d| vec![d.len() as u8], |d| vec![d.len() as u8]);
162        assert!(summary.all_match());
163    }
164
165    #[test]
166    fn compare_hashes_different() {
167        let inputs = vec![vec![1, 2, 3]];
168        let summary = compare_hashes(&inputs, |_| vec![1u8], |_| vec![2u8]);
169        assert!(!summary.all_match());
170    }
171
172    #[test]
173    fn mismatch_labels_recorded() {
174        let inputs: Vec<(String, Vec<u8>)> =
175            vec![("first".into(), vec![1]), ("second".into(), vec![2])];
176        let results = differential_test(&inputs, |input: &[u8]| input[0], |_: &[u8]| 99u8);
177        let summary = DiffSummary::from_results(&results);
178        assert!(summary.mismatches.contains(&"first".to_string()));
179        assert!(summary.mismatches.contains(&"second".to_string()));
180    }
181}