1#![forbid(unsafe_code)]
2pub mod context;
13pub mod error;
14mod prove;
15pub mod wire;
16
17pub const MS_CONTEXT_TAG: &[u8] = b"qssm-sdk-v1";
19
20pub 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 #[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 #[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 #[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 #[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}