diff --git a/common/nym_offline_compact_ecash/src/constants.rs b/common/nym_offline_compact_ecash/src/constants.rs index 0bc5a1259a..72b0893cbd 100644 --- a/common/nym_offline_compact_ecash/src/constants.rs +++ b/common/nym_offline_compact_ecash/src/constants.rs @@ -5,7 +5,7 @@ use bls12_381::Scalar; use nym_network_defaults::ecash::TICKETBOOK_VALIDITY_DAYS; use nym_network_defaults::TICKETBOOK_SIZE; -pub const PUBLIC_ATTRIBUTES_LEN: usize = 1; //expiration date +pub const PUBLIC_ATTRIBUTES_LEN: usize = 2; //expiration date and ticket type pub const PRIVATE_ATTRIBUTES_LEN: usize = 2; //user and wallet secret pub const ATTRIBUTES_LEN: usize = PUBLIC_ATTRIBUTES_LEN + PRIVATE_ATTRIBUTES_LEN; // number of attributes encoded in a single zk-nym credential diff --git a/common/nym_offline_compact_ecash/src/scheme/aggregation.rs b/common/nym_offline_compact_ecash/src/scheme/aggregation.rs index d4441179bb..b6c4e38a00 100644 --- a/common/nym_offline_compact_ecash/src/scheme/aggregation.rs +++ b/common/nym_offline_compact_ecash/src/scheme/aggregation.rs @@ -11,6 +11,7 @@ use itertools::Itertools; use crate::common_types::{PartialSignature, Signature, SignatureShare, SignerIndex}; use crate::error::{CompactEcashError, Result}; use crate::scheme::expiration_date_signatures::scalar_date; +use crate::scheme::expiration_date_signatures::scalar_type; use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth}; use crate::scheme::withdrawal::RequestInfo; use crate::scheme::{PartialWallet, Wallet, WalletSignatures}; @@ -144,17 +145,20 @@ pub fn aggregate_wallets( sk_user.sk, *req_info.get_v(), *req_info.get_expiration_date(), + *req_info.get_t_type(), ]; let aggregated_signature = aggregate_signature_shares(verification_key, &attributes, &signature_shares)?; let expiration_date_timestamp = req_info.get_expiration_date(); + let t_type = req_info.get_t_type(); Ok(Wallet { signatures: WalletSignatures { sig: aggregated_signature, v: *req_info.get_v(), expiration_date_timestamp: scalar_date(expiration_date_timestamp), + t_type: scalar_type(t_type), }, tickets_spent: 0, }) diff --git a/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs b/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs index b419d0ef73..e859ff203a 100644 --- a/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs +++ b/common/nym_offline_compact_ecash/src/scheme/expiration_date_signatures.rs @@ -411,6 +411,15 @@ pub fn scalar_date(scalar: &Scalar) -> u64 { u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]) } +pub fn type_scalar(t_type: u64) -> Scalar { + Scalar::from(t_type) +} + +pub fn scalar_type(scalar: &Scalar) -> u64 { + let b = scalar.to_bytes(); + u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]) +} + #[cfg(test)] mod tests { use super::*; diff --git a/common/nym_offline_compact_ecash/src/scheme/identify.rs b/common/nym_offline_compact_ecash/src/scheme/identify.rs index efc0e102f7..df6c9096f3 100644 --- a/common/nym_offline_compact_ecash/src/scheme/identify.rs +++ b/common/nym_offline_compact_ecash/src/scheme/identify.rs @@ -74,11 +74,12 @@ mod tests { // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! let spend_date = 1701907200; // Dec 07 2023 00:00:00 let expiration_date = 1702166400; // Dec 10 2023 00:00:00 + let t_type = 1; let user_keypair = generate_keypair_user(); let (req, req_info) = - withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); let authorities_keypairs = ttp_keygen(2, 3).unwrap(); let indices: [u64; 3] = [1, 2, 3]; let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs @@ -120,6 +121,7 @@ mod tests { user_keypair.public_key(), &req, expiration_date, + t_type, ); wallet_blinded_signatures.push(blind_signature.unwrap()); } @@ -182,11 +184,12 @@ mod tests { // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! let spend_date = 1701907200; // Dec 07 2023 00:00:00 let expiration_date = 1702166400; // Dec 10 2023 00:00:00 + let t_type = 1; let user_keypair = generate_keypair_user(); let (req, req_info) = - withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); let authorities_keypairs = ttp_keygen(2, 3).unwrap(); let indices: [u64; 3] = [1, 2, 3]; let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs @@ -228,6 +231,7 @@ mod tests { user_keypair.public_key(), &req, expiration_date, + t_type, ); wallet_blinded_signatures.push(blind_signature.unwrap()); } @@ -306,6 +310,7 @@ mod tests { // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! let spend_date = 1701907200; // Dec 07 2023 00:00:00 let expiration_date = 1702166400; // Dec 10 2023 00:00:00 + let t_type = 1; let user_keypair = generate_keypair_user(); @@ -320,7 +325,7 @@ mod tests { public_keys.push(user_keypair.public_key().clone()); let (req, req_info) = - withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); let authorities_keypairs = ttp_keygen(2, 3).unwrap(); let indices: [u64; 3] = [1, 2, 3]; let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs @@ -362,6 +367,7 @@ mod tests { user_keypair.public_key(), &req, expiration_date, + t_type, ); wallet_blinded_signatures.push(blind_signature.unwrap()); } @@ -447,6 +453,7 @@ mod tests { // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! let spend_date = 1701907200; // Dec 07 2023 00:00:00 let expiration_date = 1702166400; // Dec 10 2023 00:00:00 + let t_type = 1; let user_keypair = generate_keypair_user(); @@ -461,7 +468,7 @@ mod tests { public_keys.push(user_keypair.public_key().clone()); let (req, req_info) = - withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); let authorities_keypairs = ttp_keygen(2, 3).unwrap(); let indices: [u64; 3] = [1, 2, 3]; let secret_keys_authorities: Vec<&SecretKeyAuth> = authorities_keypairs @@ -504,6 +511,7 @@ mod tests { user_keypair.public_key(), &req, expiration_date, + t_type, ); wallet_blinded_signatures.push(blind_signature.unwrap()); } diff --git a/common/nym_offline_compact_ecash/src/scheme/mod.rs b/common/nym_offline_compact_ecash/src/scheme/mod.rs index 7440c3641c..187b7cf1c3 100644 --- a/common/nym_offline_compact_ecash/src/scheme/mod.rs +++ b/common/nym_offline_compact_ecash/src/scheme/mod.rs @@ -41,6 +41,7 @@ pub struct PartialWallet { v: Scalar, idx: SignerIndex, expiration_date: Scalar, + t_type: Scalar, } impl PartialWallet { @@ -54,23 +55,27 @@ impl PartialWallet { pub fn expiration_date(&self) -> Scalar { self.expiration_date } + pub fn t_type(&self) -> Scalar { + self.t_type + } /// Converts the `PartialWallet` to a fixed-size byte array. /// - /// The resulting byte array has a length of 168 bytes and contains serialized + /// The resulting byte array has a length of 200 bytes and contains serialized /// representations of the `Signature` (`sig`), scalar value (`v`), /// expiration date (`expiration_date`), and `idx` fields of the `PartialWallet` struct. /// /// # Returns /// - /// A fixed-size byte array (`[u8; 168]`) representing the serialized form of the `PartialWallet`. + /// A fixed-size byte array (`[u8; 200]`) representing the serialized form of the `PartialWallet`. /// - pub fn to_bytes(&self) -> [u8; 168] { - let mut bytes = [0u8; 168]; + 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..168].copy_from_slice(&self.idx.to_le_bytes()); + bytes[160..192].copy_from_slice(&self.t_type.to_bytes()); + bytes[192..200].copy_from_slice(&self.idx.to_le_bytes()); bytes } @@ -91,9 +96,10 @@ impl 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 + IDX_BYTES; + SIGNATURE_BYTES + V_BYTES + EXPIRATION_DATE_BYTES + T_TYPE_BYTES + IDX_BYTES; if bytes.len() != EXPECTED_LENGTH { return Err(CompactEcashError::DeserializationLengthMismatch { @@ -119,6 +125,11 @@ impl PartialWallet { 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; + //SAFETY: slice to array after length check + #[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; //SAFETY: slice to array after length check #[allow(clippy::unwrap_used)] @@ -130,6 +141,7 @@ impl PartialWallet { v, idx, expiration_date, + t_type, }) } } @@ -258,6 +270,7 @@ pub struct WalletSignatures { sig: Signature, v: Scalar, expiration_date_timestamp: u64, + t_type: u64, } impl WalletSignatures { @@ -301,8 +314,8 @@ pub fn compute_pay_info_hash(pay_info: &PayInfo, k: u64) -> Scalar { } impl WalletSignatures { - // signature size (96) + secret size (32) + expiration size (8) - pub const SERIALISED_SIZE: usize = 136; + // signature size (96) + secret size (32) + expiration size (8) + t_type (8) + pub const SERIALISED_SIZE: usize = 144; pub fn signature(&self) -> &Signature { &self.sig @@ -323,6 +336,7 @@ impl WalletSignatures { bytes[0..96].copy_from_slice(&self.sig.to_bytes()); bytes[96..128].copy_from_slice(&self.v.to_bytes()); bytes[128..136].copy_from_slice(&self.expiration_date_timestamp.to_be_bytes()); + bytes[136..144].copy_from_slice(&self.t_type.to_be_bytes()); bytes } @@ -342,16 +356,21 @@ impl WalletSignatures { let v_bytes: &[u8; 32] = &bytes[96..128].try_into().unwrap(); #[allow(clippy::unwrap_used)] - let expiration_date_bytes = bytes[128..].try_into().unwrap(); + let expiration_date_bytes = bytes[128..136].try_into().unwrap(); + + #[allow(clippy::unwrap_used)] + let t_type_bytes = bytes[136..].try_into().unwrap(); let sig = Signature::try_from(sig_bytes.as_slice())?; let v = Scalar::from_bytes(v_bytes).unwrap(); let expiration_date_timestamp = u64::from_be_bytes(expiration_date_bytes); + let t_type = u64::from_be_bytes(t_type_bytes); Ok(WalletSignatures { sig, v, expiration_date_timestamp, + t_type, }) } @@ -550,6 +569,7 @@ impl WalletSignatures { aa: aa.clone(), spend_value, cc, + t_type: self.t_type, zk_proof, }; @@ -669,6 +689,7 @@ pub struct Payment { pub aa: Vec, pub spend_value: u64, pub cc: G1Projective, + pub t_type: u64, pub zk_proof: SpendProof, } @@ -693,15 +714,16 @@ impl Payment { /// - The element `h` of the payment signature equals the identity. /// - The bilinear pairing check for `kappa` fails. /// - pub fn check_signature_validity(&self) -> Result<()> { + 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] * Scalar::from(self.t_type); if !check_bilinear_pairing( &self.sig.h.to_affine(), - &G2Prepared::from(self.kappa.to_affine()), + &G2Prepared::from(kappa_type.to_affine()), &self.sig.s.to_affine(), params.prepared_miller_g2(), ) { @@ -935,7 +957,7 @@ impl Payment { // verify the zk proof self.verify_spend_proof(verification_key, pay_info)?; // Verify whether the payment signature and kappa are correct - self.check_signature_validity()?; + self.check_signature_validity(verification_key)?; // Verify whether the expiration date signature and kappa_e are correct self.check_exp_signature_validity(verification_key, spend_date)?; // Verify whether the coin indices signatures and kappa_k are correct diff --git a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs index 1cd116e611..4d072cfe50 100644 --- a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs +++ b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs @@ -57,6 +57,7 @@ pub struct RequestInfo { private_attributes_openings: Vec, wallet_secret: Scalar, expiration_date: Scalar, + t_type: Scalar, } impl RequestInfo { @@ -75,6 +76,9 @@ impl RequestInfo { pub fn get_expiration_date(&self) -> &Scalar { &self.expiration_date } + pub fn get_t_type(&self) -> &Scalar { + &self.t_type + } } /// Computes Pedersen commitments for private attributes. @@ -117,6 +121,7 @@ fn compute_private_attribute_commitments( /// /// * `sk_user` - A reference to the user's secret key. /// * `expiration_date` - The expiration date for the withdrawal request. +/// * `t_type` - The type of the ticket book /// /// # Returns /// @@ -137,6 +142,7 @@ fn compute_private_attribute_commitments( pub fn withdrawal_request( sk_user: &SecretKeyUser, expiration_date: u64, + t_type: u64, ) -> Result<(WithdrawalRequest, RequestInfo)> { let params = ecash_group_parameters(); // Generate random and unique wallet secret @@ -152,8 +158,9 @@ pub fn withdrawal_request( // Compute commitment hash h #[allow(clippy::unwrap_used)] let joined_commitment_hash = hash_g1( - (joined_commitment + params.gamma_idx(2).unwrap() * Scalar::from(expiration_date)) - .to_bytes(), + (joined_commitment + + params.gamma_idx(2).unwrap() * Scalar::from(expiration_date) + + params.gamma_idx(3).unwrap() * Scalar::from(t_type)).to_bytes(), ); // Compute Pedersen commitments for private attributes (wallet secret and user's secret) @@ -192,6 +199,7 @@ pub fn withdrawal_request( private_attributes_openings: private_attributes_openings.clone(), wallet_secret: v, expiration_date: Scalar::from(expiration_date), + t_type: Scalar::from(t_type), }, )) } @@ -203,7 +211,8 @@ pub fn withdrawal_request( /// /// * `req` - The withdrawal request to be verified. /// * `pk_user` - Public key of the user associated with the withdrawal request. -/// * `expiration_date` - Expiration date for the withdrawal request. +/// * `expiration_date` - Expiration date for the ticket book. +/// * `t_type` - The type of the ticket book /// /// # Returns /// @@ -213,13 +222,16 @@ pub fn request_verify( req: &WithdrawalRequest, pk_user: PublicKeyUser, expiration_date: u64, + t_type: u64, ) -> Result<()> { let params = ecash_group_parameters(); // Verify the joined commitment hash //SAFETY: params is static with length 3 #[allow(clippy::unwrap_used)] let expected_commitment_hash = hash_g1( - (req.joined_commitment + params.gamma_idx(2).unwrap() * Scalar::from(expiration_date)) + (req.joined_commitment + + params.gamma_idx(2).unwrap() * Scalar::from(expiration_date) + + params.gamma_idx(3).unwrap() * Scalar::from(t_type)) .to_bytes(), ); if req.joined_commitment_hash != expected_commitment_hash { @@ -266,6 +278,32 @@ fn sign_expiration_date( joined_commitment_hash * (yi * Scalar::from(expiration_date)) } +/// Signs a transaction type using a joined commitment hash and a secret key. +/// +/// Given a joined commitment hash (`joined_commitment_hash`), a ticket type (`t_type`), +/// and a secret key for authentication (`sk_auth`), this function computes the signature of the +/// ticket type. +/// +/// # Arguments +/// +/// * `joined_commitment_hash` - The G1Projective point representing the joined commitment hash. +/// * `t_type` - The ticket type identifier to be signed. +/// * `sk_auth` - The secret key of the signing authority. +/// +/// # Returns +/// +/// The resulting G1Projective point representing the signed ticket type. +fn sign_t_type( + joined_commitment_hash: &G1Projective, + t_type: u64, + sk_auth: &SecretKeyAuth, +) -> G1Projective { + //SAFETY : this fn assumes a long enough key + #[allow(clippy::unwrap_used)] + let yi = sk_auth.get_y_by_idx(3).unwrap(); + joined_commitment_hash * (yi * Scalar::from(t_type)) +} + /// Issues a blinded signature for a withdrawal request, after verifying its integrity. /// /// This function first verifies the withdrawal request using the provided group parameters, @@ -289,9 +327,10 @@ pub fn issue( pk_user: PublicKeyUser, withdrawal_req: &WithdrawalRequest, expiration_date: u64, + t_type: u64, ) -> Result { // Verify the withdrawal request - request_verify(withdrawal_req, pk_user, expiration_date)?; + request_verify(withdrawal_req, pk_user, expiration_date, t_type)?; // Verify `sk_auth` is long enough if sk_auth.ys.len() < constants::ATTRIBUTES_LEN { return Err(CompactEcashError::KeyTooShort); @@ -310,9 +349,15 @@ pub fn issue( expiration_date, sk_auth, ); + // Sign the type + let t_type_sign = sign_t_type( + &withdrawal_req.joined_commitment_hash, + t_type, + sk_auth, + ); // Combine both signatures let signature = - blind_signatures + withdrawal_req.joined_commitment_hash * sk_auth.x + expiration_date_sign; + blind_signatures + withdrawal_req.joined_commitment_hash * sk_auth.x + expiration_date_sign + t_type_sign; Ok(BlindedSignature { h: withdrawal_req.joined_commitment_hash, @@ -361,7 +406,7 @@ pub fn issue_verify( .sum::(); let unblinded_c = blind_signature.c - blinding_removers; - let attr = [sk_user.sk, req_info.wallet_secret, req_info.expiration_date]; + let attr = [sk_user.sk, req_info.wallet_secret, req_info.expiration_date, req_info.t_type]; let signed_attributes = attr .iter() @@ -387,6 +432,7 @@ pub fn issue_verify( v: req_info.wallet_secret, idx: signer_index, expiration_date: req_info.expiration_date, + t_type: req_info.t_type, }) } diff --git a/common/nym_offline_compact_ecash/src/tests/e2e.rs b/common/nym_offline_compact_ecash/src/tests/e2e.rs index 7db20a03bf..24c74c813d 100644 --- a/common/nym_offline_compact_ecash/src/tests/e2e.rs +++ b/common/nym_offline_compact_ecash/src/tests/e2e.rs @@ -24,6 +24,7 @@ mod tests { // NOTE: Make sure that the date timestamp are calculated at 00:00:00!! let spend_date = 1701907200; // Dec 07 2023 00:00:00 let expiration_date = 1702166400; // Dec 10 2023 00:00:00 + let t_type = 1; let user_keypair = generate_keypair_user(); // generate authorities keys @@ -61,7 +62,7 @@ mod tests { // request a wallet let (req, req_info) = - withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); let req_bytes = req.to_bytes(); let req2 = WithdrawalRequest::try_from(req_bytes.as_slice()).unwrap(); assert_eq!(req, req2); @@ -74,6 +75,7 @@ mod tests { user_keypair.public_key(), &req, expiration_date, + t_type, ); wallet_blinded_signatures.push(blind_signature.unwrap()); } diff --git a/common/nym_offline_compact_ecash/src/tests/helpers.rs b/common/nym_offline_compact_ecash/src/tests/helpers.rs index 4e98e7b060..b8680e6efa 100644 --- a/common/nym_offline_compact_ecash/src/tests/helpers.rs +++ b/common/nym_offline_compact_ecash/src/tests/helpers.rs @@ -83,6 +83,7 @@ pub fn payment_from_keys_and_expiration_date( ecash_keypairs: &Vec, indices: &[SignerIndex], expiration_date: u64, + t_type: u64, ) -> Result<(Payment, PayInfo)> { let total_coins = 32; let params = Parameters::new(total_coins); @@ -121,7 +122,7 @@ pub fn payment_from_keys_and_expiration_date( //SAFETY : method intended for test only #[allow(clippy::unwrap_used)] // request a wallet - let (req, req_info) = withdrawal_request(user_keypair.secret_key(), expiration_date).unwrap(); + let (req, req_info) = withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); // generate blinded signatures let mut wallet_blinded_signatures = Vec::new(); @@ -132,6 +133,7 @@ pub fn payment_from_keys_and_expiration_date( user_keypair.public_key(), &req, expiration_date, + t_type, )?; wallet_blinded_signatures.push(blinded_signature) }