diff --git a/Cargo.lock b/Cargo.lock index b9bcc7eb58..7ec3531bb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5005,21 +5005,16 @@ name = "nym-bandwidth-controller" version = "0.1.0" dependencies = [ "async-trait", - "bip39", "log", "nym-credential-storage", "nym-credentials", "nym-credentials-interface", "nym-crypto", - "nym-ecash-contract-common", "nym-ecash-time", - "nym-network-defaults", "nym-task", "nym-validator-client", "rand 0.8.5", "thiserror 2.0.12", - "url", - "zeroize", ] [[package]] @@ -5572,6 +5567,7 @@ dependencies = [ "sqlx", "sqlx-pool-guard", "thiserror 2.0.12", + "time", "tokio", "zeroize", ] @@ -6443,6 +6439,7 @@ dependencies = [ "nym-bin-common", "nym-client-core-config-types", "nym-config", + "nym-credential-verification", "nym-crypto", "nym-gateway", "nym-gateway-stats-storage", @@ -6515,13 +6512,13 @@ version = "0.1.0" dependencies = [ "async-trait", "celes", - "humantime", "humantime-serde", "nym-bin-common", "nym-crypto", "nym-exit-policy", "nym-http-api-client", "nym-noise-keys", + "nym-upgrade-mode-check", "nym-wireguard-types", "rand_chacha 0.3.1", "schemars 0.8.22", @@ -6532,6 +6529,7 @@ dependencies = [ "thiserror 2.0.12", "time", "tokio", + "url", "utoipa", ] diff --git a/common/authenticator-requests/src/client_message.rs b/common/authenticator-requests/src/client_message.rs index 23bdd13ab0..45f7481a46 100644 --- a/common/authenticator-requests/src/client_message.rs +++ b/common/authenticator-requests/src/client_message.rs @@ -6,7 +6,10 @@ use nym_wireguard_types::PeerPublicKey; use crate::{ AuthenticatorVersion, Error, - traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, Versionable}, + traits::{ + FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, UpgradeModeMessage, + Versionable, + }, v2, v3, v4, v5, v6, }; @@ -18,6 +21,7 @@ pub enum ClientMessage { Final(Box), Query(Box), TopUp(Box), + UpgradeModeCheck(Box), } pub struct SerialisedRequest { @@ -131,6 +135,7 @@ impl ClientMessage { ); Ok(SerialisedRequest::new(req.to_bytes()?, id)) } + _ => Err(Error::UnsupportedMessage), } } @@ -189,6 +194,7 @@ impl ClientMessage { ); Ok(SerialisedRequest::new(req.to_bytes()?, id)) } + _ => Err(Error::UnsupportedMessage), } } @@ -237,6 +243,7 @@ impl ClientMessage { }); Ok(SerialisedRequest::new(req.to_bytes()?, id)) } + _ => Err(Error::UnsupportedMessage), } } @@ -245,6 +252,7 @@ impl ClientMessage { registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair}, request::AuthenticatorRequest, topup::TopUpMessage, + upgrade_mode_check::UpgradeModeCheckRequest, }; match self { ClientMessage::Initial(init_message) => { @@ -282,6 +290,22 @@ impl ClientMessage { }); Ok(SerialisedRequest::new(req.to_bytes()?, id)) } + ClientMessage::UpgradeModeCheck(upgrade_mode_check) => { + // currently JWT is the only emergency credential option + let Some(upgrade_mode_jwt) = + upgrade_mode_check.upgrade_mode_global_attestation_jwt() + else { + return Err(Error::conversion( + "no valid known upgrade mode check variants", + )); + }; + let msg = UpgradeModeCheckRequest::UpgradeModeJwt { + token: upgrade_mode_jwt, + }; + + let (req, id) = AuthenticatorRequest::new_upgrade_mode_check_request(msg); + Ok(SerialisedRequest::new(req.to_bytes()?, id)) + } } } } @@ -292,7 +316,7 @@ impl ClientMessage { match self { Self::Final(msg) => msg.credential().is_some(), Self::TopUp(_) => true, - Self::Initial(_) | Self::Query(_) => false, + Self::Initial(_) | Self::Query(_) | Self::UpgradeModeCheck(_) => false, } } @@ -302,6 +326,7 @@ impl ClientMessage { ClientMessage::Final(msg) => msg.version(), ClientMessage::Query(msg) => msg.version(), ClientMessage::TopUp(msg) => msg.version(), + ClientMessage::UpgradeModeCheck(msg) => msg.version(), } } diff --git a/common/authenticator-requests/src/models.rs b/common/authenticator-requests/src/models.rs index dc870df4d5..c01eec98f4 100644 --- a/common/authenticator-requests/src/models.rs +++ b/common/authenticator-requests/src/models.rs @@ -14,6 +14,12 @@ pub enum CurrentUpgradeModeStatus { Unknown, } +impl CurrentUpgradeModeStatus { + pub fn is_enabled(&self) -> bool { + matches!(self, CurrentUpgradeModeStatus::Enabled) + } +} + impl From for CurrentUpgradeModeStatus { fn from(value: bool) -> Self { if value { diff --git a/common/bandwidth-controller/Cargo.toml b/common/bandwidth-controller/Cargo.toml index 075c1702ae..a94abcc1cb 100644 --- a/common/bandwidth-controller/Cargo.toml +++ b/common/bandwidth-controller/Cargo.toml @@ -7,21 +7,16 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -async-trait = { workspace = true } -bip39 = { workspace = true } +async-trait = { workspace = true } log = { workspace = true } rand = { workspace = true } thiserror = { workspace = true } -url = { workspace = true } -zeroize = { workspace = true } nym-credential-storage = { path = "../credential-storage" } nym-credentials = { path = "../credentials" } nym-credentials-interface = { path = "../credentials-interface" } nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "stream_cipher", "aes", "hashing"] } -nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" } nym-ecash-time = { path = "../ecash-time" } -nym-network-defaults = { path = "../network-defaults" } nym-task = { path = "../task" } nym-validator-client = { path = "../client-libs/validator-client", default-features = false } diff --git a/common/bandwidth-controller/src/error.rs b/common/bandwidth-controller/src/error.rs index 4fcff3731a..c5721df092 100644 --- a/common/bandwidth-controller/src/error.rs +++ b/common/bandwidth-controller/src/error.rs @@ -21,6 +21,9 @@ pub enum BandwidthControllerError { #[error("There was a credential storage error - {0}")] CredentialStorageError(Box), + #[error("retrieved upgrade mode token is not a valid String")] + MalformedUpgradeModeToken, + #[error("the credential storage does not contain any usable credentials")] NoCredentialsAvailable, diff --git a/common/bandwidth-controller/src/lib.rs b/common/bandwidth-controller/src/lib.rs index 05be75cbdc..8002e77631 100644 --- a/common/bandwidth-controller/src/lib.rs +++ b/common/bandwidth-controller/src/lib.rs @@ -12,7 +12,7 @@ use crate::utils::{ ApiClientsWrapper, }; use log::error; -use nym_credential_storage::models::RetrievedTicketbook; +use nym_credential_storage::models::{EmergencyCredential, RetrievedTicketbook}; use nym_credential_storage::storage::Storage; use nym_credentials::ecash::bandwidth::CredentialSpendingData; use nym_credentials_interface::{ @@ -220,6 +220,19 @@ impl BandwidthController { } } } + + pub async fn get_emergency_credential( + &self, + typ: &str, + ) -> Result, BandwidthControllerError> + where + ::StorageError: Send + Sync + 'static, + { + self.storage + .get_emergency_credential(typ) + .await + .map_err(BandwidthControllerError::credential_storage_error) + } } impl Clone for BandwidthController diff --git a/common/bandwidth-controller/src/traits.rs b/common/bandwidth-controller/src/traits.rs index 8ff508f3f3..d611649004 100644 --- a/common/bandwidth-controller/src/traits.rs +++ b/common/bandwidth-controller/src/traits.rs @@ -11,6 +11,9 @@ use crate::{error::BandwidthControllerError, BandwidthController, PreparedCreden pub const DEFAULT_TICKETS_TO_SPEND: u32 = 1; +// TODO: this does not really belong here +pub const UPGRADE_MODE_JWT_TYPE: &str = "UPGRADE_MODE_JWT"; + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait BandwidthTicketProvider: Send + Sync { @@ -20,6 +23,8 @@ pub trait BandwidthTicketProvider: Send + Sync { gateway_id: ed25519::PublicKey, tickets_to_spend: u32, ) -> Result; + + async fn get_upgrade_mode_token(&self) -> Result, BandwidthControllerError>; } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -39,4 +44,16 @@ where self.prepare_ecash_ticket(ticket_type, gateway_id.to_bytes(), tickets_to_spend) .await } + + async fn get_upgrade_mode_token(&self) -> Result, BandwidthControllerError> { + let Some(emergency_credential) = + self.get_emergency_credential(UPGRADE_MODE_JWT_TYPE).await? + else { + return Ok(None); + }; + // upgrade mode credential is just a simple stringified JWT + let token = String::from_utf8(emergency_credential.data.content) + .map_err(|_| BandwidthControllerError::MalformedUpgradeModeToken)?; + Ok(Some(token)) + } } diff --git a/common/client-core/src/cli_helpers/client_init.rs b/common/client-core/src/cli_helpers/client_init.rs index feda3ab8d1..57f800c8cf 100644 --- a/common/client-core/src/cli_helpers/client_init.rs +++ b/common/client-core/src/cli_helpers/client_init.rs @@ -81,6 +81,10 @@ pub struct CommonClientInitArgs { #[cfg_attr(feature = "cli", clap(long, hide = true))] pub enabled_credentials_mode: Option, + /// Change the default minimum node performance used during initial node selection filtering. + #[cfg_attr(feature = "cli", clap(long, hide = true))] + pub minimum_gateway_performance: Option, + /// Mostly debug-related option to increase default traffic rate so that you would not need to /// modify config post init #[cfg_attr(feature = "cli", clap(long, hide = true))] @@ -173,10 +177,14 @@ where })?; hardcoded_topology.entry_capable_nodes().cloned().collect() } else { + let minimum_performance = common_args + .minimum_gateway_performance + .unwrap_or(core.debug.topology.minimum_gateway_performance); + crate::init::helpers::gateways_for_init( &core.client.nym_api_urls, user_agent, - core.debug.topology.minimum_gateway_performance, + minimum_performance, core.debug.topology.ignore_ingress_epoch_role, None, ) diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index 151ef4842e..af98d1b797 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::bandwidth::ClientBandwidth; +use crate::client::config::BandwidthTickets; use crate::error::GatewayClientError; use crate::packet_router::PacketRouter; use crate::traits::GatewayPacketRouter; @@ -177,7 +178,20 @@ impl PartiallyDelegatedRouter { let available_bi2 = bibytes2(available as f64); let required_bi2 = bibytes2(required as f64); warn!("run out of bandwidth when attempting to send the message! we got {available_bi2} available, but needed at least {required_bi2} to send the previous message"); - self.client_bandwidth.update_and_log(available, None); + // if we run out of bandwidth (and tried to send reasonable amount of data), + // the upgrade mode is implicitly disabled, as otherwise we would have been + // to proceed + let upgrade_mode = if available + < BandwidthTickets::DEFAULT_REMAINING_BANDWIDTH_THRESHOLD + { + Some(false) + } else { + // we were attempting to send a lot of data at once + // - we have no certainty about upgrade mode at this point + None + }; + self.client_bandwidth + .update_and_log(available, upgrade_mode); // UNIMPLEMENTED: we should stop sending messages until we recover bandwidth Ok(()) } diff --git a/common/credential-storage/Cargo.toml b/common/credential-storage/Cargo.toml index 1b0539dda4..9ca0eab6f3 100644 --- a/common/credential-storage/Cargo.toml +++ b/common/credential-storage/Cargo.toml @@ -14,6 +14,7 @@ bincode = { workspace = true, optional = true } log = { workspace = true } thiserror = { workspace = true } serde = { workspace = true, features = ["derive"], optional = true } +time = { workspace = true } tokio = { workspace = true, features = ["sync"] } zeroize = { workspace = true, features = ["zeroize_derive"] } diff --git a/common/credential-storage/migrations/20251030120000_upgrade_mode_token.sql b/common/credential-storage/migrations/20251030120000_upgrade_mode_token.sql new file mode 100644 index 0000000000..7f4c6f5ec8 --- /dev/null +++ b/common/credential-storage/migrations/20251030120000_upgrade_mode_token.sql @@ -0,0 +1,17 @@ +/* + * Copyright 2025 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + +CREATE TABLE emergency_credential +( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, + -- don't define any strict schema on the content as it might be implementation-dependant + content BLOB NOT NULL, + expiration TIMESTAMP WITHOUT TIME ZONE +); + +-- no point in allowing duplicate data +CREATE UNIQUE INDEX emergency_credential_unique_type_content + ON emergency_credential (type, content); diff --git a/common/credential-storage/src/backends/memory.rs b/common/credential-storage/src/backends/memory.rs index 53207a4060..28b99e4196 100644 --- a/common/credential-storage/src/backends/memory.rs +++ b/common/credential-storage/src/backends/memory.rs @@ -1,7 +1,10 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; +use crate::models::{ + BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent, + RetrievedPendingTicketbook, RetrievedTicketbook, +}; use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; use nym_compact_ecash::VerificationKeyAuth; @@ -22,6 +25,12 @@ pub struct MemoryEcachTicketbookManager { inner: Arc>, } +#[derive(Default)] +struct InternalIdCounters { + next_ticketbook_id: i64, + next_emergency_credential_id: i64, +} + #[derive(Default)] struct EcashCredentialManagerInner { ticketbooks: HashMap, @@ -29,13 +38,22 @@ struct EcashCredentialManagerInner { master_vk: HashMap, coin_indices_sigs: HashMap>, expiration_date_sigs: HashMap<(u64, Date), Vec>, - _next_id: i64, + emergency_credentials: HashMap>, + + // internal counters emulating assignment of an increasing id to new inserted database entries + internal_counters: InternalIdCounters, } impl EcashCredentialManagerInner { - fn next_id(&mut self) -> i64 { - let next = self._next_id; - self._next_id += 1; + fn next_ticketbook_id(&mut self) -> i64 { + let next = self.internal_counters.next_ticketbook_id; + self.internal_counters.next_ticketbook_id += 1; + next + } + + fn next_emergency_credential_id(&mut self) -> i64 { + let next = self.internal_counters.next_emergency_credential_id; + self.internal_counters.next_emergency_credential_id += 1; next } } @@ -170,7 +188,7 @@ impl MemoryEcachTicketbookManager { used_tickets: u32, ) { let mut guard = self.inner.write().await; - let id = guard.next_id(); + let id = guard.next_ticketbook_id(); #[allow(clippy::unwrap_used)] let mut nasty_clone = hack_clone_ticketbook(ticketbook); @@ -277,4 +295,41 @@ impl MemoryEcachTicketbookManager { sigs.signatures.clone(), ); } + + pub(crate) async fn get_emergency_credential(&self, typ: &str) -> Option { + let guard = self.inner.read().await; + + guard.emergency_credentials.get(typ)?.first().cloned() + } + + pub(crate) async fn insert_emergency_credential( + &self, + credential: &EmergencyCredentialContent, + ) { + let mut guard = self.inner.write().await; + let id = guard.next_emergency_credential_id(); + + guard + .emergency_credentials + .entry(credential.typ.clone()) + .or_default() + .push(EmergencyCredential { + id, + data: credential.clone(), + }); + } + + pub(crate) async fn remove_emergency_credential(&self, id: i64) { + let mut guard = self.inner.write().await; + + guard.emergency_credentials.retain(|_, credentials| { + credentials.retain(|c| c.id != id); + !credentials.is_empty() + }) + } + + pub(crate) async fn remove_emergency_credentials_of_type(&self, typ: &str) { + let mut guard = self.inner.write().await; + guard.emergency_credentials.remove(typ); + } } diff --git a/common/credential-storage/src/backends/sqlite.rs b/common/credential-storage/src/backends/sqlite.rs index d196cad049..cb1cddf980 100644 --- a/common/credential-storage/src/backends/sqlite.rs +++ b/common/credential-storage/src/backends/sqlite.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::models::{ - BasicTicketbookInformation, RawCoinIndexSignatures, RawExpirationDateSignatures, - RawVerificationKey, StoredIssuedTicketbook, StoredPendingTicketbook, + BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent, + RawCoinIndexSignatures, RawExpirationDateSignatures, RawVerificationKey, + StoredIssuedTicketbook, StoredPendingTicketbook, }; use nym_ecash_time::Date; use sqlx::{Executor, Sqlite, Transaction}; @@ -305,6 +306,74 @@ impl SqliteEcashTicketbookManager { .await?; Ok(()) } + + pub(crate) async fn get_emergency_credential( + &self, + typ: &str, + ) -> Result, sqlx::Error> { + sqlx::query_as( + r#" + SELECT * + FROM emergency_credential + WHERE type = ? + AND (expiration IS NULL OR expiration > CURRENT_TIMESTAMP) + ORDER BY expiration DESC NULLS LAST + LIMIT 1 + "#, + ) + .bind(typ) + .fetch_optional(&*self.connection_pool) + .await + } + + pub(crate) async fn insert_emergency_credential( + &self, + credential: &EmergencyCredentialContent, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO emergency_credential + (type, content, expiration) + VALUES (?, ?, ?) + ON CONFLICT(type, content) DO NOTHING; + "#, + credential.typ, + credential.content, + credential.expiration, + ) + .execute(&*self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn remove_emergency_credential(&self, id: i64) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM emergency_credential + WHERE id = ? + "#, + id + ) + .execute(&*self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn remove_emergency_credentials_of_type( + &self, + typ: &str, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM emergency_credential + WHERE type = ? + "#, + typ + ) + .execute(&*self.connection_pool) + .await?; + Ok(()) + } } pub(crate) async fn get_next_unspent_ticketbook<'a, E>( diff --git a/common/credential-storage/src/ephemeral_storage.rs b/common/credential-storage/src/ephemeral_storage.rs index 4c11a456e9..cd65bf3cca 100644 --- a/common/credential-storage/src/ephemeral_storage.rs +++ b/common/credential-storage/src/ephemeral_storage.rs @@ -3,7 +3,10 @@ use crate::backends::memory::MemoryEcachTicketbookManager; use crate::error::StorageError; -use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; +use crate::models::{ + BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent, + RetrievedPendingTicketbook, RetrievedTicketbook, +}; use crate::storage::Storage; use async_trait::async_trait; use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; @@ -218,6 +221,38 @@ impl Storage for EphemeralStorage { .await; Ok(()) } + + async fn get_emergency_credential( + &self, + typ: &str, + ) -> Result, Self::StorageError> { + Ok(self.storage_manager.get_emergency_credential(typ).await) + } + + async fn insert_emergency_credential( + &self, + credential: &EmergencyCredentialContent, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_emergency_credential(credential) + .await; + Ok(()) + } + + async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError> { + self.storage_manager.remove_emergency_credential(id).await; + Ok(()) + } + + async fn remove_emergency_credentials_of_type( + &self, + typ: &str, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .remove_emergency_credentials_of_type(typ) + .await; + Ok(()) + } } #[cfg(test)] diff --git a/common/credential-storage/src/models.rs b/common/credential-storage/src/models.rs index 5be1c45d29..f98bcd1f09 100644 --- a/common/credential-storage/src/models.rs +++ b/common/credential-storage/src/models.rs @@ -3,6 +3,7 @@ use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; use nym_ecash_time::Date; +use time::OffsetDateTime; use zeroize::{Zeroize, ZeroizeOnDrop}; pub struct RetrievedTicketbook { @@ -78,3 +79,20 @@ pub struct RawVerificationKey { pub serialised_key: Vec, pub serialization_revision: u8, } + +#[derive(Clone, Debug)] +#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] +pub struct EmergencyCredential { + pub id: i64, + #[cfg_attr(not(target_arch = "wasm32"), sqlx(flatten))] + pub data: EmergencyCredentialContent, +} + +#[derive(Clone, Debug)] +#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] +pub struct EmergencyCredentialContent { + #[cfg_attr(not(target_arch = "wasm32"), sqlx(rename = "type"))] + pub typ: String, + pub content: Vec, + pub expiration: Option, +} diff --git a/common/credential-storage/src/persistent_storage/mod.rs b/common/credential-storage/src/persistent_storage/mod.rs index b3302983ee..debef28093 100644 --- a/common/credential-storage/src/persistent_storage/mod.rs +++ b/common/credential-storage/src/persistent_storage/mod.rs @@ -3,6 +3,7 @@ mod legacy_helpers; +use crate::models::{EmergencyCredential, EmergencyCredentialContent}; use crate::{ backends::sqlite::{ get_next_unspent_ticketbook, increase_used_ticketbook_tickets, SqliteEcashTicketbookManager, @@ -401,4 +402,36 @@ impl Storage for PersistentStorage { .await?; Ok(()) } + + async fn get_emergency_credential( + &self, + typ: &str, + ) -> Result, Self::StorageError> { + Ok(self.storage_manager.get_emergency_credential(typ).await?) + } + + async fn insert_emergency_credential( + &self, + credential: &EmergencyCredentialContent, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .insert_emergency_credential(credential) + .await?; + Ok(()) + } + + async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError> { + self.storage_manager.remove_emergency_credential(id).await?; + Ok(()) + } + + async fn remove_emergency_credentials_of_type( + &self, + typ: &str, + ) -> Result<(), Self::StorageError> { + self.storage_manager + .remove_emergency_credentials_of_type(typ) + .await?; + Ok(()) + } } diff --git a/common/credential-storage/src/storage.rs b/common/credential-storage/src/storage.rs index add8131e12..483a4571c5 100644 --- a/common/credential-storage/src/storage.rs +++ b/common/credential-storage/src/storage.rs @@ -1,7 +1,10 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; +use crate::models::{ + BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent, + RetrievedPendingTicketbook, RetrievedTicketbook, +}; use async_trait::async_trait; use nym_compact_ecash::VerificationKeyAuth; use nym_credentials::ecash::bandwidth::serialiser::keys::EpochVerificationKey; @@ -108,4 +111,21 @@ pub trait Storage: Clone + Send + Sync { &self, signatures: &AggregatedExpirationDateSignatures, ) -> Result<(), Self::StorageError>; + + async fn get_emergency_credential( + &self, + typ: &str, + ) -> Result, Self::StorageError>; + + async fn insert_emergency_credential( + &self, + credential: &EmergencyCredentialContent, + ) -> Result<(), Self::StorageError>; + + async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError>; + + async fn remove_emergency_credentials_of_type( + &self, + typ: &str, + ) -> Result<(), Self::StorageError>; } diff --git a/common/credential-verification/src/lib.rs b/common/credential-verification/src/lib.rs index 3f2b77f840..430674c991 100644 --- a/common/credential-verification/src/lib.rs +++ b/common/credential-verification/src/lib.rs @@ -13,6 +13,7 @@ use tracing::*; pub use client_bandwidth::*; pub use error::*; +pub use upgrade_mode::UpgradeModeState; pub mod bandwidth_storage_manager; mod client_bandwidth; diff --git a/common/credential-verification/src/upgrade_mode.rs b/common/credential-verification/src/upgrade_mode.rs index 81385bece1..7dec1010f7 100644 --- a/common/credential-verification/src/upgrade_mode.rs +++ b/common/credential-verification/src/upgrade_mode.rs @@ -12,7 +12,7 @@ use std::time::Duration; use thiserror::Error; use time::OffsetDateTime; use tokio::sync::{Notify, RwLock}; -use tracing::{debug, error}; +use tracing::{debug, error, info}; #[derive(Debug, Error)] pub enum UpgradeModeEnableError { @@ -101,6 +101,10 @@ impl UpgradeModeDetails { } } + pub fn state(&self) -> &UpgradeModeState { + &self.state + } + pub fn enabled(&self) -> bool { self.state.upgrade_mode_enabled() } @@ -156,6 +160,7 @@ impl UpgradeModeDetails { // note: if attestation has been returned, it means we're definitely in upgrade mode // (otherwise it wouldn't have existed in the state) + info!("managed to initialise upgrade mode through received JWT"); Ok(()) } @@ -198,6 +203,10 @@ impl UpgradeModeState { } } + pub fn attester_pubkey(&self) -> ed25519::PublicKey { + self.inner.expected_attester_public_key + } + pub async fn attestation(&self) -> Option { self.inner.expected_attestation.read().await.clone() } diff --git a/common/upgrade-mode-check/src/error.rs b/common/upgrade-mode-check/src/error.rs index 3b2f2b5b12..acb9ec198c 100644 --- a/common/upgrade-mode-check/src/error.rs +++ b/common/upgrade-mode-check/src/error.rs @@ -9,6 +9,9 @@ pub enum UpgradeModeCheckError { #[error("failed to decode jwt metadata: {source}")] TokenMetadataDecodeFailure { source: jwt_simple::Error }, + #[error("the upgrade mode JWT is malformed")] + MalformedToken, + #[error("the jwt metadata didn't contain explicit public key")] MissingTokenPublicKey, diff --git a/common/upgrade-mode-check/src/jwt.rs b/common/upgrade-mode-check/src/jwt.rs index 64c436b23e..42329f80a5 100644 --- a/common/upgrade-mode-check/src/jwt.rs +++ b/common/upgrade-mode-check/src/jwt.rs @@ -4,7 +4,10 @@ use crate::{UpgradeModeAttestation, UpgradeModeCheckError}; use jwt_simple::claims::Claims; use jwt_simple::common::{KeyMetadata, VerificationOptions}; -use jwt_simple::prelude::{EdDSAKeyPairLike, EdDSAPublicKeyLike}; +use jwt_simple::prelude::{ + Base64UrlSafeNoPadding, EdDSAKeyPairLike, EdDSAPublicKeyLike, JWTClaims, +}; +use jwt_simple::reexports::ct_codecs::Decoder; use jwt_simple::token::Token; use nym_crypto::asymmetric::ed25519; use std::collections::HashSet; @@ -76,12 +79,26 @@ pub fn validate_upgrade_mode_jwt( Ok(attestation) } +/// Attempt to extract the upgrade mode JWT payload from the provided token +pub fn try_decode_upgrade_mode_jwt_claims( + token: &str, +) -> Result, UpgradeModeCheckError> { + let mut parts = token.split('.'); + let _header = parts.next().ok_or(UpgradeModeCheckError::MalformedToken)?; + let claims_b64 = parts.next().ok_or(UpgradeModeCheckError::MalformedToken)?; + let claims_bytes = Base64UrlSafeNoPadding::decode_to_vec(claims_b64, None) + .map_err(|_| UpgradeModeCheckError::MalformedToken)?; + + serde_json::from_slice(&claims_bytes).map_err(|_| UpgradeModeCheckError::MalformedToken) +} + #[cfg(test)] mod tests { use super::*; use crate::generate_new_attestation; use nym_crypto::asymmetric::ed25519; use nym_test_utils::helpers::deterministic_rng; + use time::OffsetDateTime; #[test] fn generate_and_validate_jwt() { @@ -142,4 +159,58 @@ mod tests { // we don't care about issuer assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, None).is_ok()); } + + #[test] + fn decode_upgrade_mode_claims() { + let invalid_jwts = [ + "", + "invalidSections", + "also.invalid.sections", + "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImp3ayI6IkZCdWsxS2lqS3ZwQ3VrU1Zhc0xoN1k1REZTZEdnVzU5WThQOUhWTDh2Mzk5In0.eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImp3ayI6IkZCdWsxS2lqS3ZwQ3VrU1Zhc0xoN1k1REZTZEdnVzU5WThQOUhWTDh2Mzk5In0.eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImp3ayI6IkZCdWsxS2lqS3ZwQ3VrU1Zhc0xoN1k1REZTZEdnVzU5WThQOUhWTDh2Mzk5In0", + ]; + + let attestation_key = ed25519::PrivateKey::from_bytes(&[ + 108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248, + 163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161, + ]) + .unwrap(); + let jwt_key = ed25519::PrivateKey::from_bytes(&[ + 152, 17, 144, 255, 213, 219, 246, 208, 109, 33, 100, 73, 1, 141, 32, 63, 141, 89, 167, + 2, 52, 215, 241, 219, 200, 18, 159, 241, 76, 111, 42, 32, + ]) + .unwrap(); + let jwt_keys = ed25519::KeyPair::from(jwt_key); + + let validity = Duration::from_secs(60 * 60); + let attestation = generate_new_attestation(&attestation_key, vec![*jwt_keys.public_key()]); + let valid_jwt = generate_jwt_for_upgrade_mode_attestation( + attestation.clone(), + validity, + &jwt_keys, + Some("nym-credential-proxy"), + ); + + for invalid in invalid_jwts { + assert!(try_decode_upgrade_mode_jwt_claims(invalid).is_err()) + } + + let decoded = try_decode_upgrade_mode_jwt_claims(&valid_jwt).unwrap(); + assert_eq!(decoded.issuer.unwrap(), "nym-credential-proxy"); + assert_eq!(decoded.custom, attestation); + + // unfortunately we can't inject current time when constructing the JWT so the best we can do is ensure its within error margin + let margin = Duration::from_secs(10); + let now = OffsetDateTime::now_utc(); + let min = now - margin; + let max = now + margin; + let issued = decoded.issued_at.unwrap(); + let issued_time = OffsetDateTime::from_unix_timestamp(issued.as_secs() as i64).unwrap(); + assert!(issued_time >= min && issued_time <= max); + + let min = now - margin + validity; + let max = now + margin + validity; + let expires = decoded.expires_at.unwrap(); + let expires_time = OffsetDateTime::from_unix_timestamp(expires.as_secs() as i64).unwrap(); + assert!(expires_time >= min && expires_time <= max); + } } diff --git a/common/upgrade-mode-check/src/lib.rs b/common/upgrade-mode-check/src/lib.rs index c2ab50284b..7a2e246e2d 100644 --- a/common/upgrade-mode-check/src/lib.rs +++ b/common/upgrade-mode-check/src/lib.rs @@ -11,8 +11,10 @@ pub use attestation::{ pub use error::UpgradeModeCheckError; pub use jwt::{ CREDENTIAL_PROXY_JWT_ISSUER, generate_jwt_for_upgrade_mode_attestation, - validate_upgrade_mode_jwt, + try_decode_upgrade_mode_jwt_claims, validate_upgrade_mode_jwt, }; #[cfg(not(target_arch = "wasm32"))] pub use attestation::attempt_retrieve_attestation; + +pub const UPGRADE_MODE_CREDENTIAL_TYPE: &str = "upgrade_mode_jwt"; diff --git a/common/wireguard-private-metadata/shared/src/lib.rs b/common/wireguard-private-metadata/shared/src/lib.rs index c0d711ac42..6a8eadcc24 100644 --- a/common/wireguard-private-metadata/shared/src/lib.rs +++ b/common/wireguard-private-metadata/shared/src/lib.rs @@ -10,7 +10,9 @@ pub mod routes; pub use models::v0; pub use models::{ AxumErrorResponse, AxumResult, Construct, ErrorResponse, Extract, Request, Response, Version, - error::Error as ModelError, interface, latest, v1, v2, + error::Error as ModelError, + interface::{self, AvailableBandwidth}, + latest, v1, v2, }; fn make_bincode_serializer() -> impl bincode::Options { diff --git a/common/wireguard-private-metadata/shared/src/models/interface.rs b/common/wireguard-private-metadata/shared/src/models/interface.rs index c97db77d2a..317e6680ca 100644 --- a/common/wireguard-private-metadata/shared/src/models/interface.rs +++ b/common/wireguard-private-metadata/shared/src/models/interface.rs @@ -229,3 +229,45 @@ impl Extract for Response { } } } + +#[derive(Debug, Clone, Copy)] +pub struct AvailableBandwidth { + pub bandwidth_bytes: i64, + pub upgrade_mode: Option, +} + +impl From for AvailableBandwidth { + fn from(value: v1::AvailableBandwidthResponse) -> Self { + AvailableBandwidth { + bandwidth_bytes: value.available_bandwidth, + upgrade_mode: None, + } + } +} + +impl From for AvailableBandwidth { + fn from(value: v2::AvailableBandwidthResponse) -> Self { + AvailableBandwidth { + bandwidth_bytes: value.available_bandwidth, + upgrade_mode: Some(value.upgrade_mode), + } + } +} + +impl From for AvailableBandwidth { + fn from(value: v1::TopUpResponse) -> Self { + AvailableBandwidth { + bandwidth_bytes: value.available_bandwidth, + upgrade_mode: None, + } + } +} + +impl From for AvailableBandwidth { + fn from(value: v2::TopUpResponse) -> Self { + AvailableBandwidth { + bandwidth_bytes: value.available_bandwidth, + upgrade_mode: Some(value.upgrade_mode), + } + } +} diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index 0eb18a602a..ead0fb18f3 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -13,7 +13,7 @@ use futures::{ channel::{mpsc, oneshot}, SinkExt, StreamExt, }; -use nym_credentials_interface::AvailableBandwidth; +use nym_credentials_interface::{AvailableBandwidth, DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD}; use nym_crypto::aes::cipher::crypto_common::rand_core::RngCore; use nym_crypto::asymmetric::ed25519; use nym_gateway_requests::authenticate::AuthenticateRequest; @@ -35,6 +35,7 @@ use nym_node_metrics::events::MetricsEvent; use nym_sphinx::DestinationAddressBytes; use nym_task::ShutdownToken; use rand::CryptoRng; +use std::cmp::max; use std::net::SocketAddr; use std::time::Duration; use thiserror::Error; @@ -528,6 +529,35 @@ impl FreshHandler { } } + /// Determine the amount of remaining bandwidth the authenticated client should see. + /// This depends on whether the bandwidth stored persistently had already expired (in which case it's set back to 0) + /// and whether the upgrade mode is enabled. In that case the minimum constant amount is returned. + async fn authenticated_bandwidth_bytes( + &self, + client_id: i64, + ) -> Result { + // 1. get the actual registered bandwidth + let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?; + + // 2. check if it had already expired + let true_remaining_bandwidth = if available_bandwidth.expired() { + self.shared_state.storage.reset_bandwidth(client_id).await?; + 0 + } else { + available_bandwidth.bytes + }; + + // 3. perform upgrade mode adjustments + if self.upgrade_mode_enabled() { + Ok(max( + true_remaining_bandwidth, + DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD + 1, + )) + } else { + Ok(true_remaining_bandwidth) + } + } + /// Tries to handle the received authentication request by checking correctness of the received data. /// /// # Arguments @@ -594,14 +624,7 @@ impl FreshHandler { .await?; // check the bandwidth - let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?; - - let bandwidth_remaining = if available_bandwidth.expired() { - self.shared_state.storage.reset_bandwidth(client_id).await?; - 0 - } else { - available_bandwidth.bytes - }; + let bandwidth_remaining = self.authenticated_bandwidth_bytes(client_id).await?; Ok(InitialAuthResult::new( Some(ClientDetails::new( @@ -681,14 +704,7 @@ impl FreshHandler { .await?; // finally check and retrieve client's bandwidth - let available_bandwidth = self.get_registered_available_bandwidth(client_id).await?; - - let bandwidth_remaining = if available_bandwidth.expired() { - self.shared_state.storage.reset_bandwidth(client_id).await?; - 0 - } else { - available_bandwidth.bytes - }; + let bandwidth_remaining = self.authenticated_bandwidth_bytes(client_id).await?; Ok(InitialAuthResult::new( Some(ClientDetails::new( diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 7f08a5e52c..ba891bd716 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -49,7 +49,7 @@ pub use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgen pub(crate) mod client_handling; pub(crate) mod internal_service_providers; mod stale_data_cleaner; -pub(crate) mod upgrade_mode; +pub mod upgrade_mode; #[derive(Debug, Clone)] pub struct LocalNetworkRequesterOpts { @@ -122,7 +122,7 @@ impl GatewayTasksBuilder { metrics: NymNodeMetrics, mnemonic: Arc>, user_agent: UserAgent, - upgrade_mode_attester_public_key: ed25519::PublicKey, + upgrade_mode_state: UpgradeModeState, shutdown_tracker: ShutdownTracker, ) -> GatewayTasksBuilder { GatewayTasksBuilder { @@ -137,7 +137,7 @@ impl GatewayTasksBuilder { mix_packet_sender, metrics_sender, metrics, - upgrade_mode_state: UpgradeModeState::new(upgrade_mode_attester_public_key), + upgrade_mode_state, mnemonic, shutdown_tracker, ecash_manager: None, diff --git a/gateway/src/node/upgrade_mode/watcher.rs b/gateway/src/node/upgrade_mode/watcher.rs index c798ddc55b..509e89c5f5 100644 --- a/gateway/src/node/upgrade_mode/watcher.rs +++ b/gateway/src/node/upgrade_mode/watcher.rs @@ -10,11 +10,17 @@ use nym_credential_verification::upgrade_mode::{ use nym_task::ShutdownToken; use nym_upgrade_mode_check::attempt_retrieve_attestation; use std::time::Duration; +use time::OffsetDateTime; use tokio::task::JoinHandle; use tokio::time::Instant; use tracing::{debug, error, info, trace}; use url::Url; +/// Specifies the threshold for retrieval failures that will trigger disabling upgrade mode. +/// This assumes the file has been removed incorrectly and has been replaced by some placeholder 404 +/// page that does not deserialise correctly +const FAILURE_THRESHOLD: usize = 5; + pub struct UpgradeModeWatcher { // default polling interval regular_polling_interval: Duration, @@ -35,6 +41,8 @@ pub struct UpgradeModeWatcher { user_agent: UserAgent, shutdown_token: ShutdownToken, + + consecutive_retrieval_failures: usize, } impl UpgradeModeWatcher { @@ -58,6 +66,7 @@ impl UpgradeModeWatcher { upgrade_mode_state, user_agent, shutdown_token, + consecutive_retrieval_failures: 0, } } @@ -65,7 +74,7 @@ impl UpgradeModeWatcher { self.check_request_sender.clone() } - async fn try_update_state(&self) { + async fn try_update_state(&mut self) { match attempt_retrieve_attestation( self.attestation_url.as_str(), Some(self.user_agent.clone()), @@ -73,10 +82,28 @@ impl UpgradeModeWatcher { .await { Err(err) => { + self.consecutive_retrieval_failures += 1; info!("upgrade mode attestation is not available at this time"); - debug!("retrieval error: {err}") + debug!("retrieval error: {err}"); + + if self.upgrade_mode_state.upgrade_mode_enabled() + && self.consecutive_retrieval_failures > FAILURE_THRESHOLD + { + self.upgrade_mode_state + .try_set_expected_attestation(None) + .await + } else { + self.upgrade_mode_state + .update_last_queried(OffsetDateTime::now_utc()); + } } Ok(attestation) => { + self.consecutive_retrieval_failures = 0; + if attestation.is_some() { + info!("retrieved valid attestation: attempting to begin upgrade mode") + } else { + info!("attempting to disable upgrade mode") + } self.upgrade_mode_state .try_set_expected_attestation(attestation) .await @@ -110,7 +137,8 @@ impl UpgradeModeWatcher { async fn run(&mut self) { info!("starting the update mode watcher"); - let check_wait = tokio::time::sleep(self.regular_polling_interval); + // make sure the first check happens immediately + let check_wait = tokio::time::sleep(Duration::new(0, 0)); tokio::pin!(check_wait); loop { diff --git a/nym-authenticator-client/src/error.rs b/nym-authenticator-client/src/error.rs index 142fbe6981..abf18e47ca 100644 --- a/nym-authenticator-client/src/error.rs +++ b/nym-authenticator-client/src/error.rs @@ -40,6 +40,12 @@ pub enum AuthenticationClientError { source: nym_bandwidth_controller::error::BandwidthControllerError, }, + #[error("failed to retrieve upgrade mode token")] + UpgradeModeToken { + #[source] + source: nym_bandwidth_controller::error::BandwidthControllerError, + }, + #[error("unknown authenticator version number")] UnsupportedAuthenticatorVersion, diff --git a/nym-authenticator-client/src/lib.rs b/nym-authenticator-client/src/lib.rs index 10b651b184..c09a766696 100644 --- a/nym-authenticator-client/src/lib.rs +++ b/nym-authenticator-client/src/lib.rs @@ -8,17 +8,18 @@ use nym_registration_common::GatewayData; use std::net::{IpAddr, SocketAddr}; use std::sync::Arc; use std::time::Duration; -use tracing::{debug, error, trace}; +use tracing::{debug, error, trace, warn}; use crate::error::Result; use crate::mixnet_listener::{MixnetMessageBroadcastReceiver, MixnetMessageInputSender}; use crate::types::{AvailableBandwidthClientResponse, TopUpClientResponse}; +use nym_authenticator_requests::models::BandwidthClaim; use nym_authenticator_requests::traits::UpgradeModeStatus; use nym_authenticator_requests::{ AuthenticatorVersion, client_message::ClientMessage, response::AuthenticatorResponse, traits::Id, v2, v3, v4, v5, v6, }; -use nym_credentials_interface::{CredentialSpendingData, TicketType}; +use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData, TicketType}; use nym_sdk::mixnet::{IncludedSurbs, Recipient, ReconstructedMessage}; use nym_service_provider_requests_common::{Protocol, ServiceProviderTypeExt}; use nym_wireguard_types::PeerPublicKey; @@ -205,6 +206,57 @@ impl AuthenticatorClient { } } + async fn produce_bandwidth_claim( + &self, + controller: &dyn BandwidthTicketProvider, + upgrade_mode_enabled: bool, + ticketbook_type: TicketType, + ) -> Result { + if upgrade_mode_enabled { + match controller + .get_upgrade_mode_token() + .await + .map_err(|source| AuthenticationClientError::UpgradeModeToken { source })? + { + None => warn!( + "the wireguard node is in the upgrade mode, whilst we do not have an upgrade mode token - we will have to use normal ZK nym instead" + ), + + Some(upgrade_mode_token) => { + return Ok(BandwidthClaim { + credential: BandwidthCredential::UpgradeModeJWT { + token: upgrade_mode_token, + }, + kind: ticketbook_type, + }); + } + } + } + + let credential = controller + .get_ecash_ticket( + ticketbook_type, + self.auth_recipient.gateway(), + DEFAULT_TICKETS_TO_SPEND, + ) + .await + .map_err(|source| AuthenticationClientError::GetTicket { + ticketbook_type, + source, + })? + .data; + + let credential = credential + .try_into() + .inspect_err(|err| { + error!( + "failed to convert {ticketbook_type} ticket to a valid BandwidthClaim: {err}" + ) + }) + .map_err(|_| AuthenticationClientError::InternalError)?; + Ok(credential) + } + pub async fn register_wireguard( &mut self, controller: &dyn BandwidthTicketProvider, @@ -255,33 +307,19 @@ impl AuthenticatorClient { &self.ip_addr, &pending_registration_response ); - // This call takes care of updating the credential count in storage, so failure of this must be counted as credential waste - let credential = Some( - controller - .get_ecash_ticket( - ticketbook_type, - self.auth_recipient.gateway(), - DEFAULT_TICKETS_TO_SPEND, - ) - .await - .map_err(|source| RegistrationError::CredentialSent { - source: AuthenticationClientError::GetTicket { - ticketbook_type, - source, - }, - })? - .data, - ); - let credential = credential - .map(TryInto::try_into) - .transpose() - .inspect_err(|err| error!("failed to convert {ticketbook_type} ticket to a valid BandwidthClaim: {err}")) - .map_err(|_| RegistrationError::CredentialSent { - source: AuthenticationClientError::InternalError, - })?; + // if the node reports upgrade mode, we can use the corresponding token for registration + // instead of spending zk-nym ticket + let upgrade_mode_enabled = pending_registration_response + .upgrade_mode_status() + .is_enabled(); + + let bandwidth_claim = self + .produce_bandwidth_claim(controller, upgrade_mode_enabled, ticketbook_type) + .await + .map_err(|source| RegistrationError::CredentialSent { source })?; let finalized_message = pending_registration_response - .finalise_registration(self.keypair.private_key(), credential); + .finalise_registration(self.keypair.private_key(), Some(bandwidth_claim)); let client_message = ClientMessage::Final(finalized_message); trace!("sending final msg to {}: {client_message:?}", &self.ip_addr); @@ -421,4 +459,32 @@ impl AuthenticatorClient { current_upgrade_mode_status, }) } + + pub async fn check_upgrade_mode(&mut self, upgrade_mode_jwt: String) -> Result { + let check_um_message = match self.auth_version { + AuthenticatorVersion::V1 + | AuthenticatorVersion::V2 + | AuthenticatorVersion::V3 + | AuthenticatorVersion::V4 + | AuthenticatorVersion::V5 + | AuthenticatorVersion::UNKNOWN => { + return Err(AuthenticationClientError::UnsupportedAuthenticatorVersion); + } + + AuthenticatorVersion::V6 => ClientMessage::UpgradeModeCheck(Box::new( + v6::upgrade_mode_check::UpgradeModeCheckRequest::UpgradeModeJwt { + token: upgrade_mode_jwt, + }, + )), + }; + + let response = self.send_and_wait_for_response(&check_um_message).await?; + let AuthenticatorResponse::UpgradeMode(upgrade_mode_check_response) = response else { + return Err(AuthenticationClientError::InvalidGatewayAuthResponse); + }; + + Ok(upgrade_mode_check_response + .upgrade_mode_status() + .is_enabled()) + } } diff --git a/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs b/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs index b544f80792..06931852fc 100644 --- a/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs +++ b/nym-credential-proxy/nym-credential-proxy/src/attestation_watcher.rs @@ -12,6 +12,11 @@ use tokio_util::sync::CancellationToken; use tracing::{debug, info}; use url::Url; +/// Specifies the threshold for retrieval failures that will trigger disabling upgrade mode. +/// This assumes the file has been removed incorrectly and has been replaced by some placeholder 404 +/// page that does not deserialise correctly +const FAILURE_THRESHOLD: usize = 5; + pub struct AttestationWatcher { // default polling interval regular_polling_interval: Duration, @@ -28,6 +33,8 @@ pub struct AttestationWatcher { jwt_validity: Duration, upgrade_mode_state: UpgradeModeState, + + consecutive_retrieval_failures: usize, } impl AttestationWatcher { @@ -49,6 +56,7 @@ impl AttestationWatcher { upgrade_mode_state: UpgradeModeState { inner: Arc::new(Default::default()), }, + consecutive_retrieval_failures: 0, } } @@ -56,7 +64,7 @@ impl AttestationWatcher { self.upgrade_mode_state.clone() } - async fn try_update_state(&self) { + async fn try_update_state(&mut self) { match attempt_retrieve_attestation( self.attestation_url.as_str(), Some(generate_user_agent!()), @@ -64,10 +72,26 @@ impl AttestationWatcher { .await { Err(err) => { + self.consecutive_retrieval_failures += 1; info!("upgrade mode attestation is not available at this time"); - debug!("retrieval error: {err}") + debug!("retrieval error: {err}"); + + if self.upgrade_mode_state.has_attestation().await + && self.consecutive_retrieval_failures > FAILURE_THRESHOLD + { + self.upgrade_mode_state + .update( + None, + self.expected_attester_public_key, + &self.jwt_signing_keys, + self.jwt_validity, + ) + .await + } } Ok(attestation) => { + self.consecutive_retrieval_failures = 0; + self.upgrade_mode_state .update( attestation, @@ -80,9 +104,10 @@ impl AttestationWatcher { } } - pub async fn run_forever(self, cancellation_token: CancellationToken) { + pub async fn run_forever(mut self, cancellation_token: CancellationToken) { info!("starting the attestation watcher task"); + // make sure the first check happens immediately let check_wait = tokio::time::sleep(Duration::new(0, 0)); tokio::pin!(check_wait); diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 6d43b0aa97..f431886ebf 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -57,6 +57,7 @@ nym-client-core-config-types = { path = "../common/client-core/config-types", fe "disk-persistence", ] } nym-config = { path = "../common/config" } +nym-credential-verification = { path = "../common/credential-verification" } nym-crypto = { path = "../common/crypto", features = ["asymmetric", "rand"] } nym-nonexhaustive-delayqueue = { path = "../common/nonexhaustive-delayqueue" } nym-mixnet-client = { path = "../common/client-libs/mixnet-client" } diff --git a/nym-node/nym-node-requests/Cargo.toml b/nym-node/nym-node-requests/Cargo.toml index 82119f47c8..ad1ed3013a 100644 --- a/nym-node/nym-node-requests/Cargo.toml +++ b/nym-node/nym-node-requests/Cargo.toml @@ -12,7 +12,6 @@ license.workspace = true [dependencies] celes = { workspace = true } # country codes -humantime = { workspace = true } humantime-serde = { workspace = true } schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } @@ -21,6 +20,7 @@ strum = { workspace = true, features = ["derive"] } strum_macros = { workspace = true } time = { workspace = true, features = ["serde", "formatting", "parsing"] } thiserror = { workspace = true } +url = { workspace = true, features = ["serde"] } nym-crypto = { path = "../../common/crypto", features = [ "asymmetric", @@ -29,6 +29,7 @@ nym-crypto = { path = "../../common/crypto", features = [ nym-exit-policy = { path = "../../common/exit-policy" } nym-noise-keys = { path = "../../common/nymnoise/keys" } nym-wireguard-types = { path = "../../common/wireguard-types", default-features = false } +nym-upgrade-mode-check = { path = "../../common/upgrade-mode-check", features = ["openapi"] } # feature-specific dependencies: diff --git a/nym-node/nym-node-requests/src/api/v1/mod.rs b/nym-node/nym-node-requests/src/api/v1/mod.rs index 544633944f..e19b611f1b 100644 --- a/nym-node/nym-node-requests/src/api/v1/mod.rs +++ b/nym-node/nym-node-requests/src/api/v1/mod.rs @@ -7,6 +7,7 @@ pub mod health; pub mod ip_packet_router; pub mod metrics; pub mod mixnode; +pub mod network; pub mod network_requester; pub mod node; pub mod node_load; diff --git a/nym-node/nym-node-requests/src/api/v1/network.rs b/nym-node/nym-node-requests/src/api/v1/network.rs new file mode 100644 index 0000000000..13811deb67 --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/network.rs @@ -0,0 +1,4 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod models; diff --git a/nym-node/nym-node-requests/src/api/v1/network/models.rs b/nym-node/nym-node-requests/src/api/v1/network/models.rs new file mode 100644 index 0000000000..be5d01b198 --- /dev/null +++ b/nym-node/nym-node-requests/src/api/v1/network/models.rs @@ -0,0 +1,28 @@ +// Copyright 2025 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_crypto::asymmetric::ed25519; +use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey; +use nym_upgrade_mode_check::UpgradeModeAttestation; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; +use url::Url; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct UpgradeModeStatus { + pub enabled: bool, + + #[serde(with = "time::serde::rfc3339")] + #[cfg_attr(feature = "openapi", schema(value_type = String))] + pub last_queried: OffsetDateTime, + + #[cfg_attr(feature = "openapi", schema(value_type = String))] + pub attestation_provider: Url, + + #[serde(with = "bs58_ed25519_pubkey")] + #[cfg_attr(feature = "openapi", schema(value_type = String))] + pub attester_pubkey: ed25519::PublicKey, + + pub published_attestation: Option, +} diff --git a/nym-node/nym-node-requests/src/lib.rs b/nym-node/nym-node-requests/src/lib.rs index 2b539f831e..aaf63baa25 100644 --- a/nym-node/nym-node-requests/src/lib.rs +++ b/nym-node/nym-node-requests/src/lib.rs @@ -36,6 +36,7 @@ pub mod routes { pub const AUXILIARY: &str = "/auxiliary-details"; pub const HEALTH: &str = "/health"; pub const LOAD: &str = "/load"; + pub const NETWORK: &str = "/network"; pub const SWAGGER: &str = "/swagger"; pub const GATEWAY: &str = "/gateway"; @@ -49,6 +50,7 @@ pub mod routes { // define helper functions to get absolute routes absolute_route!(health_absolute, v1_absolute(), HEALTH); absolute_route!(load_absolute, v1_absolute(), LOAD); + absolute_route!(network_absolute, v1_absolute(), NETWORK); absolute_route!(roles_absolute, v1_absolute(), ROLES); absolute_route!(build_info_absolute, v1_absolute(), BUILD_INFO); absolute_route!(host_info_absolute, v1_absolute(), HOST_INFO); @@ -131,6 +133,17 @@ pub mod routes { pub mod ip_packet_router { // use super::*; } + + pub mod network { + use super::*; + pub const UPGRADE_MODE_STATUS: &str = "/upgrade-mode-status"; + + absolute_route!( + upgrade_mode_status_absolute, + network_absolute(), + UPGRADE_MODE_STATUS + ); + } } } } diff --git a/nym-node/src/cli/commands/run/mod.rs b/nym-node/src/cli/commands/run/mod.rs index 36f77c7b29..450a88a761 100644 --- a/nym-node/src/cli/commands/run/mod.rs +++ b/nym-node/src/cli/commands/run/mod.rs @@ -29,7 +29,7 @@ fn check_public_ips(ips: &[IpAddr], local: bool) -> Result<(), NymNodeError> { warn!("\n##### WARNING #####"); for ip in suspicious_ip { warn!( - "The 'public' IP address you're trying to announce: {ip} may not be accessible to other clients.\ + "The 'public' IP address you're trying to announce: {ip} may not be accessible to other clients. \ Please make sure this is what you intended to announce.\ You can ignore this warning if you're running setup on a local network " ) diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index e7eb81623f..08ccef0857 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -9,6 +9,7 @@ use crate::error::NymNodeError; use celes::Country; use clap::Args; use clap::builder::ArgPredicate; +use nym_crypto::asymmetric::ed25519; use std::net::{IpAddr, SocketAddr}; use std::path::{Path, PathBuf}; use url::Url; @@ -429,12 +430,22 @@ pub(crate) struct EntryGatewayArgs { pub(crate) mnemonic: Option, /// Endpoint to query to retrieve current upgrade mode attestation. + /// This argument should never be set outside testnets and local networks. #[clap( long, env = NYMNODE_UPGRADE_MODE_ATTESTATION_URL_ARG )] #[zeroize(skip)] pub(crate) upgrade_mode_attestation_url: Option, + + /// Expected public key of the entity signing the published attestation. + /// This argument should never be set outside testnets and local networks. + #[clap( + long, + env = NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY_ARG + )] + #[zeroize(skip)] + pub(crate) upgrade_mode_attester_public_key: Option, } impl EntryGatewayArgs { @@ -465,6 +476,9 @@ impl EntryGatewayArgs { if let Some(upgrade_mode_attestation_url) = self.upgrade_mode_attestation_url.take() { section.upgrade_mode.attestation_url = upgrade_mode_attestation_url } + if let Some(upgrade_mode_attester_public_key) = self.upgrade_mode_attester_public_key { + section.upgrade_mode.attester_public_key = upgrade_mode_attester_public_key + } section } diff --git a/nym-node/src/config/gateway_tasks.rs b/nym-node/src/config/gateway_tasks.rs index 16517b77bc..ca47c15538 100644 --- a/nym-node/src/config/gateway_tasks.rs +++ b/nym-node/src/config/gateway_tasks.rs @@ -244,6 +244,7 @@ pub struct UpgradeModeWatcher { #[serde(with = "bs58_ed25519_pubkey")] pub attester_public_key: ed25519::PublicKey, + #[serde(default)] pub debug: UpgradeModeWatcherDebug, } diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index edec2a80af..08b578760e 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -695,6 +695,10 @@ impl ReplayProtectionDebug { pub const DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE: usize = 200; pub fn validate(&self) -> Result<(), NymNodeError> { + if self.unsafe_disabled { + return Ok(()); + } + if self.false_positive_rate >= 1.0 || self.false_positive_rate <= 0.0 { return Err(NymNodeError::config_validation_failure( "false positive rate for replay detection can't be larger than (or equal to) 1 or smaller than (or equal to) 0", diff --git a/nym-node/src/env.rs b/nym-node/src/env.rs index 8f9a6d65b8..1564d087a4 100644 --- a/nym-node/src/env.rs +++ b/nym-node/src/env.rs @@ -63,6 +63,8 @@ pub mod vars { pub const NYMNODE_MNEMONIC_ARG: &str = "NYMNODE_MNEMONIC"; pub const NYMNODE_UPGRADE_MODE_ATTESTATION_URL_ARG: &str = "NYMNODE_UPGRADE_MODE_ATTESTATION_URL"; + pub const NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY_ARG: &str = + "NYMNODE_UPGRADE_MODE_ATTESTER_PUBKEY"; // exit gateway: pub const NYMNODE_UPSTREAM_EXIT_POLICY_ARG: &str = "NYMNODE_UPSTREAM_EXIT_POLICY"; diff --git a/nym-node/src/node/http/router/api/v1/mod.rs b/nym-node/src/node/http/router/api/v1/mod.rs index d0f2239f1d..b9a74b27bd 100644 --- a/nym-node/src/node/http/router/api/v1/mod.rs +++ b/nym-node/src/node/http/router/api/v1/mod.rs @@ -14,6 +14,7 @@ pub mod ip_packet_router; pub mod load; pub mod metrics; pub mod mixnode; +pub mod network; pub mod network_requester; pub mod node; pub mod openapi; @@ -34,6 +35,7 @@ pub(super) fn routes(config: Config) -> Router { Router::new() .route(v1::HEALTH, get(health::root_health)) .route(v1::LOAD, get(load::root_load)) + .nest(v1::NETWORK, network::routes()) .nest(v1::METRICS, metrics::routes(config.metrics)) .nest(v1::BRIDGES, bridges::routes(config.bridges)) .nest(v1::GATEWAY, gateway::routes(config.gateway)) diff --git a/nym-node/src/node/http/router/api/v1/network/mod.rs b/nym-node/src/node/http/router/api/v1/network/mod.rs new file mode 100644 index 0000000000..2d619eba3c --- /dev/null +++ b/nym-node/src/node/http/router/api/v1/network/mod.rs @@ -0,0 +1,14 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::http::api::v1::network::upgrade_mode::upgrade_mode_status; +use crate::node::http::state::AppState; +use axum::Router; +use axum::routing::get; +use nym_node_requests::routes::api::v1::network; + +pub mod upgrade_mode; + +pub(crate) fn routes() -> Router { + Router::new().route(network::UPGRADE_MODE_STATUS, get(upgrade_mode_status)) +} diff --git a/nym-node/src/node/http/router/api/v1/network/upgrade_mode.rs b/nym-node/src/node/http/router/api/v1/network/upgrade_mode.rs new file mode 100644 index 0000000000..f487c9a4fe --- /dev/null +++ b/nym-node/src/node/http/router/api/v1/network/upgrade_mode.rs @@ -0,0 +1,39 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node::http::state::AppState; +use axum::extract::{Query, State}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use nym_node_requests::api::v1::network::models::UpgradeModeStatus; + +/// Returns current upgrade mode information as perceived by this node. +#[utoipa::path( + get, + path = "/upgrade-mode-status", + context_path = "/api/v1/network", + tag = "Network", + responses( + (status = 200, content( + (UpgradeModeStatus = "application/json"), + (UpgradeModeStatus = "application/yaml"), + (UpgradeModeStatus = "application/bincode") + )) + ), + params(OutputParams) +)] +pub(crate) async fn upgrade_mode_status( + Query(output): Query, + State(state): State, +) -> UpgradeModeStatusResponse { + let output = output.get_output(); + let um_state = &state.upgrade_mode_state.node_state; + output.to_response(UpgradeModeStatus { + enabled: um_state.upgrade_mode_enabled(), + last_queried: um_state.last_queried(), + attestation_provider: state.upgrade_mode_state.attestation_url.clone(), + attester_pubkey: um_state.attester_pubkey(), + published_attestation: um_state.attestation().await, + }) +} + +pub type UpgradeModeStatusResponse = FormattedResponse; diff --git a/nym-node/src/node/http/router/api/v1/openapi.rs b/nym-node/src/node/http/router/api/v1/openapi.rs index 98835e4c59..ac410aba5f 100644 --- a/nym-node/src/node/http/router/api/v1/openapi.rs +++ b/nym-node/src/node/http/router/api/v1/openapi.rs @@ -1,7 +1,6 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::http::router::api; use axum::Router; use nym_node_requests::api as api_requests; use nym_node_requests::routes::api::{v1, v1_absolute}; @@ -13,25 +12,31 @@ use utoipa_swagger_ui::SwaggerUi; #[openapi( info(title = "NymNode API"), paths( - api::v1::node::build_information::build_information, - api::v1::node::host_information::host_information, - api::v1::node::roles::roles, - api::v1::node::hardware::host_system, - api::v1::node::description::description, - api::v1::node::auxiliary::auxiliary, - api::v1::metrics::legacy_mixing::legacy_mixing_stats, - api::v1::metrics::packets_stats::packets_stats, - api::v1::metrics::verloc::verloc_stats, - api::v1::metrics::prometheus::prometheus_metrics, - api::v1::health::root_health, - api::v1::load::root_load, - api::v1::gateway::root::root_gateway, - api::v1::gateway::client_interfaces::client_interfaces, - api::v1::gateway::client_interfaces::mixnet_websockets, - api::v1::mixnode::root::root_mixnode, - api::v1::network_requester::root::root_network_requester, - api::v1::network_requester::exit_policy::node_exit_policy, - api::v1::ip_packet_router::root::root_ip_packet_router, + crate::node::http::router::api::v1::metrics::verloc::verloc_stats, + crate::node::http::router::api::v1::metrics::legacy_mixing::legacy_mixing_stats, + crate::node::http::router::api::v1::metrics::wireguard::wireguard_stats, + crate::node::http::router::api::v1::metrics::sessions::sessions_stats, + crate::node::http::router::api::v1::metrics::packets_stats::packets_stats, + crate::node::http::router::api::v1::metrics::prometheus::prometheus_metrics, + crate::node::http::router::api::v1::ip_packet_router::root::root_ip_packet_router, + crate::node::http::router::api::v1::health::root_health, + crate::node::http::router::api::v1::network::upgrade_mode::upgrade_mode_status, + crate::node::http::router::api::v1::mixnode::root::root_mixnode, + crate::node::http::router::api::v1::load::root_load, + crate::node::http::router::api::v1::node::host_information::host_information, + crate::node::http::router::api::v1::node::description::description, + crate::node::http::router::api::v1::node::roles::roles, + crate::node::http::router::api::v1::node::auxiliary::auxiliary, + crate::node::http::router::api::v1::node::build_information::build_information, + crate::node::http::router::api::v1::node::hardware::host_system, + crate::node::http::router::api::v1::authenticator::root::root_authenticator, + crate::node::http::router::api::v1::network_requester::exit_policy::node_exit_policy, + crate::node::http::router::api::v1::network_requester::root::root_network_requester, + crate::node::http::router::api::v1::gateway::client_interfaces::client_interfaces, + crate::node::http::router::api::v1::gateway::client_interfaces::mixnet_websockets, + crate::node::http::router::api::v1::gateway::client_interfaces::wireguard_details, + crate::node::http::router::api::v1::gateway::root::root_gateway, + ), components( schemas( diff --git a/nym-node/src/node/http/state/mod.rs b/nym-node/src/node/http/state/mod.rs index 9cd3348cb6..8ed756aa60 100644 --- a/nym-node/src/node/http/state/mod.rs +++ b/nym-node/src/node/http/state/mod.rs @@ -4,6 +4,7 @@ use crate::node::http::state::load::CachedNodeLoad; use crate::node::http::state::metrics::MetricsAppState; use crate::node::key_rotation::active_keys::ActiveSphinxKeys; +use nym_credential_verification::UpgradeModeState; use nym_crypto::asymmetric::ed25519; use nym_node_metrics::NymNodeMetrics; use nym_noise_keys::VersionedNoiseKey; @@ -12,6 +13,7 @@ use std::net::IpAddr; use std::sync::Arc; use std::time::Duration; use tokio::time::Instant; +use url::Url; pub mod load; pub mod metrics; @@ -23,6 +25,12 @@ pub(crate) struct StaticNodeInformation { pub(crate) hostname: Option, } +#[derive(Clone)] +pub(crate) struct UpgradeModeApiState { + pub(crate) node_state: UpgradeModeState, + pub(crate) attestation_url: Url, +} + #[derive(Clone)] pub(crate) struct AppState { pub(crate) startup_time: Instant, @@ -34,15 +42,18 @@ pub(crate) struct AppState { pub(crate) cached_load: CachedNodeLoad, pub(crate) metrics: MetricsAppState, + + pub(crate) upgrade_mode_state: UpgradeModeApiState, } impl AppState { - #[allow(clippy::new_without_default)] pub(crate) fn new( static_information: StaticNodeInformation, x25519_sphinx_keys: ActiveSphinxKeys, metrics: NymNodeMetrics, verloc: SharedVerlocStats, + upgrade_mode_attestation_url: Url, + upgrade_mode_state: UpgradeModeState, load_cache_ttl: Duration, ) -> Self { AppState { @@ -56,6 +67,10 @@ impl AppState { startup_time: Instant::now(), cached_load: CachedNodeLoad::new(load_cache_ttl), metrics: MetricsAppState { metrics, verloc }, + upgrade_mode_state: UpgradeModeApiState { + node_state: upgrade_mode_state, + attestation_url: upgrade_mode_attestation_url, + }, } } } diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index c0f94eb6da..8ba968ca29 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -39,6 +39,7 @@ use crate::node::shared_network::{ CachedNetwork, CachedTopologyProvider, LocalGatewayNode, NetworkRefresher, }; use nym_bin_common::bin_info; +use nym_credential_verification::UpgradeModeState; use nym_crypto::asymmetric::{ed25519, x25519}; use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder, UpgradeModeCheckRequestSender}; use nym_mixnet_client::client::ActiveConnections; @@ -373,6 +374,8 @@ pub(crate) struct NymNode { entry_gateway: GatewayTasksData, + upgrade_mode_state: UpgradeModeState, + #[allow(dead_code)] service_providers: ServiceProvidersData, @@ -461,6 +464,9 @@ impl NymNode { metrics: NymNodeMetrics::new(), verloc_stats: Default::default(), entry_gateway: GatewayTasksData::new(&config.gateway_tasks).await?, + upgrade_mode_state: UpgradeModeState::new( + config.gateway_tasks.upgrade_mode.attester_public_key, + ), service_providers: ServiceProvidersData::new(&config.service_providers)?, wireguard: Some(wireguard_data), config, @@ -625,7 +631,7 @@ impl NymNode { self.metrics.clone(), self.entry_gateway.mnemonic.clone(), Self::user_agent(), - self.config.gateway_tasks.upgrade_mode.attester_public_key, + self.upgrade_mode_state.clone(), self.shutdown_tracker().clone(), ); @@ -861,6 +867,12 @@ impl NymNode { self.active_sphinx_keys()?.clone(), self.metrics.clone(), self.verloc_stats.clone(), + self.config + .gateway_tasks + .upgrade_mode + .attestation_url + .clone(), + self.upgrade_mode_state.clone(), self.config.http.node_load_cache_ttl, ); @@ -1152,7 +1164,7 @@ impl NymNode { } pub(crate) async fn run_minimal_mixnet_processing(mut self) -> Result<(), NymNodeError> { - let noise_config = nym_noise::config::NoiseConfig::new( + let noise_config = NoiseConfig::new( self.x25519_noise_keys.clone(), NoiseNetworkView::new_empty(), self.config.mixnet.debug.initial_connection_timeout, diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 120e1cc969..4f910e1a15 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -4466,13 +4466,13 @@ version = "0.1.0" dependencies = [ "async-trait", "celes", - "humantime 2.2.0", "humantime-serde", "nym-bin-common", "nym-crypto", "nym-exit-policy", "nym-http-api-client", "nym-noise-keys", + "nym-upgrade-mode-check", "nym-wireguard-types", "schemars", "serde", @@ -4481,6 +4481,7 @@ dependencies = [ "strum_macros", "thiserror 2.0.12", "time", + "url", "utoipa", ] @@ -4606,6 +4607,7 @@ dependencies = [ "thiserror 2.0.12", "time", "tracing", + "utoipa", ] [[package]] diff --git a/tools/echo-server/Cargo.toml b/tools/echo-server/Cargo.toml index 390f746bcc..f58a0e46b3 100644 --- a/tools/echo-server/Cargo.toml +++ b/tools/echo-server/Cargo.toml @@ -26,7 +26,7 @@ serde = { workspace = true, features = ["derive"] } tracing.workspace = true tracing-subscriber = { workspace = true } bytecodec = { workspace = true } -nym-sdk = { path = "../../sdk/rust/nym-sdk/" } +nym-sdk = { path = "../../sdk/rust/nym-sdk" } bytes.workspace = true dirs.workspace = true clap.workspace = true diff --git a/tools/internal/testnet-manager/src/manager/local_client.rs b/tools/internal/testnet-manager/src/manager/local_client.rs index 8e3aff09c0..3969bfea33 100644 --- a/tools/internal/testnet-manager/src/manager/local_client.rs +++ b/tools/internal/testnet-manager/src/manager/local_client.rs @@ -213,12 +213,14 @@ impl NetworkManager { id, "--enabled-credentials-mode", "true", + "--minimum-gateway-performance", + "0", "--port", &port.to_string(), ]) - .stdout(Stdio::null()) + // .stdout(Stdio::null()) .stdin(Stdio::null()) - .stderr(Stdio::null()) + // .stderr(Stdio::null()) .kill_on_drop(true); if let Some(gateway) = &ctx.gateway { @@ -243,10 +245,10 @@ impl NetworkManager { config_file, r#" -[debug.topology] -minimum_mixnode_performance = 0 -minimum_gateway_performance = 0 -"# + [debug.topology] + minimum_mixnode_performance = 0 + minimum_gateway_performance = 0 + "# )?; ctx.println(format!("\t✅client {id} is ready to use!")); diff --git a/tools/internal/testnet-manager/src/manager/local_nodes.rs b/tools/internal/testnet-manager/src/manager/local_nodes.rs index f710939248..3ed0af70b3 100644 --- a/tools/internal/testnet-manager/src/manager/local_nodes.rs +++ b/tools/internal/testnet-manager/src/manager/local_nodes.rs @@ -577,7 +577,7 @@ impl NetworkManager { )); let id = ctx.nym_node_id(mixnode); cmds.push(format!( - "{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise" + "{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise --unsafe-disable-replay-protection" )); } @@ -588,7 +588,7 @@ impl NetworkManager { )); let id = ctx.nym_node_id(gateway); cmds.push(format!( - "{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise" + "{bin_canon_display} -c {env_canon_display} run --id {id} --local --unsafe-disable-noise --unsafe-disable-replay-protection" )); }