Skip to main content

qssm_local_prover/
lib.rs

1#![forbid(unsafe_code)]
2//! # QSSM Local Prover — Layer 4
3//!
4//! Consumes entropy and produces a complete ZK proof artifact.
5//!
6//! This crate owns:
7//! - The deterministic prove pipeline (predicates → MS → truth binding → LE).
8//! - Core proof types ([`Proof`], [`ProofContext`]).
9//! - The error type ([`ZkError`]).
10//! - The versioned wire format ([`ProofBundle`], [`WireFormatError`]).
11
12pub mod context;
13pub mod error;
14mod prove;
15pub mod wire;
16
17/// MS context tag shared between prove and verify pipelines.
18pub const MS_CONTEXT_TAG: &[u8] = b"qssm-sdk-v1";
19
20// ── Public re-exports ────────────────────────────────────────────────
21pub use context::{Proof, ProofContext};
22pub use error::ZkError;
23pub use prove::prove;
24pub use wire::{ProofBundle, WireFormatError, PROTOCOL_VERSION};
25
26#[cfg(test)]
27mod tests {
28    use super::*;
29    use qssm_utils::hashing::blake3_hash;
30    use serde_json::json;
31
32    fn test_seed() -> [u8; 32] {
33        blake3_hash(b"QSSM-SDK-TEST-SEED")
34    }
35
36    fn test_entropy() -> [u8; 32] {
37        blake3_hash(b"QSSM-SDK-TEST-ENTROPY")
38    }
39
40    fn test_binding_ctx() -> [u8; 32] {
41        blake3_hash(b"test-binding-context")
42    }
43
44    fn test_template() -> qssm_templates::QssmTemplate {
45        qssm_templates::QssmTemplate::proof_of_age("test-age")
46    }
47
48    fn test_claim() -> serde_json::Value {
49        json!({ "claim": { "age_years": 25 } })
50    }
51
52    fn test_ctx() -> ProofContext {
53        ProofContext::new(test_seed())
54    }
55
56    fn make_proof() -> Proof {
57        prove(
58            &test_ctx(),
59            &test_template(),
60            &test_claim(),
61            100,
62            50,
63            test_binding_ctx(),
64            test_entropy(),
65        )
66        .expect("prove should succeed")
67    }
68
69    // ── Prove ────────────────────────────────────────────────────────
70
71    #[test]
72    fn prove_succeeds() {
73        let _proof = make_proof();
74    }
75
76    #[test]
77    fn prove_rejects_bad_predicate() {
78        let bad_claim = json!({ "claim": { "age_years": 15 } });
79        let err = prove(
80            &test_ctx(),
81            &test_template(),
82            &bad_claim,
83            100,
84            50,
85            test_binding_ctx(),
86            test_entropy(),
87        )
88        .unwrap_err();
89        assert!(matches!(err, ZkError::PredicateFailed(_)));
90    }
91
92    #[test]
93    fn prove_is_deterministic() {
94        let p1 = make_proof();
95        let p2 = make_proof();
96        let j1 = serde_json::to_string(&ProofBundle::from_proof(&p1)).unwrap();
97        let j2 = serde_json::to_string(&ProofBundle::from_proof(&p2)).unwrap();
98        assert_eq!(j1, j2, "identical inputs must produce identical proofs");
99    }
100
101    // ── Wire format round-trip ───────────────────────────────────────
102
103    #[test]
104    fn wire_round_trip_json() {
105        let proof = make_proof();
106        let bundle = ProofBundle::from_proof(&proof);
107        let json = serde_json::to_string(&bundle).expect("serialize");
108        let bundle2: ProofBundle = serde_json::from_str(&json).expect("deserialize");
109        let recovered = bundle2.to_proof().expect("to_proof");
110        assert_eq!(recovered.ms_root(), proof.ms_root());
111        assert_eq!(recovered.value(), proof.value());
112        assert_eq!(recovered.target(), proof.target());
113    }
114
115    #[test]
116    fn wire_format_forward_compat() {
117        let proof = make_proof();
118        let bundle = ProofBundle::from_proof(&proof);
119        let json = serde_json::to_string(&bundle).expect("serialize");
120        let parsed: ProofBundle = serde_json::from_str(&json)
121            .expect("old bundle must remain parseable by current (and future) code");
122        let recovered = parsed.to_proof().expect("to_proof");
123        assert_eq!(recovered.ms_root(), proof.ms_root());
124        assert_eq!(recovered.value(), proof.value());
125        assert_eq!(recovered.target(), proof.target());
126    }
127
128    // ── Wire format rejection tests ──────────────────────────────────
129
130    #[test]
131    fn wire_rejects_bad_version() {
132        let proof = make_proof();
133        let mut bundle = ProofBundle::from_proof(&proof);
134        bundle.version = 99;
135        let json = serde_json::to_string(&bundle).expect("serialize");
136        let parsed: ProofBundle = serde_json::from_str(&json).expect("deserialize");
137        let err = parsed.to_proof().unwrap_err();
138        assert!(matches!(err, WireFormatError::UnsupportedVersion(99)));
139    }
140
141    #[test]
142    fn wire_rejects_bad_hex() {
143        let proof = make_proof();
144        let mut bundle = ProofBundle::from_proof(&proof);
145        bundle.ms_root_hex = "ZZZZ_not_hex".to_string();
146        let json = serde_json::to_string(&bundle).expect("serialize");
147        let parsed: ProofBundle = serde_json::from_str(&json).expect("deserialize");
148        let err = parsed.to_proof().unwrap_err();
149        assert!(matches!(err, WireFormatError::HexDecode { .. }));
150    }
151
152    #[test]
153    fn wire_rejects_wrong_length() {
154        let proof = make_proof();
155        let mut bundle = ProofBundle::from_proof(&proof);
156        bundle.ms_root_hex = hex::encode([0u8; 16]);
157        let json = serde_json::to_string(&bundle).expect("serialize");
158        let parsed: ProofBundle = serde_json::from_str(&json).expect("deserialize");
159        let err = parsed.to_proof().unwrap_err();
160        assert!(matches!(
161            err,
162            WireFormatError::BadLength {
163                expected: 32,
164                got: 16,
165                ..
166            }
167        ));
168    }
169
170    #[test]
171    fn wire_rejects_wrong_coeff_count() {
172        let proof = make_proof();
173        let mut bundle = ProofBundle::from_proof(&proof);
174        bundle.le_commitment_coeffs = vec![0u32; 10];
175        let json = serde_json::to_string(&bundle).expect("serialize");
176        let parsed: ProofBundle = serde_json::from_str(&json).expect("deserialize");
177        let err = parsed.to_proof().unwrap_err();
178        assert!(matches!(
179            err,
180            WireFormatError::BadCoeffCount {
181                expected: 256,
182                got: 10,
183                ..
184            }
185        ));
186    }
187
188    #[test]
189    fn wire_rejects_unknown_fields() {
190        let proof = make_proof();
191        let bundle = ProofBundle::from_proof(&proof);
192        let mut json_val: serde_json::Value = serde_json::to_value(&bundle).expect("to_value");
193        json_val
194            .as_object_mut()
195            .unwrap()
196            .insert("smuggled_field".to_string(), serde_json::Value::Bool(true));
197        let json = serde_json::to_string(&json_val).expect("serialize");
198        let result = serde_json::from_str::<ProofBundle>(&json);
199        assert!(result.is_err(), "unknown fields must be rejected");
200    }
201
202    // ── Injectivity & preservation ───────────────────────────────────
203
204    #[test]
205    fn proof_bundle_from_proof_injective() {
206        let proof_a = make_proof();
207        let proof_b = {
208            let different_entropy = blake3_hash(b"DIFFERENT-ENTROPY-SEED");
209            prove(
210                &test_ctx(),
211                &test_template(),
212                &test_claim(),
213                100,
214                50,
215                test_binding_ctx(),
216                different_entropy,
217            )
218            .expect("prove should succeed")
219        };
220        let json_a = serde_json::to_string(&ProofBundle::from_proof(&proof_a)).unwrap();
221        let json_b = serde_json::to_string(&ProofBundle::from_proof(&proof_b)).unwrap();
222        assert_ne!(
223            json_a, json_b,
224            "different proofs must produce different bundles"
225        );
226    }
227
228    #[test]
229    fn proof_bundle_preserves_all_fields() {
230        let proof = make_proof();
231        let bundle1 = ProofBundle::from_proof(&proof);
232        let recovered = bundle1.to_proof().expect("to_proof");
233        let bundle2 = ProofBundle::from_proof(&recovered);
234        let json1 = serde_json::to_string(&bundle1).unwrap();
235        let json2 = serde_json::to_string(&bundle2).unwrap();
236        assert_eq!(json1, json2, "round-trip must be lossless — no field drift");
237    }
238
239    #[test]
240    fn proof_bundle_json_field_names_stable() {
241        let proof = make_proof();
242        let bundle = ProofBundle::from_proof(&proof);
243        let val: serde_json::Value = serde_json::to_value(&bundle).expect("to_value");
244        let obj = val.as_object().expect("must be JSON object");
245        let mut keys: Vec<&str> = obj.keys().map(|k| k.as_str()).collect();
246        keys.sort();
247        let expected = vec![
248            "binding_entropy_hex",
249            "external_entropy_hex",
250            "external_entropy_included",
251            "le_challenge_seed_hex",
252            "le_commitment_coeffs",
253            "le_proof_t_coeffs",
254            "le_proof_z_coeffs",
255            "ms_bit_at_k",
256            "ms_challenge_hex",
257            "ms_k",
258            "ms_n",
259            "ms_opened_salt_hex",
260            "ms_path_hex",
261            "ms_root_hex",
262            "protocol_version",
263            "target",
264            "value",
265            "version",
266        ];
267        assert_eq!(keys, expected, "JSON field names must match frozen schema");
268    }
269}