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
10#[cfg(feature = "legacy-nullifiers")]
11use semaphore_rs::MODULUS;
12
13use serde::Serialize;
14
15use crate::{
16 credential_type::CredentialType, merkle_tree::MerkleTreeProof, u256::U256Wrapper,
17};
18
19#[derive(Clone, PartialEq, Eq, Debug)]
25#[cfg_attr(feature = "ffi", derive(uniffi::Object))]
26pub struct ProofContext {
27 pub external_nullifier: U256Wrapper,
30 pub credential_type: CredentialType,
32 pub signal_hash: U256Wrapper,
35}
36
37#[cfg_attr(feature = "ffi", uniffi::export)]
38impl ProofContext {
39 #[must_use]
55 #[cfg_attr(feature = "ffi", uniffi::constructor)]
56 pub fn new(
57 app_id: &str,
58 action: Option<String>,
59 signal: Option<String>,
60 credential_type: CredentialType,
61 ) -> Self {
62 Self::new_from_bytes(
63 app_id,
64 action.map(std::string::String::into_bytes),
65 signal.map(std::string::String::into_bytes),
66 credential_type,
67 )
68 }
69
70 #[must_use]
81 #[cfg_attr(feature = "ffi", uniffi::constructor)]
82 #[allow(clippy::needless_pass_by_value)]
83 pub fn new_from_bytes(
84 app_id: &str,
85 action: Option<Vec<u8>>,
86 signal: Option<Vec<u8>>,
87 credential_type: CredentialType,
88 ) -> Self {
89 let mut pre_image = hash_to_field(app_id.as_bytes()).abi_encode_packed();
90
91 if let Some(action) = action {
92 pre_image.extend_from_slice(&action);
93 }
94
95 let external_nullifier = hash_to_field(&pre_image).into();
96
97 Self {
98 external_nullifier,
99 credential_type,
100 signal_hash: hash_to_field(signal.unwrap_or_default().as_slice()).into(),
101 }
102 }
103}
104
105#[cfg_attr(feature = "ffi", uniffi::export)]
106#[cfg(feature = "legacy-nullifiers")]
107impl ProofContext {
108 #[must_use]
125 #[cfg_attr(feature = "ffi", uniffi::constructor)]
126 pub fn legacy_new_from_pre_image_external_nullifier(
127 external_nullifier: &[u8],
128 credential_type: CredentialType,
129 signal: Option<Vec<u8>>,
130 ) -> Self {
131 let external_nullifier: U256Wrapper = hash_to_field(external_nullifier).into();
132 Self {
133 external_nullifier,
134 credential_type,
135 signal_hash: hash_to_field(signal.unwrap_or_default().as_slice()).into(),
136 }
137 }
138
139 #[cfg_attr(feature = "ffi", uniffi::constructor)]
160 pub fn legacy_new_from_raw_external_nullifier(
161 external_nullifier: &U256Wrapper,
162 credential_type: CredentialType,
163 signal: Option<Vec<u8>>,
164 ) -> Result<Self, WalletKitError> {
165 if external_nullifier.0 > MODULUS {
166 return Err(WalletKitError::InvalidNumber);
167 }
168
169 Ok(Self {
170 external_nullifier: *external_nullifier,
171 credential_type,
172 signal_hash: hash_to_field(signal.unwrap_or_default().as_slice()).into(),
173 })
174 }
175}
176
177#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
184#[cfg_attr(feature = "ffi", derive(uniffi::Object))]
185#[allow(clippy::module_name_repetitions)]
186pub struct ProofOutput {
187 pub merkle_root: U256Wrapper,
190 pub nullifier_hash: U256Wrapper,
193 #[serde(skip_serializing)]
195 pub raw_proof: Proof,
196 pub proof: PackedProof,
199}
200
201#[cfg_attr(feature = "ffi", uniffi::export)]
202impl ProofOutput {
203 pub fn to_json(&self) -> Result<String, WalletKitError> {
208 serde_json::to_string(self).map_err(|_| WalletKitError::SerializationError)
209 }
210
211 #[must_use]
213 pub const fn get_nullifier_hash(&self) -> U256Wrapper {
214 self.nullifier_hash
215 }
216
217 #[must_use]
219 pub const fn get_merkle_root(&self) -> U256Wrapper {
220 self.merkle_root
221 }
222
223 #[must_use]
225 pub fn get_proof_as_string(&self) -> String {
226 self.proof.to_string()
227 }
228}
229
230pub fn generate_proof_with_semaphore_identity(
237 identity: &identity::Identity,
238 merkle_tree_proof: &MerkleTreeProof,
239 context: &ProofContext,
240) -> Result<ProofOutput, WalletKitError> {
241 #[cfg(not(feature = "semaphore"))]
242 return Err(WalletKitError::SemaphoreNotEnabled);
243
244 let merkle_root = merkle_tree_proof.merkle_root; let external_nullifier_hash = context.external_nullifier.into();
247 let nullifier_hash =
248 generate_nullifier_hash(identity, external_nullifier_hash).into();
249
250 let proof = generate_proof(
251 identity,
252 merkle_tree_proof.as_poseidon_proof(),
253 external_nullifier_hash,
254 context.signal_hash.into(),
255 )?;
256
257 Ok(ProofOutput {
258 merkle_root,
259 nullifier_hash,
260 raw_proof: proof,
261 proof: PackedProof::from(proof),
262 })
263}
264
265#[cfg(test)]
266mod external_nullifier_tests {
267 use alloy_core::primitives::address;
268 use ruint::{aliases::U256, uint};
269
270 use super::*;
271
272 #[test]
273 fn test_context_and_external_nullifier_hash_generation() {
274 let context = ProofContext::new(
275 "app_369183bd38f1641b6964ab51d7a20434",
276 None,
277 None,
278 CredentialType::Orb,
279 );
280 assert_eq!(
281 context.external_nullifier.to_hex_string(),
282 "0x0073e4a6b670e81dc619b1f8703aa7491dc5aaadf75409aba0ac2414014c0227"
283 );
284
285 let context = ProofContext::new(
287 "app_369183bd38f1641b6964ab51d7a20434",
288 Some(String::new()),
289 None,
290 CredentialType::Orb,
291 );
292 assert_eq!(
293 context.external_nullifier.to_hex_string(),
294 "0x0073e4a6b670e81dc619b1f8703aa7491dc5aaadf75409aba0ac2414014c0227"
295 );
296 }
297
298 #[test]
301 fn test_external_nullifier_hash_generation_string_action_staging() {
302 let context = ProofContext::new(
303 "app_staging_45068dca85829d2fd90e2dd6f0bff997",
304 Some("test-action-qli8g".to_string()),
305 None,
306 CredentialType::Orb,
307 );
308 assert_eq!(
309 context.external_nullifier.to_hex_string(),
310 "0x00d8b157e767dc59faa533120ed0ce34fc51a71937292ea8baed6ee6f4fda866"
311 );
312 }
313
314 #[test]
315 fn test_external_nullifier_hash_generation_string_action() {
316 let context = ProofContext::new(
317 "app_10eb12bd96d8f7202892ff25f094c803",
318 Some("test-123123".to_string()),
319 None,
320 CredentialType::Orb,
321 );
322 assert_eq!(
323 context.external_nullifier.0,
324 uint!(
325 0x0065ebab05692ff2e0816cc4c3b83216c33eaa4d906c6495add6323fe0e2dc89_U256
327 )
328 );
329 }
330
331 #[test]
332 fn test_external_nullifier_hash_generation_with_advanced_abi_encoded_values() {
333 let custom_action = [
334 address!("541f3cc5772a64f2ba0a47e83236CcE2F089b188").abi_encode_packed(),
335 U256::from(1).abi_encode_packed(),
336 "hello".abi_encode_packed(),
337 ]
338 .concat();
339
340 let context = ProofContext::new_from_bytes(
341 "app_10eb12bd96d8f7202892ff25f094c803",
342 Some(custom_action),
343 None,
344 CredentialType::Orb,
345 );
346 assert_eq!(
347 context.external_nullifier.to_hex_string(),
348 "0x00f974ff06219e8ca992073d8bbe05084f81250dbd8f37cae733f24fcc0c5ffd"
350 );
351 }
352
353 #[test]
354 fn test_external_nullifier_hash_generation_with_advanced_abi_encoded_values_staging(
355 ) {
356 let custom_action = [
357 "world".abi_encode_packed(),
358 U256::from(1).abi_encode_packed(),
359 "hello".abi_encode_packed(),
360 ]
361 .concat();
362
363 let context = ProofContext::new_from_bytes(
364 "app_staging_45068dca85829d2fd90e2dd6f0bff997",
365 Some(custom_action),
366 None,
367 CredentialType::Orb,
368 );
369 assert_eq!(
370 context.external_nullifier.to_hex_string(),
371 "0x005b49f95e822c7c37f4f043421689b11f880e617faa5cd0391803bc9bcc63c0"
373 );
374 }
375}
376
377#[cfg(test)]
378mod proof_tests {
379
380 use regex::Regex;
381 use semaphore_rs::protocol::verify_proof;
382 use serde_json::Value;
383
384 #[cfg(feature = "legacy-nullifiers")]
385 use ruint::{aliases::U256, uint};
386
387 use super::*;
388
389 fn helper_load_merkle_proof() -> MerkleTreeProof {
390 let json_merkle: Value = serde_json::from_str(include_str!(
391 "../tests/fixtures/inclusion_proof.json"
392 ))
393 .unwrap();
394 MerkleTreeProof::from_json_proof(
395 &serde_json::to_string(&json_merkle["proof"]).unwrap(),
396 json_merkle["root"].as_str().unwrap(),
397 )
398 .unwrap()
399 }
400
401 #[test]
402 fn test_proof_generation() {
403 let context = ProofContext::new(
404 "app_staging_45068dca85829d2fd90e2dd6f0bff997",
405 Some("test-action-89tcf".to_string()),
406 None,
407 CredentialType::Device,
408 );
409
410 let mut secret = b"not_a_real_secret".to_vec();
411
412 let identity = semaphore_rs::identity::Identity::from_secret(
413 &mut secret,
414 Some(context.credential_type.as_identity_trapdoor()),
415 );
416
417 assert_eq!(
418 U256Wrapper::from(identity.commitment()).to_hex_string(),
419 "0x1a060ef75540e13711f074b779a419c126ab5a89d2c2e7d01e64dfd121e44671"
420 );
421
422 let zkp = generate_proof_with_semaphore_identity(
424 &identity,
425 &helper_load_merkle_proof(),
426 &context,
427 )
428 .unwrap();
429
430 assert_eq!(
431 zkp.merkle_root.to_hex_string(),
432 "0x2f3a95b6df9074a19bf46e2308d7f5696e9dca49e0d64ef49a1425bbf40e0c02"
433 );
434
435 assert_eq!(
436 zkp.nullifier_hash.to_hex_string(),
437 "0x11d194ff98df5c8e239e6b6e33cce7fb1b419344cb13e064350a917970c8fea4"
438 );
439
440 assert!(verify_proof(
442 *zkp.merkle_root,
443 *zkp.nullifier_hash,
444 hash_to_field(&[]),
445 *context.external_nullifier,
446 &zkp.raw_proof,
447 30
448 )
449 .unwrap());
450 }
451
452 #[test]
453 fn test_proof_json_encoding() {
454 let context = ProofContext::new(
455 "app_staging_45068dca85829d2fd90e2dd6f0bff997",
456 Some("test-action-89tcf".to_string()),
457 None,
458 CredentialType::Device,
459 );
460
461 let mut secret = b"not_a_real_secret".to_vec();
462 let identity = semaphore_rs::identity::Identity::from_secret(
463 &mut secret,
464 Some(context.credential_type.as_identity_trapdoor()),
465 );
466
467 let zkp = generate_proof_with_semaphore_identity(
469 &identity,
470 &helper_load_merkle_proof(),
471 &context,
472 )
473 .unwrap();
474
475 let parsed_json: Value = serde_json::from_str(&zkp.to_json().unwrap()).unwrap();
476
477 assert_eq!(
478 parsed_json["nullifier_hash"].as_str().unwrap(),
479 "0x11d194ff98df5c8e239e6b6e33cce7fb1b419344cb13e064350a917970c8fea4"
480 );
481 assert_eq!(
482 parsed_json["merkle_root"].as_str().unwrap(),
483 "0x2f3a95b6df9074a19bf46e2308d7f5696e9dca49e0d64ef49a1425bbf40e0c02"
484 );
485
486 let packed_proof_pattern = r"^0x[a-f0-9]{400,600}$";
488 let re = Regex::new(packed_proof_pattern).unwrap();
489 assert!(re.is_match(parsed_json["proof"].as_str().unwrap()));
490
491 assert_eq!(
492 zkp.get_nullifier_hash().to_hex_string(),
493 parsed_json["nullifier_hash"].as_str().unwrap()
494 );
495 assert_eq!(
496 zkp.get_merkle_root().to_hex_string(),
497 parsed_json["merkle_root"].as_str().unwrap()
498 );
499 assert_eq!(
500 zkp.get_proof_as_string(),
501 parsed_json["proof"].as_str().unwrap()
502 );
503 }
504
505 #[cfg(feature = "legacy-nullifiers")]
506 #[test]
507 fn test_proof_generation_with_legacy_nullifier_address_book() {
508 let context = ProofContext::legacy_new_from_pre_image_external_nullifier(
509 b"internal_addressbook",
510 CredentialType::Device,
511 None,
512 );
513
514 let expected = uint!(377593556987874043165400752883455722895901692332643678318174569531027326541_U256);
517 assert_eq!(
518 context.external_nullifier.to_hex_string(),
519 format!("{expected:#066x}")
520 );
521 }
522
523 #[cfg(feature = "legacy-nullifiers")]
524 #[test]
525 fn test_proof_generation_with_legacy_nullifier_recurring_grant_drop() {
526 let grant_id = 48;
527
528 let worldchain_nullifier_hash_constant = uint!(
531 0x1E00000000000000000000000000000000000000000000000000000000000000_U256
532 );
533 let external_nullifier_hash =
534 worldchain_nullifier_hash_constant + U256::from(grant_id);
535
536 let context = ProofContext::legacy_new_from_raw_external_nullifier(
537 &external_nullifier_hash.into(),
538 CredentialType::Device,
539 None,
540 )
541 .unwrap();
542
543 let expected = uint!(13569385457497991651199724805705614201555076328004753598373935625927319879728_U256);
546 assert_eq!(
547 context.external_nullifier.to_hex_string(),
548 format!("{expected:#066x}")
549 );
550 }
551
552 #[cfg(feature = "legacy-nullifiers")]
553 #[test]
554 fn test_ensure_raw_external_nullifiers_are_in_the_field() {
555 let invalid_external_nullifier = MODULUS + U256::from(1);
556 assert!(ProofContext::legacy_new_from_raw_external_nullifier(
557 &invalid_external_nullifier.into(),
558 CredentialType::Device,
559 None,
560 )
561 .is_err());
562 }
563}