diff --git a/Cargo.lock b/Cargo.lock index 7d61a368d4e..65a926ac078 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5563,14 +5563,11 @@ name = "nym-credential-proxy" version = "0.1.7" dependencies = [ "anyhow", - "async-trait", "axum 0.7.9", "bip39", "bs58", "cfg-if", "clap", - "colored", - "dotenvy", "futures", "humantime", "nym-bin-common 0.6.0", @@ -5596,7 +5593,6 @@ dependencies = [ "time", "tokio", "tokio-util", - "tower 0.5.2", "tower-http 0.5.2", "tracing", "url", diff --git a/common/bandwidth-controller/src/utils.rs b/common/bandwidth-controller/src/utils.rs index 2016849eb0b..4502238c910 100644 --- a/common/bandwidth-controller/src/utils.rs +++ b/common/bandwidth-controller/src/utils.rs @@ -207,7 +207,7 @@ where ::StorageError: Send + Sync + 'static, { if let Some(stored) = storage - .get_expiration_date_signatures(expiration_date) + .get_expiration_date_signatures(expiration_date, epoch_id) .await .map_err(BandwidthControllerError::credential_storage_error)? { @@ -220,7 +220,7 @@ where ecash_apis, |api| async move { api.api_client - .global_expiration_date_signatures(Some(expiration_date)) + .global_expiration_date_signatures(Some(expiration_date), Some(epoch_id)) .await }, format!("aggregated coin index signatures for date {expiration_date}"), diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 16d1013c8b0..2e6d19d9eab 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -719,10 +719,11 @@ impl NymApiClient { pub async fn partial_expiration_date_signatures( &self, expiration_date: Option, + epoch_id: Option, ) -> Result { Ok(self .nym_api - .partial_expiration_date_signatures(expiration_date) + .partial_expiration_date_signatures(expiration_date, epoch_id) .await?) } @@ -739,10 +740,11 @@ impl NymApiClient { pub async fn global_expiration_date_signatures( &self, expiration_date: Option, + epoch_id: Option, ) -> Result { Ok(self .nym_api - .global_expiration_date_signatures(expiration_date) + .global_expiration_date_signatures(expiration_date, epoch_id) .await?) } 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 31b7014f641..aa13634503e 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -1103,8 +1103,9 @@ pub trait NymApiClientExt: ApiClient { async fn partial_expiration_date_signatures( &self, expiration_date: Option, + epoch_id: Option, ) -> Result { - let params = match expiration_date { + let mut params = match expiration_date { None => Vec::new(), Some(exp) => vec![( ecash::EXPIRATION_DATE_PARAM, @@ -1112,6 +1113,10 @@ pub trait NymApiClientExt: ApiClient { )], }; + if let Some(epoch_id) = epoch_id { + params.push((ecash::EPOCH_ID_PARAM, epoch_id.to_string())); + } + self.get_json( &[ routes::V1_API_VERSION, @@ -1148,8 +1153,9 @@ pub trait NymApiClientExt: ApiClient { async fn global_expiration_date_signatures( &self, expiration_date: Option, + epoch_id: Option, ) -> Result { - let params = match expiration_date { + let mut params = match expiration_date { None => Vec::new(), Some(exp) => vec![( ecash::EXPIRATION_DATE_PARAM, @@ -1157,6 +1163,10 @@ pub trait NymApiClientExt: ApiClient { )], }; + if let Some(epoch_id) = epoch_id { + params.push((ecash::EPOCH_ID_PARAM, epoch_id.to_string())); + } + self.get_json( &[ routes::V1_API_VERSION, diff --git a/common/commands/src/ecash/generate_ticket.rs b/common/commands/src/ecash/generate_ticket.rs index 0da2a878c4a..4752c9211c9 100644 --- a/common/commands/src/ecash/generate_ticket.rs +++ b/common/commands/src/ecash/generate_ticket.rs @@ -86,7 +86,7 @@ pub async fn execute(args: Args) -> anyhow::Result<()> { anyhow!("ticketbook got incorrectly imported - the master verification key is missing") })?; let expiration_signatures = persistent_storage - .get_expiration_date_signatures(expiration_date) + .get_expiration_date_signatures(expiration_date, epoch_id) .await? .ok_or_else(|| { anyhow!( diff --git a/common/commands/src/ecash/issue_ticket_book.rs b/common/commands/src/ecash/issue_ticket_book.rs index e57cb579dc9..fae59ad7593 100644 --- a/common/commands/src/ecash/issue_ticket_book.rs +++ b/common/commands/src/ecash/issue_ticket_book.rs @@ -120,7 +120,7 @@ async fn issue_to_file(args: Args, client: SigningClient) -> anyhow::Result<()> if args.include_expiration_date_signatures { let signatures = credentials_store - .get_expiration_date_signatures(expiration_date) + .get_expiration_date_signatures(expiration_date, epoch_id) .await? .ok_or(anyhow!("missing expiration date signatures!"))?; diff --git a/common/credential-storage/migrations/20250805120000_expiration_date_signatures_epoch_fix.sql b/common/credential-storage/migrations/20250805120000_expiration_date_signatures_epoch_fix.sql new file mode 100644 index 00000000000..042e65eed22 --- /dev/null +++ b/common/credential-storage/migrations/20250805120000_expiration_date_signatures_epoch_fix.sql @@ -0,0 +1,123 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +-- 1. add temporary `epoch_id` column +ALTER TABLE pending_issuance + ADD COLUMN epoch_id INTEGER; + +-- 2. populate the value +UPDATE pending_issuance +SET epoch_id = (SELECT epoch_id + FROM expiration_date_signatures + WHERE expiration_date_signatures.expiration_date = pending_issuance.expiration_date); + +-- 3. create new expiration_date_signatures table (with changed constraints) +CREATE TABLE expiration_date_signatures_new +( + expiration_date DATE NOT NULL, + + epoch_id INTEGER NOT NULL, + + serialization_revision INTEGER NOT NULL, + + -- combined signatures for all tuples issued for given day + serialised_signatures BLOB NOT NULL, + + PRIMARY KEY (epoch_id, expiration_date) +); + +-- 4. migrate the data +INSERT INTO expiration_date_signatures_new (expiration_date, epoch_id, serialization_revision, serialised_signatures) +SELECT expiration_date, epoch_id, serialization_revision, serialised_signatures +FROM expiration_date_signatures; + +-- 5. drop and recreate the table references (due to new FK) + +-- 5.1. +-- (data for ticketbooks that have an associated deposit, but failed to get issued) +CREATE TABLE pending_issuance_new +( + deposit_id INTEGER NOT NULL PRIMARY KEY, + + -- introduce a way for us to introduce breaking changes in serialization of data + serialization_revision INTEGER NOT NULL, + + pending_ticketbook_data BLOB NOT NULL UNIQUE, + + -- for each ticketbook we MUST have corresponding expiration date signatures + expiration_date DATE NOT NULL, + epoch_id INTEGER NOT NULL, + + -- for each ticketbook we MUST have corresponding expiration date signatures + FOREIGN KEY (epoch_id, expiration_date) REFERENCES expiration_date_signatures_new (epoch_id, expiration_date) +); + +INSERT INTO pending_issuance_new (deposit_id, serialization_revision, pending_ticketbook_data, expiration_date, + epoch_id) +SELECT deposit_id, serialization_revision, pending_ticketbook_data, expiration_date, epoch_id +FROM pending_issuance; + + +-- 5.2. +CREATE TABLE ecash_ticketbook_new +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + + -- 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, + + -- for each ticketbook we MUST have corresponding expiration date signatures + expiration_date DATE NOT NULL, + + -- for each ticketbook we MUST have corresponding coin index signatures + epoch_id INTEGER NOT NULL, + + -- the initial number of tickets the wallet has been created for + total_tickets INTEGER NOT NULL, + + -- how many tickets have been used so far (the `l` value of the wallet) + used_tickets INTEGER NOT NULL, + + + -- FOREIGN KEYS: + + -- for each ticketbook we MUST have corresponding coin index signatures + FOREIGN KEY (epoch_id) REFERENCES coin_indices_signatures (epoch_id), + + -- for each ticketbook we MUST have corresponding expiration date signatures + FOREIGN KEY (expiration_date, epoch_id) REFERENCES expiration_date_signatures_new (expiration_date, epoch_id) +); + +INSERT INTO ecash_ticketbook_new (id, serialization_revision, ticketbook_type, ticketbook_data, expiration_date, + epoch_id, total_tickets, used_tickets) +SELECT id, + serialization_revision, + ticketbook_type, + ticketbook_data, + expiration_date, + epoch_id, + total_tickets, + used_tickets +FROM ecash_ticketbook; + +-- 6. finally swap out the old tables +-- drop old tables +DROP TABLE expiration_date_signatures; +DROP TABLE pending_issuance; +DROP TABLE ecash_ticketbook; + +-- rename new tables +ALTER TABLE expiration_date_signatures_new + RENAME TO expiration_date_signatures; +ALTER TABLE pending_issuance_new + RENAME TO pending_issuance; +ALTER TABLE ecash_ticketbook_new + RENAME TO ecash_ticketbook; \ No newline at end of file diff --git a/common/credential-storage/src/backends/memory.rs b/common/credential-storage/src/backends/memory.rs index f5653767482..b142b70a202 100644 --- a/common/credential-storage/src/backends/memory.rs +++ b/common/credential-storage/src/backends/memory.rs @@ -28,7 +28,7 @@ struct EcashCredentialManagerInner { pending: HashMap, master_vk: HashMap, coin_indices_sigs: HashMap>, - expiration_date_sigs: HashMap>, + expiration_date_sigs: HashMap<(u64, Date), Vec>, _next_id: i64, } @@ -242,10 +242,14 @@ impl MemoryEcachTicketbookManager { pub(crate) async fn get_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: u64, ) -> Option> { let guard = self.inner.read().await; - guard.expiration_date_sigs.get(&expiration_date).cloned() + guard + .expiration_date_sigs + .get(&(epoch_id, expiration_date)) + .cloned() } pub(crate) async fn insert_expiration_date_signatures( @@ -254,8 +258,9 @@ impl MemoryEcachTicketbookManager { ) { let mut guard = self.inner.write().await; - guard - .expiration_date_sigs - .insert(sigs.expiration_date, sigs.signatures.clone()); + guard.expiration_date_sigs.insert( + (sigs.epoch_id, sigs.expiration_date), + sigs.signatures.clone(), + ); } } diff --git a/common/credential-storage/src/backends/sqlite.rs b/common/credential-storage/src/backends/sqlite.rs index 8b0c5027a71..d196cad0490 100644 --- a/common/credential-storage/src/backends/sqlite.rs +++ b/common/credential-storage/src/backends/sqlite.rs @@ -260,15 +260,17 @@ impl SqliteEcashTicketbookManager { pub(crate) async fn get_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( RawExpirationDateSignatures, r#" - SELECT epoch_id as "epoch_id: u32", serialised_signatures, serialization_revision as "serialization_revision: u8" + SELECT serialised_signatures, serialization_revision as "serialization_revision: u8" FROM expiration_date_signatures - WHERE expiration_date = ? + WHERE expiration_date = ? AND epoch_id = ? "#, - expiration_date + expiration_date, + epoch_id ) .fetch_optional(&*self.connection_pool) .await diff --git a/common/credential-storage/src/ephemeral_storage.rs b/common/credential-storage/src/ephemeral_storage.rs index fdcd89ccbd9..2356f2da9db 100644 --- a/common/credential-storage/src/ephemeral_storage.rs +++ b/common/credential-storage/src/ephemeral_storage.rs @@ -166,10 +166,11 @@ impl Storage for EphemeralStorage { async fn get_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: u64, ) -> Result>, Self::StorageError> { Ok(self .storage_manager - .get_expiration_date_signatures(expiration_date) + .get_expiration_date_signatures(expiration_date, epoch_id) .await) } diff --git a/common/credential-storage/src/models.rs b/common/credential-storage/src/models.rs index 2edf9df3cb7..219874d829e 100644 --- a/common/credential-storage/src/models.rs +++ b/common/credential-storage/src/models.rs @@ -60,7 +60,6 @@ pub struct StoredPendingTicketbook { #[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] pub struct RawExpirationDateSignatures { - pub epoch_id: u32, pub serialised_signatures: Vec, pub serialization_revision: u8, } diff --git a/common/credential-storage/src/persistent_storage/mod.rs b/common/credential-storage/src/persistent_storage/mod.rs index 35df10c318b..23a9f00cb9d 100644 --- a/common/credential-storage/src/persistent_storage/mod.rs +++ b/common/credential-storage/src/persistent_storage/mod.rs @@ -325,10 +325,11 @@ impl Storage for PersistentStorage { async fn get_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: u64, ) -> Result>, Self::StorageError> { let Some(raw) = self .storage_manager - .get_expiration_date_signatures(expiration_date) + .get_expiration_date_signatures(expiration_date, epoch_id as i64) .await? else { return Ok(None); diff --git a/common/credential-storage/src/storage.rs b/common/credential-storage/src/storage.rs index b4288f1b88d..cc282828890 100644 --- a/common/credential-storage/src/storage.rs +++ b/common/credential-storage/src/storage.rs @@ -92,6 +92,7 @@ pub trait Storage: Clone + Send + Sync { async fn get_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: u64, ) -> Result>, Self::StorageError>; async fn insert_expiration_date_signatures( diff --git a/common/credentials/src/ecash/utils.rs b/common/credentials/src/ecash/utils.rs index 96d24ad018a..305aafd5324 100644 --- a/common/credentials/src/ecash/utils.rs +++ b/common/credentials/src/ecash/utils.rs @@ -51,7 +51,7 @@ pub async fn obtain_expiration_date_signatures( for ecash_api_client in ecash_api_clients.iter() { match ecash_api_client .api_client - .partial_expiration_date_signatures(None) + .partial_expiration_date_signatures(None, None) .await { Ok(signature) => { diff --git a/common/nym-id/src/error.rs b/common/nym-id/src/error.rs index 9ccf73267b7..b76afa47cf6 100644 --- a/common/nym-id/src/error.rs +++ b/common/nym-id/src/error.rs @@ -25,8 +25,8 @@ pub enum NymIdError { #[error("attempted to import an expired credential (it expired on {expiration})")] ExpiredCredentialImport { expiration: Date }, - #[error("could not import ticketbook expiring at {date} since we do not have corresponding expiration date signatures")] - MissingExpirationDateSignatures { date: Date }, + #[error("could not import ticketbook expiring at {date} for epoch {epoch_id} since we do not have corresponding expiration date signatures")] + MissingExpirationDateSignatures { date: Date, epoch_id: u64 }, #[error("could not import ticketbook for epoch {epoch_id} since we do not have corresponding coin index signatures")] MissingCoinIndexSignatures { epoch_id: u64 }, diff --git a/common/nym-id/src/import_credential/helpers.rs b/common/nym-id/src/import_credential/helpers.rs index dec6228d794..e5caa1e181a 100644 --- a/common/nym-id/src/import_credential/helpers.rs +++ b/common/nym-id/src/import_credential/helpers.rs @@ -99,7 +99,7 @@ where // in order to import the ticketbook we MUST have the appropriate signatures in the storage already if credentials_store - .get_expiration_date_signatures(ticketbook.expiration_date()) + .get_expiration_date_signatures(ticketbook.expiration_date(), ticketbook.epoch_id()) .await .map_err(|source| NymIdError::StorageError { source: Box::new(source), @@ -108,6 +108,7 @@ where { return Err(NymIdError::MissingExpirationDateSignatures { date: ticketbook.expiration_date(), + epoch_id: ticketbook.epoch_id(), }); } diff --git a/nym-api/migrations/20250805120000_expiration_date_signatures_epoch_fix.sql b/nym-api/migrations/20250805120000_expiration_date_signatures_epoch_fix.sql new file mode 100644 index 00000000000..67cc50a9577 --- /dev/null +++ b/nym-api/migrations/20250805120000_expiration_date_signatures_epoch_fix.sql @@ -0,0 +1,51 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +-- Change performed in this migration: +-- remove PK on expiration_date and instead use composite (epoch_id, expiration_date) PK + + +CREATE TABLE global_expiration_date_signatures_new +( + expiration_date DATE NOT NULL, + + epoch_id INTEGER NOT NULL, + + -- combined signatures for all tuples issued for given day + serialised_signatures BLOB NOT NULL, + + PRIMARY KEY (epoch_id, expiration_date) +); + +CREATE TABLE partial_expiration_date_signatures_new +( + expiration_date DATE NOT NULL, + + epoch_id INTEGER NOT NULL, + + serialised_signatures BLOB NOT NULL, + + PRIMARY KEY (epoch_id, expiration_date) +); + +-- global +INSERT INTO global_expiration_date_signatures_new +SELECT * +FROM global_expiration_date_signatures; + +DROP TABLE global_expiration_date_signatures; + +ALTER TABLE global_expiration_date_signatures_new + RENAME TO global_expiration_date_signatures; + +-- partial +INSERT INTO partial_expiration_date_signatures_new +SELECT * +FROM partial_expiration_date_signatures; + +DROP TABLE partial_expiration_date_signatures; + +ALTER TABLE partial_expiration_date_signatures_new + RENAME TO partial_expiration_date_signatures; \ No newline at end of file diff --git a/nym-api/src/ecash/api_routes/aggregation.rs b/nym-api/src/ecash/api_routes/aggregation.rs index d832022d747..7d616434883 100644 --- a/nym-api/src/ecash/api_routes/aggregation.rs +++ b/nym-api/src/ecash/api_routes/aggregation.rs @@ -12,6 +12,7 @@ use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, }; use nym_api_requests::ecash::VerificationKeyResponse; +use nym_coconut_dkg_common::types::EpochId; use nym_ecash_time::{cred_exp_date, EcashTime}; use nym_http_api_common::{FormattedResponse, Output}; use nym_validator_client::nym_api::rfc_3339_date; @@ -71,6 +72,7 @@ async fn master_verification_key( #[derive(Deserialize, IntoParams)] struct ExpirationDateParam { expiration_date: Option, + epoch_id: Option, output: Option, } @@ -93,6 +95,7 @@ async fn expiration_date_signatures( State(state): State>, Query(ExpirationDateParam { expiration_date, + epoch_id, output, }): Query, ) -> AxumResult> { @@ -108,8 +111,13 @@ async fn expiration_date_signatures( // see if we're not in the middle of new dkg state.ensure_dkg_not_in_progress().await?; + let epoch_id = match epoch_id { + Some(epoch_id) => epoch_id, + None => state.current_dkg_epoch().await?, + }; + let expiration_date_signatures = state - .master_expiration_date_signatures(expiration_date) + .master_expiration_date_signatures(expiration_date, epoch_id) .await?; Ok( diff --git a/nym-api/src/ecash/api_routes/partial_signing.rs b/nym-api/src/ecash/api_routes/partial_signing.rs index b19e938d888..36b71535c83 100644 --- a/nym-api/src/ecash/api_routes/partial_signing.rs +++ b/nym-api/src/ecash/api_routes/partial_signing.rs @@ -13,6 +13,7 @@ use nym_api_requests::ecash::{ BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, }; +use nym_coconut_dkg_common::types::EpochId; use nym_ecash_time::{cred_exp_date, EcashTime}; use nym_http_api_common::{FormattedResponse, Output, OutputParams}; use nym_validator_client::nym_api::rfc_3339_date; @@ -114,6 +115,7 @@ async fn post_blind_sign( #[derive(Deserialize, IntoParams)] struct ExpirationDateParam { expiration_date: Option, + epoch_id: Option, output: Option, } @@ -137,6 +139,7 @@ async fn partial_expiration_date_signatures( State(state): State>, Query(ExpirationDateParam { expiration_date, + epoch_id, output, }): Query, ) -> AxumResult> { @@ -152,8 +155,13 @@ async fn partial_expiration_date_signatures( // see if we're not in the middle of new dkg state.ensure_dkg_not_in_progress().await?; + let epoch_id = match epoch_id { + Some(epoch_id) => epoch_id, + None => state.current_dkg_epoch().await?, + }; + let expiration_date_signatures = state - .partial_expiration_date_signatures(expiration_date) + .partial_expiration_date_signatures(expiration_date, epoch_id) .await?; Ok(output.to_response(PartialExpirationDateSignatureResponse { diff --git a/nym-api/src/ecash/state/global.rs b/nym-api/src/ecash/state/global.rs index 24d1ff00ff2..d1e94d4cae8 100644 --- a/nym-api/src/ecash/state/global.rs +++ b/nym-api/src/ecash/state/global.rs @@ -5,6 +5,7 @@ use crate::ecash::helpers::{ CachedImmutableEpochItem, CachedImmutableItems, IssuedCoinIndicesSignatures, IssuedExpirationDateSignatures, }; +use nym_coconut_dkg_common::types::EpochId; use nym_compact_ecash::VerificationKeyAuth; use nym_validator_client::nyxd::AccountId; use time::Date; @@ -18,7 +19,7 @@ pub(crate) struct GlobalEcachState { pub(crate) coin_index_signatures: CachedImmutableEpochItem, pub(crate) expiration_date_signatures: - CachedImmutableItems, + CachedImmutableItems<(EpochId, Date), IssuedExpirationDateSignatures>, } impl GlobalEcachState { diff --git a/nym-api/src/ecash/state/local.rs b/nym-api/src/ecash/state/local.rs index b9e54ec711f..35638259420 100644 --- a/nym-api/src/ecash/state/local.rs +++ b/nym-api/src/ecash/state/local.rs @@ -9,6 +9,7 @@ use crate::ecash::helpers::{ use crate::ecash::keys::KeyPair; use crate::ecash::storage::models::IssuedHash; use nym_api_requests::ecash::models::{CommitedDeposit, DepositId}; +use nym_coconut_dkg_common::types::EpochId; use nym_crypto::asymmetric::ed25519; use nym_ticketbooks_merkle::{ IssuedTicketbook, IssuedTicketbooksFullMerkleProof, IssuedTicketbooksMerkleTree, MerkleLeaf, @@ -143,7 +144,7 @@ pub(crate) struct LocalEcashState { pub(crate) partial_coin_index_signatures: CachedImmutableEpochItem, pub(crate) partial_expiration_date_signatures: - CachedImmutableItems, + CachedImmutableItems<(EpochId, Date), IssuedExpirationDateSignatures>, // merkle trees for ticketbooks issued for particular expiration dates pub(crate) issued_merkle_trees: Arc>>, diff --git a/nym-api/src/ecash/state/mod.rs b/nym-api/src/ecash/state/mod.rs index ce83e701df6..a4d8a39f46e 100644 --- a/nym-api/src/ecash/state/mod.rs +++ b/nym-api/src/ecash/state/mod.rs @@ -401,10 +401,11 @@ impl EcashState { pub(crate) async fn master_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: EpochId, ) -> Result> { self.global .expiration_date_signatures - .get_or_init(expiration_date, || async { + .get_or_init((epoch_id, expiration_date), || async { // 1. sanity check to see if the expiration_date is not nonsense ensure_sane_expiration_date(expiration_date)?; @@ -412,7 +413,7 @@ impl EcashState { if let Some(master_sigs) = self .aux .storage - .get_master_expiration_date_signatures(expiration_date) + .get_master_expiration_date_signatures(expiration_date, epoch_id) .await? { return Ok(master_sigs); @@ -435,13 +436,16 @@ impl EcashState { // check if we're attempting to query ourselves, in that case just get local signature // rather than making the http query let partial = if Some(api.cosmos_address) == cosmos_address { - self.partial_expiration_date_signatures(expiration_date) + self.partial_expiration_date_signatures(expiration_date, epoch_id) .await? .signatures .clone() } else { api.api_client - .partial_expiration_date_signatures(Some(expiration_date)) + .partial_expiration_date_signatures( + Some(expiration_date), + Some(epoch_id), + ) .await? .signatures }; @@ -480,10 +484,11 @@ impl EcashState { pub(crate) async fn partial_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: EpochId, ) -> Result> { self.local .partial_expiration_date_signatures - .get_or_init(expiration_date, || async { + .get_or_init((epoch_id, expiration_date), || async { // 1. sanity check to see if the expiration_date is not nonsense ensure_sane_expiration_date(expiration_date)?; @@ -491,7 +496,7 @@ impl EcashState { if let Some(partial_sigs) = self .aux .storage - .get_partial_expiration_date_signatures(expiration_date) + .get_partial_expiration_date_signatures(expiration_date, epoch_id) .await? { return Ok(partial_sigs); diff --git a/nym-api/src/ecash/storage/manager.rs b/nym-api/src/ecash/storage/manager.rs index e714600ef6b..52b5940f040 100644 --- a/nym-api/src/ecash/storage/manager.rs +++ b/nym-api/src/ecash/storage/manager.rs @@ -128,6 +128,7 @@ pub trait EcashStorageManagerExt { async fn get_partial_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: i64, ) -> Result, sqlx::Error>; async fn insert_partial_expiration_date_signatures( &self, @@ -139,6 +140,7 @@ pub trait EcashStorageManagerExt { async fn get_master_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: i64, ) -> Result, sqlx::Error>; async fn insert_master_expiration_date_signatures( &self, @@ -501,15 +503,17 @@ impl EcashStorageManagerExt for StorageManager { async fn get_partial_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( RawExpirationDateSignatures, r#" SELECT epoch_id as "epoch_id: u32", serialised_signatures FROM partial_expiration_date_signatures - WHERE expiration_date = ? + WHERE expiration_date = ? AND epoch_id = ? "#, - expiration_date + expiration_date, + epoch_id ) .fetch_optional(&self.connection_pool) .await @@ -535,15 +539,17 @@ impl EcashStorageManagerExt for StorageManager { async fn get_master_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( RawExpirationDateSignatures, r#" SELECT epoch_id as "epoch_id: u32", serialised_signatures FROM global_expiration_date_signatures - WHERE expiration_date = ? + WHERE expiration_date = ? AND epoch_id = ? "#, - expiration_date + expiration_date, + epoch_id ) .fetch_optional(&self.connection_pool) .await diff --git a/nym-api/src/ecash/storage/mod.rs b/nym-api/src/ecash/storage/mod.rs index 197affb6a85..1131b42ccbb 100644 --- a/nym-api/src/ecash/storage/mod.rs +++ b/nym-api/src/ecash/storage/mod.rs @@ -143,6 +143,7 @@ pub trait EcashStorageExt { async fn get_partial_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: EpochId, ) -> Result, NymApiStorageError>; async fn insert_partial_expiration_date_signatures( @@ -154,6 +155,7 @@ pub trait EcashStorageExt { async fn get_master_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: EpochId, ) -> Result, NymApiStorageError>; async fn insert_master_expiration_date_signatures( @@ -456,10 +458,11 @@ impl EcashStorageExt for NymApiStorage { async fn get_partial_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: EpochId, ) -> Result, NymApiStorageError> { let Some(raw) = self .manager - .get_partial_expiration_date_signatures(expiration_date) + .get_partial_expiration_date_signatures(expiration_date, epoch_id as i64) .await? else { return Ok(None); @@ -491,10 +494,11 @@ impl EcashStorageExt for NymApiStorage { async fn get_master_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: EpochId, ) -> Result, NymApiStorageError> { let Some(raw) = self .manager - .get_master_expiration_date_signatures(expiration_date) + .get_master_expiration_date_signatures(expiration_date, epoch_id as i64) .await? else { return Ok(None); diff --git a/nym-credential-proxy/nym-credential-proxy/Cargo.toml b/nym-credential-proxy/nym-credential-proxy/Cargo.toml index 0d322ad34e9..79077003844 100644 --- a/nym-credential-proxy/nym-credential-proxy/Cargo.toml +++ b/nym-credential-proxy/nym-credential-proxy/Cargo.toml @@ -11,15 +11,12 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -async-trait.workspace = true axum.workspace = true anyhow.workspace = true bip39 = { workspace = true, features = ["zeroize"] } bs58.workspace = true cfg-if = { workspace = true } -colored.workspace = true clap = { workspace = true, features = ["derive", "env"] } -dotenvy.workspace = true futures.workspace = true humantime.workspace = true rand.workspace = true @@ -33,7 +30,6 @@ time.workspace = true thiserror.workspace = true tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal"] } tokio-util = { workspace = true, features = ["rt"] } -tower.workspace = true tower-http = { workspace = true, features = ["cors"], optional = true } tracing.workspace = true url.workspace = true @@ -49,7 +45,7 @@ nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand", " nym-credentials = { path = "../../common/credentials" } nym-credentials-interface = { path = "../../common/credentials-interface" } nym-ecash-contract-common = { path = "../../common/cosmwasm-smart-contracts/ecash-contract" } -nym-http-api-common = { path = "../../common/http-api-common", features = ["utoipa"] } +nym-http-api-common = { path = "../../common/http-api-common", features = ["utoipa", "middleware"] } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-network-defaults = { path = "../../common/network-defaults" } diff --git a/nym-credential-proxy/nym-credential-proxy/migrations/04_global_expiration_date_signatures_epoch_fix.sql b/nym-credential-proxy/nym-credential-proxy/migrations/04_global_expiration_date_signatures_epoch_fix.sql new file mode 100644 index 00000000000..78a3fad0939 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/migrations/04_global_expiration_date_signatures_epoch_fix.sql @@ -0,0 +1,18 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +DROP TABLE global_expiration_date_signatures; + +CREATE TABLE global_expiration_date_signatures +( + expiration_date DATE NOT NULL, + epoch_id INTEGER NOT NULL, + serialization_revision INTEGER NOT NULL, + + -- combined signatures for all tuples issued for given day + serialised_signatures BLOB NOT NULL, + + PRIMARY KEY (epoch_id, expiration_date) +) \ No newline at end of file diff --git a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs index 5e2c858b26b..33ad66391ef 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs @@ -83,7 +83,7 @@ pub(crate) async fn try_obtain_wallet_shares( let _ = state.master_verification_key(Some(epoch)).await?; let _ = state.master_coin_index_signatures(Some(epoch)).await?; let _ = state - .master_expiration_date_signatures(expiration_date) + .master_expiration_date_signatures(epoch, expiration_date) .await?; let ecash_api_clients = state.ecash_clients(epoch).await?.clone(); diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs index 2fda74436e1..4579b677fa2 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs @@ -97,7 +97,9 @@ impl ApiState { let _ = self.ecash_threshold(epoch_id).await?; let _ = self.ecash_clients(epoch_id).await?; let _ = self.master_coin_index_signatures(Some(epoch_id)).await?; - let _ = self.master_expiration_date_signatures(today).await?; + let _ = self + .master_expiration_date_signatures(epoch_id, today) + .await?; Ok(()) } @@ -253,7 +255,7 @@ impl ApiState { let aggregated_expiration_date_signatures = if include_expiration_date_signatures { debug!("including expiration date signatures in the response"); Some( - self.master_expiration_date_signatures(expiration_date) + self.master_expiration_date_signatures(epoch_id, expiration_date) .await .map(|signatures| AggregatedExpirationDateSignaturesResponse { signatures: signatures.clone(), @@ -515,12 +517,13 @@ impl ApiState { pub(crate) async fn master_expiration_date_signatures( &self, + epoch_id: EpochId, expiration_date: Date, ) -> Result, VpnApiError> { self.inner .ecash_state .expiration_date_signatures - .get_or_init(expiration_date, || async { + .get_or_init((epoch_id, expiration_date), || async { // 1. sanity check to see if the expiration_date is not nonsense ensure_sane_expiration_date(expiration_date)?; @@ -528,7 +531,7 @@ impl ApiState { if let Some(master_sigs) = self .inner .storage - .get_master_expiration_date_signatures(expiration_date) + .get_master_expiration_date_signatures(expiration_date, epoch_id) .await? { return Ok(master_sigs); @@ -536,7 +539,7 @@ impl ApiState { info!( - "attempting to establish master expiration date signatures for {expiration_date}..." + "attempting to establish master expiration date signatures for {expiration_date} and epoch {epoch_id}..." ); // 3. go around APIs and attempt to aggregate the data @@ -553,7 +556,7 @@ impl ApiState { let partial = api .api_client - .partial_expiration_date_signatures(Some(expiration_date)) + .partial_expiration_date_signatures(Some(expiration_date), Some(epoch_id)) .await? .signatures; Ok(ExpirationDateSignatureShare { @@ -697,7 +700,7 @@ pub(crate) struct EcashState { pub(crate) coin_index_signatures: CachedImmutableEpochItem, pub(crate) expiration_date_signatures: - CachedImmutableItems, + CachedImmutableItems<(EpochId, Date), AggregatedExpirationDateSignatures>, } pub(crate) type ChainReadPermit<'a> = RwLockReadGuard<'a, DirectSigningHttpRpcNyxdClient>; diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs index 5942d4e9cc3..b5fefae4490 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs @@ -332,18 +332,20 @@ impl SqliteStorageManager { pub(crate) async fn get_master_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( RawExpirationDateSignatures, r#" - SELECT epoch_id as "epoch_id: u32", serialised_signatures, serialization_revision as "serialization_revision: u8" + SELECT serialised_signatures, serialization_revision as "serialization_revision: u8" FROM global_expiration_date_signatures - WHERE expiration_date = ? + WHERE expiration_date = ? AND epoch_id = ? "#, - expiration_date + expiration_date, + epoch_id ) - .fetch_optional(&self.connection_pool) - .await + .fetch_optional(&self.connection_pool) + .await } pub(crate) async fn insert_master_expiration_date_signatures( diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs index 1759ccdfc3b..437e62c8706 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs @@ -331,10 +331,11 @@ impl VpnApiStorage { pub(crate) async fn get_master_expiration_date_signatures( &self, expiration_date: Date, + epoch_id: EpochId, ) -> Result, VpnApiError> { let Some(raw) = self .storage_manager - .get_master_expiration_date_signatures(expiration_date) + .get_master_expiration_date_signatures(expiration_date, epoch_id as i64) .await? else { return Ok(None); diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/models.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/models.rs index 5bc443bf450..8118890b07a 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/storage/models.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/storage/models.rs @@ -36,8 +36,6 @@ pub struct FullBlindedShares { #[derive(FromRow)] pub struct RawExpirationDateSignatures { - #[allow(dead_code)] - pub epoch_id: u32, pub serialised_signatures: Vec, pub serialization_revision: u8, }