use crate::common_types::{Signature, SignerIndex};
use crate::error::{CompactEcashError, Result};
use crate::helpers::{date_scalar, type_scalar};
use crate::proofs::proof_spend::{SpendInstance, SpendProof, SpendWitness};
use crate::scheme::coin_indices_signatures::CoinIndexSignature;
use crate::scheme::expiration_date_signatures::{find_index, ExpirationDateSignature};
use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth};
use crate::scheme::setup::{GroupParameters, Parameters};
use crate::traits::Bytable;
use crate::utils::{
batch_verify_signatures, check_bilinear_pairing, hash_to_scalar, try_deserialize_scalar,
};
use crate::{constants, ecash_group_parameters};
use crate::{Base58, EncodedDate, EncodedTicketType};
use group::Curve;
use nym_bls12_381_fork::{G1Projective, G2Prepared, G2Projective, Scalar};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::borrow::Borrow;
use zeroize::{Zeroize, ZeroizeOnDrop};
pub mod aggregation;
pub mod coin_indices_signatures;
pub mod expiration_date_signatures;
pub mod identify;
pub mod keygen;
pub mod setup;
pub mod withdrawal;
#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop)]
pub struct PartialWallet {
#[zeroize(skip)]
sig: Signature,
v: Scalar,
idx: SignerIndex,
expiration_date: Scalar,
t_type: Scalar,
}
impl PartialWallet {
pub fn signature(&self) -> &Signature {
&self.sig
}
pub fn index(&self) -> SignerIndex {
self.idx
}
pub fn expiration_date(&self) -> Scalar {
self.expiration_date
}
pub fn t_type(&self) -> Scalar {
self.t_type
}
pub fn to_bytes(&self) -> [u8; 200] {
let mut bytes = [0u8; 200];
bytes[0..96].copy_from_slice(&self.sig.to_bytes());
bytes[96..128].copy_from_slice(&self.v.to_bytes());
bytes[128..160].copy_from_slice(&self.expiration_date.to_bytes());
bytes[160..192].copy_from_slice(&self.t_type.to_bytes());
bytes[192..200].copy_from_slice(&self.idx.to_le_bytes());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<PartialWallet> {
const SIGNATURE_BYTES: usize = 96;
const V_BYTES: usize = 32;
const EXPIRATION_DATE_BYTES: usize = 32;
const T_TYPE_BYTES: usize = 32;
const IDX_BYTES: usize = 8;
const EXPECTED_LENGTH: usize =
SIGNATURE_BYTES + V_BYTES + EXPIRATION_DATE_BYTES + T_TYPE_BYTES + IDX_BYTES;
if bytes.len() != EXPECTED_LENGTH {
return Err(CompactEcashError::DeserializationLengthMismatch {
type_name: "PartialWallet".into(),
expected: EXPECTED_LENGTH,
actual: bytes.len(),
});
}
let mut j = 0;
let sig = Signature::try_from(&bytes[j..j + SIGNATURE_BYTES])?;
j += SIGNATURE_BYTES;
#[allow(clippy::unwrap_used)]
let v_bytes = bytes[j..j + V_BYTES].try_into().unwrap();
let v = try_deserialize_scalar(v_bytes)?;
j += V_BYTES;
#[allow(clippy::unwrap_used)]
let expiration_date_bytes = bytes[j..j + EXPIRATION_DATE_BYTES].try_into().unwrap();
let expiration_date = try_deserialize_scalar(expiration_date_bytes)?;
j += EXPIRATION_DATE_BYTES;
#[allow(clippy::unwrap_used)]
let t_type_bytes = bytes[j..j + T_TYPE_BYTES].try_into().unwrap();
let t_type = try_deserialize_scalar(t_type_bytes)?;
j += T_TYPE_BYTES;
#[allow(clippy::unwrap_used)]
let idx_bytes = bytes[j..].try_into().unwrap();
let idx = u64::from_le_bytes(idx_bytes);
Ok(PartialWallet {
sig,
v,
idx,
expiration_date,
t_type,
})
}
}
#[derive(Debug, Clone, PartialEq, Zeroize, Serialize, Deserialize)]
pub struct Wallet {
signatures: WalletSignatures,
tickets_spent: u64,
}
impl Wallet {
pub fn new(signatures: WalletSignatures, tickets_spent: u64) -> Self {
Wallet {
signatures,
tickets_spent,
}
}
pub fn into_wallet_signatures(self) -> WalletSignatures {
self.into()
}
pub fn to_bytes(&self) -> [u8; WalletSignatures::SERIALISED_SIZE + 8] {
let mut bytes = [0u8; WalletSignatures::SERIALISED_SIZE + 8];
bytes[0..WalletSignatures::SERIALISED_SIZE].copy_from_slice(&self.signatures.to_bytes());
bytes[WalletSignatures::SERIALISED_SIZE..]
.copy_from_slice(&self.tickets_spent.to_be_bytes());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Wallet> {
if bytes.len() != WalletSignatures::SERIALISED_SIZE + 8 {
return Err(CompactEcashError::DeserializationLengthMismatch {
type_name: "Wallet".into(),
expected: WalletSignatures::SERIALISED_SIZE + 8,
actual: bytes.len(),
});
}
#[allow(clippy::unwrap_used)]
let tickets_bytes = bytes[WalletSignatures::SERIALISED_SIZE..]
.try_into()
.unwrap();
let signatures = WalletSignatures::from_bytes(&bytes[..WalletSignatures::SERIALISED_SIZE])?;
let tickets_spent = u64::from_be_bytes(tickets_bytes);
Ok(Wallet {
signatures,
tickets_spent,
})
}
pub fn ensure_allowance(
params: &Parameters,
tickets_spent: u64,
spend_value: u64,
) -> Result<()> {
if tickets_spent + spend_value > params.get_total_coins() {
Err(CompactEcashError::SpendExceedsAllowance {
spending: spend_value,
remaining: params.get_total_coins() - tickets_spent,
})
} else {
Ok(())
}
}
pub fn check_remaining_allowance(&self, params: &Parameters, spend_value: u64) -> Result<()> {
Self::ensure_allowance(params, self.tickets_spent, spend_value)
}
#[allow(clippy::too_many_arguments)]
pub fn spend(
&mut self,
params: &Parameters,
verification_key: &VerificationKeyAuth,
sk_user: &SecretKeyUser,
pay_info: &PayInfo,
spend_value: u64,
valid_dates_signatures: &[ExpirationDateSignature],
coin_indices_signatures: &[CoinIndexSignature],
spend_date_timestamp: EncodedDate,
) -> Result<Payment> {
self.check_remaining_allowance(params, spend_value)?;
let payment = self.signatures.spend(
params,
verification_key,
sk_user,
pay_info,
self.tickets_spent,
spend_value,
valid_dates_signatures,
coin_indices_signatures,
spend_date_timestamp,
)?;
self.tickets_spent += spend_value;
Ok(payment)
}
}
impl From<Wallet> for WalletSignatures {
fn from(value: Wallet) -> Self {
value.signatures
}
}
#[derive(Debug, Clone, PartialEq, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
pub struct WalletSignatures {
#[zeroize(skip)]
sig: Signature,
v: Scalar,
expiration_date_timestamp: EncodedDate,
t_type: EncodedTicketType,
}
impl WalletSignatures {
pub fn with_tickets_spent(self, tickets_spent: u64) -> Wallet {
Wallet {
signatures: self,
tickets_spent,
}
}
pub fn new_wallet(self) -> Wallet {
self.with_tickets_spent(0)
}
pub fn encoded_expiration_date(&self) -> Scalar {
date_scalar(self.expiration_date_timestamp)
}
}
pub fn compute_pay_info_hash(pay_info: &PayInfo, k: u64) -> Scalar {
let mut bytes = Vec::new();
bytes.extend_from_slice(&pay_info.pay_info_bytes);
bytes.extend_from_slice(&k.to_le_bytes());
hash_to_scalar(bytes)
}
impl WalletSignatures {
pub const SERIALISED_SIZE: usize = 133;
pub fn signature(&self) -> &Signature {
&self.sig
}
pub fn to_bytes(&self) -> [u8; Self::SERIALISED_SIZE] {
let mut bytes = [0u8; Self::SERIALISED_SIZE];
bytes[0..96].copy_from_slice(&self.sig.to_bytes());
bytes[96..128].copy_from_slice(&self.v.to_bytes());
bytes[128..132].copy_from_slice(&self.expiration_date_timestamp.to_be_bytes());
bytes[132] = self.t_type;
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<WalletSignatures> {
if bytes.len() != Self::SERIALISED_SIZE {
return Err(CompactEcashError::DeserializationLengthMismatch {
type_name: "WalletSignatures".into(),
expected: Self::SERIALISED_SIZE,
actual: bytes.len(),
});
}
#[allow(clippy::unwrap_used)]
let sig_bytes: &[u8; 96] = &bytes[..96].try_into().unwrap();
#[allow(clippy::unwrap_used)]
let v_bytes: &[u8; 32] = &bytes[96..128].try_into().unwrap();
#[allow(clippy::unwrap_used)]
let expiration_date_bytes = bytes[128..132].try_into().unwrap();
let sig = Signature::try_from(sig_bytes.as_slice())?;
let v = Scalar::from_bytes(v_bytes).unwrap();
let expiration_date_timestamp = EncodedDate::from_be_bytes(expiration_date_bytes);
let t_type = bytes[132];
Ok(WalletSignatures {
sig,
v,
expiration_date_timestamp,
t_type,
})
}
#[allow(clippy::too_many_arguments)]
pub fn spend<BI, BE>(
&self,
params: &Parameters,
verification_key: &VerificationKeyAuth,
sk_user: &SecretKeyUser,
pay_info: &PayInfo,
current_tickets_spent: u64,
spend_value: u64,
valid_dates_signatures: &[BE],
coin_indices_signatures: &[BI],
spend_date_timestamp: EncodedDate,
) -> Result<Payment>
where
BI: Borrow<CoinIndexSignature>,
BE: Borrow<ExpirationDateSignature>,
{
let grp_params = params.grp();
if verification_key.beta_g2.is_empty() {
return Err(CompactEcashError::VerificationKeyTooShort);
}
if valid_dates_signatures.len() != constants::CRED_VALIDITY_PERIOD_DAYS as usize {
return Err(CompactEcashError::InsufficientNumberOfExpirationSignatures);
}
if coin_indices_signatures.len() != params.get_total_coins() as usize {
return Err(CompactEcashError::InsufficientNumberOfIndexSignatures);
}
Wallet::ensure_allowance(params, current_tickets_spent, spend_value)?;
let attributes = [&sk_user.sk, &self.v, &self.encoded_expiration_date()];
let (signature_prime, sign_blinding_factor) = self.signature().blind_and_randomise();
let kappa = compute_kappa(
grp_params,
verification_key,
&attributes,
sign_blinding_factor,
);
let date_signature_index =
find_index(spend_date_timestamp, self.expiration_date_timestamp)?;
#[allow(clippy::unwrap_used)]
let date_signature = valid_dates_signatures
.get(date_signature_index)
.unwrap()
.borrow();
let (date_signature_prime, date_sign_blinding_factor) =
date_signature.blind_and_randomise();
#[allow(clippy::unwrap_used)]
let kappa_e: G2Projective = grp_params.gen2() * date_sign_blinding_factor
+ verification_key.alpha
+ verification_key.beta_g2.first().unwrap() * self.encoded_expiration_date();
let o_c = grp_params.random_scalar();
#[allow(clippy::unwrap_used)]
let cc = grp_params.gen1() * o_c + grp_params.gamma_idx(1).unwrap() * self.v;
let mut aa: Vec<G1Projective> = Default::default();
let mut ss: Vec<G1Projective> = Default::default();
let mut tt: Vec<G1Projective> = Default::default();
let mut rr: Vec<Scalar> = Default::default();
let mut o_a: Vec<Scalar> = Default::default();
let mut o_mu: Vec<Scalar> = Default::default();
let mut mu: Vec<Scalar> = Default::default();
let r_k_vec: Vec<Scalar> = Default::default();
let mut kappa_k_vec: Vec<G2Projective> = Default::default();
let mut lk_vec: Vec<Scalar> = Default::default();
let mut coin_indices_signatures_prime: Vec<CoinIndexSignature> = Default::default();
for k in 0..spend_value {
let lk = current_tickets_spent + k;
lk_vec.push(Scalar::from(lk));
let rr_k = compute_pay_info_hash(pay_info, k);
rr.push(rr_k);
let o_a_k = grp_params.random_scalar();
o_a.push(o_a_k);
#[allow(clippy::unwrap_used)]
let aa_k =
grp_params.gen1() * o_a_k + grp_params.gamma_idx(1).unwrap() * Scalar::from(lk);
aa.push(aa_k);
let ss_k = pseudorandom_f_delta_v(grp_params, &self.v, lk)?;
ss.push(ss_k);
let tt_k = grp_params.gen1() * sk_user.sk
+ pseudorandom_f_g_v(grp_params, &self.v, lk)? * rr_k;
tt.push(tt_k);
let maybe_mu_k: Option<Scalar> = (self.v + Scalar::from(lk) + Scalar::from(1))
.invert()
.into();
let mu_k = maybe_mu_k.ok_or(CompactEcashError::UnluckiestError)?;
mu.push(mu_k);
let o_mu_k = ((o_a_k + o_c) * mu_k).neg();
o_mu.push(o_mu_k);
#[allow(clippy::unwrap_used)]
let coin_sign = coin_indices_signatures.get(lk as usize).unwrap().borrow();
let (coin_sign_prime, coin_sign_blinding_factor) = coin_sign.blind_and_randomise();
coin_indices_signatures_prime.push(coin_sign_prime);
#[allow(clippy::unwrap_used)]
let kappa_k: G2Projective = grp_params.gen2() * coin_sign_blinding_factor
+ verification_key.alpha
+ verification_key.beta_g2.first().unwrap() * Scalar::from(lk);
kappa_k_vec.push(kappa_k);
}
let spend_instance = SpendInstance {
kappa,
cc,
aa: aa.clone(),
ss: ss.clone(),
tt: tt.clone(),
kappa_k: kappa_k_vec.clone(),
kappa_e,
};
let spend_witness = SpendWitness {
attributes: &attributes,
r: sign_blinding_factor,
o_c,
lk: lk_vec,
o_a,
mu,
o_mu,
r_k: r_k_vec,
r_e: date_sign_blinding_factor,
};
let zk_proof = SpendProof::construct(
&spend_instance,
&spend_witness,
verification_key,
&rr,
pay_info,
spend_value,
);
let pay = Payment {
kappa,
kappa_e,
sig: signature_prime,
sig_exp: date_signature_prime,
kappa_k: kappa_k_vec.clone(),
omega: coin_indices_signatures_prime,
ss: ss.clone(),
tt: tt.clone(),
aa: aa.clone(),
spend_value,
cc,
t_type: self.t_type,
zk_proof,
};
Ok(pay)
}
}
fn pseudorandom_f_delta_v(params: &GroupParameters, v: &Scalar, l: u64) -> Result<G1Projective> {
let maybe_pow: Option<Scalar> = (v + Scalar::from(l) + Scalar::from(1)).invert().into();
Ok(params.delta() * maybe_pow.ok_or(CompactEcashError::UnluckiestError)?)
}
fn pseudorandom_f_g_v(params: &GroupParameters, v: &Scalar, l: u64) -> Result<G1Projective> {
let maybe_pow: Option<Scalar> = (v + Scalar::from(l) + Scalar::from(1)).invert().into();
Ok(params.gen1() * maybe_pow.ok_or(CompactEcashError::UnluckiestError)?)
}
fn compute_kappa(
params: &GroupParameters,
verification_key: &VerificationKeyAuth,
attributes: &[&Scalar],
blinding_factor: Scalar,
) -> G2Projective {
params.gen2() * blinding_factor
+ verification_key.alpha
+ attributes
.iter()
.zip(verification_key.beta_g2.iter())
.map(|(&priv_attr, beta_i)| beta_i * priv_attr)
.sum::<G2Projective>()
}
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
pub struct PayInfo {
pub pay_info_bytes: [u8; 72],
}
impl Serialize for PayInfo {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
self.pay_info_bytes.to_vec().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for PayInfo {
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let pay_info_bytes = <Vec<u8>>::deserialize(deserializer)?;
Ok(PayInfo {
pay_info_bytes: pay_info_bytes
.try_into()
.map_err(|_| serde::de::Error::custom("invalid pay info bytes"))?,
})
}
}
impl Bytable for PayInfo {
fn to_byte_vec(&self) -> Vec<u8> {
self.pay_info_bytes.to_vec()
}
fn try_from_byte_slice(slice: &[u8]) -> std::result::Result<Self, CompactEcashError> {
if slice.len() != 72 {
return Err(CompactEcashError::DeserializationLengthMismatch {
type_name: "PayInfo".into(),
expected: 72,
actual: slice.len(),
});
}
#[allow(clippy::unwrap_used)]
Ok(Self {
pay_info_bytes: slice.try_into().unwrap(),
})
}
}
impl Base58 for PayInfo {}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Payment {
pub kappa: G2Projective,
pub kappa_e: G2Projective,
pub sig: Signature,
pub sig_exp: ExpirationDateSignature,
pub kappa_k: Vec<G2Projective>,
pub omega: Vec<CoinIndexSignature>,
pub ss: Vec<G1Projective>,
pub tt: Vec<G1Projective>,
pub aa: Vec<G1Projective>,
pub spend_value: u64,
pub cc: G1Projective,
pub t_type: EncodedTicketType,
pub zk_proof: SpendProof,
}
impl Payment {
pub fn check_signature_validity(&self, verification_key: &VerificationKeyAuth) -> Result<()> {
let params = ecash_group_parameters();
if bool::from(self.sig.h.is_identity()) {
return Err(CompactEcashError::SpendSignaturesValidity);
}
let kappa_type = self.kappa + verification_key.beta_g2[3] * type_scalar(self.t_type);
if !check_bilinear_pairing(
&self.sig.h.to_affine(),
&G2Prepared::from(kappa_type.to_affine()),
&self.sig.s.to_affine(),
params.prepared_miller_g2(),
) {
return Err(CompactEcashError::SpendSignaturesValidity);
}
Ok(())
}
pub fn check_exp_signature_validity(
&self,
verification_key: &VerificationKeyAuth,
spend_date: Scalar,
) -> Result<()> {
let grp_params = ecash_group_parameters();
if bool::from(self.sig_exp.h.is_identity()) {
return Err(CompactEcashError::ExpirationDateSignatureValidity);
}
if verification_key.beta_g2.len() < 3 {
return Err(CompactEcashError::VerificationKeyTooShort);
}
let m1: Scalar = spend_date;
let m2: Scalar = constants::TYPE_EXP;
let combined_kappa_e =
self.kappa_e + verification_key.beta_g2[1] * m1 + verification_key.beta_g2[2] * m2;
if !check_bilinear_pairing(
&self.sig_exp.h.to_affine(),
&G2Prepared::from(combined_kappa_e.to_affine()),
&self.sig_exp.s.to_affine(),
grp_params.prepared_miller_g2(),
) {
return Err(CompactEcashError::ExpirationDateSignatureValidity);
}
Ok(())
}
pub fn no_duplicate_serial_numbers(&self) -> Result<()> {
let mut seen_serial_numbers = Vec::new();
for serial_number in &self.ss {
if seen_serial_numbers.contains(serial_number) {
return Err(CompactEcashError::SpendDuplicateSerialNumber);
}
seen_serial_numbers.push(*serial_number);
}
Ok(())
}
pub fn batch_check_coin_index_signatures(
&self,
verification_key: &VerificationKeyAuth,
) -> Result<()> {
if verification_key.beta_g2.len() < 3 {
return Err(CompactEcashError::VerificationKeyTooShort);
}
if self.omega.len() != self.kappa_k.len() {
return Err(CompactEcashError::SpendSignaturesVerification);
}
let partially_signed = verification_key.beta_g2[1] * constants::TYPE_IDX
+ verification_key.beta_g2[2] * constants::TYPE_IDX;
let mut pairing_terms = Vec::with_capacity(self.omega.len());
for (sig, kappa_k) in self.omega.iter().zip(self.kappa_k.iter()) {
pairing_terms.push((sig, partially_signed + kappa_k))
}
if !batch_verify_signatures(pairing_terms.iter()) {
return Err(CompactEcashError::SpendSignaturesVerification);
}
Ok(())
}
pub fn verify_spend_proof(
&self,
verification_key: &VerificationKeyAuth,
pay_info: &PayInfo,
) -> Result<()> {
let mut rr = Vec::with_capacity(self.spend_value as usize);
for k in 0..self.spend_value {
let rr_k = compute_pay_info_hash(pay_info, k);
rr.push(rr_k);
}
let instance = SpendInstance {
kappa: self.kappa,
cc: self.cc,
aa: self.aa.clone(),
ss: self.ss.clone(),
tt: self.tt.clone(),
kappa_k: self.kappa_k.clone(),
kappa_e: self.kappa_e,
};
if !self
.zk_proof
.verify(&instance, verification_key, &rr, pay_info, self.spend_value)
{
return Err(CompactEcashError::SpendZKProofVerification);
}
Ok(())
}
pub fn spend_verify(
&self,
verification_key: &VerificationKeyAuth,
pay_info: &PayInfo,
spend_date: EncodedDate,
) -> Result<()> {
self.no_duplicate_serial_numbers()?;
self.verify_spend_proof(verification_key, pay_info)?;
self.check_signature_validity(verification_key)?;
self.check_exp_signature_validity(verification_key, date_scalar(spend_date))?;
self.batch_check_coin_index_signatures(verification_key)?;
Ok(())
}
pub fn encoded_serial_number(&self) -> Vec<u8> {
SerialNumberRef { inner: &self.ss }.to_bytes()
}
pub fn serial_number_bs58(&self) -> String {
SerialNumberRef { inner: &self.ss }.to_bs58()
}
}
pub struct SerialNumberRef<'a> {
pub(crate) inner: &'a [G1Projective],
}
impl SerialNumberRef<'_> {
pub fn to_bytes(&self) -> Vec<u8> {
let ss_len = self.inner.len();
let mut bytes: Vec<u8> = Vec::with_capacity(ss_len * 48);
for s in self.inner {
bytes.extend_from_slice(&s.to_affine().to_compressed());
}
bytes
}
pub fn to_bs58(&self) -> String {
bs58::encode(self.to_bytes()).into_string()
}
}