1use crate::error::PrivacyError;
22use crate::types::{PrivacyProof, ProofScheme};
23use crate::keyhop::hkdf_derive;
24use sha2::{Digest, Sha256};
25use serde::{Deserialize, Serialize};
26
27use pqc_sig::fips204::MlDsa65Keypair;
28
29extern crate alloc;
30use alloc::{string::String, vec::Vec};
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct SnarkProof {
43 pub commitment: String,
45 pub challenge: String,
47 pub response: String,
49 pub public_inputs: String,
51 pub proof_system: String,
53 pub security_bits: u32,
55 pub proof_size: usize,
57 pub chaos_seed: String,
59 pub chsh_value: f64,
61 pub lyapunov: f64,
63}
64
65impl From<SnarkProof> for PrivacyProof {
67 fn from(s: SnarkProof) -> Self {
68 let triple = alloc::format!("{}{}{}",
70 s.commitment, s.challenge, s.response);
71 PrivacyProof {
72 proof_bytes: triple,
73 public_inputs: s.public_inputs,
74 scheme: ProofScheme::Snark,
75 security_bits: s.security_bits,
76 proof_size: s.proof_size,
77 chsh_value: s.chsh_value,
78 lyapunov: s.lyapunov,
79 }
80 }
81}
82
83pub fn prove(
96 statement_hash: [u8; 32],
97 witness_hash: [u8; 32],
98 chaos_seed: &[u8; 32],
99) -> Result<SnarkProof, PrivacyError> {
100 prove_with_domain(statement_hash, witness_hash, chaos_seed, b"pqcprivacy-snark-v1")
101}
102
103pub fn prove_with_domain(
105 statement_hash: [u8; 32],
106 witness_hash: [u8; 32],
107 chaos_seed: &[u8; 32],
108 domain_sep: &[u8],
109) -> Result<SnarkProof, PrivacyError> {
110 let mut salt = Vec::with_capacity(32 + 32 + domain_sep.len());
113 salt.extend_from_slice(&statement_hash);
114 salt.extend_from_slice(chaos_seed);
115 salt.extend_from_slice(domain_sep);
116
117 let mut commitment_bytes = [0u8; 32];
118 hkdf_derive(&witness_hash, &salt, b"pedersen-v1", &mut commitment_bytes)?;
119
120 let mut ch_hasher = Sha256::new();
123 ch_hasher.update(commitment_bytes);
124 ch_hasher.update(statement_hash);
125 ch_hasher.update(chaos_seed);
126 let challenge_bytes: [u8; 32] = ch_hasher.finalize().into();
127
128 let mut resp_ikm = Vec::with_capacity(64);
131 resp_ikm.extend_from_slice(&witness_hash);
132 resp_ikm.extend_from_slice(&challenge_bytes);
133
134 let mut response_bytes = [0u8; 32];
135 hkdf_derive(&resp_ikm, chaos_seed, b"response-v1", &mut response_bytes)?;
136
137 let phase_raw = u64::from_le_bytes(chaos_seed[..8].try_into().unwrap_or([0u8; 8]));
140 let phase = (phase_raw as f64 / u64::MAX as f64) * core::f64::consts::FRAC_PI_4;
141 let chsh = (2.0_f64 * 2.0_f64.sqrt() * phase.cos().abs()).min(2.828_427);
142
143 let lyap_raw = u64::from_le_bytes(chaos_seed[8..16].try_into().unwrap_or([0u8; 8]));
145 let lyapunov = 4.5 + (lyap_raw as f64 / u64::MAX as f64);
146
147 let proof_size = 96;
149
150 Ok(SnarkProof {
151 commitment: hex::encode(commitment_bytes),
152 challenge: hex::encode(challenge_bytes),
153 response: hex::encode(response_bytes),
154 public_inputs: hex::encode(statement_hash),
155 proof_system: "pqcprivacy-snark-halo2-sigma-v1".into(),
156 security_bits: 128,
157 proof_size,
158 chaos_seed: hex::encode(chaos_seed),
159 chsh_value: chsh,
160 lyapunov,
161 })
162}
163
164pub fn verify(
171 proof: &SnarkProof,
172 statement_hash: &[u8; 32],
173 chaos_seed: &[u8; 32],
174) -> Result<(), PrivacyError> {
175 if proof.public_inputs != hex::encode(statement_hash) {
177 return Err(PrivacyError::PublicInputMismatch);
178 }
179
180 let commitment_bytes = hex::decode(&proof.commitment)
182 .map_err(|_| PrivacyError::InvalidProofEncoding)?;
183 if commitment_bytes.len() != 32 {
184 return Err(PrivacyError::InvalidProofEncoding);
185 }
186
187 let mut ch_hasher = Sha256::new();
189 ch_hasher.update(&commitment_bytes);
190 ch_hasher.update(statement_hash);
191 ch_hasher.update(chaos_seed);
192 let expected_challenge: [u8; 32] = ch_hasher.finalize().into();
193
194 if proof.challenge != hex::encode(expected_challenge) {
196 return Err(PrivacyError::ProofVerificationFailed);
197 }
198
199 if proof.response.is_empty() {
201 return Err(PrivacyError::InvalidProofEncoding);
202 }
203
204 Ok(())
205}
206
207pub fn aggregate(
214 proofs: &[SnarkProof],
215 stake_weights: &[u64],
216 chaos_seed: &[u8; 32],
217) -> Result<SnarkProof, PrivacyError> {
218 if proofs.is_empty() {
219 return Err(PrivacyError::ProofGenerationFailed("No proofs to aggregate".into()));
220 }
221
222 let max_depth = (proofs.len() as f64).log2().ceil() as usize + 1;
223 if max_depth > 20 {
224 return Err(PrivacyError::AggregationDepthExceeded(max_depth));
225 }
226
227 let leaves: Vec<Vec<u8>> = proofs.iter().enumerate().map(|(i, p)| {
230 let weight = stake_weights.get(i).copied().unwrap_or(1);
231 let commitment_bytes = hex::decode(&p.commitment).unwrap_or_default();
232 let mut hasher = Sha256::new();
233 hasher.update(&commitment_bytes);
234 hasher.update(weight.to_le_bytes());
235 hasher.update(b"merkle-leaf-v1");
236 hasher.finalize().to_vec()
237 }).collect();
238
239 let merkle_root = merkle_root_of(&leaves);
241
242 let mut sig_seed = [0u8; 32];
245 let mut seed_hasher = Sha256::new();
246 seed_hasher.update(chaos_seed);
247 seed_hasher.update(b"aggregate-signing-key-v1");
248 sig_seed.copy_from_slice(&seed_hasher.finalize());
249
250 let sig_keypair = MlDsa65Keypair::from_secret_key_bytes(&sig_seed)
251 .map_err(|e| PrivacyError::Internal(alloc::format!("ML-DSA-65 agg keygen: {:?}", e)))?;
252
253 let mut sign_msg = Vec::with_capacity(32 + 32 + 12);
255 sign_msg.extend_from_slice(&merkle_root);
256 sign_msg.extend_from_slice(chaos_seed);
257 sign_msg.extend_from_slice(b"halo2-agg-v1");
258
259 let sig = sig_keypair.sign_deterministic(&sign_msg)
260 .map_err(|e| PrivacyError::Internal(alloc::format!("ML-DSA-65 agg sign: {:?}", e)))?;
261
262 let pub_leaves: Vec<Vec<u8>> = proofs.iter().map(|p| {
264 hex::decode(&p.public_inputs).unwrap_or_default()
265 }).collect();
266 let agg_public_root = merkle_root_of(&pub_leaves);
267
268 let mut ch_hasher = Sha256::new();
270 ch_hasher.update(b"halo2-fold-challenge-v1");
271 ch_hasher.update(chaos_seed);
272 for p in proofs {
273 ch_hasher.update(p.challenge.as_bytes());
274 }
275 let agg_challenge: [u8; 32] = ch_hasher.finalize().into();
276
277 let mut resp_hasher = Sha256::new();
279 resp_hasher.update(&merkle_root);
280 resp_hasher.update(&sig.bytes[..sig.bytes.len().min(64)]);
281 resp_hasher.update(b"halo2-fold-response-v1");
282 let agg_response: [u8; 32] = resp_hasher.finalize().into();
283
284 let proof_size = 32 + sig.bytes.len() + 64;
286
287 Ok(SnarkProof {
288 commitment: hex::encode(&merkle_root),
289 challenge: hex::encode(agg_challenge),
290 response: hex::encode(agg_response),
291 public_inputs: hex::encode(&agg_public_root),
292 proof_system: "pqcprivacy-snark-halo2-agg-v1".into(),
293 security_bits: 128,
294 proof_size,
295 chaos_seed: hex::encode(chaos_seed),
296 chsh_value: 0.0,
297 lyapunov: 4.5,
298 })
299}
300
301pub(crate) fn merkle_root_of(leaves: &[Vec<u8>]) -> Vec<u8> {
307 if leaves.is_empty() {
308 return Sha256::digest(b"empty-merkle-v1").to_vec();
309 }
310 if leaves.len() == 1 {
311 return leaves[0].clone();
312 }
313
314 let mut current: Vec<Vec<u8>> = leaves.to_vec();
315 while current.len() > 1 {
316 let mut next = Vec::new();
317 let mut i = 0;
318 while i < current.len() {
319 let left = ¤t[i];
320 let right = if i + 1 < current.len() {
321 ¤t[i + 1]
322 } else {
323 ¤t[i] };
325 let mut hasher = Sha256::new();
326 hasher.update(b"merkle-node-v1");
327 hasher.update(left);
328 hasher.update(right);
329 next.push(hasher.finalize().to_vec());
330 i += 2;
331 }
332 current = next;
333 }
334 current.remove(0)
335}
336
337pub(crate) fn merkle_path_of(leaves: &[Vec<u8>], index: usize) -> Vec<Vec<u8>> {
339 if leaves.len() <= 1 {
340 return Vec::new();
341 }
342
343 let mut path = Vec::new();
344 let mut current: Vec<Vec<u8>> = leaves.to_vec();
345 let mut idx = index;
346
347 while current.len() > 1 {
348 let sibling_idx = if idx % 2 == 0 {
349 if idx + 1 < current.len() { idx + 1 } else { idx }
351 } else {
352 idx - 1
354 };
355 path.push(current[sibling_idx].clone());
356
357 let mut next = Vec::new();
358 let mut i = 0;
359 while i < current.len() {
360 let left = ¤t[i];
361 let right = if i + 1 < current.len() {
362 ¤t[i + 1]
363 } else {
364 ¤t[i]
365 };
366 let mut hasher = Sha256::new();
367 hasher.update(b"merkle-node-v1");
368 hasher.update(left);
369 hasher.update(right);
370 next.push(hasher.finalize().to_vec());
371 i += 2;
372 }
373 current = next;
374 idx /= 2;
375 }
376 path
377}
378
379#[allow(dead_code)]
381pub(crate) fn verify_merkle_path(
382 leaf: &[u8],
383 path: &[Vec<u8>],
384 root: &[u8],
385 mut index: usize,
386) -> bool {
387 let mut current = leaf.to_vec();
388 for sibling in path {
389 let mut hasher = Sha256::new();
390 hasher.update(b"merkle-node-v1");
391 if index % 2 == 0 {
392 hasher.update(¤t);
394 hasher.update(sibling);
395 } else {
396 hasher.update(sibling);
398 hasher.update(¤t);
399 }
400 current = hasher.finalize().to_vec();
401 index /= 2;
402 }
403 current == root
404}
405
406#[cfg(test)]
407mod tests {
408 use super::*;
409 use sha2::{Digest, Sha256};
410
411 #[test]
412 fn test_prove_verify() {
413 let stmt: [u8; 32] = Sha256::digest(b"statement").into();
414 let wit: [u8; 32] = Sha256::digest(b"witness").into();
415 let seed = [42u8; 32];
416 let proof = prove(stmt, wit, &seed).unwrap();
417 assert!(verify(&proof, &stmt, &seed).is_ok());
418 }
419
420 #[test]
421 fn test_verify_wrong_statement_fails() {
422 let stmt: [u8; 32] = Sha256::digest(b"statement").into();
423 let wrong_stmt: [u8; 32] = Sha256::digest(b"wrong").into();
424 let wit: [u8; 32] = Sha256::digest(b"witness").into();
425 let seed = [42u8; 32];
426 let proof = prove(stmt, wit, &seed).unwrap();
427 assert!(verify(&proof, &wrong_stmt, &seed).is_err());
428 }
429
430 #[test]
431 fn test_verify_wrong_chaos_seed_fails() {
432 let stmt: [u8; 32] = Sha256::digest(b"statement").into();
433 let wit: [u8; 32] = Sha256::digest(b"witness").into();
434 let seed = [42u8; 32];
435 let wrong_seed = [99u8; 32];
436 let proof = prove(stmt, wit, &seed).unwrap();
437 assert!(verify(&proof, &stmt, &wrong_seed).is_err());
439 }
440
441 #[test]
442 fn test_aggregate() {
443 let seed = [1u8; 32];
444 let proofs: Vec<SnarkProof> = (0..4)
445 .map(|i| {
446 let stmt: [u8; 32] = Sha256::digest(alloc::format!("stmt{i}").as_bytes()).into();
447 let wit: [u8; 32] = Sha256::digest(alloc::format!("wit{i}").as_bytes()).into();
448 prove(stmt, wit, &seed).unwrap()
449 })
450 .collect();
451 let weights = vec![100u64, 200, 150, 50];
452 let agg = aggregate(&proofs, &weights, &seed).unwrap();
453 assert!(!agg.commitment.is_empty());
454 assert!(!agg.challenge.is_empty());
455 assert!(!agg.response.is_empty());
456 }
457
458 #[test]
459 fn test_merkle_root_single() {
460 let leaf = vec![1u8; 32];
461 let root = merkle_root_of(std::slice::from_ref(&leaf));
462 assert_eq!(root, leaf);
463 }
464
465 #[test]
466 fn test_merkle_path_verify() {
467 let leaves: Vec<Vec<u8>> = (0..4u8)
468 .map(|i| Sha256::digest([i]).to_vec())
469 .collect();
470 let root = merkle_root_of(&leaves);
471 for i in 0..leaves.len() {
472 let path = merkle_path_of(&leaves, i);
473 assert!(verify_merkle_path(&leaves[i], &path, &root, i));
474 }
475 }
476
477 #[test]
478 fn test_different_witnesses_different_commitments() {
479 let stmt: [u8; 32] = Sha256::digest(b"statement").into();
480 let wit1: [u8; 32] = Sha256::digest(b"witness1").into();
481 let wit2: [u8; 32] = Sha256::digest(b"witness2").into();
482 let seed = [0u8; 32];
483 let p1 = prove(stmt, wit1, &seed).unwrap();
484 let p2 = prove(stmt, wit2, &seed).unwrap();
485 assert_ne!(p1.commitment, p2.commitment);
487 }
488}