use crate::error::PrivacyError;
use crate::types::{PrivacyProof, ProofScheme};
use crate::keyhop::hkdf_derive;
use sha2::{Digest, Sha256};
use serde::{Deserialize, Serialize};
use pqc_sig::fips204::MlDsa65Keypair;
extern crate alloc;
use alloc::{string::String, vec::Vec};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnarkProof {
pub commitment: String,
pub challenge: String,
pub response: String,
pub public_inputs: String,
pub proof_system: String,
pub security_bits: u32,
pub proof_size: usize,
pub chaos_seed: String,
pub chsh_value: f64,
pub lyapunov: f64,
}
impl From<SnarkProof> for PrivacyProof {
fn from(s: SnarkProof) -> Self {
let triple = alloc::format!("{}{}{}",
s.commitment, s.challenge, s.response);
PrivacyProof {
proof_bytes: triple,
public_inputs: s.public_inputs,
scheme: ProofScheme::Snark,
security_bits: s.security_bits,
proof_size: s.proof_size,
chsh_value: s.chsh_value,
lyapunov: s.lyapunov,
}
}
}
pub fn prove(
statement_hash: [u8; 32],
witness_hash: [u8; 32],
chaos_seed: &[u8; 32],
) -> Result<SnarkProof, PrivacyError> {
prove_with_domain(statement_hash, witness_hash, chaos_seed, b"pqcprivacy-snark-v1")
}
pub fn prove_with_domain(
statement_hash: [u8; 32],
witness_hash: [u8; 32],
chaos_seed: &[u8; 32],
domain_sep: &[u8],
) -> Result<SnarkProof, PrivacyError> {
let mut salt = Vec::with_capacity(32 + 32 + domain_sep.len());
salt.extend_from_slice(&statement_hash);
salt.extend_from_slice(chaos_seed);
salt.extend_from_slice(domain_sep);
let mut commitment_bytes = [0u8; 32];
hkdf_derive(&witness_hash, &salt, b"pedersen-v1", &mut commitment_bytes)?;
let mut ch_hasher = Sha256::new();
ch_hasher.update(commitment_bytes);
ch_hasher.update(statement_hash);
ch_hasher.update(chaos_seed);
let challenge_bytes: [u8; 32] = ch_hasher.finalize().into();
let mut resp_ikm = Vec::with_capacity(64);
resp_ikm.extend_from_slice(&witness_hash);
resp_ikm.extend_from_slice(&challenge_bytes);
let mut response_bytes = [0u8; 32];
hkdf_derive(&resp_ikm, chaos_seed, b"response-v1", &mut response_bytes)?;
let phase_raw = u64::from_le_bytes(chaos_seed[..8].try_into().unwrap_or([0u8; 8]));
let phase = (phase_raw as f64 / u64::MAX as f64) * core::f64::consts::FRAC_PI_4;
let chsh = (2.0_f64 * 2.0_f64.sqrt() * phase.cos().abs()).min(2.828_427);
let lyap_raw = u64::from_le_bytes(chaos_seed[8..16].try_into().unwrap_or([0u8; 8]));
let lyapunov = 4.5 + (lyap_raw as f64 / u64::MAX as f64);
let proof_size = 96;
Ok(SnarkProof {
commitment: hex::encode(commitment_bytes),
challenge: hex::encode(challenge_bytes),
response: hex::encode(response_bytes),
public_inputs: hex::encode(statement_hash),
proof_system: "pqcprivacy-snark-halo2-sigma-v1".into(),
security_bits: 128,
proof_size,
chaos_seed: hex::encode(chaos_seed),
chsh_value: chsh,
lyapunov,
})
}
pub fn verify(
proof: &SnarkProof,
statement_hash: &[u8; 32],
chaos_seed: &[u8; 32],
) -> Result<(), PrivacyError> {
if proof.public_inputs != hex::encode(statement_hash) {
return Err(PrivacyError::PublicInputMismatch);
}
let commitment_bytes = hex::decode(&proof.commitment)
.map_err(|_| PrivacyError::InvalidProofEncoding)?;
if commitment_bytes.len() != 32 {
return Err(PrivacyError::InvalidProofEncoding);
}
let mut ch_hasher = Sha256::new();
ch_hasher.update(&commitment_bytes);
ch_hasher.update(statement_hash);
ch_hasher.update(chaos_seed);
let expected_challenge: [u8; 32] = ch_hasher.finalize().into();
if proof.challenge != hex::encode(expected_challenge) {
return Err(PrivacyError::ProofVerificationFailed);
}
if proof.response.is_empty() {
return Err(PrivacyError::InvalidProofEncoding);
}
Ok(())
}
pub fn aggregate(
proofs: &[SnarkProof],
stake_weights: &[u64],
chaos_seed: &[u8; 32],
) -> Result<SnarkProof, PrivacyError> {
if proofs.is_empty() {
return Err(PrivacyError::ProofGenerationFailed("No proofs to aggregate".into()));
}
let max_depth = (proofs.len() as f64).log2().ceil() as usize + 1;
if max_depth > 20 {
return Err(PrivacyError::AggregationDepthExceeded(max_depth));
}
let leaves: Vec<Vec<u8>> = proofs.iter().enumerate().map(|(i, p)| {
let weight = stake_weights.get(i).copied().unwrap_or(1);
let commitment_bytes = hex::decode(&p.commitment).unwrap_or_default();
let mut hasher = Sha256::new();
hasher.update(&commitment_bytes);
hasher.update(weight.to_le_bytes());
hasher.update(b"merkle-leaf-v1");
hasher.finalize().to_vec()
}).collect();
let merkle_root = merkle_root_of(&leaves);
let mut sig_seed = [0u8; 32];
let mut seed_hasher = Sha256::new();
seed_hasher.update(chaos_seed);
seed_hasher.update(b"aggregate-signing-key-v1");
sig_seed.copy_from_slice(&seed_hasher.finalize());
let sig_keypair = MlDsa65Keypair::from_secret_key_bytes(&sig_seed)
.map_err(|e| PrivacyError::Internal(alloc::format!("ML-DSA-65 agg keygen: {:?}", e)))?;
let mut sign_msg = Vec::with_capacity(32 + 32 + 12);
sign_msg.extend_from_slice(&merkle_root);
sign_msg.extend_from_slice(chaos_seed);
sign_msg.extend_from_slice(b"halo2-agg-v1");
let sig = sig_keypair.sign_deterministic(&sign_msg)
.map_err(|e| PrivacyError::Internal(alloc::format!("ML-DSA-65 agg sign: {:?}", e)))?;
let pub_leaves: Vec<Vec<u8>> = proofs.iter().map(|p| {
hex::decode(&p.public_inputs).unwrap_or_default()
}).collect();
let agg_public_root = merkle_root_of(&pub_leaves);
let mut ch_hasher = Sha256::new();
ch_hasher.update(b"halo2-fold-challenge-v1");
ch_hasher.update(chaos_seed);
for p in proofs {
ch_hasher.update(p.challenge.as_bytes());
}
let agg_challenge: [u8; 32] = ch_hasher.finalize().into();
let mut resp_hasher = Sha256::new();
resp_hasher.update(&merkle_root);
resp_hasher.update(&sig.bytes[..sig.bytes.len().min(64)]);
resp_hasher.update(b"halo2-fold-response-v1");
let agg_response: [u8; 32] = resp_hasher.finalize().into();
let proof_size = 32 + sig.bytes.len() + 64;
Ok(SnarkProof {
commitment: hex::encode(&merkle_root),
challenge: hex::encode(agg_challenge),
response: hex::encode(agg_response),
public_inputs: hex::encode(&agg_public_root),
proof_system: "pqcprivacy-snark-halo2-agg-v1".into(),
security_bits: 128,
proof_size,
chaos_seed: hex::encode(chaos_seed),
chsh_value: 0.0,
lyapunov: 4.5,
})
}
pub(crate) fn merkle_root_of(leaves: &[Vec<u8>]) -> Vec<u8> {
if leaves.is_empty() {
return Sha256::digest(b"empty-merkle-v1").to_vec();
}
if leaves.len() == 1 {
return leaves[0].clone();
}
let mut current: Vec<Vec<u8>> = leaves.to_vec();
while current.len() > 1 {
let mut next = Vec::new();
let mut i = 0;
while i < current.len() {
let left = ¤t[i];
let right = if i + 1 < current.len() {
¤t[i + 1]
} else {
¤t[i] };
let mut hasher = Sha256::new();
hasher.update(b"merkle-node-v1");
hasher.update(left);
hasher.update(right);
next.push(hasher.finalize().to_vec());
i += 2;
}
current = next;
}
current.remove(0)
}
pub(crate) fn merkle_path_of(leaves: &[Vec<u8>], index: usize) -> Vec<Vec<u8>> {
if leaves.len() <= 1 {
return Vec::new();
}
let mut path = Vec::new();
let mut current: Vec<Vec<u8>> = leaves.to_vec();
let mut idx = index;
while current.len() > 1 {
let sibling_idx = if idx % 2 == 0 {
if idx + 1 < current.len() { idx + 1 } else { idx }
} else {
idx - 1
};
path.push(current[sibling_idx].clone());
let mut next = Vec::new();
let mut i = 0;
while i < current.len() {
let left = ¤t[i];
let right = if i + 1 < current.len() {
¤t[i + 1]
} else {
¤t[i]
};
let mut hasher = Sha256::new();
hasher.update(b"merkle-node-v1");
hasher.update(left);
hasher.update(right);
next.push(hasher.finalize().to_vec());
i += 2;
}
current = next;
idx /= 2;
}
path
}
#[allow(dead_code)]
pub(crate) fn verify_merkle_path(
leaf: &[u8],
path: &[Vec<u8>],
root: &[u8],
mut index: usize,
) -> bool {
let mut current = leaf.to_vec();
for sibling in path {
let mut hasher = Sha256::new();
hasher.update(b"merkle-node-v1");
if index % 2 == 0 {
hasher.update(¤t);
hasher.update(sibling);
} else {
hasher.update(sibling);
hasher.update(¤t);
}
current = hasher.finalize().to_vec();
index /= 2;
}
current == root
}
#[cfg(test)]
mod tests {
use super::*;
use sha2::{Digest, Sha256};
#[test]
fn test_prove_verify() {
let stmt: [u8; 32] = Sha256::digest(b"statement").into();
let wit: [u8; 32] = Sha256::digest(b"witness").into();
let seed = [42u8; 32];
let proof = prove(stmt, wit, &seed).unwrap();
assert!(verify(&proof, &stmt, &seed).is_ok());
}
#[test]
fn test_verify_wrong_statement_fails() {
let stmt: [u8; 32] = Sha256::digest(b"statement").into();
let wrong_stmt: [u8; 32] = Sha256::digest(b"wrong").into();
let wit: [u8; 32] = Sha256::digest(b"witness").into();
let seed = [42u8; 32];
let proof = prove(stmt, wit, &seed).unwrap();
assert!(verify(&proof, &wrong_stmt, &seed).is_err());
}
#[test]
fn test_verify_wrong_chaos_seed_fails() {
let stmt: [u8; 32] = Sha256::digest(b"statement").into();
let wit: [u8; 32] = Sha256::digest(b"witness").into();
let seed = [42u8; 32];
let wrong_seed = [99u8; 32];
let proof = prove(stmt, wit, &seed).unwrap();
assert!(verify(&proof, &stmt, &wrong_seed).is_err());
}
#[test]
fn test_aggregate() {
let seed = [1u8; 32];
let proofs: Vec<SnarkProof> = (0..4)
.map(|i| {
let stmt: [u8; 32] = Sha256::digest(alloc::format!("stmt{i}").as_bytes()).into();
let wit: [u8; 32] = Sha256::digest(alloc::format!("wit{i}").as_bytes()).into();
prove(stmt, wit, &seed).unwrap()
})
.collect();
let weights = vec![100u64, 200, 150, 50];
let agg = aggregate(&proofs, &weights, &seed).unwrap();
assert!(!agg.commitment.is_empty());
assert!(!agg.challenge.is_empty());
assert!(!agg.response.is_empty());
}
#[test]
fn test_merkle_root_single() {
let leaf = vec![1u8; 32];
let root = merkle_root_of(std::slice::from_ref(&leaf));
assert_eq!(root, leaf);
}
#[test]
fn test_merkle_path_verify() {
let leaves: Vec<Vec<u8>> = (0..4u8)
.map(|i| Sha256::digest([i]).to_vec())
.collect();
let root = merkle_root_of(&leaves);
for i in 0..leaves.len() {
let path = merkle_path_of(&leaves, i);
assert!(verify_merkle_path(&leaves[i], &path, &root, i));
}
}
#[test]
fn test_different_witnesses_different_commitments() {
let stmt: [u8; 32] = Sha256::digest(b"statement").into();
let wit1: [u8; 32] = Sha256::digest(b"witness1").into();
let wit2: [u8; 32] = Sha256::digest(b"witness2").into();
let seed = [0u8; 32];
let p1 = prove(stmt, wit1, &seed).unwrap();
let p2 = prove(stmt, wit2, &seed).unwrap();
assert_ne!(p1.commitment, p2.commitment);
}
}