world_id_primitives/
proof.rs1use ruint::aliases::U256;
2use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
3
4use crate::FieldElement;
5
6#[derive(Debug, Default, Clone, PartialEq, Eq)]
15pub struct ZeroKnowledgeProof {
16 inner: [U256; 5],
18}
19
20impl ZeroKnowledgeProof {
21 #[must_use]
23 pub const fn as_ethereum_representation(&self) -> [U256; 5] {
24 self.inner
25 }
26
27 #[must_use]
29 pub const fn from_ethereum_representation(value: [U256; 5]) -> Self {
30 Self { inner: value }
31 }
32
33 #[must_use]
35 pub fn to_compressed_bytes(&self) -> Vec<u8> {
36 self.inner
37 .iter()
38 .flat_map(U256::to_be_bytes::<32>)
39 .collect()
40 }
41
42 pub fn from_compressed_bytes(bytes: &[u8]) -> Result<Self, String> {
47 if bytes.len() != 160 {
48 return Err(format!(
49 "Invalid length: expected 160 bytes, got {}",
50 bytes.len()
51 ));
52 }
53
54 let mut inner = [U256::ZERO; 5];
55 for (i, chunk) in bytes.chunks_exact(32).enumerate() {
56 let mut arr = [0u8; 32];
57 arr.copy_from_slice(chunk);
58 inner[i] = U256::from_be_bytes(arr);
59 }
60
61 Ok(Self { inner })
62 }
63}
64
65impl Serialize for ZeroKnowledgeProof {
66 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
67 where
68 S: Serializer,
69 {
70 let bytes = self.to_compressed_bytes();
71 if serializer.is_human_readable() {
72 serializer.serialize_str(&hex::encode(bytes))
73 } else {
74 serializer.serialize_bytes(&bytes)
75 }
76 }
77}
78
79impl<'de> Deserialize<'de> for ZeroKnowledgeProof {
80 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
81 where
82 D: Deserializer<'de>,
83 {
84 let bytes = if deserializer.is_human_readable() {
85 let hex_str = String::deserialize(deserializer)?;
86 hex::decode(hex_str).map_err(D::Error::custom)?
87 } else {
88 Vec::deserialize(deserializer)?
89 };
90
91 Self::from_compressed_bytes(&bytes).map_err(D::Error::custom)
92 }
93}
94
95impl From<ZeroKnowledgeProof> for [U256; 5] {
96 fn from(value: ZeroKnowledgeProof) -> Self {
97 value.inner
98 }
99}
100
101#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
106pub struct OwnershipProof {
107 pub proof: provekit_common::WhirR1CSProof,
109 pub merkle_root: FieldElement,
111}
112
113#[cfg(test)]
114mod tests {
115 use ruint::uint;
116
117 use super::*;
118
119 #[test]
120 fn test_encoding_round_trip() {
121 let proof = ZeroKnowledgeProof::default();
122 let compressed_bytes = proof.to_compressed_bytes();
123
124 assert_eq!(compressed_bytes.len(), 160);
125
126 let encoded = serde_json::to_string(&proof).unwrap();
127 assert_eq!(
128 encoded,
129 "\"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\""
130 );
131
132 let proof_from = ZeroKnowledgeProof::from_compressed_bytes(&compressed_bytes).unwrap();
133
134 assert_eq!(proof.inner, proof_from.inner);
135 }
136
137 #[test]
138 fn test_json_deserialization() {
139 let proof = ZeroKnowledgeProof::default();
140
141 let json_str = serde_json::to_string(&proof).unwrap();
143 let deserialized_proof: ZeroKnowledgeProof = serde_json::from_str(&json_str).unwrap();
144
145 assert_eq!(proof.inner, deserialized_proof.inner);
147 }
148
149 #[test]
150 fn test_from_ethereum_representation() {
151 let values = [
152 uint!(0x0000000000000000000000000000000000000000000000000000000000000001_U256),
153 uint!(0x0000000000000000000000000000000000000000000000000000000000000002_U256),
154 uint!(0x0000000000000000000000000000000000000000000000000000000000000003_U256),
155 uint!(0x0000000000000000000000000000000000000000000000000000000000000004_U256),
156 uint!(0x11d223ce7b91ac212f42cf50f0a3439ae3fcdba4ea32acb7f194d1051ed324c2_U256),
157 ];
158
159 let proof = ZeroKnowledgeProof::from_ethereum_representation(values);
160 assert_eq!(proof.as_ethereum_representation(), values);
161
162 let bytes = proof.to_compressed_bytes();
164 assert_eq!(bytes.len(), 160);
165
166 let proof_from_bytes = ZeroKnowledgeProof::from_compressed_bytes(&bytes).unwrap();
167 assert_eq!(proof.inner, proof_from_bytes.inner);
168 }
169
170 #[test]
171 fn test_invalid_bytes_length() {
172 let too_short = vec![0u8; 159];
173 let result = ZeroKnowledgeProof::from_compressed_bytes(&too_short);
174 assert!(result.is_err());
175 assert!(result.unwrap_err().contains("Invalid length"));
176
177 let too_long = vec![0u8; 161];
178 let result = ZeroKnowledgeProof::from_compressed_bytes(&too_long);
179 assert!(result.is_err());
180 assert!(result.unwrap_err().contains("Invalid length"));
181 }
182
183 #[test]
184 fn test_ownership_proof_json_roundtrip() {
185 let whir_proof = provekit_common::WhirR1CSProof {
186 narg_string: vec![1, 2, 3],
187 hints: vec![4, 5],
188 #[cfg(debug_assertions)]
189 pattern: vec![],
190 };
191 let proof = OwnershipProof {
192 proof: whir_proof,
193 merkle_root: crate::FieldElement::from(999u64),
194 };
195 let json = serde_json::to_string(&proof).unwrap();
196 assert!(json.contains("proof"));
197 assert!(json.contains("merkle_root"));
198 let decoded: OwnershipProof = serde_json::from_str(&json).unwrap();
199 assert_eq!(proof, decoded);
200 }
201
202 #[test]
208 fn test_ownership_proof_wrong_merkle_root_is_detected() {
209 let whir_proof = provekit_common::WhirR1CSProof {
210 narg_string: vec![10, 20, 30],
211 hints: vec![],
212 #[cfg(debug_assertions)]
213 pattern: vec![],
214 };
215 let proof = OwnershipProof {
216 proof: whir_proof.clone(),
217 merkle_root: crate::FieldElement::from(12345u64),
218 };
219
220 let tampered = OwnershipProof {
222 proof: whir_proof,
223 merkle_root: crate::FieldElement::from(99999u64),
224 };
225
226 assert_ne!(
227 proof.merkle_root, tampered.merkle_root,
228 "a flipped merkle root must differ from the original"
229 );
230 assert_ne!(proof, tampered);
231 }
232
233 #[test]
239 fn test_ownership_proof_tampered_bytes_is_detected() {
240 let original_bytes = vec![0xde, 0xad, 0xbe, 0xef];
241 let proof = OwnershipProof {
242 proof: provekit_common::WhirR1CSProof {
243 narg_string: original_bytes.clone(),
244 hints: vec![],
245 #[cfg(debug_assertions)]
246 pattern: vec![],
247 },
248 merkle_root: crate::FieldElement::from(42u64),
249 };
250
251 let mut tampered_bytes = original_bytes;
253 tampered_bytes[0] ^= 0xFF;
254
255 let tampered = OwnershipProof {
256 proof: provekit_common::WhirR1CSProof {
257 narg_string: tampered_bytes,
258 hints: vec![],
259 #[cfg(debug_assertions)]
260 pattern: vec![],
261 },
262 merkle_root: crate::FieldElement::from(42u64),
263 };
264
265 assert_ne!(
266 proof.proof.narg_string, tampered.proof.narg_string,
267 "tampered proof bytes must differ from the original"
268 );
269 assert_ne!(proof, tampered);
270 }
271}