walletkit_core/
proof.rs

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/// A `ProofContext` contains the basic information on the verifier and the specific action a user will be proving.
19///
20/// It is required to generate a `Proof` and will generally be initialized from an `app_id` and `action`.
21///
22/// Note on naming: `ProofContext` is used to make it clear in FFIs which may not respect the module structure.
23#[derive(Clone, PartialEq, Eq, Debug)]
24#[cfg_attr(feature = "ffi", derive(uniffi::Object))]
25pub struct ProofContext {
26    /// The `external_nullifier` is the computed result of a specific context for which a World ID Proof is generated.
27    /// It is used in the Sempahore ZK circuit and in the computation of the `nullifier_hash` to guarantee uniqueness in a privacy-preserving way.
28    pub external_nullifier: U256Wrapper,
29    /// Represents the specific credential to be used for a World ID Proof.
30    pub credential_type: CredentialType,
31    /// The hashed signal which is included in the ZKP and committed to in the proof.
32    /// When verifying the proof, the same signal must be provided.
33    pub signal_hash: U256Wrapper,
34    /// Whether the request requires a mined on-chain proof.
35    pub require_mined_proof: bool,
36}
37
38#[cfg_attr(feature = "ffi", uniffi::export)]
39impl ProofContext {
40    /// Initializes a `ProofContext`.
41    ///
42    /// Will compute the relevant external nullifier from the provided `app_id` and `action` as defined by the
43    /// World ID Protocol. The external nullifier generation matches the logic in the
44    /// [Developer Portal](https://github.com/worldcoin/developer-portal/blob/main/web/lib/hashing.ts).
45    ///
46    /// # Arguments
47    ///
48    /// * `app_id` - The ID of the application requesting proofs.  This can be obtained from the [Developer Portal](https://developer.world.org).
49    /// * `action` - Optional. Custom incognito action being requested.
50    /// * `signal` - Optional. The signal is included in the ZKP and is committed to in the proof. When verifying the proof, the
51    ///   same signal must be provided to ensure the proof is valid. The signal can be used to prevent replay attacks, MITM or other cases.
52    ///   More details available in the [docs](https://docs.world.org/world-id/further-reading/zero-knowledge-proofs).
53    /// * `credential_type` - The type of credential being requested.
54    ///
55    #[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    /// Initializes a `Proof::ProofContext` where the `action` is provided as raw bytes. This is useful for advanced cases
72    /// where the `action` is an already ABI encoded value for on-chain usage.
73    /// See _walletkit-core/tests/solidity.rs_ for an example.
74    ///
75    /// Will compute the relevant external nullifier from the provided `app_id` and `action`.
76    ///
77    /// # Arguments
78    ///
79    /// See `ProofContext::new` for reference. The `action` and `signal` need to be provided as raw bytes.
80    ///
81    #[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    /// Initializes a `ProofContext` from an already hashed signal.
102    ///
103    /// Please note it is imperative to hash into the Semaphore field. Not all U256 are part of the field.
104    /// Use the `hash_to_field` function to hash into the field.
105    ///
106    /// # Usage
107    /// - This may be used when the hash of the signal is computed externally.
108    /// - For example, this is used for support of legacy `MiniKit` v1 commands in World App where `minikit-js` hashed the signal.
109    ///
110    /// # Arguments
111    ///
112    /// * `app_id` - The ID of the application requesting proofs.  This can be obtained from the [Developer Portal](https://developer.world.org).
113    /// * `action` - Optional. Custom incognito action being requested as bytes.
114    /// * `credential_type` - The type of credential being requested.
115    /// * `signal` - The already hashed signal as a field element.
116    ///
117    /// # Errors
118    ///
119    /// - Returns an error if the signal is not a valid number in the field.
120    #[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    /// Get the raw external nullifier for this context.
140    #[must_use]
141    pub const fn get_external_nullifier(&self) -> U256Wrapper {
142        self.external_nullifier
143    }
144
145    /// Get the signal hash for this context.
146    #[must_use]
147    pub const fn get_signal_hash(&self) -> U256Wrapper {
148        self.signal_hash
149    }
150
151    /// Get the credential type for this context.
152    #[must_use]
153    pub const fn get_credential_type(&self) -> CredentialType {
154        self.credential_type
155    }
156}
157
158// This impl block is not exported to foreign bindings.
159impl 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    /// LEGACY AND ADVANCED USE ONLY.
187    ///
188    /// Initializes a `ProofContext` from an arbitrary pre-image for an external nullifier.
189    ///
190    /// This is used for legacy nullifiers which were constructed from arbitrary bytes which don't follow
191    /// the `app_id` and `action` standard.
192    ///
193    /// # Usage (Non-exhaustive)
194    ///
195    /// - This is used for the World ID Address Book.
196    ///
197    /// # Arguments
198    ///
199    /// * `external_nullifier` - An arbitrary array of bytes that will be hashed to produce the external nullifier.
200    /// * `credential_type` - The type of credential being requested.
201    /// * `signal` - Optional. The signal is included in the ZKP and is committed to in the proof.
202    #[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    /// LEGACY AND ADVANCED USE ONLY.
220    ///
221    /// Initializes a `ProofContext` from a raw external nullifier.
222    ///
223    /// This is used for legacy nullifiers which were constructed from raw field elements.
224    ///
225    /// # Usage (Non-exhaustive)
226    ///
227    /// - This is used for Recurring Grant Claims (Worldcoin Airdrop).
228    /// - This is used to verify a World App account.
229    ///
230    /// # Arguments
231    ///
232    /// * `external_nullifier` - The raw external nullifier. Must already be a number in the field. No additional hashing is performed.
233    /// * `credential_type` - The type of credential being requested.
234    /// * `signal` - Optional. The signal is included in the ZKP and is committed to in the proof.
235    ///
236    /// # Errors
237    ///
238    /// - Returns an error if the external nullifier is not a valid number in the field.
239    #[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/// Represents the complete output of a World ID Proof (i.e. a credential persentation). This output
260/// can be serialized to JSON and can be verified easily with the Developer Portal or Sign up Sequencer.
261///
262/// For on-chain verification, the `proof` (which is packed) should generally be deserialized into `uint256[8]`.
263///
264/// More information on: [On-Chain Verification](https://docs.world.org/world-id/id/on-chain)
265#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
266#[cfg_attr(feature = "ffi", derive(uniffi::Object))]
267#[allow(clippy::module_name_repetitions)]
268pub struct ProofOutput {
269    /// The root hash of the Merkle tree used to prove membership. This root hash should match published hashes in the World ID
270    ///     protocol contract in Ethereum mainnet. See [address book](https://docs.world.org/world-id/reference/address-book).
271    pub merkle_root: U256Wrapper,
272    /// Represents the unique identifier for a specific context (app & action) and World ID. A World ID holder will always generate
273    /// the same `nullifier_hash` for the same context.
274    pub nullifier_hash: U256Wrapper,
275    /// The raw zero-knowledge proof.
276    #[serde(skip_serializing)]
277    pub raw_proof: Proof,
278    /// The ABI-encoded zero-knowledge proof represented as a string. This is the format generally used with other libraries and
279    /// can be directly used with the Developer Portal for verification.
280    pub proof: PackedProof,
281    /// The credential type used to generate the proof.
282    pub credential_type: CredentialType,
283}
284
285#[cfg_attr(feature = "ffi", uniffi::export)]
286impl ProofOutput {
287    /// Converts the entire proof output to a JSON string with standard attribute names.
288    ///
289    /// # Errors
290    /// Will error if serialization fails.
291    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    /// Exposes the nullifier hash to foreign code. Struct fields are not directly exposed to foreign code.
300    #[must_use]
301    pub const fn get_nullifier_hash(&self) -> U256Wrapper {
302        self.nullifier_hash
303    }
304
305    /// Exposes the merkle root to foreign code. Struct fields are not directly exposed to foreign code.
306    #[must_use]
307    pub const fn get_merkle_root(&self) -> U256Wrapper {
308        self.merkle_root
309    }
310
311    /// Exposes the proof as a string to foreign code. Struct fields are not directly exposed to foreign code.
312    #[must_use]
313    pub fn get_proof_as_string(&self) -> String {
314        self.proof.to_string()
315    }
316
317    /// Exposes the credential type to foreign code. Struct fields are not directly exposed to foreign code.
318    #[must_use]
319    pub const fn get_credential_type(&self) -> CredentialType {
320        self.credential_type
321    }
322}
323
324/// Generates a Semaphore ZKP for a specific Semaphore identity using the relevant provided context.
325///
326/// **Requires the `semaphore` feature flag.**
327///
328/// # Errors
329/// Returns an error if proof generation fails
330pub 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; // clone the value
339
340    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        // note the same external nullifier hash is generated for an empty string action
381        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    /// This test case comes from the real example in the docs.
394    /// Reference: <https://github.com/worldcoin/world-id-docs/blob/main/src/pages/world-id/try.tsx>
395    #[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                // cspell:disable-next-line
421                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            // expected output obtained from Solidity
444            "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            // expected output obtained from Solidity
467            "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        // the expected nullifier hash from the contract
482        // reference: <https://worldchain-mainnet.explorer.alchemy.com/tx/0x974e70f125abe3b6abaa0b3fb9cb067c09cee359b08fa847487d6623377308fd>
483        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        // The constant which is added to all the Grant IDs in World Chain.
496        // Reference: <https://github.com/worldcoin/worldcoin-grants-contracts/blob/main/src/RecurringGrantDrop.sol#L22>
497        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        // the expected nullifier hash from the contract
512        // transaction example for RGD 48: <https://worldscan.org/tx/0xbad696a88c5425a22af18ea6d00efca78ae0f5c5cceade21597adf60126a5fc4>
513        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        // Compute ZKP
644        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 proof verifies locally
662        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        // Compute ZKP
689        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        // ensure the proof is automatically encoded as packed
710        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}