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