From e338beed761ea47e0395fe56aa33e14e87509cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 30 Oct 2024 14:17:51 +0100 Subject: [PATCH 01/15] Implement non-blocking command dispatch in account controller --- nym-vpn-core/Cargo.lock | 6 +- .../nym-vpn-account-controller/Cargo.toml | 2 + .../src/commands/mod.rs | 152 ++++ .../src/commands/register_device.rs | 29 + .../src/commands/update_state.rs | 135 ++++ .../src/commands/zknym.rs | 129 ++++ .../src/controller.rs | 675 +++++++----------- .../nym-vpn-account-controller/src/error.rs | 14 +- .../nym-vpn-account-controller/src/lib.rs | 5 +- .../nym-vpn-account-controller/src/models.rs | 44 ++ .../crates/nym-vpn-api-client/src/client.rs | 1 + .../nym-vpn-lib/src/platform/account.rs | 4 +- .../src/command_interface/protobuf/account.rs | 5 + .../crates/nym-vpnd/src/service/error.rs | 5 + .../nym-vpnd/src/service/vpn_service.rs | 10 +- 15 files changed, 786 insertions(+), 430 deletions(-) create mode 100644 nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs create mode 100644 nym-vpn-core/crates/nym-vpn-account-controller/src/commands/register_device.rs create mode 100644 nym-vpn-core/crates/nym-vpn-account-controller/src/commands/update_state.rs create mode 100644 nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs create mode 100644 nym-vpn-core/crates/nym-vpn-account-controller/src/models.rs diff --git a/nym-vpn-core/Cargo.lock b/nym-vpn-core/Cargo.lock index 4a8ff21480..d1fac1d12c 100644 --- a/nym-vpn-core/Cargo.lock +++ b/nym-vpn-core/Cargo.lock @@ -4423,6 +4423,7 @@ dependencies = [ name = "nym-vpn-account-controller" version = "1.0.0-alpha.1" dependencies = [ + "futures", "nym-compact-ecash", "nym-config", "nym-credential-storage", @@ -4445,6 +4446,7 @@ dependencies = [ "tokio-util", "tracing", "url", + "uuid", ] [[package]] @@ -7528,9 +7530,9 @@ dependencies = [ [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom 0.2.15", "serde", diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/Cargo.toml b/nym-vpn-core/crates/nym-vpn-account-controller/Cargo.toml index 6cf02daaac..7963dd7f90 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/Cargo.toml +++ b/nym-vpn-core/crates/nym-vpn-account-controller/Cargo.toml @@ -31,3 +31,5 @@ tokio-util.workspace = true tokio.workspace = true tracing.workspace = true url.workspace = true +uuid = "1.11" +futures.workspace = true diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs new file mode 100644 index 0000000000..8265aa2af5 --- /dev/null +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs @@ -0,0 +1,152 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_vpn_api_client::{ + response::NymVpnDevice, + types::{Device, VpnApiAccount}, + VpnApiClient, +}; + +use crate::{ + controller::{AccountSummaryResponse, DevicesResponse, PendingCommands}, + error::Error, + SharedAccountState, +}; + +pub(crate) mod register_device; +pub(crate) mod update_state; +pub(crate) mod zknym; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum AccountCommand { + UpdateAccountState, + RegisterDevice, + RequestZkNym, + GetDeviceZkNym, +} + +#[derive(Clone, Debug)] +pub(crate) enum AccountCommandResult { + UpdatedAccountState, + RegisteredDevice(NymVpnDevice), +} + +pub(crate) struct CommandHandler { + id: uuid::Uuid, + account: VpnApiAccount, + device: Device, + pending_command: PendingCommands, + account_state: SharedAccountState, + vpn_api_client: VpnApiClient, + + last_account_summary: AccountSummaryResponse, + last_devices: DevicesResponse, +} + +impl CommandHandler { + pub(crate) fn new( + account: VpnApiAccount, + device: Device, + pending_command: PendingCommands, + account_state: SharedAccountState, + vpn_api_client: VpnApiClient, + last_account_summary: AccountSummaryResponse, + last_devices: DevicesResponse, + ) -> Self { + let id = uuid::Uuid::new_v4(); + pending_command + .lock() + .map(|mut guard| guard.insert(id, AccountCommand::UpdateAccountState)) + .map_err(|err| { + tracing::error!( + "Failed to insert command {} into pending commands: {:?}", + id, + err + ) + }) + .ok(); + tracing::debug!("Created command handler with id: {}", id); + CommandHandler { + id, + account, + device, + pending_command, + account_state, + vpn_api_client, + last_account_summary, + last_devices, + } + } + + async fn update_shared_account_state(&self) -> Result { + let update_result = update_state::update_state( + &self.account, + &self.device, + &self.account_state, + &self.vpn_api_client, + &self.last_account_summary, + &self.last_devices, + ) + .await + .map(|_| AccountCommandResult::UpdatedAccountState); + tracing::debug!("Current state: {:?}", self.account_state.lock().await); + update_result + } + + async fn register_device(&self) -> Result { + register_device::register_device( + &self.account, + &self.device, + &self.account_state, + &self.vpn_api_client, + ) + .await + .map(AccountCommandResult::RegisteredDevice) + } + + pub(crate) async fn run(self, command: AccountCommand) -> Result { + tracing::debug!("Running command {:?} with id {}", command, self.id); + match command { + AccountCommand::UpdateAccountState => self.update_shared_account_state().await, + AccountCommand::RegisterDevice => self.register_device().await, + AccountCommand::RequestZkNym => { + todo!() + } + AccountCommand::GetDeviceZkNym => { + todo!() + } + } + .inspect(|_result| { + tracing::info!("Command {:?} with id {} completed", command, self.id); + }) + .inspect_err(|err| { + tracing::warn!( + "Command {:?} with id {} completed with error", + command, + self.id + ); + tracing::debug!( + "Command {:?} with id {} failed with error: {:?}", + command, + self.id, + err + ); + }) + } +} + +impl Drop for CommandHandler { + fn drop(&mut self) { + self.pending_command + .lock() + .map(|mut guard| guard.remove(&self.id)) + .inspect_err(|err| { + tracing::error!( + "Failed to remove command {} from pending commands: {:?}", + self.id, + err + ) + }) + .ok(); + } +} diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/register_device.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/register_device.rs new file mode 100644 index 0000000000..6fd340ab51 --- /dev/null +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/register_device.rs @@ -0,0 +1,29 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_vpn_api_client::{ + response::NymVpnDevice, + types::{Device, VpnApiAccount}, +}; + +use crate::{error::Error, shared_state::DeviceState, SharedAccountState}; + +pub(crate) async fn register_device( + account: &VpnApiAccount, + device: &Device, + account_state: &SharedAccountState, + vpn_api_client: &nym_vpn_api_client::VpnApiClient, +) -> Result { + let response = vpn_api_client + .register_device(account, device) + .await + .inspect(|device_result| { + tracing::info!("Response: {:#?}", device_result); + tracing::info!("Device registered: {}", device_result.device_identity_key); + }) + .map_err(Error::RegisterDevice)?; + + let device_state = DeviceState::from(response.status); + account_state.set_device(device_state).await; + Ok(response) +} diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/update_state.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/update_state.rs new file mode 100644 index 0000000000..a87664c2a5 --- /dev/null +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/update_state.rs @@ -0,0 +1,135 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use std::sync::Arc; + +use nym_vpn_api_client::{ + response::{NymErrorResponse, NymVpnAccountSummaryResponse, NymVpnDevicesResponse}, + types::{Device, VpnApiAccount}, + HttpClientError, +}; + +use crate::{ + error::Error, + shared_state::{AccountState, DeviceState, SharedAccountState, SubscriptionState}, +}; + +pub(crate) async fn update_state( + account: &VpnApiAccount, + device: &Device, + account_state: &SharedAccountState, + vpn_api_client: &nym_vpn_api_client::VpnApiClient, + last_account_summary: &Arc>>, + last_devices: &Arc>>, +) -> Result<(), Error> { + update_account_state(account, account_state, vpn_api_client, last_account_summary).await?; + update_device_state(account, device, account_state, vpn_api_client, last_devices).await?; + Ok(()) +} + +async fn update_account_state( + account: &VpnApiAccount, + account_state: &SharedAccountState, + vpn_api_client: &nym_vpn_api_client::VpnApiClient, + last_account_summary: &Arc>>, +) -> Result<(), Error> { + tracing::debug!("Updating account state"); + let response = vpn_api_client.get_account_summary(account).await; + + // Check if the response indicates that we are not registered + if let Some(403) = &response.as_ref().err().and_then(extract_status_code) { + tracing::warn!("NymVPN API reports: access denied (403)"); + account_state.set_account(AccountState::NotRegistered).await; + } + + let account_summary = response.map_err(|source| { + tracing::warn!("NymVPN API error response: {:?}", source); + Error::GetAccountSummary { + base_url: vpn_api_client.current_url().clone(), + source: Box::new(source), + } + })?; + + if last_account_summary + .lock() + .await + .replace(account_summary.clone()) + .as_ref() + != Some(&account_summary) + { + tracing::info!("Account summary: {:#?}", account_summary); + } + + account_state + .set_account(AccountState::from(account_summary.account.status)) + .await; + + account_state + .set_subscription(SubscriptionState::from(account_summary.subscription)) + .await; + + Ok(()) +} + +async fn update_device_state( + account: &VpnApiAccount, + our_device: &Device, + account_state: &SharedAccountState, + vpn_api_client: &nym_vpn_api_client::VpnApiClient, + last_devices: &Arc>>, +) -> Result<(), Error> { + tracing::debug!("Updating device state"); + + let devices = vpn_api_client + .get_devices(account) + .await + .map_err(Error::GetDevices)?; + + if last_devices.lock().await.replace(devices.clone()).as_ref() != Some(&devices) { + tracing::info!("Registered devices: {}", devices); + } + + // TODO: pagination + let found_device = devices + .items + .iter() + .find(|device| device.device_identity_key == our_device.identity_key().to_base58_string()); + + let Some(found_device) = found_device else { + tracing::info!("Our device is not registered"); + account_state.set_device(DeviceState::NotRegistered).await; + return Ok(()); + }; + + account_state + .set_device(DeviceState::from(found_device.status)) + .await; + + Ok(()) +} + +fn extract_status_code(err: &E) -> Option +where + E: std::error::Error + 'static, +{ + let mut source = err.source(); + while let Some(err) = source { + if let Some(status) = err + .downcast_ref::>() + .and_then(extract_status_code_inner) + { + return Some(status); + } + source = err.source(); + } + None +} + +fn extract_status_code_inner( + err: &nym_vpn_api_client::HttpClientError, +) -> Option { + match err { + HttpClientError::EndpointFailure { status, .. } => Some((*status).into()), + _ => None, + } +} diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs new file mode 100644 index 0000000000..a8f8c5df89 --- /dev/null +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs @@ -0,0 +1,129 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use std::time::{Duration, Instant}; + +use nym_compact_ecash::{Base58, WithdrawalRequest}; +use nym_credentials_interface::{RequestInfo, TicketType}; +use nym_ecash_time::EcashTime; +use nym_vpn_api_client::{ + response::{NymVpnZkNym, NymVpnZkNymStatus}, + types::{Device, VpnApiAccount}, + VpnApiClientError, +}; + +use crate::error::Error; + +pub(crate) struct ZkNymRequestData { + withdrawal_request: WithdrawalRequest, + ecash_pubkey: String, + ticketbook_type: TicketType, + request_info: RequestInfo, +} + +pub(crate) fn construct_zk_nym_request_data( + account: &VpnApiAccount, + ticketbook_type: TicketType, +) -> Result { + tracing::info!("Requesting zk-nym by type: {}", ticketbook_type); + + let ecash_keypair = account + .create_ecash_keypair() + .map_err(Error::CreateEcashKeyPair)?; + let expiration_date = nym_ecash_time::ecash_default_expiration_date(); + + let (withdrawal_request, request_info) = nym_compact_ecash::withdrawal_request( + ecash_keypair.secret_key(), + expiration_date.ecash_unix_timestamp(), + ticketbook_type.encode(), + ) + .map_err(Error::ConstructWithdrawalRequest)?; + + let ecash_pubkey = ecash_keypair.public_key().to_base58_string(); + + Ok(ZkNymRequestData { + withdrawal_request, + ecash_pubkey, + ticketbook_type, + request_info, + }) +} + +pub(crate) async fn request_zk_nym( + request: ZkNymRequestData, + account: &VpnApiAccount, + device: &Device, + vpn_api_client: &nym_vpn_api_client::VpnApiClient, +) -> (ZkNymRequestData, Result) { + let response = vpn_api_client + .request_zk_nym( + account, + device, + request.withdrawal_request.to_bs58(), + request.ecash_pubkey.to_owned(), + request.ticketbook_type.to_string(), + ) + .await + .map_err(Error::RequestZkNym); + (request, response) +} + +pub(crate) async fn poll_zk_nym( + request: ZkNymRequestData, + response: NymVpnZkNym, + account: VpnApiAccount, + device: Device, + api_client: nym_vpn_api_client::VpnApiClient, +) -> PollingResult { + tracing::info!("Starting zk-nym polling task for {}", response.id); + let start_time = Instant::now(); + loop { + tokio::time::sleep(std::time::Duration::from_secs(5)).await; + + tracing::info!("Polling zk-nym status: {}", &response.id); + match api_client + .get_zk_nym_by_id(&account, &device, &response.id) + .await + { + Ok(poll_response) if response.status != NymVpnZkNymStatus::Pending => { + tracing::info!("zk-nym polling finished: {:#?}", poll_response); + return PollingResult::Finished( + poll_response, + request.ticketbook_type, + Box::new(request.request_info), + ); + } + Ok(poll_response) => { + tracing::info!("zk-nym polling not finished: {:#?}", poll_response); + if start_time.elapsed() > Duration::from_secs(60) { + tracing::error!("zk-nym polling timed out: {}", response.id); + return PollingResult::Timeout(poll_response); + } + } + Err(error) => { + tracing::error!( + "Failed to poll zk-nym ({}) status: {:#?}", + response.id, + error + ); + return PollingResult::Error(PollingError { + id: response.id, + error, + }); + } + } + } +} + +#[derive(Debug)] +pub(crate) enum PollingResult { + Finished(NymVpnZkNym, TicketType, Box), + Timeout(NymVpnZkNym), + Error(PollingError), +} + +#[derive(Debug)] +pub(crate) struct PollingError { + pub(crate) id: String, + pub(crate) error: VpnApiClientError, +} diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs index 1c485bb06b..f6bf6bb2f0 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs @@ -1,30 +1,22 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use std::{ - path::PathBuf, - sync::Arc, - time::{Duration, Instant}, -}; +use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Duration}; +use futures::StreamExt; use nym_compact_ecash::{Base58 as _, BlindedSignature, VerificationKeyAuth}; use nym_config::defaults::NymNetworkDetails; -use nym_credentials::{ - AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, IssuedTicketBook, -}; +use nym_credentials::IssuedTicketBook; use nym_credentials_interface::{RequestInfo, TicketType}; use nym_ecash_time::EcashTime; use nym_http_api_client::UserAgent; use nym_vpn_api_client::{ response::{ - NymErrorResponse, NymVpnAccountSummaryResponse, NymVpnDevicesResponse, NymVpnZkNym, - NymVpnZkNymStatus, + NymVpnAccountSummaryResponse, NymVpnDevicesResponse, NymVpnZkNym, NymVpnZkNymStatus, }, types::{Device, VpnApiAccount}, - HttpClientError, VpnApiClientError, }; use nym_vpn_store::VpnStorage; -use serde::{Deserialize, Serialize}; use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use tokio::{ sync::mpsc::{UnboundedReceiver, UnboundedSender}, @@ -34,12 +26,17 @@ use tokio_util::sync::CancellationToken; use url::Url; use crate::{ + commands::{ + zknym::{ + construct_zk_nym_request_data, poll_zk_nym, request_zk_nym, PollingResult, + ZkNymRequestData, + }, + AccountCommand, AccountCommandResult, CommandHandler, + }, ecash_client::VpnEcashApiClient, error::Error, - shared_state::{ - AccountState, DeviceState, MnemonicState, ReadyToRegisterDevice, SharedAccountState, - SubscriptionState, - }, + models::WalletShare, + shared_state::{MnemonicState, ReadyToRegisterDevice, SharedAccountState}, storage::{AccountStorage, VpnCredentialStorage}, }; @@ -48,13 +45,10 @@ use crate::{ // data/bandwidth. const TICKET_THRESHOLD: u32 = 10; -#[derive(Clone, Debug)] -pub enum AccountCommand { - UpdateSharedAccountState, - RegisterDevice, - RequestZkNym, - GetDeviceZkNym, -} +pub(crate) type PendingCommands = Arc>>; +pub(crate) type DevicesResponse = Arc>>; +pub(crate) type AccountSummaryResponse = + Arc>>; pub struct AccountController where @@ -78,11 +72,11 @@ where // The last account summary we received from the API. We use this to check if the account state // has changed. - last_account_summary: Option, + last_account_summary: AccountSummaryResponse, // The last devices we received from the API. We use this to check if the device state has // changed. - last_devices: Option, + last_devices: DevicesResponse, // Keep track of the current ecash epoch current_epoch: Option, @@ -90,11 +84,6 @@ where // Tasks used to poll the status of zk-nyms polling_tasks: JoinSet, - // Keep track of which zknyms we have imported, so that we don't import the same twice - // TODO: can we extend the credential store with a name field? - #[allow(unused)] - zk_nym_imported: Vec, - // Receiver channel used to receive commands from the consumer command_rx: UnboundedReceiver, @@ -104,6 +93,12 @@ where // Listen for cancellation signals cancel_token: CancellationToken, + + // Command tasks that are currently running + command_tasks: JoinSet>, + + // List of currently running command tasks and their type + pending_commands: PendingCommands, } impl AccountController @@ -138,88 +133,42 @@ where vpn_api_client: create_api_client(user_agent), vpn_ecash_api_client: create_ecash_api_client(), account_state: SharedAccountState::new(), - last_account_summary: None, - last_devices: None, + last_account_summary: Arc::new(tokio::sync::Mutex::new(None)), + last_devices: Arc::new(tokio::sync::Mutex::new(None)), current_epoch: None, - zk_nym_imported: Default::default(), polling_tasks: JoinSet::new(), command_rx, command_tx, cancel_token, + pending_commands: Default::default(), + command_tasks: JoinSet::new(), }) } - async fn register_device(&self) -> Result<(), Error> { - tracing::info!("Registering device"); - - let account = self.account_storage.load_account().await?; - let device = self.account_storage.load_device_keys().await?; - - self.vpn_api_client - .register_device(&account, &device) - .await - .map(|device_result| { - tracing::info!("Response: {:#?}", device_result); - tracing::info!("Device registered: {}", device_result.device_identity_key); - }) - .map_err(Error::RegisterDevice)?; - - self.command_tx - .send(AccountCommand::UpdateSharedAccountState)?; - - Ok(()) + pub fn shared_state(&self) -> SharedAccountState { + self.account_state.clone() } - async fn request_zk_nym_by_type( - &mut self, - account: &VpnApiAccount, - device: &Device, - ticketbook_type: TicketType, - ) -> Result<(), Error> { - tracing::info!("Requesting zk-nym by type: {}", ticketbook_type); - - let ecash_keypair = account - .create_ecash_keypair() - .map_err(Error::CreateEcashKeyPair)?; - let expiration_date = nym_ecash_time::ecash_default_expiration_date(); - - let (withdrawal_request, request_info) = nym_compact_ecash::withdrawal_request( - ecash_keypair.secret_key(), - expiration_date.ecash_unix_timestamp(), - ticketbook_type.encode(), - ) - .map_err(Error::ConstructWithdrawalRequest)?; - - let ecash_pubkey = ecash_keypair.public_key().to_base58_string(); - - // Insert pending request into credential storage? - // Since the id for the request is a string for the api, and a number in the storage, - // this would be awkard. - - let response = self - .vpn_api_client - .request_zk_nym( - account, - device, - withdrawal_request.to_bs58(), - ecash_pubkey, - ticketbook_type.to_string(), - ) - .await - .map_err(Error::RequestZkNym)?; - - tracing::info!("zk-nym requested: {:#?}", response); + pub fn command_tx(&self) -> UnboundedSender { + self.command_tx.clone() + } - // Spawn a task to poll the status of the zk-nym - self.spawn_polling_task( - response.id, - ticketbook_type, - request_info, - account.clone(), - device.clone(), - ) - .await; + async fn new_command_handler(&self) -> Result { + Ok(CommandHandler::new( + self.account_storage.load_account().await?, + self.account_storage.load_device_keys().await?, + self.pending_commands.clone(), + self.account_state.clone(), + self.vpn_api_client.clone(), + self.last_account_summary.clone(), + self.last_devices.clone(), + )) + } + async fn spawn_command_task(&mut self, command: AccountCommand) -> Result<(), Error> { + tracing::debug!("Spawning command: {:?}", command); + let command_handler = self.new_command_handler().await?; + self.command_tasks.spawn(command_handler.run(command)); Ok(()) } @@ -235,50 +184,86 @@ where async fn spawn_polling_task( &mut self, - id: String, - ticketbook_type: TicketType, - request_info: RequestInfo, + request: ZkNymRequestData, + response: NymVpnZkNym, account: VpnApiAccount, device: Device, ) { + let api_client = self.vpn_api_client.clone(); self.account_state.set_pending_zk_nym(true).await; + self.polling_tasks + .spawn(poll_zk_nym(request, response, account, device, api_client)); + } - let api_client = self.vpn_api_client.clone(); - self.polling_tasks.spawn(async move { - let start_time = Instant::now(); - loop { - tracing::info!("polling zk-nym status: {}", id); - tokio::time::sleep(std::time::Duration::from_secs(5)).await; - match api_client.get_zk_nym_by_id(&account, &device, &id).await { - Ok(response) if response.status != NymVpnZkNymStatus::Pending => { - tracing::info!("zk-nym polling finished: {:#?}", response); - return PollingResult::Finished( - response, - ticketbook_type, - Box::new(request_info), - ); - } - Ok(response) => { - tracing::info!("zk-nym polling not finished: {:#?}", response); - if start_time.elapsed() > Duration::from_secs(60) { - tracing::error!("zk-nym polling timed out: {}", id); - return PollingResult::Timeout(response); - } - } - Err(error) => { - tracing::error!("Failed to poll zk-nym ({}) status: {:#?}", id, error); - return PollingResult::Error(PollingError { id, error }); - } + async fn import_zk_nym( + &mut self, + response: NymVpnZkNym, + ticketbook_type: TicketType, + request_info: RequestInfo, + ) -> Result<(), Error> { + let account = self.account_storage.load_account().await?; + let ecash_keypair = account + .create_ecash_keypair() + .map_err(Error::CreateEcashKeyPair)?; + // TODO: use explicit epoch id, that we include together with the request_info + let current_epoch = self.current_epoch.ok_or(Error::NoEpoch)?; + // TODO: remove unwrap + let vk_auth = self.get_current_verification_key().await?.unwrap(); + + let mut partial_wallets = Vec::new(); + for blinded_share in response.blinded_shares { + // TODO: remove unwrap + let blinded_share: WalletShare = serde_json::from_str(&blinded_share).unwrap(); + + // TODO: remove unwrap + let blinded_sig = + BlindedSignature::try_from_bs58(&blinded_share.bs58_encoded_share).unwrap(); + + match nym_compact_ecash::issue_verify( + &vk_auth, + ecash_keypair.secret_key(), + &blinded_sig, + &request_info, + blinded_share.node_index, + ) { + Ok(partial_wallet) => partial_wallets.push(partial_wallet), + Err(err) => { + tracing::error!("Failed to issue verify: {:#?}", err); + return Err(Error::ImportZkNym(err)); } } - }); + } + + // TODO: remove unwrap + let aggregated_wallets = nym_compact_ecash::aggregate_wallets( + &vk_auth, + ecash_keypair.secret_key(), + &partial_wallets, + &request_info, + ) + .unwrap(); + + // TODO: remove unwrap + let expiration_date = OffsetDateTime::parse(&response.valid_until_utc, &Rfc3339).unwrap(); + + let ticketbook = IssuedTicketBook::new( + aggregated_wallets.into_wallet_signatures(), + current_epoch, + ecash_keypair.into(), + ticketbook_type, + expiration_date.ecash_date(), + ); + + self.credential_storage + .insert_issued_ticketbook(&ticketbook) + .await?; + + Ok(()) } // Check the local credential storage to see if we need to request more zk-nyms, the proceed to // request zk-nyms for each ticket type that we need. - async fn request_zk_nym_all(&mut self) -> Result<(), Error> { - tracing::info!("Requesting zk-nym (inner)"); - + async fn handle_request_zk_nym(&mut self) -> Result<(), Error> { let account = self.account_storage.load_account().await?; let device = self.account_storage.load_device_keys().await?; @@ -291,46 +276,42 @@ where tracing::info!("{}, remaining: {}", ticket_type, remaining); } + // Get the ticket types that are below the threshold let ticket_types_needed_to_request = local_remaining_tickets .into_iter() - .filter_map(|(ticket_type, remaining)| { - if remaining < TICKET_THRESHOLD { - Some(ticket_type) - } else { - None - } - }) + .filter(|(_, remaining)| *remaining < TICKET_THRESHOLD) + .map(|(ticket_type, _)| ticket_type) .collect::>(); - for ticketbook_type in ticket_types_needed_to_request { - if let Err(err) = self - .request_zk_nym_by_type(&account, &device, ticketbook_type) - .await - { - tracing::error!("Failed to request zk-nym: {:#?}", err); + // Request zk-nyms for each ticket type that we need + let responses = futures::stream::iter(ticket_types_needed_to_request) + .filter_map(|ticket_type| { + let account = account.clone(); + async move { construct_zk_nym_request_data(&account, ticket_type).ok() } + }) + .map(|request| { + let account = account.clone(); + let device = device.clone(); + let vpn_api_client = self.vpn_api_client.clone(); + async move { request_zk_nym(request, &account, &device, &vpn_api_client).await } + }) + .buffer_unordered(4) + .collect::>() + .await; + + // Spawn polling tasks for each zk-nym request to monitor the outcome + for (request, response) in responses { + match response { + Ok(response) => { + self.spawn_polling_task(request, response, account.clone(), device.clone()) + .await; + } + Err(err) => { + tracing::error!("Failed to request zk-nym: {:#?}", err); + } } } - Ok(()) - } - // Get and list zk-nyms for the device - async fn get_device_zk_nym(&mut self) -> Result<(), Error> { - tracing::info!("Getting device zk-nym from API"); - - let account = self.account_storage.load_account().await?; - let device = self.account_storage.load_device_keys().await?; - - let reported_device_zk_nyms = self - .vpn_api_client - .get_device_zk_nyms(&account, &device) - .await - .map_err(Error::GetZkNyms)?; - - tracing::info!("The device as the following zk-nyms associated to it on the account:"); - // TODO: pagination - for zk_nym in &reported_device_zk_nyms.items { - tracing::info!("{:?}", zk_nym); - } Ok(()) } @@ -402,96 +383,10 @@ where } } - async fn update_account_state(&mut self, account: &VpnApiAccount) -> Result<(), Error> { - tracing::debug!("Updating account state"); - let response = self.vpn_api_client.get_account_summary(account).await; - - // Check if the response indicates that we are not registered - if let Some(403) = &response.as_ref().err().and_then(extract_status_code) { - tracing::warn!("NymVPN API reports: access denied (403)"); - self.account_state - .set_account(AccountState::NotRegistered) - .await; - } - - let account_summary = response.map_err(|source| { - tracing::warn!("NymVPN API error response: {:?}", source); - Error::GetAccountSummary { - base_url: self.vpn_api_client.current_url().clone(), - source: Box::new(source), - } - })?; - - if self.last_account_summary.as_ref() != Some(&account_summary) { - self.last_account_summary = Some(account_summary.clone()); - tracing::info!("Account summary: {:#?}", account_summary); - } - - self.account_state - .set_account(AccountState::from(account_summary.account.status)) - .await; - - self.account_state - .set_subscription(SubscriptionState::from(account_summary.subscription)) - .await; - - Ok(()) - } - - async fn update_device_state(&mut self, account: &VpnApiAccount) -> Result<(), Error> { - tracing::debug!("Updating device state"); - let our_device = self.account_storage.load_device_keys().await?; - - let devices = self - .vpn_api_client - .get_devices(account) - .await - .map_err(Error::GetDevices)?; - - if self.last_devices.as_ref() != Some(&devices) { - self.last_devices = Some(devices.clone()); - tracing::info!("Registered devices: {}", devices); - } - - // TODO: pagination - let found_device = devices.items.iter().find(|device| { - device.device_identity_key == our_device.identity_key().to_base58_string() - }); - - let Some(found_device) = found_device else { - tracing::info!("Our device is not registered"); - self.account_state - .set_device(DeviceState::NotRegistered) - .await; - return Ok(()); - }; - - self.account_state - .set_device(DeviceState::from(found_device.status)) - .await; - - Ok(()) - } - - pub(crate) async fn update_shared_account_state(&mut self) -> Result<(), Error> { - let Some(account) = self.update_mnemonic_state().await else { - return Ok(()); - }; - - self.update_account_state(&account).await?; - self.update_device_state(&account).await?; - - tracing::debug!("Current state: {}", self.shared_state().lock().await); - - self.register_device_if_ready().await?; - - Ok(()) - } - async fn register_device_if_ready(&self) -> Result<(), Error> { match self.shared_state().is_ready_to_register_device().await { ReadyToRegisterDevice::Ready => { - self.command_tx.send(AccountCommand::RegisterDevice)?; + self.queue_command(AccountCommand::RegisterDevice); } ReadyToRegisterDevice::DeviceAlreadyRegistered => { tracing::debug!("Skipping device registration, already registered"); @@ -510,71 +405,40 @@ where Ok(()) } - async fn import_zk_nym( - &mut self, - response: NymVpnZkNym, - ticketbook_type: TicketType, - request_info: RequestInfo, - ) -> Result<(), Error> { - let account = self.account_storage.load_account().await?; - let ecash_keypair = account - .create_ecash_keypair() - .map_err(Error::CreateEcashKeyPair)?; - // TODO: use explicit epoch id, that we include together with the request_info - let current_epoch = self.current_epoch.ok_or(Error::NoEpoch)?; - // TODO: remove unwrap - let vk_auth = self.get_current_verification_key().await?.unwrap(); - - let mut partial_wallets = Vec::new(); - for blinded_share in response.blinded_shares { - // TODO: remove unwrap - let blinded_share: WalletShare = serde_json::from_str(&blinded_share).unwrap(); - - // TODO: remove unwrap - let blinded_sig = - BlindedSignature::try_from_bs58(&blinded_share.bs58_encoded_share).unwrap(); - - match nym_compact_ecash::issue_verify( - &vk_auth, - ecash_keypair.secret_key(), - &blinded_sig, - &request_info, - blinded_share.node_index, - ) { - Ok(partial_wallet) => partial_wallets.push(partial_wallet), - Err(err) => { - tracing::error!("Failed to issue verify: {:#?}", err); - return Err(Error::ImportZkNym(err)); - } - } - } - - // TODO: remove unwrap - let aggregated_wallets = nym_compact_ecash::aggregate_wallets( - &vk_auth, - ecash_keypair.secret_key(), - &partial_wallets, - &request_info, - ) - .unwrap(); + async fn handle_update_account_state(&mut self) -> Result<(), Error> { + let Some(_account) = self.update_mnemonic_state().await else { + return Ok(()); + }; + self.spawn_command_task(AccountCommand::UpdateAccountState) + .await + } - // TODO: remove unwrap - let expiration_date = OffsetDateTime::parse(&response.valid_until_utc, &Rfc3339).unwrap(); + async fn handle_register_device(&mut self) -> Result<(), Error> { + tracing::debug!("Registering device"); + self.spawn_command_task(AccountCommand::RegisterDevice) + .await + .ok(); + Ok(()) + } - let ticketbook = IssuedTicketBook::new( - aggregated_wallets.into_wallet_signatures(), - current_epoch, - ecash_keypair.into(), - ticketbook_type, - expiration_date.ecash_date(), - ); + // Get and list zk-nyms for the device + async fn handle_get_device_zk_nym(&mut self) -> Result<(), Error> { + tracing::info!("Getting device zk-nym from API"); - self.credential_storage - .insert_issued_ticketbook(&ticketbook) - .await?; + let account = self.account_storage.load_account().await?; + let device = self.account_storage.load_device_keys().await?; - self.zk_nym_imported.push(response.id); + let reported_device_zk_nyms = self + .vpn_api_client + .get_device_zk_nyms(&account, &device) + .await + .map_err(Error::GetZkNyms)?; + tracing::info!("The device as the following zk-nyms associated to it on the account:"); + // TODO: pagination + for zk_nym in &reported_device_zk_nyms.items { + tracing::info!("{:?}", zk_nym); + } Ok(()) } @@ -613,47 +477,87 @@ where } } + async fn is_command_running(&self, command: &AccountCommand) -> Result { + self.pending_commands + .lock() + .map(|guard| { + guard + .values() + .any(|running_command| running_command == command) + }) + .map_err(Error::internal) + } + + fn queue_command(&self, command: AccountCommand) { + if let Err(err) = self.command_tx.send(command) { + tracing::error!("Failed to queue command: {:#?}", err); + } + } + async fn handle_command(&mut self, command: AccountCommand) -> Result<(), Error> { tracing::info!("Received command: {:?}", command); + + if self.is_command_running(&command).await? { + tracing::info!("Command already running, skipping: {:?}", command); + return Ok(()); + } + match command { - AccountCommand::UpdateSharedAccountState => self.update_shared_account_state().await, - AccountCommand::RegisterDevice => self.register_device().await, - AccountCommand::RequestZkNym => self.request_zk_nym_all().await, - AccountCommand::GetDeviceZkNym => self.get_device_zk_nym().await, + AccountCommand::UpdateAccountState => self.handle_update_account_state().await, + AccountCommand::RegisterDevice => self.handle_register_device().await, + AccountCommand::RequestZkNym => self.handle_request_zk_nym().await, + AccountCommand::GetDeviceZkNym => self.handle_get_device_zk_nym().await, + } + } + + async fn handle_command_result( + &self, + result: Result, JoinError>, + ) { + let Ok(result) = result else { + tracing::error!("Polling task failed: {:#?}", result); + return; + }; + + let result = match result { + Ok(result) => result, + Err(err) => { + tracing::warn!("Command failed: {:#?}", err); + return; + } + }; + + match result { + AccountCommandResult::UpdatedAccountState => { + tracing::debug!("Account state updated"); + self.register_device_if_ready().await.ok(); + } + AccountCommandResult::RegisteredDevice(registered_device) => { + tracing::info!("Device registered: {:#?}", registered_device); + self.queue_command(AccountCommand::UpdateAccountState); + } } } async fn cleanup(mut self) { let timeout = tokio::time::sleep(Duration::from_secs(5)); tokio::pin!(timeout); - loop { + while !self.command_tasks.is_empty() && !self.polling_tasks.is_empty() { tokio::select! { _ = &mut timeout => { tracing::warn!("Timeout waiting for polling tasks to finish, pending zk-nym's not imported into local credential store!"); break; - } - result = self.polling_tasks.join_next() => match result { - Some(result) => self.handle_polling_result(result).await, - None => break, - } + }, + Some(result) = self.command_tasks.join_next() => { + self.handle_command_result(result).await + }, + Some(result) = self.polling_tasks.join_next() => { + self.handle_polling_result(result).await + }, } } } - pub fn shared_state(&self) -> SharedAccountState { - self.account_state.clone() - } - - pub fn command_tx(&self) -> UnboundedSender { - self.command_tx.clone() - } - - fn queue_command(&self, command: AccountCommand) { - if let Err(err) = self.command_tx.send(command) { - tracing::error!("Failed to queue command: {:#?}", err); - } - } - async fn print_info(&self) { let account_id = self .account_storage @@ -700,30 +604,41 @@ where }) .ok(); + // Timer to check if any command tasks have finished + let mut command_finish_timer = tokio::time::interval(Duration::from_millis(500)); + // Timer to check if any zk-nym polling tasks have finished let mut polling_timer = tokio::time::interval(Duration::from_millis(500)); // Timer to periodically refresh the remote account state - let mut update_shared_account_state_timer = - tokio::time::interval(Duration::from_secs(5 * 60)); + let mut update_account_state_timer = tokio::time::interval(Duration::from_secs(5 * 60)); loop { tokio::select! { + // Handle incoming commands Some(command) = self.command_rx.recv() => { if let Err(err) = self.handle_command(command).await { tracing::error!("{err}"); tracing::debug!("{err:#?}"); } } - _ = update_shared_account_state_timer.tick() => { - self.queue_command(AccountCommand::UpdateSharedAccountState); + // Check the results of finished tasks + _ = command_finish_timer.tick() => { + while let Some(result) = self.command_tasks.try_join_next() { + self.handle_command_result(result).await; + } } + // Check the results of finished zk nym polling tasks _ = polling_timer.tick() => { while let Some(result) = self.polling_tasks.try_join_next() { self.handle_polling_result(result).await; } self.update_pending_zk_nym_tasks().await; } + // On a timer we want to refresh the account state + _ = update_account_state_timer.tick() => { + self.queue_command(AccountCommand::UpdateAccountState); + } _ = self.cancel_token.cancelled() => { tracing::trace!("Received cancellation signal"); break; @@ -771,81 +686,3 @@ fn create_ecash_api_client() -> VpnEcashApiClient { // TODO: remove unwrap VpnEcashApiClient::new(base_url).unwrap() } - -fn extract_status_code(err: &E) -> Option -where - E: std::error::Error + 'static, -{ - let mut source = err.source(); - while let Some(err) = source { - if let Some(status) = err - .downcast_ref::>() - .and_then(extract_status_code_inner) - { - return Some(status); - } - source = err.source(); - } - None -} - -fn extract_status_code_inner( - err: &nym_vpn_api_client::HttpClientError, -) -> Option { - match err { - HttpClientError::EndpointFailure { status, .. } => Some((*status).into()), - _ => None, - } -} - -#[derive(Debug)] -enum PollingResult { - Finished(NymVpnZkNym, TicketType, Box), - Timeout(NymVpnZkNym), - Error(PollingError), -} - -#[derive(Debug)] -struct PollingError { - id: String, - error: VpnApiClientError, -} - -// These are temporarily copy pasted here from the nym-credential-proxy. They will eventually make -// their way into the crates we use through the nym repo. - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct WalletShare { - pub node_index: u64, - pub bs58_encoded_share: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -struct TicketbookWalletSharesResponse { - epoch_id: u64, - shares: Vec, - master_verification_key: Option, - aggregated_coin_index_signatures: Option, - aggregated_expiration_date_signatures: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -struct MasterVerificationKeyResponse { - epoch_id: u64, - bs58_encoded_key: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -struct AggregatedCoinIndicesSignaturesResponse { - signatures: AggregatedCoinIndicesSignatures, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -struct AggregatedExpirationDateSignaturesResponse { - signatures: AggregatedExpirationDateSignatures, -} diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/error.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/error.rs index 483bcab523..60128695d7 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/error.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/error.rs @@ -4,7 +4,7 @@ use tokio::sync::mpsc::error::SendError; use url::Url; -use crate::AccountCommand; +use crate::commands::AccountCommand; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -68,4 +68,16 @@ pub enum Error { #[error("failed to create ecash key pair")] CreateEcashKeyPair(#[source] nym_vpn_api_client::VpnApiClientError), + + #[error("internal error: {0}")] + Internal(String), + + #[error(transparent)] + NymSdk(#[from] nym_sdk::Error), +} + +impl Error { + pub fn internal(msg: impl ToString) -> Self { + Error::Internal(msg.to_string()) + } } diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/lib.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/lib.rs index e52b178867..8c5739cd11 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/lib.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/lib.rs @@ -8,11 +8,14 @@ pub mod shared_state; +mod commands; mod controller; mod ecash_client; mod error; +mod models; mod storage; -pub use controller::{AccountCommand, AccountController}; +pub use commands::AccountCommand; +pub use controller::AccountController; pub use error::Error; pub use shared_state::{AccountStateSummary, ReadyToConnect, SharedAccountState}; diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/models.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/models.rs new file mode 100644 index 0000000000..25edda1233 --- /dev/null +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/models.rs @@ -0,0 +1,44 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_credentials::{AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures}; +use serde::{Deserialize, Serialize}; + +// These are temporarily copy pasted here from the nym-credential-proxy. They will eventually make +// their way into the crates we use through the nym repo. + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct WalletShare { + pub node_index: u64, + pub bs58_encoded_share: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct TicketbookWalletSharesResponse { + epoch_id: u64, + shares: Vec, + master_verification_key: Option, + aggregated_coin_index_signatures: Option, + aggregated_expiration_date_signatures: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct MasterVerificationKeyResponse { + epoch_id: u64, + bs58_encoded_key: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct AggregatedCoinIndicesSignaturesResponse { + signatures: AggregatedCoinIndicesSignatures, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct AggregatedExpirationDateSignaturesResponse { + signatures: AggregatedExpirationDateSignatures, +} diff --git a/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs b/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs index 2d8f074a35..e4f2eab33f 100644 --- a/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs +++ b/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs @@ -277,6 +277,7 @@ impl VpnApiClient { ecash_pubkey: String, ticketbook_type: String, ) -> Result { + tracing::info!("Requesting zk-nym for type: {}", ticketbook_type); let body = RequestZkNymRequestBody { withdrawal_request, ecash_pubkey, diff --git a/nym-vpn-core/crates/nym-vpn-lib/src/platform/account.rs b/nym-vpn-core/crates/nym-vpn-lib/src/platform/account.rs index 47ac2c1e29..e138076acd 100644 --- a/nym-vpn-core/crates/nym-vpn-lib/src/platform/account.rs +++ b/nym-vpn-core/crates/nym-vpn-lib/src/platform/account.rs @@ -164,7 +164,7 @@ pub(super) async fn store_account_mnemonic(mnemonic: &str, path: &str) -> Result details: err.to_string(), })?; - send_account_command(AccountCommand::UpdateSharedAccountState).await?; + send_account_command(AccountCommand::UpdateAccountState).await?; Ok(()) } @@ -196,7 +196,7 @@ pub(super) async fn remove_account_mnemonic(path: &str) -> Result for nym_vpn_proto::AccountError { message: err.to_string(), details: hashmap! {}, }, + AccountError::Initialization { .. } => nym_vpn_proto::AccountError { + kind: AccountErrorType::Storage as i32, + message: err.to_string(), + details: hashmap! {}, + }, } } } diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/error.rs b/nym-vpn-core/crates/nym-vpnd/src/service/error.rs index d131880438..33f4d5ec88 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/error.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/error.rs @@ -361,6 +361,11 @@ pub enum AccountError { AccountControllerError { source: nym_vpn_account_controller::Error, }, + + #[error("failed to initialize")] + Initialization { + source: Box, + }, } #[derive(Debug, thiserror::Error)] diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs b/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs index 6b682cbf84..2e66651028 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs @@ -432,7 +432,7 @@ impl NymVpnService { let account_controller = AccountController::new( Arc::clone(&storage), data_dir.clone(), - user_agent.clone(), + user_agent, shutdown_token.child_token(), ) .await @@ -809,7 +809,7 @@ where })?; self.account_command_tx - .send(AccountCommand::UpdateSharedAccountState) + .send(AccountCommand::UpdateAccountState) .map_err(|err| AccountError::SendCommand { source: Box::new(err), })?; @@ -839,7 +839,7 @@ where })?; self.account_command_tx - .send(AccountCommand::UpdateSharedAccountState) + .send(AccountCommand::UpdateAccountState) .map_err(|err| AccountError::SendCommand { source: Box::new(err), })?; @@ -857,7 +857,7 @@ where async fn handle_refresh_account_state(&self) -> Result<(), AccountError> { self.account_command_tx - .send(AccountCommand::UpdateSharedAccountState) + .send(AccountCommand::UpdateAccountState) .map_err(|err| AccountError::SendCommand { source: Box::new(err), }) @@ -909,7 +909,7 @@ where })?; self.account_command_tx - .send(AccountCommand::UpdateSharedAccountState) + .send(AccountCommand::UpdateAccountState) .map_err(|err| AccountError::SendCommand { source: Box::new(err), })?; From df39624972724a24c43e8d95e139a8370e2de02c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 31 Oct 2024 16:04:36 +0100 Subject: [PATCH 02/15] Fix keeping track of running tasks --- .../src/commands/mod.rs | 19 ++++++++++++------- .../src/controller.rs | 7 ++++--- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs index 8265aa2af5..f9e069c6bd 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs @@ -33,6 +33,8 @@ pub(crate) enum AccountCommandResult { pub(crate) struct CommandHandler { id: uuid::Uuid, + command: AccountCommand, + account: VpnApiAccount, device: Device, pending_command: PendingCommands, @@ -44,7 +46,9 @@ pub(crate) struct CommandHandler { } impl CommandHandler { + #[allow(clippy::too_many_arguments)] pub(crate) fn new( + command: AccountCommand, account: VpnApiAccount, device: Device, pending_command: PendingCommands, @@ -56,7 +60,7 @@ impl CommandHandler { let id = uuid::Uuid::new_v4(); pending_command .lock() - .map(|mut guard| guard.insert(id, AccountCommand::UpdateAccountState)) + .map(|mut guard| guard.insert(id, command.clone())) .map_err(|err| { tracing::error!( "Failed to insert command {} into pending commands: {:?}", @@ -68,6 +72,7 @@ impl CommandHandler { tracing::debug!("Created command handler with id: {}", id); CommandHandler { id, + command, account, device, pending_command, @@ -104,9 +109,9 @@ impl CommandHandler { .map(AccountCommandResult::RegisteredDevice) } - pub(crate) async fn run(self, command: AccountCommand) -> Result { - tracing::debug!("Running command {:?} with id {}", command, self.id); - match command { + pub(crate) async fn run(self) -> Result { + tracing::debug!("Running command {:?} with id {}", self.command, self.id); + match self.command { AccountCommand::UpdateAccountState => self.update_shared_account_state().await, AccountCommand::RegisterDevice => self.register_device().await, AccountCommand::RequestZkNym => { @@ -117,17 +122,17 @@ impl CommandHandler { } } .inspect(|_result| { - tracing::info!("Command {:?} with id {} completed", command, self.id); + tracing::info!("Command {:?} with id {} completed", self.command, self.id); }) .inspect_err(|err| { tracing::warn!( "Command {:?} with id {} completed with error", - command, + self.command, self.id ); tracing::debug!( "Command {:?} with id {} failed with error: {:?}", - command, + self.command, self.id, err ); diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs index f6bf6bb2f0..21c73d43a1 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs @@ -153,8 +153,9 @@ where self.command_tx.clone() } - async fn new_command_handler(&self) -> Result { + async fn new_command_handler(&self, command: AccountCommand) -> Result { Ok(CommandHandler::new( + command, self.account_storage.load_account().await?, self.account_storage.load_device_keys().await?, self.pending_commands.clone(), @@ -167,8 +168,8 @@ where async fn spawn_command_task(&mut self, command: AccountCommand) -> Result<(), Error> { tracing::debug!("Spawning command: {:?}", command); - let command_handler = self.new_command_handler().await?; - self.command_tasks.spawn(command_handler.run(command)); + let command_handler = self.new_command_handler(command).await?; + self.command_tasks.spawn(command_handler.run()); Ok(()) } From a2dc06a110970300500a5c6027676a379ef7098e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Fri, 1 Nov 2024 11:02:39 +0100 Subject: [PATCH 03/15] Remove lots of unnecessary stuff --- nym-vpn-core/Cargo.lock | 295 +++++++++++++++++- nym-vpn-core/Cargo.toml | 2 + .../nym-vpn-account-controller/Cargo.toml | 3 +- .../src/commands/zknym.rs | 75 ++++- .../src/controller.rs | 169 +--------- .../src/ecash_client.rs | 104 ------ .../nym-vpn-account-controller/src/lib.rs | 1 - .../nym-vpn-account-controller/src/models.rs | 43 +-- .../nym-vpn-account-controller/src/storage.rs | 3 + .../crates/nym-vpn-api-client/src/client.rs | 4 + .../crates/nym-vpn-api-client/src/request.rs | 1 + .../crates/nym-vpn-api-client/src/response.rs | 1 + 12 files changed, 385 insertions(+), 316 deletions(-) delete mode 100644 nym-vpn-core/crates/nym-vpn-account-controller/src/ecash_client.rs diff --git a/nym-vpn-core/Cargo.lock b/nym-vpn-core/Cargo.lock index d1fac1d12c..499f12389f 100644 --- a/nym-vpn-core/Cargo.lock +++ b/nym-vpn-core/Cargo.lock @@ -280,6 +280,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "atty" version = "0.2.14" @@ -304,7 +310,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" dependencies = [ "async-trait", - "axum-core", + "axum-core 0.3.4", "bitflags 1.3.2", "bytes", "futures-util", @@ -319,10 +325,44 @@ dependencies = [ "pin-project-lite", "rustversion", "serde", - "sync_wrapper", - "tower", + "sync_wrapper 0.1.2", + "tower 0.4.13", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower 0.5.1", "tower-layer", "tower-service", + "tracing", ] [[package]] @@ -342,6 +382,27 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.1", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backon" version = "1.2.0" @@ -1738,6 +1799,21 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.1" @@ -2018,6 +2094,25 @@ dependencies = [ "tracing", ] +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.5.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "handlebars" version = "3.5.5" @@ -2298,7 +2393,7 @@ dependencies = [ "futures-channel", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "httparse", @@ -2321,6 +2416,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2 0.4.6", "http 1.1.0", "http-body 1.0.1", "httparse", @@ -2375,6 +2471,22 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.7" @@ -2390,7 +2502,7 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", + "tower 0.4.13", "tower-service", "tracing", ] @@ -2869,6 +2981,23 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + [[package]] name = "netdev" version = "0.29.0" @@ -3432,6 +3561,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "nym-credential-proxy-requests" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=d18ddcdc114e73a6f80b1d580f7b658d0fbb2ec9#d18ddcdc114e73a6f80b1d580f7b658d0fbb2ec9" +dependencies = [ + "async-trait", + "nym-credentials", + "nym-credentials-interface", + "nym-http-api-client", + "nym-http-api-common", + "nym-serde-helpers", + "reqwest 0.12.4", + "schemars", + "serde", + "serde_json", + "time", + "uuid", + "wasmtimer", +] + [[package]] name = "nym-credential-storage" version = "0.1.0" @@ -3804,6 +3953,21 @@ dependencies = [ "wasmtimer", ] +[[package]] +name = "nym-http-api-common" +version = "0.1.0" +source = "git+https://github.com/nymtech/nym?rev=d18ddcdc114e73a6f80b1d580f7b658d0fbb2ec9#d18ddcdc114e73a6f80b1d580f7b658d0fbb2ec9" +dependencies = [ + "axum 0.7.7", + "bytes", + "colored", + "mime", + "serde", + "serde_json", + "serde_yaml", + "tracing", +] + [[package]] name = "nym-id" version = "0.1.0" @@ -4426,6 +4590,7 @@ dependencies = [ "futures", "nym-compact-ecash", "nym-config", + "nym-credential-proxy-requests", "nym-credential-storage", "nym-credentials", "nym-credentials-interface", @@ -4632,7 +4797,7 @@ dependencies = [ "time", "tokio", "tonic", - "tower", + "tower 0.4.13", "vergen", ] @@ -4771,12 +4936,50 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "openssl" +version = "0.10.68" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.82", +] + [[package]] name = "openssl-probe" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "openssl-sys" +version = "0.9.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -5474,7 +5677,7 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.30", @@ -5492,7 +5695,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", "system-configuration 0.5.1", "tokio", "tokio-rustls 0.24.1", @@ -5513,18 +5716,22 @@ checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" dependencies = [ "base64 0.22.1", "bytes", + "encoding_rs", "futures-core", "futures-util", + "h2 0.4.6", "http 1.1.0", "http-body 1.0.1", "http-body-util", "hyper 1.4.1", "hyper-rustls 0.26.0", + "hyper-tls", "hyper-util", "ipnet", "js-sys", "log", "mime", + "native-tls", "once_cell", "percent-encoding", "pin-project-lite", @@ -5534,8 +5741,10 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper", + "sync_wrapper 0.1.2", + "system-configuration 0.5.1", "tokio", + "tokio-native-tls", "tokio-rustls 0.25.0", "tower-service", "url", @@ -5838,6 +6047,7 @@ dependencies = [ "schemars_derive", "serde", "serde_json", + "uuid", ] [[package]] @@ -5996,6 +6206,16 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + [[package]] name = "serde_repr" version = "0.1.19" @@ -6028,6 +6248,19 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.5.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + [[package]] name = "serdect" version = "0.2.0" @@ -6525,6 +6758,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + [[package]] name = "synstructure" version = "0.12.6" @@ -6842,6 +7081,16 @@ dependencies = [ "syn 2.0.82", ] +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + [[package]] name = "tokio-rustls" version = "0.24.1" @@ -6969,10 +7218,10 @@ checksum = "76c4eb7a4e9ef9d4763600161f12f5070b92a578e1b634db88a6887844c91a13" dependencies = [ "async-stream", "async-trait", - "axum", + "axum 0.6.20", "base64 0.21.7", "bytes", - "h2", + "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", "hyper 0.14.30", @@ -6982,7 +7231,7 @@ dependencies = [ "prost", "tokio", "tokio-stream", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -7047,6 +7296,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-http" version = "0.5.2" @@ -7462,6 +7727,12 @@ dependencies = [ "subtle 2.6.1", ] +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + [[package]] name = "untrusted" version = "0.9.0" diff --git a/nym-vpn-core/Cargo.toml b/nym-vpn-core/Cargo.toml index d2f1dcf5c5..556f3af195 100644 --- a/nym-vpn-core/Cargo.toml +++ b/nym-vpn-core/Cargo.toml @@ -123,6 +123,7 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } tun = { version = "0.6.1", features = ["async"] } uniffi = { version = "0.27.3", features = ["cli"] } url = "2.5" +uuid = "1.11" vergen = { version = "8.3.1", default-features = false } windows-sys = "0.52" x25519-dalek = "2.0" @@ -135,6 +136,7 @@ nym-client-core = { git = "https://github.com/nymtech/nym", branch = "release/20 nym-compact-ecash = { git = "https://github.com/nymtech/nym", branch = "release/2024.13-magura" } nym-config = { git = "https://github.com/nymtech/nym", branch = "release/2024.13-magura" } nym-contracts-common = { git = "https://github.com/nymtech/nym", branch = "release/2024.13-magura" } +nym-credential-proxy-requests = { git = "https://github.com/nymtech/nym", branch = "release/2024.13-magura" } nym-credential-storage = { git = "https://github.com/nymtech/nym", branch = "release/2024.13-magura" } nym-credentials = { git = "https://github.com/nymtech/nym", branch = "release/2024.13-magura" } nym-credentials-interface = { git = "https://github.com/nymtech/nym", branch = "release/2024.13-magura" } diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/Cargo.toml b/nym-vpn-core/crates/nym-vpn-account-controller/Cargo.toml index 7963dd7f90..7c833b391c 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/Cargo.toml +++ b/nym-vpn-core/crates/nym-vpn-account-controller/Cargo.toml @@ -9,8 +9,10 @@ edition.workspace = true license.workspace = true [dependencies] +futures.workspace = true nym-compact-ecash.workspace = true nym-config.workspace = true +nym-credential-proxy-requests.workspace = true nym-credential-storage.workspace = true nym-credentials-interface.workspace = true nym-credentials.workspace = true @@ -32,4 +34,3 @@ tokio.workspace = true tracing.workspace = true url.workspace = true uuid = "1.11" -futures.workspace = true diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs index a8f8c5df89..d33a6ed630 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs @@ -3,20 +3,23 @@ use std::time::{Duration, Instant}; -use nym_compact_ecash::{Base58, WithdrawalRequest}; -use nym_credentials_interface::{RequestInfo, TicketType}; +use nym_compact_ecash::{Base58, BlindedSignature, VerificationKeyAuth, WithdrawalRequest}; +use nym_credentials::IssuedTicketBook; +use nym_credentials_interface::{PublicKeyUser, RequestInfo, TicketType}; use nym_ecash_time::EcashTime; use nym_vpn_api_client::{ response::{NymVpnZkNym, NymVpnZkNymStatus}, types::{Device, VpnApiAccount}, VpnApiClientError, }; +use time::{format_description::well_known::Rfc3339, Date, OffsetDateTime}; -use crate::error::Error; +use crate::{error::Error, models::WalletShare}; pub(crate) struct ZkNymRequestData { withdrawal_request: WithdrawalRequest, - ecash_pubkey: String, + ecash_pubkey: PublicKeyUser, + expiration_date: Date, ticketbook_type: TicketType, request_info: RequestInfo, } @@ -39,11 +42,12 @@ pub(crate) fn construct_zk_nym_request_data( ) .map_err(Error::ConstructWithdrawalRequest)?; - let ecash_pubkey = ecash_keypair.public_key().to_base58_string(); + let ecash_pubkey = ecash_keypair.public_key(); Ok(ZkNymRequestData { withdrawal_request, ecash_pubkey, + expiration_date, ticketbook_type, request_info, }) @@ -60,7 +64,8 @@ pub(crate) async fn request_zk_nym( account, device, request.withdrawal_request.to_bs58(), - request.ecash_pubkey.to_owned(), + request.ecash_pubkey.to_base58_string().to_owned(), + request.expiration_date.to_string(), request.ticketbook_type.to_string(), ) .await @@ -115,6 +120,64 @@ pub(crate) async fn poll_zk_nym( } } +pub(crate) async fn unblind_and_aggregate( + response: NymVpnZkNym, + ticketbook_type: TicketType, + request_info: RequestInfo, + account: VpnApiAccount, + vk_auth: VerificationKeyAuth, +) -> Result { + let ecash_keypair = account + .create_ecash_keypair() + .map_err(Error::CreateEcashKeyPair)?; + + let mut partial_wallets = Vec::new(); + for blinded_share in response.blinded_shares { + // TODO: remove unwrap + let blinded_share: WalletShare = serde_json::from_str(&blinded_share).unwrap(); + + // TODO: remove unwrap + let blinded_sig = + BlindedSignature::try_from_bs58(&blinded_share.bs58_encoded_share).unwrap(); + + match nym_compact_ecash::issue_verify( + &vk_auth, + ecash_keypair.secret_key(), + &blinded_sig, + &request_info, + blinded_share.node_index, + ) { + Ok(partial_wallet) => partial_wallets.push(partial_wallet), + Err(err) => { + tracing::error!("Failed to issue verify: {:#?}", err); + return Err(Error::ImportZkNym(err)); + } + } + } + + // TODO: remove unwrap + let aggregated_wallets = nym_compact_ecash::aggregate_wallets( + &vk_auth, + ecash_keypair.secret_key(), + &partial_wallets, + &request_info, + ) + .unwrap(); + + // TODO: remove unwrap + let expiration_date = OffsetDateTime::parse(&response.valid_until_utc, &Rfc3339).unwrap(); + + let ticketbook = IssuedTicketBook::new( + aggregated_wallets.into_wallet_signatures(), + response.epoch.unwrap(), + ecash_keypair.into(), + ticketbook_type, + expiration_date.ecash_date(), + ); + + Ok(ticketbook) +} + #[derive(Debug)] pub(crate) enum PollingResult { Finished(NymVpnZkNym, TicketType, Box), diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs index 21c73d43a1..1216a89b53 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs @@ -4,11 +4,8 @@ use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Duration}; use futures::StreamExt; -use nym_compact_ecash::{Base58 as _, BlindedSignature, VerificationKeyAuth}; use nym_config::defaults::NymNetworkDetails; -use nym_credentials::IssuedTicketBook; use nym_credentials_interface::{RequestInfo, TicketType}; -use nym_ecash_time::EcashTime; use nym_http_api_client::UserAgent; use nym_vpn_api_client::{ response::{ @@ -17,7 +14,6 @@ use nym_vpn_api_client::{ types::{Device, VpnApiAccount}, }; use nym_vpn_store::VpnStorage; -use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use tokio::{ sync::mpsc::{UnboundedReceiver, UnboundedSender}, task::{JoinError, JoinSet}, @@ -27,15 +23,14 @@ use url::Url; use crate::{ commands::{ + self, zknym::{ construct_zk_nym_request_data, poll_zk_nym, request_zk_nym, PollingResult, ZkNymRequestData, }, AccountCommand, AccountCommandResult, CommandHandler, }, - ecash_client::VpnEcashApiClient, error::Error, - models::WalletShare, shared_state::{MnemonicState, ReadyToRegisterDevice, SharedAccountState}, storage::{AccountStorage, VpnCredentialStorage}, }; @@ -63,10 +58,6 @@ where // The API client used to interact with the nym-vpn-api vpn_api_client: nym_vpn_api_client::VpnApiClient, - // The API client used to interact with cash endpoints - // NOTE: this is a temporary solution until the data is available on the vpn-api - vpn_ecash_api_client: VpnEcashApiClient, - // The current state of the account account_state: SharedAccountState, @@ -78,9 +69,6 @@ where // changed. last_devices: DevicesResponse, - // Keep track of the current ecash epoch - current_epoch: Option, - // Tasks used to poll the status of zk-nyms polling_tasks: JoinSet, @@ -131,11 +119,9 @@ where account_storage, credential_storage, vpn_api_client: create_api_client(user_agent), - vpn_ecash_api_client: create_ecash_api_client(), account_state: SharedAccountState::new(), last_account_summary: Arc::new(tokio::sync::Mutex::new(None)), last_devices: Arc::new(tokio::sync::Mutex::new(None)), - current_epoch: None, polling_tasks: JoinSet::new(), command_rx, command_tx, @@ -203,60 +189,24 @@ where request_info: RequestInfo, ) -> Result<(), Error> { let account = self.account_storage.load_account().await?; - let ecash_keypair = account - .create_ecash_keypair() - .map_err(Error::CreateEcashKeyPair)?; - // TODO: use explicit epoch id, that we include together with the request_info - let current_epoch = self.current_epoch.ok_or(Error::NoEpoch)?; - // TODO: remove unwrap - let vk_auth = self.get_current_verification_key().await?.unwrap(); - - let mut partial_wallets = Vec::new(); - for blinded_share in response.blinded_shares { - // TODO: remove unwrap - let blinded_share: WalletShare = serde_json::from_str(&blinded_share).unwrap(); - - // TODO: remove unwrap - let blinded_sig = - BlindedSignature::try_from_bs58(&blinded_share.bs58_encoded_share).unwrap(); - - match nym_compact_ecash::issue_verify( - &vk_auth, - ecash_keypair.secret_key(), - &blinded_sig, - &request_info, - blinded_share.node_index, - ) { - Ok(partial_wallet) => partial_wallets.push(partial_wallet), - Err(err) => { - tracing::error!("Failed to issue verify: {:#?}", err); - return Err(Error::ImportZkNym(err)); - } - } - } + let master_verification_key = self + .credential_storage + .get_master_verification_key(response.epoch.unwrap()) + .await? + .unwrap(); - // TODO: remove unwrap - let aggregated_wallets = nym_compact_ecash::aggregate_wallets( - &vk_auth, - ecash_keypair.secret_key(), - &partial_wallets, - &request_info, + let issued_ticketbook = commands::zknym::unblind_and_aggregate( + response, + ticketbook_type, + request_info, + account, + master_verification_key, ) + .await .unwrap(); - // TODO: remove unwrap - let expiration_date = OffsetDateTime::parse(&response.valid_until_utc, &Rfc3339).unwrap(); - - let ticketbook = IssuedTicketBook::new( - aggregated_wallets.into_wallet_signatures(), - current_epoch, - ecash_keypair.into(), - ticketbook_type, - expiration_date.ecash_date(), - ); - self.credential_storage - .insert_issued_ticketbook(&ticketbook) + .insert_issued_ticketbook(&issued_ticketbook) .await?; Ok(()) @@ -284,6 +234,9 @@ where .map(|(ticket_type, _)| ticket_type) .collect::>(); + // For testing: uncomment to only request zk-nyms for a specific ticket type + //let ticket_types_needed_to_request = vec![TicketType::V1MixnetEntry]; + // Request zk-nyms for each ticket type that we need let responses = futures::stream::iter(ticket_types_needed_to_request) .filter_map(|ticket_type| { @@ -316,57 +269,6 @@ where Ok(()) } - #[allow(unused)] - async fn update_ecash_epoch(&mut self) -> Result<(), Error> { - self.current_epoch = self - .vpn_ecash_api_client - .get_aggregated_coin_indices_signatures() - .await - .ok() - .map(|response| response.epoch_id); - Ok(()) - } - - async fn update_verification_key(&mut self) -> Result<(), Error> { - tracing::debug!("Updating verification key"); - let verification_key = self - .vpn_ecash_api_client - .get_master_verification_key() - .await?; - self.credential_storage - .insert_master_verification_key(&verification_key) - .await - } - - async fn get_current_verification_key(&self) -> Result, Error> { - let current_epoch = self.current_epoch.ok_or(Error::NoEpoch)?; - self.credential_storage - .get_master_verification_key(current_epoch) - .await - } - - async fn update_coin_indices_signatures(&mut self) -> Result<(), Error> { - tracing::debug!("Updating coin indices signatures"); - let coin_indices_signatures = self - .vpn_ecash_api_client - .get_aggregated_coin_indices_signatures() - .await?; - self.credential_storage - .insert_coin_index_signatures(&coin_indices_signatures) - .await - } - - async fn update_expiration_date_signatures(&mut self) -> Result<(), Error> { - tracing::debug!("Updating expiration date signatures"); - let expiration_date_signatures = self - .vpn_ecash_api_client - .get_aggregated_expiration_data_signatures() - .await?; - self.credential_storage - .insert_expiration_date_signatures(&expiration_date_signatures) - .await - } - async fn update_mnemonic_state(&self) -> Option { match self.account_storage.load_account().await { Ok(account) => { @@ -586,25 +488,6 @@ where pub async fn run(mut self) { self.print_info().await; - self.update_verification_key() - .await - .inspect_err(|err| { - tracing::debug!("Failed to update master verification key: {:?}", err) - }) - .ok(); - self.update_coin_indices_signatures() - .await - .inspect_err(|err| { - tracing::debug!("Failed to update coin indices signatures: {:?}", err) - }) - .ok(); - self.update_expiration_date_signatures() - .await - .inspect_err(|err| { - tracing::debug!("Failed to update expiration date signatures: {:?}", err) - }) - .ok(); - // Timer to check if any command tasks have finished let mut command_finish_timer = tokio::time::interval(Duration::from_millis(500)); @@ -656,17 +539,6 @@ where } } -fn get_api_url() -> Result { - // TODO: remove unwrap - NymNetworkDetails::new_from_env() - .endpoints - .first() - .unwrap() - .api_url() - .ok_or(Error::MissingApiUrl) - .inspect(|url| tracing::info!("Using nym-api url: {}", url)) -} - fn get_nym_vpn_api_url() -> Result { NymNetworkDetails::new_from_env() .nym_vpn_api_url() @@ -680,10 +552,3 @@ fn create_api_client(user_agent: UserAgent) -> nym_vpn_api_client::VpnApiClient // TODO: remove unwrap nym_vpn_api_client::VpnApiClient::new(nym_vpn_api_url, user_agent).unwrap() } - -fn create_ecash_api_client() -> VpnEcashApiClient { - // TODO: remove unwrap - let base_url = get_api_url().unwrap(); - // TODO: remove unwrap - VpnEcashApiClient::new(base_url).unwrap() -} diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/ecash_client.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/ecash_client.rs deleted file mode 100644 index 884ba059b4..0000000000 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/ecash_client.rs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2024 Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use nym_credentials::{ - AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, -}; -use nym_http_api_client::{Client, HttpClientError, NO_PARAMS}; -use nym_validator_client::ecash::models::{ - AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, - MasterVerificationKeyResponse, -}; -use url::Url; - -use crate::error::Error; - -pub(crate) type VpnEcashApiClientError = HttpClientError; - -pub(crate) struct VpnEcashApiClient { - inner: Client, -} - -impl VpnEcashApiClient { - pub(crate) fn new(base_url: Url) -> Result { - Ok(Self { - inner: Client::builder(base_url)? - .with_user_agent(format!("ecash-client/{}", env!("CARGO_PKG_VERSION"))) - .build()?, - }) - } - - async fn _get_master_verification_key( - &self, - ) -> Result { - self.inner - .get_json(&["/v1", "/ecash", "/master-verification-key"], NO_PARAMS) - .await - } - - pub(crate) async fn get_master_verification_key(&self) -> Result { - let master_verification_key = self - ._get_master_verification_key() - .await - .map_err(Error::from)? - .key; - - // Temporary workaround - let current_epoch = self - ._get_aggregated_coin_indices_signatures() - .await? - .epoch_id; - - Ok(EpochVerificationKey { - epoch_id: current_epoch, - key: master_verification_key, - }) - } - - pub(crate) async fn _get_aggregated_coin_indices_signatures( - &self, - ) -> Result { - self.inner - .get_json( - &["/v1", "/ecash", "/aggregated-coin-indices-signatures"], - NO_PARAMS, - ) - .await - } - - pub(crate) async fn get_aggregated_coin_indices_signatures( - &self, - ) -> Result { - self._get_aggregated_coin_indices_signatures() - .await - .map(|response| AggregatedCoinIndicesSignatures { - epoch_id: response.epoch_id, - signatures: response.signatures, - }) - .map_err(Error::from) - } - - pub(crate) async fn _get_aggregated_expiration_data_signatures( - &self, - ) -> Result { - self.inner - .get_json( - &["/v1", "/ecash", "/aggregated-expiration-date-signatures"], - NO_PARAMS, - ) - .await - } - - pub(crate) async fn get_aggregated_expiration_data_signatures( - &self, - ) -> Result { - self._get_aggregated_expiration_data_signatures() - .await - .map(|response| AggregatedExpirationDateSignatures { - epoch_id: response.epoch_id, - expiration_date: response.expiration_date, - signatures: response.signatures, - }) - .map_err(Error::from) - } -} diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/lib.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/lib.rs index 8c5739cd11..891cca11e9 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/lib.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/lib.rs @@ -10,7 +10,6 @@ pub mod shared_state; mod commands; mod controller; -mod ecash_client; mod error; mod models; mod storage; diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/models.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/models.rs index 25edda1233..7a104c3e05 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/models.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/models.rs @@ -1,44 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_credentials::{AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures}; -use serde::{Deserialize, Serialize}; +// Re-import models from nym-credential-proxy-requests. It's not yet clear if we should possibly +// copy-paste here, or rely on it directly. -// These are temporarily copy pasted here from the nym-credential-proxy. They will eventually make -// their way into the crates we use through the nym repo. - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct WalletShare { - pub node_index: u64, - pub bs58_encoded_share: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct TicketbookWalletSharesResponse { - epoch_id: u64, - shares: Vec, - master_verification_key: Option, - aggregated_coin_index_signatures: Option, - aggregated_expiration_date_signatures: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct MasterVerificationKeyResponse { - epoch_id: u64, - bs58_encoded_key: String, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct AggregatedCoinIndicesSignaturesResponse { - signatures: AggregatedCoinIndicesSignatures, -} - -#[derive(Serialize, Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct AggregatedExpirationDateSignaturesResponse { - signatures: AggregatedExpirationDateSignatures, -} +pub use nym_credential_proxy_requests::api::v1::ticketbook::models::WalletShare; diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs index 5db11367d6..b522bc456e 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs @@ -124,6 +124,7 @@ impl VpnCredentialStorage { .map_err(Error::from) } + #[allow(unused)] pub(crate) async fn insert_master_verification_key( &self, key: &EpochVerificationKey, @@ -144,6 +145,7 @@ impl VpnCredentialStorage { .map_err(Error::from) } + #[allow(unused)] pub(crate) async fn insert_coin_index_signatures( &self, signatures: &AggregatedCoinIndicesSignatures, @@ -154,6 +156,7 @@ impl VpnCredentialStorage { .map_err(Error::from) } + #[allow(unused)] pub(crate) async fn insert_expiration_date_signatures( &self, signatures: &AggregatedExpirationDateSignatures, diff --git a/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs b/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs index e4f2eab33f..0e4f949f3c 100644 --- a/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs +++ b/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs @@ -275,14 +275,18 @@ impl VpnApiClient { device: &Device, withdrawal_request: String, ecash_pubkey: String, + expiration_date: String, ticketbook_type: String, ) -> Result { tracing::info!("Requesting zk-nym for type: {}", ticketbook_type); let body = RequestZkNymRequestBody { withdrawal_request, ecash_pubkey, + expiration_date, ticketbook_type, }; + tracing::info!("Request body: {:#?}", body); + self.post_authorized( &[ routes::PUBLIC, diff --git a/nym-vpn-core/crates/nym-vpn-api-client/src/request.rs b/nym-vpn-core/crates/nym-vpn-api-client/src/request.rs index cedabda8b2..564809bf25 100644 --- a/nym-vpn-core/crates/nym-vpn-api-client/src/request.rs +++ b/nym-vpn-core/crates/nym-vpn-api-client/src/request.rs @@ -22,6 +22,7 @@ pub struct RegisterDeviceRequestBody { pub struct RequestZkNymRequestBody { pub withdrawal_request: String, pub ecash_pubkey: String, + pub expiration_date: String, pub ticketbook_type: String, } diff --git a/nym-vpn-core/crates/nym-vpn-api-client/src/response.rs b/nym-vpn-core/crates/nym-vpn-api-client/src/response.rs index 7bb56eadd5..5240ed03b9 100644 --- a/nym-vpn-core/crates/nym-vpn-api-client/src/response.rs +++ b/nym-vpn-core/crates/nym-vpn-api-client/src/response.rs @@ -139,6 +139,7 @@ pub struct NymVpnZkNym { pub issued_bandwidth_in_gb: f64, pub blinded_shares: Vec, pub status: NymVpnZkNymStatus, + pub epoch: Option, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] From ac4a19ad32b4512a145fdd2e2bdfdd6dd4599575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Fri, 1 Nov 2024 23:57:41 +0100 Subject: [PATCH 04/15] wip: importing --- nym-vpn-core/Cargo.lock | 1 + .../src/commands/zknym.rs | 22 +++--- .../src/controller.rs | 71 ++++++++++++++++--- .../nym-vpn-account-controller/src/error.rs | 3 + .../crates/nym-vpn-api-client/Cargo.toml | 1 + .../crates/nym-vpn-api-client/src/client.rs | 71 ++++++++++++++++--- .../crates/nym-vpn-api-client/src/response.rs | 40 +++++++++-- .../crates/nym-vpn-api-client/src/routes.rs | 1 + 8 files changed, 178 insertions(+), 32 deletions(-) diff --git a/nym-vpn-core/Cargo.lock b/nym-vpn-core/Cargo.lock index 499f12389f..6f404cac7e 100644 --- a/nym-vpn-core/Cargo.lock +++ b/nym-vpn-core/Cargo.lock @@ -4627,6 +4627,7 @@ dependencies = [ "nym-compact-ecash", "nym-config", "nym-contracts-common", + "nym-credential-proxy-requests", "nym-credentials-interface", "nym-crypto", "nym-ecash-time", diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs index d33a6ed630..6e2182dcd1 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs @@ -8,7 +8,7 @@ use nym_credentials::IssuedTicketBook; use nym_credentials_interface::{PublicKeyUser, RequestInfo, TicketType}; use nym_ecash_time::EcashTime; use nym_vpn_api_client::{ - response::{NymVpnZkNym, NymVpnZkNymStatus}, + response::{NymVpnZkNym, NymVpnZkNym2, NymVpnZkNymStatus}, types::{Device, VpnApiAccount}, VpnApiClientError, }; @@ -81,6 +81,7 @@ pub(crate) async fn poll_zk_nym( api_client: nym_vpn_api_client::VpnApiClient, ) -> PollingResult { tracing::info!("Starting zk-nym polling task for {}", response.id); + tracing::info!("which had response : {:#?}", response); let start_time = Instant::now(); loop { tokio::time::sleep(std::time::Duration::from_secs(5)).await; @@ -90,7 +91,7 @@ pub(crate) async fn poll_zk_nym( .get_zk_nym_by_id(&account, &device, &response.id) .await { - Ok(poll_response) if response.status != NymVpnZkNymStatus::Pending => { + Ok(poll_response) if poll_response.status != NymVpnZkNymStatus::Pending => { tracing::info!("zk-nym polling finished: {:#?}", poll_response); return PollingResult::Finished( poll_response, @@ -121,7 +122,7 @@ pub(crate) async fn poll_zk_nym( } pub(crate) async fn unblind_and_aggregate( - response: NymVpnZkNym, + response: NymVpnZkNym2, ticketbook_type: TicketType, request_info: RequestInfo, account: VpnApiAccount, @@ -132,20 +133,21 @@ pub(crate) async fn unblind_and_aggregate( .map_err(Error::CreateEcashKeyPair)?; let mut partial_wallets = Vec::new(); - for blinded_share in response.blinded_shares { + let blinded_shares = response.blinded_shares.unwrap(); + for share in blinded_shares.shares { // TODO: remove unwrap - let blinded_share: WalletShare = serde_json::from_str(&blinded_share).unwrap(); + // let blinded_share: WalletShare = serde_json::from_str(&share).unwrap(); // TODO: remove unwrap let blinded_sig = - BlindedSignature::try_from_bs58(&blinded_share.bs58_encoded_share).unwrap(); + BlindedSignature::try_from_bs58(&share.bs58_encoded_share).unwrap(); match nym_compact_ecash::issue_verify( &vk_auth, ecash_keypair.secret_key(), &blinded_sig, &request_info, - blinded_share.node_index, + share.node_index, ) { Ok(partial_wallet) => partial_wallets.push(partial_wallet), Err(err) => { @@ -169,7 +171,7 @@ pub(crate) async fn unblind_and_aggregate( let ticketbook = IssuedTicketBook::new( aggregated_wallets.into_wallet_signatures(), - response.epoch.unwrap(), + blinded_shares.epoch_id, ecash_keypair.into(), ticketbook_type, expiration_date.ecash_date(), @@ -180,8 +182,8 @@ pub(crate) async fn unblind_and_aggregate( #[derive(Debug)] pub(crate) enum PollingResult { - Finished(NymVpnZkNym, TicketType, Box), - Timeout(NymVpnZkNym), + Finished(NymVpnZkNym2, TicketType, Box), + Timeout(NymVpnZkNym2), Error(PollingError), } diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs index 1216a89b53..383c8dfb94 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs @@ -4,12 +4,15 @@ use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Duration}; use futures::StreamExt; +use nym_compact_ecash::Base58; use nym_config::defaults::NymNetworkDetails; -use nym_credentials_interface::{RequestInfo, TicketType}; +use nym_credentials::EpochVerificationKey; +use nym_credentials_interface::{RequestInfo, TicketType, VerificationKeyAuth}; use nym_http_api_client::UserAgent; use nym_vpn_api_client::{ response::{ - NymVpnAccountSummaryResponse, NymVpnDevicesResponse, NymVpnZkNym, NymVpnZkNymStatus, + NymVpnAccountSummaryResponse, NymVpnDevicesResponse, NymVpnZkNym, NymVpnZkNym2, + NymVpnZkNymStatus, }, types::{Device, VpnApiAccount}, }; @@ -23,7 +26,6 @@ use url::Url; use crate::{ commands::{ - self, zknym::{ construct_zk_nym_request_data, poll_zk_nym, request_zk_nym, PollingResult, ZkNymRequestData, @@ -184,18 +186,44 @@ where async fn import_zk_nym( &mut self, - response: NymVpnZkNym, + response: NymVpnZkNym2, ticketbook_type: TicketType, request_info: RequestInfo, ) -> Result<(), Error> { + tracing::info!("Importing zk-nym: {:#?}", response); + let account = self.account_storage.load_account().await?; - let master_verification_key = self + + let Some(ref blinded_shares) = response.blinded_shares else { + return Err(Error::MissingBlindedShares); + }; + + let master_verification_key = match self .credential_storage - .get_master_verification_key(response.epoch.unwrap()) + .get_master_verification_key(blinded_shares.epoch_id) .await? - .unwrap(); + { + Some(master_verification_key) => master_verification_key.clone(), + None => { + let vk_bs58 = blinded_shares + .master_verification_key + .as_ref() + .unwrap() + .bs58_encoded_key + .clone(); + let vk = VerificationKeyAuth::try_from_bs58(&vk_bs58).unwrap(); + let epoch_verification_key = EpochVerificationKey { + epoch_id: blinded_shares.epoch_id, + key: vk.clone(), + }; + self.credential_storage + .insert_master_verification_key(&epoch_verification_key) + .await?; + vk + } + }; - let issued_ticketbook = commands::zknym::unblind_and_aggregate( + let issued_ticketbook = crate::commands::zknym::unblind_and_aggregate( response, ticketbook_type, request_info, @@ -235,7 +263,7 @@ where .collect::>(); // For testing: uncomment to only request zk-nyms for a specific ticket type - //let ticket_types_needed_to_request = vec![TicketType::V1MixnetEntry]; + let ticket_types_needed_to_request = vec![TicketType::V1MixnetEntry]; // Request zk-nyms for each ticket type that we need let responses = futures::stream::iter(ticket_types_needed_to_request) @@ -345,6 +373,26 @@ where Ok(()) } + async fn handle_get_zk_nyms_available_for_download(&self) -> Result<(), Error> { + tracing::info!("Getting zk-nyms available for download from API"); + + let account = self.account_storage.load_account().await?; + let device = self.account_storage.load_device_keys().await?; + + let reported_device_zk_nyms = self + .vpn_api_client + .get_zk_nyms_available_for_download(&account, &device) + .await + .map_err(Error::GetZkNyms)?; + + tracing::info!("The device as the following zk-nyms available to download:"); + // TODO: pagination + for zk_nym in &reported_device_zk_nyms.items { + tracing::info!("{:?}", zk_nym); + } + Ok(()) + } + // Once we finish polling the result of the zk-nym request, we now import the zk-nym into the // local credential store async fn handle_polling_result(&mut self, result: Result) { @@ -409,7 +457,10 @@ where AccountCommand::UpdateAccountState => self.handle_update_account_state().await, AccountCommand::RegisterDevice => self.handle_register_device().await, AccountCommand::RequestZkNym => self.handle_request_zk_nym().await, - AccountCommand::GetDeviceZkNym => self.handle_get_device_zk_nym().await, + // AccountCommand::GetDeviceZkNym => self.handle_get_device_zk_nym().await, + AccountCommand::GetDeviceZkNym => { + self.handle_get_zk_nyms_available_for_download().await + } } } diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/error.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/error.rs index 60128695d7..3b371af175 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/error.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/error.rs @@ -74,6 +74,9 @@ pub enum Error { #[error(transparent)] NymSdk(#[from] nym_sdk::Error), + + #[error("succesfull zknym response is missing blinded shares")] + MissingBlindedShares, } impl Error { diff --git a/nym-vpn-core/crates/nym-vpn-api-client/Cargo.toml b/nym-vpn-core/crates/nym-vpn-api-client/Cargo.toml index e76190b2f9..1e2d4ee153 100644 --- a/nym-vpn-core/crates/nym-vpn-api-client/Cargo.toml +++ b/nym-vpn-core/crates/nym-vpn-api-client/Cargo.toml @@ -18,6 +18,7 @@ itertools.workspace = true nym-compact-ecash.workspace = true nym-config.workspace = true nym-contracts-common.workspace = true +nym-credential-proxy-requests.workspace = true nym-credentials-interface.workspace = true nym-crypto = { workspace = true, features = ["asymmetric"] } nym-ecash-time.workspace = true diff --git a/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs b/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs index 0e4f949f3c..bd24feb78e 100644 --- a/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs +++ b/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs @@ -15,9 +15,7 @@ use crate::{ RegisterDeviceRequestBody, RequestZkNymRequestBody, }, response::{ - NymDirectoryGatewayCountriesResponse, NymDirectoryGatewaysResponse, NymVpnAccountResponse, - NymVpnAccountSummaryResponse, NymVpnDevice, NymVpnDevicesResponse, NymVpnSubscription, - NymVpnSubscriptionResponse, NymVpnSubscriptionsResponse, NymVpnZkNym, NymVpnZkNymResponse, + NymDirectoryGatewayCountriesResponse, NymDirectoryGatewaysResponse, NymVpnAccountResponse, NymVpnAccountSummaryResponse, NymVpnDevice, NymVpnDevicesResponse, NymVpnSubscription, NymVpnSubscriptionResponse, NymVpnSubscriptionsResponse, NymVpnZkNym, NymVpnZkNym2, NymVpnZkNymResponse }, routes, types::{Device, GatewayMinPerformance, GatewayType, VpnApiAccount}, @@ -78,6 +76,37 @@ impl VpnApiClient { nym_http_api_client::parse_response(response, false).await } + async fn get_authorized_debug( + &self, + path: PathSegments<'_>, + account: &VpnApiAccount, + device: Option<&Device>, + ) -> std::result::Result> + where + T: DeserializeOwned, + E: fmt::Display + DeserializeOwned, + { + let request = self + .inner + .create_get_request(path, NO_PARAMS) + .bearer_auth(account.jwt().to_string()); + + let request = match device { + Some(device) => request.header( + DEVICE_AUTHORIZATION_HEADER, + format!("Bearer {}", device.jwt()), + ), + None => request, + }; + + let response = request.send().await?; + let r = response.text().await; + tracing::info!("Response: {:#?}", r); + todo!(); + + nym_http_api_client::parse_response(response, false).await + } + async fn get_json_with_retry( &self, path: PathSegments<'_>, @@ -125,6 +154,9 @@ impl VpnApiClient { }; let response = request.send().await?; + // let r = response.text().await; + // tracing::info!("Response: {:#?}", r); + // todo!(); nym_http_api_client::parse_response(response, false).await } @@ -305,11 +337,11 @@ impl VpnApiClient { .map_err(VpnApiClientError::FailedToRequestZkNym) } - pub async fn get_active_zk_nym( + pub async fn get_zk_nyms_available_for_download( &self, account: &VpnApiAccount, device: &Device, - ) -> Result { + ) -> Result { self.get_authorized( &[ routes::PUBLIC, @@ -319,21 +351,44 @@ impl VpnApiClient { routes::DEVICE, &device.identity_key().to_string(), routes::ZKNYM, - routes::ACTIVE, + routes::AVAILABLE, ], account, Some(device), ) .await - .map_err(VpnApiClientError::FailedToGetActiveZkNym) + .map_err(VpnApiClientError::FailedToGetDeviceZkNyms) } + //pub async fn get_active_zk_nym( + // &self, + // account: &VpnApiAccount, + // device: &Device, + //) -> Result { + // self.get_authorized( + // &[ + // routes::PUBLIC, + // routes::V1, + // routes::ACCOUNT, + // &account.id(), + // routes::DEVICE, + // &device.identity_key().to_string(), + // routes::ZKNYM, + // routes::ACTIVE, + // ], + // account, + // Some(device), + // ) + // .await + // .map_err(VpnApiClientError::FailedToGetActiveZkNym) + //} + pub async fn get_zk_nym_by_id( &self, account: &VpnApiAccount, device: &Device, id: &str, - ) -> Result { + ) -> Result { self.get_authorized( &[ routes::PUBLIC, diff --git a/nym-vpn-core/crates/nym-vpn-api-client/src/response.rs b/nym-vpn-core/crates/nym-vpn-api-client/src/response.rs index 5240ed03b9..3aea945705 100644 --- a/nym-vpn-core/crates/nym-vpn-api-client/src/response.rs +++ b/nym-vpn-core/crates/nym-vpn-api-client/src/response.rs @@ -5,6 +5,7 @@ use std::fmt; use itertools::Itertools; use nym_contracts_common::Percent; +use nym_credential_proxy_requests::api::v1::ticketbook::models::TicketbookWalletSharesResponse; use serde::{Deserialize, Serialize}; const MAX_PROBE_RESULT_AGE_MINUTES: i64 = 60; @@ -129,19 +130,50 @@ pub enum NymVpnRefundUserReason { Other, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize)] pub struct NymVpnZkNym { pub created_on_utc: String, pub last_updated_utc: String, pub id: String, + pub ticketbook_type: String, pub valid_until_utc: String, pub valid_from_utc: String, pub issued_bandwidth_in_gb: f64, - pub blinded_shares: Vec, + pub blinded_shares: Option>>, pub status: NymVpnZkNymStatus, - pub epoch: Option, } +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct NymVpnZkNym2 { + pub created_on_utc: String, + pub last_updated_utc: String, + pub id: String, + pub ticketbook_type: String, + pub valid_until_utc: String, + pub valid_from_utc: String, + pub issued_bandwidth_in_gb: f64, + pub blinded_shares: Option, + pub status: NymVpnZkNymStatus, +} + +//"{ +// \"totalItems\":1, +// \"page\":1, +// \"pageSize\":100, +// \"items\":[ +// { +// \"id\":\"k09ww30bcaqv2j5\", +// \"status\":\"active\", +// \"last_updated_utc\":\"2024-11-01 21:27:29.205Z\", +// \"created_on_utc\":\"2024-11-01 21:27:22.842Z\", +// \"valid_until_utc\":\"2024-12-01 21:26:07.909Z\", +// \"valid_from_utc\":\"2024-11-01 21:27:22.503Z\", +// \"issued_bandwidth_in_gb\":25 +// } +// ] +//}", + + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum NymVpnZkNymStatus { @@ -152,7 +184,7 @@ pub enum NymVpnZkNymStatus { Error, } -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NymVpnZkNymResponse { pub total_items: u64, diff --git a/nym-vpn-core/crates/nym-vpn-api-client/src/routes.rs b/nym-vpn-core/crates/nym-vpn-api-client/src/routes.rs index eefda53607..c6418ad6de 100644 --- a/nym-vpn-core/crates/nym-vpn-api-client/src/routes.rs +++ b/nym-vpn-core/crates/nym-vpn-api-client/src/routes.rs @@ -8,6 +8,7 @@ pub(crate) const SUMMARY: &str = "summary"; pub(crate) const DEVICE: &str = "device"; pub(crate) const ACTIVE: &str = "active"; pub(crate) const ZKNYM: &str = "zknym"; +pub(crate) const AVAILABLE: &str = "available"; pub(crate) const FREEPASS: &str = "freepass"; pub(crate) const SUBSCRIPTION: &str = "subscription"; pub(crate) const DIRECTORY: &str = "directory"; From 5871c85778b1d20235300acca75efe30462cce7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Mon, 4 Nov 2024 12:29:49 +0100 Subject: [PATCH 05/15] Remove unused error case --- nym-vpn-core/Cargo.lock | 32 +++++++++++++++++-- .../src/command_interface/protobuf/account.rs | 5 --- .../crates/nym-vpnd/src/service/error.rs | 5 --- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/nym-vpn-core/Cargo.lock b/nym-vpn-core/Cargo.lock index 6f404cac7e..4e6d17a0b5 100644 --- a/nym-vpn-core/Cargo.lock +++ b/nym-vpn-core/Cargo.lock @@ -365,6 +365,17 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-client-ip" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eefda7e2b27e1bda4d6fa8a06b50803b8793769045918bc37ad062d48a6efac" +dependencies = [ + "axum 0.7.7", + "forwarded-header-value", + "serde", +] + [[package]] name = "axum-core" version = "0.3.4" @@ -1829,6 +1840,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" +[[package]] +name = "forwarded-header-value" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" +dependencies = [ + "nonempty", + "thiserror", +] + [[package]] name = "fs-err" version = "2.11.0" @@ -3159,6 +3180,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonempty" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + [[package]] name = "ntapi" version = "0.4.1" @@ -3564,7 +3591,7 @@ dependencies = [ [[package]] name = "nym-credential-proxy-requests" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=d18ddcdc114e73a6f80b1d580f7b658d0fbb2ec9#d18ddcdc114e73a6f80b1d580f7b658d0fbb2ec9" +source = "git+https://github.com/nymtech/nym?branch=release%2F2024.13-magura#5cefa7fdd45dd257d11e9ef0c2c5c021f177ee89" dependencies = [ "async-trait", "nym-credentials", @@ -3956,9 +3983,10 @@ dependencies = [ [[package]] name = "nym-http-api-common" version = "0.1.0" -source = "git+https://github.com/nymtech/nym?rev=d18ddcdc114e73a6f80b1d580f7b658d0fbb2ec9#d18ddcdc114e73a6f80b1d580f7b658d0fbb2ec9" +source = "git+https://github.com/nymtech/nym?branch=release%2F2024.13-magura#5cefa7fdd45dd257d11e9ef0c2c5c021f177ee89" dependencies = [ "axum 0.7.7", + "axum-client-ip", "bytes", "colored", "mime", diff --git a/nym-vpn-core/crates/nym-vpnd/src/command_interface/protobuf/account.rs b/nym-vpn-core/crates/nym-vpnd/src/command_interface/protobuf/account.rs index 6bed48e162..5225258255 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/command_interface/protobuf/account.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/command_interface/protobuf/account.rs @@ -172,11 +172,6 @@ impl From for nym_vpn_proto::AccountError { message: err.to_string(), details: hashmap! {}, }, - AccountError::Initialization { .. } => nym_vpn_proto::AccountError { - kind: AccountErrorType::Storage as i32, - message: err.to_string(), - details: hashmap! {}, - }, } } } diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/error.rs b/nym-vpn-core/crates/nym-vpnd/src/service/error.rs index 33f4d5ec88..d131880438 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/error.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/error.rs @@ -361,11 +361,6 @@ pub enum AccountError { AccountControllerError { source: nym_vpn_account_controller::Error, }, - - #[error("failed to initialize")] - Initialization { - source: Box, - }, } #[derive(Debug, thiserror::Error)] From 5afc8eb3762c89f8e84279eaaa3915aafe48c550 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Mon, 4 Nov 2024 15:03:00 +0100 Subject: [PATCH 06/15] wip: hack in a the delete request --- .../crates/nym-vpn-api-client/src/client.rs | 71 ++++++++++++++++++- 1 file changed, 70 insertions(+), 1 deletion(-) diff --git a/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs b/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs index bd24feb78e..8a119fdfd3 100644 --- a/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs +++ b/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs @@ -15,7 +15,10 @@ use crate::{ RegisterDeviceRequestBody, RequestZkNymRequestBody, }, response::{ - NymDirectoryGatewayCountriesResponse, NymDirectoryGatewaysResponse, NymVpnAccountResponse, NymVpnAccountSummaryResponse, NymVpnDevice, NymVpnDevicesResponse, NymVpnSubscription, NymVpnSubscriptionResponse, NymVpnSubscriptionsResponse, NymVpnZkNym, NymVpnZkNym2, NymVpnZkNymResponse + NymDirectoryGatewayCountriesResponse, NymDirectoryGatewaysResponse, NymVpnAccountResponse, + NymVpnAccountSummaryResponse, NymVpnDevice, NymVpnDevicesResponse, NymVpnSubscription, + NymVpnSubscriptionResponse, NymVpnSubscriptionsResponse, NymVpnZkNym, NymVpnZkNym2, + NymVpnZkNymResponse, }, routes, types::{Device, GatewayMinPerformance, GatewayType, VpnApiAccount}, @@ -161,6 +164,48 @@ impl VpnApiClient { nym_http_api_client::parse_response(response, false).await } + fn create_delete_request( + &self, + path: PathSegments<'_>, + params: Params<'_, &str, &str>, + ) -> reqwest::RequestBuilder { + let base_url = self.inner.current_url().clone(); + let url = nym_http_api_client::sanitize_url(&base_url, path, params); + let client = reqwest::ClientBuilder::new().build().unwrap(); + client.delete(url) + } + + async fn delete_authorized( + &self, + path: PathSegments<'_>, + account: &VpnApiAccount, + device: Option<&Device>, + ) -> std::result::Result> + where + T: DeserializeOwned, + E: fmt::Display + DeserializeOwned, + { + let request = self + .create_delete_request(path, NO_PARAMS) + .bearer_auth(account.jwt().to_string()); + + let request = match device { + Some(device) => request.header( + DEVICE_AUTHORIZATION_HEADER, + format!("Bearer {}", device.jwt()), + ), + None => request, + }; + + let response = request.send().await.unwrap(); + + // nym_http_api_client::parse_response(response, false).await + let response_text = response.text().await.unwrap(); + + let response_json = serde_json::from_str(&response_text).unwrap(); + Ok(response_json) + } + // ACCOUNT pub async fn get_account(&self, account: &VpnApiAccount) -> Result { @@ -407,6 +452,30 @@ impl VpnApiClient { .map_err(VpnApiClientError::FailedToGetZkNymById) } + pub async fn confirm_zk_nym_download_by_id( + &self, + account: &VpnApiAccount, + device: &Device, + id: &str, + ) -> Result { + self.delete_authorized( + &[ + routes::PUBLIC, + routes::V1, + routes::ACCOUNT, + &account.id(), + routes::DEVICE, + &device.identity_key().to_string(), + routes::ZKNYM, + id, + ], + account, + Some(device), + ) + .await + .map_err(VpnApiClientError::FailedToGetZkNymById) + } + // FREEPASS pub async fn get_free_passes( From ec05e957583c8311edce5ab30dd191b1660c3818 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Mon, 4 Nov 2024 16:03:25 +0100 Subject: [PATCH 07/15] wip --- .../src/commands/mod.rs | 4 + .../src/commands/zknym.rs | 14 ++- .../src/controller.rs | 91 +++++++++++++------ 3 files changed, 80 insertions(+), 29 deletions(-) diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs index f9e069c6bd..c687ac6b3f 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs @@ -23,6 +23,7 @@ pub enum AccountCommand { RegisterDevice, RequestZkNym, GetDeviceZkNym, + ImportZkNym(String), } #[derive(Clone, Debug)] @@ -120,6 +121,9 @@ impl CommandHandler { AccountCommand::GetDeviceZkNym => { todo!() } + AccountCommand::ImportZkNym(_) => { + todo!() + } } .inspect(|_result| { tracing::info!("Command {:?} with id {} completed", self.command, self.id); diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs index 6e2182dcd1..cee2ae991f 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs @@ -92,7 +92,8 @@ pub(crate) async fn poll_zk_nym( .await { Ok(poll_response) if poll_response.status != NymVpnZkNymStatus::Pending => { - tracing::info!("zk-nym polling finished: {:#?}", poll_response); + tracing::info!("zk-nym polling finished: {}", poll_response.id); + tracing::debug!("zk-nym polling finished: {:#?}", poll_response); return PollingResult::Finished( poll_response, request.ticketbook_type, @@ -132,6 +133,8 @@ pub(crate) async fn unblind_and_aggregate( .create_ecash_keypair() .map_err(Error::CreateEcashKeyPair)?; + tracing::info!("Verifying zk-nym shares"); + let mut partial_wallets = Vec::new(); let blinded_shares = response.blinded_shares.unwrap(); for share in blinded_shares.shares { @@ -139,9 +142,11 @@ pub(crate) async fn unblind_and_aggregate( // let blinded_share: WalletShare = serde_json::from_str(&share).unwrap(); // TODO: remove unwrap + tracing::info!("Creating BlindedSignature"); let blinded_sig = BlindedSignature::try_from_bs58(&share.bs58_encoded_share).unwrap(); + tracing::info!("Calling issue_verify"); match nym_compact_ecash::issue_verify( &vk_auth, ecash_keypair.secret_key(), @@ -149,7 +154,10 @@ pub(crate) async fn unblind_and_aggregate( &request_info, share.node_index, ) { - Ok(partial_wallet) => partial_wallets.push(partial_wallet), + Ok(partial_wallet) => { + tracing::info!("Partial wallet created and appended"); + partial_wallets.push(partial_wallet) + }, Err(err) => { tracing::error!("Failed to issue verify: {:#?}", err); return Err(Error::ImportZkNym(err)); @@ -157,6 +165,8 @@ pub(crate) async fn unblind_and_aggregate( } } + tracing::info!("Aggregating wallets"); + // TODO: remove unwrap let aggregated_wallets = nym_compact_ecash::aggregate_wallets( &vk_auth, diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs index 383c8dfb94..2a2c144eac 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs @@ -190,7 +190,8 @@ where ticketbook_type: TicketType, request_info: RequestInfo, ) -> Result<(), Error> { - tracing::info!("Importing zk-nym: {:#?}", response); + tracing::info!("Importing zk-nym: {}", response.id); + // tracing::debug!("Importing zk-nym: {:#?}", response); let account = self.account_storage.load_account().await?; @@ -198,30 +199,48 @@ where return Err(Error::MissingBlindedShares); }; - let master_verification_key = match self - .credential_storage - .get_master_verification_key(blinded_shares.epoch_id) - .await? - { - Some(master_verification_key) => master_verification_key.clone(), - None => { - let vk_bs58 = blinded_shares - .master_verification_key - .as_ref() - .unwrap() - .bs58_encoded_key - .clone(); - let vk = VerificationKeyAuth::try_from_bs58(&vk_bs58).unwrap(); - let epoch_verification_key = EpochVerificationKey { - epoch_id: blinded_shares.epoch_id, - key: vk.clone(), - }; - self.credential_storage - .insert_master_verification_key(&epoch_verification_key) - .await?; - vk - } - }; + tracing::info!( + "Getting master verification key for epoch: {}", + blinded_shares.epoch_id + ); + + //let master_verification_key = match self + // .credential_storage + // .get_master_verification_key(blinded_shares.epoch_id) + // .await? + //{ + // Some(master_verification_key) => { + // tracing::info!("Master verification key found in local storage"); + // master_verification_key.clone() + // } + // None => { + // tracing::info!("Master verification key not found in local storage, importing"); + // let vk_bs58 = blinded_shares + // .master_verification_key + // .as_ref() + // .unwrap() + // .bs58_encoded_key + // .clone(); + // let vk = VerificationKeyAuth::try_from_bs58(&vk_bs58).unwrap(); + // let epoch_verification_key = EpochVerificationKey { + // epoch_id: blinded_shares.epoch_id, + // key: vk.clone(), + // }; + // tracing::info!("Inserting master verification key into local storage"); + // self.credential_storage + // .insert_master_verification_key(&epoch_verification_key) + // .await?; + // vk + // } + //}; + + let vk_bs58 = blinded_shares + .master_verification_key + .as_ref() + .unwrap() + .bs58_encoded_key + .clone(); + let master_verification_key = VerificationKeyAuth::try_from_bs58(&vk_bs58).unwrap(); let issued_ticketbook = crate::commands::zknym::unblind_and_aggregate( response, @@ -230,8 +249,7 @@ where account, master_verification_key, ) - .await - .unwrap(); + .await?; self.credential_storage .insert_issued_ticketbook(&issued_ticketbook) @@ -390,9 +408,27 @@ where for zk_nym in &reported_device_zk_nyms.items { tracing::info!("{:?}", zk_nym); } + Ok(()) } + async fn handle_import_zk_nym(&mut self, id: String) -> Result<(), Error> { + tracing::info!("Importing zk-nym with id: {}", id); + + let account = self.account_storage.load_account().await?; + let device = self.account_storage.load_device_keys().await?; + + todo!(); + //let response = self + // .vpn_api_client + // .get_zk_nym_by_id(&account, &device, &id) + // .await + // .map_err(Error::GetZkNym)?; + + // self.import_zk_nym(response, TicketType::V1MixnetEntry, RequestInfo::default()) + // .await + } + // Once we finish polling the result of the zk-nym request, we now import the zk-nym into the // local credential store async fn handle_polling_result(&mut self, result: Result) { @@ -461,6 +497,7 @@ where AccountCommand::GetDeviceZkNym => { self.handle_get_zk_nyms_available_for_download().await } + AccountCommand::ImportZkNym(id) => self.handle_import_zk_nym(id).await, } } From d4e9caf0a00265a61b4fb18703720865c3f80ded Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Mon, 4 Nov 2024 20:11:22 +0100 Subject: [PATCH 08/15] Add two more commands --- .../src/commands/mod.rs | 16 ++--- .../src/commands/update_state.rs | 20 ++++++ .../src/commands/zknym.rs | 5 +- .../src/controller.rs | 32 +++++---- .../crates/nym-vpn-api-client/src/client.rs | 63 ++++++++--------- .../crates/nym-vpn-api-client/src/response.rs | 1 - nym-vpn-core/crates/nym-vpnc/src/cli.rs | 8 +++ nym-vpn-core/crates/nym-vpnc/src/main.rs | 34 ++++++++-- .../command_interface/connection_handler.rs | 15 ++++ .../src/command_interface/listener.rs | 68 ++++++++++++++++--- .../nym-vpnd/src/service/vpn_service.rs | 30 ++++++++ proto/nym/vpn.proto | 20 ++++++ 12 files changed, 241 insertions(+), 71 deletions(-) diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs index c687ac6b3f..5a11cd3a71 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs @@ -23,7 +23,8 @@ pub enum AccountCommand { RegisterDevice, RequestZkNym, GetDeviceZkNym, - ImportZkNym(String), + GetZkNymsAvailableForDownload, + GetZkNymById(String), } #[derive(Clone, Debug)] @@ -115,15 +116,10 @@ impl CommandHandler { match self.command { AccountCommand::UpdateAccountState => self.update_shared_account_state().await, AccountCommand::RegisterDevice => self.register_device().await, - AccountCommand::RequestZkNym => { - todo!() - } - AccountCommand::GetDeviceZkNym => { - todo!() - } - AccountCommand::ImportZkNym(_) => { - todo!() - } + AccountCommand::RequestZkNym => todo!(), + AccountCommand::GetDeviceZkNym => todo!(), + AccountCommand::GetZkNymsAvailableForDownload => todo!(), + AccountCommand::GetZkNymById(_) => todo!(), } .inspect(|_result| { tracing::info!("Command {:?} with id {} completed", self.command, self.id); diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/update_state.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/update_state.rs index a87664c2a5..9df8910fe2 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/update_state.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/update_state.rs @@ -24,6 +24,7 @@ pub(crate) async fn update_state( ) -> Result<(), Error> { update_account_state(account, account_state, vpn_api_client, last_account_summary).await?; update_device_state(account, device, account_state, vpn_api_client, last_devices).await?; + get_zk_nym_status(account, device, vpn_api_client).await?; Ok(()) } @@ -108,6 +109,25 @@ async fn update_device_state( Ok(()) } +async fn get_zk_nym_status( + account: &VpnApiAccount, + device: &Device, + vpn_api_client: &nym_vpn_api_client::VpnApiClient, +) -> Result<(), Error> { + tracing::debug!("Getting ZK nym status"); + + tracing::info!("Checking device zk nyms"); + let response = vpn_api_client.get_device_zk_nyms(account, device).await; + tracing::info!("{:#?}", response); + + tracing::info!("Checking zk nyms available for download"); + let response = vpn_api_client + .get_zk_nyms_available_for_download(account, device) + .await; + tracing::info!("{:#?}", response); + Ok(()) +} + fn extract_status_code(err: &E) -> Option where E: std::error::Error + 'static, diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs index cee2ae991f..ce5bf944b1 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs @@ -143,8 +143,7 @@ pub(crate) async fn unblind_and_aggregate( // TODO: remove unwrap tracing::info!("Creating BlindedSignature"); - let blinded_sig = - BlindedSignature::try_from_bs58(&share.bs58_encoded_share).unwrap(); + let blinded_sig = BlindedSignature::try_from_bs58(&share.bs58_encoded_share).unwrap(); tracing::info!("Calling issue_verify"); match nym_compact_ecash::issue_verify( @@ -157,7 +156,7 @@ pub(crate) async fn unblind_and_aggregate( Ok(partial_wallet) => { tracing::info!("Partial wallet created and appended"); partial_wallets.push(partial_wallet) - }, + } Err(err) => { tracing::error!("Failed to issue verify: {:#?}", err); return Err(Error::ImportZkNym(err)); diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs index 2a2c144eac..0a32bd57b1 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs @@ -412,21 +412,24 @@ where Ok(()) } - async fn handle_import_zk_nym(&mut self, id: String) -> Result<(), Error> { - tracing::info!("Importing zk-nym with id: {}", id); + async fn handle_get_zk_nym_by_id(&self, id: &str) -> Result<(), Error> { + tracing::info!("Getting zk-nym by id from API"); let account = self.account_storage.load_account().await?; let device = self.account_storage.load_device_keys().await?; - todo!(); - //let response = self - // .vpn_api_client - // .get_zk_nym_by_id(&account, &device, &id) - // .await - // .map_err(Error::GetZkNym)?; + let reported_device_zk_nyms = self + .vpn_api_client + .get_zk_nym_by_id(&account, &device, id) + .await + .map_err(Error::GetZkNyms)?; - // self.import_zk_nym(response, TicketType::V1MixnetEntry, RequestInfo::default()) - // .await + tracing::info!( + "The device as the following zk-nym available to download: {:#?}", + reported_device_zk_nyms + ); + + Ok(()) } // Once we finish polling the result of the zk-nym request, we now import the zk-nym into the @@ -493,11 +496,11 @@ where AccountCommand::UpdateAccountState => self.handle_update_account_state().await, AccountCommand::RegisterDevice => self.handle_register_device().await, AccountCommand::RequestZkNym => self.handle_request_zk_nym().await, - // AccountCommand::GetDeviceZkNym => self.handle_get_device_zk_nym().await, - AccountCommand::GetDeviceZkNym => { + AccountCommand::GetDeviceZkNym => self.handle_get_device_zk_nym().await, + AccountCommand::GetZkNymsAvailableForDownload => { self.handle_get_zk_nyms_available_for_download().await } - AccountCommand::ImportZkNym(id) => self.handle_import_zk_nym(id).await, + AccountCommand::GetZkNymById(id) => self.handle_get_zk_nym_by_id(&id).await, } } @@ -609,7 +612,8 @@ where } // On a timer we want to refresh the account state _ = update_account_state_timer.tick() => { - self.queue_command(AccountCommand::UpdateAccountState); + // WIP(JON): disable timer during dev work + //self.queue_command(AccountCommand::UpdateAccountState); } _ = self.cancel_token.cancelled() => { tracing::trace!("Received cancellation signal"); diff --git a/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs b/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs index 8a119fdfd3..185910eb56 100644 --- a/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs +++ b/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs @@ -103,11 +103,35 @@ impl VpnApiClient { }; let response = request.send().await?; - let r = response.text().await; - tracing::info!("Response: {:#?}", r); - todo!(); - - nym_http_api_client::parse_response(response, false).await + let status = response.status(); + tracing::info!("Response status: {:#?}", status); + + // TODO: support this mode in the upstream crate + + if status.is_success() { + let response_text = response.text().await?; + tracing::info!("Response: {:#?}", response_text); + let response_json = serde_json::from_str(&response_text) + .map_err(|e| HttpClientError::GenericRequestFailure(e.to_string()))?; + Ok(response_json) + //} else if status == reqwest::StatusCode::NOT_FOUND { + // Err(HttpClientError::NotFound) + } else { + let Ok(response_text) = response.text().await else { + return Err(HttpClientError::RequestFailure { status }); + }; + + tracing::info!("Response: {:#?}", response_text); + + if let Ok(request_error) = serde_json::from_str(&response_text) { + Err(HttpClientError::EndpointFailure { + status, + error: request_error, + }) + } else { + Err(HttpClientError::GenericRequestFailure(response_text)) + } + } } async fn get_json_with_retry( @@ -329,7 +353,7 @@ impl VpnApiClient { account: &VpnApiAccount, device: &Device, ) -> Result { - self.get_authorized( + self.get_authorized_debug( &[ routes::PUBLIC, routes::V1, @@ -387,7 +411,7 @@ impl VpnApiClient { account: &VpnApiAccount, device: &Device, ) -> Result { - self.get_authorized( + self.get_authorized_debug( &[ routes::PUBLIC, routes::V1, @@ -405,36 +429,13 @@ impl VpnApiClient { .map_err(VpnApiClientError::FailedToGetDeviceZkNyms) } - //pub async fn get_active_zk_nym( - // &self, - // account: &VpnApiAccount, - // device: &Device, - //) -> Result { - // self.get_authorized( - // &[ - // routes::PUBLIC, - // routes::V1, - // routes::ACCOUNT, - // &account.id(), - // routes::DEVICE, - // &device.identity_key().to_string(), - // routes::ZKNYM, - // routes::ACTIVE, - // ], - // account, - // Some(device), - // ) - // .await - // .map_err(VpnApiClientError::FailedToGetActiveZkNym) - //} - pub async fn get_zk_nym_by_id( &self, account: &VpnApiAccount, device: &Device, id: &str, ) -> Result { - self.get_authorized( + self.get_authorized_debug( &[ routes::PUBLIC, routes::V1, diff --git a/nym-vpn-core/crates/nym-vpn-api-client/src/response.rs b/nym-vpn-core/crates/nym-vpn-api-client/src/response.rs index 3aea945705..0a480878f8 100644 --- a/nym-vpn-core/crates/nym-vpn-api-client/src/response.rs +++ b/nym-vpn-core/crates/nym-vpn-api-client/src/response.rs @@ -173,7 +173,6 @@ pub struct NymVpnZkNym2 { // ] //}", - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum NymVpnZkNymStatus { diff --git a/nym-vpn-core/crates/nym-vpnc/src/cli.rs b/nym-vpn-core/crates/nym-vpnc/src/cli.rs index fdd2f0916e..1aaa9cb4da 100644 --- a/nym-vpn-core/crates/nym-vpnc/src/cli.rs +++ b/nym-vpn-core/crates/nym-vpnc/src/cli.rs @@ -45,6 +45,8 @@ pub(crate) enum Command { RegisterDevice, RequestZkNym, GetDeviceZkNym, + GetZkNymsAvailableForDownload, + GetZkNymById(GetZkNymByIdArgs), FetchRawAccountSummary, FetchRawDevices, } @@ -196,6 +198,12 @@ pub(crate) struct ResetDeviceIdentityArgs { pub(crate) seed: Option, } +#[derive(Args)] +pub(crate) struct GetZkNymByIdArgs { + /// The ID of the ZK Nym to fetch. + pub(crate) id: String, +} + pub(crate) fn parse_entry_point(args: &ConnectArgs) -> Result> { if let Some(ref entry_gateway_id) = args.entry.entry_gateway_id { Ok(Some(EntryPoint::Gateway { diff --git a/nym-vpn-core/crates/nym-vpnc/src/main.rs b/nym-vpn-core/crates/nym-vpnc/src/main.rs index d0c1702461..9a2a82b9e0 100644 --- a/nym-vpn-core/crates/nym-vpnc/src/main.rs +++ b/nym-vpn-core/crates/nym-vpnc/src/main.rs @@ -7,10 +7,11 @@ use nym_gateway_directory::GatewayType; use nym_vpn_proto::{ ConnectRequest, DisconnectRequest, Empty, FetchRawAccountSummaryRequest, FetchRawDevicesRequest, GetAccountIdentityRequest, GetAccountStateRequest, - GetDeviceIdentityRequest, GetDeviceZkNymsRequest, InfoRequest, InfoResponse, - IsAccountStoredRequest, IsReadyToConnectRequest, ListCountriesRequest, ListGatewaysRequest, - RefreshAccountStateRequest, RegisterDeviceRequest, RemoveAccountRequest, RequestZkNymRequest, - ResetDeviceIdentityRequest, SetNetworkRequest, StatusRequest, StoreAccountRequest, UserAgent, + GetDeviceIdentityRequest, GetDeviceZkNymsRequest, GetZkNymByIdRequest, + GetZkNymsAvailableForDownloadRequest, InfoRequest, InfoResponse, IsAccountStoredRequest, + IsReadyToConnectRequest, ListCountriesRequest, ListGatewaysRequest, RefreshAccountStateRequest, + RegisterDeviceRequest, RemoveAccountRequest, RequestZkNymRequest, ResetDeviceIdentityRequest, + SetNetworkRequest, StatusRequest, StoreAccountRequest, UserAgent, }; use protobuf_conversion::{into_gateway_type, into_threshold}; use sysinfo::System; @@ -74,6 +75,10 @@ async fn main() -> Result<()> { Command::RegisterDevice => register_device(client_type).await?, Command::RequestZkNym => request_zk_nym(client_type).await?, Command::GetDeviceZkNym => get_device_zk_nym(client_type).await?, + Command::GetZkNymsAvailableForDownload => { + get_zk_nyms_available_for_download(client_type).await? + } + Command::GetZkNymById(args) => get_zk_nym_by_id(client_type, args).await?, Command::FetchRawAccountSummary => fetch_raw_account_summary(client_type).await?, Command::FetchRawDevices => fetch_raw_devices(client_type).await?, } @@ -305,6 +310,27 @@ async fn get_device_zk_nym(client_type: ClientType) -> Result<()> { Ok(()) } +async fn get_zk_nyms_available_for_download(client_type: ClientType) -> Result<()> { + let mut client = vpnd_client::get_client(client_type).await?; + let request = tonic::Request::new(GetZkNymsAvailableForDownloadRequest {}); + let response = client + .get_zk_nyms_available_for_download(request) + .await? + .into_inner(); + println!("{:#?}", response); + Ok(()) +} + +async fn get_zk_nym_by_id(client_type: ClientType, args: cli::GetZkNymByIdArgs) -> Result<()> { + let mut client = vpnd_client::get_client(client_type).await?; + let request = tonic::Request::new(GetZkNymByIdRequest { + id: args.id.clone(), + }); + let response = client.get_zk_nym_by_id(request).await?.into_inner(); + println!("{:#?}", response); + Ok(()) +} + async fn listen_to_status(client_type: ClientType) -> Result<()> { let mut client = vpnd_client::get_client(client_type).await?; let request = tonic::Request::new(Empty {}); diff --git a/nym-vpn-core/crates/nym-vpnd/src/command_interface/connection_handler.rs b/nym-vpn-core/crates/nym-vpnd/src/command_interface/connection_handler.rs index dbc5b14935..34b6da5a86 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/command_interface/connection_handler.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/command_interface/connection_handler.rs @@ -203,6 +203,21 @@ impl CommandInterfaceConnectionHandler { .await } + pub(crate) async fn handle_get_zk_nyms_available_for_download( + &self, + ) -> Result, VpnCommandSendError> { + self.send_and_wait(VpnServiceCommand::GetZkNymsAvailableForDownload, ()) + .await + } + + pub(crate) async fn handle_get_zk_nym_by_id( + &self, + id: String, + ) -> Result, VpnCommandSendError> { + self.send_and_wait(VpnServiceCommand::GetZkNymById, id) + .await + } + pub(crate) async fn handle_fetch_raw_account_summary( &self, ) -> Result, VpnCommandSendError> { diff --git a/nym-vpn-core/crates/nym-vpnd/src/command_interface/listener.rs b/nym-vpn-core/crates/nym-vpnd/src/command_interface/listener.rs index c26cb6a9f6..228a3b73b8 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/command_interface/listener.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/command_interface/listener.rs @@ -18,14 +18,16 @@ use nym_vpn_proto::{ FetchRawAccountSummaryRequest, FetchRawAccountSummaryResponse, FetchRawDevicesRequest, FetchRawDevicesResponse, GetAccountIdentityRequest, GetAccountIdentityResponse, GetAccountStateRequest, GetAccountStateResponse, GetDeviceIdentityRequest, - GetDeviceIdentityResponse, GetDeviceZkNymsRequest, GetDeviceZkNymsResponse, InfoRequest, - InfoResponse, IsAccountStoredRequest, IsAccountStoredResponse, IsReadyToConnectRequest, - IsReadyToConnectResponse, ListCountriesRequest, ListCountriesResponse, ListGatewaysRequest, - ListGatewaysResponse, RefreshAccountStateRequest, RefreshAccountStateResponse, - RegisterDeviceRequest, RegisterDeviceResponse, RemoveAccountRequest, RemoveAccountResponse, - RequestZkNymRequest, RequestZkNymResponse, ResetDeviceIdentityRequest, - ResetDeviceIdentityResponse, SetNetworkRequest, SetNetworkResponse, StatusRequest, - StatusResponse, StoreAccountRequest, StoreAccountResponse, + GetDeviceIdentityResponse, GetDeviceZkNymsRequest, GetDeviceZkNymsResponse, + GetZkNymByIdRequest, GetZkNymByIdResponse, GetZkNymsAvailableForDownloadRequest, + GetZkNymsAvailableForDownloadResponse, InfoRequest, InfoResponse, IsAccountStoredRequest, + IsAccountStoredResponse, IsReadyToConnectRequest, IsReadyToConnectResponse, + ListCountriesRequest, ListCountriesResponse, ListGatewaysRequest, ListGatewaysResponse, + RefreshAccountStateRequest, RefreshAccountStateResponse, RegisterDeviceRequest, + RegisterDeviceResponse, RemoveAccountRequest, RemoveAccountResponse, RequestZkNymRequest, + RequestZkNymResponse, ResetDeviceIdentityRequest, ResetDeviceIdentityResponse, + SetNetworkRequest, SetNetworkResponse, StatusRequest, StatusResponse, StoreAccountRequest, + StoreAccountResponse, }; use super::{ @@ -660,6 +662,56 @@ impl NymVpnd for CommandInterface { Ok(tonic::Response::new(response)) } + async fn get_zk_nyms_available_for_download( + &self, + _request: tonic::Request, + ) -> Result, tonic::Status> { + let result = CommandInterfaceConnectionHandler::new(self.vpn_command_tx.clone()) + .handle_get_zk_nyms_available_for_download() + .await?; + + let response = match result { + Ok(response) => GetZkNymsAvailableForDownloadResponse { + json: serde_json::to_string(&response) + .unwrap_or_else(|_| "failed to serialize".to_owned()), + error: None, + }, + Err(err) => GetZkNymsAvailableForDownloadResponse { + json: err.to_string(), + error: Some(AccountError::from(err)), + }, + }; + + tracing::debug!("Returning get zk nyms available to download response"); + Ok(tonic::Response::new(response)) + } + + async fn get_zk_nym_by_id( + &self, + request: tonic::Request, + ) -> Result, tonic::Status> { + let id = request.into_inner().id; + + let result = CommandInterfaceConnectionHandler::new(self.vpn_command_tx.clone()) + .handle_get_zk_nym_by_id(id) + .await?; + + let response = match result { + Ok(response) => GetZkNymByIdResponse { + json: serde_json::to_string(&response) + .unwrap_or_else(|_| "failed to serialize".to_owned()), + error: None, + }, + Err(err) => GetZkNymByIdResponse { + json: err.to_string(), + error: Some(AccountError::from(err)), + }, + }; + + tracing::debug!("Returning get zk nym by id response"); + Ok(tonic::Response::new(response)) + } + async fn fetch_raw_account_summary( &self, _request: tonic::Request, diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs b/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs index 2e66651028..bcdb025f96 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs @@ -114,6 +114,8 @@ pub enum VpnServiceCommand { RegisterDevice(oneshot::Sender>, ()), RequestZkNym(oneshot::Sender>, ()), GetDeviceZkNyms(oneshot::Sender>, ()), + GetZkNymsAvailableForDownload(oneshot::Sender>, ()), + GetZkNymById(oneshot::Sender>, String), FetchRawAccountSummary( oneshot::Sender>, (), @@ -146,6 +148,10 @@ impl fmt::Display for VpnServiceCommand { VpnServiceCommand::RegisterDevice(..) => write!(f, "RegisterDevice"), VpnServiceCommand::RequestZkNym(..) => write!(f, "RequestZkNym"), VpnServiceCommand::GetDeviceZkNyms(..) => write!(f, "GetDeviceZkNyms"), + VpnServiceCommand::GetZkNymsAvailableForDownload(..) => { + write!(f, "GetZkNymsAvailableForDownload") + } + VpnServiceCommand::GetZkNymById(..) => write!(f, "GetZkNymById"), VpnServiceCommand::FetchRawAccountSummary(..) => write!(f, "FetchRawAccountSummery"), VpnServiceCommand::FetchRawDevices(..) => write!(f, "FetchRawDevices"), } @@ -596,6 +602,14 @@ where let result = self.handle_get_device_zk_nyms().await; let _ = tx.send(result); } + VpnServiceCommand::GetZkNymsAvailableForDownload(tx, ()) => { + let result = self.handle_get_zk_nyms_available_for_download().await; + let _ = tx.send(result); + } + VpnServiceCommand::GetZkNymById(tx, id) => { + let result = self.handle_get_zk_nym_by_id(id).await; + let _ = tx.send(result); + } VpnServiceCommand::FetchRawAccountSummary(tx, ()) => { let result = self.handle_fetch_raw_account_summary().await; let _ = tx.send(result); @@ -947,6 +961,22 @@ where }) } + async fn handle_get_zk_nyms_available_for_download(&self) -> Result<(), AccountError> { + self.account_command_tx + .send(AccountCommand::GetZkNymsAvailableForDownload) + .map_err(|err| AccountError::SendCommand { + source: Box::new(err), + }) + } + + async fn handle_get_zk_nym_by_id(&self, id: String) -> Result<(), AccountError> { + self.account_command_tx + .send(AccountCommand::GetZkNymById(id)) + .map_err(|err| AccountError::SendCommand { + source: Box::new(err), + }) + } + async fn handle_fetch_raw_account_summary( &self, ) -> Result { diff --git a/proto/nym/vpn.proto b/proto/nym/vpn.proto index e69fd75970..766a918658 100644 --- a/proto/nym/vpn.proto +++ b/proto/nym/vpn.proto @@ -694,6 +694,22 @@ message GetDeviceZkNymsResponse { AccountError error = 2; } +message GetZkNymsAvailableForDownloadRequest {} + +message GetZkNymsAvailableForDownloadResponse { + string json = 1; + AccountError error = 2; +} + +message GetZkNymByIdRequest { + string id = 1; +} + +message GetZkNymByIdResponse { + string json = 1; + AccountError error = 2; +} + message IsReadyToConnectRequest {} message IsReadyToConnectResponse { @@ -803,6 +819,10 @@ service NymVpnd { // List the zk-nyms associated with this device from the nym-vpn-api rpc GetDeviceZkNyms (GetDeviceZkNymsRequest) returns (GetDeviceZkNymsResponse) {} + rpc GetZkNymsAvailableForDownload (GetZkNymsAvailableForDownloadRequest) returns (GetZkNymsAvailableForDownloadResponse) {} + + rpc GetZkNymById (GetZkNymByIdRequest) returns (GetZkNymByIdResponse) {} + // -- Delegated remote calls -- // These query the remote nym-vpn-api state directly From 26ce26c118edca58f3c60daca3ea0ebe8c32f478 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Mon, 4 Nov 2024 20:15:00 +0100 Subject: [PATCH 09/15] Clean up some commented out stuff --- .../src/commands/zknym.rs | 5 +-- .../src/controller.rs | 33 +------------------ .../nym-vpn-account-controller/src/storage.rs | 1 + .../crates/nym-vpn-api-client/src/response.rs | 17 ---------- 4 files changed, 3 insertions(+), 53 deletions(-) diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs index ce5bf944b1..cb051aab3e 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs @@ -14,7 +14,7 @@ use nym_vpn_api_client::{ }; use time::{format_description::well_known::Rfc3339, Date, OffsetDateTime}; -use crate::{error::Error, models::WalletShare}; +use crate::error::Error; pub(crate) struct ZkNymRequestData { withdrawal_request: WithdrawalRequest, @@ -138,9 +138,6 @@ pub(crate) async fn unblind_and_aggregate( let mut partial_wallets = Vec::new(); let blinded_shares = response.blinded_shares.unwrap(); for share in blinded_shares.shares { - // TODO: remove unwrap - // let blinded_share: WalletShare = serde_json::from_str(&share).unwrap(); - // TODO: remove unwrap tracing::info!("Creating BlindedSignature"); let blinded_sig = BlindedSignature::try_from_bs58(&share.bs58_encoded_share).unwrap(); diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs index 0a32bd57b1..f7faf71978 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs @@ -6,7 +6,6 @@ use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Duration}; use futures::StreamExt; use nym_compact_ecash::Base58; use nym_config::defaults::NymNetworkDetails; -use nym_credentials::EpochVerificationKey; use nym_credentials_interface::{RequestInfo, TicketType, VerificationKeyAuth}; use nym_http_api_client::UserAgent; use nym_vpn_api_client::{ @@ -204,36 +203,6 @@ where blinded_shares.epoch_id ); - //let master_verification_key = match self - // .credential_storage - // .get_master_verification_key(blinded_shares.epoch_id) - // .await? - //{ - // Some(master_verification_key) => { - // tracing::info!("Master verification key found in local storage"); - // master_verification_key.clone() - // } - // None => { - // tracing::info!("Master verification key not found in local storage, importing"); - // let vk_bs58 = blinded_shares - // .master_verification_key - // .as_ref() - // .unwrap() - // .bs58_encoded_key - // .clone(); - // let vk = VerificationKeyAuth::try_from_bs58(&vk_bs58).unwrap(); - // let epoch_verification_key = EpochVerificationKey { - // epoch_id: blinded_shares.epoch_id, - // key: vk.clone(), - // }; - // tracing::info!("Inserting master verification key into local storage"); - // self.credential_storage - // .insert_master_verification_key(&epoch_verification_key) - // .await?; - // vk - // } - //}; - let vk_bs58 = blinded_shares .master_verification_key .as_ref() @@ -274,7 +243,7 @@ where } // Get the ticket types that are below the threshold - let ticket_types_needed_to_request = local_remaining_tickets + let _ticket_types_needed_to_request = local_remaining_tickets .into_iter() .filter(|(_, remaining)| *remaining < TICKET_THRESHOLD) .map(|(ticket_type, _)| ticket_type) diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs index b522bc456e..2be4e2b751 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs @@ -135,6 +135,7 @@ impl VpnCredentialStorage { .map_err(Error::from) } + #[allow(unused)] pub(crate) async fn get_master_verification_key( &self, epoch_id: u64, diff --git a/nym-vpn-core/crates/nym-vpn-api-client/src/response.rs b/nym-vpn-core/crates/nym-vpn-api-client/src/response.rs index 0a480878f8..1440fd0017 100644 --- a/nym-vpn-core/crates/nym-vpn-api-client/src/response.rs +++ b/nym-vpn-core/crates/nym-vpn-api-client/src/response.rs @@ -156,23 +156,6 @@ pub struct NymVpnZkNym2 { pub status: NymVpnZkNymStatus, } -//"{ -// \"totalItems\":1, -// \"page\":1, -// \"pageSize\":100, -// \"items\":[ -// { -// \"id\":\"k09ww30bcaqv2j5\", -// \"status\":\"active\", -// \"last_updated_utc\":\"2024-11-01 21:27:29.205Z\", -// \"created_on_utc\":\"2024-11-01 21:27:22.842Z\", -// \"valid_until_utc\":\"2024-12-01 21:26:07.909Z\", -// \"valid_from_utc\":\"2024-11-01 21:27:22.503Z\", -// \"issued_bandwidth_in_gb\":25 -// } -// ] -//}", - #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum NymVpnZkNymStatus { From f53aaf40ae93efc01cd40284146994c1294ad458 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Mon, 4 Nov 2024 23:18:20 +0100 Subject: [PATCH 10/15] wip: import done --- .../src/commands/zknym.rs | 47 +++++++--- .../src/controller.rs | 86 ++++++++++++++++--- .../nym-vpn-account-controller/src/error.rs | 15 ++++ .../nym-vpn-account-controller/src/lib.rs | 1 - .../nym-vpn-account-controller/src/models.rs | 7 -- .../nym-vpn-account-controller/src/storage.rs | 3 - .../crates/nym-vpn-api-client/src/client.rs | 21 +++++ .../crates/nym-vpn-api-client/src/error.rs | 5 ++ 8 files changed, 146 insertions(+), 39 deletions(-) delete mode 100644 nym-vpn-core/crates/nym-vpn-account-controller/src/models.rs diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs index cb051aab3e..540b2a8ed2 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs @@ -1,9 +1,15 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use std::time::{Duration, Instant}; +use std::{ + collections::HashMap, + time::{Duration, Instant}, +}; use nym_compact_ecash::{Base58, BlindedSignature, VerificationKeyAuth, WithdrawalRequest}; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + PartialVerificationKeysResponse, TicketbookWalletSharesResponse, +}; use nym_credentials::IssuedTicketBook; use nym_credentials_interface::{PublicKeyUser, RequestInfo, TicketType}; use nym_ecash_time::EcashTime; @@ -12,7 +18,7 @@ use nym_vpn_api_client::{ types::{Device, VpnApiAccount}, VpnApiClientError, }; -use time::{format_description::well_known::Rfc3339, Date, OffsetDateTime}; +use time::Date; use crate::error::Error; @@ -123,28 +129,44 @@ pub(crate) async fn poll_zk_nym( } pub(crate) async fn unblind_and_aggregate( - response: NymVpnZkNym2, + shares: TicketbookWalletSharesResponse, + issuers: PartialVerificationKeysResponse, + master_vk: VerificationKeyAuth, ticketbook_type: TicketType, + expiration_date: Date, request_info: RequestInfo, account: VpnApiAccount, - vk_auth: VerificationKeyAuth, ) -> Result { let ecash_keypair = account .create_ecash_keypair() .map_err(Error::CreateEcashKeyPair)?; + tracing::info!("Setting up decoded keys"); + + let mut decoded_keys = HashMap::new(); + for key in issuers.keys { + let vk = VerificationKeyAuth::try_from_bs58(&key.bs58_encoded_key).unwrap(); + decoded_keys.insert(key.node_index, vk); + } + tracing::info!("Verifying zk-nym shares"); let mut partial_wallets = Vec::new(); - let blinded_shares = response.blinded_shares.unwrap(); - for share in blinded_shares.shares { - // TODO: remove unwrap + for share in shares.shares { tracing::info!("Creating BlindedSignature"); - let blinded_sig = BlindedSignature::try_from_bs58(&share.bs58_encoded_share).unwrap(); + let blinded_sig = + BlindedSignature::try_from_bs58(&share.bs58_encoded_share).map_err(|err| { + tracing::error!("Failed to create BlindedSignature: {:#?}", err); + Error::DeserializeBlindedSignature(err) + })?; + + let Some(vk) = decoded_keys.get(&share.node_index) else { + panic!(); + }; tracing::info!("Calling issue_verify"); match nym_compact_ecash::issue_verify( - &vk_auth, + vk, ecash_keypair.secret_key(), &blinded_sig, &request_info, @@ -165,19 +187,16 @@ pub(crate) async fn unblind_and_aggregate( // TODO: remove unwrap let aggregated_wallets = nym_compact_ecash::aggregate_wallets( - &vk_auth, + &master_vk, ecash_keypair.secret_key(), &partial_wallets, &request_info, ) .unwrap(); - // TODO: remove unwrap - let expiration_date = OffsetDateTime::parse(&response.valid_until_utc, &Rfc3339).unwrap(); - let ticketbook = IssuedTicketBook::new( aggregated_wallets.into_wallet_signatures(), - blinded_shares.epoch_id, + shares.epoch_id, ecash_keypair.into(), ticketbook_type, expiration_date.ecash_date(), diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs index f7faf71978..e56799bc3e 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs @@ -6,7 +6,9 @@ use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Duration}; use futures::StreamExt; use nym_compact_ecash::Base58; use nym_config::defaults::NymNetworkDetails; +use nym_credentials::EpochVerificationKey; use nym_credentials_interface::{RequestInfo, TicketType, VerificationKeyAuth}; +use nym_ecash_time::EcashTime; use nym_http_api_client::UserAgent; use nym_vpn_api_client::{ response::{ @@ -16,6 +18,7 @@ use nym_vpn_api_client::{ types::{Device, VpnApiAccount}, }; use nym_vpn_store::VpnStorage; +use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use tokio::{ sync::mpsc::{UnboundedReceiver, UnboundedSender}, task::{JoinError, JoinSet}, @@ -190,36 +193,91 @@ where request_info: RequestInfo, ) -> Result<(), Error> { tracing::info!("Importing zk-nym: {}", response.id); - // tracing::debug!("Importing zk-nym: {:#?}", response); let account = self.account_storage.load_account().await?; - let Some(ref blinded_shares) = response.blinded_shares else { + let Some(ref shares) = response.blinded_shares else { return Err(Error::MissingBlindedShares); }; - tracing::info!( - "Getting master verification key for epoch: {}", - blinded_shares.epoch_id - ); + let issuers = self + .vpn_api_client + .get_directory_zk_nyms_ticketbookt_partial_verification_keys() + .await + .map_err(Error::GetZkNyms)?; + + if shares.epoch_id != issuers.epoch_id { + return Err(Error::InconsistentEpochId); + } - let vk_bs58 = blinded_shares + tracing::info!("epoch_id: {}", shares.epoch_id); + + let master_vk_bs58 = shares .master_verification_key - .as_ref() - .unwrap() - .bs58_encoded_key - .clone(); - let master_verification_key = VerificationKeyAuth::try_from_bs58(&vk_bs58).unwrap(); + .clone() + .ok_or(Error::MissingMasterVerificationKey)? + .bs58_encoded_key; + + let master_vk = VerificationKeyAuth::try_from_bs58(&master_vk_bs58) + .map_err(Error::InvalidMasterVerificationKey)?; + + let expiration_date = OffsetDateTime::parse(&response.valid_until_utc, &Rfc3339) + .map_err(Error::InvalidExpirationDate)?; let issued_ticketbook = crate::commands::zknym::unblind_and_aggregate( - response, + shares.clone(), + issuers, + master_vk.clone(), ticketbook_type, + expiration_date.ecash_date(), request_info, account, - master_verification_key, ) .await?; + // Insert master verification key + let epoch_vk = EpochVerificationKey { + epoch_id: shares.epoch_id, + key: master_vk, + }; + self.credential_storage + .insert_master_verification_key(&epoch_vk) + .await + .inspect_err(|err| { + tracing::error!("Failed to insert master verification key: {:#?}", err); + }) + .ok(); + + // Insert aggregated coin index signatures + self.credential_storage + .insert_coin_index_signatures( + &shares + .aggregated_coin_index_signatures + .clone() + .unwrap() + .signatures, + ) + .await + .inspect_err(|err| { + tracing::error!("Failed to insert coin index signatures: {:#?}", err); + }) + .ok(); + + // Insert aggregated expiration date signatures + self.credential_storage + .insert_expiration_date_signatures( + &shares + .aggregated_expiration_date_signatures + .clone() + .unwrap() + .signatures, + ) + .await + .inspect_err(|err| { + tracing::error!("Failed to insert expiration date signatures: {:#?}", err); + }) + .ok(); + self.credential_storage .insert_issued_ticketbook(&issued_ticketbook) .await?; diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/error.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/error.rs index 3b371af175..c57aefaaab 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/error.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/error.rs @@ -77,6 +77,21 @@ pub enum Error { #[error("succesfull zknym response is missing blinded shares")] MissingBlindedShares, + + #[error("missing master verification key")] + MissingMasterVerificationKey, + + #[error("invalid master verification key: {0}")] + InvalidMasterVerificationKey(#[source] nym_compact_ecash::CompactEcashError), + + #[error("failed to deserialize blinded signature")] + DeserializeBlindedSignature(nym_compact_ecash::CompactEcashError), + + #[error("inconsistent epoch id")] + InconsistentEpochId, + + #[error("invalid expiration date")] + InvalidExpirationDate(#[source] time::error::Parse), } impl Error { diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/lib.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/lib.rs index 891cca11e9..a395f0dd0b 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/lib.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/lib.rs @@ -11,7 +11,6 @@ pub mod shared_state; mod commands; mod controller; mod error; -mod models; mod storage; pub use commands::AccountCommand; diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/models.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/models.rs deleted file mode 100644 index 7a104c3e05..0000000000 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/models.rs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -// Re-import models from nym-credential-proxy-requests. It's not yet clear if we should possibly -// copy-paste here, or rely on it directly. - -pub use nym_credential_proxy_requests::api::v1::ticketbook::models::WalletShare; diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs index 2be4e2b751..150e422933 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs @@ -124,7 +124,6 @@ impl VpnCredentialStorage { .map_err(Error::from) } - #[allow(unused)] pub(crate) async fn insert_master_verification_key( &self, key: &EpochVerificationKey, @@ -146,7 +145,6 @@ impl VpnCredentialStorage { .map_err(Error::from) } - #[allow(unused)] pub(crate) async fn insert_coin_index_signatures( &self, signatures: &AggregatedCoinIndicesSignatures, @@ -157,7 +155,6 @@ impl VpnCredentialStorage { .map_err(Error::from) } - #[allow(unused)] pub(crate) async fn insert_expiration_date_signatures( &self, signatures: &AggregatedExpirationDateSignatures, diff --git a/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs b/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs index 185910eb56..3d08e5e608 100644 --- a/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs +++ b/nym-vpn-core/crates/nym-vpn-api-client/src/client.rs @@ -4,6 +4,7 @@ use std::{fmt, time::Duration}; use backon::Retryable; +use nym_credential_proxy_requests::api::v1::ticketbook::models::PartialVerificationKeysResponse; use nym_http_api_client::{HttpClientError, Params, PathSegments, UserAgent, NO_PARAMS}; use reqwest::Url; use serde::{de::DeserializeOwned, Deserialize, Serialize}; @@ -757,6 +758,26 @@ impl VpnApiClient { .await .map_err(VpnApiClientError::FailedToGetExitGatewayCountries) } + + // DIRECTORY ZK-NYM + + pub async fn get_directory_zk_nyms_ticketbookt_partial_verification_keys( + &self, + ) -> Result { + self.get_json_with_retry( + &[ + routes::PUBLIC, + routes::V1, + routes::DIRECTORY, + "zk-nyms", + "ticketbook", + "partial-verification-keys", + ], + NO_PARAMS, + ) + .await + .map_err(VpnApiClientError::FailedToGetDirectoryZkNymsTicketbookPartialVerificationKeys) + } } #[cfg(test)] diff --git a/nym-vpn-core/crates/nym-vpn-api-client/src/error.rs b/nym-vpn-core/crates/nym-vpn-api-client/src/error.rs index 19b6857760..f65f42bfe8 100644 --- a/nym-vpn-core/crates/nym-vpn-api-client/src/error.rs +++ b/nym-vpn-core/crates/nym-vpn-api-client/src/error.rs @@ -86,6 +86,11 @@ pub enum VpnApiClientError { #[error("failed to derive from path")] CosmosDeriveFromPath(#[source] nym_validator_client::nyxd::bip32::Error), + + #[error("failed to get directory zk-nym ticketbook partial verification keys")] + FailedToGetDirectoryZkNymsTicketbookPartialVerificationKeys( + #[source] HttpClientError, + ), } pub type Result = std::result::Result; From 3238029dab3389be42f6c9ebfcc22a07cc93705e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Tue, 5 Nov 2024 00:33:04 +0100 Subject: [PATCH 11/15] wip: working --- .../src/commands/zknym.rs | 13 ++++--- .../src/controller.rs | 34 +++++++++++++++---- 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs index 540b2a8ed2..e83fc4d39d 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/zknym.rs @@ -22,10 +22,11 @@ use time::Date; use crate::error::Error; +#[derive(Debug, Clone)] pub(crate) struct ZkNymRequestData { withdrawal_request: WithdrawalRequest, ecash_pubkey: PublicKeyUser, - expiration_date: Date, + pub(crate) expiration_date: Date, ticketbook_type: TicketType, request_info: RequestInfo, } @@ -103,7 +104,8 @@ pub(crate) async fn poll_zk_nym( return PollingResult::Finished( poll_response, request.ticketbook_type, - Box::new(request.request_info), + Box::new(request.request_info.clone()), + request, ); } Ok(poll_response) => { @@ -194,12 +196,15 @@ pub(crate) async fn unblind_and_aggregate( ) .unwrap(); + tracing::info!("Creating ticketbook"); + let ticketbook = IssuedTicketBook::new( aggregated_wallets.into_wallet_signatures(), shares.epoch_id, ecash_keypair.into(), ticketbook_type, - expiration_date.ecash_date(), + // expiration_date.ecash_date(), + expiration_date, ); Ok(ticketbook) @@ -207,7 +212,7 @@ pub(crate) async fn unblind_and_aggregate( #[derive(Debug)] pub(crate) enum PollingResult { - Finished(NymVpnZkNym2, TicketType, Box), + Finished(NymVpnZkNym2, TicketType, Box, ZkNymRequestData), Timeout(NymVpnZkNym2), Error(PollingError), } diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs index e56799bc3e..1e0d69d9c8 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs @@ -191,6 +191,7 @@ where response: NymVpnZkNym2, ticketbook_type: TicketType, request_info: RequestInfo, + request: ZkNymRequestData, ) -> Result<(), Error> { tracing::info!("Importing zk-nym: {}", response.id); @@ -221,8 +222,23 @@ where let master_vk = VerificationKeyAuth::try_from_bs58(&master_vk_bs58) .map_err(Error::InvalidMasterVerificationKey)?; - let expiration_date = OffsetDateTime::parse(&response.valid_until_utc, &Rfc3339) - .map_err(Error::InvalidExpirationDate)?; + // dbg!(&response.valid_until_utc); + //let format = "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]Z"; + //let expiration_date = OffsetDateTime::parse( + // &response.valid_until_utc, + // &time::format_description::parse(format).unwrap(), + //) + //.map_err(Error::InvalidExpirationDate)?; + //let expiration_date = OffsetDateTime::parse(&response.valid_until_utc, &Rfc3339) + // .map_err(Error::InvalidExpirationDate)?; + //let expiration_date = time::Date::parse(&response.valid_until_utc, &Rfc3339) + // .map_err(Error::InvalidExpirationDate)?; + //let format = time::format_description::parse( + // "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]Z", + //) + //.unwrap(); + //let expiration_date = time::Date::parse(&response.valid_until_utc, &format).unwrap(); + let expiration_date = request.expiration_date; let issued_ticketbook = crate::commands::zknym::unblind_and_aggregate( shares.clone(), @@ -236,6 +252,7 @@ where .await?; // Insert master verification key + tracing::info!("Inserting master verification key"); let epoch_vk = EpochVerificationKey { epoch_id: shares.epoch_id, key: master_vk, @@ -249,6 +266,7 @@ where .ok(); // Insert aggregated coin index signatures + tracing::info!("Inserting coin index signatures"); self.credential_storage .insert_coin_index_signatures( &shares @@ -264,6 +282,7 @@ where .ok(); // Insert aggregated expiration date signatures + tracing::info!("Inserting expiration date signatures"); self.credential_storage .insert_expiration_date_signatures( &shares @@ -278,6 +297,7 @@ where }) .ok(); + tracing::info!("Inserting issued ticketbook"); self.credential_storage .insert_issued_ticketbook(&issued_ticketbook) .await?; @@ -468,18 +488,18 @@ where }; match result { - PollingResult::Finished(response, ticketbook_type, request_info) + PollingResult::Finished(response, ticketbook_type, request_info, request) if response.status == NymVpnZkNymStatus::Active => { tracing::info!("Polling finished succesfully, importing ticketbook"); - self.import_zk_nym(response, ticketbook_type, *request_info) + self.import_zk_nym(response, ticketbook_type, *request_info, request) .await .inspect_err(|err| { tracing::error!("Failed to import zk-nym: {:#?}", err); }) .ok(); } - PollingResult::Finished(response, _, _) => { + PollingResult::Finished(response, _, _, _) => { tracing::warn!( "Polling finished with status: {:?}, not importing!", response.status @@ -615,6 +635,7 @@ where // Timer to periodically refresh the remote account state let mut update_account_state_timer = tokio::time::interval(Duration::from_secs(5 * 60)); + tracing::info!("Account controller starting loop"); loop { tokio::select! { // Handle incoming commands @@ -639,8 +660,7 @@ where } // On a timer we want to refresh the account state _ = update_account_state_timer.tick() => { - // WIP(JON): disable timer during dev work - //self.queue_command(AccountCommand::UpdateAccountState); + self.queue_command(AccountCommand::UpdateAccountState); } _ = self.cancel_token.cancelled() => { tracing::trace!("Received cancellation signal"); From 6aa53ed3a1fcd0da1041792463ded7424023f540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Tue, 5 Nov 2024 00:40:55 +0100 Subject: [PATCH 12/15] Start on confirm zknym download --- .../nym-vpn-account-controller/src/controller.rs | 16 ++++++++++++++++ .../nym-vpn-account-controller/src/error.rs | 5 ++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs index 1e0d69d9c8..ce8e9a0518 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs @@ -302,6 +302,22 @@ where .insert_issued_ticketbook(&issued_ticketbook) .await?; + self.confirm_zk_nym_downloaded(&response.id).await?; + + Ok(()) + } + + async fn confirm_zk_nym_downloaded(&self, id: &str) -> Result<(), Error> { + let account = self.account_storage.load_account().await?; + let device = self.account_storage.load_device_keys().await?; + + let response = self + .vpn_api_client + .confirm_zk_nym_download_by_id(&account, &device, id) + .await + .map_err(Error::ConfirmZkNymDownloaded)?; + + tracing::info!("Confirmed zk-nym downloaded: {:#?}", response); Ok(()) } diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/error.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/error.rs index c57aefaaab..2e665e7732 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/error.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/error.rs @@ -90,8 +90,11 @@ pub enum Error { #[error("inconsistent epoch id")] InconsistentEpochId, - #[error("invalid expiration date")] + #[error("invalid expiration date: {0}")] InvalidExpirationDate(#[source] time::error::Parse), + + #[error("failed to confirm zk-nym downloaded: {0}")] + ConfirmZkNymDownloaded(#[source] nym_vpn_api_client::VpnApiClientError), } impl Error { From 3d3032ac4b7cab51e826e53845fe2bd3f5b586af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Tue, 5 Nov 2024 10:16:50 +0100 Subject: [PATCH 13/15] wip: print available bandwidth --- nym-vpn-core/Cargo.lock | 1 + .../nym-vpn-account-controller/Cargo.toml | 3 + .../src/commands/mod.rs | 2 + .../src/controller.rs | 27 +++--- .../nym-vpn-account-controller/src/storage.rs | 87 ++++++++++++++++++- 5 files changed, 102 insertions(+), 18 deletions(-) diff --git a/nym-vpn-core/Cargo.lock b/nym-vpn-core/Cargo.lock index 4e6d17a0b5..5e319e4da8 100644 --- a/nym-vpn-core/Cargo.lock +++ b/nym-vpn-core/Cargo.lock @@ -4631,6 +4631,7 @@ dependencies = [ "reqwest 0.11.27", "serde", "serde_json", + "si-scale", "strum", "strum_macros", "thiserror", diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/Cargo.toml b/nym-vpn-core/crates/nym-vpn-account-controller/Cargo.toml index 7c833b391c..b6d98d1e00 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/Cargo.toml +++ b/nym-vpn-core/crates/nym-vpn-account-controller/Cargo.toml @@ -10,6 +10,7 @@ license.workspace = true [dependencies] futures.workspace = true +# nym-client-core = { workspace = true, features = ["cli", "fs-credentials-storage", "fs-surb-storage", "fs-gateways-storage"] } nym-compact-ecash.workspace = true nym-config.workspace = true nym-credential-proxy-requests.workspace = true @@ -33,4 +34,6 @@ tokio-util.workspace = true tokio.workspace = true tracing.workspace = true url.workspace = true + uuid = "1.11" +si-scale = "0.2.3" diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs index 5a11cd3a71..59cd81d493 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/commands/mod.rs @@ -25,6 +25,7 @@ pub enum AccountCommand { GetDeviceZkNym, GetZkNymsAvailableForDownload, GetZkNymById(String), + GetAvailableTickets, } #[derive(Clone, Debug)] @@ -120,6 +121,7 @@ impl CommandHandler { AccountCommand::GetDeviceZkNym => todo!(), AccountCommand::GetZkNymsAvailableForDownload => todo!(), AccountCommand::GetZkNymById(_) => todo!(), + AccountCommand::GetAvailableTickets => todo!(), } .inspect(|_result| { tracing::info!("Command {:?} with id {} completed", self.command, self.id); diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs index ce8e9a0518..b148b90fdc 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/controller.rs @@ -222,22 +222,6 @@ where let master_vk = VerificationKeyAuth::try_from_bs58(&master_vk_bs58) .map_err(Error::InvalidMasterVerificationKey)?; - // dbg!(&response.valid_until_utc); - //let format = "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]Z"; - //let expiration_date = OffsetDateTime::parse( - // &response.valid_until_utc, - // &time::format_description::parse(format).unwrap(), - //) - //.map_err(Error::InvalidExpirationDate)?; - //let expiration_date = OffsetDateTime::parse(&response.valid_until_utc, &Rfc3339) - // .map_err(Error::InvalidExpirationDate)?; - //let expiration_date = time::Date::parse(&response.valid_until_utc, &Rfc3339) - // .map_err(Error::InvalidExpirationDate)?; - //let format = time::format_description::parse( - // "[year]-[month]-[day] [hour]:[minute]:[second].[subsecond]Z", - //) - //.unwrap(); - //let expiration_date = time::Date::parse(&response.valid_until_utc, &format).unwrap(); let expiration_date = request.expiration_date; let issued_ticketbook = crate::commands::zknym::unblind_and_aggregate( @@ -302,7 +286,7 @@ where .insert_issued_ticketbook(&issued_ticketbook) .await?; - self.confirm_zk_nym_downloaded(&response.id).await?; + //self.confirm_zk_nym_downloaded(&response.id).await?; Ok(()) } @@ -495,6 +479,14 @@ where Ok(()) } + async fn handle_get_available_tickets(&self) -> Result<(), Error> { + tracing::info!("Getting available tickets from API"); + + self.credential_storage.print_info().await?; + + Ok(()) + } + // Once we finish polling the result of the zk-nym request, we now import the zk-nym into the // local credential store async fn handle_polling_result(&mut self, result: Result) { @@ -564,6 +556,7 @@ where self.handle_get_zk_nyms_available_for_download().await } AccountCommand::GetZkNymById(id) => self.handle_get_zk_nym_by_id(&id).await, + AccountCommand::GetAvailableTickets => self.handle_get_available_tickets().await, } } diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs index 150e422933..b579472224 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs @@ -1,9 +1,11 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use std::sync::Arc; +use std::{str::FromStr, sync::Arc}; use nym_compact_ecash::VerificationKeyAuth; +use nym_config::defaults::TicketTypeRepr; +use nym_credential_storage::models::BasicTicketbookInformation; use nym_credentials::{ AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, IssuedTicketBook, @@ -12,6 +14,8 @@ use nym_credentials_interface::TicketType; use nym_sdk::mixnet::CredentialStorage; use nym_vpn_api_client::types::{Device, VpnApiAccount}; use nym_vpn_store::{keys::KeyStore, mnemonic::MnemonicStorage, VpnStorage}; +use serde::{Deserialize, Serialize}; +use time::Date; use crate::error::Error; @@ -170,6 +174,28 @@ impl VpnCredentialStorage { tracing::info!("Ticketbooks stored: {}", ticketbooks_info.len()); for ticketbook in ticketbooks_info { tracing::info!("Ticketbook id: {}", ticketbook.id); + tracing::info!("Ticketbook total_tickets: {:#?}", ticketbook.total_tickets); + tracing::info!("Ticketbook used_tickets: {:#?}", ticketbook.used_tickets); + tracing::info!( + "Ticketbook ticketbook_type: {:#?}", + ticketbook.ticketbook_type + ); + tracing::info!( + "Ticketbook expiration_date: {:#?}", + ticketbook.expiration_date + ); + tracing::info!("Ticketbook epoch_id: {:#?}", ticketbook.epoch_id); + + let tickets_left = ticketbook.total_tickets - ticketbook.used_tickets; + let ticketbook_type = TicketType::from_str(&ticketbook.ticketbook_type).unwrap(); + dbg!(&ticketbook_type); + let ticketbook_type = ticketbook_type.to_repr(); + let bandwidth_left = u64::from(tickets_left) * ticketbook_type.bandwidth_value(); + tracing::info!("bandwidth_left: {:#?}", bandwidth_left); + + let avail_ticketbook = AvailableTicketbook::try_from(ticketbook).unwrap(); + // tracing::info!("Basic ticketbook: {:#?}", avail_ticketbook); + tracing::info!("Basic ticketbook: {}", avail_ticketbook); } let pending_ticketbooks = self.storage.get_pending_ticketbooks().await?; @@ -180,6 +206,65 @@ impl VpnCredentialStorage { } } +#[derive(Serialize, Deserialize)] +pub struct AvailableTicketbook { + pub id: i64, + pub typ: TicketType, + pub expiration: Date, + pub issued_tickets: u32, + pub claimed_tickets: u32, + pub ticket_size: u64, +} + +impl TryFrom for AvailableTicketbook { + type Error = Error; + + fn try_from(value: BasicTicketbookInformation) -> Result { + let typ = value.ticketbook_type.parse().map_err(|_| Error::NoEpoch)?; + Ok(AvailableTicketbook { + id: value.id, + typ, + expiration: value.expiration_date, + issued_tickets: value.total_tickets, + claimed_tickets: value.used_tickets, + ticket_size: typ.to_repr().bandwidth_value(), + }) + } +} + +impl AvailableTicketbook { + fn print(&self) { + let ecash_today = nym_ecash_time::ecash_today().date(); + + let issued = self.issued_tickets; + let si_issued = si_scale::helpers::bibytes2((issued as u64 * self.ticket_size) as f64); + + let claimed = self.claimed_tickets; + let si_claimed = si_scale::helpers::bibytes2((claimed as u64 * self.ticket_size) as f64); + + let remaining = issued - claimed; + let si_remaining = + si_scale::helpers::bibytes2((remaining as u64 * self.ticket_size) as f64); + let si_size = si_scale::helpers::bibytes2(self.ticket_size as f64); + + let expiration = if self.expiration <= ecash_today { + format!("EXPIRED ON {}", self.expiration) + } else { + self.expiration.to_string() + }; + + tracing::info!( + "Ticketbook id: {} - Type: {} - Size: {} - Issued: {} - Claimed: {} - Remaining: {} - Expiration: {}", + self.id, + self.typ, + si_size, + si_issued, + si_claimed, + si_remaining, + expiration + ); + } +} // TODO: add #[derive(EnumIter)] to TicketType so we can iterate over it directly. fn ticketbook_types() -> [TicketType; 4] { [ From cc7b2f94aec0594c7d200168a72137dd1e133da7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Tue, 5 Nov 2024 10:19:35 +0100 Subject: [PATCH 14/15] wip --- nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs index b579472224..a712f88fbb 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs @@ -194,8 +194,7 @@ impl VpnCredentialStorage { tracing::info!("bandwidth_left: {:#?}", bandwidth_left); let avail_ticketbook = AvailableTicketbook::try_from(ticketbook).unwrap(); - // tracing::info!("Basic ticketbook: {:#?}", avail_ticketbook); - tracing::info!("Basic ticketbook: {}", avail_ticketbook); + avail_ticketbook.print(); } let pending_ticketbooks = self.storage.get_pending_ticketbooks().await?; From 85666bc9c045c53c721373f521e6928529537ecd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Tue, 5 Nov 2024 10:32:47 +0100 Subject: [PATCH 15/15] wip --- .../nym-vpn-account-controller/src/storage.rs | 34 +++++-------------- 1 file changed, 8 insertions(+), 26 deletions(-) diff --git a/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs b/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs index a712f88fbb..8305ed9e27 100644 --- a/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs +++ b/nym-vpn-core/crates/nym-vpn-account-controller/src/storage.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use std::{str::FromStr, sync::Arc}; +use std::{fmt, str::FromStr, sync::Arc}; use nym_compact_ecash::VerificationKeyAuth; use nym_config::defaults::TicketTypeRepr; @@ -173,28 +173,8 @@ impl VpnCredentialStorage { let ticketbooks_info = self.storage.get_ticketbooks_info().await?; tracing::info!("Ticketbooks stored: {}", ticketbooks_info.len()); for ticketbook in ticketbooks_info { - tracing::info!("Ticketbook id: {}", ticketbook.id); - tracing::info!("Ticketbook total_tickets: {:#?}", ticketbook.total_tickets); - tracing::info!("Ticketbook used_tickets: {:#?}", ticketbook.used_tickets); - tracing::info!( - "Ticketbook ticketbook_type: {:#?}", - ticketbook.ticketbook_type - ); - tracing::info!( - "Ticketbook expiration_date: {:#?}", - ticketbook.expiration_date - ); - tracing::info!("Ticketbook epoch_id: {:#?}", ticketbook.epoch_id); - - let tickets_left = ticketbook.total_tickets - ticketbook.used_tickets; - let ticketbook_type = TicketType::from_str(&ticketbook.ticketbook_type).unwrap(); - dbg!(&ticketbook_type); - let ticketbook_type = ticketbook_type.to_repr(); - let bandwidth_left = u64::from(tickets_left) * ticketbook_type.bandwidth_value(); - tracing::info!("bandwidth_left: {:#?}", bandwidth_left); - let avail_ticketbook = AvailableTicketbook::try_from(ticketbook).unwrap(); - avail_ticketbook.print(); + tracing::info!("{avail_ticketbook}"); } let pending_ticketbooks = self.storage.get_pending_ticketbooks().await?; @@ -231,8 +211,8 @@ impl TryFrom for AvailableTicketbook { } } -impl AvailableTicketbook { - fn print(&self) { +impl fmt::Display for AvailableTicketbook { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let ecash_today = nym_ecash_time::ecash_today().date(); let issued = self.issued_tickets; @@ -252,7 +232,8 @@ impl AvailableTicketbook { self.expiration.to_string() }; - tracing::info!( + write!( + f, "Ticketbook id: {} - Type: {} - Size: {} - Issued: {} - Claimed: {} - Remaining: {} - Expiration: {}", self.id, self.typ, @@ -261,9 +242,10 @@ impl AvailableTicketbook { si_claimed, si_remaining, expiration - ); + ) } } + // TODO: add #[derive(EnumIter)] to TicketType so we can iterate over it directly. fn ticketbook_types() -> [TicketType; 4] { [