1use 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#[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 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 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 #[allow(clippy::indexing_slicing)]
115 let sig = Signature::try_from(&bytes[j..j + SIGNATURE_BYTES])?;
116 j += SIGNATURE_BYTES;
117
118 #[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 #[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 #[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 #[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 signatures: WalletSignatures,
158
159 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 #[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 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 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#[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
300pub 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 pub const SERIALISED_SIZE: usize = 133;
326
327 pub fn signature(&self) -> &Signature {
328 &self.sig
329 }
330
331 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 #[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 #[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 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 let attributes = [&sk_user.sk, &self.v, &self.encoded_expiration_date()];
437
438 let (signature_prime, sign_blinding_factor) = self.signature().blind_and_randomise();
440
441 let kappa = compute_kappa(
443 grp_params,
444 verification_key,
445 &attributes,
446 sign_blinding_factor,
447 );
448
449 let date_signature_index =
452 find_index(spend_date_timestamp, self.expiration_date_timestamp)?;
453
454 #[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 #[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 let o_c = grp_params.random_scalar();
471 #[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 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 #[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 let ss_k = pseudorandom_f_delta_v(grp_params, &self.v, lk)?;
505 ss.push(ss_k);
506 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 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 #[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 r_k_vec.push(coin_sign_blinding_factor);
530 #[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 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 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
600fn 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#[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 #[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 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 #[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 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 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 let m1: Scalar = spend_date;
797 let m2: Scalar = constants::TYPE_EXP;
798
799 #[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 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 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 #[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 pub fn verify_spend_proof(
880 &self,
881 verification_key: &VerificationKeyAuth,
882 pay_info: &PayInfo,
883 ) -> Result<()> {
884 let mut rr = Vec::with_capacity(self.spend_value as usize);
886 for k in 0..self.spend_value {
887 let rr_k = compute_pay_info_hash(pay_info, k);
889 rr.push(rr_k);
890 }
891
892 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 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 pub fn spend_verify(
929 &self,
930 verification_key: &VerificationKeyAuth,
931 pay_info: &PayInfo,
932 spend_date: EncodedDate,
933 ) -> Result<()> {
934 self.no_duplicate_serial_numbers()?;
936 self.verify_spend_proof(verification_key, pay_info)?;
938 self.check_signature_validity(verification_key)?;
940 self.check_exp_signature_validity(verification_key, date_scalar(spend_date))?;
942 self.check_coin_index_signatures(verification_key)?;
944
945 Ok(())
946 }
947
948 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 }
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}