diff --git a/common/coconut-interface/src/lib.rs b/common/coconut-interface/src/lib.rs index 99d71a04b3..44263c51ca 100644 --- a/common/coconut-interface/src/lib.rs +++ b/common/coconut-interface/src/lib.rs @@ -49,7 +49,11 @@ impl Credential { } } - pub fn blinded_serial_number(&self) -> String { + pub fn blinded_serial_number(&self) -> &BlindedSerialNumber { + &self.theta.blinded_serial_number + } + + pub fn blinded_serial_number_bs58(&self) -> String { self.theta.blinded_serial_number_bs58() } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index b97bae206f..1faedac744 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -19,8 +19,10 @@ use thiserror::Error; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError}; +use std::cmp::max; use std::{convert::TryFrom, process, time::Duration}; +use crate::node::client_handling::websocket::connection_handler::coconut::BANDWIDTH_PER_CREDENTIAL; use crate::node::{ client_handling::{ bandwidth::Bandwidth, @@ -58,6 +60,9 @@ pub(crate) enum RequestHandlingError { #[error("Provided bandwidth credential did not verify correctly on {0}")] InvalidBandwidthCredential(String), + #[error("the provided bandwidth credential has already been spent before at this gateway")] + BandwidthCredentialAlreadySpent, + #[error("This gateway is only accepting coconut credentials for bandwidth")] OnlyCoconutCredentials, @@ -229,6 +234,17 @@ where iv, )?; + // check if the credential hasn't been spent before + let already_spent = self + .inner + .storage + .contains_credential(credential.blinded_serial_number()) + .await?; + if already_spent { + return Err(RequestHandlingError::BandwidthCredentialAlreadySpent); + } + + // locally verify the credential let aggregated_verification_key = self .inner .coconut_verifier @@ -241,19 +257,36 @@ where )); } - let api_clients = self - .inner - .coconut_verifier - .api_clients(*credential.epoch_id()) + // technically this is not atomic, i.e. checking for the spending and then marking as spent, + // but because we have the `UNIQUE` constraint on the database table + // if somebody attempts to spend the same credential in another, parallel request, + // one of them will fail + // + // mark the credential as spent + // TODO: technically this should be done under a storage transaction so that if we experience any + // failures later on, it'd get reverted + self.inner + .storage + .insert_spent_credential(*credential.blinded_serial_number(), self.client.address) .await?; - self.inner - .coconut_verifier - .release_funds(&api_clients, &credential) - .await?; + // OLD CODE FOR RELEASING FUNDS + // let api_clients = self + // .inner + // .coconut_verifier + // .api_clients(*credential.epoch_id()) + // .await?; + // + // self.inner + // .coconut_verifier + // .release_funds(&api_clients, &credential) + // .await?; let bandwidth = Bandwidth::from(credential); - let bandwidth_value = bandwidth.value(); + + // if somebody decided to use a credential with bunch of tokens in it, sure, grant them that bandwidth + // otherwise use the default value + let bandwidth_value = max(bandwidth.value(), BANDWIDTH_PER_CREDENTIAL); if bandwidth_value > i64::MAX as u64 { // note that this would have represented more than 1 exabyte, diff --git a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs b/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs index 58b32f7031..f83179ff85 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs @@ -20,6 +20,8 @@ use std::collections::HashMap; use std::ops::Deref; use tokio::sync::{RwLock, RwLockReadGuard}; +pub(crate) const BANDWIDTH_PER_CREDENTIAL: u64 = 1024 * 1024 * 1024; // 1GB + pub(crate) struct CoconutVerifier { address: AccountId, nyxd_client: RwLock, @@ -165,7 +167,7 @@ impl CoconutVerifier { credential.voucher_value().into(), self.mix_denom_base.clone(), ), - credential.blinded_serial_number(), + credential.blinded_serial_number_bs58(), self.address.to_string(), None, ) diff --git a/nym-api/src/coconut/api_routes/mod.rs b/nym-api/src/coconut/api_routes/mod.rs index cb86923b89..62d4b830fa 100644 --- a/nym-api/src/coconut/api_routes/mod.rs +++ b/nym-api/src/coconut/api_routes/mod.rs @@ -110,7 +110,11 @@ pub async fn verify_bandwidth_credential( // Credential has not been spent before, and is on its way of being spent let credential_status = state .client - .get_spent_credential(verify_credential_body.credential.blinded_serial_number()) + .get_spent_credential( + verify_credential_body + .credential + .blinded_serial_number_bs58(), + ) .await? .spend_credential .ok_or(CoconutError::InvalidCredentialStatus { diff --git a/nym-api/src/coconut/tests/mod.rs b/nym-api/src/coconut/tests/mod.rs index 6c7f70ad41..42244f65f9 100644 --- a/nym-api/src/coconut/tests/mod.rs +++ b/nym-api/src/coconut/tests/mod.rs @@ -1062,7 +1062,7 @@ async fn verification_of_bandwidth_credential() { ); // Test the endpoint with no msg in the proposal action - proposal.description = credential.blinded_serial_number(); + proposal.description = credential.blinded_serial_number_bs58(); proposal_db .write() .unwrap() @@ -1117,7 +1117,7 @@ async fn verification_of_bandwidth_credential() { ); spent_credential_db.write().unwrap().insert( - credential.blinded_serial_number(), + credential.blinded_serial_number_bs58(), SpendCredentialResponse::new(None), ); let response = client @@ -1140,11 +1140,11 @@ async fn verification_of_bandwidth_credential() { // Test the endpoint with a credential that doesn't verify correctly let mut spent_credential = SpendCredential::new( funds.clone().into(), - credential.blinded_serial_number(), + credential.blinded_serial_number_bs58(), Addr::unchecked("unimportant"), ); spent_credential_db.write().unwrap().insert( - credential.blinded_serial_number(), + credential.blinded_serial_number_bs58(), SpendCredentialResponse::new(Some(spent_credential.clone())), ); let bad_credential = Credential::new( @@ -1259,7 +1259,7 @@ async fn verification_of_bandwidth_credential() { // Test the endpoint with the credential marked as Spent in the Coconut Bandwidth Contract spent_credential.mark_as_spent(); spent_credential_db.write().unwrap().insert( - credential.blinded_serial_number(), + credential.blinded_serial_number_bs58(), SpendCredentialResponse::new(Some(spent_credential)), ); let response = client