Skip to main content

nym_compact_ecash/scheme/
mod.rs

1// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::common_types::{Signature, SignerIndex};
5use crate::error::{CompactEcashError, Result};
6use crate::helpers::{date_scalar, type_scalar};
7use crate::proofs::proof_spend::{SpendInstance, SpendProof, SpendWitness};
8use crate::scheme::coin_indices_signatures::CoinIndexSignature;
9use crate::scheme::expiration_date_signatures::{find_index, ExpirationDateSignature};
10use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth};
11use crate::scheme::setup::{GroupParameters, Parameters};
12use crate::traits::Bytable;
13use crate::utils::{
14    batch_verify_signatures, check_bilinear_pairing, hash_to_scalar, try_deserialize_scalar,
15};
16use crate::{constants, ecash_group_parameters};
17use crate::{Base58, EncodedDate, EncodedTicketType};
18use group::Curve;
19use nym_bls12_381_fork::{G1Projective, G2Prepared, G2Projective, Scalar};
20use serde::{Deserialize, Deserializer, Serialize, Serializer};
21use std::borrow::Borrow;
22use zeroize::{Zeroize, ZeroizeOnDrop};
23
24pub mod aggregation;
25pub mod coin_indices_signatures;
26pub mod expiration_date_signatures;
27pub mod identify;
28pub mod keygen;
29pub mod setup;
30pub mod withdrawal;
31
32/// The struct represents a partial wallet with essential components for a payment transaction.
33///
34/// A `PartialWallet` includes a Pointcheval-Sanders signature (`sig`),
35/// a scalar value (`v`) representing the wallet's secret, an optional
36/// `SignerIndex` (`idx`) indicating the signer's index, and an expiration date (`expiration_date`).
37///
38#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop)]
39pub struct PartialWallet {
40    #[zeroize(skip)]
41    sig: Signature,
42    v: Scalar,
43    idx: SignerIndex,
44    expiration_date: Scalar,
45    t_type: Scalar,
46}
47
48impl PartialWallet {
49    pub fn signature(&self) -> &Signature {
50        &self.sig
51    }
52
53    pub fn index(&self) -> SignerIndex {
54        self.idx
55    }
56    pub fn expiration_date(&self) -> Scalar {
57        self.expiration_date
58    }
59    pub fn t_type(&self) -> Scalar {
60        self.t_type
61    }
62
63    /// Converts the `PartialWallet` to a fixed-size byte array.
64    ///
65    /// The resulting byte array has a length of 200 bytes and contains serialized
66    /// representations of the `Signature` (`sig`), scalar value (`v`),
67    /// expiration date (`expiration_date`), and `idx` fields of the `PartialWallet` struct.
68    ///
69    /// # Returns
70    ///
71    /// A fixed-size byte array (`[u8; 200]`) representing the serialized form of the `PartialWallet`.
72    ///
73    pub fn to_bytes(&self) -> [u8; 200] {
74        let mut bytes = [0u8; 200];
75        bytes[0..96].copy_from_slice(&self.sig.to_bytes());
76        bytes[96..128].copy_from_slice(&self.v.to_bytes());
77        bytes[128..160].copy_from_slice(&self.expiration_date.to_bytes());
78        bytes[160..192].copy_from_slice(&self.t_type.to_bytes());
79        bytes[192..200].copy_from_slice(&self.idx.to_le_bytes());
80        bytes
81    }
82
83    /// Convert a byte slice into a `PartialWallet` instance.
84    ///
85    /// This function performs deserialization on the provided byte slice, which
86    /// represent a serialized `PartialWallet`.
87    ///
88    /// # Arguments
89    ///
90    /// * `bytes` - A reference to the byte slice to be deserialized.
91    ///
92    /// # Returns
93    ///
94    /// A `Result` containing the deserialized `PartialWallet` if successful, or a
95    /// `CompactEcashError` indicating the reason for failure.
96    pub fn from_bytes(bytes: &[u8]) -> Result<PartialWallet> {
97        const SIGNATURE_BYTES: usize = 96;
98        const V_BYTES: usize = 32;
99        const EXPIRATION_DATE_BYTES: usize = 32;
100        const T_TYPE_BYTES: usize = 32;
101        const IDX_BYTES: usize = 8;
102        const EXPECTED_LENGTH: usize =
103            SIGNATURE_BYTES + V_BYTES + EXPIRATION_DATE_BYTES + T_TYPE_BYTES + IDX_BYTES;
104
105        if bytes.len() != EXPECTED_LENGTH {
106            return Err(CompactEcashError::DeserializationLengthMismatch {
107                type_name: "PartialWallet".into(),
108                expected: EXPECTED_LENGTH,
109                actual: bytes.len(),
110            });
111        }
112
113        let mut j = 0;
114
115        // we have performed length check
116        #[allow(clippy::indexing_slicing)]
117        let sig = Signature::try_from(&bytes[j..j + SIGNATURE_BYTES])?;
118        j += SIGNATURE_BYTES;
119
120        //SAFETY: slice to array after length check
121        #[allow(clippy::unwrap_used)]
122        #[allow(clippy::indexing_slicing)]
123        let v_bytes = bytes[j..j + V_BYTES].try_into().unwrap();
124        let v = try_deserialize_scalar(v_bytes)?;
125        j += V_BYTES;
126
127        //SAFETY: slice to array after length check
128        #[allow(clippy::unwrap_used)]
129        #[allow(clippy::indexing_slicing)]
130        let expiration_date_bytes = bytes[j..j + EXPIRATION_DATE_BYTES].try_into().unwrap();
131        let expiration_date = try_deserialize_scalar(expiration_date_bytes)?;
132        j += EXPIRATION_DATE_BYTES;
133        //SAFETY: slice to array after length check
134        #[allow(clippy::unwrap_used)]
135        #[allow(clippy::indexing_slicing)]
136        let t_type_bytes = bytes[j..j + T_TYPE_BYTES].try_into().unwrap();
137        let t_type = try_deserialize_scalar(t_type_bytes)?;
138        j += T_TYPE_BYTES;
139
140        //SAFETY: slice to array after length check
141        #[allow(clippy::unwrap_used)]
142        #[allow(clippy::indexing_slicing)]
143        let idx_bytes = bytes[j..].try_into().unwrap();
144        let idx = u64::from_le_bytes(idx_bytes);
145
146        Ok(PartialWallet {
147            sig,
148            v,
149            idx,
150            expiration_date,
151            t_type,
152        })
153    }
154}
155
156#[derive(Debug, Clone, PartialEq, Zeroize, Serialize, Deserialize)]
157pub struct Wallet {
158    /// The cryptographic materials required for producing spending proofs and payments.
159    signatures: WalletSignatures,
160
161    /// Also known as `l` parameter in the paper
162    tickets_spent: u64,
163}
164
165impl Wallet {
166    pub fn new(signatures: WalletSignatures, tickets_spent: u64) -> Self {
167        Wallet {
168            signatures,
169            tickets_spent,
170        }
171    }
172
173    pub fn into_wallet_signatures(self) -> WalletSignatures {
174        self.into()
175    }
176
177    pub fn to_bytes(&self) -> [u8; WalletSignatures::SERIALISED_SIZE + 8] {
178        let mut bytes = [0u8; WalletSignatures::SERIALISED_SIZE + 8];
179        bytes[0..WalletSignatures::SERIALISED_SIZE].copy_from_slice(&self.signatures.to_bytes());
180        bytes[WalletSignatures::SERIALISED_SIZE..]
181            .copy_from_slice(&self.tickets_spent.to_be_bytes());
182        bytes
183    }
184
185    pub fn from_bytes(bytes: &[u8]) -> Result<Wallet> {
186        if bytes.len() != WalletSignatures::SERIALISED_SIZE + 8 {
187            return Err(CompactEcashError::DeserializationLengthMismatch {
188                type_name: "Wallet".into(),
189                expected: WalletSignatures::SERIALISED_SIZE + 8,
190                actual: bytes.len(),
191            });
192        }
193
194        //SAFETY : slice to array conversions after a length check
195        #[allow(clippy::unwrap_used)]
196        #[allow(clippy::indexing_slicing)]
197        let tickets_bytes = bytes[WalletSignatures::SERIALISED_SIZE..]
198            .try_into()
199            .unwrap();
200
201        #[allow(clippy::indexing_slicing)]
202        let signatures = WalletSignatures::from_bytes(&bytes[..WalletSignatures::SERIALISED_SIZE])?;
203        let tickets_spent = u64::from_be_bytes(tickets_bytes);
204
205        Ok(Wallet {
206            signatures,
207            tickets_spent,
208        })
209    }
210
211    pub fn ensure_allowance(
212        params: &Parameters,
213        tickets_spent: u64,
214        spend_value: u64,
215    ) -> Result<()> {
216        if tickets_spent + spend_value > params.get_total_coins() {
217            Err(CompactEcashError::SpendExceedsAllowance {
218                spending: spend_value,
219                remaining: params.get_total_coins() - tickets_spent,
220            })
221        } else {
222            Ok(())
223        }
224    }
225
226    pub fn check_remaining_allowance(&self, params: &Parameters, spend_value: u64) -> Result<()> {
227        Self::ensure_allowance(params, self.tickets_spent, spend_value)
228    }
229
230    #[allow(clippy::too_many_arguments)]
231    pub fn spend(
232        &mut self,
233        params: &Parameters,
234        verification_key: &VerificationKeyAuth,
235        sk_user: &SecretKeyUser,
236        pay_info: &PayInfo,
237        spend_value: u64,
238        valid_dates_signatures: &[ExpirationDateSignature],
239        coin_indices_signatures: &[CoinIndexSignature],
240        spend_date_timestamp: EncodedDate,
241    ) -> Result<Payment> {
242        self.check_remaining_allowance(params, spend_value)?;
243
244        // produce payment
245        let payment = self.signatures.spend(
246            params,
247            verification_key,
248            sk_user,
249            pay_info,
250            self.tickets_spent,
251            spend_value,
252            valid_dates_signatures,
253            coin_indices_signatures,
254            spend_date_timestamp,
255        )?;
256
257        // update the ticket counter
258        self.tickets_spent += spend_value;
259        Ok(payment)
260    }
261}
262
263impl From<Wallet> for WalletSignatures {
264    fn from(value: Wallet) -> Self {
265        value.signatures
266    }
267}
268
269/// The struct represents a wallet with essential components for a payment transaction.
270///
271/// A `Wallet` includes a Pointcheval-Sanders signature (`sig`),
272/// a scalar value (`v`) representing the wallet's secret, an optional
273/// an expiration date (`expiration_date`)
274/// and an u64 ('l') indicating the total number of spent coins.
275///
276#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
277pub struct WalletSignatures {
278    #[zeroize(skip)]
279    sig: Signature,
280    v: Scalar,
281    expiration_date_timestamp: EncodedDate,
282    t_type: EncodedTicketType,
283}
284
285impl WalletSignatures {
286    pub fn with_tickets_spent(self, tickets_spent: u64) -> Wallet {
287        Wallet {
288            signatures: self,
289            tickets_spent,
290        }
291    }
292
293    pub fn new_wallet(self) -> Wallet {
294        self.with_tickets_spent(0)
295    }
296
297    pub fn encoded_expiration_date(&self) -> Scalar {
298        date_scalar(self.expiration_date_timestamp)
299    }
300}
301
302/// Computes the hash of payment information concatenated with a numeric value.
303///
304/// This function takes a `PayInfo` structure and a numeric value `k`, and
305/// concatenates the serialized `payinfo` field of `PayInfo` with the little-endian
306/// byte representation of `k`. The resulting byte sequence is then hashed to produce
307/// a scalar value using the `hash_to_scalar` function.
308///
309/// # Arguments
310///
311/// * `pay_info` - A reference to the `PayInfo` structure containing payment information.
312/// * `k` - A numeric value used in the hash computation.
313///
314/// # Returns
315///
316/// A `Scalar` value representing the hash of the concatenated byte sequence.
317///
318pub fn compute_pay_info_hash(pay_info: &PayInfo, k: u64) -> Scalar {
319    let mut bytes = Vec::new();
320    bytes.extend_from_slice(&pay_info.pay_info_bytes);
321    bytes.extend_from_slice(&k.to_le_bytes());
322    hash_to_scalar(bytes)
323}
324
325impl WalletSignatures {
326    // signature size (96) + secret size (32) + expiration size (4) + t_type (1)
327    pub const SERIALISED_SIZE: usize = 133;
328
329    pub fn signature(&self) -> &Signature {
330        &self.sig
331    }
332
333    /// Converts the `WalletSignatures` to a fixed-size byte array.
334    ///
335    /// The resulting byte array has a length of 168 bytes and contains serialized
336    /// representations of the `Signature` (`sig`), scalar value (`v`), and
337    /// expiration date (`expiration_date`) fields of the `WalletSignatures` struct.
338    ///
339    /// # Returns
340    ///
341    /// A fixed-size byte array (`[u8; 136]`) representing the serialized form of the `Wallet`.
342    ///
343    pub fn to_bytes(&self) -> [u8; Self::SERIALISED_SIZE] {
344        let mut bytes = [0u8; Self::SERIALISED_SIZE];
345        bytes[0..96].copy_from_slice(&self.sig.to_bytes());
346        bytes[96..128].copy_from_slice(&self.v.to_bytes());
347        bytes[128..132].copy_from_slice(&self.expiration_date_timestamp.to_be_bytes());
348        bytes[132] = self.t_type;
349        bytes
350    }
351
352    pub fn from_bytes(bytes: &[u8]) -> Result<WalletSignatures> {
353        if bytes.len() != Self::SERIALISED_SIZE {
354            return Err(CompactEcashError::DeserializationLengthMismatch {
355                type_name: "WalletSignatures".into(),
356                expected: Self::SERIALISED_SIZE,
357                actual: bytes.len(),
358            });
359        }
360        //SAFETY : slice to array conversions after a length check
361        #[allow(clippy::unwrap_used)]
362        #[allow(clippy::indexing_slicing)]
363        let sig_bytes: &[u8; 96] = &bytes[..96].try_into().unwrap();
364
365        #[allow(clippy::unwrap_used)]
366        #[allow(clippy::indexing_slicing)]
367        let v_bytes: &[u8; 32] = &bytes[96..128].try_into().unwrap();
368
369        #[allow(clippy::unwrap_used)]
370        #[allow(clippy::indexing_slicing)]
371        let expiration_date_bytes = bytes[128..132].try_into().unwrap();
372
373        let sig = Signature::try_from(sig_bytes.as_slice())?;
374        let v = Scalar::from_bytes(v_bytes).unwrap();
375        let expiration_date_timestamp = EncodedDate::from_be_bytes(expiration_date_bytes);
376        #[allow(clippy::indexing_slicing)]
377        let t_type = bytes[132];
378
379        Ok(WalletSignatures {
380            sig,
381            v,
382            expiration_date_timestamp,
383            t_type,
384        })
385    }
386
387    /// Performs a spending operation with the given parameters, updating the wallet and generating a payment.
388    ///
389    /// # Arguments
390    ///
391    /// * `verification_key` - The global verification key.
392    /// * `sk_user` - The secret key of the user who wants to spend from their wallet.
393    /// * `pay_info` - Unique information related to the payment.
394    /// * `current_tickets_spent` - The total number of tickets already spent in the associated wallet.
395    /// * `spend_value` - The amount to spend from the wallet.
396    /// * `valid_dates_signatures` - A list of **SORTED** signatures on valid dates during which we can spend from the wallet.
397    /// * `coin_indices_signatures` - A list of **SORTED** signatures on coin indices.
398    /// * `spend_date` - The date on which the spending occurs, expressed as unix timestamp.
399    ///
400    /// # Returns
401    ///
402    /// A tuple containing the generated payment and a reference to the updated wallet, or an error.
403    #[allow(clippy::too_many_arguments)]
404    pub fn spend<BI, BE>(
405        &self,
406        params: &Parameters,
407        verification_key: &VerificationKeyAuth,
408        sk_user: &SecretKeyUser,
409        pay_info: &PayInfo,
410        current_tickets_spent: u64,
411        spend_value: u64,
412        valid_dates_signatures: &[BE],
413        coin_indices_signatures: &[BI],
414        spend_date_timestamp: EncodedDate,
415    ) -> Result<Payment>
416    where
417        BI: Borrow<CoinIndexSignature>,
418        BE: Borrow<ExpirationDateSignature>,
419    {
420        // Extract group parameters
421        let grp_params = params.grp();
422
423        if verification_key.beta_g2.is_empty() {
424            return Err(CompactEcashError::VerificationKeyTooShort);
425        }
426
427        if valid_dates_signatures.len() != constants::CRED_VALIDITY_PERIOD_DAYS as usize {
428            return Err(CompactEcashError::InsufficientNumberOfExpirationSignatures);
429        }
430
431        if coin_indices_signatures.len() != params.get_total_coins() as usize {
432            return Err(CompactEcashError::InsufficientNumberOfIndexSignatures);
433        }
434
435        Wallet::ensure_allowance(params, current_tickets_spent, spend_value)?;
436
437        // Wallet attributes needed for spending
438        let attributes = [&sk_user.sk, &self.v, &self.encoded_expiration_date()];
439
440        // Randomize wallet signature
441        let (signature_prime, sign_blinding_factor) = self.signature().blind_and_randomise();
442
443        // compute kappa (i.e., blinded attributes for show) to prove possession of the wallet signature
444        let kappa = compute_kappa(
445            grp_params,
446            verification_key,
447            &attributes,
448            sign_blinding_factor,
449        );
450
451        // Randomise the expiration date signature for the date when we want to perform the spending, and compute kappa_e to prove possession of
452        // the expiration signature
453        let date_signature_index =
454            find_index(spend_date_timestamp, self.expiration_date_timestamp)?;
455
456        //SAFETY : find_index eiter returns a valid index or an error. The unwrap is therefore fine
457        #[allow(clippy::unwrap_used)]
458        let date_signature = valid_dates_signatures
459            .get(date_signature_index)
460            .unwrap()
461            .borrow();
462        let (date_signature_prime, date_sign_blinding_factor) =
463            date_signature.blind_and_randomise();
464        // compute kappa_e to prove possession of the expiration signature
465        //SAFETY: we checked that verification beta_g2 isn't empty
466        #[allow(clippy::unwrap_used)]
467        let kappa_e: G2Projective = grp_params.gen2() * date_sign_blinding_factor
468            + verification_key.alpha
469            + verification_key.beta_g2.first().unwrap() * self.encoded_expiration_date();
470
471        // pick random openings o_c and compute commitments C to v (wallet secret)
472        let o_c = grp_params.random_scalar();
473        //SAFETY: grp_params is static with length 3
474        #[allow(clippy::unwrap_used)]
475        let cc = grp_params.gen1() * o_c + grp_params.gamma_idx(1).unwrap() * self.v;
476
477        let mut aa: Vec<G1Projective> = Default::default();
478        let mut ss: Vec<G1Projective> = Default::default();
479        let mut tt: Vec<G1Projective> = Default::default();
480        let mut rr: Vec<Scalar> = Default::default();
481        let mut o_a: Vec<Scalar> = Default::default();
482        let mut o_mu: Vec<Scalar> = Default::default();
483        let mut mu: Vec<Scalar> = Default::default();
484        let r_k_vec: Vec<Scalar> = Default::default();
485        let mut kappa_k_vec: Vec<G2Projective> = Default::default();
486        let mut lk_vec: Vec<Scalar> = Default::default();
487
488        let mut coin_indices_signatures_prime: Vec<CoinIndexSignature> = Default::default();
489        for k in 0..spend_value {
490            let lk = current_tickets_spent + k;
491            lk_vec.push(Scalar::from(lk));
492
493            // compute hashes R_k = H(payinfo, k)
494            let rr_k = compute_pay_info_hash(pay_info, k);
495            rr.push(rr_k);
496
497            let o_a_k = grp_params.random_scalar();
498            o_a.push(o_a_k);
499            //SAFETY: grp_params is static with length 3
500            #[allow(clippy::unwrap_used)]
501            let aa_k =
502                grp_params.gen1() * o_a_k + grp_params.gamma_idx(1).unwrap() * Scalar::from(lk);
503            aa.push(aa_k);
504
505            // compute the serial numbers
506            let ss_k = pseudorandom_f_delta_v(grp_params, &self.v, lk)?;
507            ss.push(ss_k);
508            // compute the identification tags
509            let tt_k = grp_params.gen1() * sk_user.sk
510                + pseudorandom_f_g_v(grp_params, &self.v, lk)? * rr_k;
511            tt.push(tt_k);
512
513            // compute values mu, o_mu, lambda, o_lambda
514            let maybe_mu_k: Option<Scalar> = (self.v + Scalar::from(lk) + Scalar::from(1))
515                .invert()
516                .into();
517            let mu_k = maybe_mu_k.ok_or(CompactEcashError::UnluckiestError)?;
518            mu.push(mu_k);
519
520            let o_mu_k = ((o_a_k + o_c) * mu_k).neg();
521            o_mu.push(o_mu_k);
522
523            // Randomize the coin index signatures and compute kappa_k to prove possession of each coin's signature
524            // This involves iterating over the signatures corresponding to the coins we want to spend in this payment.
525            //SAFETY : Earlier `ensure_allowance` ensures we don't do out of of bound here
526            #[allow(clippy::unwrap_used)]
527            let coin_sign = coin_indices_signatures.get(lk as usize).unwrap().borrow();
528            let (coin_sign_prime, coin_sign_blinding_factor) = coin_sign.blind_and_randomise();
529            coin_indices_signatures_prime.push(coin_sign_prime);
530            //SAFETY: we checked that verification beta_g2 isn't empty
531            #[allow(clippy::unwrap_used)]
532            let kappa_k: G2Projective = grp_params.gen2() * coin_sign_blinding_factor
533                + verification_key.alpha
534                + verification_key.beta_g2.first().unwrap() * Scalar::from(lk);
535            kappa_k_vec.push(kappa_k);
536        }
537
538        // construct the zkp proof
539        let spend_instance = SpendInstance {
540            kappa,
541            cc,
542            aa: aa.clone(),
543            ss: ss.clone(),
544            tt: tt.clone(),
545            kappa_k: kappa_k_vec.clone(),
546            kappa_e,
547        };
548        let spend_witness = SpendWitness {
549            attributes: &attributes,
550            r: sign_blinding_factor,
551            o_c,
552            lk: lk_vec,
553            o_a,
554            mu,
555            o_mu,
556            r_k: r_k_vec,
557            r_e: date_sign_blinding_factor,
558        };
559
560        let zk_proof = SpendProof::construct(
561            &spend_instance,
562            &spend_witness,
563            verification_key,
564            &rr,
565            pay_info,
566            spend_value,
567        )?;
568
569        // output pay
570        let pay = Payment {
571            kappa,
572            kappa_e,
573            sig: signature_prime,
574            sig_exp: date_signature_prime,
575            kappa_k: kappa_k_vec.clone(),
576            omega: coin_indices_signatures_prime,
577            ss: ss.clone(),
578            tt: tt.clone(),
579            aa: aa.clone(),
580            spend_value,
581            cc,
582            t_type: self.t_type,
583            zk_proof,
584        };
585
586        Ok(pay)
587    }
588}
589
590fn pseudorandom_f_delta_v(params: &GroupParameters, v: &Scalar, l: u64) -> Result<G1Projective> {
591    let maybe_pow: Option<Scalar> = (v + Scalar::from(l) + Scalar::from(1)).invert().into();
592    Ok(params.delta() * maybe_pow.ok_or(CompactEcashError::UnluckiestError)?)
593}
594
595fn pseudorandom_f_g_v(params: &GroupParameters, v: &Scalar, l: u64) -> Result<G1Projective> {
596    let maybe_pow: Option<Scalar> = (v + Scalar::from(l) + Scalar::from(1)).invert().into();
597    Ok(params.gen1() * maybe_pow.ok_or(CompactEcashError::UnluckiestError)?)
598}
599
600/// Computes the value of kappa (blinded private attributes for show) for proving possession of the wallet signature.
601///
602/// This function calculates the value of kappa, which is used to prove possession of the wallet signature in the zero-knowledge proof.
603///
604/// # Arguments
605///
606/// * `params` - A reference to the group parameters required for the computation.
607/// * `verification_key` - The global verification key of the signing authorities.
608/// * `attributes` - A slice of private attributes associated with the wallet.
609/// * `blinding_factor` - The blinding factor used to randomise the wallet's signature.
610///
611/// # Returns
612///
613/// A `G2Projective` element representing the computed value of kappa.
614///
615fn compute_kappa(
616    params: &GroupParameters,
617    verification_key: &VerificationKeyAuth,
618    attributes: &[&Scalar],
619    blinding_factor: Scalar,
620) -> G2Projective {
621    params.gen2() * blinding_factor
622        + verification_key.alpha
623        + attributes
624            .iter()
625            .zip(verification_key.beta_g2.iter())
626            .map(|(&priv_attr, beta_i)| beta_i * priv_attr)
627            .sum::<G2Projective>()
628}
629
630/// Represents the unique payment information associated with the payment.
631///
632/// The bytes representing the payment information encode the public key of the
633/// provider with whom you are spending the payment, timestamp and a unique random 32 bytes.
634///
635/// # Fields
636///
637/// * `payinfo_bytes` - An array of bytes representing the payment information.
638///
639#[derive(PartialEq, Eq, Debug, Clone, Copy)]
640pub struct PayInfo {
641    pub pay_info_bytes: [u8; 72],
642}
643
644impl Serialize for PayInfo {
645    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
646    where
647        S: Serializer,
648    {
649        self.pay_info_bytes.to_vec().serialize(serializer)
650    }
651}
652
653impl<'de> Deserialize<'de> for PayInfo {
654    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
655    where
656        D: Deserializer<'de>,
657    {
658        let pay_info_bytes = <Vec<u8>>::deserialize(deserializer)?;
659        Ok(PayInfo {
660            pay_info_bytes: pay_info_bytes
661                .try_into()
662                .map_err(|_| serde::de::Error::custom("invalid pay info bytes"))?,
663        })
664    }
665}
666
667impl Bytable for PayInfo {
668    fn to_byte_vec(&self) -> Vec<u8> {
669        self.pay_info_bytes.to_vec()
670    }
671
672    fn try_from_byte_slice(slice: &[u8]) -> std::result::Result<Self, CompactEcashError> {
673        if slice.len() != 72 {
674            return Err(CompactEcashError::DeserializationLengthMismatch {
675                type_name: "PayInfo".into(),
676                expected: 72,
677                actual: slice.len(),
678            });
679        }
680        //safety : we checked that slices length is exactly 72, hence this unwrap won't fail
681        #[allow(clippy::unwrap_used)]
682        Ok(Self {
683            pay_info_bytes: slice.try_into().unwrap(),
684        })
685    }
686}
687
688impl Base58 for PayInfo {}
689
690#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
691pub struct Payment {
692    pub kappa: G2Projective,
693    pub kappa_e: G2Projective,
694    pub sig: Signature,
695    pub sig_exp: ExpirationDateSignature,
696    pub kappa_k: Vec<G2Projective>,
697    pub omega: Vec<CoinIndexSignature>,
698    pub ss: Vec<G1Projective>,
699    pub tt: Vec<G1Projective>,
700    pub aa: Vec<G1Projective>,
701    pub spend_value: u64,
702    pub cc: G1Projective,
703    pub t_type: EncodedTicketType,
704    pub zk_proof: SpendProof,
705}
706
707impl Payment {
708    /// Checks the validity of the payment signature.
709    ///
710    /// This function performs two checks to ensure the payment signature is valid:
711    /// - Verifies that the element `h` of the payment signature does not equal the identity.
712    /// - Performs a bilinear pairing check involving the elements of the signature and the payment (`h`, `kappa`, and `s`).
713    ///
714    /// # Arguments
715    ///
716    /// * `params` - A reference to the system parameters required for the checks.
717    ///
718    /// # Returns
719    ///
720    /// A `Result` indicating success if the signature is valid or an error if any check fails.
721    ///
722    /// # Errors
723    ///
724    /// An error is returned if:
725    /// - The element `h` of the payment signature equals the identity.
726    /// - The bilinear pairing check for `kappa` fails.
727    ///
728    pub fn check_signature_validity(&self, verification_key: &VerificationKeyAuth) -> Result<()> {
729        let params = ecash_group_parameters();
730        if bool::from(self.sig.h.is_identity()) {
731            return Err(CompactEcashError::SpendSignaturesValidity);
732        }
733
734        if verification_key.beta_g2.len() < 4 {
735            return Err(CompactEcashError::VerificationKeyTooShort);
736        }
737
738        for o in &self.omega {
739            if bool::from(o.is_at_infinity()) {
740                return Err(CompactEcashError::SpendSignaturesValidity);
741            }
742        }
743
744        // SAFETY: we have ensured we have at least 4 elements
745        #[allow(clippy::indexing_slicing)]
746        let kappa_type = self.kappa + verification_key.beta_g2[3] * type_scalar(self.t_type);
747        if !check_bilinear_pairing(
748            &self.sig.h.to_affine(),
749            &G2Prepared::from(kappa_type.to_affine()),
750            &self.sig.s.to_affine(),
751            params.prepared_miller_g2(),
752        ) {
753            return Err(CompactEcashError::SpendSignaturesValidity);
754        }
755        Ok(())
756    }
757
758    /// Checks the validity of the expiration signature encoded in the payment given a spending date.
759    /// If the spending date is within the allowed range before the expiration date, the check is successful.
760    ///
761    /// This function performs two checks to ensure the payment expiration signature is valid:
762    /// - Verifies that the element `h` of the expiration signature does not equal the identity.
763    /// - Performs a bilinear pairing check involving the elements of the expiration signature and the payment (`h`, `kappa_e`, and `s`).
764    ///
765    /// # Arguments
766    ///
767    /// * `verification_key` - The global verification key of the signing authorities.
768    /// * `spend_date` - The date associated with the payment.
769    ///
770    /// # Returns
771    ///
772    /// A `Result` indicating success if the expiration signature is valid or an error if any check fails.
773    ///
774    /// # Errors
775    ///
776    /// An error is returned if:
777    /// - The element `h` of the payment expiration signature equals the identity.
778    /// - The bilinear pairing check for `kappa_e` fails.
779    ///
780    pub fn check_exp_signature_validity(
781        &self,
782        verification_key: &VerificationKeyAuth,
783        spend_date: Scalar,
784    ) -> Result<()> {
785        let grp_params = ecash_group_parameters();
786        // Check if the element h of the payment expiration signature equals the identity.
787        if bool::from(self.sig_exp.h.is_identity()) {
788            return Err(CompactEcashError::ExpirationDateSignatureValidity);
789        }
790
791        if verification_key.beta_g2.len() < 3 {
792            return Err(CompactEcashError::VerificationKeyTooShort);
793        }
794
795        // Calculate m1 and m2 values.
796        let m1: Scalar = spend_date;
797        let m2: Scalar = constants::TYPE_EXP;
798
799        // Perform a bilinear pairing check for kappa_e
800        //SAFETY: we checked the size of beta_G2 earlier
801        #[allow(clippy::indexing_slicing)]
802        let combined_kappa_e =
803            self.kappa_e + verification_key.beta_g2[1] * m1 + verification_key.beta_g2[2] * m2;
804
805        if !check_bilinear_pairing(
806            &self.sig_exp.h.to_affine(),
807            &G2Prepared::from(combined_kappa_e.to_affine()),
808            &self.sig_exp.s.to_affine(),
809            grp_params.prepared_miller_g2(),
810        ) {
811            return Err(CompactEcashError::ExpirationDateSignatureValidity);
812        }
813
814        Ok(())
815    }
816
817    /// Checks that all serial numbers in the payment are unique.
818    ///
819    /// This function verifies that each serial number in the payment's serial number array (`ss`) is unique.
820    ///
821    /// # Returns
822    ///
823    /// A `Result` indicating success if all serial numbers are unique or an error if any serial number is duplicated.
824    ///
825    /// # Errors
826    ///
827    /// An error is returned if not all serial numbers in the payment are unique.
828    ///
829    pub fn no_duplicate_serial_numbers(&self) -> Result<()> {
830        let mut seen_serial_numbers = Vec::new();
831
832        for serial_number in &self.ss {
833            if seen_serial_numbers.contains(serial_number) {
834                return Err(CompactEcashError::SpendDuplicateSerialNumber);
835            }
836            seen_serial_numbers.push(*serial_number);
837        }
838
839        Ok(())
840    }
841
842    // /// Checks the validity of the coin index signature at a specific index.
843    // ///
844    // /// This function performs two checks to ensure the coin index signature at a given index (`k`) is valid:
845    // /// - Verifies that the element `h` of the coin index signature does not equal the identity.
846    // /// - Calculates a combined element for the bilinear pairing check involving `kappa_k`, and verifies the pairing with the coin index signature elements (`h`, `kappa_k`, and `s`).
847    // ///
848    // /// # Arguments
849    // ///
850    // /// * `verification_key` - The global verification key of the signing authorities.
851    // /// * `k` - The index at which to check the coin index signature.
852    // ///
853    // /// # Returns
854    // ///
855    // /// A `Result` indicating success if the coin index signature is valid or an error if any check fails.
856    // ///
857    // /// # Errors
858    // ///
859    // /// An error is returned if:
860    // /// - The element `h` of the coin index signature at the specified index equals the identity.
861    // /// - The bilinear pairing check for `kappa_k` at the specified index fails.
862    // /// - The specified index is out of bounds for the coin index signatures array (`omega`).
863    // ///
864    // pub fn check_coin_index_signature(
865    //     &self,
866    //     verification_key: &VerificationKeyAuth,
867    //     k: u64,
868    // ) -> Result<()> {
869    //     if let Some(coin_idx_sign) = self.omega.get(k as usize) {
870    //         if bool::from(coin_idx_sign.h.is_identity()) {
871    //             return Err(CompactEcashError::SpendSignaturesVerification);
872    //         }
873    //         if verification_key.beta_g2.len() < 3 {
874    //             return Err(CompactEcashError::VerificationKeyTooShort);
875    //         }
876    //         //SAFETY: we checked the size of beta_G2 earlier
877    //         #[allow(clippy::unwrap_used)]
878    //         let combined_kappa_k = self.kappa_k[k as usize].to_affine()
879    //             + verification_key.beta_g2.get(1).unwrap() * constants::TYPE_IDX
880    //             + verification_key.beta_g2.get(2).unwrap() * constants::TYPE_IDX;
881    //
882    //         if !check_bilinear_pairing(
883    //             &coin_idx_sign.h.to_affine(),
884    //             &G2Prepared::from(combined_kappa_k.to_affine()),
885    //             &coin_idx_sign.s.to_affine(),
886    //             ecash_group_parameters().prepared_miller_g2(),
887    //         ) {
888    //             return Err(CompactEcashError::SpendSignaturesVerification);
889    //         }
890    //     } else {
891    //         return Err(CompactEcashError::SpendSignaturesVerification);
892    //     }
893    //     Ok(())
894    // }
895
896    /// Checks the validity of all coin index signatures available.
897    pub fn batch_check_coin_index_signatures(
898        &self,
899        verification_key: &VerificationKeyAuth,
900    ) -> Result<()> {
901        if verification_key.beta_g2.len() < 3 {
902            return Err(CompactEcashError::VerificationKeyTooShort);
903        }
904
905        if self.omega.len() != self.kappa_k.len() {
906            return Err(CompactEcashError::SpendSignaturesVerification);
907        }
908
909        // SAFETY: we checked the size of beta_G2 earlier
910        #[allow(clippy::indexing_slicing)]
911        let partially_signed = verification_key.beta_g2[1] * constants::TYPE_IDX
912            + verification_key.beta_g2[2] * constants::TYPE_IDX;
913
914        let mut pairing_terms = Vec::with_capacity(self.omega.len());
915        for (sig, kappa_k) in self.omega.iter().zip(self.kappa_k.iter()) {
916            pairing_terms.push((sig, partially_signed + kappa_k))
917        }
918
919        if !batch_verify_signatures(pairing_terms.iter()) {
920            return Err(CompactEcashError::SpendSignaturesVerification);
921        }
922        Ok(())
923    }
924
925    /// Checks the validity of the attached zk proof of spending.
926    pub fn verify_spend_proof(
927        &self,
928        verification_key: &VerificationKeyAuth,
929        pay_info: &PayInfo,
930    ) -> Result<()> {
931        // Compute pay_info hash for each coin
932        let mut rr = Vec::with_capacity(self.spend_value as usize);
933        for k in 0..self.spend_value {
934            // Compute hashes R_k = H(payinfo, k)
935            let rr_k = compute_pay_info_hash(pay_info, k);
936            rr.push(rr_k);
937        }
938
939        // verify the zk proof
940        let instance = SpendInstance {
941            kappa: self.kappa,
942            cc: self.cc,
943            aa: self.aa.clone(),
944            ss: self.ss.clone(),
945            tt: self.tt.clone(),
946            kappa_k: self.kappa_k.clone(),
947            kappa_e: self.kappa_e,
948        };
949
950        // verify the zk-proof
951        if !self
952            .zk_proof
953            .verify(&instance, verification_key, &rr, pay_info, self.spend_value)
954        {
955            return Err(CompactEcashError::SpendZKProofVerification);
956        }
957
958        Ok(())
959    }
960
961    /// Verifies the validity of a spend transaction, including signature checks,
962    /// expiration date signature checks, serial number uniqueness, coin index signature checks,
963    /// and zero-knowledge proof verification.
964    ///
965    /// # Arguments
966    ///
967    /// * `params` - The cryptographic parameters.
968    /// * `verification_key` - The verification key used for validation.
969    /// * `pay_info` - The pay information associated with the transaction.
970    /// * `spend_date` - The date at which the spending transaction occurs.
971    ///
972    /// # Returns
973    ///
974    /// Returns `Ok(true)` if the spend transaction is valid; otherwise, returns an error.
975    pub fn spend_verify(
976        &self,
977        verification_key: &VerificationKeyAuth,
978        pay_info: &PayInfo,
979        spend_date: EncodedDate,
980    ) -> Result<()> {
981        // check if all serial numbers are different
982        self.no_duplicate_serial_numbers()?;
983        // verify the zk proof
984        self.verify_spend_proof(verification_key, pay_info)?;
985        // Verify whether the payment signature and kappa are correct
986        self.check_signature_validity(verification_key)?;
987        // Verify whether the expiration date signature and kappa_e are correct
988        self.check_exp_signature_validity(verification_key, date_scalar(spend_date))?;
989        // Verify whether the coin indices signatures and kappa_k are correct
990        self.batch_check_coin_index_signatures(verification_key)?;
991
992        Ok(())
993    }
994
995    pub fn encoded_serial_number(&self) -> Vec<u8> {
996        SerialNumberRef { inner: &self.ss }.to_bytes()
997    }
998
999    pub fn serial_number_bs58(&self) -> String {
1000        SerialNumberRef { inner: &self.ss }.to_bs58()
1001    }
1002
1003    // pub fn has_serial_number(&self, serial_number_bs58: &str) -> Result<bool> {
1004    //     let serial_number = SerialNumberRef::try_from_bs58(serial_number_bs58)?;
1005    //     let ret = self.ss.eq(&serial_number.inner);
1006    //     Ok(ret)
1007    // }
1008}
1009
1010pub struct SerialNumberRef<'a> {
1011    pub(crate) inner: &'a [G1Projective],
1012}
1013
1014impl SerialNumberRef<'_> {
1015    pub fn to_bytes(&self) -> Vec<u8> {
1016        let ss_len = self.inner.len();
1017        let mut bytes: Vec<u8> = Vec::with_capacity(ss_len * 48);
1018        for s in self.inner {
1019            bytes.extend_from_slice(&s.to_affine().to_compressed());
1020        }
1021        bytes
1022    }
1023
1024    pub fn to_bs58(&self) -> String {
1025        bs58::encode(self.to_bytes()).into_string()
1026    }
1027}