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::{check_bilinear_pairing, hash_to_scalar, try_deserialize_scalar};
14use crate::{constants, ecash_group_parameters};
15use crate::{Base58, EncodedDate, EncodedTicketType};
16use group::Curve;
17use nym_bls12_381_fork::{G1Projective, G2Prepared, G2Projective, Scalar};
18use serde::{Deserialize, Deserializer, Serialize, Serializer};
19use std::borrow::Borrow;
20use zeroize::{Zeroize, ZeroizeOnDrop};
21
22pub mod aggregation;
23pub mod coin_indices_signatures;
24pub mod expiration_date_signatures;
25pub mod identify;
26pub mod keygen;
27pub mod setup;
28pub mod withdrawal;
29
30/// The struct represents a partial wallet with essential components for a payment transaction.
31///
32/// A `PartialWallet` includes a Pointcheval-Sanders signature (`sig`),
33/// a scalar value (`v`) representing the wallet's secret, an optional
34/// `SignerIndex` (`idx`) indicating the signer's index, and an expiration date (`expiration_date`).
35///
36#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop)]
37pub struct PartialWallet {
38    #[zeroize(skip)]
39    sig: Signature,
40    v: Scalar,
41    idx: SignerIndex,
42    expiration_date: Scalar,
43    t_type: Scalar,
44}
45
46impl PartialWallet {
47    pub fn signature(&self) -> &Signature {
48        &self.sig
49    }
50
51    pub fn index(&self) -> SignerIndex {
52        self.idx
53    }
54    pub fn expiration_date(&self) -> Scalar {
55        self.expiration_date
56    }
57    pub fn t_type(&self) -> Scalar {
58        self.t_type
59    }
60
61    /// Converts the `PartialWallet` to a fixed-size byte array.
62    ///
63    /// The resulting byte array has a length of 200 bytes and contains serialized
64    /// representations of the `Signature` (`sig`), scalar value (`v`),
65    /// expiration date (`expiration_date`), and `idx` fields of the `PartialWallet` struct.
66    ///
67    /// # Returns
68    ///
69    /// A fixed-size byte array (`[u8; 200]`) representing the serialized form of the `PartialWallet`.
70    ///
71    pub fn to_bytes(&self) -> [u8; 200] {
72        let mut bytes = [0u8; 200];
73        bytes[0..96].copy_from_slice(&self.sig.to_bytes());
74        bytes[96..128].copy_from_slice(&self.v.to_bytes());
75        bytes[128..160].copy_from_slice(&self.expiration_date.to_bytes());
76        bytes[160..192].copy_from_slice(&self.t_type.to_bytes());
77        bytes[192..200].copy_from_slice(&self.idx.to_le_bytes());
78        bytes
79    }
80
81    /// Convert a byte slice into a `PartialWallet` instance.
82    ///
83    /// This function performs deserialization on the provided byte slice, which
84    /// represent a serialized `PartialWallet`.
85    ///
86    /// # Arguments
87    ///
88    /// * `bytes` - A reference to the byte slice to be deserialized.
89    ///
90    /// # Returns
91    ///
92    /// A `Result` containing the deserialized `PartialWallet` if successful, or a
93    /// `CompactEcashError` indicating the reason for failure.
94    pub fn from_bytes(bytes: &[u8]) -> Result<PartialWallet> {
95        const SIGNATURE_BYTES: usize = 96;
96        const V_BYTES: usize = 32;
97        const EXPIRATION_DATE_BYTES: usize = 32;
98        const T_TYPE_BYTES: usize = 32;
99        const IDX_BYTES: usize = 8;
100        const EXPECTED_LENGTH: usize =
101            SIGNATURE_BYTES + V_BYTES + EXPIRATION_DATE_BYTES + T_TYPE_BYTES + IDX_BYTES;
102
103        if bytes.len() != EXPECTED_LENGTH {
104            return Err(CompactEcashError::DeserializationLengthMismatch {
105                type_name: "PartialWallet".into(),
106                expected: EXPECTED_LENGTH,
107                actual: bytes.len(),
108            });
109        }
110
111        let mut j = 0;
112
113        // we have performed length check
114        #[allow(clippy::indexing_slicing)]
115        let sig = Signature::try_from(&bytes[j..j + SIGNATURE_BYTES])?;
116        j += SIGNATURE_BYTES;
117
118        //SAFETY: slice to array after length check
119        #[allow(clippy::unwrap_used)]
120        #[allow(clippy::indexing_slicing)]
121        let v_bytes = bytes[j..j + V_BYTES].try_into().unwrap();
122        let v = try_deserialize_scalar(v_bytes)?;
123        j += V_BYTES;
124
125        //SAFETY: slice to array after length check
126        #[allow(clippy::unwrap_used)]
127        #[allow(clippy::indexing_slicing)]
128        let expiration_date_bytes = bytes[j..j + EXPIRATION_DATE_BYTES].try_into().unwrap();
129        let expiration_date = try_deserialize_scalar(expiration_date_bytes)?;
130        j += EXPIRATION_DATE_BYTES;
131        //SAFETY: slice to array after length check
132        #[allow(clippy::unwrap_used)]
133        #[allow(clippy::indexing_slicing)]
134        let t_type_bytes = bytes[j..j + T_TYPE_BYTES].try_into().unwrap();
135        let t_type = try_deserialize_scalar(t_type_bytes)?;
136        j += T_TYPE_BYTES;
137
138        //SAFETY: slice to array after length check
139        #[allow(clippy::unwrap_used)]
140        #[allow(clippy::indexing_slicing)]
141        let idx_bytes = bytes[j..].try_into().unwrap();
142        let idx = u64::from_le_bytes(idx_bytes);
143
144        Ok(PartialWallet {
145            sig,
146            v,
147            idx,
148            expiration_date,
149            t_type,
150        })
151    }
152}
153
154#[derive(Debug, Clone, PartialEq, Zeroize, Serialize, Deserialize)]
155pub struct Wallet {
156    /// The cryptographic materials required for producing spending proofs and payments.
157    signatures: WalletSignatures,
158
159    /// Also known as `l` parameter in the paper
160    tickets_spent: u64,
161}
162
163impl Wallet {
164    pub fn new(signatures: WalletSignatures, tickets_spent: u64) -> Self {
165        Wallet {
166            signatures,
167            tickets_spent,
168        }
169    }
170
171    pub fn into_wallet_signatures(self) -> WalletSignatures {
172        self.into()
173    }
174
175    pub fn to_bytes(&self) -> [u8; WalletSignatures::SERIALISED_SIZE + 8] {
176        let mut bytes = [0u8; WalletSignatures::SERIALISED_SIZE + 8];
177        bytes[0..WalletSignatures::SERIALISED_SIZE].copy_from_slice(&self.signatures.to_bytes());
178        bytes[WalletSignatures::SERIALISED_SIZE..]
179            .copy_from_slice(&self.tickets_spent.to_be_bytes());
180        bytes
181    }
182
183    pub fn from_bytes(bytes: &[u8]) -> Result<Wallet> {
184        if bytes.len() != WalletSignatures::SERIALISED_SIZE + 8 {
185            return Err(CompactEcashError::DeserializationLengthMismatch {
186                type_name: "Wallet".into(),
187                expected: WalletSignatures::SERIALISED_SIZE + 8,
188                actual: bytes.len(),
189            });
190        }
191
192        //SAFETY : slice to array conversions after a length check
193        #[allow(clippy::unwrap_used)]
194        #[allow(clippy::indexing_slicing)]
195        let tickets_bytes = bytes[WalletSignatures::SERIALISED_SIZE..]
196            .try_into()
197            .unwrap();
198
199        #[allow(clippy::indexing_slicing)]
200        let signatures = WalletSignatures::from_bytes(&bytes[..WalletSignatures::SERIALISED_SIZE])?;
201        let tickets_spent = u64::from_be_bytes(tickets_bytes);
202
203        Ok(Wallet {
204            signatures,
205            tickets_spent,
206        })
207    }
208
209    pub fn ensure_allowance(
210        params: &Parameters,
211        tickets_spent: u64,
212        spend_value: u64,
213    ) -> Result<()> {
214        if tickets_spent + spend_value > params.get_total_coins() {
215            Err(CompactEcashError::SpendExceedsAllowance {
216                spending: spend_value,
217                remaining: params.get_total_coins() - tickets_spent,
218            })
219        } else {
220            Ok(())
221        }
222    }
223
224    pub fn check_remaining_allowance(&self, params: &Parameters, spend_value: u64) -> Result<()> {
225        Self::ensure_allowance(params, self.tickets_spent, spend_value)
226    }
227
228    #[allow(clippy::too_many_arguments)]
229    pub fn spend(
230        &mut self,
231        params: &Parameters,
232        verification_key: &VerificationKeyAuth,
233        sk_user: &SecretKeyUser,
234        pay_info: &PayInfo,
235        spend_value: u64,
236        valid_dates_signatures: &[ExpirationDateSignature],
237        coin_indices_signatures: &[CoinIndexSignature],
238        spend_date_timestamp: EncodedDate,
239    ) -> Result<Payment> {
240        self.check_remaining_allowance(params, spend_value)?;
241
242        // produce payment
243        let payment = self.signatures.spend(
244            params,
245            verification_key,
246            sk_user,
247            pay_info,
248            self.tickets_spent,
249            spend_value,
250            valid_dates_signatures,
251            coin_indices_signatures,
252            spend_date_timestamp,
253        )?;
254
255        // update the ticket counter
256        self.tickets_spent += spend_value;
257        Ok(payment)
258    }
259}
260
261impl From<Wallet> for WalletSignatures {
262    fn from(value: Wallet) -> Self {
263        value.signatures
264    }
265}
266
267/// The struct represents a wallet with essential components for a payment transaction.
268///
269/// A `Wallet` includes a Pointcheval-Sanders signature (`sig`),
270/// a scalar value (`v`) representing the wallet's secret, an optional
271/// an expiration date (`expiration_date`)
272/// and an u64 ('l') indicating the total number of spent coins.
273///
274#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
275pub struct WalletSignatures {
276    #[zeroize(skip)]
277    sig: Signature,
278    v: Scalar,
279    expiration_date_timestamp: EncodedDate,
280    t_type: EncodedTicketType,
281}
282
283impl WalletSignatures {
284    pub fn with_tickets_spent(self, tickets_spent: u64) -> Wallet {
285        Wallet {
286            signatures: self,
287            tickets_spent,
288        }
289    }
290
291    pub fn new_wallet(self) -> Wallet {
292        self.with_tickets_spent(0)
293    }
294
295    pub fn encoded_expiration_date(&self) -> Scalar {
296        date_scalar(self.expiration_date_timestamp)
297    }
298}
299
300/// Computes the hash of payment information concatenated with a numeric value.
301///
302/// This function takes a `PayInfo` structure and a numeric value `k`, and
303/// concatenates the serialized `payinfo` field of `PayInfo` with the little-endian
304/// byte representation of `k`. The resulting byte sequence is then hashed to produce
305/// a scalar value using the `hash_to_scalar` function.
306///
307/// # Arguments
308///
309/// * `pay_info` - A reference to the `PayInfo` structure containing payment information.
310/// * `k` - A numeric value used in the hash computation.
311///
312/// # Returns
313///
314/// A `Scalar` value representing the hash of the concatenated byte sequence.
315///
316pub fn compute_pay_info_hash(pay_info: &PayInfo, k: u64) -> Scalar {
317    let mut bytes = Vec::new();
318    bytes.extend_from_slice(&pay_info.pay_info_bytes);
319    bytes.extend_from_slice(&k.to_le_bytes());
320    hash_to_scalar(bytes)
321}
322
323impl WalletSignatures {
324    // signature size (96) + secret size (32) + expiration size (4) + t_type (1)
325    pub const SERIALISED_SIZE: usize = 133;
326
327    pub fn signature(&self) -> &Signature {
328        &self.sig
329    }
330
331    /// Converts the `WalletSignatures` to a fixed-size byte array.
332    ///
333    /// The resulting byte array has a length of 168 bytes and contains serialized
334    /// representations of the `Signature` (`sig`), scalar value (`v`), and
335    /// expiration date (`expiration_date`) fields of the `WalletSignatures` struct.
336    ///
337    /// # Returns
338    ///
339    /// A fixed-size byte array (`[u8; 136]`) representing the serialized form of the `Wallet`.
340    ///
341    pub fn to_bytes(&self) -> [u8; Self::SERIALISED_SIZE] {
342        let mut bytes = [0u8; Self::SERIALISED_SIZE];
343        bytes[0..96].copy_from_slice(&self.sig.to_bytes());
344        bytes[96..128].copy_from_slice(&self.v.to_bytes());
345        bytes[128..132].copy_from_slice(&self.expiration_date_timestamp.to_be_bytes());
346        bytes[132] = self.t_type;
347        bytes
348    }
349
350    pub fn from_bytes(bytes: &[u8]) -> Result<WalletSignatures> {
351        if bytes.len() != Self::SERIALISED_SIZE {
352            return Err(CompactEcashError::DeserializationLengthMismatch {
353                type_name: "WalletSignatures".into(),
354                expected: Self::SERIALISED_SIZE,
355                actual: bytes.len(),
356            });
357        }
358        //SAFETY : slice to array conversions after a length check
359        #[allow(clippy::unwrap_used)]
360        #[allow(clippy::indexing_slicing)]
361        let sig_bytes: &[u8; 96] = &bytes[..96].try_into().unwrap();
362
363        #[allow(clippy::unwrap_used)]
364        #[allow(clippy::indexing_slicing)]
365        let v_bytes: &[u8; 32] = &bytes[96..128].try_into().unwrap();
366
367        #[allow(clippy::unwrap_used)]
368        #[allow(clippy::indexing_slicing)]
369        let expiration_date_bytes = bytes[128..132].try_into().unwrap();
370
371        let sig = Signature::try_from(sig_bytes.as_slice())?;
372        let v = Scalar::from_bytes(v_bytes).unwrap();
373        let expiration_date_timestamp = EncodedDate::from_be_bytes(expiration_date_bytes);
374        #[allow(clippy::indexing_slicing)]
375        let t_type = bytes[132];
376
377        Ok(WalletSignatures {
378            sig,
379            v,
380            expiration_date_timestamp,
381            t_type,
382        })
383    }
384
385    /// Performs a spending operation with the given parameters, updating the wallet and generating a payment.
386    ///
387    /// # Arguments
388    ///
389    /// * `verification_key` - The global verification key.
390    /// * `sk_user` - The secret key of the user who wants to spend from their wallet.
391    /// * `pay_info` - Unique information related to the payment.
392    /// * `current_tickets_spent` - The total number of tickets already spent in the associated wallet.
393    /// * `spend_value` - The amount to spend from the wallet.
394    /// * `valid_dates_signatures` - A list of **SORTED** signatures on valid dates during which we can spend from the wallet.
395    /// * `coin_indices_signatures` - A list of **SORTED** signatures on coin indices.
396    /// * `spend_date` - The date on which the spending occurs, expressed as unix timestamp.
397    ///
398    /// # Returns
399    ///
400    /// A tuple containing the generated payment and a reference to the updated wallet, or an error.
401    #[allow(clippy::too_many_arguments)]
402    pub fn spend<BI, BE>(
403        &self,
404        params: &Parameters,
405        verification_key: &VerificationKeyAuth,
406        sk_user: &SecretKeyUser,
407        pay_info: &PayInfo,
408        current_tickets_spent: u64,
409        spend_value: u64,
410        valid_dates_signatures: &[BE],
411        coin_indices_signatures: &[BI],
412        spend_date_timestamp: EncodedDate,
413    ) -> Result<Payment>
414    where
415        BI: Borrow<CoinIndexSignature>,
416        BE: Borrow<ExpirationDateSignature>,
417    {
418        // Extract group parameters
419        let grp_params = params.grp();
420
421        if verification_key.beta_g2.is_empty() {
422            return Err(CompactEcashError::VerificationKeyTooShort);
423        }
424
425        if valid_dates_signatures.len() != constants::CRED_VALIDITY_PERIOD_DAYS as usize {
426            return Err(CompactEcashError::InsufficientNumberOfExpirationSignatures);
427        }
428
429        if coin_indices_signatures.len() != params.get_total_coins() as usize {
430            return Err(CompactEcashError::InsufficientNumberOfIndexSignatures);
431        }
432
433        Wallet::ensure_allowance(params, current_tickets_spent, spend_value)?;
434
435        // Wallet attributes needed for spending
436        let attributes = [&sk_user.sk, &self.v, &self.encoded_expiration_date()];
437
438        // Randomize wallet signature
439        let (signature_prime, sign_blinding_factor) = self.signature().blind_and_randomise();
440
441        // compute kappa (i.e., blinded attributes for show) to prove possession of the wallet signature
442        let kappa = compute_kappa(
443            grp_params,
444            verification_key,
445            &attributes,
446            sign_blinding_factor,
447        );
448
449        // Randomise the expiration date signature for the date when we want to perform the spending, and compute kappa_e to prove possession of
450        // the expiration signature
451        let date_signature_index =
452            find_index(spend_date_timestamp, self.expiration_date_timestamp)?;
453
454        //SAFETY : find_index eiter returns a valid index or an error. The unwrap is therefore fine
455        #[allow(clippy::unwrap_used)]
456        let date_signature = valid_dates_signatures
457            .get(date_signature_index)
458            .unwrap()
459            .borrow();
460        let (date_signature_prime, date_sign_blinding_factor) =
461            date_signature.blind_and_randomise();
462        // compute kappa_e to prove possession of the expiration signature
463        //SAFETY: we checked that verification beta_g2 isn't empty
464        #[allow(clippy::unwrap_used)]
465        let kappa_e: G2Projective = grp_params.gen2() * date_sign_blinding_factor
466            + verification_key.alpha
467            + verification_key.beta_g2.first().unwrap() * self.encoded_expiration_date();
468
469        // pick random openings o_c and compute commitments C to v (wallet secret)
470        let o_c = grp_params.random_scalar();
471        //SAFETY: grp_params is static with length 3
472        #[allow(clippy::unwrap_used)]
473        let cc = grp_params.gen1() * o_c + grp_params.gamma_idx(1).unwrap() * self.v;
474
475        let mut aa: Vec<G1Projective> = Default::default();
476        let mut ss: Vec<G1Projective> = Default::default();
477        let mut tt: Vec<G1Projective> = Default::default();
478        let mut rr: Vec<Scalar> = Default::default();
479        let mut o_a: Vec<Scalar> = Default::default();
480        let mut o_mu: Vec<Scalar> = Default::default();
481        let mut mu: Vec<Scalar> = Default::default();
482        let mut r_k_vec: Vec<Scalar> = Default::default();
483        let mut kappa_k_vec: Vec<G2Projective> = Default::default();
484        let mut lk_vec: Vec<Scalar> = Default::default();
485
486        let mut coin_indices_signatures_prime: Vec<CoinIndexSignature> = Default::default();
487        for k in 0..spend_value {
488            let lk = current_tickets_spent + k;
489            lk_vec.push(Scalar::from(lk));
490
491            // compute hashes R_k = H(payinfo, k)
492            let rr_k = compute_pay_info_hash(pay_info, k);
493            rr.push(rr_k);
494
495            let o_a_k = grp_params.random_scalar();
496            o_a.push(o_a_k);
497            //SAFETY: grp_params is static with length 3
498            #[allow(clippy::unwrap_used)]
499            let aa_k =
500                grp_params.gen1() * o_a_k + grp_params.gamma_idx(1).unwrap() * Scalar::from(lk);
501            aa.push(aa_k);
502
503            // compute the serial numbers
504            let ss_k = pseudorandom_f_delta_v(grp_params, &self.v, lk)?;
505            ss.push(ss_k);
506            // compute the identification tags
507            let tt_k = grp_params.gen1() * sk_user.sk
508                + pseudorandom_f_g_v(grp_params, &self.v, lk)? * rr_k;
509            tt.push(tt_k);
510
511            // compute values mu, o_mu, lambda, o_lambda
512            let maybe_mu_k: Option<Scalar> = (self.v + Scalar::from(lk) + Scalar::from(1))
513                .invert()
514                .into();
515            let mu_k = maybe_mu_k.ok_or(CompactEcashError::UnluckiestError)?;
516            mu.push(mu_k);
517
518            let o_mu_k = ((o_a_k + o_c) * mu_k).neg();
519            o_mu.push(o_mu_k);
520
521            // Randomize the coin index signatures and compute kappa_k to prove possession of each coin's signature
522            // This involves iterating over the signatures corresponding to the coins we want to spend in this payment.
523            //SAFETY : Earlier `ensure_allowance` ensures we don't do out of of bound here
524            #[allow(clippy::unwrap_used)]
525            let coin_sign = coin_indices_signatures.get(lk as usize).unwrap().borrow();
526            let (coin_sign_prime, coin_sign_blinding_factor) = coin_sign.blind_and_randomise();
527            coin_indices_signatures_prime.push(coin_sign_prime);
528            // proven in tt_kappa_k so the verifier can bind this signature's index to the serial
529            r_k_vec.push(coin_sign_blinding_factor);
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    /// Verifies each coin index signature against its own kappa_k with a separate pairing, so no cross-term cancellation is possible.
843    pub fn check_coin_index_signatures(
844        &self,
845        verification_key: &VerificationKeyAuth,
846    ) -> Result<()> {
847        let params = ecash_group_parameters();
848        if verification_key.beta_g2.len() < 3 {
849            return Err(CompactEcashError::VerificationKeyTooShort);
850        }
851
852        let n = self.spend_value as usize;
853        if self.omega.len() != n || self.kappa_k.len() != n {
854            return Err(CompactEcashError::SpendSignaturesVerification);
855        }
856
857        // SAFETY: we checked the size of beta_G2 earlier
858        #[allow(clippy::indexing_slicing)]
859        let partially_signed = verification_key.beta_g2[1] * constants::TYPE_IDX
860            + verification_key.beta_g2[2] * constants::TYPE_IDX;
861
862        for (sig, kappa_k) in self.omega.iter().zip(self.kappa_k.iter()) {
863            if bool::from(sig.is_at_infinity()) {
864                return Err(CompactEcashError::SpendSignaturesValidity);
865            }
866            if !check_bilinear_pairing(
867                &sig.h.to_affine(),
868                &G2Prepared::from((partially_signed + kappa_k).to_affine()),
869                &sig.s.to_affine(),
870                params.prepared_miller_g2(),
871            ) {
872                return Err(CompactEcashError::SpendSignaturesVerification);
873            }
874        }
875        Ok(())
876    }
877
878    /// Checks the validity of the attached zk proof of spending.
879    pub fn verify_spend_proof(
880        &self,
881        verification_key: &VerificationKeyAuth,
882        pay_info: &PayInfo,
883    ) -> Result<()> {
884        // Compute pay_info hash for each coin
885        let mut rr = Vec::with_capacity(self.spend_value as usize);
886        for k in 0..self.spend_value {
887            // Compute hashes R_k = H(payinfo, k)
888            let rr_k = compute_pay_info_hash(pay_info, k);
889            rr.push(rr_k);
890        }
891
892        // verify the zk proof
893        let instance = SpendInstance {
894            kappa: self.kappa,
895            cc: self.cc,
896            aa: self.aa.clone(),
897            ss: self.ss.clone(),
898            tt: self.tt.clone(),
899            kappa_k: self.kappa_k.clone(),
900            kappa_e: self.kappa_e,
901        };
902
903        // verify the zk-proof
904        if !self
905            .zk_proof
906            .verify(&instance, verification_key, &rr, pay_info, self.spend_value)
907        {
908            return Err(CompactEcashError::SpendZKProofVerification);
909        }
910
911        Ok(())
912    }
913
914    /// Verifies the validity of a spend transaction, including signature checks,
915    /// expiration date signature checks, serial number uniqueness, coin index signature checks,
916    /// and zero-knowledge proof verification.
917    ///
918    /// # Arguments
919    ///
920    /// * `params` - The cryptographic parameters.
921    /// * `verification_key` - The verification key used for validation.
922    /// * `pay_info` - The pay information associated with the transaction.
923    /// * `spend_date` - The date at which the spending transaction occurs.
924    ///
925    /// # Returns
926    ///
927    /// Returns `Ok(true)` if the spend transaction is valid; otherwise, returns an error.
928    pub fn spend_verify(
929        &self,
930        verification_key: &VerificationKeyAuth,
931        pay_info: &PayInfo,
932        spend_date: EncodedDate,
933    ) -> Result<()> {
934        // check if all serial numbers are different
935        self.no_duplicate_serial_numbers()?;
936        // verify the zk proof
937        self.verify_spend_proof(verification_key, pay_info)?;
938        // Verify whether the payment signature and kappa are correct
939        self.check_signature_validity(verification_key)?;
940        // Verify whether the expiration date signature and kappa_e are correct
941        self.check_exp_signature_validity(verification_key, date_scalar(spend_date))?;
942        // Verify whether the coin indices signatures and kappa_k are correct
943        self.check_coin_index_signatures(verification_key)?;
944
945        Ok(())
946    }
947
948    /// Whether the attached proof binds each signed coin index to its serial; older provers never sent the relation.
949    pub fn proves_coin_index_binding(&self) -> bool {
950        self.zk_proof.proves_coin_index_binding()
951    }
952
953    pub fn encoded_serial_number(&self) -> Vec<u8> {
954        SerialNumberRef { inner: &self.ss }.to_bytes()
955    }
956
957    pub fn serial_number_bs58(&self) -> String {
958        SerialNumberRef { inner: &self.ss }.to_bs58()
959    }
960
961    // pub fn has_serial_number(&self, serial_number_bs58: &str) -> Result<bool> {
962    //     let serial_number = SerialNumberRef::try_from_bs58(serial_number_bs58)?;
963    //     let ret = self.ss.eq(&serial_number.inner);
964    //     Ok(ret)
965    // }
966}
967
968pub struct SerialNumberRef<'a> {
969    pub(crate) inner: &'a [G1Projective],
970}
971
972impl SerialNumberRef<'_> {
973    pub fn to_bytes(&self) -> Vec<u8> {
974        let ss_len = self.inner.len();
975        let mut bytes: Vec<u8> = Vec::with_capacity(ss_len * 48);
976        for s in self.inner {
977            bytes.extend_from_slice(&s.to_affine().to_compressed());
978        }
979        bytes
980    }
981
982    pub fn to_bs58(&self) -> String {
983        bs58::encode(self.to_bytes()).into_string()
984    }
985}