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
138impl ProofContext {
140 fn new_from_signal_hash_unchecked(
141 app_id: &str,
142 action: Option<Vec<u8>>,
143 credential_type: CredentialType,
144 signal_hash: &U256Wrapper,
145 ) -> Self {
146 let mut pre_image = hash_to_field(app_id.as_bytes()).abi_encode_packed();
147
148 if let Some(action) = action {
149 pre_image.extend_from_slice(&action);
150 }
151
152 let external_nullifier = hash_to_field(&pre_image).into();
153
154 Self {
155 external_nullifier,
156 credential_type,
157 signal_hash: *signal_hash,
158 }
159 }
160}
161
162#[cfg_attr(feature = "ffi", uniffi::export)]
163#[cfg(feature = "legacy-nullifiers")]
164impl ProofContext {
165 #[must_use]
182 #[cfg_attr(feature = "ffi", uniffi::constructor)]
183 pub fn legacy_new_from_pre_image_external_nullifier(
184 external_nullifier: &[u8],
185 credential_type: CredentialType,
186 signal: Option<Vec<u8>>,
187 ) -> Self {
188 let external_nullifier: U256Wrapper = hash_to_field(external_nullifier).into();
189 Self {
190 external_nullifier,
191 credential_type,
192 signal_hash: hash_to_field(signal.unwrap_or_default().as_slice()).into(),
193 }
194 }
195
196 #[cfg_attr(feature = "ffi", uniffi::constructor)]
217 pub fn legacy_new_from_raw_external_nullifier(
218 external_nullifier: &U256Wrapper,
219 credential_type: CredentialType,
220 signal: Option<Vec<u8>>,
221 ) -> Result<Self, WalletKitError> {
222 if external_nullifier.0 >= MODULUS {
223 return Err(WalletKitError::InvalidNumber);
224 }
225
226 Ok(Self {
227 external_nullifier: *external_nullifier,
228 credential_type,
229 signal_hash: hash_to_field(signal.unwrap_or_default().as_slice()).into(),
230 })
231 }
232}
233
234#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
241#[cfg_attr(feature = "ffi", derive(uniffi::Object))]
242#[allow(clippy::module_name_repetitions)]
243pub struct ProofOutput {
244 pub merkle_root: U256Wrapper,
247 pub nullifier_hash: U256Wrapper,
250 #[serde(skip_serializing)]
252 pub raw_proof: Proof,
253 pub proof: PackedProof,
256 pub credential_type: CredentialType,
258}
259
260#[cfg_attr(feature = "ffi", uniffi::export)]
261impl ProofOutput {
262 pub fn to_json(&self) -> Result<String, WalletKitError> {
267 serde_json::to_string(self).map_err(|_| WalletKitError::SerializationError)
268 }
269
270 #[must_use]
272 pub const fn get_nullifier_hash(&self) -> U256Wrapper {
273 self.nullifier_hash
274 }
275
276 #[must_use]
278 pub const fn get_merkle_root(&self) -> U256Wrapper {
279 self.merkle_root
280 }
281
282 #[must_use]
284 pub fn get_proof_as_string(&self) -> String {
285 self.proof.to_string()
286 }
287
288 #[must_use]
290 pub const fn get_credential_type(&self) -> CredentialType {
291 self.credential_type
292 }
293}
294
295pub fn generate_proof_with_semaphore_identity(
302 identity: &identity::Identity,
303 merkle_tree_proof: &MerkleTreeProof,
304 context: &ProofContext,
305) -> Result<ProofOutput, WalletKitError> {
306 #[cfg(not(feature = "semaphore"))]
307 return Err(WalletKitError::SemaphoreNotEnabled);
308
309 let merkle_root = merkle_tree_proof.merkle_root; let external_nullifier_hash = context.external_nullifier.into();
312 let nullifier_hash =
313 generate_nullifier_hash(identity, external_nullifier_hash).into();
314
315 let proof = generate_proof(
316 identity,
317 merkle_tree_proof.as_poseidon_proof(),
318 external_nullifier_hash,
319 context.signal_hash.into(),
320 )?;
321
322 Ok(ProofOutput {
323 merkle_root,
324 nullifier_hash,
325 raw_proof: proof,
326 proof: PackedProof::from(proof),
327 credential_type: context.credential_type,
328 })
329}
330
331#[cfg(test)]
332mod external_nullifier_tests {
333 use alloy_core::primitives::address;
334 use ruint::{aliases::U256, uint};
335
336 use super::*;
337
338 #[test]
339 fn test_context_and_external_nullifier_hash_generation() {
340 let context = ProofContext::new(
341 "app_369183bd38f1641b6964ab51d7a20434",
342 None,
343 None,
344 CredentialType::Orb,
345 );
346 assert_eq!(
347 context.external_nullifier.to_hex_string(),
348 "0x0073e4a6b670e81dc619b1f8703aa7491dc5aaadf75409aba0ac2414014c0227"
349 );
350
351 let context = ProofContext::new(
353 "app_369183bd38f1641b6964ab51d7a20434",
354 Some(String::new()),
355 None,
356 CredentialType::Orb,
357 );
358 assert_eq!(
359 context.external_nullifier.to_hex_string(),
360 "0x0073e4a6b670e81dc619b1f8703aa7491dc5aaadf75409aba0ac2414014c0227"
361 );
362 }
363
364 #[test]
367 fn test_external_nullifier_hash_generation_string_action_staging() {
368 let context = ProofContext::new(
369 "app_staging_45068dca85829d2fd90e2dd6f0bff997",
370 Some("test-action-qli8g".to_string()),
371 None,
372 CredentialType::Orb,
373 );
374 assert_eq!(
375 context.external_nullifier.to_hex_string(),
376 "0x00d8b157e767dc59faa533120ed0ce34fc51a71937292ea8baed6ee6f4fda866"
377 );
378 }
379
380 #[test]
381 fn test_external_nullifier_hash_generation_string_action() {
382 let context = ProofContext::new(
383 "app_10eb12bd96d8f7202892ff25f094c803",
384 Some("test-123123".to_string()),
385 None,
386 CredentialType::Orb,
387 );
388 assert_eq!(
389 context.external_nullifier.0,
390 uint!(
391 0x0065ebab05692ff2e0816cc4c3b83216c33eaa4d906c6495add6323fe0e2dc89_U256
393 )
394 );
395 }
396
397 #[test]
398 fn test_external_nullifier_hash_generation_with_advanced_abi_encoded_values() {
399 let custom_action = [
400 address!("541f3cc5772a64f2ba0a47e83236CcE2F089b188").abi_encode_packed(),
401 U256::from(1).abi_encode_packed(),
402 "hello".abi_encode_packed(),
403 ]
404 .concat();
405
406 let context = ProofContext::new_from_bytes(
407 "app_10eb12bd96d8f7202892ff25f094c803",
408 Some(custom_action),
409 None,
410 CredentialType::Orb,
411 );
412 assert_eq!(
413 context.external_nullifier.to_hex_string(),
414 "0x00f974ff06219e8ca992073d8bbe05084f81250dbd8f37cae733f24fcc0c5ffd"
416 );
417 }
418
419 #[test]
420 fn test_external_nullifier_hash_generation_with_advanced_abi_encoded_values_staging(
421 ) {
422 let custom_action = [
423 "world".abi_encode_packed(),
424 U256::from(1).abi_encode_packed(),
425 "hello".abi_encode_packed(),
426 ]
427 .concat();
428
429 let context = ProofContext::new_from_bytes(
430 "app_staging_45068dca85829d2fd90e2dd6f0bff997",
431 Some(custom_action),
432 None,
433 CredentialType::Orb,
434 );
435 assert_eq!(
436 context.external_nullifier.to_hex_string(),
437 "0x005b49f95e822c7c37f4f043421689b11f880e617faa5cd0391803bc9bcc63c0"
439 );
440 }
441
442 #[cfg(feature = "legacy-nullifiers")]
443 #[test]
444 fn test_proof_generation_with_legacy_nullifier_address_book() {
445 let context = ProofContext::legacy_new_from_pre_image_external_nullifier(
446 b"internal_addressbook",
447 CredentialType::Device,
448 None,
449 );
450
451 let expected = uint!(377593556987874043165400752883455722895901692332643678318174569531027326541_U256);
454 assert_eq!(
455 context.external_nullifier.to_hex_string(),
456 format!("{expected:#066x}")
457 );
458 }
459
460 #[cfg(feature = "legacy-nullifiers")]
461 #[test]
462 fn test_proof_generation_with_legacy_nullifier_recurring_grant_drop() {
463 let grant_id = 48;
464
465 let worldchain_nullifier_hash_constant = uint!(
468 0x1E00000000000000000000000000000000000000000000000000000000000000_U256
469 );
470 let external_nullifier_hash =
471 worldchain_nullifier_hash_constant + U256::from(grant_id);
472
473 let context = ProofContext::legacy_new_from_raw_external_nullifier(
474 &external_nullifier_hash.into(),
475 CredentialType::Device,
476 None,
477 )
478 .unwrap();
479
480 let expected = uint!(13569385457497991651199724805705614201555076328004753598373935625927319879728_U256);
483 assert_eq!(
484 context.external_nullifier.to_hex_string(),
485 format!("{expected:#066x}")
486 );
487 }
488
489 #[cfg(feature = "legacy-nullifiers")]
490 #[test]
491 fn test_ensure_raw_external_nullifier_is_in_the_field() {
492 let invalid_external_nullifiers = [MODULUS, MODULUS + U256::from(1)];
493 for external_nullifier in invalid_external_nullifiers {
494 let context = ProofContext::legacy_new_from_raw_external_nullifier(
495 &external_nullifier.into(),
496 CredentialType::Device,
497 None,
498 );
499 assert!(context.is_err());
500 }
501 }
502}
503
504#[cfg(test)]
505mod signal_tests {
506 use ruint::aliases::U256;
507
508 use super::*;
509
510 #[test]
511 fn test_ensure_raw_signal_hash_is_in_the_field() {
512 let invalid_signals = [MODULUS, MODULUS + U256::from(1)];
513 for signal_hash in invalid_signals {
514 let context = ProofContext::new_from_signal_hash(
515 "my_app_id",
516 None,
517 CredentialType::Device,
518 &signal_hash.into(),
519 );
520 assert!(context.is_err());
521 }
522 }
523}
524
525#[cfg(test)]
526mod proof_tests {
527
528 use regex::Regex;
529 use semaphore_rs::protocol::verify_proof;
530 use serde_json::Value;
531
532 use super::*;
533
534 fn helper_load_merkle_proof() -> MerkleTreeProof {
535 let json_merkle: Value = serde_json::from_str(include_str!(
536 "../tests/fixtures/inclusion_proof.json"
537 ))
538 .unwrap();
539 MerkleTreeProof::from_json_proof(
540 &serde_json::to_string(&json_merkle["proof"]).unwrap(),
541 json_merkle["root"].as_str().unwrap(),
542 )
543 .unwrap()
544 }
545
546 #[test]
547 fn test_proof_generation() {
548 let context = ProofContext::new(
549 "app_staging_45068dca85829d2fd90e2dd6f0bff997",
550 Some("test-action-89tcf".to_string()),
551 None,
552 CredentialType::Device,
553 );
554
555 let mut secret = b"not_a_real_secret".to_vec();
556
557 let identity = semaphore_rs::identity::Identity::from_secret(
558 &mut secret,
559 Some(context.credential_type.as_identity_trapdoor()),
560 );
561
562 assert_eq!(
563 U256Wrapper::from(identity.commitment()).to_hex_string(),
564 "0x1a060ef75540e13711f074b779a419c126ab5a89d2c2e7d01e64dfd121e44671"
565 );
566
567 let zkp = generate_proof_with_semaphore_identity(
569 &identity,
570 &helper_load_merkle_proof(),
571 &context,
572 )
573 .unwrap();
574
575 assert_eq!(
576 zkp.merkle_root.to_hex_string(),
577 "0x2f3a95b6df9074a19bf46e2308d7f5696e9dca49e0d64ef49a1425bbf40e0c02"
578 );
579
580 assert_eq!(
581 zkp.nullifier_hash.to_hex_string(),
582 "0x11d194ff98df5c8e239e6b6e33cce7fb1b419344cb13e064350a917970c8fea4"
583 );
584
585 assert!(verify_proof(
587 *zkp.merkle_root,
588 *zkp.nullifier_hash,
589 hash_to_field(&[]),
590 *context.external_nullifier,
591 &zkp.raw_proof,
592 30
593 )
594 .unwrap());
595 }
596
597 #[test]
598 fn test_proof_json_encoding() {
599 let context = ProofContext::new(
600 "app_staging_45068dca85829d2fd90e2dd6f0bff997",
601 Some("test-action-89tcf".to_string()),
602 None,
603 CredentialType::Device,
604 );
605
606 let mut secret = b"not_a_real_secret".to_vec();
607 let identity = semaphore_rs::identity::Identity::from_secret(
608 &mut secret,
609 Some(context.credential_type.as_identity_trapdoor()),
610 );
611
612 let zkp = generate_proof_with_semaphore_identity(
614 &identity,
615 &helper_load_merkle_proof(),
616 &context,
617 )
618 .unwrap();
619
620 let parsed_json: Value = serde_json::from_str(&zkp.to_json().unwrap()).unwrap();
621
622 assert_eq!(
623 parsed_json["nullifier_hash"].as_str().unwrap(),
624 "0x11d194ff98df5c8e239e6b6e33cce7fb1b419344cb13e064350a917970c8fea4"
625 );
626 assert_eq!(
627 parsed_json["merkle_root"].as_str().unwrap(),
628 "0x2f3a95b6df9074a19bf46e2308d7f5696e9dca49e0d64ef49a1425bbf40e0c02"
629 );
630
631 assert_eq!(parsed_json["credential_type"].as_str().unwrap(), "device");
632
633 let packed_proof_pattern = r"^0x[a-f0-9]{400,600}$";
635 let re = Regex::new(packed_proof_pattern).unwrap();
636 assert!(re.is_match(parsed_json["proof"].as_str().unwrap()));
637
638 assert_eq!(
639 zkp.get_nullifier_hash().to_hex_string(),
640 parsed_json["nullifier_hash"].as_str().unwrap()
641 );
642 assert_eq!(
643 zkp.get_merkle_root().to_hex_string(),
644 parsed_json["merkle_root"].as_str().unwrap()
645 );
646 assert_eq!(
647 zkp.get_proof_as_string(),
648 parsed_json["proof"].as_str().unwrap()
649 );
650 }
651}