1#![forbid(unsafe_code)]
2mod commit_impl;
31
32use qssm_local_prover::ProofContext;
33use qssm_utils::hashing::blake3_hash;
34use serde::{Deserialize, Serialize};
35
36#[derive(Serialize, Deserialize)]
39struct WireBlueprint {
40 seed_hex: String,
41 #[serde(default, skip_serializing_if = "Option::is_none")]
42 template_id: Option<String>,
43 template_json: String,
44}
45
46#[derive(Serialize, Deserialize)]
47struct WireZkProof {
48 bundle: qssm_local_prover::ProofBundle,
49 claim: serde_json::Value,
50 binding_ctx_hex: String,
51}
52
53pub fn compile(template_id: &str) -> Result<Vec<u8>, String> {
63 let template = resolve_template_input(template_id)?;
64 let seed = qssm_entropy::harvest(&qssm_entropy::HarvestConfig::default())
65 .map_err(|e| format!("entropy unavailable: {e}"))?
66 .to_seed();
67 let wire = WireBlueprint {
68 seed_hex: hex::encode(seed),
69 template_id: Some(template.id().to_string()),
70 template_json: serde_json::to_string(&template)
71 .map_err(|e| format!("template serialization failed: {e}"))?,
72 };
73 serde_json::to_vec(&wire).map_err(|e| format!("serialization failed: {e}"))
74}
75
76#[must_use]
80pub fn commit(secret: &[u8], salt: &[u8; 32]) -> Vec<u8> {
81 commit_impl::commit_hash(secret, salt).to_vec()
82}
83
84pub fn prove(secret: &[u8], salt: &[u8; 32], blueprint: &[u8]) -> Result<Vec<u8>, String> {
97 let wire_bp: WireBlueprint =
98 serde_json::from_slice(blueprint).map_err(|e| format!("invalid blueprint: {e}"))?;
99 let seed = decode_hex_32(&wire_bp.seed_hex, "blueprint seed")?;
100 let template = template_from_blueprint(&wire_bp)?;
101 let ctx = ProofContext::new(seed);
102
103 let claim: serde_json::Value =
104 serde_json::from_slice(secret).map_err(|e| format!("invalid JSON claim: {e}"))?;
105 let binding_ctx = blake3_hash(salt);
106 let entropy_seed = qssm_entropy::harvest(&qssm_entropy::HarvestConfig::default())
107 .map_err(|e| format!("entropy unavailable: {e}"))?
108 .to_seed();
109 let (value, target) = extract_value_target(&claim, &template);
110
111 let proof = qssm_local_prover::prove(
112 &ctx,
113 &template,
114 &claim,
115 value,
116 target,
117 binding_ctx,
118 entropy_seed,
119 )
120 .map_err(|e| format!("prove failed: {e}"))?;
121
122 let wire = WireZkProof {
123 bundle: qssm_local_prover::ProofBundle::from_proof(&proof),
124 claim,
125 binding_ctx_hex: hex::encode(binding_ctx),
126 };
127 serde_json::to_vec(&wire).map_err(|e| format!("serialization failed: {e}"))
128}
129
130#[must_use]
136pub fn verify(proof: &[u8], blueprint: &[u8]) -> bool {
137 verify_inner(proof, blueprint).unwrap_or(false)
138}
139
140#[must_use]
145pub fn open(secret: &[u8], salt: &[u8; 32]) -> Vec<u8> {
146 commit_impl::commit_hash(secret, salt).to_vec()
147}
148
149fn verify_inner(proof: &[u8], blueprint: &[u8]) -> Result<bool, String> {
152 let wire_bp: WireBlueprint =
153 serde_json::from_slice(blueprint).map_err(|e| format!("invalid blueprint: {e}"))?;
154 let seed = decode_hex_32(&wire_bp.seed_hex, "blueprint seed")?;
155 let template = template_from_blueprint(&wire_bp)?;
156 let ctx = ProofContext::new(seed);
157
158 let wire_proof: WireZkProof =
159 serde_json::from_slice(proof).map_err(|e| format!("invalid proof: {e}"))?;
160 let binding_ctx = decode_hex_32(&wire_proof.binding_ctx_hex, "binding_ctx")?;
161 let inner_proof = wire_proof
162 .bundle
163 .to_proof()
164 .map_err(|e| format!("invalid proof bundle: {e}"))?;
165
166 qssm_local_verifier::verify(
167 &ctx,
168 &template,
169 &wire_proof.claim,
170 &inner_proof,
171 binding_ctx,
172 )
173 .map_err(|e| format!("verification failed: {e}"))
174}
175
176fn decode_hex_32(hex_str: &str, field: &str) -> Result<[u8; 32], String> {
177 let bytes = hex::decode(hex_str).map_err(|e| format!("invalid hex for {field}: {e}"))?;
178 <[u8; 32]>::try_from(bytes.as_slice())
179 .map_err(|_| format!("{field}: expected 32 bytes, got {}", bytes.len()))
180}
181
182fn resolve_template_input(raw: &str) -> Result<qssm_templates::QssmTemplate, String> {
183 if let Some(template) = qssm_templates::resolve(raw.trim()) {
184 return Ok(template);
185 }
186 qssm_templates::QssmTemplate::from_json_slice(raw.as_bytes())
187 .map_err(|_| format!("unknown template or invalid template JSON: {raw}"))
188}
189
190fn template_from_blueprint(
191 wire_bp: &WireBlueprint,
192) -> Result<qssm_templates::QssmTemplate, String> {
193 if !wire_bp.template_json.trim().is_empty() {
194 return qssm_templates::QssmTemplate::from_json_slice(wire_bp.template_json.as_bytes())
195 .map_err(|e| format!("invalid blueprint template: {e}"));
196 }
197
198 if let Some(template_id) = &wire_bp.template_id {
199 return qssm_templates::resolve(template_id)
200 .ok_or_else(|| format!("unknown template: {template_id}"));
201 }
202
203 Err("blueprint is missing template payload".to_string())
204}
205
206fn extract_value_target(
210 claim: &serde_json::Value,
211 template: &qssm_templates::QssmTemplate,
212) -> (u64, u64) {
213 use qssm_templates::{json_at_path, PredicateBlock};
214
215 for pred in template.predicates() {
216 match pred {
217 PredicateBlock::Range { field, min, .. } => {
218 if let Some(val) = json_at_path(claim, field).and_then(|v| v.as_u64()) {
219 return (val, (*min as u64).saturating_sub(1));
222 }
223 }
224 PredicateBlock::AtLeast { field, min } => {
225 if let Some(val) = json_at_path(claim, field).and_then(|v| v.as_u64()) {
226 return (val, (*min as u64).saturating_sub(1));
227 }
228 }
229 PredicateBlock::Compare {
230 field,
231 op: qssm_templates::CmpOp::Gt,
232 rhs,
233 } => {
234 if let (Some(lhs), Some(rhs_val)) = (
235 json_at_path(claim, field).and_then(|v| v.as_u64()),
236 rhs.as_u64(),
237 ) {
238 return (lhs, rhs_val);
239 }
240 }
241 _ => {}
242 }
243 }
244 (1, 0)
245}
246
247#[cfg(test)]
248mod tests {
249 use super::*;
250
251 #[test]
252 fn commit_open_round_trip() {
253 let secret = b"my-secret-value";
254 let salt = [42u8; 32];
255 let c = commit(secret, &salt);
256 let d = open(secret, &salt);
257 assert_eq!(c, d);
258 }
259
260 #[test]
261 fn open_rejects_wrong_secret() {
262 let salt = [42u8; 32];
263 let c = commit(b"correct", &salt);
264 let d = open(b"wrong", &salt);
265 assert_ne!(c, d);
266 }
267
268 #[test]
269 fn extract_value_target_age_gate() {
270 let template = qssm_templates::QssmTemplate::proof_of_age("age-gate-21");
271 let claim = serde_json::json!({ "claim": { "age_years": 25 } });
272 let (v, t) = extract_value_target(&claim, &template);
273 assert_eq!(v, 25);
274 assert_eq!(t, 20);
276 }
277
278 #[test]
279 fn compile_rejects_unknown_template() {
280 let result = compile("nonexistent-template-xyz");
281 assert!(result.is_err());
282 assert!(result.unwrap_err().contains("unknown template"));
283 }
284
285 #[test]
286 fn prove_value_equals_min_passes() {
287 let blueprint = compile("age-gate-21").unwrap();
288 let claim = br#"{"claim":{"age_years":21}}"#;
289 let salt = [1u8; 32];
290 let proof = prove(claim, &salt, &blueprint);
291 assert!(
292 proof.is_ok(),
293 "age=21 should pass age-gate-21: {}",
294 proof.unwrap_err()
295 );
296 assert!(verify(&proof.unwrap(), &blueprint));
297 }
298
299 #[test]
300 fn prove_value_above_min_passes() {
301 let blueprint = compile("age-gate-21").unwrap();
302 let claim = br#"{"claim":{"age_years":30}}"#;
303 let salt = [2u8; 32];
304 let proof = prove(claim, &salt, &blueprint);
305 assert!(proof.is_ok(), "age=30 should pass: {}", proof.unwrap_err());
306 assert!(verify(&proof.unwrap(), &blueprint));
307 }
308
309 #[test]
310 fn prove_value_below_min_fails() {
311 let blueprint = compile("age-gate-21").unwrap();
312 let claim = br#"{"claim":{"age_years":20}}"#;
313 let salt = [3u8; 32];
314 let proof = prove(claim, &salt, &blueprint);
315 assert!(proof.is_err(), "age=20 should fail age-gate-21");
316 }
317
318 #[test]
319 fn compile_accepts_raw_template_json() {
320 let template = serde_json::json!({
321 "qssm_template_version": 1,
322 "id": "custom-age-gate",
323 "title": "Custom age gate",
324 "allowed_anchor_kinds": ["anchor_hash", "static_root", "timestamp_unix_secs"],
325 "predicates": [
326 {
327 "kind": "at_least",
328 "field": "claim.age_years",
329 "min": 21
330 }
331 ]
332 });
333 let blueprint =
334 compile(&template.to_string()).expect("custom template JSON should compile");
335 let proof = prove(br#"{"claim":{"age_years":30}}"#, &[7u8; 32], &blueprint)
336 .expect("custom template blueprint should prove");
337 assert!(verify(&proof, &blueprint));
338 }
339}