From e14b1285b43de83bcb7db60fe8df708aa7bb4bc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 7 Aug 2025 10:29:06 +0100 Subject: [PATCH 01/10] chore: rename VpnApiError to CredentialProxyError --- .../src/credentials/ticketbook/mod.rs | 24 +++++---- .../nym-credential-proxy/src/deposit_maker.rs | 10 ++-- .../nym-credential-proxy/src/error.rs | 8 +-- .../nym-credential-proxy/src/helpers.rs | 4 +- .../nym-credential-proxy/src/http/helpers.rs | 4 +- .../nym-credential-proxy/src/http/mod.rs | 8 +-- .../http/router/api/v1/ticketbook/shares.rs | 4 +- .../src/http/state/mod.rs | 51 ++++++++++--------- .../nym-credential-proxy/src/http/types.rs | 4 +- .../src/nym_api_helpers.rs | 16 +++--- .../src/storage/manager.rs | 12 ++--- .../nym-credential-proxy/src/storage/mod.rs | 50 +++++++++--------- .../nym-credential-proxy/src/webhook.rs | 6 +-- 13 files changed, 105 insertions(+), 96 deletions(-) diff --git a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs index 33ad66391ef..3f18b418b04 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::deposit_maker::{DepositRequest, DepositResponse}; -use crate::error::VpnApiError; +use crate::error::CredentialProxyError; use crate::http::state::ApiState; use crate::storage::models::BlindedShares; use futures::{stream, StreamExt}; @@ -34,7 +34,7 @@ async fn make_deposit( state: &ApiState, pub_key: ed25519::PublicKey, deposit_amount: &Coin, -) -> Result { +) -> Result { let start = Instant::now(); let (on_done_tx, on_done_rx) = oneshot::channel(); let request = DepositRequest::new(pub_key, deposit_amount, on_done_tx); @@ -45,7 +45,7 @@ async fn make_deposit( let Ok(deposit_response) = on_done_rx.await else { error!("failed to receive deposit response: the corresponding sender channel got dropped by the DepositMaker!"); - return Err(VpnApiError::DepositFailure); + return Err(CredentialProxyError::DepositFailure); }; if time_taken > Duration::from_secs(20) { @@ -54,7 +54,7 @@ async fn make_deposit( debug!("attempting to resolve deposit request took {formatted}") } - deposit_response.ok_or(VpnApiError::DepositFailure) + deposit_response.ok_or(CredentialProxyError::DepositFailure) } #[instrument( @@ -69,7 +69,7 @@ pub(crate) async fn try_obtain_wallet_shares( request: Uuid, requested_on: OffsetDateTime, request_data: TicketbookRequest, -) -> Result, VpnApiError> { +) -> Result, CredentialProxyError> { let mut rng = OsRng; let ed25519_keypair = ed25519::KeyPair::new(&mut rng); @@ -135,7 +135,7 @@ pub(crate) async fn try_obtain_wallet_shares( client.api_client.blind_sign(&credential_request), ) .await - .map_err(|_| VpnApiError::EcashApiRequestTimeout { + .map_err(|_| CredentialProxyError::EcashApiRequestTimeout { client_repr: client.to_string(), }) .and_then(|res| res.map_err(Into::into)); @@ -176,7 +176,7 @@ pub(crate) async fn try_obtain_wallet_shares( let shares = wallet_shares.len(); if shares < threshold as usize { - return Err(VpnApiError::InsufficientNumberOfCredentials { + return Err(CredentialProxyError::InsufficientNumberOfCredentials { available: shares, threshold, }); @@ -199,12 +199,14 @@ async fn try_obtain_wallet_shares_async( request_data: TicketbookRequest, device_id: &str, credential_id: &str, -) -> Result, VpnApiError> { +) -> Result, CredentialProxyError> { let shares = match try_obtain_wallet_shares(state, request, requested_on, request_data).await { Ok(shares) => shares, Err(err) => { let obtained = match err { - VpnApiError::InsufficientNumberOfCredentials { available, .. } => available, + CredentialProxyError::InsufficientNumberOfCredentials { available, .. } => { + available + } _ => 0, }; @@ -235,7 +237,7 @@ async fn try_obtain_blinded_ticketbook_async_inner( request_data: TicketbookAsyncRequest, params: TicketbookObtainQueryParams, pending: &BlindedShares, -) -> Result<(), VpnApiError> { +) -> Result<(), CredentialProxyError> { let epoch_id = state.current_epoch_id().await?; let device_id = &request_data.device_id; @@ -318,7 +320,7 @@ async fn try_trigger_webhook_request_for_error( request_data: TicketbookAsyncRequest, pending: &BlindedShares, error_message: String, -) -> Result<(), VpnApiError> { +) -> Result<(), CredentialProxyError> { let device_id = &request_data.device_id; let credential_id = &request_data.credential_id; let secret = request_data.secret.clone(); diff --git a/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs b/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs index 2ec685d656a..f56dc462850 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::VpnApiError; +use crate::error::CredentialProxyError; use crate::http::state::ChainClient; use nym_crypto::asymmetric::ed25519; use nym_ecash_contract_common::deposit::DepositId; @@ -99,7 +99,7 @@ impl DepositMaker { pub(crate) async fn process_deposit_requests( &mut self, requests: Vec, - ) -> Result<(), VpnApiError> { + ) -> Result<(), CredentialProxyError> { let chain_write_permit = self.client.start_chain_tx().await; info!("starting deposits"); @@ -146,7 +146,7 @@ impl DepositMaker { // because it requires some serious MANUAL intervention error!("CRITICAL FAILURE: failed to parse out deposit information from the contract transaction. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually. error was: {err}"); self.cancellation_token.cancel(); - return Err(VpnApiError::DepositFailure); + return Err(CredentialProxyError::DepositFailure); } }; @@ -154,7 +154,7 @@ impl DepositMaker { // another critical failure, that one should be quite impossible and thus has to be manually inspected error!("CRITICAL FAILURE: failed to parse out all deposit information from the contract transaction. got {} responses while we sent {} deposits! either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually", contract_data.len(), replies.len()); self.cancellation_token.cancel(); - return Err(VpnApiError::DepositFailure); + return Err(CredentialProxyError::DepositFailure); } for (reply_channel, response) in replies.into_iter().zip(contract_data) { @@ -165,7 +165,7 @@ impl DepositMaker { // another impossibility error!("CRITICAL FAILURE: failed to parse out deposit id out of the response at index {response_index}: {err}. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually"); self.cancellation_token.cancel(); - return Err(VpnApiError::DepositFailure); + return Err(CredentialProxyError::DepositFailure); } }; diff --git a/nym-credential-proxy/nym-credential-proxy/src/error.rs b/nym-credential-proxy/nym-credential-proxy/src/error.rs index 56e18a0673d..0a8d11677d5 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/error.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/error.rs @@ -10,7 +10,7 @@ use thiserror::Error; use time::OffsetDateTime; #[derive(Debug, Error)] -pub enum VpnApiError { +pub enum CredentialProxyError { #[error("encountered an internal io error: {source}")] IoError { #[from] @@ -120,9 +120,9 @@ pub enum VpnApiError { DepositFailure, } -impl VpnApiError { - pub fn database_inconsistency>(reason: S) -> VpnApiError { - VpnApiError::DatabaseInconsistency { +impl CredentialProxyError { + pub fn database_inconsistency>(reason: S) -> CredentialProxyError { + CredentialProxyError::DatabaseInconsistency { reason: reason.into(), } } diff --git a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs index 8a528ff5754..35da4e8f233 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs @@ -9,7 +9,7 @@ use tracing::{debug, error, info, warn}; use crate::{ cli::Cli, deposit_maker::DepositMaker, - error::VpnApiError, + error::CredentialProxyError, http::{ state::{ApiState, ChainClient}, HttpServer, @@ -91,7 +91,7 @@ fn build_sha_short() -> &'static str { &bin_info.commit_sha[..7] } -pub(crate) async fn run_api(cli: Cli) -> Result<(), VpnApiError> { +pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> { // create the tasks let bind_address = cli.bind_address(); diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/http/helpers.rs index 844e1510c05..b3c7d41bb18 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/helpers.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/helpers.rs @@ -1,7 +1,7 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::VpnApiError; +use crate::error::CredentialProxyError; use crate::http::types::RequestError; use axum::http::StatusCode; use rand::rngs::OsRng; @@ -16,7 +16,7 @@ pub fn random_uuid() -> Uuid { Uuid::from_bytes(bytes) } -pub fn db_failure(err: VpnApiError, uuid: Uuid) -> Result { +pub fn db_failure(err: CredentialProxyError, uuid: Uuid) -> Result { warn!("db failure: {err}"); Err(RequestError::new_with_uuid( format!("oh no, something went wrong {err}"), diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs index e9359ac16ae..caf95050b89 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs @@ -1,7 +1,7 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::VpnApiError; +use crate::error::CredentialProxyError; use crate::http::router::build_router; use crate::http::state::ApiState; use axum::Router; @@ -34,13 +34,13 @@ impl HttpServer { } } - pub async fn run_forever(self) -> Result<(), VpnApiError> { + pub async fn run_forever(self) -> Result<(), CredentialProxyError> { let address = self.bind_address; info!("starting the http server on http://{address}"); let listener = tokio::net::TcpListener::bind(address) .await - .map_err(|source| VpnApiError::SocketBindFailure { address, source })?; + .map_err(|source| CredentialProxyError::SocketBindFailure { address, source })?; let cancellation = self.cancellation; @@ -51,6 +51,6 @@ impl HttpServer { ) .with_graceful_shutdown(async move { cancellation.cancelled().await }) .await - .map_err(|source| VpnApiError::HttpServerFailure { source }) + .map_err(|source| CredentialProxyError::HttpServerFailure { source }) } } diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs index 8b3dacb82d4..766cddf6617 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs @@ -1,7 +1,7 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::VpnApiError; +use crate::error::CredentialProxyError; use crate::http::helpers::{db_failure, random_uuid}; use crate::http::router::api::v1::ticketbook::FormattedTicketbookWalletSharesResponse; use crate::http::state::ApiState; @@ -35,7 +35,7 @@ async fn shares_to_response( let threshold = state.response_ecash_threshold(uuid, epoch_id).await?; if shares.len() < threshold as usize { return Err(RequestError::new_server_error( - VpnApiError::InsufficientNumberOfCredentials { + CredentialProxyError::InsufficientNumberOfCredentials { available: shares.len(), threshold, }, diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs index 4579b677fa2..ba15c0b7f0d 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::deposit_maker::{DepositRequest, DepositRequestSender}; -use crate::error::VpnApiError; +use crate::error::CredentialProxyError; use crate::helpers::LockTimer; use crate::http::types::RequestError; use crate::nym_api_helpers::{ @@ -67,7 +67,7 @@ impl ApiState { client: ChainClient, deposit_requester: DepositRequestSender, cancellation_token: CancellationToken, - ) -> Result { + ) -> Result { let state = ApiState { inner: Arc::new(ApiStateInner { storage, @@ -88,7 +88,7 @@ impl ApiState { Ok(state) } - async fn build_initial_cache(&self) -> Result<(), VpnApiError> { + async fn build_initial_cache(&self) -> Result<(), CredentialProxyError> { let today = ecash_today().date(); let epoch_id = self.current_epoch_id().await?; @@ -130,7 +130,7 @@ impl ApiState { &self.inner.zk_nym_web_hook_config } - async fn ensure_credentials_issuable(&self) -> Result<(), VpnApiError> { + async fn ensure_credentials_issuable(&self) -> Result<(), CredentialProxyError> { let epoch = self.current_epoch().await?; if epoch.state.is_final() { @@ -141,13 +141,13 @@ impl ApiState { #[allow(clippy::unwrap_used)] let finish_dt = OffsetDateTime::from_unix_timestamp(final_timestamp as i64).unwrap(); - Err(VpnApiError::CredentialsNotYetIssuable { + Err(CredentialProxyError::CredentialsNotYetIssuable { availability: finish_dt, }) } else if epoch.state.is_waiting_initialisation() { - Err(VpnApiError::UninitialisedDkg) + Err(CredentialProxyError::UninitialisedDkg) } else { - Err(VpnApiError::UnknownEcashFailure) + Err(CredentialProxyError::UnknownEcashFailure) } } @@ -155,7 +155,7 @@ impl ApiState { &self.inner.storage } - pub async fn deposit_amount(&self) -> Result { + pub async fn deposit_amount(&self) -> Result { let read_guard = self.inner.ecash_state.required_deposit_cache.read().await; if read_guard.is_valid() { return Ok(read_guard.required_amount.clone()); @@ -175,7 +175,7 @@ impl ApiState { Ok(deposit_amount.into()) } - async fn current_epoch(&self) -> Result { + async fn current_epoch(&self) -> Result { let read_guard = self.inner.ecash_state.cached_epoch.read().await; if read_guard.is_valid() { return Ok(read_guard.current_epoch); @@ -190,7 +190,7 @@ impl ApiState { Ok(epoch) } - pub async fn current_epoch_id(&self) -> Result { + pub async fn current_epoch_id(&self) -> Result { let read_guard = self.inner.ecash_state.cached_epoch.read().await; if read_guard.is_valid() { return Ok(read_guard.current_epoch.epoch_id); @@ -235,7 +235,7 @@ impl ApiState { Option, Option, ), - VpnApiError, + CredentialProxyError, > { let master_verification_key = if include_master_verification_key { debug!("including master verification key in the response"); @@ -338,7 +338,7 @@ impl ApiState { pub(crate) async fn ecash_clients( &self, epoch_id: EpochId, - ) -> Result>, VpnApiError> { + ) -> Result>, CredentialProxyError> { self.inner .ecash_state .epoch_clients @@ -355,7 +355,10 @@ impl ApiState { .await } - pub(crate) async fn ecash_threshold(&self, epoch_id: EpochId) -> Result { + pub(crate) async fn ecash_threshold( + &self, + epoch_id: EpochId, + ) -> Result { self.inner .ecash_state .threshold_values @@ -368,7 +371,7 @@ impl ApiState { { Ok(threshold) } else { - Err(VpnApiError::UnavailableThreshold { epoch_id }) + Err(CredentialProxyError::UnavailableThreshold { epoch_id }) } }) .await @@ -388,7 +391,7 @@ impl ApiState { pub(crate) async fn master_verification_key( &self, epoch_id: Option, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { let epoch_id = match epoch_id { Some(id) => id, None => self.current_epoch_id().await?, @@ -415,7 +418,7 @@ impl ApiState { let threshold = self.ecash_threshold(epoch_id).await?; if all_apis.len() < threshold as usize { - return Err(VpnApiError::InsufficientNumberOfSigners { + return Err(CredentialProxyError::InsufficientNumberOfSigners { threshold, available: all_apis.len(), }); @@ -442,7 +445,7 @@ impl ApiState { pub(crate) async fn master_coin_index_signatures( &self, epoch_id: Option, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { let epoch_id = match epoch_id { Some(id) => id, None => self.current_epoch_id().await?, @@ -519,7 +522,7 @@ impl ApiState { &self, epoch_id: EpochId, expiration_date: Date, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { self.inner .ecash_state .expiration_date_signatures @@ -598,25 +601,25 @@ impl ApiState { pub struct ChainClient(Arc>); impl ChainClient { - pub fn new(mnemonic: Mnemonic) -> Result { + pub fn new(mnemonic: Mnemonic) -> Result { let network_details = nym_network_defaults::NymNetworkDetails::new_from_env(); let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; let nyxd_url = network_details .endpoints .first() - .ok_or_else(|| VpnApiError::NoNyxEndpointsAvailable)? + .ok_or_else(|| CredentialProxyError::NoNyxEndpointsAvailable)? .nyxd_url .as_str(); let client = NyxdClient::connect_with_mnemonic(client_config, nyxd_url, mnemonic)?; if client.ecash_contract_address().is_none() { - return Err(VpnApiError::UnavailableEcashContract); + return Err(CredentialProxyError::UnavailableEcashContract); } if client.dkg_contract_address().is_none() { - return Err(VpnApiError::UnavailableDKGContract); + return Err(CredentialProxyError::UnavailableDKGContract); } Ok(ChainClient(Arc::new(RwLock::new(client)))) @@ -718,7 +721,7 @@ impl ChainWritePermit<'_> { self, short_sha: &'static str, info: Vec<(String, Coin)>, - ) -> Result { + ) -> Result { let address = self.inner.address(); let starting_sequence = self.inner.get_sequence(&address).await?.sequence; @@ -727,7 +730,7 @@ impl ChainWritePermit<'_> { let ecash_contract = self .inner .ecash_contract_address() - .ok_or(VpnApiError::UnavailableEcashContract)?; + .ok_or(CredentialProxyError::UnavailableEcashContract)?; let deposit_messages = info .into_iter() .map(|(identity_key, amount)| { diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/types.rs b/nym-credential-proxy/nym-credential-proxy/src/http/types.rs index 6538c5a9a73..aabc9288eb9 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/types.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/types.rs @@ -1,7 +1,7 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::VpnApiError; +use crate::error::CredentialProxyError; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::Json; @@ -35,7 +35,7 @@ impl RequestError { } } - pub fn new_server_error(err: VpnApiError, uuid: Uuid) -> Self { + pub fn new_server_error(err: CredentialProxyError, uuid: Uuid) -> Self { RequestError::new_with_uuid(err.to_string(), uuid, StatusCode::INTERNAL_SERVER_ERROR) } diff --git a/nym-credential-proxy/nym-credential-proxy/src/nym_api_helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/nym_api_helpers.rs index 72d0e2a4675..cd521b3c9f1 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/nym_api_helpers.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/nym_api_helpers.rs @@ -4,7 +4,7 @@ // TODO: this was just copied from nym-api; // it should have been therefore extracted to a common crate instead and imported as dependency -use crate::error::VpnApiError; +use crate::error::CredentialProxyError; use futures::{stream, StreamExt}; use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime}; use nym_validator_client::nym_api::EpochId; @@ -129,16 +129,18 @@ where } } -pub(crate) fn ensure_sane_expiration_date(expiration_date: Date) -> Result<(), VpnApiError> { +pub(crate) fn ensure_sane_expiration_date( + expiration_date: Date, +) -> Result<(), CredentialProxyError> { let today = ecash_today(); if expiration_date < today.date() { // what's the point of signatures with expiration in the past? - return Err(VpnApiError::ExpirationDateTooEarly); + return Err(CredentialProxyError::ExpirationDateTooEarly); } if expiration_date > cred_exp_date().ecash_date() { - return Err(VpnApiError::ExpirationDateTooLate); + return Err(CredentialProxyError::ExpirationDateTooLate); } Ok(()) @@ -148,10 +150,10 @@ pub(crate) async fn query_all_threshold_apis( all_apis: Vec, threshold: u64, f: F, -) -> Result, VpnApiError> +) -> Result, CredentialProxyError> where F: Fn(EcashApiClient) -> U, - U: Future>, + U: Future>, { let shares = Mutex::new(Vec::with_capacity(all_apis.len())); @@ -172,7 +174,7 @@ where let shares = shares.into_inner(); if shares.len() < threshold as usize { - return Err(VpnApiError::InsufficientNumberOfSigners { + return Err(CredentialProxyError::InsufficientNumberOfSigners { threshold, available: shares.len(), }); diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs index b5fefae4490..ef115a6c12f 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::VpnApiError; +use crate::error::CredentialProxyError; use crate::storage::models::{ BlindedShares, BlindedSharesStatus, MinimalWalletShare, RawCoinIndexSignatures, RawExpirationDateSignatures, RawVerificationKey, @@ -169,7 +169,7 @@ impl SqliteStorageManager { available_shares: i64, device_id: &str, credential_id: &str, - ) -> Result { + ) -> Result { let now = OffsetDateTime::now_utc(); let res = sqlx::query_as( r#" @@ -196,7 +196,7 @@ impl SqliteStorageManager { device_id: &str, credential_id: &str, error: &str, - ) -> Result { + ) -> Result { let now = time::OffsetDateTime::now_utc(); let res = sqlx::query_as( r#" @@ -221,7 +221,7 @@ impl SqliteStorageManager { pub(crate) async fn prune_old_blinded_shares( &self, delete_after: OffsetDateTime, - ) -> Result<(), VpnApiError> { + ) -> Result<(), CredentialProxyError> { sqlx::query!( r#" DELETE FROM blinded_shares WHERE created < ? @@ -236,7 +236,7 @@ impl SqliteStorageManager { pub(crate) async fn prune_old_partial_blinded_wallets( &self, delete_after: OffsetDateTime, - ) -> Result<(), VpnApiError> { + ) -> Result<(), CredentialProxyError> { sqlx::query!( r#" DELETE FROM partial_blinded_wallet WHERE created < ? @@ -251,7 +251,7 @@ impl SqliteStorageManager { pub(crate) async fn prune_old_partial_blinded_wallet_failures( &self, delete_after: OffsetDateTime, - ) -> Result<(), VpnApiError> { + ) -> Result<(), CredentialProxyError> { sqlx::query!( r#" DELETE FROM partial_blinded_wallet_failure WHERE created < ? diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs index 437e62c8706..12a745641a2 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::credentials::ticketbook::NodeId; -use crate::error::VpnApiError; +use crate::error::CredentialProxyError; use crate::storage::manager::SqliteStorageManager; use crate::storage::models::{BlindedShares, MinimalWalletShare}; use nym_compact_ecash::PublicKeyUser; @@ -37,7 +37,9 @@ pub struct VpnApiStorage { impl VpnApiStorage { #[instrument] - pub async fn init + Debug>(database_path: P) -> Result { + pub async fn init + Debug>( + database_path: P, + ) -> Result { debug!("Attempting to connect to database"); let opts = sqlx::sqlite::SqliteConnectOptions::new() @@ -78,7 +80,7 @@ impl VpnApiStorage { pub(crate) async fn load_blinded_shares_status_by_shares_id( &self, id: i64, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_blinded_shares_status_by_shares_id(id) @@ -88,7 +90,7 @@ impl VpnApiStorage { pub(crate) async fn load_wallet_shares_by_shares_id( &self, id: i64, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_wallet_shares_by_shares_id(id) @@ -98,7 +100,7 @@ impl VpnApiStorage { pub(crate) async fn load_shares_error_by_shares_id( &self, id: i64, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_shares_error_by_device_by_shares_id(id) @@ -110,7 +112,7 @@ impl VpnApiStorage { &self, device_id: &str, credential_id: &str, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_blinded_shares_status_by_device_and_credential_id(device_id, credential_id) @@ -121,7 +123,7 @@ impl VpnApiStorage { &self, device_id: &str, credential_id: &str, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_wallet_shares_by_device_and_credential_id(device_id, credential_id) @@ -132,7 +134,7 @@ impl VpnApiStorage { &self, device_id: &str, credential_id: &str, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { Ok(self .storage_manager .load_shares_error_by_device_and_credential_id(device_id, credential_id) @@ -144,7 +146,7 @@ impl VpnApiStorage { request: Uuid, device_id: &str, credential_id: &str, - ) -> Result { + ) -> Result { Ok(self .storage_manager .insert_new_pending_async_shares_request(request.to_string(), device_id, credential_id) @@ -156,7 +158,7 @@ impl VpnApiStorage { available_shares: usize, device_id: &str, credential_id: &str, - ) -> Result { + ) -> Result { self.storage_manager .update_pending_async_blinded_shares_issued( available_shares as i64, @@ -172,7 +174,7 @@ impl VpnApiStorage { device_id: &str, credential_id: &str, error: &str, - ) -> Result { + ) -> Result { self.storage_manager .update_pending_async_blinded_shares_error( available_shares as i64, @@ -183,7 +185,7 @@ impl VpnApiStorage { .await } - pub(crate) async fn prune_old_blinded_shares(&self) -> Result<(), VpnApiError> { + pub(crate) async fn prune_old_blinded_shares(&self) -> Result<(), CredentialProxyError> { let max_age = OffsetDateTime::now_utc() - time::Duration::days(31); self.storage_manager @@ -205,7 +207,7 @@ impl VpnApiStorage { deposit_amount: Coin, client_ecash_pubkey: &PublicKeyUser, ed22519_keypair: &ed25519::KeyPair, - ) -> Result<(), VpnApiError> { + ) -> Result<(), CredentialProxyError> { debug!("inserting deposit data"); let private_key_bytes = Zeroizing::new(ed22519_keypair.private_key().to_bytes()); @@ -230,8 +232,8 @@ impl VpnApiStorage { epoch_id: EpochId, expiration_date: Date, node_id: NodeId, - res: &Result, - ) -> Result<(), VpnApiError> { + res: &Result, + ) -> Result<(), CredentialProxyError> { debug!("inserting partial wallet share"); let now = OffsetDateTime::now_utc(); @@ -267,7 +269,7 @@ impl VpnApiStorage { pub(crate) async fn get_master_verification_key( &self, epoch_id: EpochId, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { let Some(raw) = self .storage_manager .get_master_verification_key(epoch_id as i64) @@ -278,14 +280,14 @@ impl VpnApiStorage { let deserialised = EpochVerificationKey::try_unpack(&raw.serialised_key, raw.serialization_revision) - .map_err(|err| VpnApiError::database_inconsistency(err.to_string()))?; + .map_err(|err| CredentialProxyError::database_inconsistency(err.to_string()))?; Ok(Some(deserialised)) } pub(crate) async fn insert_master_verification_key( &self, key: &EpochVerificationKey, - ) -> Result<(), VpnApiError> { + ) -> Result<(), CredentialProxyError> { let packed = key.pack(); Ok(self .storage_manager @@ -296,7 +298,7 @@ impl VpnApiStorage { pub(crate) async fn get_master_coin_index_signatures( &self, epoch_id: EpochId, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { let Some(raw) = self .storage_manager .get_master_coin_index_signatures(epoch_id as i64) @@ -309,14 +311,14 @@ impl VpnApiStorage { &raw.serialised_signatures, raw.serialization_revision, ) - .map_err(|err| VpnApiError::database_inconsistency(err.to_string()))?; + .map_err(|err| CredentialProxyError::database_inconsistency(err.to_string()))?; Ok(Some(deserialised)) } pub(crate) async fn insert_master_coin_index_signatures( &self, signatures: &AggregatedCoinIndicesSignatures, - ) -> Result<(), VpnApiError> { + ) -> Result<(), CredentialProxyError> { let packed = signatures.pack(); self.storage_manager .insert_master_coin_index_signatures( @@ -332,7 +334,7 @@ impl VpnApiStorage { &self, expiration_date: Date, epoch_id: EpochId, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { let Some(raw) = self .storage_manager .get_master_expiration_date_signatures(expiration_date, epoch_id as i64) @@ -345,14 +347,14 @@ impl VpnApiStorage { &raw.serialised_signatures, raw.serialization_revision, ) - .map_err(|err| VpnApiError::database_inconsistency(err.to_string()))?; + .map_err(|err| CredentialProxyError::database_inconsistency(err.to_string()))?; Ok(Some(deserialised)) } pub(crate) async fn insert_master_expiration_date_signatures( &self, signatures: &AggregatedExpirationDateSignatures, - ) -> Result<(), VpnApiError> { + ) -> Result<(), CredentialProxyError> { let packed = signatures.pack(); self.storage_manager .insert_master_expiration_date_signatures( diff --git a/nym-credential-proxy/nym-credential-proxy/src/webhook.rs b/nym-credential-proxy/nym-credential-proxy/src/webhook.rs index d32f27c9291..4390269092e 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/webhook.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/webhook.rs @@ -1,7 +1,7 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::VpnApiError; +use crate::error::CredentialProxyError; use clap::Args; use reqwest::header::AUTHORIZATION; use serde::Serialize; @@ -22,9 +22,9 @@ pub struct ZkNymWebHookConfig { } impl ZkNymWebHookConfig { - pub fn ensure_valid_client_url(&self) -> Result<(), VpnApiError> { + pub fn ensure_valid_client_url(&self) -> Result<(), CredentialProxyError> { self.client_url() - .map_err(|_| VpnApiError::InvalidWebhookUrl) + .map_err(|_| CredentialProxyError::InvalidWebhookUrl) .map(|_| ()) } From 2801c5c6bf988e6f648545bdff85bf0ce05ec705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 7 Aug 2025 15:35:26 +0100 Subject: [PATCH 02/10] reorganise deposit flow --- .../src/scheme/keygen.rs | 2 +- .../nym-credential-proxy/Cargo.toml | 4 + .../nym-credential-proxy/build.rs | 11 +- .../migrations/04_buffered_deposits.sql | 76 +++++ .../nym-credential-proxy/src/cli.rs | 14 +- .../src/credentials/ticketbook/mod.rs | 74 +--- .../src/deposits_buffer/helpers.rs | 26 ++ .../src/deposits_buffer/mod.rs | 322 ++++++++++++++++++ .../src/deposits_buffer/refill_task.rs | 59 ++++ .../nym-credential-proxy/src/error.rs | 3 + .../nym-credential-proxy/src/helpers.rs | 24 +- .../src/http/state/mod.rs | 96 ++---- .../src/http/state/required_deposit_cache.rs | 71 ++++ .../nym-credential-proxy/src/main.rs | 7 +- .../src/storage/manager.rs | 176 +++++----- 15 files changed, 736 insertions(+), 229 deletions(-) create mode 100644 nym-credential-proxy/nym-credential-proxy/migrations/04_buffered_deposits.sql create mode 100644 nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/helpers.rs create mode 100644 nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/mod.rs create mode 100644 nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/refill_task.rs create mode 100644 nym-credential-proxy/nym-credential-proxy/src/http/state/required_deposit_cache.rs diff --git a/common/nym_offline_compact_ecash/src/scheme/keygen.rs b/common/nym_offline_compact_ecash/src/scheme/keygen.rs index 8db1597e617..e7f649a685f 100644 --- a/common/nym_offline_compact_ecash/src/scheme/keygen.rs +++ b/common/nym_offline_compact_ecash/src/scheme/keygen.rs @@ -401,7 +401,7 @@ impl Bytable for SecretKeyUser { impl Base58 for SecretKeyUser {} -#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)] pub struct PublicKeyUser { pub(crate) pk: G1Projective, } diff --git a/nym-credential-proxy/nym-credential-proxy/Cargo.toml b/nym-credential-proxy/nym-credential-proxy/Cargo.toml index 79077003844..52ec9c2967a 100644 --- a/nym-credential-proxy/nym-credential-proxy/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy/Cargo.toml @@ -55,9 +55,13 @@ nym-credential-proxy-requests = { path = "../nym-credential-proxy-requests", fea tempfile = { workspace = true } [build-dependencies] +anyhow = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } [features] default = ["cors"] cors = ["tower-http"] + +[lints] +workspace = true \ No newline at end of file diff --git a/nym-credential-proxy/nym-credential-proxy/build.rs b/nym-credential-proxy/nym-credential-proxy/build.rs index bdbdcf0d221..d364c21f504 100644 --- a/nym-credential-proxy/nym-credential-proxy/build.rs +++ b/nym-credential-proxy/nym-credential-proxy/build.rs @@ -1,22 +1,25 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use anyhow::Context; + #[tokio::main] -async fn main() { +async fn main() -> anyhow::Result<()> { use sqlx::{Connection, SqliteConnection}; use std::env; - let out_dir = env::var("OUT_DIR").unwrap(); + let out_dir = env::var("OUT_DIR")?; let database_path = format!("{out_dir}/nym-credential-proxy-example.sqlite"); let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) .await - .expect("Failed to create SQLx database connection"); + .context("Failed to create SQLx database connection")?; sqlx::migrate!("./migrations") .run(&mut conn) .await - .expect("Failed to perform SQLx migrations"); + .context("Failed to perform SQLx migrations")?; println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); + Ok(()) } diff --git a/nym-credential-proxy/nym-credential-proxy/migrations/04_buffered_deposits.sql b/nym-credential-proxy/nym-credential-proxy/migrations/04_buffered_deposits.sql new file mode 100644 index 00000000000..28fb1425559 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/migrations/04_buffered_deposits.sql @@ -0,0 +1,76 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +CREATE TABLE ecash_deposit +( + -- id assigned [by the contract] to the deposit + deposit_id INTEGER PRIMARY KEY NOT NULL, + + -- associated tx hash + deposit_tx_hash TEXT NOT NULL, + + -- indication of when the deposit request has been created + -- (so that based on block timestamp we could potentially determine latency) + requested_on TIMESTAMP WITHOUT TIME ZONE NOT NULL, + + -- the amount put in the deposit (informative, as we expect this to change in the future) + deposit_amount TEXT NOT NULL, + + -- the private key generated for the purposes of the deposit (the public component has been put in the transaction) + ed25519_deposit_private_key BLOB NOT NULL +); + + +INSERT INTO ecash_deposit(deposit_id, deposit_tx_hash, requested_on, deposit_amount, ed25519_deposit_private_key) +SELECT deposit_id, deposit_tx_hash, requested_on, deposit_amount, ed25519_deposit_private_key +FROM ticketbook_deposit; + + +CREATE TABLE ecash_deposit_usage +( + deposit_id INTEGER PRIMARY KEY REFERENCES ecash_deposit (deposit_id), + ticketbooks_requested_on TIMESTAMP WITHOUT TIME ZONE NOT NULL, + client_pubkey BLOB NOT NULL, + request_uuid TEXT UNIQUE NOT NULL, + + -- this has to be improved later on to resume issuance or something, but for now it's fine + ticketbook_request_error TEXT +); + +INSERT INTO ecash_deposit_usage(deposit_id, ticketbooks_requested_on, client_pubkey, request_uuid) +SELECT deposit_id, 0, client_pubkey, request_uuid +FROM ticketbook_deposit; + + +CREATE TABLE partial_blinded_wallet_new +( + corresponding_deposit INTEGER NOT NULL REFERENCES ecash_deposit_usage (deposit_id), + epoch_id INTEGER NOT NULL, + expiration_date DATE NOT NULL, + node_id INTEGER NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + blinded_signature BLOB NOT NULL +); + +CREATE TABLE partial_blinded_wallet_failure_new +( + corresponding_deposit INTEGER NOT NULL REFERENCES ecash_deposit_usage (deposit_id), + epoch_id INTEGER NOT NULL, + expiration_date DATE NOT NULL, + node_id INTEGER NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + failure_message TEXT NOT NULL +); + +INSERT INTO partial_blinded_wallet_new +SELECT * +from partial_blinded_wallet; +INSERT INTO partial_blinded_wallet_failure_new +SELECT * +from partial_blinded_wallet_failure; + +DROP TABLE partial_blinded_wallet; +DROP TABLE partial_blinded_wallet_failure; + diff --git a/nym-credential-proxy/nym-credential-proxy/src/cli.rs b/nym-credential-proxy/nym-credential-proxy/src/cli.rs index cf5b25cf230..bb76744a10e 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/cli.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/cli.rs @@ -64,6 +64,15 @@ pub struct Cli { )] pub(crate) max_concurrent_deposits: usize, + /// Specify the size of the deposits buffer the credential proxy should have available at any time + /// (default: 256) + #[clap( + long, + env = "NYM_CREDENTIAL_PROXY_DEPOSITS_BUFFER", + default_value_t = 256 + )] + pub(crate) deposits_buffer_size: usize, + #[clap(long, env = "NYM_CREDENTIAL_PROXY_PERSISTENT_STORAGE_STORAGE")] pub(crate) persistent_storage_path: Option, } @@ -90,10 +99,7 @@ impl Cli { create_dir_all(parent).unwrap(); } - info!( - "setting the storage path path to {}", - default_path.display() - ); + info!("setting the storage path to {}", default_path.display()); default_path }) diff --git a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs index 3f18b418b04..cab4421212f 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs @@ -1,7 +1,6 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::deposit_maker::{DepositRequest, DepositResponse}; use crate::error::CredentialProxyError; use crate::http::state::ApiState; use crate::storage::models::BlindedShares; @@ -11,52 +10,20 @@ use nym_credential_proxy_requests::api::v1::ticketbook::models::{ TicketbookWalletSharesResponse, WalletShare, WebhookTicketbookWalletShares, WebhookTicketbookWalletSharesRequest, }; -use nym_credentials::IssuanceTicketBook; use nym_credentials_interface::Base58; -use nym_crypto::asymmetric::ed25519; use nym_validator_client::ecash::BlindSignRequestBody; -use nym_validator_client::nyxd::Coin; -use rand::rngs::OsRng; use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; use time::OffsetDateTime; -use tokio::sync::{oneshot, Mutex}; -use tokio::time::{timeout, Instant}; -use tracing::{debug, error, info, instrument, warn}; +use tokio::sync::Mutex; +use tokio::time::timeout; +use tracing::{debug, error, info, instrument}; use uuid::Uuid; // use the same type alias as our contract without importing the whole thing just for this single line pub type NodeId = u64; -#[instrument(skip(state), ret, err(Display))] -async fn make_deposit( - state: &ApiState, - pub_key: ed25519::PublicKey, - deposit_amount: &Coin, -) -> Result { - let start = Instant::now(); - let (on_done_tx, on_done_rx) = oneshot::channel(); - let request = DepositRequest::new(pub_key, deposit_amount, on_done_tx); - state.request_deposit(request).await; - - let time_taken = start.elapsed(); - let formatted = humantime::format_duration(time_taken); - - let Ok(deposit_response) = on_done_rx.await else { - error!("failed to receive deposit response: the corresponding sender channel got dropped by the DepositMaker!"); - return Err(CredentialProxyError::DepositFailure); - }; - - if time_taken > Duration::from_secs(20) { - warn!("attempting to resolve deposit request took {formatted}. perhaps the buffer is too small or the process/chain is overloaded?") - } else { - debug!("attempting to resolve deposit request took {formatted}") - } - - deposit_response.ok_or(CredentialProxyError::DepositFailure) -} - #[instrument( skip(state, request_data, request, requested_on), fields( @@ -70,12 +37,12 @@ pub(crate) async fn try_obtain_wallet_shares( requested_on: OffsetDateTime, request_data: TicketbookRequest, ) -> Result, CredentialProxyError> { - let mut rng = OsRng; - - let ed25519_keypair = ed25519::KeyPair::new(&mut rng); + // don't proceed if we don't have quorum available as the request will definitely fail + if !state.quorum_available().await { + return Err(CredentialProxyError::UnavailableSigningQuorum); + } let epoch = state.current_epoch_id().await?; - let deposit_amount = state.deposit_amount().await?; let threshold = state.ecash_threshold(epoch).await?; let expiration_date = request_data.expiration_date; @@ -87,30 +54,11 @@ pub(crate) async fn try_obtain_wallet_shares( .await?; let ecash_api_clients = state.ecash_clients(epoch).await?.clone(); - let DepositResponse { - deposit_id, - tx_hash, - } = make_deposit(state, *ed25519_keypair.public_key(), &deposit_amount).await?; - - info!(deposit_id = %deposit_id, "deposit finished"); - - // store the deposit information so if we fail, we could perhaps still reuse it for another issuance - state - .storage() - .insert_deposit_data( - deposit_id, - tx_hash, - requested_on, - request, - deposit_amount, - &request_data.ecash_pubkey, - &ed25519_keypair, - ) + let deposit_data = state + .get_deposit(request, requested_on, request_data.ecash_pubkey) .await?; - - let plaintext = - IssuanceTicketBook::request_plaintext(&request_data.withdrawal_request, deposit_id); - let signature = ed25519_keypair.private_key().sign(plaintext); + let deposit_id = deposit_data.deposit_id; + let signature = deposit_data.sign_ticketbook_plaintext(&request_data.withdrawal_request); let credential_request = BlindSignRequestBody::new( request_data.withdrawal_request.into(), diff --git a/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/helpers.rs new file mode 100644 index 00000000000..796660f9c6a --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/helpers.rs @@ -0,0 +1,26 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub(super) fn request_sizes(total: usize, max_request_size: usize) -> impl Iterator { + (0..total) + .step_by(max_request_size) + .map(move |start| std::cmp::min(max_request_size, total - start)) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_sizes_test() { + assert_eq!( + request_sizes(100, 32).collect::>(), + vec![32, 32, 32, 4] + ); + + assert_eq!(request_sizes(10, 32).collect::>(), vec![10]); + assert_eq!(request_sizes(32, 32).collect::>(), vec![32]); + assert_eq!(request_sizes(33, 32).collect::>(), vec![32, 1]); + assert_eq!(request_sizes(1, 32).collect::>(), vec![1]); + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/mod.rs new file mode 100644 index 00000000000..49784f15717 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/mod.rs @@ -0,0 +1,322 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::deposits_buffer::helpers::request_sizes; +use crate::deposits_buffer::refill_task::RefillTask; +use crate::error::CredentialProxyError; +use crate::http::state::required_deposit_cache::RequiredDepositCache; +use crate::http::state::ChainClient; +use crate::storage::VpnApiStorage; +use nym_compact_ecash::{PublicKeyUser, WithdrawalRequest}; +use nym_credentials::IssuanceTicketBook; +use nym_crypto::asymmetric::ed25519; +use nym_ecash_contract_common::deposit::DepositId; +use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData; +use nym_validator_client::nyxd::Coin; +use rand::rngs::OsRng; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::sync::Mutex as AsyncMutex; +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info, instrument, warn}; +use uuid::Uuid; + +mod helpers; +mod refill_task; + +// TODO: I guess make it configurable +const DEPOSITS_THRESHOLD_P: f32 = 0.1; + +pub(crate) struct BufferedDeposit { + pub(crate) deposit_id: u32, + // note: this type implements `ZeroizeOnDrop` + pub(crate) ed25519_private_key: ed25519::PrivateKey, + pub(crate) ed25519_public_key: ed25519::PublicKey, +} + +impl BufferedDeposit { + fn new(deposit_id: u32, keys: (ed25519::PrivateKey, ed25519::PublicKey)) -> Self { + BufferedDeposit { + deposit_id, + ed25519_private_key: keys.0, + ed25519_public_key: keys.1, + } + } + + pub(crate) fn sign_ticketbook_plaintext( + &self, + withdrawal_request: &WithdrawalRequest, + ) -> ed25519::Signature { + let plaintext = IssuanceTicketBook::request_plaintext(withdrawal_request, self.deposit_id); + self.ed25519_private_key.sign(plaintext) + } +} + +struct DepositsBufferInner { + client: ChainClient, + + required_deposit_cache: RequiredDepositCache, + + storage: VpnApiStorage, + target_amount: usize, + max_concurrent_deposits: usize, + unused_deposits: AsyncMutex>, + + deposits_refill_task: RefillTask, + short_sha: &'static str, + cancellation_token: CancellationToken, +} + +#[derive(Clone)] +pub(crate) struct DepositsBuffer { + inner: Arc, +} + +impl DepositsBuffer { + pub(crate) async fn new( + storage: VpnApiStorage, + client: ChainClient, + short_sha: &'static str, + cancellation_token: CancellationToken, + ) -> Result { + todo!("load all unused deposits from the storage") + } + + async fn deposit_amount(&self) -> Result { + self.inner + .required_deposit_cache + .get_or_update(&self.inner.client) + .await + } + + #[instrument(skip(self), err(Display))] + async fn make_deposits_request( + &self, + amount: usize, + ) -> Result, CredentialProxyError> { + let chain_write_permit = self.inner.client.start_chain_tx().await; + let mut rng = OsRng; + + let deposit_amount = self.deposit_amount().await?; + let keys = (0..amount) + .map(|_| { + let private_key = ed25519::PrivateKey::new(&mut rng); + let public_key: ed25519::PublicKey = (&private_key).into(); + (private_key, public_key) + }) + .collect::>(); + + info!("starting {amount} deposits"); + let mut contents = Vec::new(); + for (_, pub_key) in &keys { + contents.push((pub_key.to_base58_string(), deposit_amount.clone())); + } + + let execute_res = chain_write_permit + .make_deposits(self.inner.short_sha, contents) + .await?; + + let tx_hash = execute_res.transaction_hash; + info!("{amount} deposits made in transaction: {tx_hash}"); + + let contract_data = match execute_res.to_contract_data() { + Ok(contract_data) => contract_data, + Err(err) => { + // that one is tricky. deposits technically got made, but we somehow failed to parse response, + // in this case terminate the proxy with 0 exit code so it wouldn't get automatically restarted + // because it requires some serious MANUAL intervention + error!("CRITICAL FAILURE: failed to parse out deposit information from the contract transaction. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually. error was: {err}"); + self.inner.cancellation_token.cancel(); + return Err(CredentialProxyError::DepositFailure); + } + }; + + if contract_data.len() != amount { + // another critical failure, that one should be quite impossible and thus has to be manually inspected + error!("CRITICAL FAILURE: failed to parse out all deposit information from the contract transaction. got {} responses while we sent {amount} deposits! either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually", contract_data.len()); + self.inner.cancellation_token.cancel(); + return Err(CredentialProxyError::DepositFailure); + } + + let mut buffered_deposits = Vec::new(); + for (keys, response) in keys.into_iter().zip(contract_data) { + let response_index = response.message_index; + let deposit_id = match response.parse_singleton_u32_contract_data() { + Ok(deposit_id) => deposit_id, + Err(err) => { + // another impossibility + error!("CRITICAL FAILURE: failed to parse out deposit id out of the response at index {response_index}: {err}. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually"); + self.inner.cancellation_token.cancel(); + return Err(CredentialProxyError::DepositFailure); + } + }; + + buffered_deposits.push(BufferedDeposit::new(deposit_id, keys)); + } + + Ok(buffered_deposits) + } + + async fn insert_new_deposits( + &self, + mut deposits: Vec, + ) -> Result<(), CredentialProxyError> { + // 1. insert into the db + // self.inner.storage.insert_new_deposits(...) + todo!(); + + // 2. update the buffer + self.inner + .unused_deposits + .lock() + .await + .append(&mut deposits); + Ok(()) + } + + /// Start refilling our deposit buffer. + /// It chunks the amount required based on the configured maximum request size + /// and updates global state after each successful transaction. + async fn refill_deposits(&self) -> Result<(), CredentialProxyError> { + let available = self.inner.unused_deposits.lock().await.len(); + + let target = self.deposits_upper_threshold(); + let to_request = target - available; + + for request_chunk in request_sizes(to_request, self.inner.max_concurrent_deposits) { + // note: we check for cancellation between individual requests + // as opposed to wrapping that in tokio::select! so that we would never abandon chain operations + // as we wouldn't want to lose funds + if self.inner.cancellation_token.is_cancelled() { + info!("received cancellation during deposits refilling"); + return Ok(()); + } + + // make sure to insert deposits into db/vec as we get them so on initial run, + // we'd start trickling down data as soon as posible + let deposits = self.make_deposits_request(request_chunk).await?; + self.insert_new_deposits(deposits).await?; + } + + Ok(()) + } + + // if we're here, we know we're below the threshold + fn maybe_refill_deposits(&self) { + if let Some(mut guard) = self.inner.deposits_refill_task.try_get_new_task_guard() { + let this = self.clone(); + *guard = Some(tokio::spawn(async move { this.refill_deposits().await })); + } + } + + fn deposits_lower_threshold(&self) -> usize { + self.inner.target_amount - (self.inner.target_amount as f32 * DEPOSITS_THRESHOLD_P) as usize + } + + fn deposits_upper_threshold(&self) -> usize { + self.inner.target_amount + (self.inner.target_amount as f32 * DEPOSITS_THRESHOLD_P) as usize + } + + async fn mark_deposit_as_used( + &self, + deposit_id: DepositId, + request_uuid: Uuid, + requested_on: OffsetDateTime, + client_pubkey: PublicKeyUser, + ) -> Result<(), CredentialProxyError> { + // self.inner.storage.mark_deposit_as_used(deposit_id).await + todo!() + } + + pub(crate) async fn return_deposit( + &self, + deposit: BufferedDeposit, + ) -> Result<(), CredentialProxyError> { + // self.inner.storage.reset_deposit_usage(deposit.deposit_id).await?; + // self.inner.unused_deposits.lock().await.push(deposit); + // Ok(()) + todo!() + } + + async fn wait_for_deposit( + &self, + request_uuid: Uuid, + requested_on: OffsetDateTime, + client_pubkey: PublicKeyUser, + ) -> Result { + loop { + tokio::time::sleep(Duration::from_millis(500)).await; + if let Some(buffered_deposit) = self.inner.unused_deposits.lock().await.pop() { + // if the db call fails, we technically don't lose the deposit (we'll 'recover' it on restart) + self.mark_deposit_as_used( + buffered_deposit.deposit_id, + request_uuid, + requested_on, + client_pubkey, + ) + .await?; + return Ok(buffered_deposit); + } else { + // make sure there's always a task working in the background in case deposits get used up too quickly + self.maybe_refill_deposits() + } + } + } + + pub(crate) async fn get_valid_deposit( + &self, + request_uuid: Uuid, + requested_on: OffsetDateTime, + client_pubkey: PublicKeyUser, + ) -> Result { + let mut deposits_guard = self.inner.unused_deposits.lock().await; + let deposits_available = deposits_guard.len(); + + debug!("we have {deposits_available} unused deposits available"); + + let maybe_deposit = deposits_guard.pop(); + drop(deposits_guard); + + if deposits_available < self.deposits_lower_threshold() { + // if we're below threshold, start refill task + self.maybe_refill_deposits() + } + + match maybe_deposit { + None => { + warn!("we currently don't have any usable deposits! are we using them up faster than we request them?"); + + // we have to wait until refill task has completed (either initiated by this or another fn call) + self.wait_for_deposit(request_uuid, requested_on, client_pubkey) + .await + } + Some(buffered_deposit) => { + self.mark_deposit_as_used( + buffered_deposit.deposit_id, + request_uuid, + requested_on, + client_pubkey, + ) + .await?; + Ok(buffered_deposit) + } + } + } + + pub(crate) async fn wait_for_shutdown(&self) { + let task_handle = self.inner.deposits_refill_task.take_task_join_handle(); + if let Some(task_handle) = task_handle { + if !task_handle.is_finished() { + info!("the deposit refill task is currently in progress - waiting for the current transaction to finish before concluding shutdown"); + let _ = task_handle.await; + } + } + } +} + +impl DepositsBufferInner { + // +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/refill_task.rs b/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/refill_task.rs new file mode 100644 index 00000000000..c81cb6d1c97 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/refill_task.rs @@ -0,0 +1,59 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::CredentialProxyError; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Mutex as StdMutex, MutexGuard}; +use tokio::task::JoinHandle; +use tracing::{debug, error}; + +pub(super) type RefillTaskResult = Result<(), CredentialProxyError>; + +pub(super) struct RefillTask { + // note that we can only have a single transaction in progress (or it'd mess up with our sequence numbers) + // if we find that we're using up deposits more quickly than we're refilling them, + // we'll have to increase the number of deposits per transaction + join_handle: StdMutex>>, + + in_progress: AtomicBool, +} + +impl RefillTask { + pub(super) fn in_progress(&self) -> bool { + self.in_progress.load(Ordering::SeqCst) + } + + /// Attempt to set the `in_progress` value to `true` if it's not already `true`. + /// Returns boolean indicating whether it was successful + fn try_set_in_progress(&self) -> bool { + self.in_progress + .compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) + .is_ok() + } + + pub(super) fn try_get_new_task_guard( + &self, + ) -> Option>>> { + // sanity check for concurrent request + if !self.try_set_in_progress() { + debug!("another task has already started deposit refill request"); + return None; + } + + #[allow(clippy::expect_used)] + let guard = self.join_handle.lock().expect("mutex got poisoned"); + + if let Some(existing_handle) = guard.as_ref() { + if !existing_handle.is_finished() { + error!("CRITICAL BUG: there was already a deposit refill task spawned that hasn't yet finished") + } + } + + Some(guard) + } + + pub(super) fn take_task_join_handle(&self) -> Option> { + #[allow(clippy::expect_used)] + self.join_handle.lock().expect("mutex got poisoned").take() + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/error.rs b/nym-credential-proxy/nym-credential-proxy/src/error.rs index 0a8d11677d5..a63340e05b8 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/error.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/error.rs @@ -118,6 +118,9 @@ pub enum CredentialProxyError { #[error("failed to create deposit")] DepositFailure, + + #[error("can't obtain sufficient number of credential shares due to unavailable quorum")] + UnavailableSigningQuorum, } impl CredentialProxyError { diff --git a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs index 35da4e8f233..212f3007748 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs @@ -6,9 +6,9 @@ use time::OffsetDateTime; use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, warn}; +use crate::deposits_buffer::DepositsBuffer; use crate::{ cli::Cli, - deposit_maker::DepositMaker, error::CredentialProxyError, http::{ state::{ApiState, ChainClient}, @@ -77,6 +77,7 @@ pub async fn wait_for_signal() { } } +#[allow(clippy::panic)] fn build_sha_short() -> &'static str { let bin_info = bin_info!(); if bin_info.commit_sha.len() < 7 { @@ -102,19 +103,27 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> { let chain_client = ChainClient::new(mnemonic)?; let cancellation_token = CancellationToken::new(); - let deposit_maker = DepositMaker::new( - build_sha_short(), + let deposits_buffer = DepositsBuffer::new( + storage.clone(), chain_client.clone(), - cli.max_concurrent_deposits, + build_sha_short(), cancellation_token.clone(), - ); + ) + .await?; + + // let deposit_maker = DepositMaker::new( + // build_sha_short(), + // chain_client.clone(), + // cli.max_concurrent_deposits, + // cancellation_token.clone(), + // ); - let deposit_request_sender = deposit_maker.deposit_request_sender(); + // let deposit_request_sender = deposit_maker.deposit_request_sender(); let api_state = ApiState::new( storage.clone(), webhook_cfg, chain_client, - deposit_request_sender, + deposits_buffer, cancellation_token.clone(), ) .await?; @@ -129,7 +138,6 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> { // spawn all the tasks api_state.try_spawn(http_server.run_forever()); api_state.try_spawn(storage_pruner.run_forever()); - api_state.try_spawn(deposit_maker.run_forever()); // wait for cancel signal (SIGINT, SIGTERM or SIGQUIT) wait_for_signal().await; diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs index ba15c0b7f0d..1361a2c0680 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs @@ -1,9 +1,10 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::deposit_maker::{DepositRequest, DepositRequestSender}; +use crate::deposits_buffer::{BufferedDeposit, DepositsBuffer}; use crate::error::CredentialProxyError; use crate::helpers::LockTimer; +use crate::http::state::required_deposit_cache::RequiredDepositCache; use crate::http::types::RequestError; use crate::nym_api_helpers::{ ensure_sane_expiration_date, query_all_threshold_apis, CachedEpoch, CachedImmutableEpochItem, @@ -19,7 +20,7 @@ use nym_compact_ecash::scheme::coin_indices_signatures::{ use nym_compact_ecash::scheme::expiration_date_signatures::{ aggregate_annotated_expiration_signatures, ExpirationDateSignatureShare, }; -use nym_compact_ecash::Base58; +use nym_compact_ecash::{Base58, PublicKeyUser}; use nym_credential_proxy_requests::api::v1::ticketbook::models::{ AggregatedCoinIndicesSignaturesResponse, AggregatedExpirationDateSignaturesResponse, MasterVerificationKeyResponse, @@ -34,7 +35,7 @@ use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch; use nym_validator_client::nyxd::contract_traits::{ - DkgQueryClient, EcashQueryClient, NymContractsProvider, PagedDkgQueryClient, + DkgQueryClient, NymContractsProvider, PagedDkgQueryClient, }; use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult; use nym_validator_client::nyxd::{Coin, CosmWasmClient, NyxdClient}; @@ -49,9 +50,11 @@ use tokio::task::JoinHandle; use tokio::time::Instant; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker; -use tracing::{debug, info, warn}; +use tracing::{debug, info, instrument, warn}; use uuid::Uuid; +pub(crate) mod required_deposit_cache; + // currently we need to hold our keypair so that we could request a freepass credential #[derive(Clone)] pub struct ApiState { @@ -65,7 +68,7 @@ impl ApiState { storage: VpnApiStorage, zk_nym_web_hook_config: ZkNymWebHookConfig, client: ChainClient, - deposit_requester: DepositRequestSender, + deposits_buffer: DepositsBuffer, cancellation_token: CancellationToken, ) -> Result { let state = ApiState { @@ -75,7 +78,7 @@ impl ApiState { ecash_state: EcashState::default(), zk_nym_web_hook_config, task_tracker: TaskTracker::new(), - deposit_requester, + deposits_buffer, cancellation_token, }), }; @@ -123,6 +126,7 @@ impl ApiState { pub(crate) async fn cancel_and_wait(&self) { self.inner.cancellation_token.cancel(); + self.inner.deposits_buffer.wait_for_shutdown().await; self.inner.task_tracker.wait().await } @@ -130,6 +134,10 @@ impl ApiState { &self.inner.zk_nym_web_hook_config } + pub(crate) async fn quorum_available(&self) -> bool { + todo!() + } + async fn ensure_credentials_issuable(&self) -> Result<(), CredentialProxyError> { let epoch = self.current_epoch().await?; @@ -156,23 +164,11 @@ impl ApiState { } pub async fn deposit_amount(&self) -> Result { - let read_guard = self.inner.ecash_state.required_deposit_cache.read().await; - if read_guard.is_valid() { - return Ok(read_guard.required_amount.clone()); - } - - // update cache - drop(read_guard); - let mut write_guard = self.inner.ecash_state.required_deposit_cache.write().await; - let deposit_amount = self - .query_chain() + self.inner + .ecash_state + .required_deposit_cache + .get_or_update(&self.inner.client) .await - .get_required_deposit_amount() - .await?; - - write_guard.update(deposit_amount.clone().into()); - - Ok(deposit_amount.into()) } async fn current_epoch(&self) -> Result { @@ -209,17 +205,28 @@ impl ApiState { self.inner.client.query_chain().await } - pub(crate) async fn request_deposit(&self, request: DepositRequest) { + pub(crate) async fn get_deposit( + &self, + request_uuid: Uuid, + requested_on: OffsetDateTime, + client_pubkey: PublicKeyUser, + ) -> Result { let start = Instant::now(); - self.inner.deposit_requester.request_deposit(request).await; + let deposit = self + .inner + .deposits_buffer + .get_valid_deposit(request_uuid, requested_on, client_pubkey) + .await; let time_taken = start.elapsed(); let formatted = humantime::format_duration(time_taken); if time_taken > Duration::from_secs(10) { - warn!("attempting to push new deposit request onto the queue took {formatted}. perhaps the buffer is too small or the process/chain is overloaded?") + warn!("attempting to get buffered deposit took {formatted}. perhaps the buffer is too small or the process/chain is overloaded?") } else { - debug!("attempting to push new deposit request onto the queue took {formatted}") - } + debug!("attempting to get buffered deposit took {formatted}") + }; + + deposit } pub(crate) async fn global_data( @@ -647,7 +654,7 @@ struct ApiStateInner { client: ChainClient, - deposit_requester: DepositRequestSender, + deposits_buffer: DepositsBuffer, zk_nym_web_hook_config: ZkNymWebHookConfig, @@ -658,39 +665,9 @@ struct ApiStateInner { cancellation_token: CancellationToken, } -pub(crate) struct CachedDeposit { - valid_until: OffsetDateTime, - required_amount: Coin, -} - -impl CachedDeposit { - const MAX_VALIDITY: time::Duration = time::Duration::MINUTE; - - fn is_valid(&self) -> bool { - self.valid_until > OffsetDateTime::now_utc() - } - - fn update(&mut self, required_amount: Coin) { - self.valid_until = OffsetDateTime::now_utc() + Self::MAX_VALIDITY; - self.required_amount = required_amount; - } -} - -impl Default for CachedDeposit { - fn default() -> Self { - CachedDeposit { - valid_until: OffsetDateTime::UNIX_EPOCH, - required_amount: Coin { - amount: u128::MAX, - denom: "unym".to_string(), - }, - } - } -} - #[derive(Default)] pub(crate) struct EcashState { - pub(crate) required_deposit_cache: RwLock, + pub(crate) required_deposit_cache: RequiredDepositCache, pub(crate) cached_epoch: RwLock, @@ -717,6 +694,7 @@ pub(crate) struct ChainWritePermit<'a> { } impl ChainWritePermit<'_> { + #[instrument(skip(self, short_sha, info), err(Display))] pub(crate) async fn make_deposits( self, short_sha: &'static str, diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/state/required_deposit_cache.rs b/nym-credential-proxy/nym-credential-proxy/src/http/state/required_deposit_cache.rs new file mode 100644 index 00000000000..a4c429de53f --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/http/state/required_deposit_cache.rs @@ -0,0 +1,71 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::CredentialProxyError; +use crate::http::state::ChainClient; +use nym_validator_client::nyxd::contract_traits::EcashQueryClient; +use nym_validator_client::nyxd::Coin; +use std::sync::Arc; +use time::OffsetDateTime; +use tokio::sync::RwLock; + +pub(crate) struct CachedDeposit { + valid_until: OffsetDateTime, + required_amount: Coin, +} + +impl CachedDeposit { + const MAX_VALIDITY: time::Duration = time::Duration::MINUTE; + + fn is_valid(&self) -> bool { + self.valid_until > OffsetDateTime::now_utc() + } + + fn update(&mut self, required_amount: Coin) { + self.valid_until = OffsetDateTime::now_utc() + Self::MAX_VALIDITY; + self.required_amount = required_amount; + } +} + +impl Default for CachedDeposit { + fn default() -> Self { + CachedDeposit { + valid_until: OffsetDateTime::UNIX_EPOCH, + required_amount: Coin { + amount: u128::MAX, + denom: "unym".to_string(), + }, + } + } +} + +#[derive(Clone, Default)] +pub(crate) struct RequiredDepositCache { + inner: Arc>, +} + +impl RequiredDepositCache { + pub(crate) async fn get_or_update( + &self, + chain_client: &ChainClient, + ) -> Result { + let read_guard = self.inner.read().await; + if read_guard.is_valid() { + return Ok(read_guard.required_amount.clone()); + } + + // update cache + drop(read_guard); + let mut write_guard = self.inner.write().await; + let deposit_amount = chain_client + .query_chain() + .await + .get_required_deposit_amount() + .await?; + + let nym_coin: Coin = deposit_amount.into(); + + write_guard.update(nym_coin.clone()); + Ok(nym_coin) + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/main.rs b/nym-credential-proxy/nym-credential-proxy/src/main.rs index 1ca9ccf3162..7872c675cb1 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/main.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/main.rs @@ -1,10 +1,7 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -#![warn(clippy::expect_used)] -#![warn(clippy::unwrap_used)] -#![warn(clippy::todo)] -#![warn(clippy::dbg_macro)] +mod deposits_buffer; cfg_if::cfg_if! { if #[cfg(unix)] { @@ -18,7 +15,7 @@ cfg_if::cfg_if! { pub mod cli; pub mod config; pub mod credentials; - mod deposit_maker; + // mod deposit_maker; pub mod error; pub mod helpers; pub mod http; diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs index ef115a6c12f..2bba05d6538 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs @@ -37,21 +37,22 @@ impl SqliteStorageManager { &self, id: i64, ) -> Result, sqlx::Error> { - sqlx::query_as!( - MinimalWalletShare, - r#" - SELECT t1.node_id, t1.blinded_signature, t1.epoch_id, t1.expiration_date as "expiration_date!: Date" - FROM partial_blinded_wallet as t1 - JOIN ticketbook_deposit as t2 - on t1.corresponding_deposit = t2.deposit_id - JOIN blinded_shares as t3 - ON t2.request_uuid = t3.request_uuid - WHERE t3.id = ?; - "#, - id - ) - .fetch_all(&self.connection_pool) - .await + todo!() + // sqlx::query_as!( + // MinimalWalletShare, + // r#" + // SELECT t1.node_id, t1.blinded_signature, t1.epoch_id, t1.expiration_date as "expiration_date!: Date" + // FROM partial_blinded_wallet as t1 + // JOIN ticketbook_deposit as t2 + // on t1.corresponding_deposit = t2.deposit_id + // JOIN blinded_shares as t3 + // ON t2.request_uuid = t3.request_uuid + // WHERE t3.id = ?; + // "#, + // id + // ) + // .fetch_all(&self.connection_pool) + // .await } pub(crate) async fn load_shares_error_by_device_by_shares_id( @@ -97,26 +98,27 @@ impl SqliteStorageManager { credential_id: &str, ) -> Result, sqlx::Error> { // https://docs.rs/sqlx/latest/sqlx/macro.query.html#force-a-differentcustom-type - sqlx::query_as!( - MinimalWalletShare, - r#" - SELECT - t1.node_id as "node_id!", - t1.blinded_signature as "blinded_signature!", - t1.epoch_id as "epoch_id!", - t1.expiration_date as "expiration_date!: Date" - FROM partial_blinded_wallet as t1 - JOIN ticketbook_deposit as t2 - on t1.corresponding_deposit = t2.deposit_id - JOIN blinded_shares as t3 - ON t2.request_uuid = t3.request_uuid - WHERE t3.device_id = ? AND t3.credential_id = ?; - "#, - device_id, - credential_id - ) - .fetch_all(&self.connection_pool) - .await + todo!() + // sqlx::query_as!( + // MinimalWalletShare, + // r#" + // SELECT + // t1.node_id as "node_id!", + // t1.blinded_signature as "blinded_signature!", + // t1.epoch_id as "epoch_id!", + // t1.expiration_date as "expiration_date!: Date" + // FROM partial_blinded_wallet as t1 + // JOIN ticketbook_deposit as t2 + // on t1.corresponding_deposit = t2.deposit_id + // JOIN blinded_shares as t3 + // ON t2.request_uuid = t3.request_uuid + // WHERE t3.device_id = ? AND t3.credential_id = ?; + // "#, + // device_id, + // credential_id + // ) + // .fetch_all(&self.connection_pool) + // .await } pub(crate) async fn load_shares_error_by_device_and_credential_id( @@ -237,30 +239,32 @@ impl SqliteStorageManager { &self, delete_after: OffsetDateTime, ) -> Result<(), CredentialProxyError> { - sqlx::query!( - r#" - DELETE FROM partial_blinded_wallet WHERE created < ? - "#, - delete_after, - ) - .execute(&self.connection_pool) - .await?; - Ok(()) + todo!() + // sqlx::query!( + // r#" + // DELETE FROM partial_blinded_wallet WHERE created < ? + // "#, + // delete_after, + // ) + // .execute(&self.connection_pool) + // .await?; + // Ok(()) } pub(crate) async fn prune_old_partial_blinded_wallet_failures( &self, delete_after: OffsetDateTime, ) -> Result<(), CredentialProxyError> { - sqlx::query!( - r#" - DELETE FROM partial_blinded_wallet_failure WHERE created < ? - "#, - delete_after, - ) - .execute(&self.connection_pool) - .await?; - Ok(()) + todo!() + // sqlx::query!( + // r#" + // DELETE FROM partial_blinded_wallet_failure WHERE created < ? + // "#, + // delete_after, + // ) + // .execute(&self.connection_pool) + // .await?; + // Ok(()) } pub(crate) async fn get_master_verification_key( @@ -409,22 +413,23 @@ impl SqliteStorageManager { created: OffsetDateTime, share: &[u8], ) -> Result<(), sqlx::Error> { - sqlx::query!( - r#" - INSERT INTO partial_blinded_wallet(corresponding_deposit, epoch_id, expiration_date, node_id, created, blinded_signature) - VALUES (?, ?, ?, ?, ?, ?) - "#, - deposit_id, - epoch_id, - expiration_date, - node_id, - created, - share - ) - .execute(&self.connection_pool) - .await?; - - Ok(()) + todo!() + // sqlx::query!( + // r#" + // INSERT INTO partial_blinded_wallet(corresponding_deposit, epoch_id, expiration_date, node_id, created, blinded_signature) + // VALUES (?, ?, ?, ?, ?, ?) + // "#, + // deposit_id, + // epoch_id, + // expiration_date, + // node_id, + // created, + // share + // ) + // .execute(&self.connection_pool) + // .await?; + // + // Ok(()) } pub(crate) async fn insert_partial_wallet_issuance_failure( @@ -436,21 +441,22 @@ impl SqliteStorageManager { created: OffsetDateTime, failure_message: String, ) -> Result<(), sqlx::Error> { - sqlx::query!( - r#" - INSERT INTO partial_blinded_wallet_failure(corresponding_deposit, epoch_id, expiration_date, node_id, created, failure_message) - VALUES (?, ?, ?, ?, ?, ?) - "#, - deposit_id, - epoch_id, - expiration_date, - node_id, - created, - failure_message - ) - .execute(&self.connection_pool) - .await?; - - Ok(()) + todo!() + // sqlx::query!( + // r#" + // INSERT INTO partial_blinded_wallet_failure(corresponding_deposit, epoch_id, expiration_date, node_id, created, failure_message) + // VALUES (?, ?, ?, ?, ?, ?) + // "#, + // deposit_id, + // epoch_id, + // expiration_date, + // node_id, + // created, + // failure_message + // ) + // .execute(&self.connection_pool) + // .await?; + // + // Ok(()) } } From d020ac3833fda2dbccb990e30cf4899daf213e3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 11 Aug 2025 14:35:07 +0100 Subject: [PATCH 03/10] updated sql tables et al. --- .../migrations/04_buffered_deposits.sql | 9 +- .../src/deposits_buffer/helpers.rs | 75 +++++ .../src/deposits_buffer/mod.rs | 118 ++++---- .../src/deposits_buffer/refill_task.rs | 5 +- .../nym-credential-proxy/src/helpers.rs | 18 +- .../src/http/state/mod.rs | 33 ++- .../src/storage/manager.rs | 279 ++++++++++-------- .../nym-credential-proxy/src/storage/mod.rs | 118 +++++--- .../src/storage/models.rs | 47 ++- .../nym-credential-proxy/src/tasks.rs | 6 +- 10 files changed, 444 insertions(+), 264 deletions(-) diff --git a/nym-credential-proxy/nym-credential-proxy/migrations/04_buffered_deposits.sql b/nym-credential-proxy/nym-credential-proxy/migrations/04_buffered_deposits.sql index 28fb1425559..88b875a2a95 100644 --- a/nym-credential-proxy/nym-credential-proxy/migrations/04_buffered_deposits.sql +++ b/nym-credential-proxy/nym-credential-proxy/migrations/04_buffered_deposits.sql @@ -66,11 +66,16 @@ CREATE TABLE partial_blinded_wallet_failure_new INSERT INTO partial_blinded_wallet_new SELECT * -from partial_blinded_wallet; +FROM partial_blinded_wallet; INSERT INTO partial_blinded_wallet_failure_new SELECT * -from partial_blinded_wallet_failure; +FROM partial_blinded_wallet_failure; DROP TABLE partial_blinded_wallet; DROP TABLE partial_blinded_wallet_failure; +DROP TABLE ticketbook_deposit; +ALTER TABLE partial_blinded_wallet_new + RENAME TO partial_blinded_wallet; +ALTER TABLE partial_blinded_wallet_failure_new + RENAME TO partial_blinded_wallet_failure; diff --git a/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/helpers.rs index 796660f9c6a..10de031b3ba 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/helpers.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/helpers.rs @@ -1,6 +1,81 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::error::CredentialProxyError; +use crate::storage::models::StorableEcashDeposit; +use nym_compact_ecash::WithdrawalRequest; +use nym_credentials::IssuanceTicketBook; +use nym_crypto::asymmetric::ed25519; +use nym_validator_client::nyxd::{Coin, Hash}; +use time::OffsetDateTime; +use zeroize::Zeroizing; + +pub(crate) struct BufferedDeposit { + pub(crate) deposit_id: u32, + + // note: this type implements `ZeroizeOnDrop` + pub(crate) ed25519_private_key: ed25519::PrivateKey, +} + +impl TryFrom for BufferedDeposit { + type Error = CredentialProxyError; + + fn try_from(deposit: StorableEcashDeposit) -> Result { + let ed25519_private_key = ed25519::PrivateKey::from_bytes( + deposit.ed25519_deposit_private_key.as_ref(), + ) + .map_err(|err| CredentialProxyError::DatabaseInconsistency { + reason: format!("one of the stored deposit ed25519 private keys is malformed: {err}"), + })?; + + Ok(BufferedDeposit { + deposit_id: deposit.deposit_id, + ed25519_private_key, + }) + } +} + +impl BufferedDeposit { + pub(crate) fn new(deposit_id: u32, ed25519_private_key: ed25519::PrivateKey) -> Self { + BufferedDeposit { + deposit_id, + ed25519_private_key, + } + } + + pub(crate) fn sign_ticketbook_plaintext( + &self, + withdrawal_request: &WithdrawalRequest, + ) -> ed25519::Signature { + let plaintext = IssuanceTicketBook::request_plaintext(withdrawal_request, self.deposit_id); + self.ed25519_private_key.sign(plaintext) + } +} + +pub(crate) struct PerformedDeposits { + pub(crate) deposits_data: Vec, + + // shared by all performed deposits as they were included in the same tx + pub(crate) tx_hash: Hash, + pub(crate) requested_on: OffsetDateTime, + pub(crate) deposit_amount: Coin, +} + +impl PerformedDeposits { + pub(crate) fn to_storable(&self) -> Vec { + self.deposits_data + .iter() + .map(|d| StorableEcashDeposit { + deposit_id: d.deposit_id, + deposit_tx_hash: self.tx_hash.to_string(), + requested_on: self.requested_on, + deposit_amount: self.deposit_amount.to_string(), + ed25519_deposit_private_key: Zeroizing::new(d.ed25519_private_key.to_bytes()), + }) + .collect() + } +} + pub(super) fn request_sizes(total: usize, max_request_size: usize) -> impl Iterator { (0..total) .step_by(max_request_size) diff --git a/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/mod.rs index 49784f15717..29e93f234db 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/mod.rs @@ -1,66 +1,38 @@ // Copyright 2025 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::deposits_buffer::helpers::request_sizes; +use crate::deposits_buffer::helpers::{request_sizes, BufferedDeposit, PerformedDeposits}; use crate::deposits_buffer::refill_task::RefillTask; use crate::error::CredentialProxyError; use crate::http::state::required_deposit_cache::RequiredDepositCache; use crate::http::state::ChainClient; -use crate::storage::VpnApiStorage; -use nym_compact_ecash::{PublicKeyUser, WithdrawalRequest}; -use nym_credentials::IssuanceTicketBook; +use crate::storage::CredentialProxyStorage; +use nym_compact_ecash::PublicKeyUser; use nym_crypto::asymmetric::ed25519; use nym_ecash_contract_common::deposit::DepositId; use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData; use nym_validator_client::nyxd::Coin; use rand::rngs::OsRng; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::time::Duration; use time::OffsetDateTime; use tokio::sync::Mutex as AsyncMutex; -use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, instrument, warn}; use uuid::Uuid; -mod helpers; +pub(crate) mod helpers; mod refill_task; // TODO: I guess make it configurable const DEPOSITS_THRESHOLD_P: f32 = 0.1; -pub(crate) struct BufferedDeposit { - pub(crate) deposit_id: u32, - // note: this type implements `ZeroizeOnDrop` - pub(crate) ed25519_private_key: ed25519::PrivateKey, - pub(crate) ed25519_public_key: ed25519::PublicKey, -} - -impl BufferedDeposit { - fn new(deposit_id: u32, keys: (ed25519::PrivateKey, ed25519::PublicKey)) -> Self { - BufferedDeposit { - deposit_id, - ed25519_private_key: keys.0, - ed25519_public_key: keys.1, - } - } - - pub(crate) fn sign_ticketbook_plaintext( - &self, - withdrawal_request: &WithdrawalRequest, - ) -> ed25519::Signature { - let plaintext = IssuanceTicketBook::request_plaintext(withdrawal_request, self.deposit_id); - self.ed25519_private_key.sign(plaintext) - } -} - struct DepositsBufferInner { client: ChainClient, required_deposit_cache: RequiredDepositCache, - storage: VpnApiStorage, + storage: CredentialProxyStorage, target_amount: usize, max_concurrent_deposits: usize, unused_deposits: AsyncMutex>, @@ -77,12 +49,30 @@ pub(crate) struct DepositsBuffer { impl DepositsBuffer { pub(crate) async fn new( - storage: VpnApiStorage, + storage: CredentialProxyStorage, client: ChainClient, + required_deposit_cache: RequiredDepositCache, short_sha: &'static str, + target_amount: usize, + max_concurrent_deposits: usize, cancellation_token: CancellationToken, ) -> Result { - todo!("load all unused deposits from the storage") + let unused_deposits = storage.load_unused_deposits().await?; + info!("managed to load {} deposits", unused_deposits.len()); + + Ok(DepositsBuffer { + inner: Arc::new(DepositsBufferInner { + client, + required_deposit_cache, + storage, + target_amount, + max_concurrent_deposits, + unused_deposits: AsyncMutex::new(unused_deposits), + deposits_refill_task: RefillTask::default(), + short_sha, + cancellation_token, + }), + }) } async fn deposit_amount(&self) -> Result { @@ -96,23 +86,21 @@ impl DepositsBuffer { async fn make_deposits_request( &self, amount: usize, - ) -> Result, CredentialProxyError> { + ) -> Result { + let requested_on = OffsetDateTime::now_utc(); let chain_write_permit = self.inner.client.start_chain_tx().await; let mut rng = OsRng; let deposit_amount = self.deposit_amount().await?; let keys = (0..amount) - .map(|_| { - let private_key = ed25519::PrivateKey::new(&mut rng); - let public_key: ed25519::PublicKey = (&private_key).into(); - (private_key, public_key) - }) + .map(|_| ed25519::PrivateKey::new(&mut rng)) .collect::>(); info!("starting {amount} deposits"); let mut contents = Vec::new(); - for (_, pub_key) in &keys { - contents.push((pub_key.to_base58_string(), deposit_amount.clone())); + for key in &keys { + let public_key: ed25519::PublicKey = key.into(); + contents.push((public_key.to_base58_string(), deposit_amount.clone())); } let execute_res = chain_write_permit @@ -141,8 +129,8 @@ impl DepositsBuffer { return Err(CredentialProxyError::DepositFailure); } - let mut buffered_deposits = Vec::new(); - for (keys, response) in keys.into_iter().zip(contract_data) { + let mut deposits_data = Vec::new(); + for (key, response) in keys.into_iter().zip(contract_data) { let response_index = response.message_index; let deposit_id = match response.parse_singleton_u32_contract_data() { Ok(deposit_id) => deposit_id, @@ -154,26 +142,30 @@ impl DepositsBuffer { } }; - buffered_deposits.push(BufferedDeposit::new(deposit_id, keys)); + deposits_data.push(BufferedDeposit::new(deposit_id, key)); } - Ok(buffered_deposits) + Ok(PerformedDeposits { + deposits_data, + tx_hash, + requested_on, + deposit_amount, + }) } async fn insert_new_deposits( &self, - mut deposits: Vec, + mut deposits: PerformedDeposits, ) -> Result<(), CredentialProxyError> { // 1. insert into the db - // self.inner.storage.insert_new_deposits(...) - todo!(); + self.inner.storage.insert_new_deposits(&deposits).await?; // 2. update the buffer self.inner .unused_deposits .lock() .await - .append(&mut deposits); + .append(&mut deposits.deposits_data); Ok(()) } @@ -196,7 +188,7 @@ impl DepositsBuffer { } // make sure to insert deposits into db/vec as we get them so on initial run, - // we'd start trickling down data as soon as posible + // we'd start trickling down data as soon as possible let deposits = self.make_deposits_request(request_chunk).await?; self.insert_new_deposits(deposits).await?; } @@ -223,22 +215,14 @@ impl DepositsBuffer { async fn mark_deposit_as_used( &self, deposit_id: DepositId, - request_uuid: Uuid, requested_on: OffsetDateTime, client_pubkey: PublicKeyUser, + request_uuid: Uuid, ) -> Result<(), CredentialProxyError> { - // self.inner.storage.mark_deposit_as_used(deposit_id).await - todo!() - } - - pub(crate) async fn return_deposit( - &self, - deposit: BufferedDeposit, - ) -> Result<(), CredentialProxyError> { - // self.inner.storage.reset_deposit_usage(deposit.deposit_id).await?; - // self.inner.unused_deposits.lock().await.push(deposit); - // Ok(()) - todo!() + self.inner + .storage + .insert_deposit_usage(deposit_id, requested_on, client_pubkey, request_uuid) + .await } async fn wait_for_deposit( @@ -253,9 +237,9 @@ impl DepositsBuffer { // if the db call fails, we technically don't lose the deposit (we'll 'recover' it on restart) self.mark_deposit_as_used( buffered_deposit.deposit_id, - request_uuid, requested_on, client_pubkey, + request_uuid, ) .await?; return Ok(buffered_deposit); @@ -296,9 +280,9 @@ impl DepositsBuffer { Some(buffered_deposit) => { self.mark_deposit_as_used( buffered_deposit.deposit_id, - request_uuid, requested_on, client_pubkey, + request_uuid, ) .await?; Ok(buffered_deposit) diff --git a/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/refill_task.rs b/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/refill_task.rs index c81cb6d1c97..4f09df55b04 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/refill_task.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/refill_task.rs @@ -9,6 +9,7 @@ use tracing::{debug, error}; pub(super) type RefillTaskResult = Result<(), CredentialProxyError>; +#[derive(Default)] pub(super) struct RefillTask { // note that we can only have a single transaction in progress (or it'd mess up with our sequence numbers) // if we find that we're using up deposits more quickly than we're refilling them, @@ -19,10 +20,6 @@ pub(super) struct RefillTask { } impl RefillTask { - pub(super) fn in_progress(&self) -> bool { - self.in_progress.load(Ordering::SeqCst) - } - /// Attempt to set the `in_progress` value to `true` if it's not already `true`. /// Returns boolean indicating whether it was successful fn try_set_in_progress(&self) -> bool { diff --git a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs index 212f3007748..65daadb85d1 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs @@ -7,6 +7,7 @@ use tokio_util::sync::CancellationToken; use tracing::{debug, error, info, warn}; use crate::deposits_buffer::DepositsBuffer; +use crate::http::state::required_deposit_cache::RequiredDepositCache; use crate::{ cli::Cli, error::CredentialProxyError, @@ -14,7 +15,7 @@ use crate::{ state::{ApiState, ChainClient}, HttpServer, }, - storage::VpnApiStorage, + storage::CredentialProxyStorage, tasks::StoragePruner, }; @@ -96,34 +97,33 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> { // create the tasks let bind_address = cli.bind_address(); - let storage = VpnApiStorage::init(cli.persistent_storage_path()).await?; + let storage = CredentialProxyStorage::init(cli.persistent_storage_path()).await?; let mnemonic = cli.mnemonic; let auth_token = cli.http_auth_token; let webhook_cfg = cli.webhook; let chain_client = ChainClient::new(mnemonic)?; let cancellation_token = CancellationToken::new(); + let required_deposit_cache = RequiredDepositCache::default(); + let deposits_buffer = DepositsBuffer::new( storage.clone(), chain_client.clone(), + required_deposit_cache.clone(), build_sha_short(), + cli.deposits_buffer_size, + cli.max_concurrent_deposits, cancellation_token.clone(), ) .await?; - // let deposit_maker = DepositMaker::new( - // build_sha_short(), - // chain_client.clone(), - // cli.max_concurrent_deposits, - // cancellation_token.clone(), - // ); - // let deposit_request_sender = deposit_maker.deposit_request_sender(); let api_state = ApiState::new( storage.clone(), webhook_cfg, chain_client, deposits_buffer, + required_deposit_cache, cancellation_token.clone(), ) .await?; diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs index 1361a2c0680..ca3fe54acd4 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs @@ -1,7 +1,8 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::deposits_buffer::{BufferedDeposit, DepositsBuffer}; +use crate::deposits_buffer::helpers::BufferedDeposit; +use crate::deposits_buffer::DepositsBuffer; use crate::error::CredentialProxyError; use crate::helpers::LockTimer; use crate::http::state::required_deposit_cache::RequiredDepositCache; @@ -10,7 +11,7 @@ use crate::nym_api_helpers::{ ensure_sane_expiration_date, query_all_threshold_apis, CachedEpoch, CachedImmutableEpochItem, CachedImmutableItems, }; -use crate::storage::VpnApiStorage; +use crate::storage::CredentialProxyStorage; use crate::webhook::ZkNymWebHookConfig; use axum::http::StatusCode; use bip39::Mnemonic; @@ -58,24 +59,33 @@ pub(crate) mod required_deposit_cache; // currently we need to hold our keypair so that we could request a freepass credential #[derive(Clone)] pub struct ApiState { - inner: Arc, + inner: Arc, } // a lot of functionalities, mostly to do with caching and storage is just copy-pasted from nym-api, // since we have to do more or less the same work impl ApiState { - pub async fn new( - storage: VpnApiStorage, + pub(crate) async fn new( + storage: CredentialProxyStorage, zk_nym_web_hook_config: ZkNymWebHookConfig, client: ChainClient, deposits_buffer: DepositsBuffer, + required_deposit_cache: RequiredDepositCache, cancellation_token: CancellationToken, ) -> Result { let state = ApiState { - inner: Arc::new(ApiStateInner { + inner: Arc::new(CredentialProxyStateInner { storage, client, - ecash_state: EcashState::default(), + ecash_state: EcashState { + required_deposit_cache, + cached_epoch: Default::default(), + master_verification_key: Default::default(), + threshold_values: Default::default(), + epoch_clients: Default::default(), + coin_index_signatures: Default::default(), + expiration_date_signatures: Default::default(), + }, zk_nym_web_hook_config, task_tracker: TaskTracker::new(), deposits_buffer, @@ -159,7 +169,7 @@ impl ApiState { } } - pub(crate) fn storage(&self) -> &VpnApiStorage { + pub(crate) fn storage(&self) -> &CredentialProxyStorage { &self.inner.storage } @@ -647,10 +657,8 @@ impl ChainClient { } } -// - -struct ApiStateInner { - storage: VpnApiStorage, +struct CredentialProxyStateInner { + storage: CredentialProxyStorage, client: ChainClient, @@ -665,7 +673,6 @@ struct ApiStateInner { cancellation_token: CancellationToken, } -#[derive(Default)] pub(crate) struct EcashState { pub(crate) required_deposit_cache: RequiredDepositCache, diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs index 2bba05d6538..f72341cd087 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs @@ -1,13 +1,13 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::CredentialProxyError; use crate::storage::models::{ BlindedShares, BlindedSharesStatus, MinimalWalletShare, RawCoinIndexSignatures, - RawExpirationDateSignatures, RawVerificationKey, + RawExpirationDateSignatures, RawVerificationKey, StorableEcashDeposit, }; use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; use time::{Date, OffsetDateTime}; +use tracing::error; #[derive(Clone)] pub(crate) struct SqliteStorageManager { @@ -37,22 +37,21 @@ impl SqliteStorageManager { &self, id: i64, ) -> Result, sqlx::Error> { - todo!() - // sqlx::query_as!( - // MinimalWalletShare, - // r#" - // SELECT t1.node_id, t1.blinded_signature, t1.epoch_id, t1.expiration_date as "expiration_date!: Date" - // FROM partial_blinded_wallet as t1 - // JOIN ticketbook_deposit as t2 - // on t1.corresponding_deposit = t2.deposit_id - // JOIN blinded_shares as t3 - // ON t2.request_uuid = t3.request_uuid - // WHERE t3.id = ?; - // "#, - // id - // ) - // .fetch_all(&self.connection_pool) - // .await + sqlx::query_as!( + MinimalWalletShare, + r#" + SELECT t1.node_id, t1.blinded_signature, t1.epoch_id, t1.expiration_date as "expiration_date!: Date" + FROM partial_blinded_wallet as t1 + JOIN ecash_deposit_usage as t2 + on t1.corresponding_deposit = t2.deposit_id + JOIN blinded_shares as t3 + ON t2.request_uuid = t3.request_uuid + WHERE t3.id = ?; + "#, + id + ) + .fetch_all(&self.connection_pool) + .await } pub(crate) async fn load_shares_error_by_device_by_shares_id( @@ -98,27 +97,26 @@ impl SqliteStorageManager { credential_id: &str, ) -> Result, sqlx::Error> { // https://docs.rs/sqlx/latest/sqlx/macro.query.html#force-a-differentcustom-type - todo!() - // sqlx::query_as!( - // MinimalWalletShare, - // r#" - // SELECT - // t1.node_id as "node_id!", - // t1.blinded_signature as "blinded_signature!", - // t1.epoch_id as "epoch_id!", - // t1.expiration_date as "expiration_date!: Date" - // FROM partial_blinded_wallet as t1 - // JOIN ticketbook_deposit as t2 - // on t1.corresponding_deposit = t2.deposit_id - // JOIN blinded_shares as t3 - // ON t2.request_uuid = t3.request_uuid - // WHERE t3.device_id = ? AND t3.credential_id = ?; - // "#, - // device_id, - // credential_id - // ) - // .fetch_all(&self.connection_pool) - // .await + sqlx::query_as!( + MinimalWalletShare, + r#" + SELECT + t1.node_id as "node_id!", + t1.blinded_signature as "blinded_signature!", + t1.epoch_id as "epoch_id!", + t1.expiration_date as "expiration_date!: Date" + FROM partial_blinded_wallet as t1 + JOIN ecash_deposit_usage as t2 + on t1.corresponding_deposit = t2.deposit_id + JOIN blinded_shares as t3 + ON t2.request_uuid = t3.request_uuid + WHERE t3.device_id = ? AND t3.credential_id = ?; + "#, + device_id, + credential_id + ) + .fetch_all(&self.connection_pool) + .await } pub(crate) async fn load_shares_error_by_device_and_credential_id( @@ -171,7 +169,7 @@ impl SqliteStorageManager { available_shares: i64, device_id: &str, credential_id: &str, - ) -> Result { + ) -> Result { let now = OffsetDateTime::now_utc(); let res = sqlx::query_as( r#" @@ -198,7 +196,7 @@ impl SqliteStorageManager { device_id: &str, credential_id: &str, error: &str, - ) -> Result { + ) -> Result { let now = time::OffsetDateTime::now_utc(); let res = sqlx::query_as( r#" @@ -223,7 +221,7 @@ impl SqliteStorageManager { pub(crate) async fn prune_old_blinded_shares( &self, delete_after: OffsetDateTime, - ) -> Result<(), CredentialProxyError> { + ) -> Result<(), sqlx::Error> { sqlx::query!( r#" DELETE FROM blinded_shares WHERE created < ? @@ -238,33 +236,31 @@ impl SqliteStorageManager { pub(crate) async fn prune_old_partial_blinded_wallets( &self, delete_after: OffsetDateTime, - ) -> Result<(), CredentialProxyError> { - todo!() - // sqlx::query!( - // r#" - // DELETE FROM partial_blinded_wallet WHERE created < ? - // "#, - // delete_after, - // ) - // .execute(&self.connection_pool) - // .await?; - // Ok(()) + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM partial_blinded_wallet WHERE created < ? + "#, + delete_after, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) } pub(crate) async fn prune_old_partial_blinded_wallet_failures( &self, delete_after: OffsetDateTime, - ) -> Result<(), CredentialProxyError> { - todo!() - // sqlx::query!( - // r#" - // DELETE FROM partial_blinded_wallet_failure WHERE created < ? - // "#, - // delete_after, - // ) - // .execute(&self.connection_pool) - // .await?; - // Ok(()) + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM partial_blinded_wallet_failure WHERE created < ? + "#, + delete_after, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) } pub(crate) async fn get_master_verification_key( @@ -374,32 +370,87 @@ impl SqliteStorageManager { Ok(()) } - #[allow(clippy::too_many_arguments)] - pub(crate) async fn insert_deposit_data( + pub(crate) async fn insert_new_deposits( + &self, + deposits: Vec, + ) -> Result<(), sqlx::Error> { + if deposits.is_empty() { + // this should NEVER happen + error!("attempted to insert empty list of deposits"); + return Ok(()); + } + + let mut query_builder = + sqlx::QueryBuilder::new("INSERT INTO ecash_deposit (deposit_id, deposit_tx_hash, requested_on, deposit_amount, ed25519_deposit_private_key) "); + + query_builder.push_values(&deposits, |mut b, deposit| { + b.push_bind(deposit.deposit_id) + .push_bind(deposit.deposit_tx_hash.clone()) + .push_bind(deposit.requested_on) + .push_bind(deposit.deposit_amount.clone()) + .push_bind(deposit.ed25519_deposit_private_key.as_ref()); + }); + + query_builder.build().execute(&self.connection_pool).await?; + Ok(()) + } + + pub(crate) async fn load_unused_deposits( + &self, + ) -> Result, sqlx::Error> { + // select all entries from ecash_deposit where there is NO associated marked usage + sqlx::query_as( + r#" + SELECT ecash_deposit.* + FROM ecash_deposit ecash_deposit + LEFT JOIN ecash_deposit_usage deposit_usage + ON ecash_deposit.deposit_id = deposit_usage.deposit_id + WHERE deposit_usage.deposit_id IS NULL; + "#, + ) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn insert_deposit_usage( &self, deposit_id: DepositId, - deposit_tx_hash: String, requested_on: OffsetDateTime, + client_pubkey: Vec, request_uuid: String, - deposit_amount: String, - client_pubkey: &[u8], - deposit_ed25519_private_key: &[u8], ) -> Result<(), sqlx::Error> { sqlx::query!( r#" - INSERT INTO ticketbook_deposit(deposit_id, deposit_tx_hash, requested_on, request_uuid, deposit_amount, client_pubkey, ed25519_deposit_private_key) - VALUES (?, ?, ?, ?, ?, ?, ?) + INSERT INTO ecash_deposit_usage (deposit_id, ticketbooks_requested_on, client_pubkey, request_uuid) + VALUES (?, ?, ?, ?) "#, - deposit_id, - deposit_tx_hash, - requested_on, - request_uuid, - deposit_amount, - client_pubkey, - deposit_ed25519_private_key, + deposit_id, + requested_on, + client_pubkey, + request_uuid ) - .execute(&self.connection_pool) - .await?; + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn insert_deposit_usage_error( + &self, + deposit_id: DepositId, + error: String, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + UPDATE ecash_deposit_usage + SET ticketbook_request_error = ? + WHERE deposit_id = ? + "#, + error, + deposit_id + ) + .execute(&self.connection_pool) + .await?; Ok(()) } @@ -413,23 +464,22 @@ impl SqliteStorageManager { created: OffsetDateTime, share: &[u8], ) -> Result<(), sqlx::Error> { - todo!() - // sqlx::query!( - // r#" - // INSERT INTO partial_blinded_wallet(corresponding_deposit, epoch_id, expiration_date, node_id, created, blinded_signature) - // VALUES (?, ?, ?, ?, ?, ?) - // "#, - // deposit_id, - // epoch_id, - // expiration_date, - // node_id, - // created, - // share - // ) - // .execute(&self.connection_pool) - // .await?; - // - // Ok(()) + sqlx::query!( + r#" + INSERT INTO partial_blinded_wallet(corresponding_deposit, epoch_id, expiration_date, node_id, created, blinded_signature) + VALUES (?, ?, ?, ?, ?, ?) + "#, + deposit_id, + epoch_id, + expiration_date, + node_id, + created, + share + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) } pub(crate) async fn insert_partial_wallet_issuance_failure( @@ -441,22 +491,21 @@ impl SqliteStorageManager { created: OffsetDateTime, failure_message: String, ) -> Result<(), sqlx::Error> { - todo!() - // sqlx::query!( - // r#" - // INSERT INTO partial_blinded_wallet_failure(corresponding_deposit, epoch_id, expiration_date, node_id, created, failure_message) - // VALUES (?, ?, ?, ?, ?, ?) - // "#, - // deposit_id, - // epoch_id, - // expiration_date, - // node_id, - // created, - // failure_message - // ) - // .execute(&self.connection_pool) - // .await?; - // - // Ok(()) + sqlx::query!( + r#" + INSERT INTO partial_blinded_wallet_failure(corresponding_deposit, epoch_id, expiration_date, node_id, created, failure_message) + VALUES (?, ?, ?, ?, ?, ?) + "#, + deposit_id, + epoch_id, + expiration_date, + node_id, + created, + failure_message + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) } } diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs index 12a745641a2..73b6e0f1599 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs @@ -2,20 +2,18 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::credentials::ticketbook::NodeId; +use crate::deposits_buffer::helpers::{BufferedDeposit, PerformedDeposits}; use crate::error::CredentialProxyError; use crate::storage::manager::SqliteStorageManager; use crate::storage::models::{BlindedShares, MinimalWalletShare}; use nym_compact_ecash::PublicKeyUser; -use nym_credentials::ecash::bandwidth::issuance::Hash; use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; use nym_credentials::{ AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, }; -use nym_crypto::asymmetric::ed25519; use nym_validator_client::ecash::BlindedSignatureResponse; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; -use nym_validator_client::nyxd::Coin; use sqlx::sqlite::{SqliteAutoVacuum, SqliteSynchronous}; use sqlx::ConnectOptions; use std::fmt::Debug; @@ -25,17 +23,16 @@ use time::{Date, OffsetDateTime}; use tracing::log::LevelFilter; use tracing::{debug, error, info, instrument}; use uuid::Uuid; -use zeroize::Zeroizing; mod manager; pub mod models; #[derive(Clone)] -pub struct VpnApiStorage { +pub struct CredentialProxyStorage { pub(crate) storage_manager: SqliteStorageManager, } -impl VpnApiStorage { +impl CredentialProxyStorage { #[instrument] pub async fn init + Debug>( database_path: P, @@ -71,7 +68,7 @@ impl VpnApiStorage { info!("Database migration finished!"); - Ok(VpnApiStorage { + Ok(CredentialProxyStorage { storage_manager: SqliteStorageManager { connection_pool }, }) } @@ -159,13 +156,14 @@ impl VpnApiStorage { device_id: &str, credential_id: &str, ) -> Result { - self.storage_manager + Ok(self + .storage_manager .update_pending_async_blinded_shares_issued( available_shares as i64, device_id, credential_id, ) - .await + .await?) } pub(crate) async fn update_pending_async_blinded_shares_error( @@ -175,14 +173,15 @@ impl VpnApiStorage { credential_id: &str, error: &str, ) -> Result { - self.storage_manager + Ok(self + .storage_manager .update_pending_async_blinded_shares_error( available_shares as i64, device_id, credential_id, error, ) - .await + .await?) } pub(crate) async fn prune_old_blinded_shares(&self) -> Result<(), CredentialProxyError> { @@ -194,38 +193,64 @@ impl VpnApiStorage { self.storage_manager .prune_old_partial_blinded_wallet_failures(max_age) .await?; - self.storage_manager.prune_old_blinded_shares(max_age).await + self.storage_manager + .prune_old_blinded_shares(max_age) + .await?; + Ok(()) } - #[allow(clippy::too_many_arguments)] - pub(crate) async fn insert_deposit_data( + pub(crate) async fn insert_new_deposits( &self, - deposit_id: DepositId, - deposit_tx_hash: Hash, - requested_on: OffsetDateTime, - request: Uuid, - deposit_amount: Coin, - client_ecash_pubkey: &PublicKeyUser, - ed22519_keypair: &ed25519::KeyPair, + deposits: &PerformedDeposits, ) -> Result<(), CredentialProxyError> { - debug!("inserting deposit data"); + debug!("inserting {} deposits data", deposits.deposits_data.len()); + + self.storage_manager + .insert_new_deposits(deposits.to_storable()) + .await?; + Ok(()) + } - let private_key_bytes = Zeroizing::new(ed22519_keypair.private_key().to_bytes()); + pub(crate) async fn load_unused_deposits( + &self, + ) -> Result, CredentialProxyError> { + self.storage_manager + .load_unused_deposits() + .await? + .into_iter() + .map(|deposit| deposit.try_into()) + .collect() + } + pub(crate) async fn insert_deposit_usage( + &self, + deposit_id: DepositId, + requested_on: OffsetDateTime, + client_pubkey: PublicKeyUser, + request_uuid: Uuid, + ) -> Result<(), CredentialProxyError> { self.storage_manager - .insert_deposit_data( + .insert_deposit_usage( deposit_id, - deposit_tx_hash.to_string(), requested_on, - request.to_string(), - deposit_amount.to_string(), - &client_ecash_pubkey.to_bytes(), - private_key_bytes.as_ref(), + client_pubkey.to_bytes(), + request_uuid.to_string(), ) .await?; Ok(()) } + pub(crate) async fn insert_deposit_usage_error( + &self, + deposit_id: DepositId, + error: String, + ) -> Result<(), CredentialProxyError> { + self.storage_manager + .insert_deposit_usage_error(deposit_id, error) + .await?; + Ok(()) + } + pub(crate) async fn insert_partial_wallet_share( &self, deposit_id: DepositId, @@ -376,6 +401,8 @@ mod tests { use crate::http::helpers; use crate::storage::models::BlindedSharesStatus; use nym_compact_ecash::scheme::keygen::KeyPairUser; + use nym_crypto::asymmetric::ed25519; + use nym_validator_client::nyxd::{Coin, Hash}; use rand::rngs::OsRng; use rand::RngCore; use std::ops::Deref; @@ -383,7 +410,7 @@ mod tests { // create the wrapper so the underlying file gets deleted when it's no longer needed struct StorageTestWrapper { - inner: VpnApiStorage, + inner: CredentialProxyStorage, _path: TempPath, } @@ -395,12 +422,12 @@ mod tests { println!("Creating database at {path:?}..."); Ok(StorageTestWrapper { - inner: VpnApiStorage::init(&path).await?, + inner: CredentialProxyStorage::init(&path).await?, _path: path, }) } - async fn insert_dummy_deposit(&self, uuid: Uuid) -> anyhow::Result { + async fn insert_dummy_used_deposit(&self, uuid: Uuid) -> anyhow::Result { let mut rng = OsRng; let deposit_id = rng.next_u32(); let tx_hash = Hash::Sha256(Default::default()); @@ -409,18 +436,21 @@ mod tests { let client_keypair = KeyPairUser::new(); let client_ecash_pubkey = &client_keypair.public_key(); - let deposit_keypair = ed25519::KeyPair::new(&mut rng); + let deposit_key = ed25519::PrivateKey::new(&mut rng); self.inner - .insert_deposit_data( - deposit_id, + .insert_new_deposits(&PerformedDeposits { + deposits_data: vec![BufferedDeposit { + deposit_id, + ed25519_private_key: deposit_key, + }], tx_hash, requested_on, - uuid, deposit_amount, - client_ecash_pubkey, - &deposit_keypair, - ) + }) + .await?; + self.inner + .insert_deposit_usage(deposit_id, requested_on, *client_ecash_pubkey, uuid) .await?; Ok(deposit_id) @@ -428,7 +458,7 @@ mod tests { } impl Deref for StorageTestWrapper { - type Target = VpnApiStorage; + type Target = CredentialProxyStorage; fn deref(&self) -> &Self::Target { &self.inner } @@ -453,7 +483,7 @@ mod tests { let dummy_uuid = helpers::random_uuid(); println!("🚀 insert_pending_blinded_share..."); - storage.insert_dummy_deposit(dummy_uuid).await?; + storage.insert_dummy_used_deposit(dummy_uuid).await?; let res = storage .insert_new_pending_async_shares_request(dummy_uuid, "1234", "1234") .await; @@ -461,7 +491,7 @@ mod tests { println!("❌ {e}"); } assert!(res.is_ok()); - let res = res.unwrap(); + let res = res?; println!("res = {res:?}"); assert_eq!(res.status, BlindedSharesStatus::Pending); @@ -473,7 +503,7 @@ mod tests { println!("❌ {e}"); } assert!(res.is_ok()); - let res = res.unwrap(); + let res = res?; println!("res = {res:?}"); assert!(res.error_message.is_some()); assert_eq!(res.status, BlindedSharesStatus::Error); @@ -486,7 +516,7 @@ mod tests { println!("❌ {e}"); } assert!(res.is_ok()); - let res = res.unwrap(); + let res = res?; println!("res = {res:?}"); assert_eq!(res.status, BlindedSharesStatus::Issued); assert!(res.error_message.is_none()); diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/models.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/models.rs index 8118890b07a..2c369d199d3 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/models.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/storage/models.rs @@ -1,11 +1,49 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_crypto::asymmetric::ed25519; use serde::{Deserialize, Serialize}; -use sqlx::{FromRow, Type}; -use std::convert::Into; +use sqlx::sqlite::SqliteRow; +use sqlx::{FromRow, Row, Type}; use strum_macros::{Display, EnumString}; use time::{Date, OffsetDateTime}; +use zeroize::Zeroizing; + +pub(crate) struct StorableEcashDeposit { + pub(crate) deposit_id: u32, + pub(crate) deposit_tx_hash: String, + pub(crate) requested_on: OffsetDateTime, + pub(crate) deposit_amount: String, + pub(crate) ed25519_deposit_private_key: Zeroizing<[u8; ed25519::SECRET_KEY_LENGTH]>, +} + +impl<'r> FromRow<'r, SqliteRow> for StorableEcashDeposit { + fn from_row(row: &'r SqliteRow) -> Result { + let deposit_id = row.try_get("deposit_id")?; + let deposit_tx_hash = row.try_get("deposit_tx_hash")?; + let requested_on = row.try_get("requested_on")?; + let deposit_amount = row.try_get("deposit_amount")?; + let ed25519_deposit_private_key: Vec = row.try_get("ed25519_deposit_private_key")?; + if ed25519_deposit_private_key.len() != ed25519::SECRET_KEY_LENGTH { + return Err(sqlx::Error::decode( + "stored ed25519_deposit_private_key has invalid length", + )); + } + + // SAFETY: we just checked the length is correct + #[allow(clippy::unwrap_used)] + let ed25519_deposit_private_key: [u8; ed25519::SECRET_KEY_LENGTH] = + ed25519_deposit_private_key.try_into().unwrap(); + + Ok(StorableEcashDeposit { + deposit_id, + deposit_tx_hash, + requested_on, + deposit_amount, + ed25519_deposit_private_key: Zeroizing::new(ed25519_deposit_private_key), + }) + } +} #[derive(Serialize, Deserialize, Debug, Clone, EnumString, Type, PartialEq, Display)] #[sqlx(rename_all = "snake_case")] @@ -29,11 +67,6 @@ pub struct BlindedShares { pub updated: OffsetDateTime, } -pub struct FullBlindedShares { - pub status: BlindedShares, - pub shares: (), -} - #[derive(FromRow)] pub struct RawExpirationDateSignatures { pub serialised_signatures: Vec, diff --git a/nym-credential-proxy/nym-credential-proxy/src/tasks.rs b/nym-credential-proxy/nym-credential-proxy/src/tasks.rs index f886b7fd440..9cd115eda92 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/tasks.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/tasks.rs @@ -1,17 +1,17 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::storage::VpnApiStorage; +use crate::storage::CredentialProxyStorage; use tokio_util::sync::CancellationToken; use tracing::{error, info}; pub struct StoragePruner { cancellation_token: CancellationToken, - storage: VpnApiStorage, + storage: CredentialProxyStorage, } impl StoragePruner { - pub fn new(cancellation_token: CancellationToken, storage: VpnApiStorage) -> Self { + pub fn new(cancellation_token: CancellationToken, storage: CredentialProxyStorage) -> Self { Self { cancellation_token, storage, From 020feb8e6a4e96a1bd17d61e1bfbca48216c98d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 11 Aug 2025 14:43:54 +0100 Subject: [PATCH 04/10] insert information about deposit usage failure --- .../src/credentials/ticketbook/mod.rs | 8 ++++++-- .../nym-credential-proxy/src/http/state/mod.rs | 14 +++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs index cab4421212f..030cc47151c 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs @@ -124,10 +124,14 @@ pub(crate) async fn try_obtain_wallet_shares( let shares = wallet_shares.len(); if shares < threshold as usize { - return Err(CredentialProxyError::InsufficientNumberOfCredentials { + let err = CredentialProxyError::InsufficientNumberOfCredentials { available: shares, threshold, - }); + }; + state + .insert_deposit_usage_error(deposit_id, err.to_string()) + .await; + return Err(err); } Ok(wallet_shares diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs index ca3fe54acd4..8896a5f5b35 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs @@ -31,6 +31,7 @@ use nym_credentials::{ AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, }; use nym_credentials_interface::VerificationKeyAuth; +use nym_ecash_contract_common::deposit::DepositId; use nym_ecash_contract_common::msg::ExecuteMsg; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nym_api::EpochId; @@ -51,7 +52,7 @@ use tokio::task::JoinHandle; use tokio::time::Instant; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker; -use tracing::{debug, info, instrument, warn}; +use tracing::{debug, error, info, instrument, warn}; use uuid::Uuid; pub(crate) mod required_deposit_cache; @@ -239,6 +240,17 @@ impl ApiState { deposit } + pub(crate) async fn insert_deposit_usage_error(&self, deposit_id: DepositId, error: String) { + if let Err(err) = self + .inner + .storage + .insert_deposit_usage_error(deposit_id, error) + .await + { + error!("failed to insert information about deposit (id: {deposit_id}) usage failure: {err}") + } + } + pub(crate) async fn global_data( &self, include_master_verification_key: bool, From 152d89acb984d52bbcb4e3027bd34124ff041e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 11 Aug 2025 14:46:17 +0100 Subject: [PATCH 05/10] remove old deposit maker --- .../nym-credential-proxy/src/deposit_maker.rs | 205 ------------------ 1 file changed, 205 deletions(-) delete mode 100644 nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs diff --git a/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs b/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs deleted file mode 100644 index f56dc462850..00000000000 --- a/nym-credential-proxy/nym-credential-proxy/src/deposit_maker.rs +++ /dev/null @@ -1,205 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::error::CredentialProxyError; -use crate::http::state::ChainClient; -use nym_crypto::asymmetric::ed25519; -use nym_ecash_contract_common::deposit::DepositId; -use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData; -use nym_validator_client::nyxd::{Coin, Hash}; -use tokio::sync::{mpsc, oneshot}; -use tokio_util::sync::CancellationToken; -use tracing::{debug, error, info, warn}; - -#[derive(Debug)] -pub(crate) struct DepositResponse { - pub tx_hash: Hash, - pub deposit_id: DepositId, -} - -pub(crate) struct DepositRequest { - pubkey: ed25519::PublicKey, - deposit_amount: Coin, - on_done: oneshot::Sender>, -} - -impl DepositRequest { - pub(crate) fn new( - pubkey: ed25519::PublicKey, - deposit_amount: &Coin, - on_done: oneshot::Sender>, - ) -> Self { - DepositRequest { - pubkey, - deposit_amount: deposit_amount.clone(), - on_done, - } - } -} - -pub(crate) type DepositRequestReceiver = mpsc::Receiver; - -pub(crate) fn new_control_channels( - max_concurrent_deposits: usize, -) -> (DepositRequestSender, DepositRequestReceiver) { - let (tx, rx) = mpsc::channel(max_concurrent_deposits); - (tx.into(), rx) -} - -#[derive(Debug, Clone)] -pub struct DepositRequestSender(mpsc::Sender); - -impl From> for DepositRequestSender { - fn from(inner: mpsc::Sender) -> Self { - DepositRequestSender(inner) - } -} - -impl DepositRequestSender { - pub(crate) async fn request_deposit(&self, request: DepositRequest) { - if self.0.send(request).await.is_err() { - error!("failed to request deposit: the DepositMaker must have died!") - } - } -} - -pub(crate) struct DepositMaker { - client: ChainClient, - max_concurrent_deposits: usize, - deposit_request_sender: DepositRequestSender, - deposit_request_receiver: DepositRequestReceiver, - short_sha: &'static str, - cancellation_token: CancellationToken, -} - -impl DepositMaker { - pub(crate) fn new( - short_sha: &'static str, - client: ChainClient, - max_concurrent_deposits: usize, - cancellation_token: CancellationToken, - ) -> Self { - let (deposit_request_sender, deposit_request_receiver) = - new_control_channels(max_concurrent_deposits); - - DepositMaker { - client, - max_concurrent_deposits, - deposit_request_sender, - deposit_request_receiver, - short_sha, - cancellation_token, - } - } - - pub(crate) fn deposit_request_sender(&self) -> DepositRequestSender { - self.deposit_request_sender.clone() - } - - pub(crate) async fn process_deposit_requests( - &mut self, - requests: Vec, - ) -> Result<(), CredentialProxyError> { - let chain_write_permit = self.client.start_chain_tx().await; - - info!("starting deposits"); - let mut contents = Vec::new(); - let mut replies = Vec::new(); - for request in requests { - // check if the channel is still open in case the receiver client has cancelled the request - if request.on_done.is_closed() { - warn!( - "the request for deposit from {} got cancelled", - request.pubkey - ); - continue; - } - - contents.push((request.pubkey.to_base58_string(), request.deposit_amount)); - replies.push(request.on_done); - } - - let deposits_res = chain_write_permit - .make_deposits(self.short_sha, contents) - .await; - let execute_res = match deposits_res { - Ok(res) => res, - Err(err) => { - // we have to let requesters know the deposit(s) failed - for reply in replies { - if reply.send(None).is_err() { - warn!("one of the deposit requesters has been terminated") - } - } - return Err(err); - } - }; - - let tx_hash = execute_res.transaction_hash; - info!("{} deposits made in transaction: {tx_hash}", replies.len()); - - let contract_data = match execute_res.to_contract_data() { - Ok(contract_data) => contract_data, - Err(err) => { - // that one is tricky. deposits technically got made, but we somehow failed to parse response, - // in this case terminate the proxy with 0 exit code so it wouldn't get automatically restarted - // because it requires some serious MANUAL intervention - error!("CRITICAL FAILURE: failed to parse out deposit information from the contract transaction. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually. error was: {err}"); - self.cancellation_token.cancel(); - return Err(CredentialProxyError::DepositFailure); - } - }; - - if contract_data.len() != replies.len() { - // another critical failure, that one should be quite impossible and thus has to be manually inspected - error!("CRITICAL FAILURE: failed to parse out all deposit information from the contract transaction. got {} responses while we sent {} deposits! either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually", contract_data.len(), replies.len()); - self.cancellation_token.cancel(); - return Err(CredentialProxyError::DepositFailure); - } - - for (reply_channel, response) in replies.into_iter().zip(contract_data) { - let response_index = response.message_index; - let deposit_id = match response.parse_singleton_u32_contract_data() { - Ok(deposit_id) => deposit_id, - Err(err) => { - // another impossibility - error!("CRITICAL FAILURE: failed to parse out deposit id out of the response at index {response_index}: {err}. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually"); - self.cancellation_token.cancel(); - return Err(CredentialProxyError::DepositFailure); - } - }; - - if reply_channel - .send(Some(DepositResponse { - deposit_id, - tx_hash, - })) - .is_err() - { - warn!("one of the deposit requesters has been terminated. deposit {deposit_id} will remain unclaimed!"); - // this shouldn't happen as the requester task shouldn't be killed, but it's not a critical failure - // we just lost some tokens, but it's not an undefined on-chain behaviour - } - } - - Ok(()) - } - - pub async fn run_forever(mut self) { - info!("starting the deposit maker task"); - loop { - let mut receive_buffer = Vec::with_capacity(self.max_concurrent_deposits); - tokio::select! { - _ = self.cancellation_token.cancelled() => { - break - } - received = self.deposit_request_receiver.recv_many(&mut receive_buffer, self.max_concurrent_deposits) => { - debug!("received {received} deposit requests"); - if let Err(err) = self.process_deposit_requests(receive_buffer).await { - error!("failed to process received deposit requests: {err}") - } - } - } - } - } -} From 345ebc6aaf79e42544b8cd90764e5907b699e947 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 11 Aug 2025 15:23:50 +0100 Subject: [PATCH 06/10] nym credential proxy to monitor quorum state to stop issuance if it'd fail --- Cargo.lock | 3 +- .../src/ecash/bandwidth/issuance.rs | 2 +- common/ecash-signer-check/src/lib.rs | 41 ++++++- .../src/scheme/keygen.rs | 2 +- .../nym-credential-proxy/Cargo.toml | 3 +- .../nym-credential-proxy/src/cli.rs | 9 ++ .../src/credentials/ticketbook/mod.rs | 2 +- .../nym-credential-proxy/src/error.rs | 7 ++ .../nym-credential-proxy/src/helpers.rs | 11 ++ .../src/http/state/mod.rs | 9 +- .../nym-credential-proxy/src/main.rs | 1 + .../src/quorum_checker.rs | 102 ++++++++++++++++++ .../nym-credential-proxy/src/tasks.rs | 1 + 13 files changed, 182 insertions(+), 11 deletions(-) create mode 100644 nym-credential-proxy/nym-credential-proxy/src/quorum_checker.rs diff --git a/Cargo.lock b/Cargo.lock index 4cfa46fddb6..0d23bf6c5c5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5371,7 +5371,7 @@ dependencies = [ [[package]] name = "nym-credential-proxy" -version = "0.1.7" +version = "0.1.8" dependencies = [ "anyhow", "axum 0.7.9", @@ -5389,6 +5389,7 @@ dependencies = [ "nym-credentials-interface", "nym-crypto", "nym-ecash-contract-common", + "nym-ecash-signer-check", "nym-http-api-common", "nym-network-defaults", "nym-validator-client", diff --git a/common/credentials/src/ecash/bandwidth/issuance.rs b/common/credentials/src/ecash/bandwidth/issuance.rs index 3d1cacc72d2..f6de39c259a 100644 --- a/common/credentials/src/ecash/bandwidth/issuance.rs +++ b/common/credentials/src/ecash/bandwidth/issuance.rs @@ -108,7 +108,7 @@ impl IssuanceTicketBook { signing_request.withdrawal_request.clone(), self.deposit_id, request_signature, - signing_request.ecash_pub_key.clone(), + signing_request.ecash_pub_key, signing_request.expiration_date, signing_request.ticketbook_type, ) diff --git a/common/ecash-signer-check/src/lib.rs b/common/ecash-signer-check/src/lib.rs index 2915119a272..c7ddb071130 100644 --- a/common/ecash-signer-check/src/lib.rs +++ b/common/ecash-signer-check/src/lib.rs @@ -15,6 +15,9 @@ use nym_validator_client::ecash::models::EcashSignerStatusResponse; use nym_validator_client::models::{ ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse, }; +use nym_validator_client::nyxd::contract_traits::dkg_query_client::{ + ContractVKShare, DealerDetails, Epoch, +}; mod client_check; pub mod error; @@ -48,7 +51,22 @@ pub async fn check_signers( check_signers_with_client(&client).await } +pub struct DkgDetails { + pub dkg_epoch: Epoch, + pub threshold: Option, + pub network_dealers: Vec, + pub submitted_shared: HashMap, +} + pub async fn check_signers_with_client(client: &C) -> Result +where + C: DkgQueryClient + Sync, +{ + let dkg_details = dkg_details_with_client(client).await?; + check_known_dealers(dkg_details).await +} + +pub async fn dkg_details_with_client(client: &C) -> Result where C: DkgQueryClient + Sync, { @@ -79,16 +97,31 @@ where .map(|share| (share.node_index, share)) .collect(); + Ok(DkgDetails { + dkg_epoch, + threshold, + network_dealers: dealers, + submitted_shared: shares, + }) +} + +pub async fn check_known_dealers( + dkg_details: DkgDetails, +) -> Result { // 6. for each dealer attempt to perform the checks - let results = dealers + let results = dkg_details + .network_dealers .into_iter() .map(|d| { - let share = shares.get(&d.assigned_index); - check_client(d, dkg_epoch.epoch_id, share) + let share = dkg_details.submitted_shared.get(&d.assigned_index); + check_client(d, dkg_details.dkg_epoch.epoch_id, share) }) .collect::>() .collect::>() .await; - Ok(SignersTestResult { threshold, results }) + Ok(SignersTestResult { + threshold: dkg_details.threshold, + results, + }) } diff --git a/common/nym_offline_compact_ecash/src/scheme/keygen.rs b/common/nym_offline_compact_ecash/src/scheme/keygen.rs index e7f649a685f..faa5db6b59d 100644 --- a/common/nym_offline_compact_ecash/src/scheme/keygen.rs +++ b/common/nym_offline_compact_ecash/src/scheme/keygen.rs @@ -554,7 +554,7 @@ impl KeyPairUser { } pub fn public_key(&self) -> PublicKeyUser { - self.public_key.clone() + self.public_key } pub fn to_bytes(&self) -> Vec { diff --git a/nym-credential-proxy/nym-credential-proxy/Cargo.toml b/nym-credential-proxy/nym-credential-proxy/Cargo.toml index 52ec9c2967a..32dfa6d4e4e 100644 --- a/nym-credential-proxy/nym-credential-proxy/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-credential-proxy" -version = "0.1.7" +version = "0.1.8" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -50,6 +50,7 @@ nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-network-defaults = { path = "../../common/network-defaults" } nym-credential-proxy-requests = { path = "../nym-credential-proxy-requests", features = ["openapi"] } +nym-ecash-signer-check = { path = "../../common/ecash-signer-check" } [dev-dependencies] tempfile = { workspace = true } diff --git a/nym-credential-proxy/nym-credential-proxy/src/cli.rs b/nym-credential-proxy/nym-credential-proxy/src/cli.rs index bb76744a10e..161ac5a15ca 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/cli.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/cli.rs @@ -10,6 +10,7 @@ use std::fs::create_dir_all; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::sync::OnceLock; +use std::time::Duration; use tracing::info; fn pretty_build_info_static() -> &'static str { @@ -73,6 +74,14 @@ pub struct Cli { )] pub(crate) deposits_buffer_size: usize, + #[clap( + long, + env = "NYM_CREDENTIAL_PROXY_QUORUM_CHECK_INTERVAL", + default_value = "5m", + value_parser = humantime::parse_duration + )] + pub(crate) quorum_check_interval: Duration, + #[clap(long, env = "NYM_CREDENTIAL_PROXY_PERSISTENT_STORAGE_STORAGE")] pub(crate) persistent_storage_path: Option, } diff --git a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs index 030cc47151c..3cc5c9eff34 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs @@ -38,7 +38,7 @@ pub(crate) async fn try_obtain_wallet_shares( request_data: TicketbookRequest, ) -> Result, CredentialProxyError> { // don't proceed if we don't have quorum available as the request will definitely fail - if !state.quorum_available().await { + if !state.quorum_available() { return Err(CredentialProxyError::UnavailableSigningQuorum); } diff --git a/nym-credential-proxy/nym-credential-proxy/src/error.rs b/nym-credential-proxy/nym-credential-proxy/src/error.rs index a63340e05b8..f6f5b028192 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/error.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/error.rs @@ -1,6 +1,7 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_ecash_signer_check::SignerCheckError; use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::error::NyxdError; @@ -121,6 +122,12 @@ pub enum CredentialProxyError { #[error("can't obtain sufficient number of credential shares due to unavailable quorum")] UnavailableSigningQuorum, + + #[error("failed to perform quorum check: {source}")] + QuorumCheckFailure { + #[from] + source: SignerCheckError, + }, } impl CredentialProxyError { diff --git a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs index 65daadb85d1..d4c6b9589e5 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs @@ -8,6 +8,7 @@ use tracing::{debug, error, info, warn}; use crate::deposits_buffer::DepositsBuffer; use crate::http::state::required_deposit_cache::RequiredDepositCache; +use crate::quorum_checker::QuorumStateChecker; use crate::{ cli::Cli, error::CredentialProxyError, @@ -106,6 +107,14 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> { let required_deposit_cache = RequiredDepositCache::default(); + let quorum_state_checker = QuorumStateChecker::new( + chain_client.clone(), + cli.quorum_check_interval, + cancellation_token.clone(), + ) + .await?; + let quorum_state = quorum_state_checker.quorum_state_ref(); + let deposits_buffer = DepositsBuffer::new( storage.clone(), chain_client.clone(), @@ -120,6 +129,7 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> { // let deposit_request_sender = deposit_maker.deposit_request_sender(); let api_state = ApiState::new( storage.clone(), + quorum_state, webhook_cfg, chain_client, deposits_buffer, @@ -138,6 +148,7 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> { // spawn all the tasks api_state.try_spawn(http_server.run_forever()); api_state.try_spawn(storage_pruner.run_forever()); + api_state.try_spawn(quorum_state_checker.run_forever()); // wait for cancel signal (SIGINT, SIGTERM or SIGQUIT) wait_for_signal().await; diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs index 8896a5f5b35..4f6285b833e 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs @@ -11,6 +11,7 @@ use crate::nym_api_helpers::{ ensure_sane_expiration_date, query_all_threshold_apis, CachedEpoch, CachedImmutableEpochItem, CachedImmutableItems, }; +use crate::quorum_checker::QuorumState; use crate::storage::CredentialProxyStorage; use crate::webhook::ZkNymWebHookConfig; use axum::http::StatusCode; @@ -68,6 +69,7 @@ pub struct ApiState { impl ApiState { pub(crate) async fn new( storage: CredentialProxyStorage, + quorum_state: QuorumState, zk_nym_web_hook_config: ZkNymWebHookConfig, client: ChainClient, deposits_buffer: DepositsBuffer, @@ -80,6 +82,7 @@ impl ApiState { client, ecash_state: EcashState { required_deposit_cache, + quorum_state, cached_epoch: Default::default(), master_verification_key: Default::default(), threshold_values: Default::default(), @@ -145,8 +148,8 @@ impl ApiState { &self.inner.zk_nym_web_hook_config } - pub(crate) async fn quorum_available(&self) -> bool { - todo!() + pub(crate) fn quorum_available(&self) -> bool { + self.inner.ecash_state.quorum_state.available() } async fn ensure_credentials_issuable(&self) -> Result<(), CredentialProxyError> { @@ -688,6 +691,8 @@ struct CredentialProxyStateInner { pub(crate) struct EcashState { pub(crate) required_deposit_cache: RequiredDepositCache, + pub(crate) quorum_state: QuorumState, + pub(crate) cached_epoch: RwLock, pub(crate) master_verification_key: CachedImmutableEpochItem, diff --git a/nym-credential-proxy/nym-credential-proxy/src/main.rs b/nym-credential-proxy/nym-credential-proxy/src/main.rs index 7872c675cb1..75b6dfdac35 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/main.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/main.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only mod deposits_buffer; +mod quorum_checker; cfg_if::cfg_if! { if #[cfg(unix)] { diff --git a/nym-credential-proxy/nym-credential-proxy/src/quorum_checker.rs b/nym-credential-proxy/nym-credential-proxy/src/quorum_checker.rs new file mode 100644 index 00000000000..8cb3963757f --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/quorum_checker.rs @@ -0,0 +1,102 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::CredentialProxyError; +use crate::http::state::ChainClient; +use nym_ecash_signer_check::{check_known_dealers, dkg_details_with_client}; +use std::ops::Deref; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Duration; +use tokio_util::sync::CancellationToken; +use tracing::{error, info, warn}; + +#[derive(Clone)] +pub(crate) struct QuorumState { + available: Arc, +} + +impl QuorumState { + pub(crate) fn available(&self) -> bool { + self.available.load(Ordering::Acquire) + } +} + +pub(crate) struct QuorumStateChecker { + client: ChainClient, + cancellation_token: CancellationToken, + check_interval: Duration, + quorum_state: QuorumState, +} + +impl QuorumStateChecker { + pub async fn new( + client: ChainClient, + check_interval: Duration, + cancellation_token: CancellationToken, + ) -> Result { + let this = QuorumStateChecker { + client, + cancellation_token, + check_interval, + quorum_state: QuorumState { + available: Arc::new(Default::default()), + }, + }; + + // first check MUST succeed, otherwise we shouldn't start + let quorum_available = this.check_quorum_state().await?; + this.quorum_state + .available + .store(quorum_available, Ordering::Relaxed); + Ok(this) + } + + pub fn quorum_state_ref(&self) -> QuorumState { + self.quorum_state.clone() + } + + async fn check_quorum_state(&self) -> Result { + let client_guard = self.client.query_chain().await; + + // split the operation as we only need to hold the reference to chain client for the first part + // and the second half doesn't rely on it (and takes way longer) + let dkg_details = dkg_details_with_client(client_guard.deref()).await?; + drop(client_guard); + + let res = check_known_dealers(dkg_details).await?; + + let Some(signing_threshold) = res.threshold else { + warn!("signing threshold is currently unavailable and we have not yet implemented credential issuance during DKG transition"); + return Ok(false); + }; + + let mut working_issuer = 0; + + for result in res.results { + if result.chain_available() && result.signing_available() { + working_issuer += 1; + } + } + + Ok((working_issuer as u64) >= signing_threshold) + } + + pub async fn run_forever(self) { + info!("starting quorum state checker"); + loop { + tokio::select! { + biased; + _ = self.cancellation_token.cancelled() => { + break + } + _ = tokio::time::sleep(self.check_interval) => { + match self.check_quorum_state().await { + Ok(available) => self.quorum_state.available.store(available, Ordering::SeqCst), + Err(err) => error!("failed to check current quorum state: {err}"), + } + } + } + } + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/tasks.rs b/nym-credential-proxy/nym-credential-proxy/src/tasks.rs index 9cd115eda92..475b81ef3e5 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/tasks.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/tasks.rs @@ -22,6 +22,7 @@ impl StoragePruner { info!("starting the storage pruner task"); loop { tokio::select! { + biased; _ = self.cancellation_token.cancelled() => { break } From 2f23930f802a26381fbac1df9c43f1b9118c1380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 11 Aug 2025 16:35:20 +0100 Subject: [PATCH 07/10] clippy --- common/gateway-requests/src/models.rs | 2 +- common/nym_offline_compact_ecash/src/scheme/identify.rs | 8 ++++---- nym-api/src/ecash/helpers.rs | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/common/gateway-requests/src/models.rs b/common/gateway-requests/src/models.rs index 32b2a9f8129..f7e79d76856 100644 --- a/common/gateway-requests/src/models.rs +++ b/common/gateway-requests/src/models.rs @@ -89,7 +89,7 @@ mod tests { .unwrap(); let blind_sig = issue( keypair.secret_key(), - sig_req.ecash_pub_key.clone(), + sig_req.ecash_pub_key, &sig_req.withdrawal_request, expiration_date.ecash_unix_timestamp(), issuance.ticketbook_type().encode(), diff --git a/common/nym_offline_compact_ecash/src/scheme/identify.rs b/common/nym_offline_compact_ecash/src/scheme/identify.rs index 38489e28c97..06e51e151aa 100644 --- a/common/nym_offline_compact_ecash/src/scheme/identify.rs +++ b/common/nym_offline_compact_ecash/src/scheme/identify.rs @@ -319,9 +319,9 @@ mod tests { let sk = grp.random_scalar(); let sk_user = SecretKeyUser { sk }; let pk_user = sk_user.public_key(); - public_keys.push(pk_user.clone()); + public_keys.push(pk_user); } - public_keys.push(user_keypair.public_key().clone()); + public_keys.push(user_keypair.public_key()); let (req, req_info) = withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); @@ -462,9 +462,9 @@ mod tests { let sk = grp.random_scalar(); let sk_user = SecretKeyUser { sk }; let pk_user = sk_user.public_key(); - public_keys.push(pk_user.clone()); + public_keys.push(pk_user); } - public_keys.push(user_keypair.public_key().clone()); + public_keys.push(user_keypair.public_key()); let (req, req_info) = withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap(); diff --git a/nym-api/src/ecash/helpers.rs b/nym-api/src/ecash/helpers.rs index c636d47157b..7e28e86e96f 100644 --- a/nym-api/src/ecash/helpers.rs +++ b/nym-api/src/ecash/helpers.rs @@ -50,7 +50,7 @@ impl CredentialRequest for BlindSignRequestBody { } fn ecash_pubkey(&self) -> PublicKeyUser { - self.ecash_pubkey.clone() + self.ecash_pubkey } } @@ -60,7 +60,7 @@ pub(crate) fn blind_sign( ) -> Result { Ok(nym_compact_ecash::scheme::withdrawal::issue( signing_key, - request.ecash_pubkey().clone(), + request.ecash_pubkey(), request.withdrawal_request(), request.expiration_date_timestamp(), request.ticketbook_type(), From e2534dd4e78346e5505c74677b2810efd894ebf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 11 Aug 2025 17:00:56 +0100 Subject: [PATCH 08/10] target lock new modules --- nym-credential-proxy/nym-credential-proxy/src/main.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/nym-credential-proxy/nym-credential-proxy/src/main.rs b/nym-credential-proxy/nym-credential-proxy/src/main.rs index 75b6dfdac35..cb196dc3b3f 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/main.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/main.rs @@ -1,9 +1,6 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -mod deposits_buffer; -mod quorum_checker; - cfg_if::cfg_if! { if #[cfg(unix)] { use crate::cli::Cli; @@ -16,7 +13,6 @@ cfg_if::cfg_if! { pub mod cli; pub mod config; pub mod credentials; - // mod deposit_maker; pub mod error; pub mod helpers; pub mod http; @@ -24,6 +20,8 @@ cfg_if::cfg_if! { pub mod storage; pub mod tasks; mod webhook; + mod deposits_buffer; + mod quorum_checker; } } From ea7c0cd4a1570e7e79b4607cea3e97230ed07dc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 11 Aug 2025 17:06:28 +0100 Subject: [PATCH 09/10] windows clippy --- nym-credential-proxy/nym-credential-proxy/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/nym-credential-proxy/nym-credential-proxy/src/main.rs b/nym-credential-proxy/nym-credential-proxy/src/main.rs index cb196dc3b3f..08f1dd013d6 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/main.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/main.rs @@ -51,5 +51,6 @@ async fn main() -> anyhow::Result<()> { #[tokio::main] async fn main() -> anyhow::Result<()> { eprintln!("This tool is only supported on Unix systems"); + #[allow(clippy::exit)] std::process::exit(1) } From 5549d71153c52c9178598b14adc16878208706b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Fri, 29 Aug 2025 09:14:47 +0100 Subject: [PATCH 10/10] renamed migration file due to rebasing --- .../{04_buffered_deposits.sql => 05_buffered_deposits.sql} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename nym-credential-proxy/nym-credential-proxy/migrations/{04_buffered_deposits.sql => 05_buffered_deposits.sql} (100%) diff --git a/nym-credential-proxy/nym-credential-proxy/migrations/04_buffered_deposits.sql b/nym-credential-proxy/nym-credential-proxy/migrations/05_buffered_deposits.sql similarity index 100% rename from nym-credential-proxy/nym-credential-proxy/migrations/04_buffered_deposits.sql rename to nym-credential-proxy/nym-credential-proxy/migrations/05_buffered_deposits.sql