1use crate::error::WalletKitError;
2
3use alloy_core::sol_types::SolValue;
4use semaphore_rs::{
5 hash_to_field, identity,
6 packed_proof::PackedProof,
7 protocol::{generate_nullifier_hash, generate_proof, Proof},
8};
9
10use serde::Serialize;
11
12use crate::{
13 credential_type::CredentialType, merkle_tree::MerkleTreeProof, u256::U256Wrapper,
14};
15
16#[derive(Clone, PartialEq, Eq, Debug)]
22#[cfg_attr(feature = "ffi", derive(uniffi::Object))]
23pub struct ProofContext {
24 pub external_nullifier: U256Wrapper,
27 pub credential_type: CredentialType,
29 pub signal: U256Wrapper,
31}
32
33#[cfg_attr(feature = "ffi", uniffi::export)]
34impl ProofContext {
35 #[must_use]
51 #[cfg_attr(feature = "ffi", uniffi::constructor)]
52 pub fn new(
53 app_id: &str,
54 action: Option<String>,
55 signal: Option<String>,
56 credential_type: CredentialType,
57 ) -> Self {
58 Self::new_from_bytes(
59 app_id,
60 action.map(std::string::String::into_bytes),
61 signal.map(std::string::String::into_bytes),
62 credential_type,
63 )
64 }
65
66 #[must_use]
77 #[cfg_attr(feature = "ffi", uniffi::constructor)]
78 #[allow(clippy::needless_pass_by_value)]
79 pub fn new_from_bytes(
80 app_id: &str,
81 action: Option<Vec<u8>>,
82 signal: Option<Vec<u8>>,
83 credential_type: CredentialType,
84 ) -> Self {
85 let mut pre_image = hash_to_field(app_id.as_bytes()).abi_encode_packed();
86
87 if let Some(action) = action {
88 pre_image.extend_from_slice(&action);
89 }
90
91 let external_nullifier = hash_to_field(&pre_image).into();
92
93 Self {
94 external_nullifier,
95 credential_type,
96 signal: hash_to_field(signal.unwrap_or_default().as_slice()).into(),
97 }
98 }
99}
100
101#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
108#[cfg_attr(feature = "ffi", derive(uniffi::Object))]
109#[allow(clippy::module_name_repetitions)]
110pub struct ProofOutput {
111 pub merkle_root: U256Wrapper,
114 pub nullifier_hash: U256Wrapper,
117 #[serde(skip_serializing)]
119 pub raw_proof: Proof,
120 pub proof: PackedProof,
123}
124
125#[cfg_attr(feature = "ffi", uniffi::export)]
126impl ProofOutput {
127 pub fn to_json(&self) -> Result<String, WalletKitError> {
132 serde_json::to_string(self).map_err(|_| WalletKitError::SerializationError)
133 }
134
135 #[must_use]
137 pub const fn get_nullifier_hash(&self) -> U256Wrapper {
138 self.nullifier_hash
139 }
140
141 #[must_use]
143 pub const fn get_merkle_root(&self) -> U256Wrapper {
144 self.merkle_root
145 }
146
147 #[must_use]
149 pub fn get_proof_as_string(&self) -> String {
150 self.proof.to_string()
151 }
152}
153
154pub fn generate_proof_with_semaphore_identity(
161 identity: &identity::Identity,
162 merkle_tree_proof: &MerkleTreeProof,
163 context: &ProofContext,
164) -> Result<ProofOutput, WalletKitError> {
165 #[cfg(not(feature = "semaphore"))]
166 return Err(WalletKitError::SemaphoreNotEnabled);
167
168 let merkle_root = merkle_tree_proof.merkle_root; let external_nullifier_hash = context.external_nullifier.into();
171 let nullifier_hash =
172 generate_nullifier_hash(identity, external_nullifier_hash).into();
173
174 let proof = generate_proof(
175 identity,
176 merkle_tree_proof.as_poseidon_proof(),
177 external_nullifier_hash,
178 context.signal.into(),
179 )?;
180
181 Ok(ProofOutput {
182 merkle_root,
183 nullifier_hash,
184 raw_proof: proof,
185 proof: PackedProof::from(proof),
186 })
187}
188
189#[cfg(test)]
190mod external_nullifier_tests {
191 use alloy_core::primitives::address;
192 use ruint::{aliases::U256, uint};
193
194 use super::*;
195
196 #[test]
197 fn test_context_and_external_nullifier_hash_generation() {
198 let context = ProofContext::new(
199 "app_369183bd38f1641b6964ab51d7a20434",
200 None,
201 None,
202 CredentialType::Orb,
203 );
204 assert_eq!(
205 context.external_nullifier.to_hex_string(),
206 "0x0073e4a6b670e81dc619b1f8703aa7491dc5aaadf75409aba0ac2414014c0227"
207 );
208
209 let context = ProofContext::new(
211 "app_369183bd38f1641b6964ab51d7a20434",
212 Some(String::new()),
213 None,
214 CredentialType::Orb,
215 );
216 assert_eq!(
217 context.external_nullifier.to_hex_string(),
218 "0x0073e4a6b670e81dc619b1f8703aa7491dc5aaadf75409aba0ac2414014c0227"
219 );
220 }
221
222 #[test]
225 fn test_external_nullifier_hash_generation_string_action_staging() {
226 let context = ProofContext::new(
227 "app_staging_45068dca85829d2fd90e2dd6f0bff997",
228 Some("test-action-qli8g".to_string()),
229 None,
230 CredentialType::Orb,
231 );
232 assert_eq!(
233 context.external_nullifier.to_hex_string(),
234 "0x00d8b157e767dc59faa533120ed0ce34fc51a71937292ea8baed6ee6f4fda866"
235 );
236 }
237
238 #[test]
239 fn test_external_nullifier_hash_generation_string_action() {
240 let context = ProofContext::new(
241 "app_10eb12bd96d8f7202892ff25f094c803",
242 Some("test-123123".to_string()),
243 None,
244 CredentialType::Orb,
245 );
246 assert_eq!(
247 context.external_nullifier.0,
248 uint!(
249 0x0065ebab05692ff2e0816cc4c3b83216c33eaa4d906c6495add6323fe0e2dc89_U256
251 )
252 );
253 }
254
255 #[test]
256 fn test_external_nullifier_hash_generation_with_advanced_abi_encoded_values() {
257 let custom_action = [
258 address!("541f3cc5772a64f2ba0a47e83236CcE2F089b188").abi_encode_packed(),
259 U256::from(1).abi_encode_packed(),
260 "hello".abi_encode_packed(),
261 ]
262 .concat();
263
264 let context = ProofContext::new_from_bytes(
265 "app_10eb12bd96d8f7202892ff25f094c803",
266 Some(custom_action),
267 None,
268 CredentialType::Orb,
269 );
270 assert_eq!(
271 context.external_nullifier.to_hex_string(),
272 "0x00f974ff06219e8ca992073d8bbe05084f81250dbd8f37cae733f24fcc0c5ffd"
274 );
275 }
276
277 #[test]
278 fn test_external_nullifier_hash_generation_with_advanced_abi_encoded_values_staging(
279 ) {
280 let custom_action = [
281 "world".abi_encode_packed(),
282 U256::from(1).abi_encode_packed(),
283 "hello".abi_encode_packed(),
284 ]
285 .concat();
286
287 let context = ProofContext::new_from_bytes(
288 "app_staging_45068dca85829d2fd90e2dd6f0bff997",
289 Some(custom_action),
290 None,
291 CredentialType::Orb,
292 );
293 assert_eq!(
294 context.external_nullifier.to_hex_string(),
295 "0x005b49f95e822c7c37f4f043421689b11f880e617faa5cd0391803bc9bcc63c0"
297 );
298 }
299}
300
301#[cfg(test)]
302mod proof_tests {
303
304 use regex::Regex;
305 use semaphore_rs::protocol::verify_proof;
306 use serde_json::Value;
307
308 use super::*;
309
310 fn helper_load_merkle_proof() -> MerkleTreeProof {
311 let json_merkle: Value = serde_json::from_str(include_str!(
312 "../tests/fixtures/inclusion_proof.json"
313 ))
314 .unwrap();
315 MerkleTreeProof::from_json_proof(
316 &serde_json::to_string(&json_merkle["proof"]).unwrap(),
317 json_merkle["root"].as_str().unwrap(),
318 )
319 .unwrap()
320 }
321
322 #[test]
323 fn test_proof_generation() {
324 let context = ProofContext::new(
325 "app_staging_45068dca85829d2fd90e2dd6f0bff997",
326 Some("test-action-89tcf".to_string()),
327 None,
328 CredentialType::Device,
329 );
330
331 let mut secret = b"not_a_real_secret".to_vec();
332
333 let identity = semaphore_rs::identity::Identity::from_secret(
334 &mut secret,
335 Some(context.credential_type.as_identity_trapdoor()),
336 );
337
338 assert_eq!(
339 U256Wrapper::from(identity.commitment()).to_hex_string(),
340 "0x1a060ef75540e13711f074b779a419c126ab5a89d2c2e7d01e64dfd121e44671"
341 );
342
343 let zkp = generate_proof_with_semaphore_identity(
345 &identity,
346 &helper_load_merkle_proof(),
347 &context,
348 )
349 .unwrap();
350
351 assert_eq!(
352 zkp.merkle_root.to_hex_string(),
353 "0x2f3a95b6df9074a19bf46e2308d7f5696e9dca49e0d64ef49a1425bbf40e0c02"
354 );
355
356 assert_eq!(
357 zkp.nullifier_hash.to_hex_string(),
358 "0x11d194ff98df5c8e239e6b6e33cce7fb1b419344cb13e064350a917970c8fea4"
359 );
360
361 assert!(verify_proof(
363 *zkp.merkle_root,
364 *zkp.nullifier_hash,
365 hash_to_field(&[]),
366 *context.external_nullifier,
367 &zkp.raw_proof,
368 30
369 )
370 .unwrap());
371 }
372
373 #[test]
374 fn test_proof_json_encoding() {
375 let context = ProofContext::new(
376 "app_staging_45068dca85829d2fd90e2dd6f0bff997",
377 Some("test-action-89tcf".to_string()),
378 None,
379 CredentialType::Device,
380 );
381
382 let mut secret = b"not_a_real_secret".to_vec();
383 let identity = semaphore_rs::identity::Identity::from_secret(
384 &mut secret,
385 Some(context.credential_type.as_identity_trapdoor()),
386 );
387
388 let zkp = generate_proof_with_semaphore_identity(
390 &identity,
391 &helper_load_merkle_proof(),
392 &context,
393 )
394 .unwrap();
395
396 let parsed_json: Value = serde_json::from_str(&zkp.to_json().unwrap()).unwrap();
397
398 assert_eq!(
399 parsed_json["nullifier_hash"].as_str().unwrap(),
400 "0x11d194ff98df5c8e239e6b6e33cce7fb1b419344cb13e064350a917970c8fea4"
401 );
402 assert_eq!(
403 parsed_json["merkle_root"].as_str().unwrap(),
404 "0x2f3a95b6df9074a19bf46e2308d7f5696e9dca49e0d64ef49a1425bbf40e0c02"
405 );
406
407 let packed_proof_pattern = r"^0x[a-f0-9]{400,600}$";
409 let re = Regex::new(packed_proof_pattern).unwrap();
410 assert!(re.is_match(parsed_json["proof"].as_str().unwrap()));
411
412 assert_eq!(
413 zkp.get_nullifier_hash().to_hex_string(),
414 parsed_json["nullifier_hash"].as_str().unwrap()
415 );
416 assert_eq!(
417 zkp.get_merkle_root().to_hex_string(),
418 parsed_json["merkle_root"].as_str().unwrap()
419 );
420 assert_eq!(
421 zkp.get_proof_as_string(),
422 parsed_json["proof"].as_str().unwrap()
423 );
424 }
425
426 #[test]
427 const fn test_proof_generation_with_local_merkle_tree() {
428 }
430
431 #[ignore = "To be run manually as it requires a call to the Sign up Sequencer"]
432 #[test]
433 fn test_proof_verification_with_sign_up_sequencer() {
434 todo!("implement me");
435 }
436
437 #[ignore = "To be run manually as it requires a call to the Developer Portal"]
438 #[test]
439 fn test_proof_verification_with_developer_portal() {
440 todo!("implement me");
441 }
442}