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}
35
36#[cfg_attr(feature = "ffi", uniffi::export)]
37impl ProofContext {
38    /// Initializes a `ProofContext`.
39    ///
40    /// Will compute the relevant external nullifier from the provided `app_id` and `action` as defined by the
41    /// World ID Protocol. The external nullifier generation matches the logic in the
42    /// [Developer Portal](https://github.com/worldcoin/developer-portal/blob/main/web/lib/hashing.ts).
43    ///
44    /// # Arguments
45    ///
46    /// * `app_id` - The ID of the application requesting proofs.  This can be obtained from the [Developer Portal](https://developer.world.org).
47    /// * `action` - Optional. Custom incognito action being requested.
48    /// * `signal` - Optional. The signal is included in the ZKP and is committed to in the proof. When verifying the proof, the
49    ///   same signal must be provided to ensure the proof is valid. The signal can be used to prevent replay attacks, MITM or other cases.
50    ///   More details available in the [docs](https://docs.world.org/world-id/further-reading/zero-knowledge-proofs).
51    /// * `credential_type` - The type of credential being requested.
52    ///
53    #[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    /// Initializes a `Proof::ProofContext` where the `action` is provided as raw bytes. This is useful for advanced cases
70    /// where the `action` is an already ABI encoded value for on-chain usage.
71    /// See _walletkit-core/tests/solidity.rs_ for an example.
72    ///
73    /// Will compute the relevant external nullifier from the provided `app_id` and `action`.
74    ///
75    /// # Arguments
76    ///
77    /// See `ProofContext::new` for reference. The `action` and `signal` need to be provided as raw bytes.
78    ///
79    #[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    /// Initializes a `ProofContext` from an already hashed signal.
100    ///
101    /// Please note it is imperative to hash into the Semaphore field. Not all U256 are part of the field.
102    /// Use the `hash_to_field` function to hash into the field.
103    ///
104    /// # Usage
105    /// - This may be used when the hash of the signal is computed externally.
106    /// - For example, this is used for support of legacy `MiniKit` v1 commands in World App where `minikit-js` hashed the signal.
107    ///
108    /// # Arguments
109    ///
110    /// * `app_id` - The ID of the application requesting proofs.  This can be obtained from the [Developer Portal](https://developer.world.org).
111    /// * `action` - Optional. Custom incognito action being requested as bytes.
112    /// * `credential_type` - The type of credential being requested.
113    /// * `signal` - The already hashed signal as a field element.
114    ///
115    /// # Errors
116    ///
117    /// - Returns an error if the signal is not a valid number in the field.
118    #[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    /// Get the raw external nullifier for this context.
138    #[must_use]
139    pub const fn get_external_nullifier(&self) -> U256Wrapper {
140        self.external_nullifier
141    }
142
143    /// Get the signal hash for this context.
144    #[must_use]
145    pub const fn get_signal_hash(&self) -> U256Wrapper {
146        self.signal_hash
147    }
148
149    /// Get the credential type for this context.
150    #[must_use]
151    pub const fn get_credential_type(&self) -> CredentialType {
152        self.credential_type
153    }
154}
155
156// This impl block is not exported to foreign bindings.
157impl 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    /// LEGACY AND ADVANCED USE ONLY.
184    ///
185    /// Initializes a `ProofContext` from an arbitrary pre-image for an external nullifier.
186    ///
187    /// This is used for legacy nullifiers which were constructed from arbitrary bytes which don't follow
188    /// the `app_id` and `action` standard.
189    ///
190    /// # Usage (Non-exhaustive)
191    ///
192    /// - This is used for the World ID Address Book.
193    ///
194    /// # Arguments
195    ///
196    /// * `external_nullifier` - An arbitrary array of bytes that will be hashed to produce the external nullifier.
197    /// * `credential_type` - The type of credential being requested.
198    /// * `signal` - Optional. The signal is included in the ZKP and is committed to in the proof.
199    #[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    /// LEGACY AND ADVANCED USE ONLY.
215    ///
216    /// Initializes a `ProofContext` from a raw external nullifier.
217    ///
218    /// This is used for legacy nullifiers which were constructed from raw field elements.
219    ///
220    /// # Usage (Non-exhaustive)
221    ///
222    /// - This is used for Recurring Grant Claims (Worldcoin Airdrop).
223    /// - This is used to verify a World App account.
224    ///
225    /// # Arguments
226    ///
227    /// * `external_nullifier` - The raw external nullifier. Must already be a number in the field. No additional hashing is performed.
228    /// * `credential_type` - The type of credential being requested.
229    /// * `signal` - Optional. The signal is included in the ZKP and is committed to in the proof.
230    ///
231    /// # Errors
232    ///
233    /// - Returns an error if the external nullifier is not a valid number in the field.
234    #[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/// Represents the complete output of a World ID Proof (i.e. a credential persentation). This output
253/// can be serialized to JSON and can be verified easily with the Developer Portal or Sign up Sequencer.
254///
255/// For on-chain verification, the `proof` (which is packed) should generally be deserialized into `uint256[8]`.
256///
257/// More information on: [On-Chain Verification](https://docs.world.org/world-id/id/on-chain)
258#[derive(Clone, PartialEq, Eq, Debug, Serialize)]
259#[cfg_attr(feature = "ffi", derive(uniffi::Object))]
260#[allow(clippy::module_name_repetitions)]
261pub struct ProofOutput {
262    /// The root hash of the Merkle tree used to prove membership. This root hash should match published hashes in the World ID
263    ///     protocol contract in Ethereum mainnet. See [address book](https://docs.world.org/world-id/reference/address-book).
264    pub merkle_root: U256Wrapper,
265    /// Represents the unique identifier for a specific context (app & action) and World ID. A World ID holder will always generate
266    /// the same `nullifier_hash` for the same context.
267    pub nullifier_hash: U256Wrapper,
268    /// The raw zero-knowledge proof.
269    #[serde(skip_serializing)]
270    pub raw_proof: Proof,
271    /// The ABI-encoded zero-knowledge proof represented as a string. This is the format generally used with other libraries and
272    /// can be directly used with the Developer Portal for verification.
273    pub proof: PackedProof,
274    /// The credential type used to generate the proof.
275    pub credential_type: CredentialType,
276}
277
278#[cfg_attr(feature = "ffi", uniffi::export)]
279impl ProofOutput {
280    /// Converts the entire proof output to a JSON string with standard attribute names.
281    ///
282    /// # Errors
283    /// Will error if serialization fails.
284    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    /// Exposes the nullifier hash to foreign code. Struct fields are not directly exposed to foreign code.
293    #[must_use]
294    pub const fn get_nullifier_hash(&self) -> U256Wrapper {
295        self.nullifier_hash
296    }
297
298    /// Exposes the merkle root to foreign code. Struct fields are not directly exposed to foreign code.
299    #[must_use]
300    pub const fn get_merkle_root(&self) -> U256Wrapper {
301        self.merkle_root
302    }
303
304    /// Exposes the proof as a string to foreign code. Struct fields are not directly exposed to foreign code.
305    #[must_use]
306    pub fn get_proof_as_string(&self) -> String {
307        self.proof.to_string()
308    }
309
310    /// Exposes the credential type to foreign code. Struct fields are not directly exposed to foreign code.
311    #[must_use]
312    pub const fn get_credential_type(&self) -> CredentialType {
313        self.credential_type
314    }
315}
316
317/// Generates a Semaphore ZKP for a specific Semaphore identity using the relevant provided context.
318///
319/// **Requires the `semaphore` feature flag.**
320///
321/// # Errors
322/// Returns an error if proof generation fails
323pub 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; // clone the value
332
333    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        // note the same external nullifier hash is generated for an empty string action
374        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    /// This test case comes from the real example in the docs.
387    /// Reference: <https://github.com/worldcoin/world-id-docs/blob/main/src/pages/world-id/try.tsx>
388    #[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                // cspell:disable-next-line
414                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            // expected output obtained from Solidity
437            "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            // expected output obtained from Solidity
460            "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        // the expected nullifier hash from the contract
474        // reference: <https://worldchain-mainnet.explorer.alchemy.com/tx/0x974e70f125abe3b6abaa0b3fb9cb067c09cee359b08fa847487d6623377308fd>
475        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        // The constant which is added to all the Grant IDs in World Chain.
488        // Reference: <https://github.com/worldcoin/worldcoin-grants-contracts/blob/main/src/RecurringGrantDrop.sol#L22>
489        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        // the expected nullifier hash from the contract
503        // transaction example for RGD 48: <https://worldscan.org/tx/0xbad696a88c5425a22af18ea6d00efca78ae0f5c5cceade21597adf60126a5fc4>
504        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        // Compute ZKP
634        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 proof verifies locally
652        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        // Compute ZKP
679        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        // ensure the proof is automatically encoded as packed
700        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}