// Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::ecash::traits::EcashManager; use async_trait::async_trait; use bandwidth_storage_manager::BandwidthStorageManager; use nym_credentials::ecash::utils::{EcashTime, cred_exp_date, ecash_today}; use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType}; use nym_gateway_requests::models::CredentialSpendingRequest; use std::sync::Arc; use std::time::Instant; use time::{Date, OffsetDateTime}; use tracing::*; pub use client_bandwidth::*; pub use error::*; pub use upgrade_mode::UpgradeModeState; pub mod bandwidth_storage_manager; mod client_bandwidth; pub mod ecash; pub mod error; pub mod upgrade_mode; const MOCK_BANDWIDTH: i64 = 2024 * 1024 * 1024; // Histogram buckets for ecash verification duration (in seconds) const ECASH_VERIFICATION_DURATION_BUCKETS: &[f64] = &[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0]; pub struct CredentialVerifier { credential: CredentialSpendingRequest, ecash_verifier: Arc, bandwidth_storage_manager: BandwidthStorageManager, } impl CredentialVerifier { pub fn new( credential: CredentialSpendingRequest, ecash_verifier: Arc, bandwidth_storage_manager: BandwidthStorageManager, ) -> Self { CredentialVerifier { credential, ecash_verifier, bandwidth_storage_manager, } } fn check_credential_spending_date(&self, today: Date) -> Result<()> { let proposed = self.credential.data.spend_date; trace!("checking ticket spending date..."); if today != proposed { trace!("invalid credential spending date. received {proposed}"); return Err(Error::InvalidCredentialSpendingDate { got: proposed, expected: today, }); } Ok(()) } async fn check_local_db_for_double_spending(&self, serial_number: &[u8]) -> Result<()> { trace!("checking local db for double spending..."); let spent = self .bandwidth_storage_manager .storage .contains_ticket(serial_number) .await?; if spent { trace!("the credential has already been spent before at this gateway"); nym_metrics::inc!("ecash_verification_failures_double_spending"); return Err(Error::BandwidthCredentialAlreadySpent); } Ok(()) } async fn cryptographically_verify_ticket(&self) -> Result<()> { trace!("attempting to perform ticket verification..."); let aggregated_verification_key = self .ecash_verifier .verification_key(self.credential.data.epoch_id) .await?; self.ecash_verifier .check_payment(&self.credential.data, &aggregated_verification_key) .await?; Ok(()) } async fn store_received_ticket(&self, received_at: OffsetDateTime) -> Result { trace!("storing received ticket"); let ticket_id = self .bandwidth_storage_manager .storage .insert_received_ticket( self.bandwidth_storage_manager.client_id, received_at, self.credential.encoded_serial_number(), self.credential.to_bytes(), ) .await?; Ok(ticket_id) } fn async_verify_ticket(&self, ticket_id: i64) { let client_ticket = ClientTicket::new(self.credential.data.clone(), ticket_id); self.ecash_verifier.async_verify(client_ticket); } pub async fn verify(&mut self) -> Result { if self.ecash_verifier.is_mock() { // if we're in the mock mode (local testing), skip cryptographic verification // and just return a dummy bandwidth value since we don't have blockchain access // Return a reasonable test bandwidth value (e.g., 1GB in bytes) return Ok(MOCK_BANDWIDTH); } let start = Instant::now(); nym_metrics::inc!("ecash_verification_attempts"); let received_at = OffsetDateTime::now_utc(); let spend_date = ecash_today(); // check if the credential hasn't been spent before let serial_number = self.credential.data.encoded_serial_number(); let credential_type = TicketType::try_from_encoded(self.credential.data.payment.t_type)?; if self.credential.data.payment.spend_value != 1 { nym_metrics::inc!("ecash_verification_failures_multiple_tickets"); return Err(Error::MultipleTickets); } if let Err(e) = self.check_credential_spending_date(spend_date.ecash_date()) { nym_metrics::inc!("ecash_verification_failures_invalid_spend_date"); return Err(e); } self.check_local_db_for_double_spending(&serial_number) .await?; // TODO: do we HAVE TO do it? let verify_result = self.cryptographically_verify_ticket().await; // Track verification duration let duration = start.elapsed().as_secs_f64(); nym_metrics::add_histogram_obs!( "ecash_verification_duration_seconds", duration, ECASH_VERIFICATION_DURATION_BUCKETS ); // Track epoch ID - use dynamic metric name via registry let epoch_id = self.credential.data.epoch_id; let epoch_metric = format!( "nym_credential_verification_ecash_epoch_{}_verifications", epoch_id ); nym_metrics::metrics_registry().maybe_register_and_inc(&epoch_metric, None); // Check verification result after timing verify_result?; let ticket_id = self.store_received_ticket(received_at).await?; self.async_verify_ticket(ticket_id); // TODO: double storing? // self.store_spent_credential(serial_number_bs58).await?; let bandwidth = Bandwidth::ticket_amount(credential_type.into()); self.bandwidth_storage_manager .increase_bandwidth(bandwidth, cred_exp_date()) .await?; nym_metrics::inc!("ecash_verification_success"); Ok(self .bandwidth_storage_manager .client_bandwidth .available() .await) } } #[async_trait] pub trait TicketVerifier { /// Verify that the ticket is valid and cryptographically correct. /// If the verification succeeds, also increase the bandwidth with the ticket's /// amount and return the latest available bandwidth async fn verify(&mut self) -> Result; } #[async_trait] impl TicketVerifier for CredentialVerifier { async fn verify(&mut self) -> Result { self.verify().await } }