nym_credential_verification/
lib.rs

1// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::ecash::traits::EcashManager;
5use async_trait::async_trait;
6use bandwidth_storage_manager::BandwidthStorageManager;
7use nym_credentials::ecash::utils::{EcashTime, cred_exp_date, ecash_today};
8use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType};
9use nym_gateway_requests::models::CredentialSpendingRequest;
10use std::sync::Arc;
11use std::time::Instant;
12use time::{Date, OffsetDateTime};
13use tracing::*;
14
15pub use client_bandwidth::*;
16pub use error::*;
17pub use upgrade_mode::UpgradeModeState;
18
19pub mod bandwidth_storage_manager;
20mod client_bandwidth;
21pub mod ecash;
22pub mod error;
23pub mod upgrade_mode;
24
25// Histogram buckets for ecash verification duration (in seconds)
26const ECASH_VERIFICATION_DURATION_BUCKETS: &[f64] =
27    &[0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0];
28
29pub struct CredentialVerifier {
30    credential: CredentialSpendingRequest,
31    ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
32    bandwidth_storage_manager: BandwidthStorageManager,
33}
34
35impl CredentialVerifier {
36    pub fn new(
37        credential: CredentialSpendingRequest,
38        ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
39        bandwidth_storage_manager: BandwidthStorageManager,
40    ) -> Self {
41        CredentialVerifier {
42            credential,
43            ecash_verifier,
44            bandwidth_storage_manager,
45        }
46    }
47
48    fn check_credential_spending_date(&self, today: Date) -> Result<()> {
49        let proposed = self.credential.data.spend_date;
50        trace!("checking ticket spending date...");
51
52        if today != proposed {
53            trace!("invalid credential spending date. received {proposed}");
54            return Err(Error::InvalidCredentialSpendingDate {
55                got: proposed,
56                expected: today,
57            });
58        }
59        Ok(())
60    }
61
62    async fn check_local_db_for_double_spending(&self, serial_number: &[u8]) -> Result<()> {
63        trace!("checking local db for double spending...");
64
65        let spent = self
66            .bandwidth_storage_manager
67            .storage
68            .contains_ticket(serial_number)
69            .await?;
70        if spent {
71            trace!("the credential has already been spent before at this gateway");
72            nym_metrics::inc!("ecash_verification_failures_double_spending");
73            return Err(Error::BandwidthCredentialAlreadySpent);
74        }
75        Ok(())
76    }
77
78    async fn cryptographically_verify_ticket(&self) -> Result<()> {
79        trace!("attempting to perform ticket verification...");
80
81        let aggregated_verification_key = self
82            .ecash_verifier
83            .verification_key(self.credential.data.epoch_id)
84            .await?;
85
86        self.ecash_verifier
87            .check_payment(&self.credential.data, &aggregated_verification_key)
88            .await?;
89        Ok(())
90    }
91
92    async fn store_received_ticket(&self, received_at: OffsetDateTime) -> Result<i64> {
93        trace!("storing received ticket");
94        let ticket_id = self
95            .bandwidth_storage_manager
96            .storage
97            .insert_received_ticket(
98                self.bandwidth_storage_manager.client_id,
99                received_at,
100                self.credential.encoded_serial_number(),
101                self.credential.to_bytes(),
102            )
103            .await?;
104        Ok(ticket_id)
105    }
106
107    fn async_verify_ticket(&self, ticket_id: i64) {
108        let client_ticket = ClientTicket::new(self.credential.data.clone(), ticket_id);
109
110        self.ecash_verifier.async_verify(client_ticket);
111    }
112
113    pub async fn verify(&mut self) -> Result<i64> {
114        let start = Instant::now();
115        nym_metrics::inc!("ecash_verification_attempts");
116
117        let received_at = OffsetDateTime::now_utc();
118        let spend_date = ecash_today();
119
120        // check if the credential hasn't been spent before
121        let serial_number = self.credential.data.encoded_serial_number();
122        let credential_type = TicketType::try_from_encoded(self.credential.data.payment.t_type)?;
123
124        if self.credential.data.payment.spend_value != 1 {
125            nym_metrics::inc!("ecash_verification_failures_multiple_tickets");
126            return Err(Error::MultipleTickets);
127        }
128
129        if let Err(e) = self.check_credential_spending_date(spend_date.ecash_date()) {
130            nym_metrics::inc!("ecash_verification_failures_invalid_spend_date");
131            return Err(e);
132        }
133
134        self.check_local_db_for_double_spending(&serial_number)
135            .await?;
136
137        // TODO: do we HAVE TO do it?
138        let verify_result = self.cryptographically_verify_ticket().await;
139
140        // Track verification duration
141        let duration = start.elapsed().as_secs_f64();
142        nym_metrics::add_histogram_obs!(
143            "ecash_verification_duration_seconds",
144            duration,
145            ECASH_VERIFICATION_DURATION_BUCKETS
146        );
147
148        // Track epoch ID - use dynamic metric name via registry
149        let epoch_id = self.credential.data.epoch_id;
150        let epoch_metric = format!(
151            "nym_credential_verification_ecash_epoch_{}_verifications",
152            epoch_id
153        );
154        nym_metrics::metrics_registry().maybe_register_and_inc(&epoch_metric, None);
155
156        // Check verification result after timing
157        verify_result?;
158
159        let ticket_id = self.store_received_ticket(received_at).await?;
160        self.async_verify_ticket(ticket_id);
161
162        // TODO: double storing?
163        // self.store_spent_credential(serial_number_bs58).await?;
164
165        let bandwidth = Bandwidth::ticket_amount(credential_type.into());
166
167        self.bandwidth_storage_manager
168            .increase_bandwidth(bandwidth, cred_exp_date())
169            .await?;
170
171        nym_metrics::inc!("ecash_verification_success");
172
173        Ok(self
174            .bandwidth_storage_manager
175            .client_bandwidth
176            .available()
177            .await)
178    }
179}
180
181#[async_trait]
182pub trait TicketVerifier {
183    /// Verify that the ticket is valid and cryptographically correct.
184    /// If the verification succeeds, also increase the bandwidth with the ticket's
185    /// amount and return the latest available bandwidth
186    async fn verify(&mut self) -> Result<i64>;
187}
188
189#[async_trait]
190impl TicketVerifier for CredentialVerifier {
191    async fn verify(&mut self) -> Result<i64> {
192        self.verify().await
193    }
194}