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