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