diff --git a/nym-api/src/coconut/helpers.rs b/nym-api/src/coconut/helpers.rs index 699c9b8823..f521da8940 100644 --- a/nym-api/src/coconut/helpers.rs +++ b/nym-api/src/coconut/helpers.rs @@ -1,12 +1,14 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use std::sync::atomic::{AtomicU64, Ordering}; use crate::coconut::error::CoconutError; use crate::coconut::state::bandwidth_credential_params; use nym_api_requests::coconut::models::FreePassRequest; use nym_api_requests::coconut::BlindSignRequestBody; -use nym_coconut::{Attribute, BlindSignRequest, BlindedSignature, SecretKey}; +use nym_compact_ecash::setup::{sign_coin_indices, CoinIndexSignature, Parameters}; use nym_validator_client::nyxd::error::NyxdError::AbciError; +use tokio::sync::RwLock; // If the result is already established, the vote might be redundant and // thus the transaction might fail @@ -52,13 +54,109 @@ pub(crate) fn blind_sign( request: &C, signing_key: &SecretKey, ) -> Result { - let public_attributes = request.public_attributes(); - let attributes_ref = public_attributes.iter().collect::>(); - - Ok(nym_coconut::blind_sign( - bandwidth_credential_params(), - signing_key, - request.blind_sign_request(), - &attributes_ref, - )?) +pub(crate) struct CoinIndexSignatureCache { + pub(crate) epoch_id: AtomicU64, + pub(crate) signatures: RwLock>>, +} + +impl CoinIndexSignatureCache { + pub(crate) fn new() -> Self { + CoinIndexSignatureCache { + epoch_id: AtomicU64::new(u64::MAX), + signatures: RwLock::new(None), + } + } + // if the epoch id cached is the one expected, return the cached signatures, else return None + pub(crate) async fn get_signatures( + &self, + expected_epoch_id: u64, + ) -> Option> { + if self.epoch_id.load(Ordering::Acquire) == expected_epoch_id { + let signatures = self.signatures.read().await; + signatures.clone() + } else { + None + } + } + + // refreshes (if needed) and returns the signatures. + pub(crate) async fn refresh_signatures( + &self, + expected_epoch_id: u64, + ecash_parameters: &Parameters, + verification_key: &VerificationKeyAuth, + secret_key: &SecretKeyAuth, + ) -> Vec { + let mut signatures = self.signatures.write().await; + + //if this fails, it means someone else updated the signatures in the meantime + // => We don't have to update them, and we know they exist + // (this check can spare us some signing) + if self.epoch_id.load(Ordering::Acquire) != expected_epoch_id { + *signatures = Some(sign_coin_indices( + ecash_parameters, + verification_key, + secret_key, + )); + self.epoch_id.store(expected_epoch_id, Ordering::Release); + } + + signatures.clone().unwrap() // Either we or someone else update the signatures, so they must be there + } +} + +pub(crate) struct ExpirationDateSignatureCache { + pub(crate) epoch_id: AtomicU64, + pub(crate) expiration_date: AtomicU64, + pub(crate) signatures: RwLock>>, +} + +impl ExpirationDateSignatureCache { + pub(crate) fn new() -> Self { + ExpirationDateSignatureCache { + epoch_id: AtomicU64::new(u64::MAX), + expiration_date: AtomicU64::new(u64::MAX), + signatures: RwLock::new(None), + } + } + // if the epoch id cached and expiration_date cached are the ones expected, return the cached signatures, else return None + pub(crate) async fn get_signatures( + &self, + expected_epoch_id: u64, + expected_exp_date: u64, + ) -> Option> { + if self.epoch_id.load(Ordering::Acquire) == expected_epoch_id + && self.expiration_date.load(Ordering::Acquire) == expected_exp_date + { + let signatures = self.signatures.read().await; + signatures.clone() + } else { + None + } + } + + // refreshes (if needed) and returns the signatures. + pub(crate) async fn refresh_signatures( + &self, + expected_epoch_id: u64, + expected_exp_date: u64, + secret_key: &SecretKeyAuth, + ) -> Vec { + let mut signatures = self.signatures.write().await; + + //if this fails, it means someone else updated the signatures in the meantime + // => We don't have to update them, and we know they exist + // (this check can spare us some signing) + if self.epoch_id.load(Ordering::Acquire) != expected_epoch_id + || self.expiration_date.load(Ordering::Acquire) != expected_exp_date + { + *signatures = Some(sign_expiration_date(secret_key, expected_exp_date)); + self.epoch_id.store(expected_epoch_id, Ordering::Release); + self.expiration_date + .store(expected_exp_date, Ordering::Release); + } + + signatures.clone().unwrap() // Either we or someone else update the signatures, so they must be there + } +} } diff --git a/nym-api/src/coconut/mod.rs b/nym-api/src/coconut/mod.rs index 5047982304..8db10d2022 100644 --- a/nym-api/src/coconut/mod.rs +++ b/nym-api/src/coconut/mod.rs @@ -29,7 +29,6 @@ pub(crate) const MINIMUM_BALANCE: u128 = 10_000000; pub fn stage( client: C, - mix_denom: String, identity_keypair: identity::KeyPair, key_pair: KeyPair, comm_channel: D, @@ -39,14 +38,7 @@ where C: LocalClient + Send + Sync + 'static, D: APICommunicationChannel + Send + Sync + 'static, { - let state = State::new( - client, - mix_denom, - identity_keypair, - key_pair, - comm_channel, - storage, - ); + let state = State::new(client, identity_keypair, key_pair, comm_channel, storage); AdHoc::on_ignite("Internal Sign Request Stage", |rocket| async { rocket.manage(state).mount( // this format! is so ugly... diff --git a/nym-api/src/coconut/state.rs b/nym-api/src/coconut/state.rs index 3e04110888..ebe810f4aa 100644 --- a/nym-api/src/coconut/state.rs +++ b/nym-api/src/coconut/state.rs @@ -10,7 +10,17 @@ use crate::coconut::storage::CoconutStorageExt; use crate::support::storage::NymApiStorage; use nym_api_requests::coconut::helpers::issued_credential_plaintext; use nym_api_requests::coconut::BlindSignRequestBody; -use nym_coconut::{BlindedSignature, VerificationKey}; +use nym_compact_ecash::{ + constants, + scheme::expiration_date_signatures::{sign_expiration_date, ExpirationDateSignature}, + setup::{setup, CoinIndexSignature}, + utils::BlindedSignature, + VerificationKeyAuth, +}; +use super::{ + error::CoconutError, + helpers::{accepted_vote_err, CoinIndexSignatureCache, ExpirationDateSignatureCache}, +}; use nym_coconut_dkg_common::types::EpochId; use nym_crypto::asymmetric::identity; use nym_validator_client::nyxd::{AccountId, Hash, TxResponse}; @@ -24,18 +34,18 @@ pub use nym_credentials::coconut::bandwidth::bandwidth_credential_params; pub struct State { pub(crate) client: Arc, pub(crate) bandwidth_contract_admin: OnceCell>, - pub(crate) mix_denom: String, pub(crate) coconut_keypair: KeyPair, pub(crate) identity_keypair: identity::KeyPair, pub(crate) comm_channel: Arc, pub(crate) storage: NymApiStorage, + coin_indices_sigs_cache: Arc, + exp_date_sigs_cache: Arc, pub(crate) freepass_nonce: Arc>, } impl State { pub(crate) fn new( client: C, - mix_denom: String, identity_keypair: identity::KeyPair, key_pair: KeyPair, comm_channel: D, @@ -54,11 +64,12 @@ impl State { Self { client, bandwidth_contract_admin: OnceCell::new(), - mix_denom, coconut_keypair: key_pair, identity_keypair, comm_channel, storage, + coin_indices_sigs_cache: Arc::new(CoinIndexSignatureCache::new()), + exp_date_sigs_cache: Arc::new(ExpirationDateSignatureCache::new()), freepass_nonce: Arc::new(RwLock::new(nonce)), } } @@ -104,7 +115,7 @@ impl State { request_body.tx_hash, blinded_signature, &encoded_commitments, - &request_body.public_attributes_plain, + request_body.expiration_date.try_into().unwrap(), //will fail in approx 290 billion years ); let signature = self.identity_keypair.private_key().sign(plaintext); @@ -120,7 +131,7 @@ impl State { blinded_signature, signature, encoded_commitments, - request_body.public_attributes_plain, + request_body.expiration_date.try_into().unwrap(), //will fail in approx 290 billion years ) .await?; @@ -153,4 +164,82 @@ impl State { .aggregated_verification_key(epoch_id) .await } + pub async fn get_coin_indices_signatures(&self) -> Result> { + let current_epoch = self.client.get_current_epoch().await?; + match self + .coin_indices_sigs_cache + .get_signatures(current_epoch.epoch_id) + .await + { + Some(signatures) => Ok(signatures), + None => { + let ecash_params = setup(constants::NB_TICKETS); + let verification_key = self.verification_key(current_epoch.epoch_id).await?; + let maybe_keypair_guard = self.coconut_keypair.get().await; + let Some(keypair_guard) = maybe_keypair_guard.as_ref() else { + return Err(CoconutError::KeyPairNotDerivedYet); + }; + let Some(signing_key) = keypair_guard.as_ref() else { + return Err(CoconutError::KeyPairNotDerivedYet); + }; + Ok(self + .coin_indices_sigs_cache + .refresh_signatures( + current_epoch.epoch_id, + &ecash_params, + &verification_key, + &signing_key.keys.secret_key(), + ) + .await) + } + } + } + + pub async fn get_exp_date_signatures(&self) -> Result> { + let current_epoch = self.client.get_current_epoch().await?; + let expiration_ts = cred_exp_date_timestamp(); + match self + .exp_date_sigs_cache + .get_signatures(current_epoch.epoch_id, expiration_ts) + .await + { + Some(signatures) => Ok(signatures), + None => { + let maybe_keypair_guard = self.coconut_keypair.get().await; + let Some(keypair_guard) = maybe_keypair_guard.as_ref() else { + return Err(CoconutError::KeyPairNotDerivedYet); + }; + let Some(signing_key) = keypair_guard.as_ref() else { + return Err(CoconutError::KeyPairNotDerivedYet); + }; + Ok(self + .exp_date_sigs_cache + .refresh_signatures( + current_epoch.epoch_id, + expiration_ts, + &signing_key.keys.secret_key(), + ) + .await) + } + } + } + + //this one gives the signatures for a particular day. No cache because it's only gonna be used for recovery attempt and freepasses + pub async fn get_exp_date_signatures_timestamp( + &self, + timestamp: u64, + ) -> Result> { + let maybe_keypair_guard = self.coconut_keypair.get().await; + let Some(keypair_guard) = maybe_keypair_guard.as_ref() else { + return Err(CoconutError::KeyPairNotDerivedYet); + }; + let Some(signing_key) = keypair_guard.as_ref() else { + return Err(CoconutError::KeyPairNotDerivedYet); + }; + + Ok(sign_expiration_date( + &signing_key.keys.secret_key(), + timestamp, + )) + } } diff --git a/nym-api/src/support/http/mod.rs b/nym-api/src/support/http/mod.rs index 9bf7d9b6aa..51edc1b55d 100644 --- a/nym-api/src/support/http/mod.rs +++ b/nym-api/src/support/http/mod.rs @@ -98,7 +98,6 @@ pub(crate) async fn setup_rocket( let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); rocket.attach(coconut::stage( nyxd_client.clone(), - mix_denom, identity_keypair, coconut_keypair, comm_channel,