From 4c10cebf1b5149e9c1b1ed7c20225ad62ee40d7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 25 Jul 2024 17:32:03 +0100 Subject: [PATCH] propagated new ticket type through the whole stack --- Cargo.lock | 8 +- clients/native/Cargo.toml | 2 +- clients/socks5/Cargo.toml | 2 +- .../bandwidth-controller/src/acquire/mod.rs | 3 + common/client-core/Cargo.toml | 2 + .../cli_helpers/client_show_ticketbooks.rs | 29 ++-- .../src/client/base_client/storage/mod.rs | 2 +- common/client-core/src/error.rs | 3 + common/client-core/src/lib.rs | 2 + .../gateway-client/src/client/config.rs | 4 +- .../validator-client/src/nym_api/mod.rs | 4 +- .../commands/src/coconut/issue_ticket_book.rs | 13 +- .../20241104120001_add_ecash_tables.sql | 3 + .../credential-storage/src/backends/memory.rs | 1 + .../credential-storage/src/backends/sqlite.rs | 9 +- common/credential-storage/src/models.rs | 3 + .../src/persistent_storage/mod.rs | 1 + common/credential-utils/Cargo.toml | 2 +- common/credential-utils/src/utils.rs | 14 +- common/credentials-interface/Cargo.toml | 4 +- common/credentials-interface/src/lib.rs | 84 ++++++++++- .../src/ecash/bandwidth/issuance.rs | 21 ++- .../credentials/src/ecash/bandwidth/issued.rs | 16 +- common/credentials/src/ecash/utils.rs | 18 +-- common/network-defaults/src/ecash.rs | 22 +-- .../nym_offline_compact_ecash/src/helpers.rs | 4 +- common/nym_offline_compact_ecash/src/lib.rs | 1 + common/socks5-client-core/Cargo.toml | 2 +- gateway/src/node/client_handling/bandwidth.rs | 4 +- .../20240708120000_ecash_tables.sql | 20 +-- nym-api/nym-api-requests/src/ecash/helpers.rs | 10 +- nym-api/nym-api-requests/src/ecash/models.rs | 33 ++-- nym-api/src/ecash/api_routes/helpers.rs | 8 +- nym-api/src/ecash/deposit.rs | 2 + nym-api/src/ecash/error.rs | 4 + nym-api/src/ecash/helpers.rs | 12 +- nym-api/src/ecash/state/mod.rs | 6 +- nym-api/src/ecash/storage/manager.rs | 104 ++++++++----- nym-api/src/ecash/storage/mod.rs | 40 ++--- nym-api/src/ecash/storage/models.rs | 75 +++++----- nym-api/src/ecash/tests/issued_credentials.rs | 8 +- nym-api/src/ecash/tests/mod.rs | 12 +- nym-validator-rewarder/Cargo.toml | 5 + nym-validator-rewarder/src/error.rs | 3 +- .../rewarder/credential_issuance/monitor.rs | 141 ++++++++++++------ .../src/rewarder/storage/mod.rs | 8 +- nym-wallet/Cargo.lock | 2 + sdk/rust/nym-sdk/Cargo.toml | 3 +- sdk/rust/nym-sdk/examples/bandwidth.rs | 4 +- sdk/rust/nym-sdk/src/bandwidth.rs | 2 +- sdk/rust/nym-sdk/src/bandwidth/client.rs | 12 +- sdk/rust/nym-sdk/src/mixnet/client.rs | 5 +- 52 files changed, 547 insertions(+), 255 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d7982ca09..a2e038a0da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4562,6 +4562,7 @@ dependencies = [ "nym-config", "nym-country-group", "nym-credential-storage", + "nym-credentials-interface", "nym-crypto", "nym-ecash-time", "nym-explorer-client", @@ -4808,10 +4809,10 @@ dependencies = [ "log", "nym-bandwidth-controller", "nym-client-core", - "nym-compact-ecash", "nym-config", "nym-credential-storage", "nym-credentials", + "nym-credentials-interface", "nym-ecash-time", "nym-validator-client", "thiserror", @@ -4848,8 +4849,10 @@ dependencies = [ "bls12_381", "nym-compact-ecash", "nym-ecash-time", + "nym-network-defaults", "rand 0.8.5", "serde", + "strum 0.25.0", "thiserror", "time", ] @@ -5660,6 +5663,7 @@ dependencies = [ "nym-credential-storage", "nym-credential-utils", "nym-credentials", + "nym-credentials-interface", "nym-crypto", "nym-gateway-requests", "nym-network-defaults", @@ -6129,12 +6133,14 @@ dependencies = [ "nym-compact-ecash", "nym-config", "nym-credentials", + "nym-credentials-interface", "nym-crypto", "nym-ecash-time", "nym-network-defaults", "nym-task", "nym-validator-client", "nyxd-scraper", + "rand_chacha 0.3.1", "serde", "serde_with", "sha2 0.10.8", diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index 1cb349c872..9680601e6e 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -38,7 +38,7 @@ zeroize = { workspace = true } ## internal nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } nym-bin-common = { path = "../../common/bin-common", features = ["output_format", "clap"] } -nym-client-core = { path = "../../common/client-core", features = ["fs-surb-storage", "fs-gateways-storage", "cli"] } +nym-client-core = { path = "../../common/client-core", features = ["fs-credentials-storage", "fs-surb-storage", "fs-gateways-storage", "cli"] } nym-config = { path = "../../common/config" } nym-credential-storage = { path = "../../common/credential-storage" } nym-credentials = { path = "../../common/credentials" } diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 7788430c9c..dd09cd82bb 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -23,7 +23,7 @@ zeroize = { workspace = true } # internal nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] } -nym-client-core = { path = "../../common/client-core", features = ["fs-surb-storage", "fs-gateways-storage", "cli"] } +nym-client-core = { path = "../../common/client-core", features = ["fs-credentials-storage", "fs-surb-storage", "fs-gateways-storage", "cli"] } nym-config = { path = "../../common/config" } nym-credential-storage = { path = "../../common/credential-storage" } nym-credentials = { path = "../../common/credentials" } diff --git a/common/bandwidth-controller/src/acquire/mod.rs b/common/bandwidth-controller/src/acquire/mod.rs index e9f3b528d4..51afe221fe 100644 --- a/common/bandwidth-controller/src/acquire/mod.rs +++ b/common/bandwidth-controller/src/acquire/mod.rs @@ -8,6 +8,7 @@ use nym_credential_storage::storage::Storage; use nym_credentials::ecash::bandwidth::IssuanceTicketBook; use nym_credentials::ecash::utils::obtain_aggregate_wallet; use nym_credentials::IssuedTicketBook; +use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::identity; use nym_ecash_time::{ecash_default_expiration_date, Date}; use nym_validator_client::coconut::all_ecash_api_clients; @@ -22,6 +23,7 @@ pub async fn make_deposit( client: &C, client_id: &[u8], expiration: Option, + ticketbook_type: TicketType, ) -> Result where C: EcashSigningClient + EcashQueryClient + Sync, @@ -48,6 +50,7 @@ where deposit_id, client_id, signing_key, + ticketbook_type, expiration, )) } diff --git a/common/client-core/Cargo.toml b/common/client-core/Cargo.toml index 92250213c8..1378d2f77f 100644 --- a/common/client-core/Cargo.toml +++ b/common/client-core/Cargo.toml @@ -46,6 +46,7 @@ nym-pemstore = { path = "../pemstore" } nym-topology = { path = "../topology", features = ["serializable"] } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } nym-task = { path = "../task" } +nym-credentials-interface = { path = "../credentials-interface" } nym-credential-storage = { path = "../credential-storage" } nym-network-defaults = { path = "../network-defaults" } nym-client-core-config-types = { path = "./config-types", features = ["disk-persistence"] } @@ -115,6 +116,7 @@ tempfile = { workspace = true } [features] default = [] cli = ["clap", "comfy-table"] +fs-credentials-storage = ["nym-credential-storage/persistent-storage"] fs-surb-storage = ["nym-client-core-surb-storage/fs-surb-storage"] fs-gateways-storage = ["nym-client-core-gateways-storage/fs-gateways-storage"] wasm = ["nym-gateway-client/wasm"] diff --git a/common/client-core/src/cli_helpers/client_show_ticketbooks.rs b/common/client-core/src/cli_helpers/client_show_ticketbooks.rs index b988f1d79e..3a91a6665b 100644 --- a/common/client-core/src/cli_helpers/client_show_ticketbooks.rs +++ b/common/client-core/src/cli_helpers/client_show_ticketbooks.rs @@ -5,14 +5,15 @@ use crate::cli_helpers::{CliClient, CliClientConfig}; use crate::error::ClientCoreError; use nym_credential_storage::models::BasicTicketbookInformation; use nym_credential_storage::storage::Storage; +use nym_credentials_interface::TicketType; use nym_ecash_time::ecash_today; -use nym_network_defaults::TicketbookType::MixnetEntry; use serde::{Deserialize, Serialize}; use time::Date; #[derive(Serialize, Deserialize)] pub struct AvailableTicketbook { pub id: i64, + pub typ: TicketType, pub expiration: Date, pub issued_tickets: u32, pub claimed_tickets: u32, @@ -45,6 +46,7 @@ impl AvailableTicketbook { vec![ comfy_table::Cell::new(self.id.to_string()), + comfy_table::Cell::new(self.typ), expiration, comfy_table::Cell::new(format!("{issued} ({si_issued})")), comfy_table::Cell::new(format!("{claimed} ({si_claimed})")), @@ -55,17 +57,22 @@ impl AvailableTicketbook { } } -impl From for AvailableTicketbook { - fn from(value: BasicTicketbookInformation) -> Self { - AvailableTicketbook { +impl TryFrom for AvailableTicketbook { + type Error = ClientCoreError; + + fn try_from(value: BasicTicketbookInformation) -> Result { + let typ = value + .ticketbook_type + .parse() + .map_err(|_| ClientCoreError::UnknownTicketType)?; + Ok(AvailableTicketbook { id: value.id, + typ, expiration: value.expiration_date, issued_tickets: value.total_tickets, claimed_tickets: value.used_tickets, - - // TODO: this will change when 'type' field is introduced; for now doesn't matter what we put there - ticket_size: MixnetEntry.bandwidth_value(), - } + ticket_size: typ.to_repr().bandwidth_value(), + }) } } @@ -79,6 +86,7 @@ impl std::fmt::Display for AvailableTicketbooks { let mut table = comfy_table::Table::new(); table.set_header(vec![ "id", + "type", "expiration", "issued tickets (bandwidth)", "claimed tickets (bandwidth)", @@ -124,6 +132,9 @@ where })?; Ok(AvailableTicketbooks( - ticketbooks.into_iter().map(Into::into).collect(), + ticketbooks + .into_iter() + .map(TryInto::::try_into) + .collect::>()?, )) } diff --git a/common/client-core/src/client/base_client/storage/mod.rs b/common/client-core/src/client/base_client/storage/mod.rs index e7c15315a6..8e325ddcd5 100644 --- a/common/client-core/src/client/base_client/storage/mod.rs +++ b/common/client-core/src/client/base_client/storage/mod.rs @@ -23,7 +23,7 @@ use crate::{ config::{self, disk_persistence::CommonClientPaths}, error::ClientCoreError, }; -#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] +#[cfg(all(not(target_arch = "wasm32"), feature = "fs-credentials-storage"))] use nym_credential_storage::persistent_storage::PersistentStorage as PersistentCredentialStorage; pub use nym_client_core_gateways_storage as gateways_storage; diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index bcaca3763c..cb9b9126a3 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -68,6 +68,9 @@ pub enum ClientCoreError { source: Box, }, + #[error("the provided ticket type is invalid")] + UnknownTicketType, + #[error("the gateway id is invalid - {0}")] UnableToCreatePublicKeyFromGatewayId(Ed25519RecoveryError), diff --git a/common/client-core/src/lib.rs b/common/client-core/src/lib.rs index 1eb90ddcf7..ffa1402859 100644 --- a/common/client-core/src/lib.rs +++ b/common/client-core/src/lib.rs @@ -2,7 +2,9 @@ use std::future::Future; #[cfg(all( not(target_arch = "wasm32"), + feature = "cli", feature = "fs-surb-storage", + feature = "fs-credentials-storage", feature = "fs-gateways-storage" ))] pub mod cli_helpers; diff --git a/common/client-libs/gateway-client/src/client/config.rs b/common/client-libs/gateway-client/src/client/config.rs index 2461779d74..fd7bfc142d 100644 --- a/common/client-libs/gateway-client/src/client/config.rs +++ b/common/client-libs/gateway-client/src/client/config.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::GatewayClientError; -use nym_network_defaults::TicketbookType::MixnetEntry; +use nym_network_defaults::TicketTypeRepr::V1MixnetEntry; use si_scale::helpers::bibytes2; use std::time::Duration; @@ -103,7 +103,7 @@ impl BandwidthTickets { // 20% of entry ticket value pub const DEFAULT_REMAINING_BANDWIDTH_THRESHOLD: i64 = - (MixnetEntry.bandwidth_value() / 5) as i64; + (V1MixnetEntry.bandwidth_value() / 5) as i64; pub const DEFAULT_CUTOFF_REMAINING_BANDWIDTH_THRESHOLD: Option = None; diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index b9b3d91cd2..fa0bb2b7a9 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -23,8 +23,8 @@ use nym_api_requests::ecash::VerificationKeyResponse; pub use nym_api_requests::{ ecash::{ models::{ - EpochCredentialsResponse, IssuedCredential, IssuedCredentialBody, - IssuedCredentialResponse, IssuedCredentialsResponse, SpentCredentialsResponse, + EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, + IssuedTicketbook, IssuedTicketbookBody, SpentCredentialsResponse, }, BlindSignRequestBody, BlindedSignatureResponse, CredentialsRequestBody, PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, diff --git a/common/commands/src/coconut/issue_ticket_book.rs b/common/commands/src/coconut/issue_ticket_book.rs index a1d2f0f8ef..c6b0997ba5 100644 --- a/common/commands/src/coconut/issue_ticket_book.rs +++ b/common/commands/src/coconut/issue_ticket_book.rs @@ -7,11 +7,16 @@ use anyhow::bail; use clap::Parser; use nym_credential_storage::initialise_persistent_storage; use nym_credential_utils::utils; +use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::identity; use std::path::PathBuf; #[derive(Debug, Parser)] pub struct Args { + /// Specify which type of ticketbook should be issued + #[clap(long, default_value_t = TicketType::default())] + pub(crate) ticketbook_type: TicketType, + /// Config file of the client that is supposed to use the credential. #[clap(long)] pub(crate) client_config: PathBuf, @@ -39,7 +44,13 @@ pub async fn execute(args: Args, client: SigningClient) -> anyhow::Result<()> { let persistent_storage = initialise_persistent_storage(credentials_store).await; let private_id_key: identity::PrivateKey = nym_pemstore::load_key(private_id_key)?; - utils::issue_credential(&client, &persistent_storage, &private_id_key.to_bytes()).await?; + utils::issue_credential( + &client, + &persistent_storage, + &private_id_key.to_bytes(), + args.ticketbook_type, + ) + .await?; Ok(()) } diff --git a/common/credential-storage/migrations/20241104120001_add_ecash_tables.sql b/common/credential-storage/migrations/20241104120001_add_ecash_tables.sql index 10b585d096..d2f879bfee 100644 --- a/common/credential-storage/migrations/20241104120001_add_ecash_tables.sql +++ b/common/credential-storage/migrations/20241104120001_add_ecash_tables.sql @@ -35,6 +35,9 @@ CREATE TABLE ecash_ticketbook -- introduce a way for us to introduce breaking changes in serialization of data serialization_revision INTEGER NOT NULL, + -- the type of the associated ticketbook + ticketbook_type TEXT NOT NULL, + -- the actual crypto data of the ticketbook (wallet, keys, etc.) ticketbook_data BLOB NOT NULL UNIQUE, diff --git a/common/credential-storage/src/backends/memory.rs b/common/credential-storage/src/backends/memory.rs index 998c25cdeb..71aea890b0 100644 --- a/common/credential-storage/src/backends/memory.rs +++ b/common/credential-storage/src/backends/memory.rs @@ -175,6 +175,7 @@ impl MemoryEcachTicketbookManager { .map(|t| BasicTicketbookInformation { id: t.ticketbook_id, expiration_date: t.ticketbook.expiration_date(), + ticketbook_type: t.ticketbook.ticketbook_type().to_string(), epoch_id: t.ticketbook.epoch_id() as u32, total_tickets: t.ticketbook.spent_tickets() as u32, used_tickets: t.ticketbook.params_total_tickets() as u32, diff --git a/common/credential-storage/src/backends/sqlite.rs b/common/credential-storage/src/backends/sqlite.rs index a3b8a00349..43f019afa3 100644 --- a/common/credential-storage/src/backends/sqlite.rs +++ b/common/credential-storage/src/backends/sqlite.rs @@ -61,11 +61,13 @@ impl SqliteEcashTicketbookManager { Ok(()) } + #[allow(clippy::too_many_arguments)] pub(crate) async fn insert_new_ticketbook( &self, serialisation_revision: u8, data: &[u8], expiration_date: Date, + typ: &str, epoch_id: u32, total_tickets: u32, used_tickets: u32, @@ -73,12 +75,13 @@ impl SqliteEcashTicketbookManager { sqlx::query!( r#" INSERT INTO ecash_ticketbook - (serialization_revision, ticketbook_data, expiration_date, epoch_id, total_tickets, used_tickets) - VALUES (?, ?, ?, ?, ?, ?) + (serialization_revision, ticketbook_data, expiration_date, ticketbook_type, epoch_id, total_tickets, used_tickets) + VALUES (?, ?, ?, ?, ?, ?, ?) "#, serialisation_revision, data, expiration_date, + typ, epoch_id, total_tickets, used_tickets, @@ -92,7 +95,7 @@ impl SqliteEcashTicketbookManager { ) -> Result, sqlx::Error> { sqlx::query_as( r#" - SELECT id, expiration_date, epoch_id, total_tickets, used_tickets + SELECT id, expiration_date, ticketbook_type, epoch_id, total_tickets, used_tickets FROM ecash_ticketbook "#, ) diff --git a/common/credential-storage/src/models.rs b/common/credential-storage/src/models.rs index c400631645..2a3faa2d70 100644 --- a/common/credential-storage/src/models.rs +++ b/common/credential-storage/src/models.rs @@ -19,6 +19,7 @@ pub struct RetrievedPendingTicketbook { pub struct BasicTicketbookInformation { pub id: i64, pub expiration_date: Date, + pub ticketbook_type: String, pub epoch_id: u32, pub total_tickets: u32, pub used_tickets: u32, @@ -31,6 +32,8 @@ pub struct StoredIssuedTicketbook { pub serialization_revision: u8, + pub ticketbook_type: String, + pub ticketbook_data: Vec, #[zeroize(skip)] diff --git a/common/credential-storage/src/persistent_storage/mod.rs b/common/credential-storage/src/persistent_storage/mod.rs index 4ad915db15..3940d82c3c 100644 --- a/common/credential-storage/src/persistent_storage/mod.rs +++ b/common/credential-storage/src/persistent_storage/mod.rs @@ -114,6 +114,7 @@ impl Storage for PersistentStorage { serialisation_revision, &data, ticketbook.expiration_date(), + &ticketbook.ticketbook_type().to_string(), ticketbook.epoch_id() as u32, ticketbook.params_total_tickets() as u32, ticketbook.spent_tickets() as u32, diff --git a/common/credential-utils/Cargo.toml b/common/credential-utils/Cargo.toml index 3e2d534fa2..81ba19a85b 100644 --- a/common/credential-utils/Cargo.toml +++ b/common/credential-utils/Cargo.toml @@ -14,9 +14,9 @@ time.workspace = true nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } nym-credentials = { path = "../../common/credentials" } +nym-credentials-interface = { path = "../../common/credentials-interface" } nym-credential-storage = { path = "../../common/credential-storage", features = ["persistent-storage"] } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-config = { path = "../../common/config" } nym-client-core = { path = "../../common/client-core" } -nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-ecash-time = { path = "../../common/ecash-time" } \ No newline at end of file diff --git a/common/credential-utils/src/utils.rs b/common/credential-utils/src/utils.rs index 9e8ad0e847..41381d53f9 100644 --- a/common/credential-utils/src/utils.rs +++ b/common/credential-utils/src/utils.rs @@ -1,3 +1,6 @@ +// Copyright 2023-2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + use crate::errors::{Error, Result}; use log::*; use nym_bandwidth_controller::acquire::{ @@ -7,6 +10,7 @@ use nym_client_core::config::disk_persistence::CommonClientPaths; use nym_config::DEFAULT_DATA_DIR; use nym_credential_storage::persistent_storage::PersistentStorage; use nym_credential_storage::storage::Storage; +use nym_credentials_interface::TicketType; use nym_ecash_time::ecash_default_expiration_date; use nym_validator_client::coconut::all_ecash_api_clients; use nym_validator_client::nyxd::contract_traits::{ @@ -16,7 +20,12 @@ use std::path::PathBuf; use std::time::Duration; use time::OffsetDateTime; -pub async fn issue_credential(client: &C, storage: &S, client_id: &[u8]) -> Result<()> +pub async fn issue_credential( + client: &C, + storage: &S, + client_id: &[u8], + typ: TicketType, +) -> Result<()> where C: DkgQueryClient + EcashSigningClient + EcashQueryClient + Send + Sync, S: Storage, @@ -49,6 +58,7 @@ where client, client_id, Some(ticketbook_expiration), + typ, ) .await?; info!("Deposit done"); @@ -65,7 +75,7 @@ where }).map_err(Error::storage_error)? } - info!("Succeeded adding a ticketbook"); + info!("Succeeded adding a ticketbook of type '{typ}'"); Ok(()) } diff --git a/common/credentials-interface/Cargo.toml b/common/credentials-interface/Cargo.toml index 7d2365683f..2142acf14a 100644 --- a/common/credentials-interface/Cargo.toml +++ b/common/credentials-interface/Cargo.toml @@ -14,8 +14,10 @@ license.workspace = true bls12_381 = { workspace = true, default-features = false } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } +strum = { workspace = true, features = ["derive"] } time = { workspace = true, features = ["serde"] } rand = { workspace = true } nym-compact-ecash = { path = "../nym_offline_compact_ecash" } -nym-ecash-time = { path = "../ecash-time" } \ No newline at end of file +nym-ecash-time = { path = "../ecash-time" } +nym-network-defaults = { path = "../network-defaults" } diff --git a/common/credentials-interface/src/lib.rs b/common/credentials-interface/src/lib.rs index 765c479644..0d4f995e3c 100644 --- a/common/credentials-interface/src/lib.rs +++ b/common/credentials-interface/src/lib.rs @@ -1,8 +1,10 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use nym_network_defaults::TicketTypeRepr; use rand::Rng; use serde::{Deserialize, Serialize}; +use thiserror::Error; use time::{Date, OffsetDateTime}; pub use nym_compact_ecash::{ @@ -15,7 +17,6 @@ pub use nym_compact_ecash::{ PartialCoinIndexSignature, }, scheme::expiration_date_signatures::aggregate_expiration_signatures, - scheme::expiration_date_signatures::date_scalar, scheme::expiration_date_signatures::{ AnnotatedExpirationDateSignature, ExpirationDateSignature, ExpirationDateSignatureShare, PartialExpirationDateSignature, @@ -24,8 +25,8 @@ pub use nym_compact_ecash::{ scheme::withdrawal::RequestInfo, scheme::Payment, scheme::{Wallet, WalletSignatures}, - withdrawal_request, Base58, BlindedSignature, Bytable, PartialWallet, PayInfo, PublicKeyUser, - SecretKeyUser, VerificationKeyAuth, WithdrawalRequest, + withdrawal_request, Base58, BlindedSignature, Bytable, EncodedDate, EncodedTicketType, + PartialWallet, PayInfo, PublicKeyUser, SecretKeyUser, VerificationKeyAuth, WithdrawalRequest, }; use nym_ecash_time::EcashTime; @@ -38,6 +39,8 @@ pub struct CredentialSigningData { pub ecash_pub_key: PublicKeyUser, pub expiration_date: Date, + + pub ticketbook_type: TicketType, } #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] @@ -58,7 +61,7 @@ impl CredentialSpendingData { self.payment.spend_verify( verification_key, &self.pay_info, - date_scalar(self.spend_date.ecash_unix_timestamp()), + self.spend_date.ecash_unix_timestamp(), ) } @@ -216,3 +219,76 @@ impl From for NymPayInfo { } } } + +#[derive( + Default, + Copy, + Clone, + Debug, + PartialEq, + Serialize, + Deserialize, + strum::Display, + strum::EnumString, +)] +#[serde(rename_all = "kebab-case")] +#[strum(serialize_all = "kebab-case")] +pub enum TicketType { + #[default] + V1MixnetEntry, + V1MixnetExit, + V1WireguardEntry, + V1WireguardExit, +} + +#[derive(Debug, Copy, Clone, Error)] +#[error("provided unknown ticketbook type")] +pub struct UnknownTicketType; + +impl TicketType { + pub fn to_repr(&self) -> TicketTypeRepr { + (*self).into() + } + + pub fn encode(&self) -> EncodedTicketType { + self.to_repr() as EncodedTicketType + } + + pub fn try_from_encoded(val: EncodedTicketType) -> Result { + match val { + n if n == TicketTypeRepr::V1MixnetEntry as u8 => { + Ok(TicketTypeRepr::V1MixnetEntry.into()) + } + n if n == TicketTypeRepr::V1MixnetExit as u8 => Ok(TicketTypeRepr::V1MixnetExit.into()), + n if n == TicketTypeRepr::V1WireguardEntry as u8 => { + Ok(TicketTypeRepr::V1WireguardEntry.into()) + } + n if n == TicketTypeRepr::V1WireguardExit as u8 => { + Ok(TicketTypeRepr::V1WireguardExit.into()) + } + _ => Err(UnknownTicketType), + } + } +} + +impl From for TicketTypeRepr { + fn from(value: TicketType) -> Self { + match value { + TicketType::V1MixnetEntry => TicketTypeRepr::V1MixnetEntry, + TicketType::V1MixnetExit => TicketTypeRepr::V1MixnetExit, + TicketType::V1WireguardEntry => TicketTypeRepr::V1WireguardEntry, + TicketType::V1WireguardExit => TicketTypeRepr::V1WireguardExit, + } + } +} + +impl From for TicketType { + fn from(value: TicketTypeRepr) -> Self { + match value { + TicketTypeRepr::V1MixnetEntry => TicketType::V1MixnetEntry, + TicketTypeRepr::V1MixnetExit => TicketType::V1MixnetExit, + TicketTypeRepr::V1WireguardEntry => TicketType::V1WireguardEntry, + TicketTypeRepr::V1WireguardExit => TicketType::V1WireguardExit, + } + } +} diff --git a/common/credentials/src/ecash/bandwidth/issuance.rs b/common/credentials/src/ecash/bandwidth/issuance.rs index 5cbdea45bf..72b1dd0a25 100644 --- a/common/credentials/src/ecash/bandwidth/issuance.rs +++ b/common/credentials/src/ecash/bandwidth/issuance.rs @@ -2,14 +2,15 @@ // SPDX-License-Identifier: Apache-2.0 use crate::ecash::bandwidth::issued::IssuedTicketBook; +use crate::ecash::bandwidth::serialiser::VersionedSerialise; use crate::ecash::bandwidth::CredentialSigningData; use crate::ecash::utils::cred_exp_date; use crate::error::Error; use nym_api_requests::ecash::BlindSignRequestBody; use nym_credentials_interface::{ aggregate_wallets, generate_keypair_user_from_seed, issue_verify, withdrawal_request, - BlindedSignature, KeyPairUser, PartialWallet, VerificationKeyAuth, WalletSignatures, - WithdrawalRequest, + BlindedSignature, KeyPairUser, PartialWallet, TicketType, VerificationKeyAuth, + WalletSignatures, WithdrawalRequest, }; use nym_crypto::asymmetric::identity; use nym_ecash_contract_common::deposit::DepositId; @@ -18,7 +19,6 @@ use nym_validator_client::nym_api::EpochId; use serde::{Deserialize, Serialize}; use time::Date; -use crate::ecash::bandwidth::serialiser::VersionedSerialise; pub use nym_validator_client::nyxd::{Coin, Hash}; #[derive(Serialize, Deserialize)] @@ -32,6 +32,9 @@ pub struct IssuanceTicketBook { /// ecash keypair related to the credential ecash_keypair: KeyPairUser, + /// the type of the ticketbook to be issued + ticketbook_type: TicketType, + /// expiration_date of that credential expiration_date: Date, } @@ -41,12 +44,14 @@ impl IssuanceTicketBook { deposit_id: DepositId, identifier: M, signing_key: identity::PrivateKey, + ticketbook_type: TicketType, ) -> Self { //this expiration date will get fed to the ecash library, force midnight to be set Self::new_with_expiration( deposit_id, identifier, signing_key, + ticketbook_type, ecash_default_expiration_date(), ) } @@ -55,6 +60,7 @@ impl IssuanceTicketBook { deposit_id: DepositId, identifier: M, signing_key: identity::PrivateKey, + ticketbook_type: TicketType, expiration_date: Date, ) -> Self { let ecash_keypair = generate_keypair_user_from_seed(identifier); @@ -62,6 +68,7 @@ impl IssuanceTicketBook { deposit_id, signing_key, ecash_keypair, + ticketbook_type, expiration_date, } } @@ -76,6 +83,10 @@ impl IssuanceTicketBook { self.expiration_date } + pub fn ticketbook_type(&self) -> TicketType { + self.ticketbook_type + } + pub fn request_plaintext(request: &WithdrawalRequest, deposit_id: DepositId) -> Vec { let mut message = request.to_bytes(); message.extend_from_slice(&deposit_id.to_be_bytes()); @@ -99,6 +110,7 @@ impl IssuanceTicketBook { request_signature, signing_request.ecash_pub_key.clone(), signing_request.expiration_date, + signing_request.ticketbook_type, ) } @@ -133,6 +145,7 @@ impl IssuanceTicketBook { let (withdrawal_request, request_info) = withdrawal_request( self.ecash_keypair.secret_key(), self.expiration_date.ecash_unix_timestamp(), + self.ticketbook_type.encode(), ) .unwrap(); @@ -141,6 +154,7 @@ impl IssuanceTicketBook { request_info, ecash_pub_key: self.ecash_keypair.public_key(), expiration_date: self.expiration_date, + ticketbook_type: self.ticketbook_type, } } @@ -218,6 +232,7 @@ impl IssuanceTicketBook { wallet, epoch_id, self.ecash_keypair.secret_key().clone(), + self.ticketbook_type, self.expiration_date, ) } diff --git a/common/credentials/src/ecash/bandwidth/issued.rs b/common/credentials/src/ecash/bandwidth/issued.rs index 5d20af3ee8..8787528ae4 100644 --- a/common/credentials/src/ecash/bandwidth/issued.rs +++ b/common/credentials/src/ecash/bandwidth/issued.rs @@ -6,8 +6,8 @@ use crate::ecash::bandwidth::CredentialSpendingData; use crate::ecash::utils::ecash_today; use crate::error::Error; use nym_credentials_interface::{ - CoinIndexSignature, ExpirationDateSignature, PayInfo, SecretKeyUser, VerificationKeyAuth, - Wallet, WalletSignatures, + CoinIndexSignature, ExpirationDateSignature, PayInfo, SecretKeyUser, TicketType, + VerificationKeyAuth, Wallet, WalletSignatures, }; use nym_ecash_time::EcashTime; use nym_validator_client::nym_api::EpochId; @@ -36,6 +36,10 @@ pub struct IssuedTicketBook { /// expiration_date for easier discarding #[zeroize(skip)] expiration_date: Date, + + /// the type of the ticketbook to got issued + #[zeroize(skip)] + ticketbook_type: TicketType, } impl IssuedTicketBook { @@ -43,6 +47,7 @@ impl IssuedTicketBook { wallet: WalletSignatures, epoch_id: EpochId, ecash_secret_key: SecretKeyUser, + ticketbook_type: TicketType, expiration_date: Date, ) -> Self { IssuedTicketBook { @@ -51,6 +56,7 @@ impl IssuedTicketBook { epoch_id, ecash_secret_key, expiration_date, + ticketbook_type, } } @@ -58,6 +64,7 @@ impl IssuedTicketBook { signatures_wallet: WalletSignatures, epoch_id: EpochId, ecash_secret_key: SecretKeyUser, + ticketbook_type: TicketType, expiration_date: Date, spent_tickets: u64, ) -> Self { @@ -67,6 +74,7 @@ impl IssuedTicketBook { epoch_id, ecash_secret_key, expiration_date, + ticketbook_type, } } @@ -78,6 +86,10 @@ impl IssuedTicketBook { self.epoch_id } + pub fn ticketbook_type(&self) -> TicketType { + self.ticketbook_type + } + pub fn current_serialization_revision(&self) -> u8 { CURRENT_SERIALIZATION_REVISION } diff --git a/common/credentials/src/ecash/utils.rs b/common/credentials/src/ecash/utils.rs index 38ee31f2c1..970288b075 100644 --- a/common/credentials/src/ecash/utils.rs +++ b/common/credentials/src/ecash/utils.rs @@ -36,22 +36,6 @@ pub fn aggregate_verification_keys( )?) } -pub fn obtain_aggregated_verification_key( - _api_clients: &[EcashApiClient], -) -> Result { - // TODO: - // let total = api_clients.len(); - // let mut rng = thread_rng(); - // let indices = sample(&mut rng, total, total); - // for index in indices { - // // randomly try apis until we succeed - // // if let Ok(res) = api_clients[index].api_client.get_aggregated_verification_key().await { - // // // - // // } - // } - todo!() -} - pub async fn obtain_expiration_date_signatures( ecash_api_clients: &[EcashApiClient], verification_key: &VerificationKeyAuth, @@ -63,7 +47,7 @@ pub async fn obtain_expiration_date_signatures( let mut signatures_shares: Vec<_> = Vec::with_capacity(ecash_api_clients.len()); - let expiration_date = cred_exp_date().unix_timestamp() as u64; + let expiration_date = cred_exp_date().ecash_unix_timestamp(); for ecash_api_client in ecash_api_clients.iter() { match ecash_api_client .api_client diff --git a/common/network-defaults/src/ecash.rs b/common/network-defaults/src/ecash.rs index f6ceb630aa..21d1ddfe85 100644 --- a/common/network-defaults/src/ecash.rs +++ b/common/network-defaults/src/ecash.rs @@ -7,17 +7,19 @@ pub const TICKETBOOK_VALIDITY_DAYS: u32 = 7; /// Specifies the number of tickets in each issued ticketbook. pub const TICKETBOOK_SIZE: u64 = 50; +/// This type is defined mostly for the purposes of having constants (like sizes) associated with given variants +/// It's not meant to be serialised or have any fancy traits defined on it (in this crate) #[derive(Default, Copy, Clone, Debug, PartialEq)] #[repr(u8)] -pub enum TicketbookType { +pub enum TicketTypeRepr { #[default] - MixnetEntry = 0, - MixnetExit = 1, - WireguardEntry = 2, - WireguardExit = 3, + V1MixnetEntry = 0, + V1MixnetExit = 1, + V1WireguardEntry = 2, + V1WireguardExit = 3, } -impl TicketbookType { +impl TicketTypeRepr { pub const WIREGUARD_ENTRY_TICKET_SIZE: u64 = 500 * 1024 * 1024; // 500 MB // TBD: @@ -28,10 +30,10 @@ impl TicketbookType { /// How much bandwidth (in bytes) one ticket can grant pub const fn bandwidth_value(&self) -> u64 { match self { - TicketbookType::MixnetEntry => Self::MIXNET_ENTRY_TICKET_SIZE, - TicketbookType::MixnetExit => Self::MIXNET_EXIT_TICKET_SIZE, - TicketbookType::WireguardEntry => Self::WIREGUARD_ENTRY_TICKET_SIZE, - TicketbookType::WireguardExit => Self::WIREGUARD_EXIT_TICKET_SIZE, + TicketTypeRepr::V1MixnetEntry => Self::MIXNET_ENTRY_TICKET_SIZE, + TicketTypeRepr::V1MixnetExit => Self::MIXNET_EXIT_TICKET_SIZE, + TicketTypeRepr::V1WireguardEntry => Self::WIREGUARD_ENTRY_TICKET_SIZE, + TicketTypeRepr::V1WireguardExit => Self::WIREGUARD_EXIT_TICKET_SIZE, } } } diff --git a/common/nym_offline_compact_ecash/src/helpers.rs b/common/nym_offline_compact_ecash/src/helpers.rs index 7c18214c7d..af741d5f34 100644 --- a/common/nym_offline_compact_ecash/src/helpers.rs +++ b/common/nym_offline_compact_ecash/src/helpers.rs @@ -36,7 +36,7 @@ pub(crate) fn recover_g1_tuple( Ok((first, second)) } -pub(crate) fn date_scalar(date: EncodedDate) -> Scalar { +pub fn date_scalar(date: EncodedDate) -> Scalar { Scalar::from(date as u64) } @@ -48,7 +48,7 @@ pub(crate) fn scalar_date(scalar: &Scalar) -> EncodedDate { u64::from_le_bytes([b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7]]) as EncodedDate } -pub(crate) fn type_scalar(t_type: EncodedTicketType) -> Scalar { +pub fn type_scalar(t_type: EncodedTicketType) -> Scalar { Scalar::from(t_type as u64) } diff --git a/common/nym_offline_compact_ecash/src/lib.rs b/common/nym_offline_compact_ecash/src/lib.rs index f522145047..c6346a159f 100644 --- a/common/nym_offline_compact_ecash/src/lib.rs +++ b/common/nym_offline_compact_ecash/src/lib.rs @@ -13,6 +13,7 @@ pub use crate::error::CompactEcashError; pub use crate::traits::Bytable; pub use bls12_381::G1Projective; pub use common_types::{BlindedSignature, Signature}; +pub use helpers::{date_scalar, type_scalar}; pub use scheme::aggregation::aggregate_verification_keys; pub use scheme::aggregation::aggregate_wallets; pub use scheme::identify; diff --git a/common/socks5-client-core/Cargo.toml b/common/socks5-client-core/Cargo.toml index 290016a332..ce8421f3d7 100644 --- a/common/socks5-client-core/Cargo.toml +++ b/common/socks5-client-core/Cargo.toml @@ -22,7 +22,7 @@ tokio = { workspace = true, features = ["rt-multi-thread", "net", "signal"] } url = { workspace = true } nym-bandwidth-controller = { path = "../../common/bandwidth-controller" } -nym-client-core = { path = "../client-core", features = ["fs-surb-storage", "fs-gateways-storage"] } +nym-client-core = { path = "../client-core", features = ["fs-credentials-storage", "fs-surb-storage", "fs-gateways-storage"] } nym-config = { path = "../config" } nym-contracts-common = { path = "../cosmwasm-smart-contracts/contracts-common" } nym-credential-storage = { path = "../credential-storage" } diff --git a/gateway/src/node/client_handling/bandwidth.rs b/gateway/src/node/client_handling/bandwidth.rs index 6ceb76ad5a..ebfb91b0c1 100644 --- a/gateway/src/node/client_handling/bandwidth.rs +++ b/gateway/src/node/client_handling/bandwidth.rs @@ -1,7 +1,7 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_network_defaults::TicketbookType; +use nym_network_defaults::TicketTypeRepr; use std::num::ParseIntError; use thiserror::Error; use time::error::ComponentRange; @@ -42,7 +42,7 @@ impl Bandwidth { Bandwidth { value } } - pub fn ticket_amount(typ: TicketbookType) -> Self { + pub fn ticket_amount(typ: TicketTypeRepr) -> Self { Bandwidth { value: typ.bandwidth_value(), } diff --git a/nym-api/migrations/20240708120000_ecash_tables.sql b/nym-api/migrations/20240708120000_ecash_tables.sql index 68037fe83d..ca2daf6fd9 100644 --- a/nym-api/migrations/20240708120000_ecash_tables.sql +++ b/nym-api/migrations/20240708120000_ecash_tables.sql @@ -64,17 +64,17 @@ CREATE TABLE partial_expiration_date_signatures ( DROP TABLE issued_credential; --- particular **partial** ecash credential issued in this epoch -CREATE TABLE issued_credential +-- particular **partial** ecash ticketbook issued in this epoch +CREATE TABLE issued_ticketbook ( - id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, - epoch_id INTEGER NOT NULL, - deposit_id INTEGER NOT NULL UNIQUE, --- at some point those should be blobified - bs58_partial_credential VARCHAR NOT NULL, - bs58_signature VARCHAR NOT NULL, - joined_private_commitments VARCHAR NOT NULL, - expiration_date DATE NOT NULL + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + epoch_id INTEGER NOT NULL, + deposit_id INTEGER NOT NULL UNIQUE, + partial_credential BLOB NOT NULL, + signature BLOB NOT NULL, + joined_private_commitments BLOB NOT NULL, + expiration_date DATE NOT NULL, + ticketbook_type_repr INTEGER NOT NULL ); CREATE TABLE ticket_providers diff --git a/nym-api/nym-api-requests/src/ecash/helpers.rs b/nym-api/nym-api-requests/src/ecash/helpers.rs index 154b733f9f..936fced5dd 100644 --- a/nym-api/nym-api-requests/src/ecash/helpers.rs +++ b/nym-api/nym-api-requests/src/ecash/helpers.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use nym_compact_ecash::BlindedSignature; +use nym_credentials_interface::TicketType; use nym_ecash_time::EcashTime; +use std::iter::once; use time::Date; // recomputes plaintext on the credential nym-api has used for signing @@ -12,8 +14,9 @@ pub fn issued_credential_plaintext( epoch_id: u32, deposit_id: u32, blinded_partial_credential: &BlindedSignature, - bs58_encoded_private_attributes_commitments: &[String], + encoded_private_attributes_commitments: &[Vec], expiration_date: Date, + ticketbook_type: TicketType, ) -> Vec { epoch_id .to_be_bytes() @@ -21,10 +24,11 @@ pub fn issued_credential_plaintext( .chain(deposit_id.to_be_bytes()) .chain(blinded_partial_credential.to_bytes()) .chain( - bs58_encoded_private_attributes_commitments + encoded_private_attributes_commitments .iter() - .flat_map(|attr| attr.as_bytes().iter().copied()), + .flat_map(|attr| attr.iter().copied()), ) .chain(expiration_date.ecash_unix_timestamp().to_be_bytes()) + .chain(once(ticketbook_type.to_repr() as u8)) .collect() } diff --git a/nym-api/nym-api-requests/src/ecash/models.rs b/nym-api/nym-api-requests/src/ecash/models.rs index b22fcd7e1d..395073afaa 100644 --- a/nym-api/nym-api-requests/src/ecash/models.rs +++ b/nym-api/nym-api-requests/src/ecash/models.rs @@ -6,6 +6,8 @@ use crate::helpers::PlaceholderJsonSchemaImpl; use cosmrs::AccountId; use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; +use nym_compact_ecash::Bytable; +use nym_credentials_interface::TicketType; use nym_credentials_interface::{ BlindedSignature, CompactEcashError, CredentialSpendingData, PublicKeyUser, VerificationKeyAuth, WithdrawalRequest, @@ -115,6 +117,9 @@ pub struct BlindSignRequestBody { #[schemars(with = "String")] #[serde(with = "crate::helpers::date_serde")] pub expiration_date: Date, + + #[schemars(with = "String")] + pub ticketbook_type: TicketType, } impl BlindSignRequestBody { @@ -124,6 +129,7 @@ impl BlindSignRequestBody { signature: identity::Signature, ecash_pubkey: PublicKeyUser, expiration_date: Date, + ticketbook_type: TicketType, ) -> BlindSignRequestBody { BlindSignRequestBody { inner_sign_request, @@ -131,16 +137,15 @@ impl BlindSignRequestBody { signature, ecash_pubkey, expiration_date, + ticketbook_type, } } - pub fn encode_commitments(&self) -> Vec { - use nym_compact_ecash::Base58; - + pub fn encode_commitments(&self) -> Vec> { self.inner_sign_request .get_private_attributes_commitments() .iter() - .map(|c| c.to_bs58()) + .map(|c| c.to_byte_vec()) .collect() } } @@ -371,26 +376,26 @@ pub struct EpochCredentialsResponse { #[serde(rename_all = "camelCase")] pub struct IssuedCredentialsResponse { // note: BTreeMap returns ordered results so it's fine to use it with pagination - pub credentials: BTreeMap, + pub credentials: BTreeMap, } #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] #[serde(rename_all = "camelCase")] pub struct IssuedCredentialResponse { - pub credential: Option, + pub credential: Option, } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "camelCase")] -pub struct IssuedCredentialBody { - pub credential: IssuedCredential, +pub struct IssuedTicketbookBody { + pub credential: IssuedTicketbook, #[schemars(with = "PlaceholderJsonSchemaImpl")] pub signature: identity::Signature, } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, JsonSchema)] #[serde(rename_all = "camelCase")] -pub struct IssuedCredential { +pub struct IssuedTicketbook { pub id: i64, pub epoch_id: u32, pub deposit_id: u32, @@ -400,22 +405,26 @@ pub struct IssuedCredential { // so that nym-api wouldn't need to parse the value out of its storage #[schemars(with = "PlaceholderJsonSchemaImpl")] pub blinded_partial_credential: BlindedSignature, - pub bs58_encoded_private_attributes_commitments: Vec, + pub encoded_private_attributes_commitments: Vec>, #[schemars(with = "String")] #[serde(with = "crate::helpers::date_serde")] pub expiration_date: Date, + + #[schemars(with = "String")] + pub ticketbook_type: TicketType, } -impl IssuedCredential { +impl IssuedTicketbook { // this method doesn't have to be reversible so just naively concatenate everything pub fn signable_plaintext(&self) -> Vec { issued_credential_plaintext( self.epoch_id, self.deposit_id, &self.blinded_partial_credential, - &self.bs58_encoded_private_attributes_commitments, + &self.encoded_private_attributes_commitments, self.expiration_date, + self.ticketbook_type, ) } } diff --git a/nym-api/src/ecash/api_routes/helpers.rs b/nym-api/src/ecash/api_routes/helpers.rs index b05272d472..521f11067a 100644 --- a/nym-api/src/ecash/api_routes/helpers.rs +++ b/nym-api/src/ecash/api_routes/helpers.rs @@ -2,19 +2,19 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::ecash::error::Result; -use crate::ecash::storage::models::IssuedCredential; -use nym_api_requests::ecash::models::IssuedCredentialBody; +use crate::ecash::storage::models::IssuedTicketbook; use nym_api_requests::ecash::models::IssuedCredentialsResponse; +use nym_api_requests::ecash::models::IssuedTicketbookBody; use std::collections::BTreeMap; pub(crate) fn build_credentials_response( - raw: Vec, + raw: Vec, ) -> Result { let mut credentials = BTreeMap::new(); for raw_credential in raw { let id = raw_credential.id; - let api_issued = IssuedCredentialBody::try_from(raw_credential)?; + let api_issued = IssuedTicketbookBody::try_from(raw_credential)?; let old = credentials.insert(id, api_issued); if old.is_some() { // why do we panic here rather than return an error? because it's a critical failure because diff --git a/nym-api/src/ecash/deposit.rs b/nym-api/src/ecash/deposit.rs index cc14884895..9118e9d3af 100644 --- a/nym-api/src/ecash/deposit.rs +++ b/nym-api/src/ecash/deposit.rs @@ -23,6 +23,7 @@ mod test { use crate::ecash::error::EcashError; use crate::ecash::tests::voucher_fixture; use nym_compact_ecash::{generate_keypair_user, scheme::withdrawal::WithdrawalRequest}; + use nym_credentials_interface::TicketType; use rand::rngs::OsRng; use time::macros::date; @@ -95,6 +96,7 @@ mod test { "3MpHDLYMCmuMvZ9zkZXPkTK6nKArvQW3dJA1notoPPxnbBW2ommkR2dkpRWoeWSkUjQSLv1nRyiRzMWbobGLw1eh".parse().unwrap(), ecash_keypair.public_key(), expiration_date, + TicketType::V1MixnetEntry, ); let good_deposit = Deposit { diff --git a/nym-api/src/ecash/error.rs b/nym-api/src/ecash/error.rs index 0464c5baa4..94201847dc 100644 --- a/nym-api/src/ecash/error.rs +++ b/nym-api/src/ecash/error.rs @@ -3,6 +3,7 @@ use crate::node_status_api::models::NymApiStorageError; use nym_coconut_dkg_common::types::{ChunkIndex, DealingIndex, EpochId}; +use nym_credentials_interface::UnknownTicketType; use nym_crypto::asymmetric::{ encryption::KeyRecoveryError, identity::{Ed25519RecoveryError, SignatureError}, @@ -213,6 +214,9 @@ pub enum EcashError { #[error("could not obtain enough shares for aggregation. got {shares} shares whilst the threshold is {threshold}")] InsufficientNumberOfShares { threshold: Threshold, shares: usize }, + + #[error(transparent)] + UnknownTicketBookType(#[from] UnknownTicketType), } impl<'r, 'o: 'r> Responder<'r, 'o> for EcashError { diff --git a/nym-api/src/ecash/helpers.rs b/nym-api/src/ecash/helpers.rs index 6a047d63c2..677b9c355b 100644 --- a/nym-api/src/ecash/helpers.rs +++ b/nym-api/src/ecash/helpers.rs @@ -7,7 +7,7 @@ use nym_coconut_dkg_common::types::EpochId; use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; use nym_compact_ecash::scheme::keygen::SecretKeyAuth; -use nym_compact_ecash::BlindedSignature; +use nym_compact_ecash::{BlindedSignature, EncodedDate, EncodedTicketType}; use nym_compact_ecash::{PublicKeyUser, WithdrawalRequest}; use nym_ecash_time::EcashTime; use serde::{Deserialize, Serialize}; @@ -31,7 +31,8 @@ pub(crate) struct IssuedCoinIndicesSignatures { pub(crate) trait CredentialRequest { fn withdrawal_request(&self) -> &WithdrawalRequest; - fn expiration_date_timestamp(&self) -> u64; + fn expiration_date_timestamp(&self) -> EncodedDate; + fn ticketbook_type(&self) -> EncodedTicketType; fn ecash_pubkey(&self) -> PublicKeyUser; } @@ -40,10 +41,14 @@ impl CredentialRequest for BlindSignRequestBody { &self.inner_sign_request } - fn expiration_date_timestamp(&self) -> u64 { + fn expiration_date_timestamp(&self) -> EncodedDate { self.expiration_date.ecash_unix_timestamp() } + fn ticketbook_type(&self) -> EncodedTicketType { + self.ticketbook_type.encode() + } + fn ecash_pubkey(&self) -> PublicKeyUser { self.ecash_pubkey.clone() } @@ -58,6 +63,7 @@ pub(crate) fn blind_sign( request.ecash_pubkey().clone(), request.withdrawal_request(), request.expiration_date_timestamp(), + request.ticketbook_type(), )?) } diff --git a/nym-api/src/ecash/state/mod.rs b/nym-api/src/ecash/state/mod.rs index c31181e17e..9f0beca02e 100644 --- a/nym-api/src/ecash/state/mod.rs +++ b/nym-api/src/ecash/state/mod.rs @@ -599,12 +599,13 @@ impl EcashState { blinded_signature, &encoded_commitments, request_body.expiration_date, + request_body.ticketbook_type, ); let signature = self.local.identity_keypair.private_key().sign(plaintext); - // note: we have a UNIQUE constraint on the tx_hash column of the credential - // and so if the api is processing request for the same hash at the same time, + // note: we have a UNIQUE constraint on the deposit_id column of the credential + // and so if the api is processing request for the same deposit at the same time, // only one of them will be successfully inserted to the database let credential_id = self .aux @@ -616,6 +617,7 @@ impl EcashState { signature, encoded_commitments, request_body.expiration_date, + request_body.ticketbook_type, ) .await?; diff --git a/nym-api/src/ecash/storage/manager.rs b/nym-api/src/ecash/storage/manager.rs index 4689ffa10a..8da7591c22 100644 --- a/nym-api/src/ecash/storage/manager.rs +++ b/nym-api/src/ecash/storage/manager.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::ecash::storage::models::{ - EpochCredentials, IssuedCredential, RawExpirationDateSignatures, SerialNumberWrapper, + EpochCredentials, IssuedTicketbook, RawExpirationDateSignatures, SerialNumberWrapper, StoredBloomfilterParams, TicketProvider, VerifiedTicket, }; use crate::support::storage::manager::StorageManager; @@ -49,7 +49,7 @@ pub trait EcashStorageManagerExt { async fn get_issued_credential( &self, credential_id: i64, - ) -> Result, sqlx::Error>; + ) -> Result, sqlx::Error>; /// Attempts to retrieve an issued credential from the data store. /// @@ -59,17 +59,18 @@ pub trait EcashStorageManagerExt { async fn get_issued_bandwidth_credential_by_deposit_id( &self, deposit_id: DepositId, - ) -> Result, sqlx::Error>; + ) -> Result, sqlx::Error>; /// Store the provided issued credential information and return its (database) id. - async fn store_issued_credential( + async fn store_issued_ticketbook( &self, epoch_id: u32, deposit_id: DepositId, - bs58_partial_credential: String, - bs58_signature: String, - joined_private_commitments: String, + partial_credential: &[u8], + signature: &[u8], + joined_private_commitments: &[u8], expiration_date: Date, + ticketbook_type_repr: u8, ) -> Result; /// Attempts to retrieve issued credentials from the data store using provided ids. @@ -77,10 +78,10 @@ pub trait EcashStorageManagerExt { /// # Arguments /// /// * `credential_ids`: (database) ids of the issued credentials - async fn get_issued_credentials( + async fn get_issued_ticketbooks( &self, credential_ids: Vec, - ) -> Result, sqlx::Error>; + ) -> Result, sqlx::Error>; /// Attempts to retrieve issued credentials from the data store using pagination specification. /// @@ -88,11 +89,11 @@ pub trait EcashStorageManagerExt { /// /// * `start_after`: the value preceding the first retrieved result /// * `limit`: the maximum number of entries to retrieve - async fn get_issued_credentials_paged( + async fn get_issued_ticketbooks_paged( &self, start_after: i64, limit: u32, - ) -> Result, sqlx::Error>; + ) -> Result, sqlx::Error>; async fn insert_ticket_provider(&self, gateway_address: &str) -> Result; @@ -360,12 +361,20 @@ impl EcashStorageManagerExt for StorageManager { async fn get_issued_credential( &self, credential_id: i64, - ) -> Result, sqlx::Error> { + ) -> Result, sqlx::Error> { sqlx::query_as!( - IssuedCredential, + IssuedTicketbook, r#" - SELECT id, epoch_id as "epoch_id: u32", deposit_id as "deposit_id: DepositId", bs58_partial_credential, bs58_signature,joined_private_commitments, expiration_date as "expiration_date: Date" - FROM issued_credential + SELECT + id, + epoch_id as "epoch_id: u32", + deposit_id as "deposit_id: DepositId", + partial_credential, + signature, + joined_private_commitments, + expiration_date as "expiration_date: Date", + ticketbook_type_repr as "ticketbook_type_repr: u8" + FROM issued_ticketbook WHERE id = ? "#, credential_id @@ -382,38 +391,47 @@ impl EcashStorageManagerExt for StorageManager { async fn get_issued_bandwidth_credential_by_deposit_id( &self, deposit_id: DepositId, - ) -> Result, sqlx::Error> { + ) -> Result, sqlx::Error> { sqlx::query_as!( - IssuedCredential, + IssuedTicketbook, r#" - SELECT id, epoch_id as "epoch_id: u32", deposit_id as "deposit_id: u32", bs58_partial_credential, bs58_signature,joined_private_commitments, expiration_date as "expiration_date: Date" - FROM issued_credential + SELECT + id, + epoch_id as "epoch_id: u32", + deposit_id as "deposit_id: DepositId", + partial_credential, + signature, + joined_private_commitments, + expiration_date as "expiration_date: Date", + ticketbook_type_repr as "ticketbook_type_repr: u8" + FROM issued_ticketbook WHERE deposit_id = ? "#, deposit_id ) - .fetch_optional(&self.connection_pool) - .await + .fetch_optional(&self.connection_pool) + .await } /// Store the provided issued credential information and return its (database) id. - async fn store_issued_credential( + async fn store_issued_ticketbook( &self, epoch_id: u32, deposit_id: DepositId, - bs58_partial_credential: String, - bs58_signature: String, - joined_private_commitments: String, + partial_credential: &[u8], + signature: &[u8], + joined_private_commitments: &[u8], expiration_date: Date, + ticketbook_type_repr: u8, ) -> Result { let row_id = sqlx::query!( r#" - INSERT INTO issued_credential - (epoch_id, deposit_id, bs58_partial_credential, bs58_signature, joined_private_commitments, expiration_date) + INSERT INTO issued_ticketbook + (epoch_id, deposit_id, partial_credential, signature, joined_private_commitments, expiration_date, ticketbook_type_repr) VALUES - (?, ?, ?, ?, ?, ?) + (?, ?, ?, ?, ?, ?, ?) "#, - epoch_id, deposit_id, bs58_partial_credential, bs58_signature, joined_private_commitments, expiration_date + epoch_id, deposit_id, partial_credential, signature, joined_private_commitments, expiration_date, ticketbook_type_repr ).execute(&self.connection_pool).await?.last_insert_rowid(); Ok(row_id) @@ -424,14 +442,14 @@ impl EcashStorageManagerExt for StorageManager { /// # Arguments /// /// * `credential_ids`: (database) ids of the issued credentials - async fn get_issued_credentials( + async fn get_issued_ticketbooks( &self, credential_ids: Vec, - ) -> Result, sqlx::Error> { + ) -> Result, sqlx::Error> { // that sucks : ( // https://stackoverflow.com/a/70032524 let params = format!("?{}", ", ?".repeat(credential_ids.len() - 1)); - let query_str = format!("SELECT * FROM issued_credential WHERE id IN ( {params} )"); + let query_str = format!("SELECT * FROM issued_ticketbook WHERE id IN ( {params} )"); let mut query = sqlx::query_as(&query_str); for id in credential_ids { query = query.bind(id) @@ -446,16 +464,24 @@ impl EcashStorageManagerExt for StorageManager { /// /// * `start_after`: the value preceding the first retrieved result /// * `limit`: the maximum number of entries to retrieve - async fn get_issued_credentials_paged( + async fn get_issued_ticketbooks_paged( &self, start_after: i64, limit: u32, - ) -> Result, sqlx::Error> { + ) -> Result, sqlx::Error> { sqlx::query_as!( - IssuedCredential, + IssuedTicketbook, r#" - SELECT id, epoch_id as "epoch_id: u32", deposit_id as "deposit_id: u32", bs58_partial_credential, bs58_signature,joined_private_commitments, expiration_date as "expiration_date: Date" - FROM issued_credential + SELECT + id, + epoch_id as "epoch_id: u32", + deposit_id as "deposit_id: DepositId", + partial_credential, + signature, + joined_private_commitments, + expiration_date as "expiration_date: Date", + ticketbook_type_repr as "ticketbook_type_repr: u8" + FROM issued_ticketbook WHERE id > ? ORDER BY id LIMIT ? @@ -463,8 +489,8 @@ impl EcashStorageManagerExt for StorageManager { start_after, limit ) - .fetch_all(&self.connection_pool) - .await + .fetch_all(&self.connection_pool) + .await } async fn insert_ticket_provider(&self, gateway_address: &str) -> Result { diff --git a/nym-api/src/ecash/storage/mod.rs b/nym-api/src/ecash/storage/mod.rs index cc7621b1ae..752081e4af 100644 --- a/nym-api/src/ecash/storage/mod.rs +++ b/nym-api/src/ecash/storage/mod.rs @@ -8,7 +8,7 @@ use crate::ecash::storage::helpers::{ }; use crate::ecash::storage::manager::EcashStorageManagerExt; use crate::ecash::storage::models::{ - join_attributes, EpochCredentials, IssuedCredential, SerialNumberWrapper, TicketProvider, + join_attributes, EpochCredentials, IssuedTicketbook, SerialNumberWrapper, TicketProvider, }; use crate::node_status_api::models::NymApiStorageError; use crate::support::storage::NymApiStorage; @@ -16,9 +16,10 @@ use nym_api_requests::ecash::models::Pagination; use nym_coconut_dkg_common::types::EpochId; use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; use nym_compact_ecash::BlindedSignature; -use nym_compact_ecash::{Base58, VerificationKeyAuth}; +use nym_compact_ecash::VerificationKeyAuth; use nym_config::defaults::BloomfilterParameters; use nym_credentials::CredentialSpendingData; +use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::identity; use nym_ecash_contract_common::deposit::DepositId; use nym_validator_client::nyxd::AccountId; @@ -81,12 +82,12 @@ pub trait EcashStorageExt { async fn get_issued_credential( &self, credential_id: i64, - ) -> Result, NymApiStorageError>; + ) -> Result, NymApiStorageError>; async fn get_issued_bandwidth_credential_by_deposit_id( &self, deposit_id: DepositId, - ) -> Result, NymApiStorageError>; + ) -> Result, NymApiStorageError>; async fn store_issued_credential( &self, @@ -94,19 +95,20 @@ pub trait EcashStorageExt { deposit_id: DepositId, partial_credential: &BlindedSignature, signature: identity::Signature, - private_commitments: Vec, + private_commitments: Vec>, expiration_date: Date, + ticketbook_type: TicketType, ) -> Result; async fn get_issued_credentials( &self, credential_ids: Vec, - ) -> Result, NymApiStorageError>; + ) -> Result, NymApiStorageError>; async fn get_issued_credentials_paged( &self, pagination: Pagination, - ) -> Result, NymApiStorageError>; + ) -> Result, NymApiStorageError>; // // async fn insert_credential( // &self, @@ -308,14 +310,14 @@ impl EcashStorageExt for NymApiStorage { async fn get_issued_credential( &self, credential_id: i64, - ) -> Result, NymApiStorageError> { + ) -> Result, NymApiStorageError> { Ok(self.manager.get_issued_credential(credential_id).await?) } async fn get_issued_bandwidth_credential_by_deposit_id( &self, deposit_id: DepositId, - ) -> Result, NymApiStorageError> { + ) -> Result, NymApiStorageError> { Ok(self .manager .get_issued_bandwidth_credential_by_deposit_id(deposit_id) @@ -328,18 +330,20 @@ impl EcashStorageExt for NymApiStorage { deposit_id: DepositId, partial_credential: &BlindedSignature, signature: identity::Signature, - private_commitments: Vec, + private_commitments: Vec>, expiration_date: Date, + ticketbook_type: TicketType, ) -> Result { Ok(self .manager - .store_issued_credential( + .store_issued_ticketbook( epoch_id, deposit_id, - partial_credential.to_bs58(), - signature.to_base58_string(), - join_attributes(private_commitments), + &partial_credential.to_bytes(), + &signature.to_bytes(), + &join_attributes(private_commitments), expiration_date, + ticketbook_type.encode(), ) .await?) } @@ -347,14 +351,14 @@ impl EcashStorageExt for NymApiStorage { async fn get_issued_credentials( &self, credential_ids: Vec, - ) -> Result, NymApiStorageError> { - Ok(self.manager.get_issued_credentials(credential_ids).await?) + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_issued_ticketbooks(credential_ids).await?) } async fn get_issued_credentials_paged( &self, pagination: Pagination, - ) -> Result, NymApiStorageError> { + ) -> Result, NymApiStorageError> { // rows start at 1 let start_after = pagination.last_key.unwrap_or(0); let limit = match pagination.limit { @@ -370,7 +374,7 @@ impl EcashStorageExt for NymApiStorage { Ok(self .manager - .get_issued_credentials_paged(start_after, limit) + .get_issued_ticketbooks_paged(start_after, limit) .await?) } diff --git a/nym-api/src/ecash/storage/models.rs b/nym-api/src/ecash/storage/models.rs index c73d759b4e..6f150f238f 100644 --- a/nym-api/src/ecash/storage/models.rs +++ b/nym-api/src/ecash/storage/models.rs @@ -4,15 +4,16 @@ use crate::ecash::error::EcashError; use crate::node_status_api::models::NymApiStorageError; use nym_api_requests::ecash::models::{ - EpochCredentialsResponse, IssuedCredential as ApiIssuedCredential, - IssuedCredentialBody as ApiIssuedCredentialInner, + EpochCredentialsResponse, IssuedTicketbook as ApiIssuedCredential, + IssuedTicketbookBody as ApiIssuedCredentialInner, }; use nym_api_requests::ecash::BlindedSignatureResponse; -use nym_compact_ecash::{Base58, BlindedSignature}; +use nym_compact_ecash::BlindedSignature; use nym_config::defaults::BloomfilterParameters; +use nym_credentials_interface::TicketType; +use nym_crypto::asymmetric::ed25519; use nym_ecash_contract_common::deposit::DepositId; use sqlx::FromRow; -use std::fmt::Display; use std::ops::Deref; use time::{Date, OffsetDateTime}; @@ -71,21 +72,22 @@ pub struct VerifiedTicket { } #[derive(FromRow)] -pub struct IssuedCredential { +pub struct IssuedTicketbook { pub id: i64, pub epoch_id: u32, pub deposit_id: DepositId, - /// base58-encoded issued credential - pub bs58_partial_credential: String, + pub partial_credential: Vec, - /// base58-encoded signature on the issued credential (and the attributes) - pub bs58_signature: String, + /// signature on the issued credential (and the attributes) + pub signature: Vec, // i.e. "'attr1','attr2',..." - pub joined_private_commitments: String, + pub joined_private_commitments: Vec, pub expiration_date: Date, + + pub ticketbook_type_repr: u8, } #[derive(FromRow)] @@ -141,58 +143,61 @@ impl<'a> TryFrom<&'a StoredBloomfilterParams> for BloomfilterParameters { } } -impl TryFrom for ApiIssuedCredentialInner { +impl TryFrom for ApiIssuedCredentialInner { type Error = EcashError; - fn try_from(value: IssuedCredential) -> Result { + fn try_from(value: IssuedTicketbook) -> Result { Ok(ApiIssuedCredentialInner { credential: ApiIssuedCredential { id: value.id, epoch_id: value.epoch_id, deposit_id: value.deposit_id, - blinded_partial_credential: BlindedSignature::try_from_bs58( - value.bs58_partial_credential, + blinded_partial_credential: BlindedSignature::from_bytes( + &value.partial_credential, )?, - bs58_encoded_private_attributes_commitments: split_attributes( - &value.joined_private_commitments, + encoded_private_attributes_commitments: split_attributes( + value.joined_private_commitments, ), expiration_date: value.expiration_date, + ticketbook_type: TicketType::try_from_encoded(value.ticketbook_type_repr)?, }, - signature: value.bs58_signature.parse()?, + signature: ed25519::Signature::from_bytes(&value.signature)?, }) } } -impl TryFrom for BlindedSignatureResponse { +impl TryFrom for BlindedSignatureResponse { type Error = EcashError; - fn try_from(value: IssuedCredential) -> Result { + fn try_from(value: IssuedTicketbook) -> Result { Ok(BlindedSignatureResponse { - blinded_signature: BlindedSignature::try_from_bs58(value.bs58_partial_credential)?, + blinded_signature: BlindedSignature::from_bytes(&value.partial_credential)?, }) } } -impl TryFrom for BlindedSignature { +impl TryFrom for BlindedSignature { type Error = EcashError; - fn try_from(value: IssuedCredential) -> Result { - Ok(BlindedSignature::try_from_bs58( - value.bs58_partial_credential, - )?) + fn try_from(value: IssuedTicketbook) -> Result { + Ok(BlindedSignature::from_bytes(&value.partial_credential)?) } } -pub fn join_attributes(attrs: I) -> String -where - I: IntoIterator, - M: Display, -{ - // I could have used `attrs.into_iter().join(",")`, - // but my IDE didn't like it (compiler was fine) - itertools::Itertools::join(&mut attrs.into_iter(), ",") +pub(crate) fn join_attributes(attrs: Vec>) -> Vec { + // note: 48 is length of encoded G1 element + let mut out = Vec::with_capacity(48 * attrs.len()); + for mut attr in attrs { + // since this is called internally only, we expect valid attributes here! + assert_eq!(attr.len(), 48); + + out.append(&mut attr) + } + + out } -pub fn split_attributes(attrs: &str) -> Vec { - attrs.split(',').map(|s| s.to_string()).collect() +pub(crate) fn split_attributes(attrs: Vec) -> Vec> { + assert_eq!(attrs.len() % 48, 0, "database corruption"); + attrs.chunks_exact(48).map(|c| c.to_vec()).collect() } diff --git a/nym-api/src/ecash/tests/issued_credentials.rs b/nym-api/src/ecash/tests/issued_credentials.rs index 2dfbf1c473..5a86d70923 100644 --- a/nym-api/src/ecash/tests/issued_credentials.rs +++ b/nym-api/src/ecash/tests/issued_credentials.rs @@ -148,9 +148,7 @@ async fn issued_credential() { assert_eq!( request1.encode_commitments(), - issued1 - .credential - .bs58_encoded_private_attributes_commitments + issued1.credential.encoded_private_attributes_commitments ); assert_eq!( voucher1.expiration_date(), @@ -167,9 +165,7 @@ async fn issued_credential() { assert_eq!( request2.encode_commitments(), - issued2 - .credential - .bs58_encoded_private_attributes_commitments + issued2.credential.encoded_private_attributes_commitments ); assert_eq!( voucher2.expiration_date(), diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index 6184ac7899..fb037a85f0 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -14,7 +14,7 @@ use cosmwasm_std::{ }; use cw3::{Proposal, ProposalResponse, Vote, VoteInfo, VoteResponse, Votes}; use cw4::{Cw4Contract, MemberResponse}; -use nym_api_requests::ecash::models::{IssuedCredentialBody, IssuedCredentialResponse}; +use nym_api_requests::ecash::models::{IssuedCredentialResponse, IssuedTicketbookBody}; use nym_api_requests::ecash::{BlindSignRequestBody, BlindedSignatureResponse}; use nym_coconut_dkg_common::dealer::{ DealerDetails, DealerDetailsResponse, DealerType, RegisteredDealerDetails, @@ -33,6 +33,7 @@ use nym_compact_ecash::BlindedSignature; use nym_compact_ecash::{ttp_keygen, VerificationKeyAuth}; use nym_contracts_common::IdentityKey; use nym_credentials::IssuanceTicketBook; +use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::identity; use nym_dkg::{NodeIndex, Threshold}; use nym_ecash_contract_common::blacklist::{BlacklistedAccountResponse, Blacklisting}; @@ -1223,7 +1224,7 @@ pub fn voucher_fixture(deposit_id: Option) -> IssuanceTicketBook { identity::PrivateKey::from_bytes(&identity_keypair.private_key().to_bytes()).unwrap(); let identifier = [44u8; 32]; // (voucher, request) - IssuanceTicketBook::new(deposit_id, identifier, id_priv) + IssuanceTicketBook::new(deposit_id, identifier, id_priv, TicketType::V1MixnetEntry) } fn dummy_signature() -> identity::Signature { @@ -1372,7 +1373,7 @@ impl TestFixture { serde_json::from_str(&response.into_string().await.unwrap()).unwrap() } - async fn issued_unchecked(&self, id: i64) -> IssuedCredentialBody { + async fn issued_unchecked(&self, id: i64) -> IssuedTicketbookBody { self.issued_credential(id) .await .unwrap() @@ -1409,6 +1410,7 @@ mod credential_tests { dummy_signature(), commitments, expiration_date, + voucher.ticketbook_type(), ) .await .unwrap(); @@ -1494,6 +1496,7 @@ mod credential_tests { dummy_signature(), commitments.clone(), expiration_date, + voucher.ticketbook_type(), ) .await .unwrap(); @@ -1527,6 +1530,7 @@ mod credential_tests { dummy_signature(), commitments.clone(), expiration_date, + voucher.ticketbook_type(), ) .await; assert!(storage_err.is_err()); @@ -1542,6 +1546,7 @@ mod credential_tests { dummy_signature(), commitments.clone(), expiration_date, + voucher.ticketbook_type(), ) .await .unwrap(); @@ -1572,6 +1577,7 @@ mod credential_tests { identity_keypair.private_key().to_base58_string(), ) .unwrap(), + TicketType::V1MixnetEntry, ); let deposit = Deposit { diff --git a/nym-validator-rewarder/Cargo.toml b/nym-validator-rewarder/Cargo.toml index b8198fb7df..58a7472c43 100644 --- a/nym-validator-rewarder/Cargo.toml +++ b/nym-validator-rewarder/Cargo.toml @@ -47,3 +47,8 @@ nyxd-scraper = { path = "../common/nyxd-scraper" } [build-dependencies] sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } + +[dev-dependencies] +rand_chacha = { workspace = true } +nym-credentials-interface = { path = "../common/credentials-interface" } +nym-crypto = { path = "../common/crypto", features = ["rand"] } diff --git a/nym-validator-rewarder/src/error.rs b/nym-validator-rewarder/src/error.rs index 25807722d9..4c72c6ef37 100644 --- a/nym-validator-rewarder/src/error.rs +++ b/nym-validator-rewarder/src/error.rs @@ -115,9 +115,8 @@ pub enum NymRewarderError { received: usize, }, - #[error("the following private attribute commitment is malformed: {raw}: {source}")] + #[error("one of the provided private attribute commitment is malformed: {source}")] MalformedCredentialCommitment { - raw: String, #[source] source: CompactEcashError, }, diff --git a/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs b/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs index d497bd818a..b4a2befa02 100644 --- a/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs +++ b/nym-validator-rewarder/src/rewarder/credential_issuance/monitor.rs @@ -12,13 +12,12 @@ use crate::rewarder::storage::RewarderStorage; use bip39::rand::prelude::SliceRandom; use bip39::rand::thread_rng; use nym_coconut_dkg_common::types::EpochId; -use nym_compact_ecash::scheme::expiration_date_signatures::date_scalar; use nym_compact_ecash::scheme::withdrawal::verify_partial_blind_signature; -use nym_compact_ecash::{Base58, G1Projective, VerificationKeyAuth}; +use nym_compact_ecash::{date_scalar, type_scalar, Bytable, G1Projective, VerificationKeyAuth}; use nym_credentials::ecash::utils::EcashTime; use nym_crypto::asymmetric::ed25519; use nym_task::TaskClient; -use nym_validator_client::nym_api::{IssuedCredential, IssuedCredentialBody, NymApiClientExt}; +use nym_validator_client::nym_api::{IssuedTicketbook, IssuedTicketbookBody, NymApiClientExt}; use std::cmp::max; use tokio::time::interval; use tracing::{debug, error, info, instrument, trace}; @@ -47,7 +46,7 @@ impl CredentialIssuanceMonitor { fn validate_credential_signature( &mut self, - issued_credential: &IssuedCredentialBody, + issued_credential: &IssuedTicketbookBody, identity_key: &ed25519::PublicKey, ) -> Result<(), NymRewarderError> { let plaintext = issued_credential.credential.signable_plaintext(); @@ -66,7 +65,7 @@ impl CredentialIssuanceMonitor { async fn check_deposit_reuse( &mut self, issuer_identity: &str, - credential_info: &IssuedCredentialBody, + credential_info: &IssuedTicketbookBody, ) -> Result { let credential_id = credential_info.credential.id; let deposit_id = credential_info.credential.deposit_id; @@ -102,7 +101,7 @@ impl CredentialIssuanceMonitor { async fn validate_deposit( &mut self, - issued_credential: &IssuedCredentialBody, + issued_credential: &IssuedTicketbookBody, ) -> Result<(), NymRewarderError> { // check if this deposit even exists let deposit_id = issued_credential.credential.deposit_id; @@ -115,51 +114,18 @@ impl CredentialIssuanceMonitor { } fn verify_credential( - &mut self, + &self, vk: &VerificationKeyAuth, - credential: &IssuedCredential, + credential: &IssuedTicketbook, ) -> Result<(), NymRewarderError> { - let public_attributes = [date_scalar( - credential.expiration_date.ecash_unix_timestamp(), - )]; - - #[allow(clippy::map_identity)] - let attributes_refs = public_attributes.iter().collect::>(); - - let mut public_attribute_commitments = - Vec::with_capacity(credential.bs58_encoded_private_attributes_commitments.len()); - - for raw_cm in &credential.bs58_encoded_private_attributes_commitments { - match G1Projective::try_from_bs58(raw_cm) { - Ok(cm) => public_attribute_commitments.push(cm), - Err(source) => { - return Err(NymRewarderError::MalformedCredentialCommitment { - raw: raw_cm.clone(), - source, - }) - } - } - } - - // actually do verify the credential now - if !verify_partial_blind_signature( - &public_attribute_commitments, - &attributes_refs, - &credential.blinded_partial_credential, - vk, - ) { - return Err(NymRewarderError::BlindVerificationFailure); - } - trace!("credential correctly verifies"); - - Ok(()) + verify_credential(vk, credential) } #[instrument(skip_all, fields(credential_id = %issued_credential.credential.id, deposit_id = %issued_credential.credential.deposit_id))] async fn validate_issued_credential( &mut self, issuer: &CredentialIssuer, - issued_credential: &IssuedCredentialBody, + issued_credential: &IssuedTicketbookBody, ) -> Result<(), NymRewarderError> { // check if the issuer has actually signed that issued credential information self.validate_credential_signature(issued_credential, &issuer.public_key)?; @@ -323,3 +289,92 @@ impl CredentialIssuanceMonitor { } } } + +fn verify_credential( + vk: &VerificationKeyAuth, + credential: &IssuedTicketbook, +) -> Result<(), NymRewarderError> { + let public_attributes = [ + date_scalar(credential.expiration_date.ecash_unix_timestamp()), + type_scalar(credential.ticketbook_type.encode()), + ]; + + #[allow(clippy::map_identity)] + let attributes_refs = public_attributes.iter().collect::>(); + + let mut public_attribute_commitments = + Vec::with_capacity(credential.encoded_private_attributes_commitments.len()); + + for raw_cm in &credential.encoded_private_attributes_commitments { + match G1Projective::try_from_byte_slice(raw_cm) { + Ok(cm) => public_attribute_commitments.push(cm), + Err(source) => return Err(NymRewarderError::MalformedCredentialCommitment { source }), + } + } + + // actually do verify the credential now + if !verify_partial_blind_signature( + &public_attribute_commitments, + &attributes_refs, + &credential.blinded_partial_credential, + vk, + ) { + return Err(NymRewarderError::BlindVerificationFailure); + } + trace!("credential correctly verifies"); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_compact_ecash::ttp_keygen; + use nym_credentials::IssuanceTicketBook; + use nym_crypto::asymmetric::{ed25519, identity}; + use rand_chacha::rand_core::{RngCore, SeedableRng}; + + #[test] + fn verify_issued_partial_credential() -> anyhow::Result<()> { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + + let ecash_keypair = ttp_keygen(1, 1)?.pop().unwrap(); + + let deposit_id = rng.next_u32(); + + // create dummy ticketbook + let identity_keypair = ed25519::KeyPair::new(&mut rng); + let id_priv = + identity::PrivateKey::from_bytes(&identity_keypair.private_key().to_bytes()).unwrap(); + let identifier = [44u8; 32]; + + let issuance = IssuanceTicketBook::new(deposit_id, identifier, id_priv, Default::default()); + let signing_data = issuance.prepare_for_signing(); + let request = issuance.create_blind_sign_request_body(&signing_data); + + let partial = nym_compact_ecash::scheme::withdrawal::issue( + ecash_keypair.secret_key(), + request.ecash_pubkey.clone(), + &request.inner_sign_request, + request.expiration_date.ecash_unix_timestamp(), + request.ticketbook_type.encode(), + )?; + + let commitments = request.encode_commitments(); + + let issued = IssuedTicketbook { + id: 0, + epoch_id: 0, + deposit_id, + blinded_partial_credential: partial, + encoded_private_attributes_commitments: commitments, + expiration_date: issuance.expiration_date(), + ticketbook_type: issuance.ticketbook_type(), + }; + + assert!(verify_credential(ecash_keypair.verification_key_ref(), &issued).is_ok()); + + Ok(()) + } +} diff --git a/nym-validator-rewarder/src/rewarder/storage/mod.rs b/nym-validator-rewarder/src/rewarder/storage/mod.rs index 20969b7c93..b002ab46e5 100644 --- a/nym-validator-rewarder/src/rewarder/storage/mod.rs +++ b/nym-validator-rewarder/src/rewarder/storage/mod.rs @@ -6,7 +6,7 @@ use crate::rewarder::credential_issuance::types::CredentialIssuer; use crate::rewarder::epoch::Epoch; use crate::rewarder::storage::manager::StorageManager; use crate::rewarder::{EpochRewards, RewardingResult}; -use nym_validator_client::nym_api::IssuedCredentialBody; +use nym_validator_client::nym_api::IssuedTicketbookBody; use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; use nym_validator_client::nyxd::Coin; use sqlx::ConnectOptions; @@ -95,7 +95,7 @@ impl RewarderStorage { pub(crate) async fn insert_validated_deposit( &self, operator_identity_bs58: String, - credential_info: &IssuedCredentialBody, + credential_info: &IssuedTicketbookBody, ) -> Result<(), NymRewarderError> { self.manager .insert_validated_deposit( @@ -113,7 +113,7 @@ impl RewarderStorage { &self, operator_identity_bs58: String, original_credential_id: i64, - credential_info: &IssuedCredentialBody, + credential_info: &IssuedTicketbookBody, ) -> Result<(), NymRewarderError> { self.manager .insert_double_signing_evidence( @@ -131,7 +131,7 @@ impl RewarderStorage { pub(crate) async fn insert_issuance_foul_play_evidence( &self, issuer: &CredentialIssuer, - credential_info: &IssuedCredentialBody, + credential_info: &IssuedTicketbookBody, error_message: String, ) -> Result<(), NymRewarderError> { self.manager diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 42f823162f..3b8afce000 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -3193,8 +3193,10 @@ dependencies = [ "bls12_381", "nym-compact-ecash", "nym-ecash-time", + "nym-network-defaults", "rand 0.8.5", "serde", + "strum 0.25.0", "thiserror", "time", ] diff --git a/sdk/rust/nym-sdk/Cargo.toml b/sdk/rust/nym-sdk/Cargo.toml index 09dac6dc5e..b9ed55e096 100644 --- a/sdk/rust/nym-sdk/Cargo.toml +++ b/sdk/rust/nym-sdk/Cargo.toml @@ -9,11 +9,12 @@ license.workspace = true [dependencies] async-trait = { workspace = true } bip39 = { workspace = true } -nym-client-core = { path = "../../../common/client-core", features = ["fs-surb-storage", "fs-gateways-storage"] } +nym-client-core = { path = "../../../common/client-core", features = ["fs-credentials-storage", "fs-surb-storage", "fs-gateways-storage"] } nym-crypto = { path = "../../../common/crypto" } nym-gateway-requests = { path = "../../../gateway/gateway-requests" } nym-bandwidth-controller = { path = "../../../common/bandwidth-controller" } nym-credentials = { path = "../../../common/credentials" } +nym-credentials-interface = { path = "../../../common/credentials-interface" } nym-credential-storage = { path = "../../../common/credential-storage" } nym-credential-utils = { path = "../../../common/credential-utils" } nym-network-defaults = { path = "../../../common/network-defaults" } diff --git a/sdk/rust/nym-sdk/examples/bandwidth.rs b/sdk/rust/nym-sdk/examples/bandwidth.rs index 36858ae654..b104ee51ea 100644 --- a/sdk/rust/nym-sdk/examples/bandwidth.rs +++ b/sdk/rust/nym-sdk/examples/bandwidth.rs @@ -18,7 +18,9 @@ async fn main() -> anyhow::Result<()> { .enable_credentials_mode() .build()?; - let bandwidth_client = mixnet_client.create_bandwidth_client(mnemonic).await?; + let bandwidth_client = mixnet_client + .create_bandwidth_client(mnemonic, Default::default()) + .await?; // Get a bandwidth credential for the mixnet_client bandwidth_client.acquire().await?; diff --git a/sdk/rust/nym-sdk/src/bandwidth.rs b/sdk/rust/nym-sdk/src/bandwidth.rs index de4272ef6b..2152319f97 100644 --- a/sdk/rust/nym-sdk/src/bandwidth.rs +++ b/sdk/rust/nym-sdk/src/bandwidth.rs @@ -15,7 +15,7 @@ //! .build() //! .unwrap(); //! -//! let bandwidth_client = mixnet_client.create_bandwidth_client(String::from("my super secret mnemonic")).await.unwrap(); +//! let bandwidth_client = mixnet_client.create_bandwidth_client(String::from("my super secret mnemonic"), Default::default()).await.unwrap(); //! //! // Get a bandwidth credential for the mixnet_client //! bandwidth_client.acquire().await.unwrap(); diff --git a/sdk/rust/nym-sdk/src/bandwidth/client.rs b/sdk/rust/nym-sdk/src/bandwidth/client.rs index 162866452a..19da50eccb 100644 --- a/sdk/rust/nym-sdk/src/bandwidth/client.rs +++ b/sdk/rust/nym-sdk/src/bandwidth/client.rs @@ -4,6 +4,7 @@ use crate::error::Result; use nym_credential_storage::storage::Storage; use nym_credential_utils::utils::issue_credential; +use nym_credentials_interface::TicketType; use nym_network_defaults::NymNetworkDetails; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; use zeroize::Zeroizing; @@ -17,6 +18,7 @@ pub struct BandwidthAcquireClient<'a, St: Storage> { client: DirectSigningHttpRpcNyxdClient, storage: &'a St, client_id: Zeroizing, + ticketbook_type: TicketType, } impl<'a, St> BandwidthAcquireClient<'a, St> @@ -29,6 +31,7 @@ where mnemonic: String, storage: &'a St, client_id: String, + ticketbook_type: TicketType, ) -> Result { let nyxd_url = network_details.endpoints[0].nyxd_url.as_str(); let config = nyxd::Config::try_from_nym_network_details(&network_details)?; @@ -42,11 +45,18 @@ where client, storage, client_id: client_id.into(), + ticketbook_type, }) } pub async fn acquire(&self) -> Result<()> { - issue_credential(&self.client, self.storage, self.client_id.as_bytes()).await?; + issue_credential( + &self.client, + self.storage, + self.client_id.as_bytes(), + self.ticketbook_type, + ) + .await?; Ok(()) } } diff --git a/sdk/rust/nym-sdk/src/mixnet/client.rs b/sdk/rust/nym-sdk/src/mixnet/client.rs index f697553d18..7d26b1f9aa 100644 --- a/sdk/rust/nym-sdk/src/mixnet/client.rs +++ b/sdk/rust/nym-sdk/src/mixnet/client.rs @@ -28,6 +28,7 @@ use nym_client_core::error::ClientCoreError; use nym_client_core::init::helpers::current_gateways; use nym_client_core::init::setup_gateway; use nym_client_core::init::types::{GatewaySelectionSpecification, GatewaySetup}; +use nym_credentials_interface::TicketType; use nym_network_defaults::WG_TUN_DEVICE_IP_ADDRESS; use nym_socks5_client_core::config::Socks5; use nym_task::manager::TaskStatus; @@ -550,10 +551,11 @@ where } /// Creates an associated [`BandwidthAcquireClient`] that can be used to acquire bandwidth - /// credentials for this client to consume. + /// credentials of particular type for this client to consume. pub async fn create_bandwidth_client( &self, mnemonic: String, + ticketbook_type: TicketType, ) -> Result> { if !self.config.enabled_credentials_mode { return Err(Error::DisabledCredentialsMode); @@ -574,6 +576,7 @@ where mnemonic, self.storage.credential_store(), client_id, + ticketbook_type, ) }