diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs index 29e7c52d67..9a68702167 100644 --- a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs @@ -226,4 +226,8 @@ impl EpochState { pub fn is_final(&self) -> bool { *self == EpochState::InProgress } + + pub fn is_in_progress(&self) -> bool { + matches!(self, EpochState::InProgress) + } } 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 103c1ad102..b97bae206f 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -229,26 +229,11 @@ where iv, )?; - // Get the latest coconut signers and their VK - let credential_api_clients = self + let aggregated_verification_key = self .inner .coconut_verifier - .all_coconut_api_clients(*credential.epoch_id()) + .verification_key(*credential.epoch_id()) .await?; - let current_api_clients = self - .inner - .coconut_verifier - .all_current_coconut_api_clients() - .await?; - if credential_api_clients.is_empty() || current_api_clients.is_empty() { - return Err(RequestHandlingError::NotEnoughNymAPIs { - received: 0, - needed: 1, - }); - } - - let aggregated_verification_key = - nym_credentials::obtain_aggregate_verification_key(&credential_api_clients).await?; if !credential.verify(&aggregated_verification_key) { return Err(RequestHandlingError::InvalidBandwidthCredential( @@ -256,9 +241,15 @@ where )); } + let api_clients = self + .inner + .coconut_verifier + .api_clients(*credential.epoch_id()) + .await?; + self.inner .coconut_verifier - .release_funds(current_api_clients, &credential) + .release_funds(&api_clients, &credential) .await?; let bandwidth = Bandwidth::from(credential); 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 d4929d378e..ef22786fcb 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs @@ -1,57 +1,151 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use super::authenticated::RequestHandlingError; use log::*; -use nym_coconut_interface::Credential; +use nym_coconut_interface::{Credential, VerificationKey}; use nym_validator_client::coconut::all_coconut_api_clients; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::MultisigQueryClient; use nym_validator_client::nyxd::AccountId; use nym_validator_client::{ nyxd::{ - contract_traits::{ - CoconutBandwidthSigningClient, DkgQueryClient, MultisigQueryClient, - MultisigSigningClient, - }, + contract_traits::{CoconutBandwidthSigningClient, DkgQueryClient, MultisigSigningClient}, cosmwasm_client::logs::{find_attribute, BANDWIDTH_PROPOSAL_ID}, Coin, }, CoconutApiClient, DirectSigningHttpRpcNyxdClient, }; +use std::collections::HashMap; use std::ops::Deref; -use tokio::sync::RwLock; +use tokio::sync::{RwLock, RwLockReadGuard}; pub(crate) struct CoconutVerifier { address: AccountId, nyxd_client: RwLock, + + // **CURRENTLY** api client addresses don't change during the epochs + api_clients: RwLock>>, + + // keys never change during epochs + master_keys: RwLock>, mix_denom_base: String, } impl CoconutVerifier { - pub async fn new(nyxd_client: DirectSigningHttpRpcNyxdClient) -> Self { + pub async fn new( + nyxd_client: DirectSigningHttpRpcNyxdClient, + ) -> Result { let mix_denom_base = nyxd_client.current_chain_details().mix_denom.base.clone(); let address = nyxd_client.address(); - CoconutVerifier { + let mut master_keys = HashMap::new(); + let mut api_clients = HashMap::new(); + + // might as well obtain the key for the current epoch, if applicable + let current_epoch = nyxd_client.get_current_epoch().await?; + if current_epoch.state.is_in_progress() { + // note: even though we're constructing clients here, we will NOT be making any network requests + let epoch_api_clients = + all_coconut_api_clients(&nyxd_client, current_epoch.epoch_id).await?; + let threshold = nyxd_client.get_current_epoch_threshold().await?; + + // SAFETY: + // if epoch state is in the 'in progress' state, it means the threshold value MUST HAVE + // been established. if it wasn't, there's an underlying issue with the DKG contract in which + // case we shouldn't continue anyway because here be dragons + #[allow(clippy::expect_used)] + let threshold = threshold.expect("unavailable threshold value") as usize; + if epoch_api_clients.len() < threshold { + return Err(RequestHandlingError::NotEnoughNymAPIs { + received: epoch_api_clients.len(), + needed: threshold, + }); + } + let aggregated_verification_key = + nym_credentials::obtain_aggregate_verification_key(&epoch_api_clients).await?; + + api_clients.insert(current_epoch.epoch_id, epoch_api_clients); + master_keys.insert(current_epoch.epoch_id, aggregated_verification_key); + } + + Ok(CoconutVerifier { address, nyxd_client: RwLock::new(nyxd_client), + api_clients: RwLock::new(api_clients), + master_keys: RwLock::new(master_keys), mix_denom_base, - } + }) } - pub async fn all_current_coconut_api_clients( + pub async fn api_clients( &self, - ) -> Result, RequestHandlingError> { - let epoch_id = self - .nyxd_client - .read() - .await - .get_current_epoch() - .await? - .epoch_id; - self.all_coconut_api_clients(epoch_id).await + epoch_id: EpochId, + ) -> Result>, RequestHandlingError> { + let guard = self.api_clients.read().await; + + // the key was already in the map + if let Ok(mapped) = RwLockReadGuard::try_map(guard, |clients| clients.get(&epoch_id)) { + return Ok(mapped); + } + + let api_clients = self.query_api_clients(epoch_id).await?; + + // EDGE CASE: + // if this epoch is from the past, we can't query for its threshold + // we can only hope that enough valid keys were submitted + // the best we can do is check if we have at least a api + if api_clients.is_empty() { + return Err(RequestHandlingError::NotEnoughNymAPIs { + received: 0, + needed: 1, + }); + } + + let mut guard = self.api_clients.write().await; + guard.insert(epoch_id, api_clients); + let guard = guard.downgrade(); + + // SAFETY: + // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) + // so it MUST exist and thus the unwrap is fine + #[allow(clippy::unwrap_used)] + Ok(RwLockReadGuard::map(guard, |clients| { + clients.get(&epoch_id).unwrap() + })) } - pub async fn all_coconut_api_clients( + pub async fn verification_key( + &self, + epoch_id: EpochId, + ) -> Result, RequestHandlingError> { + let guard = self.master_keys.read().await; + + // the key was already in the map + if let Ok(mapped) = RwLockReadGuard::try_map(guard, |keys| keys.get(&epoch_id)) { + return Ok(mapped); + } + + let api_clients = self.api_clients(epoch_id).await?; + + let mut guard = self.master_keys.write().await; + + let aggregated_verification_key = + nym_credentials::obtain_aggregate_verification_key(&api_clients).await?; + + guard.insert(epoch_id, aggregated_verification_key); + let guard = guard.downgrade(); + + // SAFETY: + // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) + // so it MUST exist and thus the unwrap is fine + #[allow(clippy::unwrap_used)] + Ok(RwLockReadGuard::map(guard, |keys| { + keys.get(&epoch_id).unwrap() + })) + } + + pub async fn query_api_clients( &self, epoch_id: u64, ) -> Result, RequestHandlingError> { @@ -60,7 +154,7 @@ impl CoconutVerifier { pub async fn release_funds( &self, - api_clients: Vec, + api_clients: &[CoconutApiClient], credential: &Credential, ) -> Result<(), RequestHandlingError> { let res = self diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 7c147670ac..9fa6d1af8e 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -457,7 +457,7 @@ impl Gateway { let coconut_verifier = { let nyxd_client = self.random_nyxd_client()?; CoconutVerifier::new(nyxd_client).await - }; + }?; let mix_forwarding_channel = self.start_packet_forwarder(shutdown.subscribe().named("PacketForwarder"));