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 semaphore_rs::MODULUS;
11
12use serde::Serialize;
13
14use crate::{
15 credential_type::CredentialType, merkle_tree::MerkleTreeProof, u256::U256Wrapper,
16};
17
18#[derive(Clone, PartialEq, Eq, Debug)]
24#[cfg_attr(feature = "ffi", derive(uniffi::Object))]
25pub struct ProofContext {
26 pub external_nullifier: U256Wrapper,
29 pub credential_type: CredentialType,
31 pub signal_hash: U256Wrapper,
34 pub require_mined_proof: bool,
36}
37
38#[cfg_attr(feature = "ffi", uniffi::export)]
39impl ProofContext {
40 #[must_use]
56 #[cfg_attr(feature = "ffi", uniffi::constructor)]
57 pub fn new(
58 app_id: &str,
59 action: Option<String>,
60 signal: Option<String>,
61 credential_type: CredentialType,
62 ) -> Self {
63 Self::new_from_bytes(
64 app_id,
65 action.map(std::string::String::into_bytes),
66 signal.map(std::string::String::into_bytes),
67 credential_type,
68 )
69 }
70
71 #[must_use]
82 #[cfg_attr(feature = "ffi", uniffi::constructor)]
83 #[allow(clippy::needless_pass_by_value)]
84 pub fn new_from_bytes(
85 app_id: &str,
86 action: Option<Vec<u8>>,
87 signal: Option<Vec<u8>>,
88 credential_type: CredentialType,
89 ) -> Self {
90 let signal_hash =
91 U256Wrapper::from(hash_to_field(signal.unwrap_or_default().as_slice()));
92
93 Self::new_from_signal_hash_unchecked(
94 app_id,
95 action,
96 credential_type,
97 &signal_hash,
98 )
99 }
100
101 #[cfg_attr(feature = "ffi", uniffi::constructor)]
121 pub fn new_from_signal_hash(
122 app_id: &str,
123 action: Option<Vec<u8>>,
124 credential_type: CredentialType,
125 signal_hash: &U256Wrapper,
126 ) -> Result<Self, WalletKitError> {
127 if signal_hash.0 >= MODULUS {
128 return Err(WalletKitError::InvalidNumber);
129 }
130
131 Ok(Self::new_from_signal_hash_unchecked(
132 app_id,
133 action,
134 credential_type,
135 signal_hash,
136 ))
137 }
138
139 #[must_use]
141 pub const fn get_external_nullifier(&self) -> U256Wrapper {
142 self.external_nullifier
143 }
144
145 #[must_use]
147 pub const fn get_signal_hash(&self) -> U256Wrapper {
148 self.signal_hash
149 }
150
151 #[must_use]
153 pub const fn get_credential_type(&self) -> CredentialType {
154 self.credential_type
155 }
156}
157
158impl ProofContext {
160 fn new_from_signal_hash_unchecked(
161 app_id: &str,
162 action: Option<Vec<u8>>,
163 credential_type: CredentialType,
164 signal_hash: &U256Wrapper,
165 ) -> Self {
166 let mut pre_image = hash_to_field(app_id.as_bytes()).abi_encode_packed();
167
168 if let Some(action) = action {
169 pre_image.extend_from_slice(&action);
170 }
171
172 let external_nullifier = hash_to_field(&pre_image).into();
173
174 Self {
175 external_nullifier,
176 credential_type,
177 signal_hash: *signal_hash,
178 require_mined_proof: false,
179 }
180 }
181}
182
183#[cfg_attr(feature = "ffi", uniffi::export)]
184#[cfg(feature = "legacy-nullifiers")]
185impl ProofContext {
186 #[must_use]
203 #[cfg_attr(feature = "ffi", uniffi::constructor)]
204 pub fn legacy_new_from_pre_image_external_nullifier(
205 external_nullifier: &[u8],
206 credential_type: CredentialType,
207 signal: Option<Vec<u8>>,
208 require_mined_proof: bool,
209 ) -> Self {
210 let external_nullifier: U256Wrapper = hash_to_field(external_nullifier).into();
211 Self {
212 external_nullifier,
213 credential_type,
214 signal_hash: hash_to_field(signal.unwrap_or_default().as_slice()).into(),
215 require_mined_proof,
216 }
217 }
218
219 #[cfg_attr(feature = "ffi", uniffi::constructor)]
240 pub fn legacy_new_from_raw_external_nullifier(
241 external_nullifier: &U256Wrapper,
242 credential_type: CredentialType,
243 signal: Option<Vec<u8>>,
244 require_mined_proof: bool,
245 ) -> Result<Self, WalletKitError> {
246 if external_nullifier.0 >= MODULUS {
247 return Err(WalletKitError::InvalidNumber);
248 }
249
250 Ok(Self {
251 external_nullifier: *external_nullifier,
252 credential_type,
253 signal_hash: hash_to_field(signal.unwrap_or_default().as_slice()).into(),
254 require_mined_proof,
255 })
256 }
257}
258
259#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
266#[cfg_attr(feature = "ffi", derive(uniffi::Object))]
267#[allow(clippy::module_name_repetitions)]
268pub struct ProofOutput {
269 pub merkle_root: U256Wrapper,
272 pub nullifier_hash: U256Wrapper,
275 #[serde(skip_serializing)]
277 pub raw_proof: Proof,
278 pub proof: PackedProof,
281 pub credential_type: CredentialType,
283}
284
285#[cfg_attr(feature = "ffi", uniffi::export)]
286impl ProofOutput {
287 pub fn to_json(&self) -> Result<String, WalletKitError> {
292 serde_json::to_string(self).map_err(|e| {
293 WalletKitError::SerializationError(format!(
294 "Failed to serialize proof: {e}"
295 ))
296 })
297 }
298
299 #[must_use]
301 pub const fn get_nullifier_hash(&self) -> U256Wrapper {
302 self.nullifier_hash
303 }
304
305 #[must_use]
307 pub const fn get_merkle_root(&self) -> U256Wrapper {
308 self.merkle_root
309 }
310
311 #[must_use]
313 pub fn get_proof_as_string(&self) -> String {
314 self.proof.to_string()
315 }
316
317 #[must_use]
319 pub const fn get_credential_type(&self) -> CredentialType {
320 self.credential_type
321 }
322}
323
324pub fn generate_proof_with_semaphore_identity(
331 identity: &identity::Identity,
332 merkle_tree_proof: &MerkleTreeProof,
333 context: &ProofContext,
334) -> Result<ProofOutput, WalletKitError> {
335 #[cfg(not(feature = "semaphore"))]
336 return Err(WalletKitError::SemaphoreNotEnabled);
337
338 let merkle_root = merkle_tree_proof.merkle_root; let external_nullifier_hash = context.external_nullifier.into();
341 let nullifier_hash =
342 generate_nullifier_hash(identity, external_nullifier_hash).into();
343
344 let proof = generate_proof(
345 identity,
346 merkle_tree_proof.as_poseidon_proof(),
347 external_nullifier_hash,
348 context.signal_hash.into(),
349 )?;
350
351 Ok(ProofOutput {
352 merkle_root,
353 nullifier_hash,
354 raw_proof: proof,
355 proof: PackedProof::from(proof),
356 credential_type: context.credential_type,
357 })
358}
359
360#[cfg(test)]
361mod external_nullifier_tests {
362 use alloy_core::primitives::address;
363 use ruint::{aliases::U256, uint};
364
365 use super::*;
366
367 #[test]
368 fn test_context_and_external_nullifier_hash_generation() {
369 let context = ProofContext::new(
370 "app_369183bd38f1641b6964ab51d7a20434",
371 None,
372 None,
373 CredentialType::Orb,
374 );
375 assert_eq!(
376 context.external_nullifier.to_hex_string(),
377 "0x0073e4a6b670e81dc619b1f8703aa7491dc5aaadf75409aba0ac2414014c0227"
378 );
379
380 let context = ProofContext::new(
382 "app_369183bd38f1641b6964ab51d7a20434",
383 Some(String::new()),
384 None,
385 CredentialType::Orb,
386 );
387 assert_eq!(
388 context.external_nullifier.to_hex_string(),
389 "0x0073e4a6b670e81dc619b1f8703aa7491dc5aaadf75409aba0ac2414014c0227"
390 );
391 }
392
393 #[test]
396 fn test_external_nullifier_hash_generation_string_action_staging() {
397 let context = ProofContext::new(
398 "app_staging_45068dca85829d2fd90e2dd6f0bff997",
399 Some("test-action-qli8g".to_string()),
400 None,
401 CredentialType::Orb,
402 );
403 assert_eq!(
404 context.external_nullifier.to_hex_string(),
405 "0x00d8b157e767dc59faa533120ed0ce34fc51a71937292ea8baed6ee6f4fda866"
406 );
407 }
408
409 #[test]
410 fn test_external_nullifier_hash_generation_string_action() {
411 let context = ProofContext::new(
412 "app_10eb12bd96d8f7202892ff25f094c803",
413 Some("test-123123".to_string()),
414 None,
415 CredentialType::Orb,
416 );
417 assert_eq!(
418 context.external_nullifier.0,
419 uint!(
420 0x0065ebab05692ff2e0816cc4c3b83216c33eaa4d906c6495add6323fe0e2dc89_U256
422 )
423 );
424 }
425
426 #[test]
427 fn test_external_nullifier_hash_generation_with_advanced_abi_encoded_values() {
428 let custom_action = [
429 address!("541f3cc5772a64f2ba0a47e83236CcE2F089b188").abi_encode_packed(),
430 U256::from(1).abi_encode_packed(),
431 "hello".abi_encode_packed(),
432 ]
433 .concat();
434
435 let context = ProofContext::new_from_bytes(
436 "app_10eb12bd96d8f7202892ff25f094c803",
437 Some(custom_action),
438 None,
439 CredentialType::Orb,
440 );
441 assert_eq!(
442 context.external_nullifier.to_hex_string(),
443 "0x00f974ff06219e8ca992073d8bbe05084f81250dbd8f37cae733f24fcc0c5ffd"
445 );
446 }
447
448 #[test]
449 fn test_external_nullifier_hash_generation_with_advanced_abi_encoded_values_staging(
450 ) {
451 let custom_action = [
452 "world".abi_encode_packed(),
453 U256::from(1).abi_encode_packed(),
454 "hello".abi_encode_packed(),
455 ]
456 .concat();
457
458 let context = ProofContext::new_from_bytes(
459 "app_staging_45068dca85829d2fd90e2dd6f0bff997",
460 Some(custom_action),
461 None,
462 CredentialType::Orb,
463 );
464 assert_eq!(
465 context.external_nullifier.to_hex_string(),
466 "0x005b49f95e822c7c37f4f043421689b11f880e617faa5cd0391803bc9bcc63c0"
468 );
469 }
470
471 #[cfg(feature = "legacy-nullifiers")]
472 #[test]
473 fn test_proof_generation_with_legacy_nullifier_address_book() {
474 let context = ProofContext::legacy_new_from_pre_image_external_nullifier(
475 b"internal_addressbook",
476 CredentialType::Device,
477 None,
478 false,
479 );
480
481 let expected = uint!(377593556987874043165400752883455722895901692332643678318174569531027326541_U256);
484 assert_eq!(
485 context.external_nullifier.to_hex_string(),
486 format!("{expected:#066x}")
487 );
488 }
489
490 #[cfg(feature = "legacy-nullifiers")]
491 #[test]
492 fn test_proof_generation_with_legacy_nullifier_recurring_grant_drop() {
493 let grant_id = 48;
494
495 let worldchain_nullifier_hash_constant = uint!(
498 0x1E00000000000000000000000000000000000000000000000000000000000000_U256
499 );
500 let external_nullifier_hash =
501 worldchain_nullifier_hash_constant + U256::from(grant_id);
502
503 let context = ProofContext::legacy_new_from_raw_external_nullifier(
504 &external_nullifier_hash.into(),
505 CredentialType::Device,
506 None,
507 false,
508 )
509 .unwrap();
510
511 let expected = uint!(13569385457497991651199724805705614201555076328004753598373935625927319879728_U256);
514 assert_eq!(
515 context.external_nullifier.to_hex_string(),
516 format!("{expected:#066x}")
517 );
518 }
519
520 #[cfg(feature = "legacy-nullifiers")]
521 #[test]
522 fn test_ensure_raw_external_nullifier_is_in_the_field() {
523 let invalid_external_nullifiers = [MODULUS, MODULUS + U256::from(1)];
524 for external_nullifier in invalid_external_nullifiers {
525 let context = ProofContext::legacy_new_from_raw_external_nullifier(
526 &external_nullifier.into(),
527 CredentialType::Device,
528 None,
529 false,
530 );
531 assert!(context.is_err());
532 }
533 }
534}
535
536#[cfg(test)]
537mod signal_tests {
538 use ruint::aliases::U256;
539
540 use super::*;
541
542 #[test]
543 fn test_ensure_raw_signal_hash_is_in_the_field() {
544 let invalid_signals = [MODULUS, MODULUS + U256::from(1)];
545 for signal_hash in invalid_signals {
546 let context = ProofContext::new_from_signal_hash(
547 "my_app_id",
548 None,
549 CredentialType::Device,
550 &signal_hash.into(),
551 );
552 assert!(context.is_err());
553 }
554 }
555
556 #[test]
557 fn test_get_external_nullifier() {
558 let context = ProofContext::new(
559 "app_369183bd38f1641b6964ab51d7a20434",
560 Some("test-action".to_string()),
561 None,
562 CredentialType::Orb,
563 );
564
565 let external_nullifier = context.get_external_nullifier();
566 assert_eq!(external_nullifier, context.external_nullifier);
567 assert_eq!(
568 external_nullifier.to_hex_string(),
569 "0x00dd12b56cebf29593d6d3208a061bbb19e60152c56045f277a15989d25d5215"
570 );
571 }
572
573 #[test]
574 fn test_get_signal_hash() {
575 let signal = "test_signal_123".to_string();
576 let context = ProofContext::new(
577 "app_10eb12bd96d8f7202892ff25f094c803",
578 None,
579 Some(signal.clone()),
580 CredentialType::Device,
581 );
582
583 let signal_hash = context.get_signal_hash();
584 assert_eq!(signal_hash, context.signal_hash);
585
586 let expected_hash = U256Wrapper::from(hash_to_field(signal.as_bytes()));
587 assert_eq!(signal_hash, expected_hash);
588 }
589
590 #[test]
591 fn test_get_credential_type() {
592 let orb_context = ProofContext::new("app_123", None, None, CredentialType::Orb);
593 assert_eq!(orb_context.get_credential_type(), CredentialType::Orb);
594
595 let device_context =
596 ProofContext::new("app_456", None, None, CredentialType::Device);
597 assert_eq!(device_context.get_credential_type(), CredentialType::Device);
598 }
599}
600
601#[cfg(test)]
602mod proof_tests {
603
604 use regex::Regex;
605 use semaphore_rs::protocol::verify_proof;
606 use serde_json::Value;
607
608 use super::*;
609
610 fn helper_load_merkle_proof() -> MerkleTreeProof {
611 let json_merkle: Value = serde_json::from_str(include_str!(
612 "../tests/fixtures/inclusion_proof.json"
613 ))
614 .unwrap();
615 MerkleTreeProof::from_json_proof(
616 &serde_json::to_string(&json_merkle["proof"]).unwrap(),
617 json_merkle["root"].as_str().unwrap(),
618 )
619 .unwrap()
620 }
621
622 #[test]
623 fn test_proof_generation() {
624 let context = ProofContext::new(
625 "app_staging_45068dca85829d2fd90e2dd6f0bff997",
626 Some("test-action-89tcf".to_string()),
627 None,
628 CredentialType::Device,
629 );
630
631 let mut secret = b"not_a_real_secret".to_vec();
632
633 let identity = semaphore_rs::identity::Identity::from_secret(
634 &mut secret,
635 Some(context.credential_type.as_identity_trapdoor()),
636 );
637
638 assert_eq!(
639 U256Wrapper::from(identity.commitment()).to_hex_string(),
640 "0x1a060ef75540e13711f074b779a419c126ab5a89d2c2e7d01e64dfd121e44671"
641 );
642
643 let zkp = generate_proof_with_semaphore_identity(
645 &identity,
646 &helper_load_merkle_proof(),
647 &context,
648 )
649 .unwrap();
650
651 assert_eq!(
652 zkp.merkle_root.to_hex_string(),
653 "0x2f3a95b6df9074a19bf46e2308d7f5696e9dca49e0d64ef49a1425bbf40e0c02"
654 );
655
656 assert_eq!(
657 zkp.nullifier_hash.to_hex_string(),
658 "0x11d194ff98df5c8e239e6b6e33cce7fb1b419344cb13e064350a917970c8fea4"
659 );
660
661 assert!(verify_proof(
663 *zkp.merkle_root,
664 *zkp.nullifier_hash,
665 hash_to_field(&[]),
666 *context.external_nullifier,
667 &zkp.raw_proof,
668 30
669 )
670 .unwrap());
671 }
672
673 #[test]
674 fn test_proof_json_encoding() {
675 let context = ProofContext::new(
676 "app_staging_45068dca85829d2fd90e2dd6f0bff997",
677 Some("test-action-89tcf".to_string()),
678 None,
679 CredentialType::Device,
680 );
681
682 let mut secret = b"not_a_real_secret".to_vec();
683 let identity = semaphore_rs::identity::Identity::from_secret(
684 &mut secret,
685 Some(context.credential_type.as_identity_trapdoor()),
686 );
687
688 let zkp = generate_proof_with_semaphore_identity(
690 &identity,
691 &helper_load_merkle_proof(),
692 &context,
693 )
694 .unwrap();
695
696 let parsed_json: Value = serde_json::from_str(&zkp.to_json().unwrap()).unwrap();
697
698 assert_eq!(
699 parsed_json["nullifier_hash"].as_str().unwrap(),
700 "0x11d194ff98df5c8e239e6b6e33cce7fb1b419344cb13e064350a917970c8fea4"
701 );
702 assert_eq!(
703 parsed_json["merkle_root"].as_str().unwrap(),
704 "0x2f3a95b6df9074a19bf46e2308d7f5696e9dca49e0d64ef49a1425bbf40e0c02"
705 );
706
707 assert_eq!(parsed_json["credential_type"].as_str().unwrap(), "device");
708
709 let packed_proof_pattern = r"^0x[a-f0-9]{400,600}$";
711 let re = Regex::new(packed_proof_pattern).unwrap();
712 assert!(re.is_match(parsed_json["proof"].as_str().unwrap()));
713
714 assert_eq!(
715 zkp.get_nullifier_hash().to_hex_string(),
716 parsed_json["nullifier_hash"].as_str().unwrap()
717 );
718 assert_eq!(
719 zkp.get_merkle_root().to_hex_string(),
720 parsed_json["merkle_root"].as_str().unwrap()
721 );
722 assert_eq!(
723 zkp.get_proof_as_string(),
724 parsed_json["proof"].as_str().unwrap()
725 );
726 }
727}