Skip to main content

nym_compact_ecash/scheme/
withdrawal.rs

1// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::common_types::{BlindedSignature, Signature, SignerIndex};
5use crate::error::{CompactEcashError, Result};
6use crate::helpers::{date_scalar, type_scalar};
7use crate::proofs::proof_withdrawal::{
8    WithdrawalReqInstance, WithdrawalReqProof, WithdrawalReqWitness,
9};
10use crate::scheme::keygen::{PublicKeyUser, SecretKeyAuth, SecretKeyUser, VerificationKeyAuth};
11use crate::scheme::setup::GroupParameters;
12use crate::scheme::PartialWallet;
13use crate::utils::{check_bilinear_pairing, hash_g1};
14use crate::{constants, ecash_group_parameters, Attribute, EncodedDate, EncodedTicketType};
15use group::{Curve, Group, GroupEncoding};
16use nym_bls12_381_fork::{multi_miller_loop, G1Projective, G2Prepared, G2Projective, Scalar};
17use serde::{Deserialize, Serialize};
18use std::ops::Neg;
19use zeroize::{Zeroize, ZeroizeOnDrop};
20
21/// Represents a withdrawal request generate by the client who wants to obtain a zk-nym credential.
22///
23/// This struct encapsulates the necessary components for a withdrawal request, including the joined commitment hash, the joined commitment,
24/// individual Pedersen commitments for private attributes, and a zero-knowledge proof for the withdrawal request.
25///
26/// # Fields
27///
28/// * `joined_commitment_hash` - The joined commitment hash represented as a G1Projective element.
29/// * `joined_commitment` - The joined commitment represented as a G1Projective element.
30/// * `private_attributes_commitments` - A vector of individual Pedersen commitments for private attributes represented as G1Projective elements.
31/// * `zk_proof` - The zero-knowledge proof for the withdrawal request.
32///
33/// # Derives
34///
35/// The struct derives `Debug` and `PartialEq` to provide debug output and basic comparison functionality.
36///
37#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
38pub struct WithdrawalRequest {
39    joined_commitment_hash: G1Projective,
40    joined_commitment: G1Projective,
41    private_attributes_commitments: Vec<G1Projective>,
42    zk_proof: WithdrawalReqProof,
43}
44
45impl WithdrawalRequest {
46    pub fn get_private_attributes_commitments(&self) -> &[G1Projective] {
47        &self.private_attributes_commitments
48    }
49}
50
51/// Represents information associated with a withdrawal request.
52///
53/// This structure holds the commitment hash, commitment opening, private attributes openings,
54/// the wallet secret (scalar), and the expiration date related to a withdrawal request.
55#[derive(Debug, Clone, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
56pub struct RequestInfo {
57    joined_commitment_hash: G1Projective,
58    joined_commitment_opening: Scalar,
59    private_attributes_openings: Vec<Scalar>,
60    wallet_secret: Scalar,
61    expiration_date: Scalar,
62    t_type: Scalar,
63}
64
65impl RequestInfo {
66    pub fn get_joined_commitment_hash(&self) -> &G1Projective {
67        &self.joined_commitment_hash
68    }
69    pub fn get_joined_commitment_opening(&self) -> &Scalar {
70        &self.joined_commitment_opening
71    }
72    pub fn get_private_attributes_openings(&self) -> &[Scalar] {
73        &self.private_attributes_openings
74    }
75    pub fn get_v(&self) -> &Scalar {
76        &self.wallet_secret
77    }
78    pub fn get_expiration_date(&self) -> &Scalar {
79        &self.expiration_date
80    }
81    pub fn get_t_type(&self) -> &Scalar {
82        &self.t_type
83    }
84}
85
86/// Computes Pedersen commitments for private attributes.
87///
88/// Given a set of private attributes and the commitment hash for all attributes,
89/// this function generates random blinding factors (`openings`) and computes corresponding
90/// Pedersen commitments for each private attribute.
91/// Pedersen commitments have the hiding and binding properties, providing a secure way
92/// to represent private values in a commitment scheme.
93///
94/// # Arguments
95///
96/// * `params` - Group parameters for the cryptographic group.
97/// * `joined_commitment_hash` - The commitment hash to be used in the Pedersen commitments.
98/// * `private_attributes` - A slice of private attributes to be committed.
99///
100/// # Returns
101///
102/// A tuple containing vectors of blinding factors (`openings`) and corresponding
103/// Pedersen commitments for each private attribute.
104fn compute_private_attribute_commitments(
105    params: &GroupParameters,
106    joined_commitment_hash: &G1Projective,
107    private_attributes: &[&Scalar],
108) -> (Vec<Scalar>, Vec<G1Projective>) {
109    let (openings, commitments): (Vec<Scalar>, Vec<G1Projective>) = private_attributes
110        .iter()
111        .map(|&m_j| {
112            let o_j = params.random_scalar();
113            (o_j, params.gen1() * o_j + joined_commitment_hash * m_j)
114        })
115        .unzip();
116
117    (openings, commitments)
118}
119
120/// Generates a non-identity hash of joined commitment.
121///
122/// This function attempts to create a valid joined commitment and hash by
123/// repeatedly generating a random `joined_commitment_opening` and computing
124/// the corresponding `joined_commitment` and `joined_commitment_hash`.
125/// It continues this process until the `joined_commitment_hash` is not the
126/// identity element.
127fn generate_non_identity_h(
128    params: &GroupParameters,
129    sk_user: &SecretKeyUser,
130    v: &Scalar,
131    expiration_date: Scalar,
132    t_type: Scalar,
133) -> Result<(G1Projective, G1Projective, Scalar)> {
134    let gamma = params.gammas();
135    if gamma.len() < 4 {
136        return Err(CompactEcashError::IncompatibleConstruction);
137    }
138
139    // SAFETY: we have ensured we have at least 4 gammas
140    #[allow(clippy::indexing_slicing)]
141    loop {
142        let joined_commitment_opening = params.random_scalar();
143
144        // Compute joined commitment for all attributes (public and private)
145        let joined_commitment =
146            params.gen1() * joined_commitment_opening + gamma[0] * sk_user.sk + gamma[1] * v;
147
148        // Compute commitment hash h
149        let joined_commitment_hash = hash_g1(
150            (joined_commitment + gamma[2] * expiration_date + gamma[3] * t_type).to_bytes(),
151        );
152
153        // Check if the joined_commitment_hash is not the identity element
154        if !bool::from(joined_commitment_hash.is_identity()) {
155            return Ok((
156                joined_commitment,
157                joined_commitment_hash,
158                joined_commitment_opening,
159            ));
160        }
161    }
162}
163/// Generates a withdrawal request for the given user to request a zk-nym credential wallet.
164///
165/// # Arguments
166///
167/// * `sk_user` - A reference to the user's secret key.
168/// * `expiration_date` - The expiration date for the withdrawal request.
169/// * `t_type` - The type of the ticket book
170///
171/// # Returns
172///
173/// A tuple containing the generated `WithdrawalRequest` and `RequestInfo`, or an error if the operation fails.
174///
175/// # Details
176///
177/// The function starts by generating a random, unique wallet secret `v` and computing the joined commitment for all attributes,
178/// including public (expiration date) and private ones (user secret key and wallet secret).
179/// It then calculates the commitment hash (`joined_commitment_hash`) and computes Pedersen commitments for private attributes.
180/// A zero-knowledge proof of knowledge is constructed to prove possession of specific attributes.
181///
182/// The resulting `WithdrawalRequest` includes the commitment hash, joined commitment, commitments for private
183/// attributes, and the constructed zero-knowledge proof.
184///
185/// The associated `RequestInfo` includes information such as commitment hash, commitment opening,
186/// openings for private attributes, `v`, and the expiration date.
187pub fn withdrawal_request(
188    sk_user: &SecretKeyUser,
189    expiration_date: EncodedDate,
190    t_type: EncodedTicketType,
191) -> Result<(WithdrawalRequest, RequestInfo)> {
192    let params = ecash_group_parameters();
193    // Generate random and unique wallet secret
194    let v = params.random_scalar();
195    let expiration_date = date_scalar(expiration_date);
196    let t_type = type_scalar(t_type);
197
198    // Generate a non-identity commitment hash
199    let (joined_commitment, joined_commitment_hash, joined_commitment_opening) =
200        generate_non_identity_h(params, sk_user, &v, expiration_date, t_type)?;
201
202    // Compute Pedersen commitments for private attributes (wallet secret and user's secret)
203    let private_attributes = vec![&sk_user.sk, &v];
204    let (private_attributes_openings, private_attributes_commitments) =
205        compute_private_attribute_commitments(params, &joined_commitment_hash, &private_attributes);
206
207    // construct a NIZK proof of knowledge proving possession of m1, m2, o, o1, o2
208    let instance = WithdrawalReqInstance {
209        joined_commitment,
210        joined_commitment_hash,
211        private_attributes_commitments: private_attributes_commitments.clone(),
212        pk_user: PublicKeyUser {
213            pk: params.gen1() * sk_user.sk,
214        },
215    };
216
217    let witness = WithdrawalReqWitness {
218        private_attributes,
219        joined_commitment_opening: &joined_commitment_opening,
220        private_attributes_openings: &private_attributes_openings,
221    };
222    let zk_proof = WithdrawalReqProof::construct(&instance, &witness)?;
223
224    // Create and return WithdrawalRequest and RequestInfo
225    Ok((
226        WithdrawalRequest {
227            joined_commitment_hash,
228            joined_commitment,
229            private_attributes_commitments,
230            zk_proof,
231        },
232        RequestInfo {
233            joined_commitment_hash,
234            joined_commitment_opening,
235            private_attributes_openings,
236            wallet_secret: v,
237            expiration_date,
238            t_type,
239        },
240    ))
241}
242
243/// Verifies the integrity of a withdrawal request, including the joined commitment hash
244/// and the zero-knowledge proof of knowledge.
245///
246/// # Arguments
247///
248/// * `req` - The withdrawal request to be verified.
249/// * `pk_user` - Public key of the user associated with the withdrawal request.
250/// * `expiration_date` - Expiration date for the ticket book.
251/// * `t_type` - The type of the ticket book
252///
253/// # Returns
254///
255/// Returns `Ok(true)` if the verification is successful, otherwise returns an error
256/// with a specific message indicating the verification failure.
257pub fn request_verify(
258    req: &WithdrawalRequest,
259    pk_user: PublicKeyUser,
260    expiration_date: EncodedDate,
261    t_type: EncodedTicketType,
262) -> Result<()> {
263    let params = ecash_group_parameters();
264
265    let gamma = params.gammas();
266    let expiration_date = date_scalar(expiration_date);
267    let t_type = type_scalar(t_type);
268
269    if bool::from(req.joined_commitment_hash.is_identity()) {
270        return Err(CompactEcashError::IdentityCommitmentHash);
271    }
272
273    if gamma.len() < 4 {
274        return Err(CompactEcashError::IncompatibleConstruction);
275    }
276
277    // SAFETY: we have ensured we have at least 4 gammas
278    #[allow(clippy::indexing_slicing)]
279    let expected_commitment_hash = hash_g1(
280        (req.joined_commitment + gamma[2] * expiration_date + gamma[3] * t_type).to_bytes(),
281    );
282    if req.joined_commitment_hash != expected_commitment_hash {
283        return Err(CompactEcashError::WithdrawalRequestVerification);
284    }
285    // Verify zk proof
286    let instance = WithdrawalReqInstance {
287        joined_commitment: req.joined_commitment,
288        joined_commitment_hash: req.joined_commitment_hash,
289        private_attributes_commitments: req.private_attributes_commitments.clone(),
290        pk_user,
291    };
292    if !req.zk_proof.verify(&instance) {
293        return Err(CompactEcashError::WithdrawalRequestVerification);
294    }
295    Ok(())
296}
297
298/// Signs an expiration date using a joined commitment hash and a secret key.
299///
300/// Given a joined commitment hash (`joined_commitment_hash`), an expiration date (`expiration_date`),
301/// and a secret key for authentication (`sk_auth`), this function computes the signature of the
302/// expiration date by multiplying the commitment hash with the blinding factor derived from the secret key
303/// and the expiration date.
304///
305/// # Arguments
306///
307/// * `joined_commitment_hash` - The G1Projective point representing the joined commitment hash.
308/// * `expiration_date` - The expiration date timestamp to be signed.
309/// * `sk_auth` - The secret key of the signing authority. Assumes key is long enough.
310///
311/// # Returns
312///
313/// A `Result` containing the resulting G1Projective point if successful, or an error if the
314/// authentication secret key index is out of bounds.
315fn sign_expiration_date(
316    joined_commitment_hash: &G1Projective,
317    expiration_date: EncodedDate,
318    sk_auth: &SecretKeyAuth,
319) -> Result<G1Projective> {
320    let el = sk_auth.ys.get(2).ok_or(CompactEcashError::KeyTooShort)?;
321    Ok(joined_commitment_hash * (el * date_scalar(expiration_date)))
322}
323
324/// Signs a transaction type using a joined commitment hash and a secret key.
325///
326/// Given a joined commitment hash (`joined_commitment_hash`), a ticket type (`t_type`),
327/// and a secret key for authentication (`sk_auth`), this function computes the signature of the
328/// ticket type.
329///
330/// # Arguments
331///
332/// * `joined_commitment_hash` - The G1Projective point representing the joined commitment hash.
333/// * `t_type` - The ticket type identifier to be signed.
334/// * `sk_auth` - The secret key of the signing authority.
335///
336/// # Returns
337///
338/// The resulting G1Projective point representing the signed ticket type.
339fn sign_t_type(
340    joined_commitment_hash: &G1Projective,
341    t_type: EncodedTicketType,
342    sk_auth: &SecretKeyAuth,
343) -> Result<G1Projective> {
344    let el = sk_auth.ys.get(3).ok_or(CompactEcashError::KeyTooShort)?;
345    Ok(joined_commitment_hash * (el * type_scalar(t_type)))
346}
347
348/// Issues a blinded signature for a withdrawal request, after verifying its integrity.
349///
350/// This function first verifies the withdrawal request using the provided group parameters,
351/// user's public key, and expiration date. If the verification is successful,
352/// the function proceeds to blind sign the private attributes and sign the expiration date,
353/// combining both signatures into a final signature.
354///
355/// # Arguments
356///
357/// * `sk_auth` - Secret key of the signing authority.
358/// * `pk_user` - Public key of the user associated with the withdrawal request.
359/// * `withdrawal_req` - The withdrawal request to be signed.
360/// * `expiration_date` - Expiration date for the withdrawal request.
361///
362/// # Returns
363///
364/// Returns a `BlindedSignature` if the issuance process is successful, otherwise returns an error
365/// with a specific message indicating the failure.
366pub fn issue(
367    sk_auth: &SecretKeyAuth,
368    pk_user: PublicKeyUser,
369    withdrawal_req: &WithdrawalRequest,
370    expiration_date: EncodedDate,
371    t_type: EncodedTicketType,
372) -> Result<BlindedSignature> {
373    // Verify the withdrawal request
374    request_verify(withdrawal_req, pk_user, expiration_date, t_type)?;
375    // Verify `sk_auth` is long enough
376    if sk_auth.ys.len() < constants::ATTRIBUTES_LEN {
377        return Err(CompactEcashError::KeyTooShort);
378    }
379    // Blind sign the private attributes
380    let blind_signatures: G1Projective = withdrawal_req
381        .private_attributes_commitments
382        .iter()
383        .zip(sk_auth.ys.iter().take(2))
384        .map(|(pc, yi)| pc * yi)
385        .sum();
386    // Sign the expiration date
387    let expiration_date_sign = sign_expiration_date(
388        &withdrawal_req.joined_commitment_hash,
389        expiration_date,
390        sk_auth,
391    )?;
392    // Sign the type
393    let t_type_sign = sign_t_type(&withdrawal_req.joined_commitment_hash, t_type, sk_auth)?;
394    // Combine both signatures
395    let signature = blind_signatures
396        + withdrawal_req.joined_commitment_hash * sk_auth.x
397        + expiration_date_sign
398        + t_type_sign;
399
400    Ok(BlindedSignature {
401        h: withdrawal_req.joined_commitment_hash,
402        c: signature,
403    })
404}
405
406/// Verifies the integrity and correctness of a blinded signature
407/// and returns an unblinded partial zk-nym wallet.
408///
409/// This function first verifies the integrity of the received blinded signature by checking
410/// if the joined commitment hash matches the one provided in the `req_info`. If the verification
411/// is successful, it proceeds to unblind the blinded signature and verify its correctness.
412///
413/// # Arguments
414///
415/// * `vk_auth` - Verification key of the signing authority.
416/// * `sk_user` - Secret key of the user.
417/// * `blind_signature` - Blinded signature received from the authority.
418/// * `req_info` - Information associated with the request, including the joined commitment hash,
419///   private attributes openings, v, and expiration date.
420///
421/// # Returns
422///
423/// Returns a `PartialWallet` if the verification process is successful, otherwise returns an error
424/// with a specific message indicating the failure.
425pub fn issue_verify(
426    vk_auth: &VerificationKeyAuth,
427    sk_user: &SecretKeyUser,
428    blind_signature: &BlindedSignature,
429    req_info: &RequestInfo,
430    signer_index: SignerIndex,
431) -> Result<PartialWallet> {
432    let params = ecash_group_parameters();
433    // Verify the integrity of the response from the authority
434    if req_info.joined_commitment_hash != blind_signature.h {
435        return Err(CompactEcashError::IssuanceVerification);
436    }
437    if bool::from(blind_signature.h.is_identity()) {
438        return Err(CompactEcashError::IdentitySignature);
439    }
440
441    // Unblind the blinded signature on the partial signature
442    let blinding_removers = vk_auth
443        .beta_g1
444        .iter()
445        .zip(&req_info.private_attributes_openings)
446        .map(|(beta, opening)| beta * opening)
447        .sum::<G1Projective>();
448    let unblinded_c = blind_signature.c - blinding_removers;
449
450    let attr = [
451        sk_user.sk,
452        req_info.wallet_secret,
453        req_info.expiration_date,
454        req_info.t_type,
455    ];
456
457    let signed_attributes = attr
458        .iter()
459        .zip(vk_auth.beta_g2.iter())
460        .map(|(attr, beta_i)| beta_i * attr)
461        .sum::<G2Projective>();
462
463    // Verify the signature correctness on the wallet share
464    if !check_bilinear_pairing(
465        &blind_signature.h.to_affine(),
466        &G2Prepared::from((vk_auth.alpha + signed_attributes).to_affine()),
467        &unblinded_c.to_affine(),
468        params.prepared_miller_g2(),
469    ) {
470        return Err(CompactEcashError::IssuanceVerification);
471    }
472
473    Ok(PartialWallet {
474        sig: Signature {
475            h: blind_signature.h,
476            s: unblinded_c,
477        },
478        v: req_info.wallet_secret,
479        idx: signer_index,
480        expiration_date: req_info.expiration_date,
481        t_type: req_info.t_type,
482    })
483}
484
485/// Verifies a partial blind signature using the provided parameters and validator's verification key.
486///
487/// # Arguments
488///
489/// * `blind_sign_request` - A reference to the blind signature request signed by the client.
490/// * `public_attributes` - A reference to the public attributes included in the client's request.
491/// * `blind_sig` - A reference to the issued partial blinded signature to be verified.
492/// * `partial_verification_key` - A reference to the validator's partial verification key.
493///
494/// # Returns
495///
496/// A boolean indicating whether the partial blind signature is valid (`true`) or not (`false`).
497///
498/// # Remarks
499///
500/// This function verifies the correctness and validity of a partial blind signature using
501/// the provided cryptographic parameters, blind signature request, blinded signature,
502/// and partial verification key.
503/// It calculates pairings based on the provided values and checks whether the partial blind signature
504/// is consistent with the verification key and commitments in the blind signature request.
505/// The function returns `true` if the partial blind signature is valid, and `false` otherwise.
506pub fn verify_partial_blind_signature(
507    private_attribute_commitments: &[G1Projective],
508    public_attributes: &[&Attribute],
509    blind_sig: &BlindedSignature,
510    partial_verification_key: &VerificationKeyAuth,
511) -> bool {
512    let params = ecash_group_parameters();
513    let num_private_attributes = private_attribute_commitments.len();
514    if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() {
515        return false;
516    }
517    // Note: This check is useful if someone uses the code of those functions
518    // to verify Pointcheval-Sanders signatures in a context different for their use
519    // in zk-nyms
520    if bool::from(blind_sig.h.is_identity()) {
521        return false;
522    }
523    // TODO: we're losing some memory here due to extra allocation,
524    // but worst-case scenario (given SANE amount of attributes), it's just few kb at most
525    let c_neg = blind_sig.c.to_affine().neg();
526    let g2_prep = params.prepared_miller_g2();
527
528    let mut terms = vec![
529        // (c^{-1}, g2)
530        (c_neg, g2_prep.clone()),
531        // (s, alpha)
532        (
533            blind_sig.h.to_affine(),
534            G2Prepared::from(partial_verification_key.alpha.to_affine()),
535        ),
536    ];
537
538    // for each private attribute, add (cm_i, beta_i) to the miller terms
539    for (private_attr_commit, beta_g2) in private_attribute_commitments
540        .iter()
541        .zip(&partial_verification_key.beta_g2)
542    {
543        // (cm_i, beta_i)
544        terms.push((
545            private_attr_commit.to_affine(),
546            G2Prepared::from(beta_g2.to_affine()),
547        ))
548    }
549
550    // for each public attribute, add (s^pub_j, beta_{priv + j}) to the miller terms
551    for (&pub_attr, beta_g2) in public_attributes.iter().zip(
552        partial_verification_key
553            .beta_g2
554            .iter()
555            .skip(num_private_attributes),
556    ) {
557        // (s^pub_j, beta_j)
558        terms.push((
559            (blind_sig.h * pub_attr).to_affine(),
560            G2Prepared::from(beta_g2.to_affine()),
561        ))
562    }
563
564    // get the references to all the terms to get the arguments the miller loop expects
565    #[allow(clippy::map_identity)]
566    let terms_refs = terms.iter().map(|(g1, g2)| (g1, g2)).collect::<Vec<_>>();
567
568    // since checking whether e(a, b) == e(c, d)
569    // is equivalent to checking e(a, b) • e(c, d)^{-1} == id
570    // and thus to e(a, b) • e(c^{-1}, d) == id
571    //
572    // compute e(c^{-1}, g2) • e(s, alpha) • e(cm_0, beta_0) • e(cm_i, beta_i) • (s^pub_0, beta_{i+1}) (s^pub_j, beta_{i + j})
573    multi_miller_loop(&terms_refs)
574        .final_exponentiation()
575        .is_identity()
576        .into()
577}
578
579#[cfg(test)]
580mod tests {
581    use super::{generate_non_identity_h, verify_partial_blind_signature};
582    use crate::common_types::BlindedSignature;
583    use crate::ecash_group_parameters;
584    use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth};
585    use nym_bls12_381_fork::G1Projective;
586
587    #[test]
588    fn test_generate_non_identity_h() {
589        let params = ecash_group_parameters();
590        // Create dummy values for testing
591        let sk_user = SecretKeyUser {
592            sk: params.random_scalar(),
593        };
594        let v = params.random_scalar();
595        let expiration_date = params.random_scalar();
596        let t_type = params.random_scalar();
597
598        // Generate the commitment and hash
599        let (_, joined_commitment_hash, _) =
600            generate_non_identity_h(params, &sk_user, &v, expiration_date, t_type).unwrap();
601
602        // Ensure that the joined_commitment_hash is not the identity element
603        assert!(
604            !bool::from(joined_commitment_hash.is_identity()),
605            "Joined commitment hash should not be the identity element"
606        );
607    }
608
609    #[test]
610    fn test_verify_partial_blind_signature_blind_sig_identity() {
611        let params = ecash_group_parameters();
612        let private_attribute_commitments = vec![params.gen1() * params.random_scalar()];
613        let public_attributes = vec![];
614        // Create a blinded signature with h being the identity element
615        let blind_sig = BlindedSignature {
616            h: G1Projective::identity(),
617            c: params.gen1() * params.random_scalar(),
618        };
619        // Create a mock partial verification key
620        let partial_verification_key = VerificationKeyAuth {
621            alpha: params.gen2() * params.random_scalar(),
622            beta_g1: vec![params.gen1() * params.random_scalar()],
623            beta_g2: vec![params.gen2() * params.random_scalar()],
624        };
625
626        // Test with identity h, expecting false
627        assert!(
628            !verify_partial_blind_signature(
629                &private_attribute_commitments,
630                &public_attributes,
631                &blind_sig,
632                &partial_verification_key
633            ),
634            "Expected verification to return false for identity h in blind signature"
635        );
636    }
637}