use ruint::aliases::U256;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
use crate::FieldElement;
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ZeroKnowledgeProof {
inner: [U256; 5],
}
impl ZeroKnowledgeProof {
#[must_use]
pub const fn as_ethereum_representation(&self) -> [U256; 5] {
self.inner
}
#[must_use]
pub const fn from_ethereum_representation(value: [U256; 5]) -> Self {
Self { inner: value }
}
#[must_use]
pub fn to_compressed_bytes(&self) -> Vec<u8> {
self.inner
.iter()
.flat_map(U256::to_be_bytes::<32>)
.collect()
}
pub fn from_compressed_bytes(bytes: &[u8]) -> Result<Self, String> {
if bytes.len() != 160 {
return Err(format!(
"Invalid length: expected 160 bytes, got {}",
bytes.len()
));
}
let mut inner = [U256::ZERO; 5];
for (i, chunk) in bytes.chunks_exact(32).enumerate() {
let mut arr = [0u8; 32];
arr.copy_from_slice(chunk);
inner[i] = U256::from_be_bytes(arr);
}
Ok(Self { inner })
}
}
impl Serialize for ZeroKnowledgeProof {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let bytes = self.to_compressed_bytes();
if serializer.is_human_readable() {
serializer.serialize_str(&hex::encode(bytes))
} else {
serializer.serialize_bytes(&bytes)
}
}
}
impl<'de> Deserialize<'de> for ZeroKnowledgeProof {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let bytes = if deserializer.is_human_readable() {
let hex_str = String::deserialize(deserializer)?;
hex::decode(hex_str).map_err(D::Error::custom)?
} else {
Vec::deserialize(deserializer)?
};
Self::from_compressed_bytes(&bytes).map_err(D::Error::custom)
}
}
impl From<ZeroKnowledgeProof> for [U256; 5] {
fn from(value: ZeroKnowledgeProof) -> Self {
value.inner
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OwnershipProof {
pub proof: provekit_common::WhirR1CSProof,
pub merkle_root: FieldElement,
}
#[cfg(test)]
mod tests {
use ruint::uint;
use super::*;
#[test]
fn test_encoding_round_trip() {
let proof = ZeroKnowledgeProof::default();
let compressed_bytes = proof.to_compressed_bytes();
assert_eq!(compressed_bytes.len(), 160);
let encoded = serde_json::to_string(&proof).unwrap();
assert_eq!(
encoded,
"\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\""
);
let proof_from = ZeroKnowledgeProof::from_compressed_bytes(&compressed_bytes).unwrap();
assert_eq!(proof.inner, proof_from.inner);
}
#[test]
fn test_json_deserialization() {
let proof = ZeroKnowledgeProof::default();
let json_str = serde_json::to_string(&proof).unwrap();
let deserialized_proof: ZeroKnowledgeProof = serde_json::from_str(&json_str).unwrap();
assert_eq!(proof.inner, deserialized_proof.inner);
}
#[test]
fn test_from_ethereum_representation() {
let values = [
uint!(0x0000000000000000000000000000000000000000000000000000000000000001_U256),
uint!(0x0000000000000000000000000000000000000000000000000000000000000002_U256),
uint!(0x0000000000000000000000000000000000000000000000000000000000000003_U256),
uint!(0x0000000000000000000000000000000000000000000000000000000000000004_U256),
uint!(0x11d223ce7b91ac212f42cf50f0a3439ae3fcdba4ea32acb7f194d1051ed324c2_U256),
];
let proof = ZeroKnowledgeProof::from_ethereum_representation(values);
assert_eq!(proof.as_ethereum_representation(), values);
let bytes = proof.to_compressed_bytes();
assert_eq!(bytes.len(), 160);
let proof_from_bytes = ZeroKnowledgeProof::from_compressed_bytes(&bytes).unwrap();
assert_eq!(proof.inner, proof_from_bytes.inner);
}
#[test]
fn test_invalid_bytes_length() {
let too_short = vec![0u8; 159];
let result = ZeroKnowledgeProof::from_compressed_bytes(&too_short);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid length"));
let too_long = vec![0u8; 161];
let result = ZeroKnowledgeProof::from_compressed_bytes(&too_long);
assert!(result.is_err());
assert!(result.unwrap_err().contains("Invalid length"));
}
#[test]
fn test_ownership_proof_json_roundtrip() {
let whir_proof = provekit_common::WhirR1CSProof {
narg_string: vec![1, 2, 3],
hints: vec![4, 5],
#[cfg(debug_assertions)]
pattern: vec![],
};
let proof = OwnershipProof {
proof: whir_proof,
merkle_root: crate::FieldElement::from(999u64),
};
let json = serde_json::to_string(&proof).unwrap();
assert!(json.contains("proof"));
assert!(json.contains("merkle_root"));
let decoded: OwnershipProof = serde_json::from_str(&json).unwrap();
assert_eq!(proof, decoded);
}
#[test]
fn test_ownership_proof_wrong_merkle_root_is_detected() {
let whir_proof = provekit_common::WhirR1CSProof {
narg_string: vec![10, 20, 30],
hints: vec![],
#[cfg(debug_assertions)]
pattern: vec![],
};
let proof = OwnershipProof {
proof: whir_proof.clone(),
merkle_root: crate::FieldElement::from(12345u64),
};
let tampered = OwnershipProof {
proof: whir_proof,
merkle_root: crate::FieldElement::from(99999u64),
};
assert_ne!(
proof.merkle_root, tampered.merkle_root,
"a flipped merkle root must differ from the original"
);
assert_ne!(proof, tampered);
}
#[test]
fn test_ownership_proof_tampered_bytes_is_detected() {
let original_bytes = vec![0xde, 0xad, 0xbe, 0xef];
let proof = OwnershipProof {
proof: provekit_common::WhirR1CSProof {
narg_string: original_bytes.clone(),
hints: vec![],
#[cfg(debug_assertions)]
pattern: vec![],
},
merkle_root: crate::FieldElement::from(42u64),
};
let mut tampered_bytes = original_bytes;
tampered_bytes[0] ^= 0xFF;
let tampered = OwnershipProof {
proof: provekit_common::WhirR1CSProof {
narg_string: tampered_bytes,
hints: vec![],
#[cfg(debug_assertions)]
pattern: vec![],
},
merkle_root: crate::FieldElement::from(42u64),
};
assert_ne!(
proof.proof.narg_string, tampered.proof.narg_string,
"tampered proof bytes must differ from the original"
);
assert_ne!(proof, tampered);
}
}