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/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/common/nym_offline_compact_ecash/src/scheme/keygen.rs b/common/nym_offline_compact_ecash/src/scheme/keygen.rs index 8db1597e617..faa5db6b59d 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, } @@ -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-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(), diff --git a/nym-credential-proxy/nym-credential-proxy/Cargo.toml b/nym-credential-proxy/nym-credential-proxy/Cargo.toml index 79077003844..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,14 +50,19 @@ 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 } [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/05_buffered_deposits.sql b/nym-credential-proxy/nym-credential-proxy/migrations/05_buffered_deposits.sql new file mode 100644 index 00000000000..88b875a2a95 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/migrations/05_buffered_deposits.sql @@ -0,0 +1,81 @@ +/* + * 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; +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/cli.rs b/nym-credential-proxy/nym-credential-proxy/src/cli.rs index cf5b25cf230..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 { @@ -64,6 +65,23 @@ 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_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, } @@ -90,10 +108,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 33ad66391ef..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 @@ -1,8 +1,7 @@ // Copyright 2024 Nym Technologies SA // 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}; @@ -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(VpnApiError::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(VpnApiError::DepositFailure) -} - #[instrument( skip(state, request_data, request, requested_on), fields( @@ -69,13 +36,13 @@ pub(crate) async fn try_obtain_wallet_shares( request: Uuid, requested_on: OffsetDateTime, request_data: TicketbookRequest, -) -> Result, VpnApiError> { - let mut rng = OsRng; - - let ed25519_keypair = ed25519::KeyPair::new(&mut rng); +) -> Result, CredentialProxyError> { + // don't proceed if we don't have quorum available as the request will definitely fail + if !state.quorum_available() { + 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(), @@ -135,7 +83,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,10 +124,14 @@ pub(crate) async fn try_obtain_wallet_shares( let shares = wallet_shares.len(); if shares < threshold as usize { - return Err(VpnApiError::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 @@ -199,12 +151,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 +189,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 +272,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 deleted file mode 100644 index 2ec685d656a..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::VpnApiError; -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<(), VpnApiError> { - 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(VpnApiError::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(VpnApiError::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(VpnApiError::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}") - } - } - } - } - } -} 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..10de031b3ba --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/helpers.rs @@ -0,0 +1,101 @@ +// 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) + .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..29e93f234db --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/mod.rs @@ -0,0 +1,306 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +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::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::Arc; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::sync::Mutex as AsyncMutex; +use tokio_util::sync::CancellationToken; +use tracing::{debug, error, info, instrument, warn}; +use uuid::Uuid; + +pub(crate) mod helpers; +mod refill_task; + +// TODO: I guess make it configurable +const DEPOSITS_THRESHOLD_P: f32 = 0.1; + +struct DepositsBufferInner { + client: ChainClient, + + required_deposit_cache: RequiredDepositCache, + + storage: CredentialProxyStorage, + 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: CredentialProxyStorage, + client: ChainClient, + required_deposit_cache: RequiredDepositCache, + short_sha: &'static str, + target_amount: usize, + max_concurrent_deposits: usize, + cancellation_token: CancellationToken, + ) -> Result { + 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 { + 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 { + 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(|_| ed25519::PrivateKey::new(&mut rng)) + .collect::>(); + + info!("starting {amount} deposits"); + let mut contents = Vec::new(); + 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 + .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 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, + 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); + } + }; + + deposits_data.push(BufferedDeposit::new(deposit_id, key)); + } + + Ok(PerformedDeposits { + deposits_data, + tx_hash, + requested_on, + deposit_amount, + }) + } + + async fn insert_new_deposits( + &self, + mut deposits: PerformedDeposits, + ) -> Result<(), CredentialProxyError> { + // 1. insert into the db + self.inner.storage.insert_new_deposits(&deposits).await?; + + // 2. update the buffer + self.inner + .unused_deposits + .lock() + .await + .append(&mut deposits.deposits_data); + 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 possible + 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, + requested_on: OffsetDateTime, + client_pubkey: PublicKeyUser, + request_uuid: Uuid, + ) -> Result<(), CredentialProxyError> { + self.inner + .storage + .insert_deposit_usage(deposit_id, requested_on, client_pubkey, request_uuid) + .await + } + + 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, + requested_on, + client_pubkey, + request_uuid, + ) + .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, + requested_on, + client_pubkey, + request_uuid, + ) + .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..4f09df55b04 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/deposits_buffer/refill_task.rs @@ -0,0 +1,56 @@ +// 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>; + +#[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, + // we'll have to increase the number of deposits per transaction + join_handle: StdMutex>>, + + in_progress: AtomicBool, +} + +impl RefillTask { + /// 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 56e18a0673d..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; @@ -10,7 +11,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] @@ -118,11 +119,20 @@ pub enum VpnApiError { #[error("failed to create deposit")] DepositFailure, + + #[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 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..d4c6b9589e5 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs @@ -6,15 +6,17 @@ use time::OffsetDateTime; 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::quorum_checker::QuorumStateChecker; use crate::{ cli::Cli, - deposit_maker::DepositMaker, - error::VpnApiError, + error::CredentialProxyError, http::{ state::{ApiState, ChainClient}, HttpServer, }, - storage::VpnApiStorage, + storage::CredentialProxyStorage, tasks::StoragePruner, }; @@ -77,6 +79,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 { @@ -91,30 +94,46 @@ 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(); - 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 deposit_maker = DepositMaker::new( - build_sha_short(), + 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(), + required_deposit_cache.clone(), + build_sha_short(), + cli.deposits_buffer_size, cli.max_concurrent_deposits, cancellation_token.clone(), - ); + ) + .await?; - 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(), + quorum_state, webhook_cfg, chain_client, - deposit_request_sender, + deposits_buffer, + required_deposit_cache, cancellation_token.clone(), ) .await?; @@ -129,7 +148,7 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), VpnApiError> { // 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()); + 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/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..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 @@ -1,15 +1,18 @@ // Copyright 2024 Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::deposit_maker::{DepositRequest, DepositRequestSender}; -use crate::error::VpnApiError; +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; use crate::http::types::RequestError; use crate::nym_api_helpers::{ ensure_sane_expiration_date, query_all_threshold_apis, CachedEpoch, CachedImmutableEpochItem, CachedImmutableItems, }; -use crate::storage::VpnApiStorage; +use crate::quorum_checker::QuorumState; +use crate::storage::CredentialProxyStorage; use crate::webhook::ZkNymWebHookConfig; use axum::http::StatusCode; use bip39::Mnemonic; @@ -19,7 +22,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, @@ -29,12 +32,13 @@ 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; 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,33 +53,46 @@ 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, error, 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 { - 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, + quorum_state: QuorumState, zk_nym_web_hook_config: ZkNymWebHookConfig, client: ChainClient, - deposit_requester: DepositRequestSender, + deposits_buffer: DepositsBuffer, + required_deposit_cache: RequiredDepositCache, cancellation_token: CancellationToken, - ) -> Result { + ) -> Result { let state = ApiState { - inner: Arc::new(ApiStateInner { + inner: Arc::new(CredentialProxyStateInner { storage, client, - ecash_state: EcashState::default(), + ecash_state: EcashState { + required_deposit_cache, + quorum_state, + 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(), - deposit_requester, + deposits_buffer, cancellation_token, }), }; @@ -88,7 +105,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?; @@ -123,6 +140,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,7 +148,11 @@ impl ApiState { &self.inner.zk_nym_web_hook_config } - async fn ensure_credentials_issuable(&self) -> Result<(), VpnApiError> { + pub(crate) fn quorum_available(&self) -> bool { + self.inner.ecash_state.quorum_state.available() + } + + async fn ensure_credentials_issuable(&self) -> Result<(), CredentialProxyError> { let epoch = self.current_epoch().await?; if epoch.state.is_final() { @@ -141,41 +163,29 @@ 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) } } - pub(crate) fn storage(&self) -> &VpnApiStorage { + pub(crate) fn storage(&self) -> &CredentialProxyStorage { &self.inner.storage } - 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() + pub async fn deposit_amount(&self) -> Result { + 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 { + 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 +200,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); @@ -209,16 +219,38 @@ 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 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}") } } @@ -235,7 +267,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 +370,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 +387,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 +403,7 @@ impl ApiState { { Ok(threshold) } else { - Err(VpnApiError::UnavailableThreshold { epoch_id }) + Err(CredentialProxyError::UnavailableThreshold { epoch_id }) } }) .await @@ -388,7 +423,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 +450,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 +477,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 +554,7 @@ impl ApiState { &self, epoch_id: EpochId, expiration_date: Date, - ) -> Result, VpnApiError> { + ) -> Result, CredentialProxyError> { self.inner .ecash_state .expiration_date_signatures @@ -598,25 +633,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)))) @@ -637,14 +672,12 @@ impl ChainClient { } } -// - -struct ApiStateInner { - storage: VpnApiStorage, +struct CredentialProxyStateInner { + storage: CredentialProxyStorage, client: ChainClient, - deposit_requester: DepositRequestSender, + deposits_buffer: DepositsBuffer, zk_nym_web_hook_config: ZkNymWebHookConfig, @@ -655,39 +688,10 @@ 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) quorum_state: QuorumState, pub(crate) cached_epoch: RwLock, @@ -714,11 +718,12 @@ 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, info: Vec<(String, Coin)>, - ) -> Result { + ) -> Result { let address = self.inner.address(); let starting_sequence = self.inner.get_sequence(&address).await?.sequence; @@ -727,7 +732,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/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/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/main.rs b/nym-credential-proxy/nym-credential-proxy/src/main.rs index 1ca9ccf3162..08f1dd013d6 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/main.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/main.rs @@ -1,11 +1,6 @@ // 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)] - cfg_if::cfg_if! { if #[cfg(unix)] { use crate::cli::Cli; @@ -18,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; @@ -26,6 +20,8 @@ cfg_if::cfg_if! { pub mod storage; pub mod tasks; mod webhook; + mod deposits_buffer; + mod quorum_checker; } } @@ -55,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) } 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/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/storage/manager.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs index b5fefae4490..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::VpnApiError; 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 { @@ -42,7 +42,7 @@ impl SqliteStorageManager { 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 + 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 @@ -106,7 +106,7 @@ impl SqliteStorageManager { t1.epoch_id as "epoch_id!", t1.expiration_date as "expiration_date!: Date" FROM partial_blinded_wallet as t1 - JOIN ticketbook_deposit as t2 + 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 @@ -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<(), sqlx::Error> { 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<(), sqlx::Error> { 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<(), sqlx::Error> { sqlx::query!( r#" DELETE FROM partial_blinded_wallet_failure WHERE created < ? @@ -370,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(()) } 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..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::error::VpnApiError; +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,19 +23,20 @@ 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) -> Result { + pub async fn init + Debug>( + database_path: P, + ) -> Result { debug!("Attempting to connect to database"); let opts = sqlx::sqlite::SqliteConnectOptions::new() @@ -69,7 +68,7 @@ impl VpnApiStorage { info!("Database migration finished!"); - Ok(VpnApiStorage { + Ok(CredentialProxyStorage { storage_manager: SqliteStorageManager { connection_pool }, }) } @@ -78,7 +77,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 +87,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 +97,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 +109,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 +120,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 +131,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 +143,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,14 +155,15 @@ impl VpnApiStorage { available_shares: usize, device_id: &str, credential_id: &str, - ) -> Result { - self.storage_manager + ) -> Result { + 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( @@ -172,18 +172,19 @@ impl VpnApiStorage { device_id: &str, credential_id: &str, error: &str, - ) -> Result { - self.storage_manager + ) -> Result { + 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<(), 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 @@ -192,46 +193,72 @@ 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, - ) -> Result<(), VpnApiError> { - debug!("inserting deposit data"); + deposits: &PerformedDeposits, + ) -> Result<(), CredentialProxyError> { + 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, 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 +294,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 +305,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 +323,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 +336,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 +359,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 +372,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( @@ -374,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; @@ -381,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, } @@ -393,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()); @@ -407,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) @@ -426,7 +458,7 @@ mod tests { } impl Deref for StorageTestWrapper { - type Target = VpnApiStorage; + type Target = CredentialProxyStorage; fn deref(&self) -> &Self::Target { &self.inner } @@ -451,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; @@ -459,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); @@ -471,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); @@ -484,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..475b81ef3e5 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, @@ -22,6 +22,7 @@ impl StoragePruner { info!("starting the storage pruner task"); loop { tokio::select! { + biased; _ = self.cancellation_token.cancelled() => { break } 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(|_| ()) }