Skip to main content

confium_transparency/
test_vectors.rs

1//! Deterministic test vectors for the transparency log.
2//!
3//! Generates reproducible Merkle tree fixtures with known-good roots
4//! and inclusion proofs. Used for:
5//! - Cross-implementation compatibility testing
6//! - Regression detection
7//! - Binding verification (Ruby, Python, WASM, Go)
8
9use crate::entry::{ArtifactType, MerkleEntry};
10use crate::merkle::{Hash, InclusionProof, MerkleTree};
11use serde::{Deserialize, Serialize};
12
13/// A single test vector entry.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct TestVector {
16    /// Human-readable description.
17    pub description: String,
18    /// Tree entries (artifact hashes as hex).
19    pub artifact_hashes_hex: Vec<String>,
20    /// Expected root hash (hex).
21    pub expected_root_hex: String,
22    /// Inclusion proofs for each entry (0-indexed).
23    pub inclusion_proofs: Vec<InclusionProofJson>,
24}
25
26/// JSON-serializable inclusion proof.
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct InclusionProofJson {
29    /// Sequence number (0-indexed).
30    pub sequence: u64,
31    /// Proof steps: { sibling_hex, side }.
32    pub steps: Vec<ProofStepJson>,
33}
34
35/// JSON-serializable proof step.
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct ProofStepJson {
38    /// Sibling hash (hex).
39    pub sibling_hex: String,
40    /// Side: "left" or "right".
41    pub side: String,
42}
43
44/// Generate a test vector for a tree with `n` entries.
45///
46/// Uses deterministic entries where each entry's artifact hash is
47/// `[i; 32]` (all bytes set to the index). Timestamps are fixed to
48/// a known epoch value for reproducibility.
49pub fn generate_vector(n: u64) -> TestVector {
50    let mut tree = MerkleTree::new();
51    let fixed_time = chrono::DateTime::parse_from_rfc3339("2026-01-01T00:00:00Z")
52        .unwrap()
53        .with_timezone(&chrono::Utc);
54
55    for i in 0..n {
56        let mut entry = MerkleEntry::new(i, ArtifactType::ThresholdSignature, [i as u8; 32]);
57        entry.timestamp = fixed_time;
58        tree.append(entry);
59    }
60
61    let root = tree.root();
62    let mut proofs = Vec::new();
63    for i in 0..n {
64        let proof = tree.inclusion_proof(i).unwrap();
65        let steps: Vec<ProofStepJson> = proof
66            .steps
67            .iter()
68            .map(|s| ProofStepJson {
69                sibling_hex: hex::encode(s.sibling),
70                side: match s.side {
71                    crate::merkle::Side::Left => "left".into(),
72                    crate::merkle::Side::Right => "right".into(),
73                },
74            })
75            .collect();
76        proofs.push(InclusionProofJson { sequence: i, steps });
77    }
78
79    let artifact_hashes: Vec<String> = (0..n).map(|i| hex::encode([i as u8; 32])).collect();
80
81    TestVector {
82        description: format!("Tree with {n} entries, deterministic hashes"),
83        artifact_hashes_hex: artifact_hashes,
84        expected_root_hex: hex::encode(root),
85        inclusion_proofs: proofs,
86    }
87}
88
89/// Generate a suite of test vectors for common tree sizes.
90pub fn generate_suite() -> Vec<TestVector> {
91    vec![
92        generate_vector(1),
93        generate_vector(2),
94        generate_vector(3),
95        generate_vector(4),
96        generate_vector(8),
97        generate_vector(16),
98        generate_vector(32),
99    ]
100}
101
102/// Verify a test vector against the current MerkleTree implementation.
103/// Returns `Ok(())` if all proofs verify, `Err` with details otherwise.
104pub fn verify_vector(vector: &TestVector) -> Result<(), String> {
105    let root = decode_hash(&vector.expected_root_hex)?;
106    let mut tree = MerkleTree::new();
107    let fixed_time = chrono::DateTime::parse_from_rfc3339("2026-01-01T00:00:00Z")
108        .unwrap()
109        .with_timezone(&chrono::Utc);
110
111    for (i, hash_hex) in vector.artifact_hashes_hex.iter().enumerate() {
112        let hash = decode_hash(hash_hex)?;
113        let mut entry = MerkleEntry::new(i as u64, ArtifactType::ThresholdSignature, hash);
114        entry.timestamp = fixed_time;
115        tree.append(entry);
116    }
117
118    let actual_root = tree.root();
119    if actual_root != root {
120        return Err(format!(
121            "root mismatch: expected {}, got {}",
122            hex::encode(root),
123            hex::encode(actual_root)
124        ));
125    }
126
127    for proof_json in &vector.inclusion_proofs {
128        let seq = proof_json.sequence;
129        let entry = tree.entry(seq).map_err(|e| format!("{e:?}"))?.clone();
130        let steps: Vec<crate::merkle::ProofStep> = proof_json
131            .steps
132            .iter()
133            .map(|s| {
134                Ok(crate::merkle::ProofStep {
135                    sibling: decode_hash(&s.sibling_hex)?,
136                    side: match s.side.as_str() {
137                        "left" => crate::merkle::Side::Left,
138                        "right" => crate::merkle::Side::Right,
139                        _ => return Err(format!("invalid side: {}", s.side)),
140                    },
141                })
142            })
143            .collect::<Result<Vec<_>, String>>()?;
144
145        let proof = InclusionProof {
146            sequence: seq,
147            steps,
148        };
149        MerkleTree::verify_inclusion(&entry, &proof, root).map_err(|e| format!("{e:?}"))?;
150    }
151
152    Ok(())
153}
154
155/// Serialize a test vector suite to JSON.
156pub fn suite_to_json(suite: &[TestVector]) -> Result<String, serde_json::Error> {
157    serde_json::to_string_pretty(suite)
158}
159
160fn decode_hash(hex_str: &str) -> Result<Hash, String> {
161    let bytes = hex::decode(hex_str).map_err(|e| e.to_string())?;
162    if bytes.len() != 32 {
163        return Err(format!("expected 32 bytes, got {}", bytes.len()));
164    }
165    let mut arr = [0u8; 32];
166    arr.copy_from_slice(&bytes);
167    Ok(arr)
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn single_entry_vector_has_correct_root() {
176        let vector = generate_vector(1);
177        assert_eq!(vector.artifact_hashes_hex.len(), 1);
178        assert_eq!(vector.inclusion_proofs.len(), 1);
179        assert!(!vector.expected_root_hex.is_empty());
180    }
181
182    #[test]
183    fn vector_is_deterministic() {
184        let v1 = generate_vector(5);
185        let v2 = generate_vector(5);
186        assert_eq!(v1.expected_root_hex, v2.expected_root_hex);
187        assert_eq!(v1.inclusion_proofs.len(), v2.inclusion_proofs.len());
188    }
189
190    #[test]
191    fn different_sizes_produce_different_roots() {
192        let v1 = generate_vector(1);
193        let v2 = generate_vector(2);
194        assert_ne!(v1.expected_root_hex, v2.expected_root_hex);
195    }
196
197    #[test]
198    fn verify_vector_passes_for_generated() {
199        for n in [1, 2, 3, 5, 8, 16] {
200            let vector = generate_vector(n);
201            verify_vector(&vector).unwrap_or_else(|e| panic!("verify n={n}: {e}"));
202        }
203    }
204
205    #[test]
206    fn generate_suite_has_7_sizes() {
207        let suite = generate_suite();
208        assert_eq!(suite.len(), 7);
209    }
210
211    #[test]
212    fn suite_serializes_to_json() {
213        let suite = generate_suite();
214        let json = suite_to_json(&suite).unwrap();
215        assert!(json.contains("expected_root_hex"));
216        assert!(json.contains("description"));
217    }
218
219    #[test]
220    fn verify_vector_detects_tampered_root() {
221        let mut vector = generate_vector(3);
222        vector.expected_root_hex = hex::encode([0xFF; 32]);
223        assert!(verify_vector(&vector).is_err());
224    }
225
226    #[test]
227    fn inclusion_proof_count_matches_entries() {
228        let vector = generate_vector(8);
229        assert_eq!(vector.inclusion_proofs.len(), 8);
230        for (i, proof) in vector.inclusion_proofs.iter().enumerate() {
231            assert_eq!(proof.sequence, i as u64);
232        }
233    }
234
235    #[test]
236    fn power_of_two_tree_has_clean_proofs() {
237        let vector = generate_vector(8);
238        for proof in &vector.inclusion_proofs {
239            assert!(
240                proof.steps.len() <= 4,
241                "8-entry tree has at most 4 proof steps"
242            );
243        }
244    }
245}