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