// Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::error::BandwidthControllerError; use log::warn; use nym_credential_storage::storage::Storage; use nym_credentials::ecash::bandwidth::serialiser::keys::EpochVerificationKey; use nym_credentials::ecash::bandwidth::serialiser::signatures::{ AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, }; use nym_credentials_interface::{ AnnotatedCoinIndexSignature, AnnotatedExpirationDateSignature, VerificationKeyAuth, }; use nym_ecash_time::Date; use nym_validator_client::coconut::all_ecash_api_clients; use nym_validator_client::nym_api::EpochId; use nym_validator_client::nyxd::contract_traits::DkgQueryClient; use nym_validator_client::EcashApiClient; use rand::prelude::SliceRandom; use rand::thread_rng; use std::fmt::Display; use std::future::Future; pub(crate) trait EcashClientsProvider { async fn try_get_ecash_clients( &mut self, ) -> Result, BandwidthControllerError>; } impl EcashClientsProvider for Vec { async fn try_get_ecash_clients( &mut self, ) -> Result, BandwidthControllerError> { Ok(self.clone()) } } impl EcashClientsProvider for &mut ApiClientsWrapper<'_, C> where C: DkgQueryClient + Sync + Send, { async fn try_get_ecash_clients( &mut self, ) -> Result, BandwidthControllerError> { self.clients().await } } pub(crate) enum ApiClientsWrapper<'a, C> { Uninitialised { query_client: &'a C, epoch_id: EpochId, }, Cached { clients: Vec, }, } impl<'a, C> ApiClientsWrapper<'a, C> { pub(crate) fn new(query_client: &'a C, epoch_id: EpochId) -> Self { ApiClientsWrapper::Uninitialised { query_client, epoch_id, } } async fn clients(&mut self) -> Result, BandwidthControllerError> where C: DkgQueryClient + Sync + Send, { match self { ApiClientsWrapper::Uninitialised { query_client, epoch_id, } => { let clients = all_ecash_api_clients(*query_client, *epoch_id).await?; *self = ApiClientsWrapper::Cached { clients: clients.clone(), }; Ok(clients) } ApiClientsWrapper::Cached { clients } => Ok(clients.clone()), } } } pub(crate) async fn query_random_apis_until_success( mut apis: Vec, f: F, typ: impl Into, ) -> Result where F: Fn(EcashApiClient) -> U, U: Future>, E: Display, { // try apis in pseudorandom way to remove any bias towards the first registered dealer apis.shuffle(&mut thread_rng()); for api in apis { let disp = api.to_string(); match f(api).await { Ok(res) => return Ok(res), Err(err) => { warn!("failed to obtain valid response from API {disp}: {err}") } } } Err(BandwidthControllerError::ExhaustedApiQueries { typ: typ.into() }) } pub(crate) async fn get_aggregate_verification_key( storage: &St, epoch_id: EpochId, mut ecash_apis: impl EcashClientsProvider, ) -> Result where St: Storage, ::StorageError: Send + Sync + 'static, { if let Some(stored) = storage .get_master_verification_key(epoch_id) .await .map_err(BandwidthControllerError::credential_storage_error)? { return Ok(stored); }; let ecash_apis = ecash_apis.try_get_ecash_clients().await?; let master_vk = query_random_apis_until_success( ecash_apis, |api| async move { api.api_client.master_verification_key(Some(epoch_id)).await }, format!("aggregated verification key for epoch {epoch_id}"), ) .await? .key; let full = EpochVerificationKey { epoch_id, key: master_vk, }; // store the retrieved key storage .insert_master_verification_key(&full) .await .map_err(BandwidthControllerError::credential_storage_error)?; Ok(full.key) } pub(crate) async fn get_coin_index_signatures( storage: &St, epoch_id: EpochId, mut ecash_apis: impl EcashClientsProvider, ) -> Result, BandwidthControllerError> where St: Storage, ::StorageError: Send + Sync + 'static, { if let Some(stored) = storage .get_coin_index_signatures(epoch_id) .await .map_err(BandwidthControllerError::credential_storage_error)? { return Ok(stored); }; let ecash_apis = ecash_apis.try_get_ecash_clients().await?; let index_sigs = query_random_apis_until_success( ecash_apis, |api| async move { api.api_client .global_coin_indices_signatures(Some(epoch_id)) .await }, format!("aggregated coin index signatures for epoch {epoch_id}"), ) .await? .signatures; let aggregated = AggregatedCoinIndicesSignatures { epoch_id, signatures: index_sigs, }; // store the retrieved key storage .insert_coin_index_signatures(&aggregated) .await .map_err(BandwidthControllerError::credential_storage_error)?; Ok(aggregated.signatures) } pub(crate) async fn get_expiration_date_signatures( storage: &St, epoch_id: EpochId, expiration_date: Date, mut ecash_apis: impl EcashClientsProvider, ) -> Result, BandwidthControllerError> where St: Storage, ::StorageError: Send + Sync + 'static, { if let Some(stored) = storage .get_expiration_date_signatures(expiration_date) .await .map_err(BandwidthControllerError::credential_storage_error)? { return Ok(stored); }; let ecash_apis = ecash_apis.try_get_ecash_clients().await?; let expiration_sigs = query_random_apis_until_success( ecash_apis, |api| async move { api.api_client .global_expiration_date_signatures(Some(expiration_date)) .await }, format!("aggregated coin index signatures for date {expiration_date}"), ) .await? .signatures; let aggregated = AggregatedExpirationDateSignatures { epoch_id, expiration_date, signatures: expiration_sigs, }; // store the retrieved key storage .insert_expiration_date_signatures(&aggregated) .await .map_err(BandwidthControllerError::credential_storage_error)?; Ok(aggregated.signatures) }