nym_bandwidth_controller/
lib.rs

1// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
2// SPDX-License-Identifier: Apache-2.0
3
4#![warn(clippy::expect_used)]
5#![warn(clippy::unwrap_used)]
6#![warn(clippy::todo)]
7#![warn(clippy::dbg_macro)]
8
9use crate::error::BandwidthControllerError;
10use crate::utils::{
11    get_aggregate_verification_key, get_coin_index_signatures, get_expiration_date_signatures,
12    ApiClientsWrapper,
13};
14use log::error;
15use nym_credential_storage::models::{EmergencyCredential, RetrievedTicketbook};
16use nym_credential_storage::storage::Storage;
17use nym_credentials::ecash::bandwidth::CredentialSpendingData;
18use nym_credentials_interface::{
19    AnnotatedCoinIndexSignature, AnnotatedExpirationDateSignature, TicketType, VerificationKeyAuth,
20};
21use nym_ecash_time::Date;
22use nym_validator_client::nym_api::EpochId;
23use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
24
25pub use event::BandwidthStatusMessage;
26pub use traits::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND};
27
28pub mod acquire;
29pub mod error;
30mod event;
31pub mod mock;
32mod traits;
33mod utils;
34
35#[derive(Debug)]
36pub struct BandwidthController<C, St> {
37    storage: St,
38    client: C,
39}
40
41pub struct PreparedCredential {
42    /// The cryptographic material required for spending the underlying credential.
43    pub data: CredentialSpendingData,
44
45    /// The (DKG) epoch id under which the credential has been issued so that the verifier
46    /// could use correct verification key for validation.
47    pub epoch_id: EpochId,
48
49    /// Auxiliary metadata associated with the withdrawn credential
50    pub metadata: PreparedCredentialMetadata,
51}
52
53#[derive(Copy, Clone)]
54pub struct PreparedCredentialMetadata {
55    /// The database id of the stored credential.
56    pub ticketbook_id: i64,
57
58    /// The number of tickets withdrawn in this credential
59    pub tickets_withdrawn: u32,
60
61    /// The amount of tickets used INCLUDING those tickets that JUST got withdrawn
62    pub used_tickets: u32,
63}
64
65impl<C, St: Storage> BandwidthController<C, St> {
66    pub fn new(storage: St, client: C) -> Self {
67        BandwidthController { storage, client }
68    }
69
70    /// Tries to retrieve one of the stored, unused credentials for the given type that hasn't yet expired.
71    pub async fn get_next_usable_ticketbook(
72        &self,
73        ticketbook_type: TicketType,
74        tickets: u32,
75    ) -> Result<RetrievedTicketbook, BandwidthControllerError>
76    where
77        <St as Storage>::StorageError: Send + Sync + 'static,
78    {
79        let Some(ticketbook) = self
80            .storage
81            .get_next_unspent_usable_ticketbook(ticketbook_type.to_string(), tickets)
82            .await
83            .map_err(BandwidthControllerError::credential_storage_error)?
84        else {
85            return Err(BandwidthControllerError::NoCredentialsAvailable);
86        };
87
88        Ok(ticketbook)
89    }
90
91    pub async fn attempt_revert_ticket_usage(
92        &self,
93        info: PreparedCredentialMetadata,
94    ) -> Result<bool, BandwidthControllerError>
95    where
96        <St as Storage>::StorageError: Send + Sync + 'static,
97    {
98        self.storage
99            .attempt_revert_ticketbook_withdrawal(
100                info.ticketbook_id,
101                info.used_tickets,
102                info.tickets_withdrawn,
103            )
104            .await
105            .map_err(BandwidthControllerError::credential_storage_error)
106    }
107
108    async fn get_aggregate_verification_key(
109        &self,
110        epoch_id: EpochId,
111        ecash_apis: &mut ApiClientsWrapper<'_, C>,
112    ) -> Result<VerificationKeyAuth, BandwidthControllerError>
113    where
114        C: DkgQueryClient + Sync + Send,
115        <St as Storage>::StorageError: Send + Sync + 'static,
116    {
117        get_aggregate_verification_key(&self.storage, epoch_id, ecash_apis).await
118    }
119
120    async fn get_coin_index_signatures(
121        &self,
122        epoch_id: EpochId,
123        ecash_apis: &mut ApiClientsWrapper<'_, C>,
124    ) -> Result<Vec<AnnotatedCoinIndexSignature>, BandwidthControllerError>
125    where
126        C: DkgQueryClient + Sync + Send,
127        <St as Storage>::StorageError: Send + Sync + 'static,
128    {
129        get_coin_index_signatures(&self.storage, epoch_id, ecash_apis).await
130    }
131
132    async fn get_expiration_date_signatures(
133        &self,
134        epoch_id: EpochId,
135        expiration_date: Date,
136        ecash_apis: &mut ApiClientsWrapper<'_, C>,
137    ) -> Result<Vec<AnnotatedExpirationDateSignature>, BandwidthControllerError>
138    where
139        C: DkgQueryClient + Sync + Send,
140        <St as Storage>::StorageError: Send + Sync + 'static,
141    {
142        get_expiration_date_signatures(&self.storage, epoch_id, expiration_date, ecash_apis).await
143    }
144
145    async fn prepare_ecash_ticket_inner(
146        &self,
147        provider_pk: [u8; 32],
148        tickets_to_spend: u32,
149        mut retrieved_ticketbook: RetrievedTicketbook,
150    ) -> Result<CredentialSpendingData, BandwidthControllerError>
151    where
152        C: DkgQueryClient + Sync + Send,
153        <St as Storage>::StorageError: Send + Sync + 'static,
154    {
155        let epoch_id = retrieved_ticketbook.ticketbook.epoch_id();
156        let expiration_date = retrieved_ticketbook.ticketbook.expiration_date();
157        let mut api_clients = ApiClientsWrapper::new(&self.client, epoch_id);
158
159        let verification_key = self
160            .get_aggregate_verification_key(epoch_id, &mut api_clients)
161            .await?;
162        let expiration_signatures = self
163            .get_expiration_date_signatures(epoch_id, expiration_date, &mut api_clients)
164            .await?;
165        let coin_indices_signatures = self
166            .get_coin_index_signatures(epoch_id, &mut api_clients)
167            .await?;
168
169        let pay_info = retrieved_ticketbook
170            .ticketbook
171            .generate_pay_info(provider_pk);
172
173        let spend_request = retrieved_ticketbook.ticketbook.prepare_for_spending(
174            &verification_key,
175            pay_info.into(),
176            &coin_indices_signatures,
177            &expiration_signatures,
178            tickets_to_spend as u64,
179        )?;
180        Ok(spend_request)
181    }
182
183    pub async fn prepare_ecash_ticket(
184        &self,
185        ticketbook_type: TicketType,
186        provider_pk: [u8; 32],
187        tickets_to_spend: u32,
188    ) -> Result<PreparedCredential, BandwidthControllerError>
189    where
190        C: DkgQueryClient + Sync + Send,
191        <St as Storage>::StorageError: Send + Sync + 'static,
192    {
193        let retrieved_ticketbook = self
194            .get_next_usable_ticketbook(ticketbook_type, tickets_to_spend)
195            .await?;
196
197        let ticketbook_id = retrieved_ticketbook.ticketbook_id;
198        let epoch_id = retrieved_ticketbook.ticketbook.epoch_id();
199
200        let used_tickets =
201            retrieved_ticketbook.ticketbook.spent_tickets() as u32 + tickets_to_spend;
202        let metadata = PreparedCredentialMetadata {
203            ticketbook_id,
204            tickets_withdrawn: tickets_to_spend,
205            used_tickets,
206        };
207
208        match self
209            .prepare_ecash_ticket_inner(provider_pk, tickets_to_spend, retrieved_ticketbook)
210            .await
211        {
212            Ok(data) => Ok(PreparedCredential {
213                data,
214                epoch_id,
215                metadata,
216            }),
217            Err(err) => {
218                error!("failed to prepare credential spending request. attempting to revert withdrawal...");
219                self.attempt_revert_ticket_usage(metadata).await?;
220                Err(err)
221            }
222        }
223    }
224
225    pub async fn get_emergency_credential(
226        &self,
227        typ: &str,
228    ) -> Result<Option<EmergencyCredential>, BandwidthControllerError>
229    where
230        <St as Storage>::StorageError: Send + Sync + 'static,
231    {
232        self.storage
233            .get_emergency_credential(typ)
234            .await
235            .map_err(BandwidthControllerError::credential_storage_error)
236    }
237}
238
239impl<C, St> Clone for BandwidthController<C, St>
240where
241    C: Clone,
242    St: Clone,
243{
244    fn clone(&self) -> Self {
245        BandwidthController {
246            storage: self.storage.clone(),
247            client: self.client.clone(),
248        }
249    }
250}