Compare commits

...

7 Commits

Author SHA1 Message Date
Bogdan-Ștefan Neacşu 407725f697 Introduce event backchannel (#6119)
* Introduce even backchannel

* Rust fmt

* Rename Event to MixnetClientEvent

* Use unbounded_send for events

* Remove unused file

* Remove mut borrow

* Event hierarchy and mixnet client intermediary

* Export MixTrafficEvent in sdk
2025-10-17 08:47:43 +03:00
Simon Wicky 51779c06a4 Registration Client (#6059)
* removing wg-gateway-client

* bandwidth_provider trait

* authenticator client

* adapt ip-packet-client

* nit

* registration_client

* accomodate new shutdown and bugfix

* sdk changes

* cleanup and shutdown management

* remove credential mode

* error cleanup

* better error handling

* removing useless cover traffic delay

* wasm client stuff

* cfg unix

* more wasm stuff

* change authenticator client to not be blocked by mixnet client
2025-09-30 15:50:04 +02:00
import this 5cc650e901 Feature: Ping probe all nodes /described from a server (#6074)
* initialise test-nodes-pings.sh

* add retry
2025-09-30 13:30:54 +00:00
Simon Wicky a7ec178c9f [Stats API] Add flat table to stats API (#6073)
* add flat table to stats API

* remove day column
2025-09-30 14:30:05 +02:00
Mark Sinclair a0e37e78e2 Node Status API: add bridge information to dVPN endpoint (#6069)
* ns api: add node scraper for bridge information and add to dVPN gateway output

* extra error reporting

* run sqlx-prepare

* fix clippy

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
Co-authored-by: benedettadavico <benedetta.davico@gmail.com>
2025-09-29 16:27:10 +01:00
Jędrzej Stuczyński b3d02e3ba7 feat: NS ticket faucet (#6047)
* ns-api: remove sqlite support

ns-api: add env var to skip migrations for local dev

ns-api: tidy up imports

ns-api: fix deserialisation fo node descriptions

update dockerfile

update README

fix up README and example env

ns-api: bump major version to 4

ns-api: add more geoip data and new performance field in dvpn responses

* ability to import partial ticketbooks

* wip: adding common ecash state to NS API

* buffering ticketbooks

* wip

* distribute tickets when getting testrun assignment

* passing ticketbook data to gateway probe

* wrapped around storage tx

* ticketbook query fixes

* clippy

* modified testrun assignment to always return tickets

* Update version

* Update push-node-status-agent.yaml

* Update Cargo.toml

* add entrypoint for ns agents

* sqlx prepare and cargo fmt

* clippy fixes

* Update ci-check-ns-api-version.yml

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
Co-authored-by: benedetta davico <46782255+benedettadavico@users.noreply.github.com>
Co-authored-by: benedettadavico <benedetta.davico@gmail.com>
2025-09-29 14:53:15 +01:00
Mark Sinclair f5b5177073 Update push-node-status-api.yaml 2025-09-26 20:14:30 +01:00
157 changed files with 6594 additions and 3065 deletions
@@ -10,7 +10,7 @@ env:
jobs:
check-if-tag-exists:
runs-on: arc-ubuntu-22.04-dind
runs-on: arc-linux-latest-dind
steps:
- name: Checkout repo
uses: actions/checkout@v4
@@ -20,7 +20,7 @@ env:
jobs:
build-container:
runs-on: arc-ubuntu-22.04-dind
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
+1 -1
View File
@@ -14,7 +14,7 @@ env:
jobs:
build-container:
runs-on: arc-ubuntu-22.04-dind
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
Generated
+87 -158
View File
@@ -563,7 +563,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
dependencies = [
"async-trait",
"axum-core 0.4.5",
"axum-core",
"axum-macros",
"bytes",
"futures-util",
@@ -573,41 +573,7 @@ dependencies = [
"hyper 1.6.0",
"hyper-util",
"itoa",
"matchit 0.7.3",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper 1.0.2",
"tokio",
"tower 0.5.2",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "021e862c184ae977658b36c4500f7feac3221ca5da43e3f25bd04ab6c79a29b5"
dependencies = [
"axum-core 0.5.2",
"bytes",
"form_urlencoded",
"futures-util",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"hyper 1.6.0",
"hyper-util",
"itoa",
"matchit 0.8.4",
"matchit",
"memchr",
"mime",
"percent-encoding",
@@ -631,7 +597,7 @@ version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9eefda7e2b27e1bda4d6fa8a06b50803b8793769045918bc37ad062d48a6efac"
dependencies = [
"axum 0.7.9",
"axum",
"forwarded-header-value",
"serde",
]
@@ -657,34 +623,14 @@ dependencies = [
"tracing",
]
[[package]]
name = "axum-core"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68464cd0412f486726fb3373129ef5d2993f90c34bc2bc1c1e9943b2f4fc7ca6"
dependencies = [
"bytes",
"futures-core",
"http 1.3.1",
"http-body 1.0.1",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper 1.0.2",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-extra"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c794b30c904f0a1c2fb7740f7df7f7972dfaa14ef6f57cb6178dc63e5dca2f04"
dependencies = [
"axum 0.7.9",
"axum-core 0.4.5",
"axum",
"axum-core",
"bytes",
"fastrand 2.3.0",
"futures-util",
@@ -721,9 +667,9 @@ dependencies = [
"anyhow",
"assert-json-diff",
"auto-future",
"axum 0.7.9",
"axum",
"bytes",
"bytesize 1.3.3",
"bytesize",
"cookie",
"http 1.3.1",
"http-body-util",
@@ -732,37 +678,7 @@ dependencies = [
"mime",
"pretty_assertions",
"reserve-port",
"rust-multipart-rfc7578_2 0.6.1",
"serde",
"serde_json",
"serde_urlencoded",
"smallvec",
"tokio",
"tower 0.5.2",
"url",
]
[[package]]
name = "axum-test"
version = "17.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eb1dfb84bd48bad8e4aa1acb82ed24c2bb5e855b659959b4e03b4dca118fcac"
dependencies = [
"anyhow",
"assert-json-diff",
"auto-future",
"axum 0.8.4",
"bytes",
"bytesize 2.0.1",
"cookie",
"http 1.3.1",
"http-body-util",
"hyper 1.6.0",
"hyper-util",
"mime",
"pretty_assertions",
"reserve-port",
"rust-multipart-rfc7578_2 0.8.0",
"rust-multipart-rfc7578_2",
"serde",
"serde_json",
"serde_urlencoded",
@@ -1106,12 +1022,6 @@ version = "1.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e93abca9e28e0a1b9877922aacb20576e05d4679ffa78c3d6dc22a26a216659"
[[package]]
name = "bytesize"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3c8f83209414aacf0eeae3cf730b18d6981697fba62f200fcfb92b9f082acba"
[[package]]
name = "camino"
version = "1.1.10"
@@ -4446,12 +4356,6 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "md-5"
version = "0.10.6"
@@ -4887,8 +4791,8 @@ version = "1.1.66"
dependencies = [
"anyhow",
"async-trait",
"axum 0.7.9",
"axum-test 16.4.1",
"axum",
"axum-test",
"bincode",
"bip39",
"bs58",
@@ -4908,6 +4812,7 @@ dependencies = [
"nym-api-requests",
"nym-bandwidth-controller",
"nym-bin-common",
"nym-cache",
"nym-coconut-dkg-common",
"nym-compact-ecash",
"nym-config",
@@ -5018,11 +4923,16 @@ dependencies = [
"bincode",
"futures",
"nym-authenticator-requests",
"nym-bandwidth-controller",
"nym-credentials-interface",
"nym-crypto",
"nym-pemstore",
"nym-registration-common",
"nym-sdk",
"nym-service-provider-requests-common",
"nym-validator-client",
"nym-wireguard-types",
"rand 0.8.5",
"semver 1.0.26",
"thiserror 2.0.12",
"tokio",
@@ -5044,8 +4954,10 @@ dependencies = [
"nym-sphinx",
"nym-wireguard-types",
"rand 0.8.5",
"semver 1.0.26",
"serde",
"sha2 0.10.9",
"strum_macros",
"thiserror 2.0.12",
"x25519-dalek",
]
@@ -5054,6 +4966,7 @@ dependencies = [
name = "nym-bandwidth-controller"
version = "0.1.0"
dependencies = [
"async-trait",
"bip39",
"log",
"nym-credential-storage",
@@ -5093,6 +5006,13 @@ dependencies = [
"vergen",
]
[[package]]
name = "nym-cache"
version = "0.1.0"
dependencies = [
"tokio",
]
[[package]]
name = "nym-cli"
version = "1.1.63"
@@ -5457,7 +5377,7 @@ name = "nym-credential-proxy"
version = "0.2.0"
dependencies = [
"anyhow",
"axum 0.7.9",
"axum",
"bip39",
"bs58",
"cfg-if",
@@ -5503,11 +5423,12 @@ name = "nym-credential-proxy-lib"
version = "0.1.0"
dependencies = [
"anyhow",
"axum 0.7.9",
"axum",
"bip39",
"bs58",
"futures",
"humantime",
"nym-cache",
"nym-compact-ecash",
"nym-credential-proxy-requests",
"nym-credentials",
@@ -5567,7 +5488,10 @@ dependencies = [
"log",
"nym-compact-ecash",
"nym-credentials",
"nym-credentials-interface",
"nym-crypto",
"nym-ecash-time",
"nym-test-utils",
"serde",
"sqlx",
"sqlx-pool-guard",
@@ -6034,7 +5958,7 @@ dependencies = [
name = "nym-http-api-common"
version = "0.1.0"
dependencies = [
"axum 0.7.9",
"axum",
"axum-client-ip",
"bincode",
"bytes",
@@ -6091,6 +6015,7 @@ dependencies = [
name = "nym-ip-packet-client"
version = "0.1.0"
dependencies = [
"bincode",
"bytes",
"futures",
"nym-ip-packet-requests",
@@ -6284,7 +6209,7 @@ name = "nym-network-monitor"
version = "1.0.2"
dependencies = [
"anyhow",
"axum 0.7.9",
"axum",
"clap",
"dashmap",
"futures",
@@ -6372,7 +6297,7 @@ dependencies = [
"arc-swap",
"arrayref",
"async-trait",
"axum 0.7.9",
"axum",
"bip39",
"blake2 0.8.1",
"bloomfilter",
@@ -6491,7 +6416,7 @@ dependencies = [
[[package]]
name = "nym-node-status-agent"
version = "1.0.6"
version = "1.0.7"
dependencies = [
"anyhow",
"clap",
@@ -6508,13 +6433,14 @@ dependencies = [
[[package]]
name = "nym-node-status-api"
version = "4.0.3"
version = "4.0.5"
dependencies = [
"ammonia",
"anyhow",
"axum 0.7.9",
"axum-test 17.3.0",
"axum",
"axum-test",
"bip39",
"bs58",
"celes",
"clap",
"cosmwasm-std",
@@ -6524,7 +6450,10 @@ dependencies = [
"moka",
"nym-bin-common",
"nym-contracts-common",
"nym-credential-proxy-lib",
"nym-credentials",
"nym-crypto",
"nym-ecash-time",
"nym-http-api-client",
"nym-http-api-common",
"nym-mixnet-contract-common",
@@ -6550,6 +6479,7 @@ dependencies = [
"thiserror 2.0.12",
"time",
"tokio",
"tokio-stream",
"tokio-util",
"tower-http 0.5.2",
"tracing",
@@ -6558,6 +6488,7 @@ dependencies = [
"utoipa",
"utoipa-swagger-ui",
"utoipauto",
"zeroize",
]
[[package]]
@@ -6566,6 +6497,8 @@ version = "0.2.0"
dependencies = [
"anyhow",
"bincode",
"bs58",
"nym-credentials",
"nym-crypto",
"reqwest 0.12.22",
"serde",
@@ -6735,6 +6668,37 @@ dependencies = [
"time",
]
[[package]]
name = "nym-registration-client"
version = "0.1.0"
dependencies = [
"futures",
"nym-authenticator-client",
"nym-bandwidth-controller",
"nym-credential-storage",
"nym-credentials-interface",
"nym-ip-packet-client",
"nym-registration-common",
"nym-sdk",
"nym-validator-client",
"thiserror 2.0.12",
"tokio",
"tokio-util",
"tracing",
"url",
]
[[package]]
name = "nym-registration-common"
version = "0.1.0"
dependencies = [
"nym-authenticator-requests",
"nym-crypto",
"nym-ip-packet-requests",
"nym-sphinx",
"tokio-util",
]
[[package]]
name = "nym-sdk"
version = "0.1.0"
@@ -7112,10 +7076,10 @@ dependencies = [
[[package]]
name = "nym-statistics-api"
version = "0.1.5"
version = "0.2.1"
dependencies = [
"anyhow",
"axum 0.7.9",
"axum",
"axum-client-ip",
"axum-extra",
"celes",
@@ -7480,26 +7444,6 @@ dependencies = [
"ts-rs",
]
[[package]]
name = "nym-wg-gateway-client"
version = "0.1.0"
dependencies = [
"nym-authenticator-client",
"nym-authenticator-requests",
"nym-bandwidth-controller",
"nym-credentials-interface",
"nym-crypto",
"nym-node-requests",
"nym-pemstore",
"nym-sdk",
"nym-statistics-common",
"nym-validator-client",
"rand 0.8.5",
"thiserror 2.0.12",
"tracing",
"url",
]
[[package]]
name = "nym-wireguard"
version = "0.1.0"
@@ -7548,7 +7492,7 @@ version = "1.0.0"
dependencies = [
"anyhow",
"async-trait",
"axum 0.7.9",
"axum",
"futures",
"nym-credential-verification",
"nym-credentials-interface",
@@ -7566,7 +7510,7 @@ dependencies = [
name = "nym-wireguard-private-metadata-shared"
version = "1.0.0"
dependencies = [
"axum 0.7.9",
"axum",
"bincode",
"nym-credentials-interface",
"schemars 0.8.22",
@@ -7580,7 +7524,7 @@ name = "nym-wireguard-private-metadata-tests"
version = "1.0.0"
dependencies = [
"async-trait",
"axum 0.7.9",
"axum",
"nym-credential-verification",
"nym-credentials-interface",
"nym-http-api-client",
@@ -7645,7 +7589,7 @@ version = "0.1.14"
dependencies = [
"anyhow",
"async-trait",
"axum 0.7.9",
"axum",
"chrono",
"clap",
"nym-bin-common",
@@ -8978,21 +8922,6 @@ dependencies = [
"thiserror 1.0.69",
]
[[package]]
name = "rust-multipart-rfc7578_2"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c839d037155ebc06a571e305af66ff9fd9063a6e662447051737e1ac75beea41"
dependencies = [
"bytes",
"futures-core",
"futures-util",
"http 1.3.1",
"mime",
"rand 0.9.2",
"thiserror 2.0.12",
]
[[package]]
name = "rustc-demangle"
version = "0.1.25"
@@ -10884,7 +10813,7 @@ checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52"
dependencies = [
"async-stream",
"async-trait",
"axum 0.7.9",
"axum",
"base64 0.22.1",
"bytes",
"h2 0.4.11",
@@ -11558,7 +11487,7 @@ version = "8.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db4b5ac679cc6dfc5ea3f2823b0291c777750ffd5e13b21137e0f7ac0e8f9617"
dependencies = [
"axum 0.7.9",
"axum",
"base64 0.22.1",
"mime_guess",
"regex",
+7 -5
View File
@@ -66,7 +66,7 @@ members = [
"common/mixnode-common",
"common/network-defaults",
"common/node-tester-utils",
"common/nonexhaustive-delayqueue",
"common/nonexhaustive-delayqueue", "common/nym-cache",
"common/nym-id",
"common/nym-metrics",
"common/nym_offline_compact_ecash",
@@ -85,6 +85,7 @@ members = [
"common/nymsphinx/types",
"common/nyxd-scraper",
"common/pemstore",
"common/registration",
"common/serde-helpers",
"common/service-provider-requests-common",
"common/socks5-client-core",
@@ -92,7 +93,8 @@ members = [
"common/socks5/requests",
"common/statistics",
"common/store-cipher",
"common/task", "common/test-utils",
"common/task",
"common/test-utils",
"common/ticketbooks-merkle",
"common/topology",
"common/tun",
@@ -125,10 +127,11 @@ members = [
"nym-node-status-api/nym-node-status-client",
"nym-node/nym-node-metrics",
"nym-node/nym-node-requests",
"nym-outfox", "nym-signers-monitor",
"nym-outfox",
"nym-registration-client",
"nym-signers-monitor",
"nym-statistics-api",
"nym-validator-rewarder",
"nym-wg-gateway-client",
"nyx-chain-watcher",
"sdk/ffi/cpp",
"sdk/ffi/go",
@@ -145,7 +148,6 @@ members = [
# "tools/internal/sdk-version-bump",
"tools/internal/ssl-inject",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
"tools/internal/validator-status-check",
"tools/nym-cli",
+2
View File
@@ -13,6 +13,8 @@ base64 = { workspace = true }
bincode = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
semver = { workspace = true }
strum_macros = { workspace = true }
thiserror = { workspace = true }
nym-credentials-interface = { path = "../credentials-interface" }
@@ -0,0 +1,272 @@
// Copyright 2025 Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_sphinx::addressing::Recipient;
use nym_wireguard_types::PeerPublicKey;
use crate::{
latest::registration::IpPair,
traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, Versionable},
v2, v3, v4, v5, AuthenticatorVersion, Error,
};
// This is very redundant with AuthenticatorRequest and I reckon they could be smooshed.
// It is a bit out of scope for me at the moment though
#[derive(Debug)]
pub enum ClientMessage {
Initial(Box<dyn InitMessage + Send + Sync + 'static>),
Final(Box<dyn FinalMessage + Send + Sync + 'static>),
Query(Box<dyn QueryBandwidthMessage + Send + Sync + 'static>),
TopUp(Box<dyn TopUpMessage + Send + Sync + 'static>),
}
impl ClientMessage {
// check if message is wasteful e.g. contains a credential
pub fn is_wasteful(&self) -> bool {
match self {
Self::Final(msg) => msg.credential().is_some(),
Self::TopUp(_) => true,
Self::Initial(_) | Self::Query(_) => false,
}
}
fn version(&self) -> AuthenticatorVersion {
match self {
ClientMessage::Initial(msg) => msg.version(),
ClientMessage::Final(msg) => msg.version(),
ClientMessage::Query(msg) => msg.version(),
ClientMessage::TopUp(msg) => msg.version(),
}
}
pub fn bytes(&self, reply_to: Recipient) -> Result<(Vec<u8>, u64), Error> {
match self.version() {
AuthenticatorVersion::V1 => Err(Error::UnsupportedVersion),
AuthenticatorVersion::V2 => {
use v2::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ip: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?
.into(),
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) = AuthenticatorRequest::new_query_request(
query_message.pub_key(),
reply_to,
);
Ok((req.to_bytes()?, id))
}
_ => Err(Error::UnsupportedMessage),
}
}
AuthenticatorVersion::V3 => {
use v3::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ip: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?
.into(),
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) = AuthenticatorRequest::new_query_request(
query_message.pub_key(),
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(
TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
}
}
AuthenticatorVersion::V4 => {
use v4::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ips: IpPair {
ipv4: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?,
ipv6: final_message
.gateway_client_ipv6()
.ok_or(Error::UnsupportedMessage)?,
}
.into(),
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) = AuthenticatorRequest::new_query_request(
query_message.pub_key(),
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(
TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
}
}
AuthenticatorVersion::V5 => {
use v5::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage {
pub_key: init_message.pub_key(),
});
Ok((req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ips: IpPair {
ipv4: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?,
ipv6: final_message
.gateway_client_ipv6()
.ok_or(Error::UnsupportedMessage)?,
},
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
});
Ok((req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) =
AuthenticatorRequest::new_query_request(query_message.pub_key());
Ok((req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
});
Ok((req.to_bytes()?, id))
}
}
}
AuthenticatorVersion::UNKNOWN => Err(Error::UnknownVersion),
}
}
pub fn use_surbs(&self) -> bool {
use AuthenticatorVersion::*;
match self.version() {
V1 | V2 | V3 | V4 => false,
V5 => true,
UNKNOWN => true,
}
}
}
// Same comment as above struct
#[derive(Debug)]
pub struct QueryMessageImpl {
pub pub_key: PeerPublicKey,
pub version: AuthenticatorVersion,
}
impl Versionable for QueryMessageImpl {
fn version(&self) -> AuthenticatorVersion {
self.version
}
}
impl QueryBandwidthMessage for QueryMessageImpl {
fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
}
+13 -2
View File
@@ -23,6 +23,17 @@ pub enum Error {
#[error("conversion: {0}")]
Conversion(String),
#[error("failed to serialize response packet: {source}")]
FailedToSerializeResponsePacket { source: Box<bincode::ErrorKind> },
// TODO add version number for debugging
#[error("unknown version number")]
UnknownVersion,
// TODO add version number for debugging
#[error("unsupported request version")]
UnsupportedVersion,
#[error("gateway doesn't support this type of message")]
UnsupportedMessage,
#[error(transparent)]
Bincode(#[from] bincode::Error),
}
+6 -1
View File
@@ -1,6 +1,9 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod client_message;
pub mod request;
pub mod response;
pub mod traits;
pub mod v1;
pub mod v2;
@@ -10,11 +13,13 @@ pub mod v5;
mod error;
mod util;
mod version;
pub use error::Error;
pub use v5 as latest;
pub use version::AuthenticatorVersion;
pub const CURRENT_VERSION: u8 = 5;
pub const CURRENT_VERSION: u8 = latest::VERSION;
fn make_bincode_serializer() -> impl bincode::Options {
use bincode::Options;
@@ -0,0 +1,204 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use nym_sphinx::addressing::Recipient;
use crate::traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage};
use crate::{v1, v2, v3, v4, v5};
#[derive(Debug)]
pub enum AuthenticatorRequest {
Initial {
msg: Box<dyn InitMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Option<Recipient>,
request_id: u64,
},
Final {
msg: Box<dyn FinalMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Option<Recipient>,
request_id: u64,
},
QueryBandwidth {
msg: Box<dyn QueryBandwidthMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Option<Recipient>,
request_id: u64,
},
TopUpBandwidth {
msg: Box<dyn TopUpMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Option<Recipient>,
request_id: u64,
},
}
impl From<v1::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v1::request::AuthenticatorRequest) -> Self {
match value.data {
v1::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: Protocol {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v1::request::AuthenticatorRequestData::Final(gateway_client) => Self::Final {
msg: Box::new(gateway_client),
protocol: Protocol {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v1::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: Protocol {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
}
}
impl From<v2::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v2::request::AuthenticatorRequest) -> Self {
match value.data {
v2::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v2::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v2::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
}
}
impl From<v3::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v3::request::AuthenticatorRequest) -> Self {
match value.data {
v3::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v3::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v3::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
v3::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
}
}
impl From<v4::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v4::request::AuthenticatorRequest) -> Self {
match value.data {
v4::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v4::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v4::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
v4::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
}
}
impl From<v5::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v5::request::AuthenticatorRequest) -> Self {
match value.data {
v5::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
},
v5::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
},
v5::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
}
}
v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
}
}
}
}
}
@@ -0,0 +1,106 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::traits::{
Id, PendingRegistrationResponse, RegisteredResponse, RemainingBandwidthResponse,
TopUpBandwidthResponse,
};
use crate::{v2, v3, v4, v5};
#[derive(Debug)]
pub enum AuthenticatorResponse {
PendingRegistration(Box<dyn PendingRegistrationResponse + Send + Sync + 'static>),
Registered(Box<dyn RegisteredResponse + Send + Sync + 'static>),
RemainingBandwidth(Box<dyn RemainingBandwidthResponse + Send + Sync + 'static>),
TopUpBandwidth(Box<dyn TopUpBandwidthResponse + Send + Sync + 'static>),
}
impl Id for AuthenticatorResponse {
fn id(&self) -> u64 {
match self {
AuthenticatorResponse::PendingRegistration(pending_registration_response) => {
pending_registration_response.id()
}
AuthenticatorResponse::Registered(registered_response) => registered_response.id(),
AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => {
remaining_bandwidth_response.id()
}
AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => {
top_up_bandwidth_response.id()
}
}
}
}
impl From<v2::response::AuthenticatorResponse> for AuthenticatorResponse {
fn from(value: v2::response::AuthenticatorResponse) -> Self {
match value.data {
v2::response::AuthenticatorResponseData::PendingRegistration(
pending_registration_response,
) => Self::PendingRegistration(Box::new(pending_registration_response)),
v2::response::AuthenticatorResponseData::Registered(registered_response) => {
Self::Registered(Box::new(registered_response))
}
v2::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response,
) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)),
}
}
}
impl From<v3::response::AuthenticatorResponse> for AuthenticatorResponse {
fn from(value: v3::response::AuthenticatorResponse) -> Self {
match value.data {
v3::response::AuthenticatorResponseData::PendingRegistration(
pending_registration_response,
) => Self::PendingRegistration(Box::new(pending_registration_response)),
v3::response::AuthenticatorResponseData::Registered(registered_response) => {
Self::Registered(Box::new(registered_response))
}
v3::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response,
) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)),
v3::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => {
Self::TopUpBandwidth(Box::new(top_up_bandwidth_response))
}
}
}
}
impl From<v4::response::AuthenticatorResponse> for AuthenticatorResponse {
fn from(value: v4::response::AuthenticatorResponse) -> Self {
match value.data {
v4::response::AuthenticatorResponseData::PendingRegistration(
pending_registration_response,
) => Self::PendingRegistration(Box::new(pending_registration_response)),
v4::response::AuthenticatorResponseData::Registered(registered_response) => {
Self::Registered(Box::new(registered_response))
}
v4::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response,
) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)),
v4::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => {
Self::TopUpBandwidth(Box::new(top_up_bandwidth_response))
}
}
}
}
impl From<v5::response::AuthenticatorResponse> for AuthenticatorResponse {
fn from(value: v5::response::AuthenticatorResponse) -> Self {
match value.data {
v5::response::AuthenticatorResponseData::PendingRegistration(
pending_registration_response,
) => Self::PendingRegistration(Box::new(pending_registration_response)),
v5::response::AuthenticatorResponseData::Registered(registered_response) => {
Self::Registered(Box::new(registered_response))
}
v5::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response,
) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)),
v5::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => {
Self::TopUpBandwidth(Box::new(top_up_bandwidth_response))
}
}
}
}
+437 -220
View File
@@ -1,49 +1,105 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::fmt;
use std::net::{Ipv4Addr, Ipv6Addr};
use nym_credentials_interface::CredentialSpendingData;
use nym_crypto::asymmetric::x25519::PrivateKey;
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use nym_sphinx::addressing::clients::Recipient;
use nym_wireguard_types::PeerPublicKey;
use crate::{
v1, v2, v3, v4,
v5::{self, registration::IpPair},
Error,
};
use crate::latest::registration::IpPair;
use crate::{v1, v2, v3, v4, v5, AuthenticatorVersion, Error};
#[derive(Copy, Clone, Debug)]
pub enum AuthenticatorVersion {
V1,
V2,
V3,
V4,
V5,
UNKNOWN,
pub trait Versionable {
fn version(&self) -> AuthenticatorVersion;
}
impl From<Protocol> for AuthenticatorVersion {
fn from(value: Protocol) -> Self {
if value.service_provider_type != ServiceProviderType::Authenticator {
AuthenticatorVersion::UNKNOWN
} else if value.version == v1::VERSION {
AuthenticatorVersion::V1
} else if value.version == v2::VERSION {
AuthenticatorVersion::V2
} else if value.version == v3::VERSION {
AuthenticatorVersion::V3
} else if value.version == v4::VERSION {
AuthenticatorVersion::V4
} else if value.version == v5::VERSION {
AuthenticatorVersion::V5
} else {
AuthenticatorVersion::UNKNOWN
}
impl Versionable for v1::GatewayClient {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V1
}
}
pub trait InitMessage {
impl Versionable for v1::registration::InitMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V1
}
}
impl Versionable for v2::registration::InitMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V2
}
}
impl Versionable for v3::registration::InitMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V3
}
}
impl Versionable for v4::registration::InitMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V4
}
}
impl Versionable for v5::registration::InitMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V5
}
}
impl Versionable for v2::registration::FinalMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V2
}
}
impl Versionable for v3::registration::FinalMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V3
}
}
impl Versionable for v4::registration::FinalMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V4
}
}
impl Versionable for v5::registration::FinalMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V5
}
}
impl Versionable for PeerPublicKey {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V3
}
}
impl Versionable for v3::topup::TopUpMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V3
}
}
impl Versionable for v4::topup::TopUpMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V4
}
}
impl Versionable for v5::topup::TopUpMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V5
}
}
pub trait InitMessage: Versionable + fmt::Debug {
fn pub_key(&self) -> PeerPublicKey;
}
@@ -77,15 +133,18 @@ impl InitMessage for v5::registration::InitMessage {
}
}
pub trait FinalMessage {
fn pub_key(&self) -> PeerPublicKey;
pub trait FinalMessage: Versionable + fmt::Debug {
fn gateway_client_pub_key(&self) -> PeerPublicKey;
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error>;
fn private_ips(&self) -> IpPair;
fn gateway_client_ipv4(&self) -> Option<Ipv4Addr>;
fn gateway_client_ipv6(&self) -> Option<Ipv6Addr>;
fn gateway_client_mac(&self) -> Vec<u8>;
fn credential(&self) -> Option<CredentialSpendingData>;
}
impl FinalMessage for v1::GatewayClient {
fn pub_key(&self) -> PeerPublicKey {
fn gateway_client_pub_key(&self) -> PeerPublicKey {
self.pub_key
}
@@ -97,13 +156,28 @@ impl FinalMessage for v1::GatewayClient {
self.private_ip.into()
}
fn gateway_client_ipv4(&self) -> Option<Ipv4Addr> {
match self.private_ip {
std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr),
std::net::IpAddr::V6(_) => None,
}
}
fn gateway_client_ipv6(&self) -> Option<Ipv6Addr> {
None
}
fn gateway_client_mac(&self) -> Vec<u8> {
self.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
None
}
}
impl FinalMessage for v2::registration::FinalMessage {
fn pub_key(&self) -> PeerPublicKey {
fn gateway_client_pub_key(&self) -> PeerPublicKey {
self.gateway_client.pub_key
}
@@ -115,13 +189,28 @@ impl FinalMessage for v2::registration::FinalMessage {
self.gateway_client.private_ip.into()
}
fn gateway_client_ipv4(&self) -> Option<Ipv4Addr> {
match self.gateway_client.private_ip {
std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr),
std::net::IpAddr::V6(_) => None,
}
}
fn gateway_client_ipv6(&self) -> Option<Ipv6Addr> {
None
}
fn gateway_client_mac(&self) -> Vec<u8> {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
}
}
impl FinalMessage for v3::registration::FinalMessage {
fn pub_key(&self) -> PeerPublicKey {
fn gateway_client_pub_key(&self) -> PeerPublicKey {
self.gateway_client.pub_key
}
@@ -133,13 +222,28 @@ impl FinalMessage for v3::registration::FinalMessage {
self.gateway_client.private_ip.into()
}
fn gateway_client_ipv4(&self) -> Option<Ipv4Addr> {
match self.gateway_client.private_ip {
std::net::IpAddr::V4(ipv4_addr) => Some(ipv4_addr),
std::net::IpAddr::V6(_) => None,
}
}
fn gateway_client_ipv6(&self) -> Option<Ipv6Addr> {
None
}
fn gateway_client_mac(&self) -> Vec<u8> {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
}
}
impl FinalMessage for v4::registration::FinalMessage {
fn pub_key(&self) -> PeerPublicKey {
fn gateway_client_pub_key(&self) -> PeerPublicKey {
self.gateway_client.pub_key
}
@@ -151,13 +255,25 @@ impl FinalMessage for v4::registration::FinalMessage {
self.gateway_client.private_ips.into()
}
fn gateway_client_ipv4(&self) -> Option<Ipv4Addr> {
Some(self.gateway_client.private_ips.ipv4)
}
fn gateway_client_ipv6(&self) -> Option<Ipv6Addr> {
Some(self.gateway_client.private_ips.ipv6)
}
fn gateway_client_mac(&self) -> Vec<u8> {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
}
}
impl FinalMessage for v5::registration::FinalMessage {
fn pub_key(&self) -> PeerPublicKey {
fn gateway_client_pub_key(&self) -> PeerPublicKey {
self.gateway_client.pub_key
}
@@ -169,12 +285,24 @@ impl FinalMessage for v5::registration::FinalMessage {
self.gateway_client.private_ips
}
fn gateway_client_ipv4(&self) -> Option<Ipv4Addr> {
Some(self.gateway_client.private_ips.ipv4)
}
fn gateway_client_ipv6(&self) -> Option<Ipv6Addr> {
Some(self.gateway_client.private_ips.ipv6)
}
fn gateway_client_mac(&self) -> Vec<u8> {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
}
}
pub trait QueryBandwidthMessage {
pub trait QueryBandwidthMessage: Versionable + fmt::Debug {
fn pub_key(&self) -> PeerPublicKey;
}
@@ -184,7 +312,7 @@ impl QueryBandwidthMessage for PeerPublicKey {
}
}
pub trait TopUpMessage {
pub trait TopUpMessage: Versionable + fmt::Debug {
fn pub_key(&self) -> PeerPublicKey;
fn credential(&self) -> CredentialSpendingData;
}
@@ -219,197 +347,286 @@ impl TopUpMessage for v5::topup::TopUpMessage {
}
}
pub enum AuthenticatorRequest {
Initial {
msg: Box<dyn InitMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Option<Recipient>,
request_id: u64,
},
Final {
msg: Box<dyn FinalMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Option<Recipient>,
request_id: u64,
},
QueryBandwidth {
msg: Box<dyn QueryBandwidthMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Option<Recipient>,
request_id: u64,
},
TopUpBandwidth {
msg: Box<dyn TopUpMessage + Send + Sync + 'static>,
protocol: Protocol,
reply_to: Option<Recipient>,
request_id: u64,
},
pub trait Id {
fn id(&self) -> u64;
}
impl From<v1::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v1::request::AuthenticatorRequest) -> Self {
match value.data {
v1::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: Protocol {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v1::request::AuthenticatorRequestData::Final(gateway_client) => Self::Final {
msg: Box::new(gateway_client),
protocol: Protocol {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v1::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: Protocol {
version: value.version,
service_provider_type: ServiceProviderType::Authenticator,
},
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
impl Id for v2::response::PendingRegistrationResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl From<v2::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v2::request::AuthenticatorRequest) -> Self {
match value.data {
v2::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v2::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v2::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
impl Id for v3::response::PendingRegistrationResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl From<v3::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v3::request::AuthenticatorRequest) -> Self {
match value.data {
v3::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v3::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v3::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
v3::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
impl Id for v4::response::PendingRegistrationResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl From<v4::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v4::request::AuthenticatorRequest) -> Self {
match value.data {
v4::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v4::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
},
v4::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
v4::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: Some(value.reply_to),
request_id: value.request_id,
}
}
}
impl Id for v5::response::PendingRegistrationResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl From<v5::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v5::request::AuthenticatorRequest) -> Self {
match value.data {
v5::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
},
v5::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
},
v5::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
}
}
v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
}
}
}
impl Id for v2::response::RegisteredResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v3::response::RegisteredResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v4::response::RegisteredResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v5::response::RegisteredResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v2::response::RemainingBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v3::response::RemainingBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v4::response::RemainingBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v5::response::RemainingBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v3::response::TopUpBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v4::response::TopUpBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v5::response::TopUpBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
pub trait PendingRegistrationResponse: Id + fmt::Debug {
fn nonce(&self) -> u64;
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error>;
fn pub_key(&self) -> PeerPublicKey;
fn private_ips(&self) -> IpPair;
}
impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse {
fn nonce(&self) -> u64 {
self.reply.nonce
}
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.gateway_data.pub_key
}
fn private_ips(&self) -> IpPair {
self.reply.gateway_data.private_ip.into()
}
}
impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse {
fn nonce(&self) -> u64 {
self.reply.nonce
}
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.gateway_data.pub_key
}
fn private_ips(&self) -> IpPair {
self.reply.gateway_data.private_ip.into()
}
}
impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse {
fn nonce(&self) -> u64 {
self.reply.nonce
}
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.gateway_data.pub_key
}
fn private_ips(&self) -> IpPair {
self.reply.gateway_data.private_ips.into()
}
}
impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse {
fn nonce(&self) -> u64 {
self.reply.nonce
}
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.gateway_data.pub_key
}
fn private_ips(&self) -> IpPair {
self.reply.gateway_data.private_ips
}
}
pub trait RegisteredResponse: Id + fmt::Debug {
fn private_ips(&self) -> IpPair;
fn pub_key(&self) -> PeerPublicKey;
fn wg_port(&self) -> u16;
}
impl RegisteredResponse for v2::response::RegisteredResponse {
fn private_ips(&self) -> IpPair {
self.reply.private_ip.into()
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.pub_key
}
fn wg_port(&self) -> u16 {
self.reply.wg_port
}
}
impl RegisteredResponse for v3::response::RegisteredResponse {
fn private_ips(&self) -> IpPair {
self.reply.private_ip.into()
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.pub_key
}
fn wg_port(&self) -> u16 {
self.reply.wg_port
}
}
impl RegisteredResponse for v4::response::RegisteredResponse {
fn private_ips(&self) -> IpPair {
self.reply.private_ips.into()
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.pub_key
}
fn wg_port(&self) -> u16 {
self.reply.wg_port
}
}
impl RegisteredResponse for v5::response::RegisteredResponse {
fn private_ips(&self) -> IpPair {
self.reply.private_ips
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.pub_key
}
fn wg_port(&self) -> u16 {
self.reply.wg_port
}
}
pub trait RemainingBandwidthResponse: Id + fmt::Debug {
fn available_bandwidth(&self) -> Option<i64>;
}
impl RemainingBandwidthResponse for v2::response::RemainingBandwidthResponse {
fn available_bandwidth(&self) -> Option<i64> {
self.reply.as_ref().map(|r| r.available_bandwidth)
}
}
impl RemainingBandwidthResponse for v3::response::RemainingBandwidthResponse {
fn available_bandwidth(&self) -> Option<i64> {
self.reply.as_ref().map(|r| r.available_bandwidth)
}
}
impl RemainingBandwidthResponse for v4::response::RemainingBandwidthResponse {
fn available_bandwidth(&self) -> Option<i64> {
self.reply.as_ref().map(|r| r.available_bandwidth)
}
}
impl RemainingBandwidthResponse for v5::response::RemainingBandwidthResponse {
fn available_bandwidth(&self) -> Option<i64> {
self.reply.as_ref().map(|r| r.available_bandwidth)
}
}
pub trait TopUpBandwidthResponse: Id + fmt::Debug {
fn available_bandwidth(&self) -> i64;
}
impl TopUpBandwidthResponse for v3::response::TopUpBandwidthResponse {
fn available_bandwidth(&self) -> i64 {
self.reply.available_bandwidth
}
}
impl TopUpBandwidthResponse for v4::response::TopUpBandwidthResponse {
fn available_bandwidth(&self) -> i64 {
self.reply.available_bandwidth
}
}
impl TopUpBandwidthResponse for v5::response::TopUpBandwidthResponse {
fn available_bandwidth(&self) -> i64 {
self.reply.available_bandwidth
}
}
@@ -0,0 +1,191 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::{v1, v2, v3, v4, v5};
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
#[derive(Copy, Clone, Debug, PartialEq, strum_macros::Display)]
#[strum(serialize_all = "snake_case")]
pub enum AuthenticatorVersion {
/// introduced in wispa release (1.1.5)
V1,
/// introduced in aero release (1.1.9)
V2,
/// introduced in magura release (1.1.10)
V3,
/// introduced in crunch release (1.2.0)
V4,
/// introduced in dorina-patched release (1.6.1)
V5,
UNKNOWN,
}
impl AuthenticatorVersion {
pub const LATEST: Self = Self::V5;
pub const fn release_version(&self) -> semver::Version {
match self {
AuthenticatorVersion::V1 => semver::Version::new(1, 1, 5),
AuthenticatorVersion::V2 => semver::Version::new(1, 1, 9),
AuthenticatorVersion::V3 => semver::Version::new(1, 1, 10),
AuthenticatorVersion::V4 => semver::Version::new(1, 2, 0),
AuthenticatorVersion::V5 => semver::Version::new(1, 6, 1),
AuthenticatorVersion::UNKNOWN => semver::Version::new(0, 0, 0),
}
}
}
impl From<Protocol> for AuthenticatorVersion {
fn from(value: Protocol) -> Self {
if value.service_provider_type != ServiceProviderType::Authenticator {
AuthenticatorVersion::UNKNOWN
} else if value.version == v1::VERSION {
AuthenticatorVersion::V1
} else if value.version == v2::VERSION {
AuthenticatorVersion::V2
} else if value.version == v3::VERSION {
AuthenticatorVersion::V3
} else if value.version == v4::VERSION {
AuthenticatorVersion::V4
} else if value.version == v5::VERSION {
AuthenticatorVersion::V5
} else {
AuthenticatorVersion::UNKNOWN
}
}
}
impl From<u8> for AuthenticatorVersion {
fn from(value: u8) -> Self {
if value == v1::VERSION {
AuthenticatorVersion::V1
} else if value == v2::VERSION {
AuthenticatorVersion::V2
} else if value == v3::VERSION {
AuthenticatorVersion::V3
} else if value == v4::VERSION {
AuthenticatorVersion::V4
} else if value == v5::VERSION {
AuthenticatorVersion::V5
} else {
AuthenticatorVersion::UNKNOWN
}
}
}
impl From<&str> for AuthenticatorVersion {
fn from(value: &str) -> Self {
let Ok(semver) = semver::Version::parse(value) else {
return Self::UNKNOWN;
};
semver.into()
}
}
impl From<Option<&String>> for AuthenticatorVersion {
fn from(value: Option<&String>) -> Self {
match value {
None => Self::UNKNOWN,
Some(value) => value.as_str().into(),
}
}
}
impl From<String> for AuthenticatorVersion {
fn from(value: String) -> Self {
Self::from(value.as_str())
}
}
impl From<Option<String>> for AuthenticatorVersion {
fn from(value: Option<String>) -> Self {
value.as_ref().into()
}
}
impl From<semver::Version> for AuthenticatorVersion {
fn from(semver: semver::Version) -> Self {
if semver < AuthenticatorVersion::V1.release_version() {
return Self::UNKNOWN;
}
if semver < AuthenticatorVersion::V2.release_version() {
return Self::V1;
}
if semver < AuthenticatorVersion::V3.release_version() {
return Self::V2;
}
if semver < AuthenticatorVersion::V4.release_version() {
return Self::V3;
}
if semver < AuthenticatorVersion::V5.release_version() {
return Self::V4;
}
// if provided version is higher (or equal) to release version of V5,
// we return the latest (i.e. v5)
debug_assert_eq!(Self::V5, Self::LATEST, "a new AuthenticatorVersion variant has been introduced without adjusting the `From<semver::Version>` trait");
Self::LATEST
}
}
#[cfg(test)]
mod tests {
use super::super::latest;
use super::*;
#[test]
fn strum_display() {
// sanity check on formatting and casing
assert_eq!("v1", AuthenticatorVersion::V1.to_string());
assert_eq!("v2", AuthenticatorVersion::V2.to_string());
assert_eq!("unknown", AuthenticatorVersion::UNKNOWN.to_string());
}
#[test]
fn u8_conversion() {
assert_eq!(AuthenticatorVersion::V1, AuthenticatorVersion::from(1u8));
assert_eq!(AuthenticatorVersion::V2, AuthenticatorVersion::from(2u8));
assert_eq!(
AuthenticatorVersion::UNKNOWN,
AuthenticatorVersion::from(latest::VERSION + 1)
);
assert_eq!(
AuthenticatorVersion::UNKNOWN,
AuthenticatorVersion::from(0u8)
);
assert_eq!(
AuthenticatorVersion::UNKNOWN,
AuthenticatorVersion::from(255u8)
);
}
#[test]
fn semver_checks() {
assert_eq!(AuthenticatorVersion::UNKNOWN, "1.1.4".into());
assert_eq!(AuthenticatorVersion::UNKNOWN, "0.1.0".into());
assert_eq!(AuthenticatorVersion::UNKNOWN, "1.0.4".into());
assert_eq!(AuthenticatorVersion::V1, "1.1.5".into());
assert_eq!(AuthenticatorVersion::V1, "1.1.6".into());
assert_eq!(AuthenticatorVersion::V1, "1.1.8".into());
assert_eq!(AuthenticatorVersion::V2, "1.1.9".into());
assert_eq!(AuthenticatorVersion::V3, "1.1.10".into());
assert_eq!(AuthenticatorVersion::V3, "1.1.11".into());
assert_eq!(AuthenticatorVersion::V3, "1.1.60".into());
assert_eq!(AuthenticatorVersion::V4, "1.2.0".into());
assert_eq!(AuthenticatorVersion::V4, "1.2.1".into());
assert_eq!(AuthenticatorVersion::V4, "1.5.1".into());
assert_eq!(AuthenticatorVersion::V4, "1.6.0".into());
assert_eq!(AuthenticatorVersion::V5, "1.6.1".into());
assert_eq!(AuthenticatorVersion::V5, "1.6.11".into());
assert_eq!(AuthenticatorVersion::V5, "1.7.0".into());
assert_eq!(AuthenticatorVersion::V5, "1.16.11".into());
assert_eq!(AuthenticatorVersion::V5, "1.17.0".into());
}
}
+1
View File
@@ -7,6 +7,7 @@ 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 }
log = { workspace = true }
rand = { workspace = true }
+2
View File
@@ -23,10 +23,12 @@ use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
pub use event::BandwidthStatusMessage;
pub use traits::{BandwidthTicketProvider, DEFAULT_TICKETS_TO_SPEND};
pub mod acquire;
pub mod error;
mod event;
mod traits;
mod utils;
#[derive(Debug)]
+42
View File
@@ -0,0 +1,42 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait;
use nym_credential_storage::storage::Storage;
use nym_credentials_interface::TicketType;
use nym_crypto::asymmetric::ed25519;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use crate::{error::BandwidthControllerError, BandwidthController, PreparedCredential};
pub const DEFAULT_TICKETS_TO_SPEND: u32 = 1;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait BandwidthTicketProvider: Send + Sync {
async fn get_ecash_ticket(
&self,
ticket_type: TicketType,
gateway_id: ed25519::PublicKey,
tickets_to_spend: u32,
) -> Result<PreparedCredential, BandwidthControllerError>;
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C, St> BandwidthTicketProvider for BandwidthController<C, St>
where
C: DkgQueryClient + Sync + Send,
St: nym_credential_storage::storage::Storage,
<St as Storage>::StorageError: Send + Sync + 'static,
{
async fn get_ecash_ticket(
&self,
ticket_type: TicketType,
gateway_id: ed25519::PublicKey,
tickets_to_spend: u32,
) -> Result<PreparedCredential, BandwidthControllerError> {
self.prepare_ecash_ticket(ticket_type, gateway_id.to_bytes(), tickets_to_spend)
.await
}
}
@@ -7,11 +7,12 @@ use super::statistics_control::StatisticsControl;
use crate::client::base_client::storage::helpers::store_client_keys;
use crate::client::base_client::storage::MixnetClientStorage;
use crate::client::cover_traffic_stream::LoopCoverTrafficStream;
use crate::client::event_control::EventControl;
use crate::client::inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender};
use crate::client::key_manager::persistence::KeyStore;
use crate::client::key_manager::ClientKeys;
use crate::client::mix_traffic::transceiver::{GatewayReceiver, GatewayTransceiver, RemoteGateway};
use crate::client::mix_traffic::{BatchMixMessageSender, MixTrafficController};
use crate::client::mix_traffic::{BatchMixMessageSender, MixTrafficController, MixTrafficEvent};
use crate::client::real_messages_control;
use crate::client::real_messages_control::RealMessagesController;
use crate::client::received_buffer::{
@@ -66,7 +67,6 @@ use std::path::Path;
use std::sync::Arc;
use time::OffsetDateTime;
use tokio::sync::mpsc::Sender;
use tracing::*;
use url::Url;
#[cfg(all(
@@ -79,6 +79,23 @@ pub mod non_wasm_helpers;
pub mod helpers;
pub mod storage;
#[derive(Clone, Copy, Debug)]
pub enum MixnetClientEvent {
Traffic(MixTrafficEvent),
}
pub type EventReceiver = mpsc::UnboundedReceiver<MixnetClientEvent>;
#[derive(Clone)]
pub struct EventSender(pub mpsc::UnboundedSender<MixnetClientEvent>);
impl EventSender {
pub fn send(&self, event: MixnetClientEvent) {
if let Err(err) = self.0.unbounded_send(event) {
tracing::warn!("Failed to send error event. The caller event reader was closed: {err}");
}
}
}
#[derive(Clone)]
pub struct ClientInput {
pub connection_command_sender: ConnectionCommandSender,
@@ -194,6 +211,7 @@ pub struct BaseClientBuilder<C, S: MixnetClientStorage> {
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send>>,
shutdown: Option<ShutdownTracker>,
event_tx: Option<EventSender>,
user_agent: Option<UserAgent>,
setup_method: GatewaySetup,
@@ -222,6 +240,7 @@ where
custom_topology_provider: None,
custom_gateway_transceiver: None,
shutdown: None,
event_tx: None,
user_agent: None,
setup_method: GatewaySetup::MustLoad { gateway_id: None },
#[cfg(unix)]
@@ -284,6 +303,12 @@ where
self
}
#[must_use]
pub fn with_event_tx(mut self, event_tx: EventSender) -> Self {
self.event_tx = Some(event_tx);
self
}
#[must_use]
pub fn with_user_agent(mut self, user_agent: UserAgent) -> Self {
self.user_agent = Some(user_agent);
@@ -314,6 +339,18 @@ where
details.client_address()
}
fn start_event_control(
parent_event_tx: Option<EventSender>,
children_event_rx: EventReceiver,
shutdown_tracker: &ShutdownTracker,
) {
let event_control = EventControl::new(parent_event_tx, children_event_rx);
shutdown_tracker.try_spawn_named_with_shutdown(
async move { event_control.run().await },
"EventControl",
);
}
// future constantly pumping loop cover traffic at some specified average rate
// the pumped traffic goes to the MixTrafficController
fn start_cover_traffic_stream(
@@ -325,7 +362,7 @@ where
stats_tx: ClientStatsSender,
shutdown_tracker: &ShutdownTracker,
) {
info!("Starting loop cover traffic stream...");
tracing::info!("Starting loop cover traffic stream...");
let mut stream = LoopCoverTrafficStream::new(
ack_key,
@@ -357,7 +394,7 @@ where
stats_tx: ClientStatsSender,
shutdown_tracker: &ShutdownTracker,
) {
info!("Starting real traffic stream...");
tracing::info!("Starting real traffic stream...");
let real_messages_controller = RealMessagesController::new(
controller_config,
@@ -442,7 +479,7 @@ where
metrics_reporter: ClientStatsSender,
shutdown_tracker: &ShutdownTracker,
) {
info!("Starting received messages buffer controller...");
tracing::info!("Starting received messages buffer controller...");
let controller = ReceivedMessagesBufferController::<SphinxMessageReceiver>::new(
local_encryption_keypair,
query_receiver,
@@ -553,7 +590,7 @@ where
details_store
.upgrade_stored_remote_gateway_key(gateway_client.gateway_identity(), &updated_key)
.await.map_err(|err| {
error!("failed to store upgraded gateway key! this connection might be forever broken now: {err}");
tracing::error!("failed to store upgraded gateway key! this connection might be forever broken now: {err}");
ClientCoreError::GatewaysDetailsStoreError { source: Box::new(err) }
})?
}
@@ -650,7 +687,7 @@ where
if topology_config.disable_refreshing {
// if we're not spawning the refresher, don't cause shutdown immediately
info!("The background topology refresher is not going to be started");
tracing::info!("The background topology refresher is not going to be started");
}
let mut topology_refresher = TopologyRefresher::new(
@@ -660,7 +697,7 @@ where
);
// before returning, block entire runtime to refresh the current network view so that any
// components depending on topology would see a non-empty view
info!("Obtaining initial network topology");
tracing::info!("Obtaining initial network topology");
topology_refresher.try_refresh().await;
if let Err(err) = topology_refresher.ensure_topology_is_routable().await {
@@ -686,13 +723,13 @@ where
.wait_for_gateway(local_gateway, waiting_timeout)
.await
{
error!(
tracing::error!(
"the gateway did not come back online within the specified timeout: {err}"
);
return Err(err.into());
}
} else {
error!("the gateway we're supposedly connected to does not exist. We'll not be able to send any packets to ourselves: {err}");
tracing::error!("the gateway we're supposedly connected to does not exist. We'll not be able to send any packets to ourselves: {err}");
return Err(err.into());
}
}
@@ -700,7 +737,7 @@ where
if !topology_config.disable_refreshing {
// don't spawn the refresher if we don't want to be refreshing the topology.
// only use the initial values obtained
info!("Starting topology refresher...");
tracing::info!("Starting topology refresher...");
shutdown_tracker.try_spawn_named_with_shutdown(
async move { topology_refresher.run().await },
"TopologyRefresher",
@@ -717,7 +754,7 @@ where
input_sender: Sender<InputMessage>,
shutdown_tracker: &ShutdownTracker,
) -> ClientStatsSender {
info!("Starting statistics control...");
tracing::info!("Starting statistics control...");
StatisticsControl::create_and_start(
config.debug.stats_reporting,
user_agent
@@ -732,10 +769,14 @@ where
fn start_mix_traffic_controller(
gateway_transceiver: Box<dyn GatewayTransceiver + Send>,
shutdown_tracker: &ShutdownTracker,
event_tx: EventSender,
) -> (BatchMixMessageSender, ClientRequestSender) {
info!("Starting mix traffic controller...");
let (mut mix_traffic_controller, mix_tx, client_tx) =
MixTrafficController::new(gateway_transceiver, shutdown_tracker.clone_shutdown_token());
tracing::info!("Starting mix traffic controller...");
let (mut mix_traffic_controller, mix_tx, client_tx) = MixTrafficController::new(
gateway_transceiver,
shutdown_tracker.clone_shutdown_token(),
event_tx,
);
shutdown_tracker.try_spawn_named(
async move { mix_traffic_controller.run().await },
@@ -799,7 +840,7 @@ where
{
// if client keys do not exist already, create and persist them
if key_store.load_keys().await.is_err() {
info!("could not find valid client keys - a new set will be generated");
tracing::info!("could not find valid client keys - a new set will be generated");
let mut rng = OsRng;
let keys = if let Some(derivation_material) = derivation_material {
ClientKeys::from_master_key(&mut rng, &derivation_material)
@@ -846,7 +887,7 @@ where
<S::CredentialStore as CredentialStorage>::StorageError: Send + Sync + 'static,
<S::GatewaysDetailsStore as GatewaysDetailsStore>::StorageError: Sync + Send,
{
info!("Starting nym client");
tracing::info!("Starting nym client");
// derive (or load) client keys and gateway configuration
let init_res = Self::initialise_keys_and_gateway(
@@ -875,6 +916,9 @@ where
// channels responsible for controlling real messages
let (input_sender, input_receiver) = tokio::sync::mpsc::channel::<InputMessage>(1);
// channels responsible for event management
let (event_sender, event_receiver) = mpsc::unbounded();
// channels responsible for controlling ack messages
let (ack_sender, ack_receiver) = mpsc::unbounded();
let shared_topology_accessor =
@@ -887,6 +931,8 @@ where
None => nym_task::get_sdk_shutdown_tracker()?,
};
Self::start_event_control(self.event_tx, event_receiver, &shutdown_tracker);
// channels responsible for dealing with reply-related fun
let (reply_controller_sender, reply_controller_receiver) =
reply_controller::requests::new_control_channels();
@@ -977,6 +1023,7 @@ where
let (message_sender, client_request_sender) = Self::start_mix_traffic_controller(
gateway_transceiver,
&shutdown_tracker.child_tracker(),
EventSender(event_sender),
);
// Channels that the websocket listener can use to signal downstream to the real traffic
@@ -1026,8 +1073,8 @@ where
);
}
debug!("Core client startup finished!");
debug!("The address of this client is: {self_address}");
tracing::debug!("Core client startup finished!");
tracing::debug!("The address of this client is: {self_address}");
Ok(BaseClient {
address: self_address,
@@ -1050,7 +1097,7 @@ where
gateway_connection: GatewayConnection { gateway_ws_fd },
},
stats_reporter,
shutdown_handle: Some(shutdown_tracker), // The primary tracker for this client
shutdown_handle: shutdown_tracker, // The primary tracker for this client
client_request_sender,
forget_me: self.config.debug.forget_me,
remember_me: self.config.debug.remember_me,
@@ -1066,7 +1113,7 @@ pub struct BaseClient {
pub client_state: ClientState,
pub stats_reporter: ClientStatsSender,
pub client_request_sender: ClientRequestSender,
pub shutdown_handle: Option<ShutdownTracker>,
pub shutdown_handle: ShutdownTracker,
pub forget_me: ForgetMe,
pub remember_me: RememberMe,
}
@@ -0,0 +1,40 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use futures::StreamExt;
use crate::client::base_client::{EventReceiver, EventSender, MixnetClientEvent};
/// Launches and manages task events, propagating upwards what is not strictly internal.
pub(crate) struct EventControl {
parent_event_tx: Option<EventSender>,
children_event_rx: EventReceiver,
}
impl EventControl {
pub(crate) fn new(
parent_event_tx: Option<EventSender>,
children_event_rx: EventReceiver,
) -> Self {
EventControl {
parent_event_tx,
children_event_rx,
}
}
fn is_internal(event: MixnetClientEvent) -> bool {
match event {
MixnetClientEvent::Traffic(_) => false,
}
}
pub(crate) async fn run(mut self) {
while let Some(event) = self.children_event_rx.next().await {
if let Some(parent_event_tx) = &self.parent_event_tx {
if !Self::is_internal(event) {
parent_event_tx.send(event);
}
}
}
}
}
@@ -1,7 +1,10 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::mix_traffic::transceiver::GatewayTransceiver;
use crate::client::{
base_client::{EventSender, MixnetClientEvent},
mix_traffic::transceiver::GatewayTransceiver,
};
use nym_gateway_requests::ClientRequest;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_task::ShutdownToken;
@@ -22,6 +25,11 @@ const MAX_FAILURE_COUNT: usize = 100;
// that's also disgusting.
pub struct Empty;
#[derive(Clone, Copy, Debug)]
pub enum MixTrafficEvent {
FailedSendingSphinx,
}
pub struct MixTrafficController {
gateway_transceiver: Box<dyn GatewayTransceiver + Send>,
@@ -33,12 +41,14 @@ pub struct MixTrafficController {
consecutive_gateway_failure_count: usize,
shutdown_token: ShutdownToken,
event_tx: EventSender,
}
impl MixTrafficController {
pub fn new<T>(
gateway_transceiver: T,
shutdown_token: ShutdownToken,
event_tx: EventSender,
) -> (
MixTrafficController,
BatchMixMessageSender,
@@ -59,6 +69,7 @@ impl MixTrafficController {
client_rx: client_receiver,
consecutive_gateway_failure_count: 0,
shutdown_token,
event_tx,
},
message_sender,
client_sender,
@@ -68,6 +79,7 @@ impl MixTrafficController {
pub fn new_dynamic(
gateway_transceiver: Box<dyn GatewayTransceiver + Send>,
shutdown_token: ShutdownToken,
event_tx: EventSender,
) -> (
MixTrafficController,
BatchMixMessageSender,
@@ -83,6 +95,7 @@ impl MixTrafficController {
client_rx: client_receiver,
consecutive_gateway_failure_count: 0,
shutdown_token,
event_tx,
},
message_sender,
client_sender,
@@ -155,6 +168,7 @@ impl MixTrafficController {
error!("Failed to send sphinx packet to the gateway {MAX_FAILURE_COUNT} times in a row - assuming the gateway is dead");
// Do we need to handle the embedded mixnet client case
// separately?
self.event_tx.send(MixnetClientEvent::Traffic(MixTrafficEvent::FailedSendingSphinx));
break;
}
}
@@ -170,7 +184,8 @@ impl MixTrafficController {
},
None => {
trace!("MixTrafficController, client request channel closed");
break}
break
}
},
}
}
+1
View File
@@ -3,6 +3,7 @@
pub mod base_client;
pub mod cover_traffic_stream;
pub(crate) mod event_control;
pub(crate) mod helpers;
pub mod inbound_messages;
pub mod key_manager;
+1
View File
@@ -42,6 +42,7 @@ nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
nym-validator-client = { path = "../client-libs/validator-client" }
nym-network-defaults = { path = "../network-defaults" }
nym-cache = { path = "../nym-cache" }
[dev-dependencies]
tempfile = { workspace = true }
@@ -2,12 +2,18 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::CredentialProxyError;
use crate::shared_state::nyxd_client::ChainClient;
use crate::storage::models::StorableEcashDeposit;
use nym_compact_ecash::WithdrawalRequest;
use nym_credentials::IssuanceTicketBook;
use nym_crypto::asymmetric::ed25519;
use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData;
use nym_validator_client::nyxd::{Coin, Hash};
use rand::rngs::OsRng;
use std::fmt::Debug;
use time::OffsetDateTime;
use tokio_util::sync::CancellationToken;
use tracing::{error, info, instrument};
use zeroize::Zeroizing;
pub struct BufferedDeposit {
@@ -76,7 +82,80 @@ impl PerformedDeposits {
}
}
pub(super) fn request_sizes(total: usize, max_request_size: usize) -> impl Iterator<Item = usize> {
#[instrument(skip(client, cancellation_on_critical_failure), err(Display))]
pub async fn make_deposits_request(
client: &ChainClient,
deposit_amount: Coin,
memo: impl Into<String> + Debug,
amount: usize,
cancellation_on_critical_failure: &CancellationToken,
) -> Result<PerformedDeposits, CredentialProxyError> {
let requested_on = OffsetDateTime::now_utc();
let chain_write_permit = client.start_chain_tx().await;
let mut rng = OsRng;
let keys = (0..amount)
.map(|_| ed25519::PrivateKey::new(&mut rng))
.collect::<Vec<_>>();
info!("starting {amount} deposits");
let mut contents = Vec::new();
for key in &keys {
let public_key: ed25519::PublicKey = key.into();
contents.push((public_key.to_base58_string(), deposit_amount.clone()));
}
let execute_res = chain_write_permit
.make_deposits(memo.into(), contents)
.await?;
let tx_hash = execute_res.transaction_hash;
info!("{amount} deposits made in transaction: {tx_hash}");
let contract_data = match execute_res.to_contract_data() {
Ok(contract_data) => contract_data,
Err(err) => {
// that one is tricky. deposits technically got made, but we somehow failed to parse response,
// in this case terminate the proxy with 0 exit code so it wouldn't get automatically restarted
// because it requires some serious MANUAL intervention
error!("CRITICAL FAILURE: failed to parse out deposit information from the contract transaction. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually. error was: {err}");
cancellation_on_critical_failure.cancel();
return Err(CredentialProxyError::DepositFailure);
}
};
if contract_data.len() != amount {
// another critical failure, that one should be quite impossible and thus has to be manually inspected
error!("CRITICAL FAILURE: failed to parse out all deposit information from the contract transaction. got {} responses while we sent {amount} deposits! either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually", contract_data.len());
cancellation_on_critical_failure.cancel();
return Err(CredentialProxyError::DepositFailure);
}
let mut deposits_data = Vec::new();
for (key, response) in keys.into_iter().zip(contract_data) {
let response_index = response.message_index;
let deposit_id = match response.parse_singleton_u32_contract_data() {
Ok(deposit_id) => deposit_id,
Err(err) => {
// another impossibility
error!("CRITICAL FAILURE: failed to parse out deposit id out of the response at index {response_index}: {err}. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually");
cancellation_on_critical_failure.cancel();
return Err(CredentialProxyError::DepositFailure);
}
};
deposits_data.push(BufferedDeposit::new(deposit_id, key));
}
Ok(PerformedDeposits {
deposits_data,
tx_hash,
requested_on,
deposit_amount,
})
}
pub fn split_deposits(total: usize, max_request_size: usize) -> impl Iterator<Item = usize> {
(0..total)
.step_by(max_request_size)
.map(move |start| std::cmp::min(max_request_size, total - start))
@@ -89,13 +168,13 @@ mod tests {
#[test]
fn request_sizes_test() {
assert_eq!(
request_sizes(100, 32).collect::<Vec<_>>(),
split_deposits(100, 32).collect::<Vec<_>>(),
vec![32, 32, 32, 4]
);
assert_eq!(request_sizes(10, 32).collect::<Vec<_>>(), vec![10]);
assert_eq!(request_sizes(32, 32).collect::<Vec<_>>(), vec![32]);
assert_eq!(request_sizes(33, 32).collect::<Vec<_>>(), vec![32, 1]);
assert_eq!(request_sizes(1, 32).collect::<Vec<_>>(), vec![1]);
assert_eq!(split_deposits(10, 32).collect::<Vec<_>>(), vec![10]);
assert_eq!(split_deposits(32, 32).collect::<Vec<_>>(), vec![32]);
assert_eq!(split_deposits(33, 32).collect::<Vec<_>>(), vec![32, 1]);
assert_eq!(split_deposits(1, 32).collect::<Vec<_>>(), vec![1]);
}
}
@@ -1,27 +1,23 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::deposits_buffer::helpers::request_sizes;
use crate::deposits_buffer::refill_task::RefillTask;
use crate::error::CredentialProxyError;
use crate::shared_state::nyxd_client::ChainClient;
use crate::shared_state::required_deposit_cache::RequiredDepositCache;
use crate::storage::CredentialProxyStorage;
use nym_compact_ecash::PublicKeyUser;
use nym_crypto::asymmetric::ed25519;
use nym_ecash_contract_common::deposit::DepositId;
use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData;
use nym_validator_client::nyxd::Coin;
use rand::rngs::OsRng;
use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime;
use tokio::sync::Mutex as AsyncMutex;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, instrument, warn};
use tracing::{debug, info, instrument, warn};
use uuid::Uuid;
pub use helpers::{BufferedDeposit, PerformedDeposits};
pub use helpers::{make_deposits_request, split_deposits, BufferedDeposit, PerformedDeposits};
pub(crate) mod helpers;
mod refill_task;
@@ -89,70 +85,20 @@ impl DepositsBuffer {
&self,
amount: usize,
) -> Result<PerformedDeposits, CredentialProxyError> {
let requested_on = OffsetDateTime::now_utc();
let chain_write_permit = self.inner.client.start_chain_tx().await;
let mut rng = OsRng;
let memo = format!(
"credential-proxy-{}: performing {amount} deposits",
self.inner.short_sha
);
let deposit_amount = self.deposit_amount().await?;
let keys = (0..amount)
.map(|_| ed25519::PrivateKey::new(&mut rng))
.collect::<Vec<_>>();
info!("starting {amount} deposits");
let mut contents = Vec::new();
for key in &keys {
let public_key: ed25519::PublicKey = key.into();
contents.push((public_key.to_base58_string(), deposit_amount.clone()));
}
let execute_res = chain_write_permit
.make_deposits(self.inner.short_sha, contents)
.await?;
let tx_hash = execute_res.transaction_hash;
info!("{amount} deposits made in transaction: {tx_hash}");
let contract_data = match execute_res.to_contract_data() {
Ok(contract_data) => contract_data,
Err(err) => {
// that one is tricky. deposits technically got made, but we somehow failed to parse response,
// in this case terminate the proxy with 0 exit code so it wouldn't get automatically restarted
// because it requires some serious MANUAL intervention
error!("CRITICAL FAILURE: failed to parse out deposit information from the contract transaction. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually. error was: {err}");
self.inner.cancellation_token.cancel();
return Err(CredentialProxyError::DepositFailure);
}
};
if contract_data.len() != amount {
// another critical failure, that one should be quite impossible and thus has to be manually inspected
error!("CRITICAL FAILURE: failed to parse out all deposit information from the contract transaction. got {} responses while we sent {amount} deposits! either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually", contract_data.len());
self.inner.cancellation_token.cancel();
return Err(CredentialProxyError::DepositFailure);
}
let mut deposits_data = Vec::new();
for (key, response) in keys.into_iter().zip(contract_data) {
let response_index = response.message_index;
let deposit_id = match response.parse_singleton_u32_contract_data() {
Ok(deposit_id) => deposit_id,
Err(err) => {
// another impossibility
error!("CRITICAL FAILURE: failed to parse out deposit id out of the response at index {response_index}: {err}. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually");
self.inner.cancellation_token.cancel();
return Err(CredentialProxyError::DepositFailure);
}
};
deposits_data.push(BufferedDeposit::new(deposit_id, key));
}
Ok(PerformedDeposits {
deposits_data,
tx_hash,
requested_on,
make_deposits_request(
&self.inner.client,
deposit_amount,
})
&memo,
amount,
&self.inner.cancellation_token,
)
.await
}
async fn insert_new_deposits(
@@ -180,7 +126,7 @@ impl DepositsBuffer {
let target = self.deposits_upper_threshold();
let to_request = target - available;
for request_chunk in request_sizes(to_request, self.inner.max_concurrent_deposits) {
for request_chunk in split_deposits(to_request, self.inner.max_concurrent_deposits) {
// note: we check for cancellation between individual requests
// as opposed to wrapping that in tokio::select! so that we would never abandon chain operations
// as we wouldn't want to lose funds
+2 -69
View File
@@ -6,17 +6,15 @@
use crate::error::CredentialProxyError;
use futures::{stream, StreamExt};
use nym_cache::CachedImmutableItems;
use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime};
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch;
use nym_validator_client::EcashApiClient;
use std::cmp::min;
use std::collections::HashMap;
use std::future::Future;
use std::hash::Hash;
use std::ops::Deref;
use time::{Date, OffsetDateTime};
use tokio::sync::{Mutex, RwLock, RwLockReadGuard};
use tokio::sync::Mutex;
use tracing::warn;
pub struct CachedEpoch {
@@ -57,74 +55,9 @@ impl CachedEpoch {
}
}
// a map of items that never change for given key
pub struct CachedImmutableItems<K, V> {
// I wonder if there's a more efficient structure with OnceLock or OnceCell or something
inner: RwLock<HashMap<K, V>>,
}
// an item that stays constant throughout given epoch
pub type CachedImmutableEpochItem<T> = CachedImmutableItems<EpochId, T>;
impl<K, V> Default for CachedImmutableItems<K, V> {
fn default() -> Self {
CachedImmutableItems {
inner: RwLock::new(HashMap::new()),
}
}
}
impl<K, V> Deref for CachedImmutableItems<K, V> {
type Target = RwLock<HashMap<K, V>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<K, V> CachedImmutableItems<K, V>
where
K: Eq + Hash,
{
pub async fn get_or_init<F, U, E>(&self, key: K, f: F) -> Result<RwLockReadGuard<'_, V>, E>
where
F: FnOnce() -> U,
U: Future<Output = Result<V, E>>,
K: Clone,
{
// 1. see if we already have the item cached
let guard = self.inner.read().await;
if let Ok(item) = RwLockReadGuard::try_map(guard, |map| map.get(&key)) {
return Ok(item);
}
// 2. attempt to retrieve (and cache) it
let mut write_guard = self.inner.write().await;
// see if another task has already set the item whilst we were waiting for the lock
if write_guard.get(&key).is_some() {
let read_guard = write_guard.downgrade();
// SAFETY: we just checked the entry exists and we never dropped the guard
#[allow(clippy::unwrap_used)]
return Ok(RwLockReadGuard::map(read_guard, |map| {
map.get(&key).unwrap()
}));
}
let init = f().await?;
write_guard.insert(key.clone(), init);
let guard = write_guard.downgrade();
// SAFETY:
// we just inserted the entry into the map while NEVER dropping the lock (only downgraded it)
// so it MUST exist and thus the unwrap is fine
#[allow(clippy::unwrap_used)]
Ok(RwLockReadGuard::map(guard, |map| map.get(&key).unwrap()))
}
}
pub fn ensure_sane_expiration_date(expiration_date: Date) -> Result<(), CredentialProxyError> {
let today = ecash_today();
@@ -1,15 +1,37 @@
// Copyright 2025 Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::nym_api_helpers::{CachedEpoch, CachedImmutableEpochItem, CachedImmutableItems};
use crate::error::CredentialProxyError;
use crate::nym_api_helpers::{
ensure_sane_expiration_date, query_all_threshold_apis, CachedEpoch, CachedImmutableEpochItem,
};
use crate::quorum_checker::QuorumState;
use crate::shared_state::nyxd_client::ChainClient;
use crate::shared_state::required_deposit_cache::RequiredDepositCache;
use nym_compact_ecash::VerificationKeyAuth;
use nym_credentials::{AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures};
use crate::storage::traits::GlobalEcashDataCache;
use nym_cache::CachedImmutableItems;
use nym_compact_ecash::scheme::coin_indices_signatures::aggregate_annotated_indices_signatures;
use nym_compact_ecash::scheme::expiration_date_signatures::aggregate_annotated_expiration_signatures;
use nym_credentials::ecash::utils::EcashTime;
use nym_credentials::{
AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey,
};
use nym_validator_client::client::NymApiClientExt;
use nym_validator_client::coconut::EcashApiError;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch;
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
use nym_validator_client::nyxd::Coin;
use nym_validator_client::EcashApiClient;
use time::Date;
use tokio::sync::RwLock;
use time::{Date, OffsetDateTime};
use tokio::sync::{RwLock, RwLockReadGuard};
use tracing::info;
pub use nym_compact_ecash::scheme::coin_indices_signatures::CoinIndexSignatureShare;
pub use nym_compact_ecash::scheme::expiration_date_signatures::ExpirationDateSignatureShare;
pub use nym_compact_ecash::VerificationKeyAuth;
pub use nym_credentials::{IssuanceTicketBook, IssuedTicketBook};
pub use nym_credentials_interface::{TicketType, TicketTypeRepr};
pub struct EcashState {
pub required_deposit_cache: RequiredDepositCache,
@@ -46,4 +68,309 @@ impl EcashState {
expiration_date_signatures: Default::default(),
}
}
pub async fn ensure_credentials_issuable(
&self,
client: &ChainClient,
) -> Result<(), CredentialProxyError> {
let epoch = self.current_epoch(client).await?;
if epoch.state.is_final() {
Ok(())
} else if let Some(final_timestamp) = epoch.final_timestamp_secs() {
// SAFETY: the timestamp values in our DKG contract should be valid timestamps,
// otherwise it means the chain is seriously misbehaving
#[allow(clippy::unwrap_used)]
let finish_dt = OffsetDateTime::from_unix_timestamp(final_timestamp as i64).unwrap();
Err(CredentialProxyError::CredentialsNotYetIssuable {
availability: finish_dt,
})
} else if epoch.state.is_waiting_initialisation() {
Err(CredentialProxyError::UninitialisedDkg)
} else {
Err(CredentialProxyError::UnknownEcashFailure)
}
}
pub async fn deposit_amount(&self, client: &ChainClient) -> Result<Coin, CredentialProxyError> {
self.required_deposit_cache.get_or_update(client).await
}
pub async fn ecash_clients(
&self,
client: &ChainClient,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<'_, Vec<EcashApiClient>>, CredentialProxyError> {
self.epoch_clients
.get_or_init(epoch_id, || async {
Ok(client
.query_chain()
.await
.get_all_verification_key_shares(epoch_id)
.await?
.into_iter()
.map(TryInto::try_into)
.collect::<anyhow::Result<Vec<_>, EcashApiError>>()?)
})
.await
}
pub async fn current_epoch(&self, client: &ChainClient) -> Result<Epoch, CredentialProxyError> {
let read_guard = self.cached_epoch.read().await;
if read_guard.is_valid() {
return Ok(read_guard.current_epoch);
}
// update cache
drop(read_guard);
let mut write_guard = self.cached_epoch.write().await;
let epoch = client.query_chain().await.get_current_epoch().await?;
write_guard.update(epoch);
Ok(epoch)
}
pub async fn current_epoch_id(
&self,
client: &ChainClient,
) -> Result<EpochId, CredentialProxyError> {
let read_guard = self.cached_epoch.read().await;
if read_guard.is_valid() {
return Ok(read_guard.current_epoch.epoch_id);
}
// update cache
drop(read_guard);
let mut write_guard = self.cached_epoch.write().await;
let epoch = client.query_chain().await.get_current_epoch().await?;
write_guard.update(epoch);
Ok(epoch.epoch_id)
}
pub async fn master_verification_key<S>(
&self,
client: &ChainClient,
storage: &S,
epoch_id: Option<EpochId>,
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, CredentialProxyError>
where
S: GlobalEcashDataCache,
{
let epoch_id = match epoch_id {
Some(id) => id,
None => self.current_epoch_id(client).await?,
};
self.master_verification_key
.get_or_init(epoch_id, || async {
// 1. check the storage
if let Some(stored) = storage.get_master_verification_key(epoch_id).await? {
return Ok(stored.key);
}
info!("attempting to establish master verification key for epoch {epoch_id}...");
// 2. perform actual aggregation
let all_apis = self.ecash_clients(client, epoch_id).await?;
let threshold = self.ecash_threshold(client, epoch_id).await?;
if all_apis.len() < threshold as usize {
return Err(CredentialProxyError::InsufficientNumberOfSigners {
threshold,
available: all_apis.len(),
});
}
let master_key = nym_credentials::aggregate_verification_keys(&all_apis)?;
let epoch = EpochVerificationKey {
epoch_id,
key: master_key,
};
// 3. save the key in the storage for when we reboot
storage.insert_master_verification_key(&epoch).await?;
Ok(epoch.key)
})
.await
}
pub async fn master_coin_index_signatures<S>(
&self,
client: &ChainClient,
storage: &S,
epoch_id: Option<EpochId>,
) -> Result<RwLockReadGuard<'_, AggregatedCoinIndicesSignatures>, CredentialProxyError>
where
S: GlobalEcashDataCache,
{
let epoch_id = match epoch_id {
Some(id) => id,
None => self.current_epoch_id(client).await?,
};
self.coin_index_signatures
.get_or_init(epoch_id, || async {
// 1. check the storage
if let Some(master_sigs) =
storage.get_master_coin_index_signatures(epoch_id).await?
{
return Ok(master_sigs);
}
info!(
"attempting to establish master coin index signatures for epoch {epoch_id}..."
);
// 2. go around APIs and attempt to aggregate the data
let master_vk = self
.master_verification_key(client, storage, Some(epoch_id))
.await?;
let all_apis = self.ecash_clients(client, epoch_id).await?;
let threshold = self.ecash_threshold(client, epoch_id).await?;
let get_partial_signatures = |api: EcashApiClient| async {
// move the api into the closure
let api = api;
let node_index = api.node_id;
let partial_vk = api.verification_key;
let partial = api
.api_client
.partial_coin_indices_signatures(Some(epoch_id))
.await?
.signatures;
Ok(CoinIndexSignatureShare {
index: node_index,
key: partial_vk,
signatures: partial,
})
};
let shares =
query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures)
.await?;
let aggregated = aggregate_annotated_indices_signatures(
nym_credentials_interface::ecash_parameters(),
&master_vk,
&shares,
)?;
let sigs = AggregatedCoinIndicesSignatures {
epoch_id,
signatures: aggregated,
};
// 3. save the signatures in the storage for when we reboot
storage.insert_master_coin_index_signatures(&sigs).await?;
Ok(sigs)
})
.await
}
pub async fn master_expiration_date_signatures<S>(
&self,
client: &ChainClient,
storage: &S,
epoch_id: EpochId,
expiration_date: Date,
) -> Result<RwLockReadGuard<'_, AggregatedExpirationDateSignatures>, CredentialProxyError>
where
S: GlobalEcashDataCache,
{
self
.expiration_date_signatures
.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)?;
// 2. check the storage
if let Some(master_sigs) = storage
.get_master_expiration_date_signatures(expiration_date, epoch_id)
.await?
{
return Ok(master_sigs);
}
info!(
"attempting to establish master expiration date signatures for {expiration_date} and epoch {epoch_id}..."
);
// 3. go around APIs and attempt to aggregate the data
let epoch_id = self.current_epoch_id(client).await?;
let master_vk = self.master_verification_key(client, storage, Some(epoch_id)).await?;
let all_apis = self.ecash_clients(client, epoch_id).await?;
let threshold = self.ecash_threshold(client, epoch_id).await?;
let get_partial_signatures = |api: EcashApiClient| async {
// move the api into the closure
let api = api;
let node_index = api.node_id;
let partial_vk = api.verification_key;
let partial = api
.api_client
.partial_expiration_date_signatures(Some(expiration_date), Some(epoch_id))
.await?
.signatures;
Ok(ExpirationDateSignatureShare {
index: node_index,
key: partial_vk,
signatures: partial,
})
};
let shares =
query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures)
.await?;
let aggregated = aggregate_annotated_expiration_signatures(
&master_vk,
expiration_date.ecash_unix_timestamp(),
&shares,
)?;
let sigs = AggregatedExpirationDateSignatures {
epoch_id,
expiration_date,
signatures: aggregated,
};
// 4. save the signatures in the storage for when we reboot
storage
.insert_master_expiration_date_signatures(&sigs)
.await?;
Ok(sigs)
})
.await
}
pub async fn ecash_threshold(
&self,
client: &ChainClient,
epoch_id: EpochId,
) -> Result<u64, CredentialProxyError> {
self.threshold_values
.get_or_init(epoch_id, || async {
if let Some(threshold) = client
.query_chain()
.await
.get_epoch_threshold(epoch_id)
.await?
{
Ok(threshold)
} else {
Err(CredentialProxyError::UnavailableThreshold { epoch_id })
}
})
.await
.map(|t| *t)
}
}
+24 -278
View File
@@ -3,39 +3,26 @@
use crate::deposits_buffer::{BufferedDeposit, DepositsBuffer};
use crate::error::CredentialProxyError;
use crate::nym_api_helpers::{ensure_sane_expiration_date, query_all_threshold_apis};
use crate::shared_state::ecash_state::EcashState;
use crate::shared_state::nyxd_client::ChainClient;
use crate::storage::CredentialProxyStorage;
use nym_compact_ecash::scheme::coin_indices_signatures::{
aggregate_annotated_indices_signatures, CoinIndexSignatureShare,
};
use nym_compact_ecash::scheme::expiration_date_signatures::{
aggregate_annotated_expiration_signatures, ExpirationDateSignatureShare,
};
use nym_compact_ecash::{Base58, PublicKeyUser, VerificationKeyAuth};
use nym_credential_proxy_requests::api::v1::ticketbook::models::{
AggregatedCoinIndicesSignaturesResponse, AggregatedExpirationDateSignaturesResponse,
GlobalDataParams, MasterVerificationKeyResponse,
};
use nym_credentials::ecash::utils::EcashTime;
use nym_credentials::{
AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey,
};
use nym_credentials::{AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures};
use nym_ecash_contract_common::deposit::DepositId;
use nym_validator_client::client::NymApiClientExt;
use nym_validator_client::coconut::EcashApiError;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch;
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
use nym_validator_client::nyxd::Coin;
use nym_validator_client::{DirectSigningHttpRpcNyxdClient, EcashApiClient};
use nym_validator_client::EcashApiClient;
use std::sync::Arc;
use std::time::Duration;
use time::{Date, OffsetDateTime};
use tokio::sync::RwLockReadGuard;
use tokio::time::Instant;
use tracing::{debug, error, info, warn};
use tracing::{debug, error, warn};
use uuid::Uuid;
pub mod ecash_state;
@@ -69,10 +56,7 @@ impl CredentialProxyState {
}
pub async fn deposit_amount(&self) -> Result<Coin, CredentialProxyError> {
self.ecash_state()
.required_deposit_cache
.get_or_update(self.client())
.await
self.ecash_state().deposit_amount(self.client()).await
}
pub fn client(&self) -> &ChainClient {
@@ -87,29 +71,10 @@ impl CredentialProxyState {
&self.inner.ecash_state
}
pub(crate) async fn query_chain(&self) -> RwLockReadGuard<'_, DirectSigningHttpRpcNyxdClient> {
self.inner.client.query_chain().await
}
pub async fn ensure_credentials_issuable(&self) -> Result<(), CredentialProxyError> {
let epoch = self.current_epoch().await?;
if epoch.state.is_final() {
Ok(())
} else if let Some(final_timestamp) = epoch.final_timestamp_secs() {
// SAFETY: the timestamp values in our DKG contract should be valid timestamps,
// otherwise it means the chain is seriously misbehaving
#[allow(clippy::unwrap_used)]
let finish_dt = OffsetDateTime::from_unix_timestamp(final_timestamp as i64).unwrap();
Err(CredentialProxyError::CredentialsNotYetIssuable {
availability: finish_dt,
})
} else if epoch.state.is_waiting_initialisation() {
Err(CredentialProxyError::UninitialisedDkg)
} else {
Err(CredentialProxyError::UnknownEcashFailure)
}
self.ecash_state()
.ensure_credentials_issuable(self.client())
.await
}
pub async fn get_deposit(
@@ -146,33 +111,11 @@ impl CredentialProxyState {
}
pub async fn current_epoch_id(&self) -> Result<EpochId, CredentialProxyError> {
let read_guard = self.inner.ecash_state.cached_epoch.read().await;
if read_guard.is_valid() {
return Ok(read_guard.current_epoch.epoch_id);
}
// update cache
drop(read_guard);
let mut write_guard = self.inner.ecash_state.cached_epoch.write().await;
let epoch = self.query_chain().await.get_current_epoch().await?;
write_guard.update(epoch);
Ok(epoch.epoch_id)
self.ecash_state().current_epoch_id(self.client()).await
}
pub async fn current_epoch(&self) -> Result<Epoch, CredentialProxyError> {
let read_guard = self.ecash_state().cached_epoch.read().await;
if read_guard.is_valid() {
return Ok(read_guard.current_epoch);
}
// update cache
drop(read_guard);
let mut write_guard = self.ecash_state().cached_epoch.write().await;
let epoch = self.query_chain().await.get_current_epoch().await?;
write_guard.update(epoch);
Ok(epoch)
self.ecash_state().current_epoch(self.client()).await
}
pub async fn global_data(
@@ -243,53 +186,8 @@ impl CredentialProxyState {
&self,
epoch_id: Option<EpochId>,
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, CredentialProxyError> {
let epoch_id = match epoch_id {
Some(id) => id,
None => self.current_epoch_id().await?,
};
self.inner
.ecash_state
.master_verification_key
.get_or_init(epoch_id, || async {
// 1. check the storage
if let Some(stored) = self
.inner
.storage
.get_master_verification_key(epoch_id)
.await?
{
return Ok(stored.key);
}
info!("attempting to establish master verification key for epoch {epoch_id}...");
// 2. perform actual aggregation
let all_apis = self.ecash_clients(epoch_id).await?;
let threshold = self.ecash_threshold(epoch_id).await?;
if all_apis.len() < threshold as usize {
return Err(CredentialProxyError::InsufficientNumberOfSigners {
threshold,
available: all_apis.len(),
});
}
let master_key = nym_credentials::aggregate_verification_keys(&all_apis)?;
let epoch = EpochVerificationKey {
epoch_id,
key: master_key,
};
// 3. save the key in the storage for when we reboot
self.inner
.storage
.insert_master_verification_key(&epoch)
.await?;
Ok(epoch.key)
})
self.ecash_state()
.master_verification_key(self.client(), self.storage(), epoch_id)
.await
}
@@ -297,75 +195,8 @@ impl CredentialProxyState {
&self,
epoch_id: Option<EpochId>,
) -> Result<RwLockReadGuard<'_, AggregatedCoinIndicesSignatures>, CredentialProxyError> {
let epoch_id = match epoch_id {
Some(id) => id,
None => self.current_epoch_id().await?,
};
self.inner
.ecash_state
.coin_index_signatures
.get_or_init(epoch_id, || async {
// 1. check the storage
if let Some(master_sigs) = self
.inner
.storage
.get_master_coin_index_signatures(epoch_id)
.await?
{
return Ok(master_sigs);
}
info!(
"attempting to establish master coin index signatures for epoch {epoch_id}..."
);
// 2. go around APIs and attempt to aggregate the data
let master_vk = self.master_verification_key(Some(epoch_id)).await?;
let all_apis = self.ecash_clients(epoch_id).await?;
let threshold = self.ecash_threshold(epoch_id).await?;
let get_partial_signatures = |api: EcashApiClient| async {
// move the api into the closure
let api = api;
let node_index = api.node_id;
let partial_vk = api.verification_key;
let partial = api
.api_client
.partial_coin_indices_signatures(Some(epoch_id))
.await?
.signatures;
Ok(CoinIndexSignatureShare {
index: node_index,
key: partial_vk,
signatures: partial,
})
};
let shares =
query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures)
.await?;
let aggregated = aggregate_annotated_indices_signatures(
nym_credentials_interface::ecash_parameters(),
&master_vk,
&shares,
)?;
let sigs = AggregatedCoinIndicesSignatures {
epoch_id,
signatures: aggregated,
};
// 3. save the signatures in the storage for when we reboot
self.inner
.storage
.insert_master_coin_index_signatures(&sigs)
.await?;
Ok(sigs)
})
self.ecash_state()
.master_coin_index_signatures(self.client(), self.storage(), epoch_id)
.await
}
@@ -374,73 +205,13 @@ impl CredentialProxyState {
epoch_id: EpochId,
expiration_date: Date,
) -> Result<RwLockReadGuard<'_, AggregatedExpirationDateSignatures>, CredentialProxyError> {
self.inner.ecash_state
.expiration_date_signatures
.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)?;
// 2. check the storage
if let Some(master_sigs) = self
.storage()
.get_master_expiration_date_signatures(expiration_date, epoch_id)
.await?
{
return Ok(master_sigs);
}
info!(
"attempting to establish master expiration date signatures for {expiration_date} and epoch {epoch_id}..."
);
// 3. go around APIs and attempt to aggregate the data
let epoch_id = self.current_epoch_id().await?;
let master_vk = self.master_verification_key(Some(epoch_id)).await?;
let all_apis = self.ecash_clients(epoch_id).await?;
let threshold = self.ecash_threshold(epoch_id).await?;
let get_partial_signatures = |api: EcashApiClient| async {
// move the api into the closure
let api = api;
let node_index = api.node_id;
let partial_vk = api.verification_key;
let partial = api
.api_client
.partial_expiration_date_signatures(Some(expiration_date), Some(epoch_id))
.await?
.signatures;
Ok(ExpirationDateSignatureShare {
index: node_index,
key: partial_vk,
signatures: partial,
})
};
let shares =
query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures)
.await?;
let aggregated = aggregate_annotated_expiration_signatures(
&master_vk,
expiration_date.ecash_unix_timestamp(),
&shares,
)?;
let sigs = AggregatedExpirationDateSignatures {
epoch_id,
expiration_date,
signatures: aggregated,
};
// 4. save the signatures in the storage for when we reboot
self.inner.storage
.insert_master_expiration_date_signatures(&sigs)
.await?;
Ok(sigs)
})
self.ecash_state()
.master_expiration_date_signatures(
self.client(),
self.storage(),
epoch_id,
expiration_date,
)
.await
}
@@ -448,40 +219,15 @@ impl CredentialProxyState {
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<'_, Vec<EcashApiClient>>, CredentialProxyError> {
self.inner
.ecash_state
.epoch_clients
.get_or_init(epoch_id, || async {
Ok(self
.query_chain()
.await
.get_all_verification_key_shares(epoch_id)
.await?
.into_iter()
.map(TryInto::try_into)
.collect::<anyhow::Result<Vec<_>, EcashApiError>>()?)
})
self.ecash_state()
.ecash_clients(self.client(), epoch_id)
.await
}
pub async fn ecash_threshold(&self, epoch_id: EpochId) -> Result<u64, CredentialProxyError> {
self.inner
.ecash_state
.threshold_values
.get_or_init(epoch_id, || async {
if let Some(threshold) = self
.query_chain()
.await
.get_epoch_threshold(epoch_id)
.await?
{
Ok(threshold)
} else {
Err(CredentialProxyError::UnavailableThreshold { epoch_id })
}
})
self.ecash_state()
.ecash_threshold(self.client(), epoch_id)
.await
.map(|t| *t)
}
}
@@ -6,7 +6,7 @@ use crate::helpers::LockTimer;
use nym_ecash_contract_common::msg::ExecuteMsg;
use nym_validator_client::nyxd::contract_traits::NymContractsProvider;
use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult;
use nym_validator_client::nyxd::{Coin, CosmWasmClient, NyxdClient};
use nym_validator_client::nyxd::{Coin, Config, CosmWasmClient, NyxdClient};
use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient};
use std::ops::Deref;
use std::sync::Arc;
@@ -29,6 +29,14 @@ impl ChainClient {
.nyxd_url
.as_str();
Self::new_with_config(client_config, nyxd_url, mnemonic)
}
pub fn new_with_config(
client_config: Config,
nyxd_url: &str,
mnemonic: bip39::Mnemonic,
) -> Result<Self, CredentialProxyError> {
let client = NyxdClient::connect_with_mnemonic(client_config, nyxd_url, mnemonic)?;
if client.ecash_contract_address().is_none() {
@@ -68,17 +76,15 @@ pub struct ChainWritePermit<'a> {
}
impl ChainWritePermit<'_> {
#[instrument(skip(self, short_sha, info), err(Display))]
#[instrument(skip(self, memo, info), err(Display))]
pub async fn make_deposits(
self,
short_sha: &'static str,
memo: String,
info: Vec<(String, Coin)>,
) -> Result<ExecuteResult, CredentialProxyError> {
let address = self.inner.address();
let starting_sequence = self.inner.get_sequence(&address).await?.sequence;
let deposits = info.len();
let ecash_contract = self
.inner
.ecash_contract_address()
@@ -95,12 +101,7 @@ impl ChainWritePermit<'_> {
let res = self
.inner
.execute_multiple(
ecash_contract,
deposit_messages,
None,
format!("cp-{short_sha}: performing {deposits} deposits"),
)
.execute_multiple(ecash_contract, deposit_messages, None, memo)
.await?;
loop {
@@ -26,6 +26,7 @@ use uuid::Uuid;
mod manager;
pub mod models;
pub(crate) mod pruner;
pub mod traits;
// TODO: proper import
type NodeId = u64;
@@ -0,0 +1,94 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::CredentialProxyError;
use crate::storage::CredentialProxyStorage;
use nym_validator_client::nym_api::EpochId;
use time::Date;
pub use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise;
pub use nym_credentials::{
AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey,
};
// we use it in our code so it's fine
#[allow(async_fn_in_trait)]
pub trait GlobalEcashDataCache {
async fn get_master_verification_key(
&self,
epoch_id: EpochId,
) -> Result<Option<EpochVerificationKey>, CredentialProxyError>;
async fn insert_master_verification_key(
&self,
key: &EpochVerificationKey,
) -> Result<(), CredentialProxyError>;
async fn get_master_coin_index_signatures(
&self,
epoch_id: EpochId,
) -> Result<Option<AggregatedCoinIndicesSignatures>, CredentialProxyError>;
async fn insert_master_coin_index_signatures(
&self,
signatures: &AggregatedCoinIndicesSignatures,
) -> Result<(), CredentialProxyError>;
async fn get_master_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: EpochId,
) -> Result<Option<AggregatedExpirationDateSignatures>, CredentialProxyError>;
async fn insert_master_expiration_date_signatures(
&self,
signatures: &AggregatedExpirationDateSignatures,
) -> Result<(), CredentialProxyError>;
}
impl GlobalEcashDataCache for CredentialProxyStorage {
async fn get_master_verification_key(
&self,
epoch_id: EpochId,
) -> Result<Option<EpochVerificationKey>, CredentialProxyError> {
self.get_master_verification_key(epoch_id).await
}
async fn insert_master_verification_key(
&self,
key: &EpochVerificationKey,
) -> Result<(), CredentialProxyError> {
self.insert_master_verification_key(key).await
}
async fn get_master_coin_index_signatures(
&self,
epoch_id: EpochId,
) -> Result<Option<AggregatedCoinIndicesSignatures>, CredentialProxyError> {
self.get_master_coin_index_signatures(epoch_id).await
}
async fn insert_master_coin_index_signatures(
&self,
signatures: &AggregatedCoinIndicesSignatures,
) -> Result<(), CredentialProxyError> {
self.insert_master_coin_index_signatures(signatures).await
}
async fn get_master_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: EpochId,
) -> Result<Option<AggregatedExpirationDateSignatures>, CredentialProxyError> {
self.get_master_expiration_date_signatures(expiration_date, epoch_id)
.await
}
async fn insert_master_expiration_date_signatures(
&self,
signatures: &AggregatedExpirationDateSignatures,
) -> Result<(), CredentialProxyError> {
self.insert_master_expiration_date_signatures(signatures)
.await
}
}
@@ -11,7 +11,7 @@ use crate::shared_state::CredentialProxyState;
use crate::storage::pruner::StoragePruner;
use crate::storage::CredentialProxyStorage;
use crate::webhook::ZkNymWebhook;
use nym_credentials::ecash::utils::ecash_today;
use nym_credentials::ecash::utils::ecash_default_expiration_date;
use nym_validator_client::nym_api::EpochId;
use std::future::Future;
use std::time::Duration;
@@ -100,7 +100,7 @@ impl TicketbookManager {
}
async fn build_initial_cache(&self) -> Result<(), CredentialProxyError> {
let today = ecash_today().date();
let default_expiration = ecash_default_expiration_date();
let epoch_id = self.state.current_epoch_id().await?;
let _ = self.state.deposit_amount().await?;
@@ -113,7 +113,7 @@ impl TicketbookManager {
.await?;
let _ = self
.state
.master_expiration_date_signatures(epoch_id, today)
.master_expiration_date_signatures(epoch_id, default_expiration)
.await?;
Ok(())
+8
View File
@@ -33,6 +33,14 @@ workspace = true
features = ["rt-multi-thread", "net", "signal", "fs"]
[dev-dependencies]
anyhow = { workspace = true }
nym-crypto = { path = "../crypto", features = ["asymmetric", "rand"] }
nym-test-utils = { path = "../test-utils" }
nym-credentials-interface = { path = "../credentials-interface" }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
[build-dependencies]
anyhow = { workspace = true }
sqlx = { workspace = true, features = [
@@ -44,6 +44,7 @@ impl EcashCredentialManagerInner {
fn hack_clone_ticketbook(book: &IssuedTicketBook) -> IssuedTicketBook {
let ser = book.pack();
let data = Zeroizing::new(ser.data);
#[allow(clippy::unwrap_used)]
IssuedTicketBook::try_unpack(&data, None).unwrap()
}
@@ -79,18 +80,24 @@ impl MemoryEcachTicketbookManager {
let mut guard = self.inner.write().await;
for t in guard.ticketbooks.values_mut() {
if !t.ticketbook.expired()
&& t.ticketbook.spent_tickets() + tickets as u64
<= t.ticketbook.params_total_tickets()
&& t.ticketbook.ticketbook_type().to_string() == ticketbook_type
{
t.ticketbook
.update_spent_tickets(t.ticketbook.spent_tickets() + tickets as u64);
return Some(RetrievedTicketbook {
ticketbook_id: t.ticketbook_id,
ticketbook: hack_clone_ticketbook(&t.ticketbook),
});
if t.ticketbook.expired() {
continue;
}
if t.ticketbook.spent_tickets() + tickets as u64 > t.total_tickets as u64 {
continue;
}
if t.ticketbook.ticketbook_type().to_string() != ticketbook_type {
continue;
}
let cloned = hack_clone_ticketbook(&t.ticketbook);
t.ticketbook
.update_spent_tickets(t.ticketbook.spent_tickets() + tickets as u64);
return Some(RetrievedTicketbook {
ticketbook_id: t.ticketbook_id,
total_tickets: t.total_tickets,
ticketbook: cloned,
});
}
None
@@ -156,18 +163,25 @@ impl MemoryEcachTicketbookManager {
guard.pending.remove(&pending_id);
}
pub(crate) async fn insert_new_ticketbook(&self, ticketbook: &IssuedTicketBook) {
pub(crate) async fn insert_new_ticketbook(
&self,
ticketbook: &IssuedTicketBook,
total_tickets: u32,
used_tickets: u32,
) {
let mut guard = self.inner.write().await;
let id = guard.next_id();
// hehe, that's hacky AF, but it works as a **TEMPORARY** workaround
let ser = ticketbook.pack();
let data = Zeroizing::new(ser.data);
#[allow(clippy::unwrap_used)]
let mut nasty_clone = hack_clone_ticketbook(ticketbook);
nasty_clone.update_spent_tickets(used_tickets as u64);
guard.ticketbooks.insert(
id,
RetrievedTicketbook {
ticketbook_id: id,
ticketbook: IssuedTicketBook::try_unpack(&data, None).unwrap(),
total_tickets,
ticketbook: nasty_clone,
},
);
}
@@ -199,7 +213,7 @@ impl MemoryEcachTicketbookManager {
ticketbook_type: t.ticketbook.ticketbook_type().to_string(),
epoch_id: t.ticketbook.epoch_id() as u32,
total_tickets: t.ticketbook.spent_tickets() as u32,
used_tickets: t.ticketbook.params_total_tickets() as u32,
used_tickets: t.total_tickets,
})
.collect()
}
@@ -66,7 +66,42 @@ impl Storage for EphemeralStorage {
&self,
ticketbook: &IssuedTicketBook,
) -> Result<(), StorageError> {
self.storage_manager.insert_new_ticketbook(ticketbook).await;
self.storage_manager
.insert_new_ticketbook(
ticketbook,
ticketbook.params_total_tickets() as u32,
ticketbook.spent_tickets() as u32,
)
.await;
Ok(())
}
async fn insert_partial_issued_ticketbook(
&self,
ticketbook: &IssuedTicketBook,
allowed_start_ticket_index: u32,
allowed_final_ticket_index: u32,
) -> Result<(), Self::StorageError> {
// sanity check: start <= final && final <= params max
if allowed_start_ticket_index > allowed_final_ticket_index {
return Err(StorageError::database_inconsistency(
"start_ticket_index must be less than or equal to final_ticket_index",
));
}
if allowed_final_ticket_index > ticketbook.params_total_tickets() as u32 {
return Err(StorageError::database_inconsistency(
"final ticket index must be less than or equal to params_total_tickets()",
));
}
self.storage_manager
.insert_new_ticketbook(
ticketbook,
allowed_final_ticket_index + 1,
allowed_start_ticket_index,
)
.await;
Ok(())
}
@@ -184,3 +219,104 @@ impl Storage for EphemeralStorage {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use nym_compact_ecash::tests::helpers::generate_expiration_date_signatures;
use nym_compact_ecash::{issue, ttp_keygen};
use nym_credentials_interface::TicketType;
use nym_crypto::asymmetric::ed25519;
use nym_ecash_time::EcashTime;
use nym_test_utils::helpers::deterministic_rng;
fn mock_ticketbook() -> anyhow::Result<IssuedTicketBook> {
let signing_keys = ttp_keygen(1, 1)?.remove(0);
let deposit_id = 42;
let identifier = "foomp";
let mut rng = deterministic_rng();
let key = ed25519::PrivateKey::new(&mut rng);
let typ = TicketType::V1MixnetEntry;
let issuance = IssuanceTicketBook::new(deposit_id, identifier, key, typ);
let expiration_date = issuance.expiration_date();
let sig_req = issuance.prepare_for_signing();
let _exp_date_sigs = generate_expiration_date_signatures(
sig_req.expiration_date.ecash_unix_timestamp(),
&[signing_keys.secret_key()],
&vec![signing_keys.verification_key()],
&signing_keys.verification_key(),
&[1],
)?;
let blind_sig = issue(
signing_keys.secret_key(),
sig_req.ecash_pub_key,
&sig_req.withdrawal_request,
expiration_date.ecash_unix_timestamp(),
issuance.ticketbook_type().encode(),
)?;
let partial_wallet =
issuance.unblind_signature(&signing_keys.verification_key(), &sig_req, blind_sig, 1)?;
let wallet = issuance.aggregate_signature_shares(
&signing_keys.verification_key(),
&vec![partial_wallet],
sig_req,
)?;
Ok(issuance.into_issued_ticketbook(wallet, 1))
}
#[tokio::test]
async fn storing_partial_ticketbook() -> anyhow::Result<()> {
let storage = EphemeralStorage::default();
let ticketbook = mock_ticketbook()?;
let typ = ticketbook.ticketbook_type();
storage
.insert_partial_issued_ticketbook(&ticketbook, 5, 5)
.await?;
let retrieved = storage
.get_next_unspent_usable_ticketbook(typ.to_string(), 1)
.await?;
assert!(retrieved.is_some());
let val = retrieved.unwrap();
assert_eq!(val.total_tickets, 6);
assert_eq!(val.ticketbook.spent_tickets(), 5);
// we only had 1 ticket
let retrieved2 = storage
.get_next_unspent_usable_ticketbook(typ.to_string(), 1)
.await?;
assert!(retrieved2.is_none());
let _another = mock_ticketbook()?;
let typ = ticketbook.ticketbook_type();
// 3 tickets (4, 5, 6)
storage
.insert_partial_issued_ticketbook(&ticketbook, 4, 6)
.await?;
assert!(storage
.get_next_unspent_usable_ticketbook(typ.to_string(), 1)
.await?
.is_some());
assert!(storage
.get_next_unspent_usable_ticketbook(typ.to_string(), 1)
.await?
.is_some());
assert!(storage
.get_next_unspent_usable_ticketbook(typ.to_string(), 1)
.await?
.is_some());
assert!(storage
.get_next_unspent_usable_ticketbook(typ.to_string(), 1)
.await?
.is_none());
Ok(())
}
}
+1
View File
@@ -7,6 +7,7 @@ use zeroize::{Zeroize, ZeroizeOnDrop};
pub struct RetrievedTicketbook {
pub ticketbook_id: i64,
pub total_tickets: u32,
pub ticketbook: IssuedTicketBook,
}
@@ -149,6 +149,44 @@ impl Storage for PersistentStorage {
Ok(())
}
async fn insert_partial_issued_ticketbook(
&self,
ticketbook: &IssuedTicketBook,
allowed_start_ticket_index: u32,
allowed_final_ticket_index: u32,
) -> Result<(), Self::StorageError> {
// sanity check: start <= final && final <= params max
if allowed_start_ticket_index > allowed_final_ticket_index {
return Err(StorageError::database_inconsistency(
"start_ticket_index must be less than or equal to final_ticket_index",
));
}
if allowed_final_ticket_index > ticketbook.params_total_tickets() as u32 {
return Err(StorageError::database_inconsistency(
"final ticket index must be less than or equal to params_total_tickets()",
));
}
let ser = ticketbook.pack();
let data = Zeroizing::new(ser.data);
let serialisation_revision = ser.revision;
self.storage_manager
.insert_new_ticketbook(
serialisation_revision,
&data,
ticketbook.expiration_date(),
&ticketbook.ticketbook_type().to_string(),
ticketbook.epoch_id() as u32,
allowed_final_ticket_index + 1,
allowed_start_ticket_index,
)
.await?;
Ok(())
}
async fn contains_issued_ticketbook(
&self,
ticketbook: &IssuedTicketBook,
@@ -235,6 +273,7 @@ impl Storage for PersistentStorage {
deserialised.update_spent_tickets(raw.used_tickets as u64);
Ok(Some(RetrievedTicketbook {
ticketbook_id: raw.id,
total_tickets: raw.total_tickets,
ticketbook: deserialised,
}))
}
+11 -2
View File
@@ -3,8 +3,6 @@
use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook};
use async_trait::async_trait;
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
use nym_compact_ecash::VerificationKeyAuth;
use nym_credentials::ecash::bandwidth::serialiser::keys::EpochVerificationKey;
use nym_credentials::ecash::bandwidth::serialiser::signatures::{
@@ -14,6 +12,9 @@ use nym_credentials::{IssuanceTicketBook, IssuedTicketBook};
use nym_ecash_time::Date;
use std::error::Error;
pub use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
pub use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
// for future reference, if you want to make a query for "how much bandwidth do we have left"
// do something along the lines of
// `SELECT total_tickets, used_tickets FROM ecash_ticketbook WHERE expiration_date >= ?`, today_date
@@ -37,6 +38,14 @@ pub trait Storage: Clone + Send + Sync {
ticketbook: &IssuedTicketBook,
) -> Result<(), Self::StorageError>;
// note that both start and final are **INCLUSIVE**
async fn insert_partial_issued_ticketbook(
&self,
ticketbook: &IssuedTicketBook,
allowed_start_ticket_index: u32,
allowed_final_ticket_index: u32,
) -> Result<(), Self::StorageError>;
async fn contains_issued_ticketbook(
&self,
ticketbook: &IssuedTicketBook,
+1 -1
View File
@@ -1,7 +1,6 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_network_defaults::TicketTypeRepr;
use rand::Rng;
use serde::{Deserialize, Serialize};
use thiserror::Error;
@@ -29,6 +28,7 @@ pub use nym_compact_ecash::{
PartialWallet, PayInfo, PublicKeyUser, SecretKeyUser, VerificationKeyAuth, WithdrawalRequest,
};
pub use nym_ecash_time::{ecash_today, EcashTime};
pub use nym_network_defaults::TicketTypeRepr;
#[derive(Debug, Clone)]
pub struct CredentialSigningData {
@@ -64,6 +64,22 @@ impl IssuanceTicketBook {
expiration_date: Date,
) -> Self {
let ecash_keypair = generate_keypair_user_from_seed(identifier);
Self::new_with_keypair(
deposit_id,
ecash_keypair,
signing_key,
ticketbook_type,
expiration_date,
)
}
pub fn new_with_keypair(
deposit_id: DepositId,
ecash_keypair: KeyPairUser,
signing_key: ed25519::PrivateKey,
ticketbook_type: TicketType,
expiration_date: Date,
) -> Self {
IssuanceTicketBook {
deposit_id,
signing_key,
@@ -103,10 +103,14 @@ impl IssuedTicketBook {
self.expiration_date < ecash_today().date()
}
pub fn params_total_tickets(&self) -> u64 {
pub fn global_total_tickets() -> u64 {
nym_credentials_interface::ecash_parameters().get_total_coins()
}
pub fn params_total_tickets(&self) -> u64 {
Self::global_total_tickets()
}
pub fn spent_tickets(&self) -> u64 {
self.spent_tickets
}
@@ -17,7 +17,6 @@ pub struct VersionSerialised<T: ?Sized> {
pub data: Vec<u8>,
pub revision: u8,
// still wondering if there's any point in having the phantom in here
#[zeroize(skip)]
#[serde(skip)]
_phantom: PhantomData<T>,
+3 -1
View File
@@ -13,7 +13,9 @@ use nym_validator_client::client::EcashApiClient;
use nym_validator_client::nym_api::NymApiClientExt;
// so we wouldn't break all the existing imports
pub use nym_ecash_time::{cred_exp_date, ecash_date_offset, ecash_today, EcashTime};
pub use nym_ecash_time::{
cred_exp_date, ecash_date_offset, ecash_default_expiration_date, ecash_today, EcashTime,
};
pub fn aggregate_verification_keys(
api_clients: &[EcashApiClient],
+17
View File
@@ -0,0 +1,17 @@
[package]
name = "nym-cache"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
[dependencies]
tokio = { workspace = true, features = ["sync"] }
[lints]
workspace = true
+77
View File
@@ -0,0 +1,77 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::collections::HashMap;
use std::future::Future;
use std::hash::Hash;
use std::ops::Deref;
use tokio::sync::{RwLock, RwLockReadGuard};
/// A map of items that never change for given key
pub struct CachedImmutableItems<K, V> {
// I wonder if there's a more efficient structure with OnceLock or OnceCell or something
inner: RwLock<HashMap<K, V>>,
}
impl<K, V> Default for CachedImmutableItems<K, V> {
fn default() -> Self {
CachedImmutableItems {
inner: RwLock::new(HashMap::new()),
}
}
}
impl<K, V> Deref for CachedImmutableItems<K, V> {
type Target = RwLock<HashMap<K, V>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<K, V> CachedImmutableItems<K, V>
where
K: Eq + Hash,
{
pub async fn get_or_init<F, U, E>(
&self,
key: K,
initialiser: F,
) -> Result<RwLockReadGuard<'_, V>, E>
where
F: FnOnce() -> U,
U: Future<Output = Result<V, E>>,
K: Clone,
{
// 1. see if we already have the item cached
let guard = self.inner.read().await;
if let Ok(item) = RwLockReadGuard::try_map(guard, |map| map.get(&key)) {
return Ok(item);
}
// 2. attempt to retrieve (and cache) it
let mut write_guard = self.inner.write().await;
// see if another task has already set the item whilst we were waiting for the lock
if write_guard.get(&key).is_some() {
let read_guard = write_guard.downgrade();
// SAFETY: we just checked the entry exists and we never dropped the guard
#[allow(clippy::unwrap_used)]
return Ok(RwLockReadGuard::map(read_guard, |map| {
map.get(&key).unwrap()
}));
}
let init = initialiser().await?;
write_guard.insert(key.clone(), init);
let guard = write_guard.downgrade();
// SAFETY:
// we just inserted the entry into the map while NEVER dropping the lock (only downgraded it)
// so it MUST exist and thus the unwrap is fine
#[allow(clippy::unwrap_used)]
Ok(RwLockReadGuard::map(guard, |map| map.get(&key).unwrap()))
}
}
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "nym-registration-common"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
tokio-util.workspace = true
nym-authenticator-requests = { path = "../authenticator-requests" }
nym-crypto = { path = "../crypto" }
nym-ip-packet-requests = { path = "../ip-packet-requests" }
nym-sphinx = { path = "../nymsphinx" }
+40
View File
@@ -0,0 +1,40 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use nym_authenticator_requests::AuthenticatorVersion;
use nym_crypto::asymmetric::x25519::PublicKey;
use nym_ip_packet_requests::IpPair;
use nym_sphinx::addressing::{NodeIdentity, Recipient};
pub const DEFAULT_PRIVATE_ENTRY_WIREGUARD_KEY_FILENAME: &str = "free_private_entry_wireguard.pem";
pub const DEFAULT_PUBLIC_ENTRY_WIREGUARD_KEY_FILENAME: &str = "free_public_entry_wireguard.pem";
pub const DEFAULT_PRIVATE_EXIT_WIREGUARD_KEY_FILENAME: &str = "free_private_exit_wireguard.pem";
pub const DEFAULT_PUBLIC_EXIT_WIREGUARD_KEY_FILENAME: &str = "free_public_exit_wireguard.pem";
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct NymNode {
pub identity: NodeIdentity,
pub ip_address: IpAddr,
pub ipr_address: Option<Recipient>,
pub authenticator_address: Option<Recipient>,
pub version: AuthenticatorVersion,
}
#[derive(Clone, Debug)]
pub struct GatewayData {
pub public_key: PublicKey,
pub endpoint: SocketAddr,
pub private_ipv4: Ipv4Addr,
pub private_ipv6: Ipv6Addr,
}
#[derive(Clone, Copy, Debug)]
pub struct AssignedAddresses {
pub entry_mixnet_gateway_ip: IpAddr,
pub exit_mixnet_gateway_ip: IpAddr,
pub mixnet_client_address: Recipient,
pub exit_mix_address: Recipient,
pub interface_addresses: IpPair,
}
@@ -42,6 +42,40 @@ impl TryFrom<u8> for ServiceProviderType {
}
}
pub trait ServiceProviderTypeExt {
fn is_network_requester(&self) -> bool;
fn is_ip_packet_router(&self) -> bool;
fn is_authenticator(&self) -> bool;
}
impl ServiceProviderTypeExt for ServiceProviderType {
fn is_network_requester(&self) -> bool {
matches!(self, Self::NetworkRequester)
}
fn is_ip_packet_router(&self) -> bool {
matches!(self, Self::IpPacketRouter)
}
fn is_authenticator(&self) -> bool {
matches!(self, Self::Authenticator)
}
}
impl ServiceProviderTypeExt for u8 {
fn is_network_requester(&self) -> bool {
ServiceProviderType::NetworkRequester as u8 == *self
}
fn is_ip_packet_router(&self) -> bool {
ServiceProviderType::IpPacketRouter as u8 == *self
}
fn is_authenticator(&self) -> bool {
ServiceProviderType::Authenticator as u8 == *self
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Protocol {
pub version: u8,
@@ -19,11 +19,9 @@ use nym_authenticator_requests::{
};
use nym_authenticator_requests::{
latest::registration::{GatewayClient, PendingRegistrations, PrivateIPs},
traits::{
AuthenticatorRequest, AuthenticatorVersion, FinalMessage, InitMessage,
QueryBandwidthMessage, TopUpMessage,
},
v1, v2, v3, v4, v5, CURRENT_VERSION,
request::AuthenticatorRequest,
traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage},
v1, v2, v3, v4, v5, AuthenticatorVersion, CURRENT_VERSION,
};
use nym_credential_verification::ecash::traits::EcashManager;
use nym_credential_verification::{
@@ -480,7 +478,7 @@ impl MixnetListener {
let mut registred_and_free = self.registred_and_free.write().await;
let registration_data = registred_and_free
.registration_in_progres
.get(&final_message.pub_key())
.get(&final_message.gateway_client_pub_key())
.ok_or(AuthenticatorError::RegistrationNotInProgress)?
.clone();
@@ -491,7 +489,7 @@ impl MixnetListener {
return Err(AuthenticatorError::MacVerificationFailure);
}
let mut peer = Peer::new(Key::new(final_message.pub_key().to_bytes()));
let mut peer = Peer::new(Key::new(final_message.gateway_client_pub_key().to_bytes()));
peer.allowed_ips
.push(IpAddrMask::new(final_message.private_ips().ipv4.into(), 32));
peer.allowed_ips.push(IpAddrMask::new(
@@ -532,7 +530,7 @@ impl MixnetListener {
registred_and_free
.registration_in_progres
.remove(&final_message.pub_key());
.remove(&final_message.gateway_client_pub_key());
let bytes = match AuthenticatorVersion::from(protocol) {
AuthenticatorVersion::V1 => v1::response::AuthenticatorResponse::new_registered(
+1
View File
@@ -67,6 +67,7 @@ utoipa-swagger-ui = { workspace = true, features = ["axum"] }
tracing = { workspace = true }
## internal
nym-cache = { path = "../common/nym-cache" }
nym-bandwidth-controller = { path = "../common/bandwidth-controller" }
nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" }
nym-ecash-time = { path = "../common/ecash-time", features = ["expiration"] }
+1 -78
View File
@@ -3,6 +3,7 @@
use crate::ecash::error::EcashError;
use nym_api_requests::ecash::BlindSignRequestBody;
use nym_cache::CachedImmutableItems;
use nym_coconut_dkg_common::types::EpochId;
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
@@ -11,11 +12,6 @@ use nym_compact_ecash::{BlindedSignature, EncodedDate, EncodedTicketType};
use nym_compact_ecash::{PublicKeyUser, WithdrawalRequest};
use nym_ecash_time::EcashTime;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::future::Future;
use std::hash::Hash;
use std::ops::Deref;
use tokio::sync::{RwLock, RwLockReadGuard};
#[derive(Serialize, Deserialize)]
pub(crate) struct IssuedExpirationDateSignatures {
@@ -67,78 +63,5 @@ pub(crate) fn blind_sign<C: CredentialRequest>(
)?)
}
// a map of items that never change for given key
pub(crate) struct CachedImmutableItems<K, V> {
// I wonder if there's a more efficient structure with OnceLock or OnceCell or something
inner: RwLock<HashMap<K, V>>,
}
// an item that stays constant throughout given epoch
pub(crate) type CachedImmutableEpochItem<T> = CachedImmutableItems<EpochId, T>;
impl<K, V> Default for CachedImmutableItems<K, V> {
fn default() -> Self {
CachedImmutableItems {
inner: RwLock::new(HashMap::new()),
}
}
}
impl<K, V> Deref for CachedImmutableItems<K, V> {
type Target = RwLock<HashMap<K, V>>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<K, V> CachedImmutableItems<K, V>
where
K: Eq + Hash,
{
pub(crate) async fn get_or_init<F, U>(
&self,
key: K,
f: F,
) -> Result<RwLockReadGuard<'_, V>, EcashError>
where
F: FnOnce() -> U,
U: Future<Output = Result<V, EcashError>>,
K: Clone,
{
// 1. see if we already have the item cached
let guard = self.inner.read().await;
if let Ok(item) = RwLockReadGuard::try_map(guard, |map| map.get(&key)) {
return Ok(item);
}
// 2. attempt to retrieve (and cache) it
let mut write_guard = self.inner.write().await;
// see if another task has already set the item whilst we were waiting for the lock
if write_guard.get(&key).is_some() {
let read_guard = write_guard.downgrade();
// SAFETY: we just checked the entry exists and we never dropped the guard
#[allow(clippy::unwrap_used)]
return Ok(RwLockReadGuard::map(read_guard, |map| {
map.get(&key).unwrap()
}));
}
let init = f().await?;
write_guard.insert(key.clone(), init);
let guard = write_guard.downgrade();
// SAFETY:
// we just inserted the entry into the map while NEVER dropping the lock (only downgraded it)
// so it MUST exist and thus the unwrap is fine
#[allow(clippy::unwrap_used)]
Ok(RwLockReadGuard::map(guard, |map| map.get(&key).unwrap()))
}
// pub(crate) async fn remove(&self, key: K) {
// self.inner.write().await.remove(&key);
// }
}
+2 -2
View File
@@ -2,9 +2,9 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::ecash::helpers::{
CachedImmutableEpochItem, CachedImmutableItems, IssuedCoinIndicesSignatures,
IssuedExpirationDateSignatures,
CachedImmutableEpochItem, IssuedCoinIndicesSignatures, IssuedExpirationDateSignatures,
};
use nym_cache::CachedImmutableItems;
use nym_coconut_dkg_common::types::EpochId;
use nym_compact_ecash::VerificationKeyAuth;
use nym_validator_client::nyxd::AccountId;
+2 -2
View File
@@ -3,12 +3,12 @@
use crate::ecash::error::EcashError;
use crate::ecash::helpers::{
CachedImmutableEpochItem, CachedImmutableItems, IssuedCoinIndicesSignatures,
IssuedExpirationDateSignatures,
CachedImmutableEpochItem, IssuedCoinIndicesSignatures, IssuedExpirationDateSignatures,
};
use crate::ecash::keys::KeyPair;
use crate::ecash::storage::models::IssuedHash;
use nym_api_requests::ecash::models::{CommitedDeposit, DepositId};
use nym_cache::CachedImmutableItems;
use nym_coconut_dkg_common::types::EpochId;
use nym_crypto::asymmetric::ed25519;
use nym_ticketbooks_merkle::{
+1 -1
View File
@@ -176,7 +176,7 @@ impl EcashState {
.active_signer
.get_or_init(epoch_id, || async {
let Ok(address) = self.aux.client.address().await else {
return Ok(false);
return Ok::<_, EcashError>(false);
};
let ecash_signers = self.aux.comm_channel.ecash_clients(epoch_id).await?;
+5
View File
@@ -14,6 +14,7 @@ workspace = true
[dependencies]
bincode.workspace = true
futures.workspace = true
rand.workspace = true
semver.workspace = true
thiserror.workspace = true
tokio-util.workspace = true
@@ -21,8 +22,12 @@ tokio.workspace = true
tracing.workspace = true
nym-authenticator-requests = { path = "../common/authenticator-requests" }
nym-bandwidth-controller = { path = "../common/bandwidth-controller" }
nym-credentials-interface = { path = "../common/credentials-interface" }
nym-crypto = { path = "../common/crypto" }
nym-pemstore = { path = "../common/pemstore" }
nym-registration-common = { path = "../common/registration" }
nym-sdk = { path = "../sdk/rust/nym-sdk" }
nym-service-provider-requests-common = { path = "../common/service-provider-requests-common" }
nym-validator-client = { path = "../common/client-libs/validator-client" }
nym-wireguard-types = { path = "../common/wireguard-types" }
+29 -20
View File
@@ -1,41 +1,50 @@
use nym_credentials_interface::TicketType;
use nym_sdk::mixnet::InputMessage;
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("mixnet client stopped returning responses")]
NoMixnetMessagesReceived,
#[error("failed to get version from message")]
NoVersionInMessage,
#[error(
"received response with version v{received}, the client is too new and can only understand v{expected}"
)]
ReceivedResponseWithOldVersion { expected: u8, received: u8 },
#[error(
"received response with version v{received}, the client is too old and can only understand v{expected}"
)]
ReceivedResponseWithNewVersion { expected: u8, received: u8 },
#[error("failed to send mixnet message")]
SendMixnetMessage(#[source] Box<nym_sdk::Error>),
SendMixnetMessage(#[source] Box<tokio::sync::mpsc::error::SendError<InputMessage>>),
#[error("timeout waiting for connect response from exit gateway (authenticator)")]
TimeoutWaitingForConnectResponse,
#[error("unable to get mixnet handle when sending authenticator message")]
UnableToGetMixnetHandle,
#[error("unknown version number")]
UnknownVersion,
#[error("unsupported request version")]
UnsupportedVersion,
#[error(transparent)]
Bincode(#[from] bincode::Error),
#[error("gateway doesn't support this type of message")]
UnsupportedMessage,
#[error(transparent)]
AuthenticatorRequests(#[from] nym_authenticator_requests::Error),
#[error("verification failure")]
VerificationFailed(#[source] nym_authenticator_requests::Error),
#[error("failed to parse entry gateway socket addr")]
FailedToParseEntryGatewaySocketAddr(#[source] std::net::AddrParseError),
#[error("received invalid response from gateway authenticator")]
InvalidGatewayAuthResponse,
#[error("failed to get {ticketbook_type} ticket")]
GetTicket {
ticketbook_type: TicketType,
#[source]
source: nym_bandwidth_controller::error::BandwidthControllerError,
},
#[error("unknown authenticator version number")]
UnsupportedAuthenticatorVersion,
#[error("failed to wait on AuthenticatorClientListener")]
FailedToJoinOnTask(#[from] tokio::task::JoinError),
}
// Result type based on our error type
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_crypto::asymmetric::x25519::KeyPair;
use nym_pemstore::KeyPairPath;
use nym_sdk::mixnet::{IncludedSurbs, Recipient, TransmissionLane};
use rand::{CryptoRng, RngCore};
pub(crate) fn create_input_message(
recipient: Recipient,
data: Vec<u8>,
surbs: IncludedSurbs,
) -> nym_sdk::mixnet::InputMessage {
match surbs {
IncludedSurbs::Amount(surbs) => nym_sdk::mixnet::InputMessage::new_anonymous(
recipient,
data,
surbs,
TransmissionLane::General,
None,
),
IncludedSurbs::ExposeSelfAddress => nym_sdk::mixnet::InputMessage::new_regular(
recipient,
data,
TransmissionLane::General,
None,
),
}
}
pub(crate) fn load_or_generate_keypair<R: RngCore + CryptoRng>(
rng: &mut R,
paths: KeyPairPath,
) -> KeyPair {
match nym_pemstore::load_keypair(&paths) {
Ok(keypair) => keypair,
Err(_) => {
let keypair = KeyPair::new(rng);
if let Err(e) = nym_pemstore::store_keypair(&keypair, &paths) {
tracing::error!(
"could not store generated keypair at {:?} - {:?}; will use ephemeral keys",
paths,
e
);
}
keypair
}
}
}
+224
View File
@@ -0,0 +1,224 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::time::Duration;
use tracing::{debug, error};
use crate::mixnet_listener::{MixnetMessageBroadcastReceiver, MixnetMessageInputSender};
use crate::{helpers, ClientMessage, Error, Result};
use nym_authenticator_requests::{
client_message::QueryMessageImpl, response::AuthenticatorResponse, traits::Id, v2, v3, v4, v5,
AuthenticatorVersion,
};
use nym_credentials_interface::CredentialSpendingData;
use nym_crypto::asymmetric::x25519::{KeyPair, PublicKey};
use nym_sdk::mixnet::{IncludedSurbs, Recipient};
use nym_service_provider_requests_common::{Protocol, ServiceProviderTypeExt};
use nym_wireguard_types::PeerPublicKey;
impl crate::AuthenticatorClient {
pub fn into_legacy_and_keypair(self) -> (LegacyAuthenticatorClient, KeyPair) {
(
LegacyAuthenticatorClient {
public_key: *self.keypair.public_key(),
mixnet_listener: self.mixnet_listener,
mixnet_sender: self.mixnet_sender,
our_nym_address: self.our_nym_address,
auth_recipient: self.auth_recipient,
auth_version: self.auth_version,
},
self.keypair,
)
}
}
// This is the legacy Authenticator that has to be used to handle bandwidth top up for legacy gateaways
pub struct LegacyAuthenticatorClient {
public_key: PublicKey,
mixnet_listener: MixnetMessageBroadcastReceiver,
mixnet_sender: MixnetMessageInputSender,
our_nym_address: Recipient,
pub auth_recipient: Recipient,
auth_version: AuthenticatorVersion,
}
impl LegacyAuthenticatorClient {
pub async fn send_and_wait_for_response(
&mut self,
message: &ClientMessage,
) -> Result<AuthenticatorResponse> {
let request_id = self.send_request(message).await?;
debug!("Waiting for reply...");
self.listen_for_response(request_id).await
}
async fn send_request(&self, message: &ClientMessage) -> Result<u64> {
let (data, request_id) = message.bytes(self.our_nym_address)?;
// We use 20 surbs for the connect request because typically the
// authenticator mixnet client on the nym-node is configured to have a min
// threshold of 10 surbs that it reserves for itself to request additional
// surbs.
let surbs = if message.use_surbs() {
match &message {
ClientMessage::Initial(_) => IncludedSurbs::new(20),
_ => IncludedSurbs::new(1),
}
} else {
IncludedSurbs::ExposeSelfAddress
};
let input_message = helpers::create_input_message(self.auth_recipient, data, surbs);
self.mixnet_sender
.send(input_message)
.await
.map_err(|e| Error::SendMixnetMessage(Box::new(e)))?;
Ok(request_id)
}
async fn listen_for_response(&mut self, request_id: u64) -> Result<AuthenticatorResponse> {
let timeout = tokio::time::sleep(Duration::from_secs(10));
tokio::pin!(timeout);
loop {
tokio::select! {
_ = &mut timeout => {
error!("Timed out waiting for reply to connect request");
return Err(Error::TimeoutWaitingForConnectResponse);
}
msg = self.mixnet_listener.recv() => match msg {
Err(_) => {
return Err(Error::NoMixnetMessagesReceived);
}
Ok(msg) => {
let Some(header) = msg.message.first_chunk::<2>() else {
debug!("received too short message that couldn't have been from the authenticator while waiting for connect response");
continue;
};
let Ok(protocol) = Protocol::try_from(header) else {
debug!("received a message not meant to any service provider while waiting for connect response");
continue;
};
if !protocol.service_provider_type.is_authenticator() {
debug!("Received non-authenticator message while waiting for connect response");
continue;
}
// Confirm that the version is correct
let version = AuthenticatorVersion::from(protocol.version);
// Then we deserialize the message
debug!("AuthClient: got message while waiting for connect response with version {version:?}");
let ret: Result<AuthenticatorResponse> = match version {
AuthenticatorVersion::V1 => Err(Error::UnsupportedVersion),
AuthenticatorVersion::V2 => v2::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into),
AuthenticatorVersion::V3 => v3::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into),
AuthenticatorVersion::V4 => v4::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into),
AuthenticatorVersion::V5 => v5::response::AuthenticatorResponse::from_reconstructed_message(&msg).map(Into::into).map_err(Into::into),
AuthenticatorVersion::UNKNOWN => Err(Error::UnknownVersion),
};
let Ok(response) = ret else {
// This is ok, it's likely just one of our self-pings
debug!("Failed to deserialize reconstructed message");
continue;
};
if response.id() == request_id {
debug!("Got response with matching id");
return Ok(response);
}
}
}
}
}
}
pub async fn query_bandwidth(&mut self) -> Result<Option<i64>> {
let query_message = match self.auth_version {
AuthenticatorVersion::V1 => return Err(Error::UnsupportedAuthenticatorVersion),
AuthenticatorVersion::V2 => ClientMessage::Query(Box::new(QueryMessageImpl {
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
version: AuthenticatorVersion::V2,
})),
AuthenticatorVersion::V3 => ClientMessage::Query(Box::new(QueryMessageImpl {
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
version: AuthenticatorVersion::V3,
})),
AuthenticatorVersion::V4 => ClientMessage::Query(Box::new(QueryMessageImpl {
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
version: AuthenticatorVersion::V4,
})),
AuthenticatorVersion::V5 => ClientMessage::Query(Box::new(QueryMessageImpl {
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
version: AuthenticatorVersion::V5,
})),
AuthenticatorVersion::UNKNOWN => return Err(Error::UnsupportedAuthenticatorVersion),
};
let response = self.send_and_wait_for_response(&query_message).await?;
let available_bandwidth = match response {
AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => {
if let Some(available_bandwidth) =
remaining_bandwidth_response.available_bandwidth()
{
available_bandwidth
} else {
return Ok(None);
}
}
_ => return Err(Error::InvalidGatewayAuthResponse),
};
let remaining_pretty = if available_bandwidth > 1024 * 1024 {
format!("{:.2} MB", available_bandwidth as f64 / 1024.0 / 1024.0)
} else {
format!("{} KB", available_bandwidth / 1024)
};
tracing::debug!(
"Remaining wireguard bandwidth with gateway {} for today: {}",
self.auth_recipient.gateway(),
remaining_pretty
);
if available_bandwidth < 1024 * 1024 {
tracing::warn!(
"Remaining bandwidth is under 1 MB. The wireguard mode will get suspended after that until tomorrow, UTC time. The client might shutdown with timeout soon"
);
}
Ok(Some(available_bandwidth))
}
pub async fn top_up(&mut self, credential: CredentialSpendingData) -> Result<i64> {
let top_up_message = match self.auth_version {
AuthenticatorVersion::V3 => ClientMessage::TopUp(Box::new(v3::topup::TopUpMessage {
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
credential,
})),
// NOTE: looks like a bug here using v3. But we're leaving it as is since it's working
// and V4 is deprecated in favour of V5
AuthenticatorVersion::V4 => ClientMessage::TopUp(Box::new(v4::topup::TopUpMessage {
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
credential,
})),
AuthenticatorVersion::V5 => ClientMessage::TopUp(Box::new(v5::topup::TopUpMessage {
pub_key: PeerPublicKey::new(self.public_key.to_bytes().into()),
credential,
})),
AuthenticatorVersion::V1 | AuthenticatorVersion::V2 | AuthenticatorVersion::UNKNOWN => {
return Err(Error::UnsupportedAuthenticatorVersion);
}
};
let response = self.send_and_wait_for_response(&top_up_message).await?;
let remaining_bandwidth = match response {
AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => {
top_up_bandwidth_response.available_bandwidth()
}
_ => return Err(Error::InvalidGatewayAuthResponse),
};
Ok(remaining_bandwidth)
}
}
File diff suppressed because it is too large Load Diff
+96 -49
View File
@@ -1,113 +1,160 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
// To remove with the Registration Client PR
#![allow(clippy::unwrap_used)]
use std::sync::Arc;
use futures::StreamExt;
use nym_sdk::mixnet::{MixnetClient, ReconstructedMessage};
use tokio::{sync::broadcast, task::JoinHandle};
use nym_sdk::mixnet::{InputMessage, MixnetClient, MixnetMessageSender, ReconstructedMessage};
use tokio::{
sync::{broadcast, mpsc},
task::JoinHandle,
};
use tokio_util::sync::CancellationToken;
use crate::AuthenticatorMixnetClient;
pub type SharedMixnetClient = Arc<tokio::sync::Mutex<Option<MixnetClient>>>;
pub type MixnetMessageBroadcastSender = broadcast::Sender<Arc<ReconstructedMessage>>;
pub type MixnetMessageBroadcastReceiver = broadcast::Receiver<Arc<ReconstructedMessage>>;
pub type MixnetMessageInputSender = mpsc::Sender<InputMessage>;
pub type MixnetMessageInputReceiver = mpsc::Receiver<InputMessage>; // This could be another type, to abstract the mixnet message creation to here
// The AuthClientsMixnetListener listens to mixnet messages and rebroadcasts them to the
// AuthClients, or whoever else is interested.
// While it is running, it has a lock on the shared mixnet client. This is the reason it's
// designed to be able to start and stop, so that the lock can be released when it's not needed.
// It also manages the message input for the mixnet so it can keep the sole ownership of the MixnetClient
//
// NOTE: this is potentially bit wasteful. Ideally we should have proper channels where the
// recipient only gets messages they're interested in.
pub struct AuthClientMixnetListener {
// The shared mixnet client that we're listening to
mixnet_client: SharedMixnetClient,
// The mixnet client that we're listening to
mixnet_client: MixnetClient,
// Broadcast channel for the messages that we re-broadcast to the AuthClients
message_broadcast: MixnetMessageBroadcastSender,
// Channel for message to send to the mixnet
input_message_tx: MixnetMessageInputSender, // we keep on to make sure it's open
input_message_rx: MixnetMessageInputReceiver,
// Listen to cancel from the outside world
shutdown_token: CancellationToken,
}
impl AuthClientMixnetListener {
pub fn new(mixnet_client: SharedMixnetClient, shutdown_token: CancellationToken) -> Self {
pub fn new(mixnet_client: MixnetClient, shutdown_token: CancellationToken) -> Self {
let (message_broadcast, _) = broadcast::channel(100);
let (input_message_tx, input_message_rx) = mpsc::channel(100);
Self {
mixnet_client,
message_broadcast,
input_message_tx,
input_message_rx,
shutdown_token,
}
}
pub fn subscribe(&self) -> MixnetMessageBroadcastReceiver {
self.message_broadcast.subscribe()
}
async fn run(mut self) -> Self {
let mixnet_cancel_token = self.mixnet_client.cancellation_token();
self.shutdown_token.run_until_cancelled(async {
loop {
tokio::select! {
biased;
_ = mixnet_cancel_token.cancelled() => {
tracing::debug!("AuthClientMixnetListener: mixnet client was shutdown");
break;
}
async fn run(self) {
let mut mixnet_client = self.mixnet_client.lock().await.take().unwrap();
self.shutdown_token
.run_until_cancelled(async {
while let Some(event) = mixnet_client.next().await {
if let Err(err) = self.message_broadcast.send(Arc::new(event)) {
tracing::error!("Failed to broadcast mixnet message: {err}");
// Sending loop
input_msg = self.input_message_rx.recv() => {
match input_msg {
None => {
tracing::error!("All senders were dropped. It shouldn't happen as we're holding one");
break;
},
Some(mix_msg) => {
if let Err(err) = self.mixnet_client.send(mix_msg).await {
tracing::error!("Failed to send mixnet message: {err}");
}
},
}
}
// Receiving loop
msg = self.mixnet_client.next() => {
match msg {
None => {
tracing::error!("Mixnet client stream ended unexpectedly");
break;
},
Some(event) => {
if let Err(err) = self.message_broadcast.send(Arc::new(event)) {
tracing::error!("Failed to broadcast mixnet message: {err}");
}
},
}
}
}
tracing::error!("Mixnet client stream ended unexpectedly");
})
.await;
self.mixnet_client.lock().await.replace(mixnet_client);
}
tracing::debug!("AuthClientMixnetListener is shutting down");
}).await;
self
}
// Disconnects the mixnet client and effectively drop itself, since it doesn't work without one, and reconnecting isn't supported
pub async fn disconnect_mixnet_client(self) {
if !self.mixnet_client.cancellation_token().is_cancelled() {
self.mixnet_client.disconnect().await;
}
}
pub fn start(self) -> AuthClientMixnetListenerHandle {
let mixnet_client = self.mixnet_client.clone();
let message_broadcast = self.message_broadcast.clone();
let message_sender = self.input_message_tx.clone();
// Allows stopping only this, e.g. if we don't need it in the new bandwidth controller
let cancellation_token = self.shutdown_token.clone();
let mixnet_cancellation_token = self.mixnet_client.cancellation_token();
let handle = tokio::spawn(self.run());
AuthClientMixnetListenerHandle {
mixnet_client,
message_broadcast,
message_sender,
cancellation_token,
mixnet_cancellation_token,
handle,
}
}
}
pub struct AuthClientMixnetListenerHandle {
mixnet_client: SharedMixnetClient,
message_broadcast: MixnetMessageBroadcastSender,
handle: JoinHandle<()>,
message_sender: MixnetMessageInputSender,
cancellation_token: CancellationToken,
mixnet_cancellation_token: CancellationToken,
handle: JoinHandle<AuthClientMixnetListener>,
}
impl AuthClientMixnetListenerHandle {
/// Returns new `AuthClient` or `None` if `MixnetClient` is already moved from shared reference.
pub async fn new_auth_client(&self) -> Option<AuthenticatorMixnetClient> {
let mixnet_client_guard = self.mixnet_client.lock().await;
let mixnet_client_ref = mixnet_client_guard.as_ref()?;
let mixnet_sender = mixnet_client_ref.split_sender();
let nym_address = *mixnet_client_ref.nym_address();
Some(
AuthenticatorMixnetClient::new(
mixnet_sender,
self.message_broadcast.subscribe(),
nym_address,
)
.await,
)
pub fn mixnet_sender(&self) -> MixnetMessageInputSender {
self.message_sender.clone()
}
pub fn subscribe(&self) -> MixnetMessageBroadcastReceiver {
self.message_broadcast.subscribe()
}
pub async fn wait(self) {
if let Err(err) = self.handle.await {
tracing::error!("Error waiting for auth clients mixnet listener to stop: {err}");
pub fn mixnet_cancel_token(&self) -> CancellationToken {
self.mixnet_cancellation_token.clone()
}
pub async fn stop(self) {
// If shutdown was externally called, that call is a no-op
// If we're only stopping this, it is very much needed
self.cancellation_token.cancel();
match self.handle.await {
Ok(auth_client_mixnet_listener) => {
auth_client_mixnet_listener.disconnect_mixnet_client().await;
}
Err(e) => {
tracing::error!("Error waiting for auth clients mixnet listener to stop: {e}");
}
}
}
}
@@ -76,6 +76,7 @@ pub struct Cli {
)]
pub(crate) deposits_buffer_size: usize,
/// Specify interval at which the nym credential proxy should check if signing quorum is available
#[clap(
long,
env = "NYM_CREDENTIAL_PROXY_QUORUM_CHECK_INTERVAL",
+1
View File
@@ -12,6 +12,7 @@ license.workspace = true
workspace = true
[dependencies]
bincode.workspace = true
bytes.workspace = true
futures.workspace = true
thiserror.workspace = true
+23 -24
View File
@@ -1,15 +1,11 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
// To remove with the Registration Client PR
#![allow(clippy::unwrap_used)]
use std::{sync::Arc, time::Duration};
use std::time::Duration;
use nym_ip_packet_requests::IpPair;
use nym_sdk::mixnet::{
InputMessage, MixnetClient, MixnetClientSender, MixnetMessageSender, Recipient,
TransmissionLane,
InputMessage, MixnetClient, MixnetMessageSender, Recipient, TransmissionLane,
};
use tokio::time::sleep;
use tokio_util::sync::CancellationToken;
@@ -27,8 +23,6 @@ use crate::{
helpers::check_ipr_message_version,
};
pub type SharedMixnetClient = Arc<tokio::sync::Mutex<Option<MixnetClient>>>;
const IPR_CONNECT_TIMEOUT: Duration = Duration::from_secs(10);
#[derive(Clone, Debug, PartialEq, Eq)]
@@ -43,24 +37,24 @@ enum ConnectionState {
pub struct IprClientConnect {
// During connection we need the mixnet client, but once connected we expect to setup a channel
// from the main mixnet listener at the top-level.
// As such, we drop the shared mixnet client once we're connected.
mixnet_client: SharedMixnetClient,
mixnet_sender: MixnetClientSender,
mixnet_client: MixnetClient,
connected: ConnectionState,
cancel_token: CancellationToken,
}
impl IprClientConnect {
pub async fn new(mixnet_client: SharedMixnetClient, cancel_token: CancellationToken) -> Self {
let mixnet_sender = mixnet_client.lock().await.as_ref().unwrap().split_sender();
pub async fn new(mixnet_client: MixnetClient, cancel_token: CancellationToken) -> Self {
Self {
mixnet_client,
mixnet_sender,
connected: ConnectionState::Disconnected,
cancel_token,
}
}
pub fn into_mixnet_client(self) -> MixnetClient {
self.mixnet_client
}
pub async fn connect(&mut self, ip_packet_router_address: Recipient) -> Result<IpPair> {
if self.connected != ConnectionState::Disconnected {
return Err(Error::AlreadyConnected);
@@ -95,12 +89,12 @@ impl IprClientConnect {
// We use 20 surbs for the connect request because typically the IPR is configured to have
// a min threshold of 10 surbs that it reserves for itself to request additional surbs.
let surbs = 20;
self.mixnet_sender
self.mixnet_client
.send(create_input_message(
ip_packet_router_address,
request,
surbs,
))
)?)
.await
.map_err(|err| Error::SdkError(Box::new(err)))?;
@@ -133,26 +127,31 @@ impl IprClientConnect {
}
}
async fn listen_for_connect_response(&self, request_id: u64) -> Result<IpPair> {
async fn listen_for_connect_response(&mut self, request_id: u64) -> Result<IpPair> {
// Connecting is basically synchronous from the perspective of the mixnet client, so it's safe
// to just grab ahold of the mutex and keep it until we get the response.
let mut mixnet_client_handle = self.mixnet_client.lock().await;
let mixnet_client = mixnet_client_handle.as_mut().unwrap();
let timeout = sleep(IPR_CONNECT_TIMEOUT);
tokio::pin!(timeout);
let mixnet_cancel_token = self.mixnet_client.cancellation_token();
loop {
tokio::select! {
_ = self.cancel_token.cancelled() => {
error!("Cancelled while waiting for reply to connect request");
return Err(Error::Cancelled);
},
_ = mixnet_cancel_token.cancelled() => {
error!("Mixnet client stopped while waiting for reply to connect request");
return Err(Error::Cancelled);
},
_ = &mut timeout => {
error!("Timed out waiting for reply to connect request");
return Err(Error::TimeoutWaitingForConnectResponse);
},
msgs = mixnet_client.wait_for_messages() => match msgs {
msgs = self.mixnet_client.wait_for_messages() => match msgs {
None => {
return Err(Error::NoMixnetMessagesReceived);
}
@@ -188,12 +187,12 @@ fn create_input_message(
recipient: Recipient,
request: IpPacketRequest,
surbs: u32,
) -> InputMessage {
InputMessage::new_anonymous(
) -> Result<InputMessage> {
Ok(InputMessage::new_anonymous(
recipient,
request.to_bytes().unwrap(),
request.to_bytes()?,
surbs,
TransmissionLane::General,
None,
)
))
}
+2 -7
View File
@@ -18,9 +18,6 @@ pub enum Error {
)]
ReceivedResponseWithNewVersion { expected: u8, received: u8 },
#[error("got reply for connect request, but it appears intended for the wrong address?")]
GotReplyIntendedForWrongAddress,
#[error("unexpected connect response")]
UnexpectedConnectResponse,
@@ -42,10 +39,8 @@ pub enum Error {
#[error("already connected to the mixnet")]
AlreadyConnected,
#[error("failed to create connect request")]
FailedToCreateConnectRequest {
source: nym_ip_packet_requests::sign::SignatureError,
},
#[error(transparent)]
Bincode(#[from] bincode::Error),
}
// Result type based on our error type
@@ -3,7 +3,7 @@
[package]
name = "nym-node-status-agent"
version = "1.0.6"
version = "1.0.7"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
@@ -5,7 +5,7 @@ echo "Starting agent loop with sleep interval: ${SLEEP_TIME}s"
# Trap SIGTERM to allow graceful shutdown
trap "echo 'Stopping...'; exit 0" SIGTERM
DEFAULT_ARGS="run-probe --server \"${NODE_STATUS_AGENT_SERVER_ADDRESS}|${NODE_STATUS_AGENT_SERVER_PORT}\" --mnemonic \"${NYM_NODE_MNEMONICS}\""
DEFAULT_ARGS="run-probe --server \"${NODE_STATUS_AGENT_SERVER_ADDRESS}|${NODE_STATUS_AGENT_SERVER_PORT}\" "
ARGS=${NODE_STATUS_AGENT_ARGS:-${DEFAULT_ARGS}}
COMMAND="/nym/nym-node-status-agent ${ARGS}"
@@ -15,6 +15,6 @@ echo "command = '${COMMAND}'"
# Run probe in an infinite loop
while true; do
eval $COMMAND
eval "$COMMAND"
sleep "$SLEEP_TIME"
done
@@ -59,10 +59,6 @@ pub(crate) enum Command {
#[arg(long, env = "NODE_STATUS_AGENT_PROBE_PATH")]
probe_path: String,
/// mnemonic for acquiring zk-nyms
#[arg(long, env = "NYM_NODE_MNEMONICS")]
mnemonic: String,
#[arg(
long,
env = "NODE_STATUS_AGENT_PROBE_EXTRA_ARGS",
@@ -83,7 +79,6 @@ impl Args {
Command::RunProbe {
server,
probe_path,
mnemonic,
probe_extra_args,
} => {
// Parse server configs
@@ -98,7 +93,7 @@ impl Args {
}
}
run_probe::run_probe(&servers, probe_path, mnemonic, probe_extra_args)
run_probe::run_probe(&servers, probe_path, probe_extra_args)
.await
.inspect_err(|err| {
tracing::error!("{err}");
@@ -3,7 +3,6 @@ use crate::cli::{GwProbe, ServerConfig};
pub(crate) async fn run_probe(
servers: &[ServerConfig],
probe_path: &str,
mnemonic: &str,
probe_extra_args: &Vec<String>,
) -> anyhow::Result<()> {
if servers.is_empty() {
@@ -33,111 +32,89 @@ pub(crate) async fn run_probe(
auth_key,
);
match ns_api_client.request_testrun().await {
Ok(Some(testrun)) => {
tracing::info!(
"Received testrun {} for gateway {} from primary",
testrun.testrun_id,
testrun.gateway_identity_key
);
// Run the probe
let log = probe.run_and_get_log(
&Some(testrun.gateway_identity_key.clone()),
mnemonic,
probe_extra_args,
);
// Submit to ALL servers in parallel
let handles = servers
.iter()
.enumerate()
.map(|(idx, server)| {
let testrun = testrun.clone();
let log = log.clone();
async move {
let auth_key = nym_crypto::asymmetric::ed25519::PrivateKey::from_bytes(
&server.auth_key.to_bytes(),
)
.expect("Failed to clone auth key");
let client = nym_node_status_client::NsApiClient::new(
&server.address,
server.port,
auth_key,
);
let result = if idx == 0 {
// Primary server: submit regular results without context
client
.submit_results(
testrun.testrun_id as i64,
log,
testrun.assigned_at_utc,
)
.await
} else {
// Other servers: submit results with context
client
.submit_results_with_context(
testrun.testrun_id,
log,
testrun.assigned_at_utc,
testrun.gateway_identity_key,
)
.await
};
(idx, server.address.clone(), server.port, result)
}
})
.collect::<Vec<_>>();
let results = futures::future::join_all(handles).await;
for result in results {
match result.3 {
Ok(()) => {
let method = if result.0 == 0 {
"regular"
} else {
"with context"
};
tracing::info!(
"✅ Successfully submitted {} to server[{}] {}:{}",
method,
result.0,
result.1,
result.2
);
}
Err(e) => {
let method = if result.0 == 0 {
"regular"
} else {
"with context"
};
tracing::warn!(
"❌ Failed to submit {} to server[{}] {}:{} - {}",
method,
result.0,
result.1,
result.2,
e
);
}
}
}
Ok(())
}
let testrun = match ns_api_client.request_testrun().await {
Ok(Some(testrun)) => testrun,
Ok(None) => {
tracing::info!("No testruns available from primary server");
Ok(())
return Ok(());
}
Err(e) => {
tracing::error!("Failed to contact primary server: {}", e);
Err(e)
Err(err) => {
tracing::error!("Failed to contact primary server: {err}");
return Err(err);
}
};
let testrun_id = testrun.assignment.testrun_id;
let testrun_assigned_at = testrun.assignment.assigned_at_utc;
let gateway_identity_key = testrun.assignment.gateway_identity_key;
tracing::info!("Received testrun {testrun_id} for gateway {gateway_identity_key} from primary",);
// Run the probe
let log = probe.run_and_get_log(
&Some(gateway_identity_key.clone()),
probe_extra_args,
testrun.ticket_materials,
);
// Submit to ALL servers in parallel
let handles = servers
.iter()
.enumerate()
.map(move |(idx, server)| {
let log = log.clone();
let gateway_identity_key = gateway_identity_key.clone();
async move {
let auth_key = nym_crypto::asymmetric::ed25519::PrivateKey::from_bytes(
&server.auth_key.to_bytes(),
)
.expect("Failed to clone auth key");
let client = nym_node_status_client::NsApiClient::new(
&server.address,
server.port,
auth_key,
);
let result = if idx == 0 {
// Primary server: submit regular results without context
client
.submit_results(testrun_id as i64, log, testrun_assigned_at)
.await
} else {
// Other servers: submit results with context
client
.submit_results_with_context(
testrun_id,
log,
testrun_assigned_at,
gateway_identity_key,
)
.await
};
(idx, server.address.clone(), server.port, result)
}
})
.collect::<Vec<_>>();
let results = futures::future::join_all(handles).await;
for (index, server_address, server_port, result) in results {
let method = if index == 0 {
"regular"
} else {
"with context"
};
match result {
Ok(()) => {
tracing::info!("✅ Successfully submitted {method} to server[{index}] {server_address}:{server_port}");
}
Err(e) => {
tracing::warn!("❌ Failed to submit {method} to server[{index}] {server_address}:{server_port} - {e}");
}
}
}
Ok(())
}
@@ -1,3 +1,4 @@
use nym_node_status_client::models::{AttachedTicketMaterials, VersionedSerialise};
use tracing::{debug, error, info};
pub(crate) struct GwProbe {
@@ -73,8 +74,8 @@ impl GwProbe {
pub(crate) fn run_and_get_log(
&self,
gateway_key: &Option<String>,
mnemonic: &str,
probe_extra_args: &Vec<String>,
ticket_materials: AttachedTicketMaterials,
) -> String {
let mut command = std::process::Command::new(&self.path);
command.stdout(std::process::Stdio::piped());
@@ -82,7 +83,6 @@ impl GwProbe {
if let Some(gateway_id) = gateway_key {
command.arg("--gateway").arg(gateway_id);
}
command.arg("--mnemonic").arg(mnemonic);
tracing::info!("Extra args for the probe:");
for arg in probe_extra_args {
@@ -94,6 +94,14 @@ impl GwProbe {
command.arg(format!("--{name}")).arg(value);
}
info!("attaching ticket materials to the probe");
let serialised = ticket_materials.to_serialised_string();
command.arg("--ticket-materials").arg(serialised);
command.arg("--ticket-materials-revision").arg(
<AttachedTicketMaterials as VersionedSerialise>::CURRENT_SERIALISATION_REVISION
.to_string(),
);
match command.spawn() {
Ok(child) => {
if let Ok(output) = child.wait_with_output() {
@@ -0,0 +1,32 @@
{
"db_name": "SQLite",
"query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_key, serialization_revision as \"serialization_revision: u8\"\n FROM master_verification_key WHERE epoch_id = ?\n ",
"describe": {
"columns": [
{
"name": "epoch_id: u32",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "serialised_key",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "serialization_revision: u8",
"ordinal": 2,
"type_info": "Integer"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false
]
},
"hash": "0112296b190328a3856d1adf51aafa2525da6c0b871633aad80ad555db9cf47c"
}
@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT epoch_id as \"epoch_id: i32\", serialised_key, serialization_revision as \"serialization_revision: i16\"\n FROM master_verification_key WHERE epoch_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "epoch_id: i32",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "serialised_key",
"type_info": "Bytea"
},
{
"ordinal": 2,
"name": "serialization_revision: i16",
"type_info": "Int2"
}
],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "1b626bc314423d78b7810dbb81ab620b7b75a61def51e229a3f93aac3f91322c"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO global_coin_index_signatures(epoch_id, serialised_signatures, serialization_revision) VALUES ($1, $2, $3)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Bytea",
"Int2"
]
},
"nullable": []
},
"hash": "1dfa8fe950f6126e87daaef8cceaf5ba8d4263e56ab489bafd08d420195f3670"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO ecash_deposit_usage (deposit_id, ticketbooks_requested_on, client_pubkey, request_uuid)\n VALUES (?, ?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "1fc72f8ba24039548047e1766c9105614dea7fd301f0ec38bfe85bfe546dad40"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO master_verification_key(epoch_id, serialised_key, serialization_revision) VALUES ($1, $2, $3)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Bytea",
"Int2"
]
},
"nullable": []
},
"hash": "2226e8df296fc708cdbb15128d97cf948453bba95f75f50e3cbb0b0d54cf88d9"
}
@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT SUM(total_tickets - used_tickets) AS available_tickets\n FROM ecash_ticketbook\n WHERE ticketbook_type = $1;\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "available_tickets",
"type_info": "Int8"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
null
]
},
"hash": "256a87f60c00040ccce949308928c8fe61fed57837756d7badd38b24c6182a9d"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n DELETE FROM blinded_shares WHERE created < ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "28681fcd8e2d4326f628681b8f2a317aabce063a650be362d3a8ed83cc7c3549"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO global_expiration_date_signatures(expiration_date, epoch_id, serialised_signatures, serialization_revision)\n VALUES (?, ?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "2930ca6e3875c74acb7abb9ad889f166ad7f57681f76a1d0c7723d007c1f2c1e"
}
@@ -0,0 +1,20 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO ecash_ticketbook\n (serialization_revision, ticketbook_data, expiration_date, ticketbook_type, epoch_id, total_tickets, used_tickets)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int2",
"Bytea",
"Date",
"Text",
"Int4",
"Int4",
"Int4"
]
},
"nullable": []
},
"hash": "2bd12767753de51209a0599bdc65af76e8b7337f070151ddd6470c3ee6849dcf"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO pending_issuance\n (deposit_id, serialization_revision, pending_ticketbook_data, expiration_date)\n VALUES ($1, $2, $3, $4)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Int2",
"Bytea",
"Date"
]
},
"nullable": []
},
"hash": "2ee6b058d423a66114d8411e7c287ade31137b30407dc0254d30f60e2d0101cf"
}
@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "\n SELECT error_message\n FROM blinded_shares\n WHERE id = ?;\n ",
"describe": {
"columns": [
{
"name": "error_message",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true
]
},
"hash": "396f40c33f0f62796eb7449d640bd97845350f4fb9f806c60b93c7cebd5e410d"
}
@@ -0,0 +1,26 @@
{
"db_name": "SQLite",
"query": "\n SELECT serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM global_expiration_date_signatures\n WHERE expiration_date = ? AND epoch_id = ?\n ",
"describe": {
"columns": [
{
"name": "serialised_signatures",
"ordinal": 0,
"type_info": "Blob"
},
{
"name": "serialization_revision: u8",
"ordinal": 1,
"type_info": "Integer"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false
]
},
"hash": "3cc446220668fb3e02f0578104291d2a2af57656b405212af414d765b2263347"
}
@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "\n UPDATE gateways SET bridges = $1 WHERE gateway_identity_key = $2;\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Text"
]
},
"nullable": []
},
"hash": "452c787b2e4550220958310586aadd3611392e2a0297ec028e8bf13aacd8c791"
}
@@ -0,0 +1,34 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT epoch_id as \"epoch_id: i32\", serialised_signatures, serialization_revision as \"serialization_revision: i16\"\n FROM global_coin_index_signatures WHERE epoch_id = $1\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "epoch_id: i32",
"type_info": "Int4"
},
{
"ordinal": 1,
"name": "serialised_signatures",
"type_info": "Bytea"
},
{
"ordinal": 2,
"name": "serialization_revision: i16",
"type_info": "Int2"
}
],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": [
false,
false,
false
]
},
"hash": "4d2979588fda03671a99d7f1174d1215cc01bf67fe974a5884673a92500db25f"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n DELETE FROM partial_blinded_wallet_failure WHERE created < ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "52b378e282d93db941eff53b5b311e5732ece0bf84ea98f2328b20add8f2b5ef"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO master_verification_key(epoch_id, serialised_key, serialization_revision) VALUES (?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "70d8f240ad6edda6b8c7f2e800e7fca89d80869484f2f3c66cabb898f0298c62"
}
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE ecash_ticketbook SET used_tickets = used_tickets + 1 WHERE id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4"
]
},
"nullable": []
},
"hash": "8c92a413a2853a2508c0e8a17ae8723c400930663c4c76e96dfdc7e8c98501ca"
}
@@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO distributed_partial_ticketbook\n (testrun_id, ticketbook_id, assigned_index)\n VALUES ($1, $2, $3)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Int4",
"Int4",
"Int4"
]
},
"nullable": []
},
"hash": "8dee97133fe356820af331fe3cff80bdb29a0e49190ecaf98141698f14999bbd"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO partial_blinded_wallet_failure(corresponding_deposit, epoch_id, expiration_date, node_id, created, failure_message)\n VALUES (?, ?, ?, ?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "97d97ebb6bc8f4114fdea9ebc9f57f91a11f5057273cb70bd0e629712d17dd41"
}
@@ -0,0 +1,17 @@
{
"db_name": "PostgreSQL",
"query": "\n INSERT INTO global_expiration_date_signatures(expiration_date, epoch_id, serialised_signatures, serialization_revision)\n VALUES ($1, $2, $3, $4)\n ",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Date",
"Int4",
"Bytea",
"Int2"
]
},
"nullable": []
},
"hash": "a859dc5549c099aebf04236b2268f9420b269de9b9b3ffcfc4358940c4f0a9fc"
}
@@ -0,0 +1,32 @@
{
"db_name": "SQLite",
"query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM global_coin_index_signatures WHERE epoch_id = ?\n ",
"describe": {
"columns": [
{
"name": "epoch_id: u32",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "serialised_signatures",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "serialization_revision: u8",
"ordinal": 2,
"type_info": "Integer"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false
]
},
"hash": "a8b7ce0fe4755c28b96d1e503e313ab15fed747fb0cee1c9f949fb58461b3f79"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n DELETE FROM partial_blinded_wallet WHERE created < ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "b8257a0832d0124f0a8aaaf81dc6a811c593aea8febf1f891117e5e84213f147"
}
@@ -0,0 +1,38 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n t1.node_id as \"node_id!\",\n t1.blinded_signature as \"blinded_signature!\",\n t1.epoch_id as \"epoch_id!\",\n t1.expiration_date as \"expiration_date!: Date\"\n FROM partial_blinded_wallet as t1\n JOIN ecash_deposit_usage as t2\n on t1.corresponding_deposit = t2.deposit_id\n JOIN blinded_shares as t3\n ON t2.request_uuid = t3.request_uuid\n WHERE t3.device_id = ? AND t3.credential_id = ?;\n ",
"describe": {
"columns": [
{
"name": "node_id!",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "blinded_signature!",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "epoch_id!",
"ordinal": 2,
"type_info": "Integer"
},
{
"name": "expiration_date!: Date",
"ordinal": 3,
"type_info": "Date"
}
],
"parameters": {
"Right": 2
},
"nullable": [
true,
true,
true,
true
]
},
"hash": "c2b841762bdb963fff337ef5c8ec9f560017b4da6b0303ea0397d9568229e167"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "INSERT INTO global_coin_index_signatures(epoch_id, serialised_signatures, serialization_revision) VALUES (?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "d3510846941fa2525926b9bfbcdabd806877ce914b514d4f7cd6be318c4debe6"
}
@@ -0,0 +1,29 @@
{
"db_name": "PostgreSQL",
"query": "\n SELECT serialised_signatures, serialization_revision as \"serialization_revision: i16\"\n FROM global_expiration_date_signatures\n WHERE expiration_date = $1 AND epoch_id = $2\n ",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "serialised_signatures",
"type_info": "Bytea"
},
{
"ordinal": 1,
"name": "serialization_revision: i16",
"type_info": "Int2"
}
],
"parameters": {
"Left": [
"Date",
"Int4"
]
},
"nullable": [
false,
false
]
},
"hash": "d574930f0f8e41466f2249e36311dc42dd190bfb1810a631a608793000e20241"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO partial_blinded_wallet(corresponding_deposit, epoch_id, expiration_date, node_id, created, blinded_signature)\n VALUES (?, ?, ?, ?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "db176e98198fe594d88eb860d918f633a94d18a19b7f0f96935a62560def7d0f"
}
@@ -0,0 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE ecash_deposit_usage\n SET ticketbook_request_error = ?\n WHERE deposit_id = ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "e584253e3856355899537eb8fc152f2bfed2d918b894ec0f588e38dd5e8ad726"
}
@@ -0,0 +1,38 @@
{
"db_name": "SQLite",
"query": "\n SELECT t1.node_id, t1.blinded_signature, t1.epoch_id, t1.expiration_date as \"expiration_date!: Date\"\n FROM partial_blinded_wallet as t1\n JOIN ecash_deposit_usage as t2\n on t1.corresponding_deposit = t2.deposit_id\n JOIN blinded_shares as t3\n ON t2.request_uuid = t3.request_uuid\n WHERE t3.id = ?;\n ",
"describe": {
"columns": [
{
"name": "node_id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "blinded_signature",
"ordinal": 1,
"type_info": "Blob"
},
{
"name": "epoch_id",
"ordinal": 2,
"type_info": "Integer"
},
{
"name": "expiration_date!: Date",
"ordinal": 3,
"type_info": "Date"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false
]
},
"hash": "e77ffab19b099b84470fe5611716a2e314787586a46cffd074abb67f2f4d109e"
}
@@ -0,0 +1,20 @@
{
"db_name": "SQLite",
"query": "\n SELECT error_message\n FROM blinded_shares\n WHERE device_id = ? AND credential_id = ?;\n ",
"describe": {
"columns": [
{
"name": "error_message",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 2
},
"nullable": [
true
]
},
"hash": "ef60c2683211cc4ec2d3e46392518a1f62fa67dfe8f130deb876ebee11bf1602"
}
@@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT\n gw.gateway_identity_key as \"gateway_identity_key!\",\n gw.bonded as \"bonded: bool\",\n gw.performance as \"performance!\",\n gw.self_described as \"self_described?\",\n gw.explorer_pretty_bond as \"explorer_pretty_bond?\",\n gw.last_probe_result as \"last_probe_result?\",\n gw.last_probe_log as \"last_probe_log?\",\n gw.last_testrun_utc as \"last_testrun_utc?\",\n gw.last_updated_utc as \"last_updated_utc!\",\n COALESCE(gd.moniker, 'NA') as \"moniker!\",\n COALESCE(gd.website, 'NA') as \"website!\",\n COALESCE(gd.security_contact, 'NA') as \"security_contact!\",\n COALESCE(gd.details, 'NA') as \"details!\"\n FROM gateways gw\n LEFT JOIN gateway_description gd\n ON gw.gateway_identity_key = gd.gateway_identity_key\n ORDER BY gw.gateway_identity_key",
"query": "SELECT\n gw.gateway_identity_key as \"gateway_identity_key!\",\n gw.bonded as \"bonded: bool\",\n gw.performance as \"performance!\",\n gw.self_described as \"self_described?\",\n gw.explorer_pretty_bond as \"explorer_pretty_bond?\",\n gw.last_probe_result as \"last_probe_result?\",\n gw.last_probe_log as \"last_probe_log?\",\n gw.last_testrun_utc as \"last_testrun_utc?\",\n gw.last_updated_utc as \"last_updated_utc!\",\n gw.bridges as \"bridges?: serde_json::Value\",\n COALESCE(gd.moniker, 'NA') as \"moniker!\",\n COALESCE(gd.website, 'NA') as \"website!\",\n COALESCE(gd.security_contact, 'NA') as \"security_contact!\",\n COALESCE(gd.details, 'NA') as \"details!\"\n FROM gateways gw\n LEFT JOIN gateway_description gd\n ON gw.gateway_identity_key = gd.gateway_identity_key\n ORDER BY gw.gateway_identity_key",
"describe": {
"columns": [
{
@@ -50,21 +50,26 @@
},
{
"ordinal": 9,
"name": "bridges?: serde_json::Value",
"type_info": "Jsonb"
},
{
"ordinal": 10,
"name": "moniker!",
"type_info": "Varchar"
},
{
"ordinal": 10,
"ordinal": 11,
"name": "website!",
"type_info": "Varchar"
},
{
"ordinal": 11,
"ordinal": 12,
"name": "security_contact!",
"type_info": "Varchar"
},
{
"ordinal": 12,
"ordinal": 13,
"name": "details!",
"type_info": "Varchar"
}
@@ -82,11 +87,12 @@
true,
true,
false,
true,
null,
null,
null,
null
]
},
"hash": "c09be83be9d5a679a08ab528c6305fddae499f26b55c0f8388392b27b179514b"
"hash": "f25a4eb90c11957669cfad4800d7a0a384b672077abb123c3437962def194e8a"
}
@@ -3,7 +3,7 @@
[package]
name = "nym-node-status-api"
version = "4.0.3"
version = "4.0.5"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
@@ -17,6 +17,7 @@ ammonia = { workspace = true }
anyhow = { workspace = true }
axum = { workspace = true, features = ["tokio", "macros"] }
bip39 = { workspace = true }
bs58 = { workspace = true }
celes = { workspace = true }
clap = { workspace = true, features = ["cargo", "derive", "env", "string"] }
cosmwasm-std = { workspace = true }
@@ -25,6 +26,8 @@ humantime = { workspace = true }
itertools = { workspace = true }
moka = { workspace = true, features = ["future"] }
nym-credentials = { path = "../../common/credentials" }
nym-credential-proxy-lib = { path = "../../common/credential-proxy" }
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] }
nym-bin-common = { path = "../../common/bin-common", features = ["models"] }
@@ -38,6 +41,7 @@ nym-statistics-common = { path = "../../common/statistics" }
nym-validator-client = { path = "../../common/client-libs/validator-client" }
nym-task = { path = "../../common/task" }
nym-node-requests = { path = "../../nym-node/nym-node-requests", features = ["openapi"] }
nym-ecash-time = { path = "../../common/ecash-time", features = ["expiration"] }
rand = { workspace = true }
rand_chacha = { workspace = true }
@@ -54,6 +58,7 @@ thiserror = { workspace = true }
time = { workspace = true, features = ["formatting"] }
tokio = { workspace = true, features = ["rt-multi-thread"] }
tokio-util = { workspace = true }
tokio-stream = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
tracing-log = { workspace = true }
@@ -61,6 +66,7 @@ tower-http = { workspace = true, features = ["cors", "trace"] }
utoipa = { workspace = true, features = ["axum_extras", "time"] }
utoipa-swagger-ui = { workspace = true, features = ["axum"] }
utoipauto = { workspace = true }
zeroize = { workspace = true }
nym-node-metrics = { path = "../../nym-node/nym-node-metrics" }
@@ -75,5 +81,5 @@ sqlx = { workspace = true, features = [
] }
[dev-dependencies]
axum-test = "17.3.0"
axum-test = { workspace = true }
time = { workspace = true, features = ["macros"] }
@@ -0,0 +1,113 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: GPL-3.0-only
*/
CREATE TABLE master_verification_key
(
epoch_id INTEGER PRIMARY KEY NOT NULL,
serialization_revision SMALLINT NOT NULL,
serialised_key BYTEA NOT NULL
);
CREATE TABLE global_coin_index_signatures
(
-- we can only have a single entry
epoch_id INTEGER PRIMARY KEY NOT NULL,
serialization_revision SMALLINT NOT NULL,
-- combined signatures for all indices
serialised_signatures BYTEA NOT NULL
);
CREATE TABLE global_expiration_date_signatures
(
expiration_date DATE NOT NULL,
epoch_id INTEGER NOT NULL,
serialization_revision SMALLINT NOT NULL,
-- combined signatures for all tuples issued for given day
serialised_signatures BYTEA NOT NULL,
PRIMARY KEY (epoch_id, expiration_date)
);
CREATE TABLE ticketbook_deposit
(
deposit_id INTEGER PRIMARY KEY NOT NULL,
deposit_tx_hash TEXT NOT NULL,
requested_on TIMESTAMP WITHOUT TIME ZONE NOT NULL,
deposit_amount TEXT NOT NULL,
client_pubkey BYTEA NOT NULL,
ed25519_deposit_private_key BYTEA NOT NULL
);
CREATE TABLE pending_issuance
(
deposit_id INTEGER NOT NULL PRIMARY KEY,
failure_message TEXT NOT NULL,
-- introduce a way for us to introduce breaking changes in serialization of data
serialization_revision SMALLINT NOT NULL,
pending_ticketbook_data BYTEA 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 global_expiration_date_signatures (epoch_id, expiration_date)
);
CREATE TABLE ecash_ticketbook
(
id SERIAL NOT NULL PRIMARY KEY,
-- introduce a way for us to introduce breaking changes in serialization of data
serialization_revision SMALLINT 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 BYTEA 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 global_coin_index_signatures (epoch_id),
-- for each ticketbook we MUST have corresponding expiration date signatures
FOREIGN KEY (expiration_date, epoch_id) REFERENCES global_expiration_date_signatures (expiration_date, epoch_id)
);
CREATE TABLE distributed_partial_ticketbook
(
testrun_id INTEGER NOT NULL,
ticketbook_id INTEGER NOT NULL,
-- index of the ticket from the underlying ticketbook given to the runner
assigned_index INTEGER NOT NULL,
-- FOREIGN KEYS:
FOREIGN KEY (testrun_id) REFERENCES testruns (id),
FOREIGN KEY (ticketbook_id) REFERENCES ecash_ticketbook (id)
);
@@ -0,0 +1,2 @@
ALTER TABLE gateways
ADD COLUMN bridges jsonb;

Some files were not shown because too many files have changed in this diff Show More