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}