Compare commits

...

53 Commits

Author SHA1 Message Date
Simon Wicky bda262810b changes due to crate moving 2025-09-05 15:03:47 +02:00
Simon Wicky 1596277c85 moving crates as is 2025-09-05 14:29:35 +02:00
Bogdan-Ștefan Neacşu 1898853263 Use default value for the ports until api is deployed 2025-09-04 11:42:11 +03:00
durch 4e5ccf7926 fix: provide all API URLs for automatic failover in endpoint rotation
Previously, when rotating API endpoints, only a single URL was provided to the
HTTP client, defeating the purpose of having multiple URLs for resilience.

Changes:
- NymApiTopologyProvider now provides all URLs in rotated order when switching endpoints
- NymApisClient similarly provides all URLs starting from the working endpoint
- Added clarifying comments for broadcast/exhaustive query methods where single URLs are intentionally used
- This enables the HTTP client's built-in failover mechanism while maintaining endpoint rotation behavior

The fix ensures that if the primary endpoint fails, the client can automatically
failover to alternative endpoints without manual intervention, improving overall
network resilience.
2025-08-29 13:37:45 +02:00
durch 0a8eb940bb feat: complete migration to unified HTTP client and fix all compilation errors
- Added missing NymApiClientExt trait methods (get_all_expanded_nodes, change_base_urls)
- Fixed all compilation errors across the workspace
- Updated nym-node to use unified client instead of deprecated NymApiClient
- Fixed type conversions for RewardedSetResponse → EpochRewardedSet
- Added nym-http-api-client dependency where needed
- Updated all examples and documentation to use new client API
2025-08-29 13:36:22 +02:00
durch 34fb67602c fix: resolve all compilation errors after NymApiClient migration
- Add missing nym-http-api-client dependencies to multiple crates
- Add NymApiClientExt trait imports where needed
- Fix type mismatches from NymApiClient to unified Client
- Add error conversions for NymAPIError in various error enums
- Implement missing trait methods (get_current_rewarded_set, get_all_basic_nodes_with_metadata, get_all_described_nodes)
- Fix type conversions for RewardedSetResponse in network monitor
- Update all API client instantiation to use new unified HTTP client
2025-08-29 13:35:41 +02:00
durch 766ae8dd8e feat: migrate NymApiClient usage to unified HTTP client
- Wire up domain fronting configuration in NymNetworkDetails
- Implement NymApiClientExt trait for base nym_http_api_client::Client
- Migrate direct NymApiClient usage in multiple components:
  - nym-network-monitor
  - verloc measurements
  - connection tester
  - coconut/ecash client
  - validator rewarder
- Add Copy derive to ApiUrlConst to enable iteration
- Update error handling and Display implementations

This enables automatic domain fronting for all Nym API calls via the configured CDN front hosts.
2025-08-29 13:33:37 +02:00
durch bd1fd73ba0 feat: unify HTTP client creation and enable domain fronting
Enhanced the base nym_http_api_client to reduce fragmentation and enable domain fronting:

- Added SerializationFormat enum for explicit JSON/bincode choice (no auto-detection)
- Added from_network() method to create clients from NymNetworkDetails with domain fronting
- Added with_bincode() builder method for explicit serialization configuration
- Set Accept header based on serialization preference
- Added deprecation paths for NymApiClient wrapper and nym_api::Client re-export
- Enabled domain fronting support via network defaults feature

This is part of a broader effort to consolidate HTTP client implementations across the codebase,
reducing ~500 lines of wrapper code and providing automatic domain fronting for censorship resistance.
2025-08-29 13:33:37 +02:00
Jędrzej Stuczyński b2266d04ef chore: internal hidden command to force advance nyx epoch (#5964) 2025-08-29 11:41:22 +01:00
Jędrzej Stuczyński 911b365609 chore: purge temp databases on build (#5984)
* purge any temp databases on build

* updated min rust version

* fixed clippy::manual_abs_diff' in verloc due to updated msrv

* wasm
2025-08-29 11:41:08 +01:00
Jędrzej Stuczyński e9acc014ed feat: credential proxy deposit pool (#5945)
* chore: rename VpnApiError to CredentialProxyError

* reorganise deposit flow

* updated sql tables et al.

* insert information about deposit usage failure

* remove old deposit maker

* nym credential proxy to monitor quorum state to stop issuance if it'd fail

* clippy

* target lock new modules

* windows clippy

* renamed migration file due to rebasing
2025-08-29 09:39:57 +01:00
Jędrzej Stuczyński 0f66e5a154 bugfix: make sure tables are removed in correct order to not trigger FK constraint issue (#5987) 2025-08-29 09:03:22 +01:00
Bogdan-Ștefan Neacşu f8337d9b38 Wireguard metadata client library (#5943) 2025-08-28 15:43:46 +03:00
Bogdan-Ștefan Neacşu 4fb252c44b Wireguard private metadata (#5915) 2025-08-28 15:14:52 +03:00
Jędrzej Stuczyński 17708cdf92 bugfix: manually calculate per node work on rewarded set changes (#5972) 2025-08-27 12:33:24 +01:00
Andrej Mihajlov a9c56ef9ac Merge pull request #5976 from nymtech/am/update-sysinfo
Update sysinfo to the latest
2025-08-27 10:58:06 +02:00
Jędrzej Stuczyński 724420f97c chore: move authenticator into gateway crate (#5982)
* removed unused bits of authenticator config

* moved authenticator into gateway

* cleaned up imports

* clippy
2025-08-27 09:05:02 +01:00
dynco-nym 0a48fa6172 Remove freshness check on testrun submit (#5977)
* Remove freshness check on testrun submit
- freshness is enforced by a background task
  that marks testruns as stale after a
  configured amount of time

* Move code around

* Add humantime

* Update launch script

* Fix typo

* Adjust agent run script

* Configure user agent

* Bump version
2025-08-26 12:26:13 +02:00
Andrej Mihajlov 5c8749a2e1 Update sysinfo to the latest
Shakes out windows@0.57 from the tree
2025-08-23 09:29:47 +02:00
p17o 18d9d807f2 Added VPS provider hostraha.com (#5959) 2025-08-22 12:31:31 +00:00
import this 3a7393d316 [DOCs/operators]: Roll back from v2025.15-gruyere to v2025.14-feta (#5973)
* release notes and version bump up

* update stats

* roll version back and comment new notes
2025-08-22 11:23:16 +00:00
import this 6ce5f707c6 [DOCs/operators]: Release notes for v2025.15 gruyere (#5969)
* release notes and version bump up

* update stats
2025-08-21 11:49:52 +00:00
benedetta davico 766a1d4497 Merge pull request #5965 from nymtech/benny/fix-ns-agent-ci
fixing the ci for ns agent
2025-08-21 12:22:05 +02:00
benedetta davico 35c83f0a31 Merge pull request #5967 from nymtech/release/2025.15-gruyere
merge gruyere to develop
2025-08-21 12:20:43 +02:00
benedetta davico 01dd4a7972 Merge pull request #5958 from nymtech/bugfix/linux-build-ci
bugfix: fix ci-build for linux (and use updated runner)
2025-08-21 10:32:51 +02:00
Jędrzej Stuczyński c2e335557e Feature/testing utils (#5963)
* helper wrapper for stream-sink channel

* similar helper for async read/write

* example tests and clippy
2025-08-20 16:17:09 +01:00
benedettadavico 40e1cbc7a9 update changelog 2025-08-20 12:34:04 +02:00
Jędrzej Stuczyński c133e0e88b chore: updated refs to cheddar rev of nym repo (#5955)
* chore: updated refs to cheddar rev of nym repo

* update statistics-api version
2025-08-19 09:28:42 +01:00
Andrej Mihajlov 5b716633de Merge pull request #5960 from nymtech/am/update-strum
Migrate strum to 0.27.2
2025-08-18 18:28:22 +02:00
Andrej Mihajlov 834538300d Migrate to strum 0.27.2 2025-08-18 18:02:41 +02:00
Jędrzej Stuczyński bd0d70f7cd bugfix: fix ci-build for linux (and use updated runner) 2025-08-15 09:37:41 +01:00
Jędrzej Stuczyński 979485c582 http api client adjustment (#5953)
* missing feature lock for attempting to clone client

* added helper macro to generate user agent without additional imports
2025-08-13 12:52:16 +01:00
Bogdan-Ștefan Neacşu d95f66bd90 Move credential verifier in peer controller (#5938)
* Move credential verifier in peer controller

* Send back errors of peer controller
2025-08-13 13:09:44 +03:00
Jędrzej Stuczyński 906dfb2fb0 change PK/FK on expiration date signatures tables (#5934)
* update nym-credential-proxy

* update credential-storage

* update nym-api

* clippy
2025-08-12 09:03:53 +01:00
Jędrzej Stuczyński 7daa726626 feat: introduce additional checks when attempting to send to bounded channels (#5941)
* feat: introduce additional checks when attempting to send to bounded channels

or to a fallible gateway

* return error rather than panic when merging socket during shutdown
2025-08-11 09:15:12 +01:00
Jędrzej Stuczyński 067f492ad6 chore: fix rust 1.89 clippy issues (#5944) 2025-08-08 13:03:05 +01:00
Jędrzej Stuczyński ed73ec9ce6 chore: remove unused import (#5942) 2025-08-07 09:56:08 +01:00
import this 61606630bd [DOCs/operators]: Release notes v2025.14-feta (#5935)
* update release

* fix typos
2025-08-06 08:24:38 +00:00
benedettadavico 2d3deeb424 bump versions 2025-08-06 09:56:08 +02:00
benedetta davico 3827dc357d Merge pull request #5936 from nymtech/release/2025.14-feta
Merge release/2025.14-feta to develop
2025-08-06 09:54:29 +02:00
benedetta davico a70e9e23d3 Merge branch 'develop' into release/2025.14-feta 2025-08-06 09:48:22 +02:00
Jędrzej Stuczyński dc59149a5d squashed feature/ecash-liveness-check (#5890) (#5890)
delay to gruyere

chore: delay to Feta

added threshold information to the response

nym api test clippy

bugfixes and endpoint improvements

expose results on api endpoints

wip: making nym api monitor network signers

added fallback legacy queries to get basic support idea

refactored the code to expose bool-only methods for status

ecash-signer-check lib for obtaining basic ecash signer information
2025-08-05 12:28:42 +01:00
benedetta davico e418c7587a Merge pull request #5914 from nymtech/feature/nym-node-gw-reset
nym-node debug command to reset providers db
2025-08-05 12:05:44 +02:00
import this 33339c085d [DOCs/operators]: Update ISP list (#5918)
* update ISP list

* remove typo
2025-07-31 13:47:27 +00:00
Sachin Kamath 863f329106 docs: update validator instructions and waitlist callout (#5922) 2025-07-30 15:03:39 +00:00
import this 314a37cabe WG exit policy scripts update (#5921)
* add NIP-3 ports to WG manager script

* add monero ports to local testing script

* console output snippet update
2025-07-30 09:43:39 +00:00
Jack Wampler 917f391948 Make DNS Resolver fallback optional (#5920)
default to no dns system fallback, but keep support
2025-07-29 11:00:24 -06:00
Jędrzej Stuczyński 0b4deda621 nym-node debug command to reset providers db 2025-07-25 13:33:12 +01:00
benedetta davico d01867ca8d save version 2025-07-25 11:27:42 +02:00
benedetta davico 502c63b291 Fix broken CI 2025-07-25 11:19:27 +02:00
Jędrzej Stuczyński a4e674c98b basic zulip client for sending messages (#5913) 2025-07-24 16:22:35 +01:00
Jędrzej Stuczyński 7f97f13799 chore: nym node tokio console (#5909)
* conditionally enable console-subscriber within nym-node

* Update ci-build-upload-binaries.yml

* Update ci-build-upload-binaries.yml

add features console

* updated feature name

* fixed filtering on tracing layers

* add track_caller when spawning futures for better tokio-console support

* allow [client] tasks to specify their names when used within tokio console

* clippy

* pre-emptively fix wasm clippy

---------

Co-authored-by: Tommy Verrall <60836166+tommyv1987@users.noreply.github.com>
2025-07-24 11:00:58 +01:00
benedettadavico 85604e8305 bump versions 2025-07-23 10:18:45 +02:00
399 changed files with 19267 additions and 7326 deletions
@@ -38,15 +38,14 @@ jobs:
rm -rf ci-builds || true
mkdir -p $OUTPUT_DIR
echo $OUTPUT_DIR
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libudev-dev
- name: Sets env vars for tokio if set in manual dispatch inputs
run: |
echo 'RUSTFLAGS="--cfg tokio_unstable"' >> $GITHUB_ENV
if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true
run: |
echo "RUSTFLAGS=--cfg tokio_unstable" >> $GITHUB_ENV
echo "CARGO_FEATURES=--features tokio-console" >> $GITHUB_ENV
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
@@ -103,7 +102,6 @@ jobs:
if [ ${{ github.event_name == 'workflow_dispatch' && inputs.enable_deb == true }} = true ]; then
cp target/debian/*.deb $OUTPUT_DIR
fi
- name: Deploy branch to CI www
continue-on-error: true
uses: easingthemes/ssh-deploy@main
+7 -7
View File
@@ -38,7 +38,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ arc-ubuntu-22.04, custom-windows-11, custom-macos-15 ]
os: [ arc-linux-latest, custom-windows-11, custom-macos-15 ]
runs-on: ${{ matrix.os }}
env:
CARGO_TERM_COLOR: always
@@ -46,9 +46,9 @@ jobs:
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler cmake
continue-on-error: true
if: contains(matrix.os, 'ubuntu')
if: contains(matrix.os, 'linux')
- name: Check out repository code
uses: actions/checkout@v4
@@ -63,7 +63,7 @@ jobs:
# To avoid running out of disk space, skip generating debug symbols
- name: Set debug to false (unix)
if: contains(matrix.os, 'ubuntu') || contains(matrix.os, 'mac')
if: contains(matrix.os, 'linux') || contains(matrix.os, 'mac')
run: |
sed -i.bak 's/\[profile.dev\]/\[profile.dev\]\ndebug = false/' Cargo.toml
git diff
@@ -93,14 +93,14 @@ jobs:
command: build
- name: Build all examples
if: contains(matrix.os, 'ubuntu')
if: contains(matrix.os, 'linux')
uses: actions-rs/cargo@v1
with:
command: build
args: --workspace --examples
- name: Run all tests
if: contains(matrix.os, 'ubuntu')
if: contains(matrix.os, 'linux')
uses: actions-rs/cargo@v1
env:
NYM_API: https://sandbox-nym-api1.nymtech.net/api
@@ -109,7 +109,7 @@ jobs:
args: --workspace
- name: Run expensive tests
if: (github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master') && contains(matrix.os, 'ubuntu')
if: (github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master') && contains(matrix.os, 'linux')
uses: actions-rs/cargo@v1
with:
command: test
@@ -40,7 +40,8 @@ jobs:
- name: Get version from cargo.toml
id: get_version
run: |
yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
echo "result=$VERSION" >> $GITHUB_OUTPUT
- name: cleanup-gateway-probe-ref
id: cleanup_gateway_probe_ref
@@ -52,13 +53,16 @@ jobs:
- name: Set GIT_TAG variable
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
- name: Set RELEASE_TAG variable
- name: Initialize RELEASE_TAG
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
- name: Set RELEASE_TAG for release
if: github.event.inputs.release_image == 'true'
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
- name: Set IMAGE_NAME_AND_TAGS variable
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
- name: New env vars
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
+36
View File
@@ -4,6 +4,42 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.15-gruyere] (2025-08-20)
- Migrate strum to 0.27.2 ([#5960])
- WG exit policy scripts update ([#5921])
- Make DNS Resolver fallback optional ([#5920])
- nym-node debug command to reset providers db ([#5914])
- basic zulip client for sending messages ([#5913])
- chore: allow compatibility with 'CDLA-Permissive-2.0' ([#5910])
- feat: ecash liveness check ([#5890])
- Remove old free credential handle ([#5864])
[#5960]: https://github.com/nymtech/nym/pull/5960
[#5921]: https://github.com/nymtech/nym/pull/5921
[#5920]: https://github.com/nymtech/nym/pull/5920
[#5914]: https://github.com/nymtech/nym/pull/5914
[#5913]: https://github.com/nymtech/nym/pull/5913
[#5910]: https://github.com/nymtech/nym/pull/5910
[#5890]: https://github.com/nymtech/nym/pull/5890
[#5864]: https://github.com/nymtech/nym/pull/5864
## [2025.14-feta] (2025-08-05)
- chore: nym node tokio console ([#5909])
- Feature/dkg snapshot epoch ([#5900])
- Feature/dkg epoch dealers query ([#5899])
- sqlx-pool-guard: allocate more memory on windows ([#5896])
- Support mnemonic in the NS agent ([#5883])
- Allow PG database backend ([#5880])
[#5909]: https://github.com/nymtech/nym/pull/5909
[#5900]: https://github.com/nymtech/nym/pull/5900
[#5899]: https://github.com/nymtech/nym/pull/5899
[#5896]: https://github.com/nymtech/nym/pull/5896
[#5883]: https://github.com/nymtech/nym/pull/5883
[#5880]: https://github.com/nymtech/nym/pull/5880
## [2025.13-emmental] (2025-07-22)
- fix: don't allow mixnode running in exit mode ([#5898])
Generated
+776 -1491
View File
File diff suppressed because it is too large Load Diff
+21 -9
View File
@@ -39,7 +39,8 @@ members = [
"common/cosmwasm-smart-contracts/ecash-contract",
"common/cosmwasm-smart-contracts/group-contract",
"common/cosmwasm-smart-contracts/mixnet-contract",
"common/cosmwasm-smart-contracts/multisig-contract", "common/cosmwasm-smart-contracts/nym-performance-contract",
"common/cosmwasm-smart-contracts/multisig-contract",
"common/cosmwasm-smart-contracts/nym-performance-contract",
"common/cosmwasm-smart-contracts/nym-pool-contract",
"common/cosmwasm-smart-contracts/vesting-contract",
"common/credential-storage",
@@ -49,6 +50,8 @@ members = [
"common/credentials-interface",
"common/crypto",
"common/dkg",
"common/ecash-signer-check",
"common/ecash-signer-check-types",
"common/ecash-time",
"common/execute",
"common/exit-policy",
@@ -89,7 +92,7 @@ members = [
"common/socks5/requests",
"common/statistics",
"common/store-cipher",
"common/task",
"common/task", "common/test-utils",
"common/ticketbooks-merkle",
"common/topology",
"common/tun",
@@ -99,15 +102,22 @@ members = [
"common/wasm/storage",
"common/wasm/utils",
"common/wireguard",
"common/wireguard-private-metadata/client",
"common/wireguard-private-metadata/server",
"common/wireguard-private-metadata/shared",
"common/wireguard-private-metadata/tests",
"common/wireguard-types",
"common/zulip-client",
"documentation/autodoc",
"gateway",
"nym-api",
"nym-api/nym-api-requests",
"nym-authenticator-client",
"nym-browser-extension/storage",
"nym-credential-proxy/nym-credential-proxy",
"nym-credential-proxy/nym-credential-proxy-requests",
"nym-credential-proxy/vpn-api-lib-wasm",
"nym-ip-packet-client",
"nym-network-monitor",
"nym-node",
"nym-node-status-api/nym-node-status-agent",
@@ -118,12 +128,12 @@ members = [
"nym-outfox",
"nym-statistics-api",
"nym-validator-rewarder",
"nym-wg-gateway-client",
"nyx-chain-watcher",
"sdk/ffi/cpp",
"sdk/ffi/go",
"sdk/ffi/shared",
"sdk/rust/nym-sdk",
"service-providers/authenticator",
"service-providers/common",
"service-providers/ip-packet-router",
"service-providers/network-requester",
@@ -161,7 +171,6 @@ default-members = [
"nym-statistics-api",
"nym-validator-rewarder",
"nyx-chain-watcher",
"service-providers/authenticator",
"service-providers/ip-packet-router",
"service-providers/network-requester",
"tools/nymvisor",
@@ -176,7 +185,7 @@ homepage = "https://nymtech.net"
documentation = "https://nymtech.net"
edition = "2021"
license = "Apache-2.0"
rust-version = "1.80"
rust-version = "1.81"
readme = "README.md"
[workspace.dependencies]
@@ -218,7 +227,7 @@ clap_complete_fig = "4.5"
colored = "2.2"
comfy-table = "7.1.4"
console = "0.15.11"
console-subscriber = "0.1.1"
console-subscriber = "0.4.1"
console_error_panic_hook = "0.1"
const-str = "0.5.6"
const_format = "0.2.34"
@@ -317,11 +326,11 @@ si-scale = "0.2.3"
snow = "0.9.6"
sphinx-packet = "=0.6.0"
sqlx = "0.8.6"
strum = "0.26"
strum_macros = "0.26"
strum = "0.27.2"
strum_macros = "0.27.2"
subtle-encoding = "0.5"
syn = "1"
sysinfo = "0.33.0"
sysinfo = "0.37.0"
tap = "1.0.1"
tar = "0.4.44"
tempfile = "3.20"
@@ -435,6 +444,9 @@ opt-level = 'z'
# lto = true
opt-level = 'z'
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] }
[workspace.lints.clippy]
unwrap_used = "deny"
expect_used = "deny"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.59"
version = "1.1.61"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -111,7 +111,7 @@ impl SocketClient {
let dkg_query_client = if self.config.base.client.disabled_credentials_mode {
None
} else {
Some(default_query_dkg_client_from_config(&self.config.base))
Some(default_query_dkg_client_from_config(&self.config.base)?)
};
let storage = self.initialise_storage().await?;
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.59"
version = "1.1.61"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
+3 -3
View File
@@ -13,7 +13,7 @@ use nym_credentials_interface::{
};
use nym_ecash_time::Date;
use nym_validator_client::coconut::all_ecash_api_clients;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nym_api::{EpochId, NymApiClientExt};
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use nym_validator_client::EcashApiClient;
use rand::prelude::SliceRandom;
@@ -207,7 +207,7 @@ where
<St as Storage>::StorageError: Send + Sync + 'static,
{
if let Some(stored) = storage
.get_expiration_date_signatures(expiration_date)
.get_expiration_date_signatures(expiration_date, epoch_id)
.await
.map_err(BandwidthControllerError::credential_storage_error)?
{
@@ -220,7 +220,7 @@ where
ecash_apis,
|api| async move {
api.api_client
.global_expiration_date_signatures(Some(expiration_date))
.global_expiration_date_signatures(Some(expiration_date), Some(epoch_id))
.await
},
format!("aggregated coin index signatures for date {expiration_date}"),
+5
View File
@@ -13,6 +13,7 @@ async-trait = { workspace = true }
base64 = { workspace = true }
bs58 = { workspace = true }
clap = { workspace = true, optional = true }
cfg-if = { workspace = true }
comfy-table = { workspace = true, optional = true }
futures = { workspace = true }
humantime = { workspace = true }
@@ -52,6 +53,7 @@ nym-client-core-config-types = { path = "./config-types", features = [
nym-client-core-surb-storage = { path = "./surb-storage" }
nym-client-core-gateways-storage = { path = "./gateways-storage" }
nym-ecash-time = { path = "../ecash-time" }
nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false }
@@ -123,3 +125,6 @@ fs-surb-storage = ["nym-client-core-surb-storage/fs-surb-storage"]
fs-gateways-storage = ["nym-client-core-gateways-storage/fs-gateways-storage"]
wasm = ["nym-gateway-client/wasm"]
metrics-server = []
[lints]
workspace = true
@@ -3,6 +3,7 @@ name = "nym-client-core-gateways-storage"
version = "0.1.0"
edition = "2021"
license.workspace = true
rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -26,6 +27,7 @@ features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"]
optional = true
[build-dependencies]
anyhow = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
+13 -4
View File
@@ -2,23 +2,30 @@
// SPDX-License-Identifier: Apache-2.0
#[tokio::main]
async fn main() {
async fn main() -> anyhow::Result<()> {
#[cfg(feature = "fs-gateways-storage")]
{
use anyhow::Context;
use sqlx::{Connection, SqliteConnection};
use std::env;
let out_dir = env::var("OUT_DIR").unwrap();
let out_dir = env::var("OUT_DIR")?;
let database_path = format!("{out_dir}/gateways-storage-example.sqlite");
// remove the db file if it already existed from previous build
// in case it was from a different branch
if std::fs::exists(&database_path)? {
std::fs::remove_file(&database_path)?;
}
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
.await
.expect("Failed to create SQLx database connection");
.context("Failed to create SQLx database connection")?;
sqlx::migrate!("./fs_gateways_migrations")
.run(&mut conn)
.await
.expect("Failed to perform SQLx migrations");
.context("Failed to perform SQLx migrations")?;
#[cfg(target_family = "unix")]
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
@@ -28,4 +35,6 @@ async fn main() {
// not a valid windows path... but hey, it works...
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
}
Ok(())
}
@@ -58,6 +58,7 @@ where
Some(data) => data,
None => {
// SAFETY: one of those arguments must have been set
#[allow(clippy::unwrap_used)]
fs::read(common_args.signatures_path.unwrap())?
}
};
@@ -64,6 +64,7 @@ where
Some(data) => data,
None => {
// SAFETY: one of those arguments must have been set
#[allow(clippy::unwrap_used)]
fs::read(common_args.credential_path.unwrap())?
}
};
@@ -58,6 +58,7 @@ where
Some(data) => data,
None => {
// SAFETY: one of those arguments must have been set
#[allow(clippy::unwrap_used)]
fs::read(common_args.signatures_path.unwrap())?
}
};
@@ -58,6 +58,7 @@ where
Some(data) => data,
None => {
// SAFETY: one of those arguments must have been set
#[allow(clippy::unwrap_used)]
fs::read(common_args.key_path.unwrap())?
}
};
@@ -57,7 +57,7 @@ use nym_task::{TaskClient, TaskHandle};
use nym_topology::provider_trait::TopologyProvider;
use nym_topology::HardcodedTopologyProvider;
use nym_validator_client::nym_api::NymApiClientExt;
use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, NymApiClient, UserAgent};
use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, UserAgent};
use rand::prelude::SliceRandom;
use rand::rngs::OsRng;
use rand::thread_rng;
@@ -135,9 +135,11 @@ pub enum ClientInputStatus {
}
impl ClientInputStatus {
#[allow(clippy::panic)]
pub fn register_producer(&mut self) -> ClientInput {
match std::mem::replace(self, ClientInputStatus::Connected) {
ClientInputStatus::AwaitingProducer { client_input } => client_input,
// critical failure implying misuse of software
ClientInputStatus::Connected => panic!("producer was already registered before"),
}
}
@@ -149,9 +151,11 @@ pub enum ClientOutputStatus {
}
impl ClientOutputStatus {
#[allow(clippy::panic)]
pub fn register_consumer(&mut self) -> ClientOutput {
match std::mem::replace(self, ClientOutputStatus::Connected) {
ClientOutputStatus::AwaitingConsumer { client_output } => client_output,
// critical failure implying misuse of software
ClientOutputStatus::Connected => panic!("consumer was already registered before"),
}
}
@@ -562,7 +566,7 @@ where
custom_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
config_topology: config::Topology,
nym_api_urls: Vec<Url>,
nym_api_client: NymApiClient,
nym_api_client: nym_http_api_client::Client,
) -> Box<dyn TopologyProvider + Send + Sync> {
// if no custom provider was ... provided ..., create one using nym-api
custom_provider.unwrap_or_else(|| {
@@ -707,11 +711,14 @@ where
})?;
let store_clone = mem_store.clone();
spawn_future(async move {
persistent_storage
.flush_on_shutdown(store_clone, shutdown)
.await
});
spawn_future!(
async move {
persistent_storage
.flush_on_shutdown(store_clone, shutdown)
.await
},
"PersistentReplyStorage::flush_on_shutdown"
);
Ok(mem_store)
}
@@ -732,7 +739,7 @@ where
let mut rng = OsRng;
let keys = if let Some(derivation_material) = derivation_material {
ClientKeys::from_master_key(&mut rng, &derivation_material)
.map_err(|_| ClientCoreError::HkdfDerivationError {})?
.map_err(|_| ClientCoreError::HkdfDerivationError)?
} else {
ClientKeys::generate_new(&mut rng)
};
@@ -742,21 +749,42 @@ where
setup_gateway(setup_method, key_store, details_store).await
}
fn construct_nym_api_client(config: &Config, user_agent: Option<UserAgent>) -> NymApiClient {
fn construct_nym_api_client(
config: &Config,
user_agent: Option<UserAgent>,
) -> Result<nym_http_api_client::Client, ClientCoreError> {
let mut nym_api_urls = config.get_nym_api_endpoints();
nym_api_urls.shuffle(&mut thread_rng());
let mut builder = nym_http_api_client::Client::builder::<
_,
nym_validator_client::models::RequestError,
>(nym_api_urls[0].clone())
.map_err(|e| ClientCoreError::NymApiQueryFailure {
source: nym_validator_client::nym_api::error::NymAPIError::GenericRequestFailure(
e.to_string(),
),
})?;
if let Some(user_agent) = user_agent {
NymApiClient::new_with_user_agent(nym_api_urls[0].clone(), user_agent)
} else {
NymApiClient::new(nym_api_urls[0].clone())
builder = builder.with_user_agent(user_agent);
}
builder = builder.with_bincode();
builder
.build::<nym_validator_client::models::RequestError>()
.map_err(|e| ClientCoreError::NymApiQueryFailure {
source: nym_validator_client::nym_api::error::NymAPIError::GenericRequestFailure(
e.to_string(),
),
})
}
async fn determine_key_rotation_state(
client: &NymApiClient,
client: &nym_http_api_client::Client,
) -> Result<KeyRotationConfig, ClientCoreError> {
Ok(client.nym_api.get_key_rotation_info().await?.into())
Ok(client.get_key_rotation_info().await?.into())
}
pub async fn start_base(mut self) -> Result<BaseClient, ClientCoreError>
@@ -823,7 +851,7 @@ where
.dkg_query_client
.map(|client| BandwidthController::new(credential_store, client));
let nym_api_client = Self::construct_nym_api_client(&self.config, self.user_agent.clone());
let nym_api_client = Self::construct_nym_api_client(&self.config, self.user_agent.clone())?;
let key_rotation_config = Self::determine_key_rotation_state(&nym_api_client).await?;
let topology_provider = Self::setup_topology_provider(
@@ -114,41 +114,32 @@ pub async fn setup_fs_gateways_storage<P: AsRef<Path>>(
})
}
pub fn create_bandwidth_controller<St: CredentialStorage>(
config: &Config,
storage: St,
) -> BandwidthController<QueryHttpRpcNyxdClient, St> {
let nyxd_url = config
.get_validator_endpoints()
.pop()
.expect("No nyxd validator endpoint provided");
create_bandwidth_controller_with_urls(nyxd_url, storage)
}
pub fn create_bandwidth_controller_with_urls<St: CredentialStorage>(
nyxd_url: Url,
storage: St,
) -> BandwidthController<QueryHttpRpcNyxdClient, St> {
let client = default_query_dkg_client(nyxd_url);
) -> Result<BandwidthController<QueryHttpRpcNyxdClient, St>, ClientCoreError> {
let client = default_query_dkg_client(nyxd_url)?;
BandwidthController::new(storage, client)
Ok(BandwidthController::new(storage, client))
}
pub fn default_query_dkg_client_from_config(config: &Config) -> QueryHttpRpcNyxdClient {
pub fn default_query_dkg_client_from_config(
config: &Config,
) -> Result<QueryHttpRpcNyxdClient, ClientCoreError> {
let nyxd_url = config
.get_validator_endpoints()
.pop()
.expect("No nyxd validator endpoint provided");
.ok_or(ClientCoreError::RpcClientMissingUrl)?;
default_query_dkg_client(nyxd_url)
}
pub fn default_query_dkg_client(nyxd_url: Url) -> QueryHttpRpcNyxdClient {
pub fn default_query_dkg_client(nyxd_url: Url) -> Result<QueryHttpRpcNyxdClient, ClientCoreError> {
let details = nym_network_defaults::NymNetworkDetails::new_from_env();
let client_config = nyxd::Config::try_from_nym_network_details(&details)
.expect("failed to construct validator client config");
.map_err(|source| ClientCoreError::InvalidNetworkDetails { source })?;
// overwrite env configuration with config URLs
QueryHttpRpcNyxdClient::connect(client_config, nyxd_url.as_str())
.expect("Could not construct query client")
.map_err(|source| ClientCoreError::RpcClientCreationFailure { source })
}
@@ -235,6 +235,7 @@ impl LoopCoverTrafficStream<OsRng> {
tokio::task::yield_now().await;
}
#[allow(clippy::panic)]
pub fn start(mut self) {
if self.cover_traffic.disable_loop_cover_traffic_stream {
// we should have never got here in the first place - the task should have never been created to begin with
@@ -251,27 +252,30 @@ impl LoopCoverTrafficStream<OsRng> {
let mut shutdown = self.task_client.fork("select");
spawn_future(async move {
debug!("Started LoopCoverTrafficStream with graceful shutdown support");
spawn_future!(
async move {
debug!("Started LoopCoverTrafficStream with graceful shutdown support");
while !shutdown.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv() => {
tracing::trace!("LoopCoverTrafficStream: Received shutdown");
}
next = self.next() => {
if next.is_some() {
self.on_new_message().await;
} else {
tracing::trace!("LoopCoverTrafficStream: Stopping since channel closed");
break;
while !shutdown.is_shutdown() {
tokio::select! {
biased;
_ = shutdown.recv() => {
tracing::trace!("LoopCoverTrafficStream: Received shutdown");
}
next = self.next() => {
if next.is_some() {
self.on_new_message().await;
} else {
tracing::trace!("LoopCoverTrafficStream: Stopping since channel closed");
break;
}
}
}
}
}
shutdown.recv_timeout().await;
tracing::debug!("LoopCoverTrafficStream: Exiting");
})
shutdown.recv_timeout().await;
tracing::debug!("LoopCoverTrafficStream: Exiting");
},
"LoopCoverTrafficStream"
)
}
}
@@ -96,72 +96,93 @@ impl MixTrafficController {
mut mix_packets: Vec<MixPacket>,
) -> Result<(), ErasedGatewayError> {
debug_assert!(!mix_packets.is_empty());
let result = if mix_packets.len() == 1 {
let send_future = if mix_packets.len() == 1 {
// SAFETY: we just checked we have one packet
#[allow(clippy::unwrap_used)]
let mix_packet = mix_packets.pop().unwrap();
self.gateway_transceiver.send_mix_packet(mix_packet).await
self.gateway_transceiver.send_mix_packet(mix_packet)
} else {
self.gateway_transceiver
.batch_send_mix_packets(mix_packets)
.await
self.gateway_transceiver.batch_send_mix_packets(mix_packets)
};
if result.is_err() {
self.consecutive_gateway_failure_count += 1;
} else {
trace!("We *might* have managed to forward sphinx packet(s) to the gateway!");
self.consecutive_gateway_failure_count = 0;
}
tokio::select! {
biased;
_ = self.task_client.recv() => {
trace!("received shutdown while handling messages");
Ok(())
}
result = send_future => {
if result.is_err() {
self.consecutive_gateway_failure_count += 1;
} else {
trace!("We *might* have managed to forward sphinx packet(s) to the gateway!");
self.consecutive_gateway_failure_count = 0;
}
result
result
}
}
}
async fn on_client_request(&mut self, client_request: ClientRequest) {
tokio::select! {
biased;
_ = self.task_client.recv() => {
trace!("received shutdown while handling client request");
}
result = self.gateway_transceiver.send_client_request(client_request) => {
if let Err(err) = result {
error!("Failed to send client request: {err}")
}
}
}
}
pub fn start(mut self) {
spawn_future(async move {
debug!("Started MixTrafficController with graceful shutdown support");
while !self.task_client.is_shutdown() {
tokio::select! {
mix_packets = self.mix_rx.recv() => match mix_packets {
Some(mix_packets) => {
if let Err(err) = self.on_messages(mix_packets).await {
error!("Failed to send sphinx packet(s) to the gateway: {err}");
if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT {
// Disconnect from the gateway. If we should try to re-connect
// is handled at a higher layer.
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.task_client.send_we_stopped(Box::new(ClientCoreError::GatewayFailedToForwardMessages));
break;
}
}
},
None => {
tracing::trace!("MixTrafficController: Stopping since channel closed");
spawn_future!(
async move {
debug!("Started MixTrafficController with graceful shutdown support");
while !self.task_client.is_shutdown() {
tokio::select! {
biased;
_ = self.task_client.recv() => {
tracing::trace!("MixTrafficController: Received shutdown");
break;
}
},
client_request = self.client_rx.recv() => match client_request {
Some(client_request) => {
match self.gateway_transceiver.send_client_request(client_request).await {
Ok(_) => (),
Err(e) => error!("Failed to send client request: {e}"),
};
mix_packets = self.mix_rx.recv() => match mix_packets {
Some(mix_packets) => {
if let Err(err) = self.on_messages(mix_packets).await {
error!("Failed to send sphinx packet(s) to the gateway: {err}");
if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT {
// Disconnect from the gateway. If we should try to re-connect
// is handled at a higher layer.
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.task_client.send_we_stopped(Box::new(ClientCoreError::GatewayFailedToForwardMessages));
break;
}
}
},
None => {
tracing::trace!("MixTrafficController: Stopping since channel closed");
break;
}
},
client_request = self.client_rx.recv() => match client_request {
Some(client_request) => {
self.on_client_request(client_request).await;
},
None => {
tracing::trace!("MixTrafficController, client request channel closed");
}
},
None => {
tracing::trace!("MixTrafficController, client request channel closed");
}
},
_ = self.task_client.recv() => {
tracing::trace!("MixTrafficController: Received shutdown");
break;
}
}
}
self.task_client.recv_timeout().await;
tracing::debug!("MixTrafficController: Exiting");
});
self.task_client.recv_timeout().await;
tracing::debug!("MixTrafficController: Exiting");
},
"MixTrafficController"
);
}
}
@@ -269,6 +269,8 @@ pub struct MockGateway {
}
impl Default for MockGateway {
// test code
#[allow(clippy::unwrap_used)]
fn default() -> Self {
MockGateway {
dummy_identity: "3ebjp1Fb9hdcS1AR6AZihgeJiMHkB5jjJUsvqNnfQwU7"
@@ -194,10 +194,11 @@ impl ActionController {
trace!("{frag_id} is updating its delay");
// TODO: is it possible to solve this without either locking or temporarily removing the value?
if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.remove(&frag_id) {
// this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets)
// SAFETY: this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets)
// or `ReplyController` (for 'reply' packets) which held the other potential
// reference to this Arc. HOWEVER, before the Action was pushed onto the queue, the reference
// was dropped hence this unwrap is safe.
#[allow(clippy::unwrap_used)]
let mut inner_data = Arc::try_unwrap(pending_ack_data).unwrap();
inner_data.update_retransmitted(delay);
@@ -209,6 +210,7 @@ impl ActionController {
}
// note: when the entry expires it's automatically removed from pending_acks_timers
#[allow(clippy::panic)]
fn handle_expired_ack_timer(&mut self, expired_ack: Expired<FragmentIdentifier>) {
let frag_id = expired_ack.into_inner();
@@ -120,6 +120,7 @@ where
}
}
#[allow(clippy::panic)]
async fn on_input_message(&mut self, msg: InputMessage) {
match msg {
InputMessage::Regular {
@@ -213,7 +214,9 @@ where
self.handle_premade_packets(msgs, lane).await
}
// MessageWrappers can't be nested
InputMessage::MessageWrapper { .. } => unimplemented!(),
InputMessage::MessageWrapper { .. } => {
panic!("attempted to use nested MessageWrapper")
}
},
};
}
@@ -223,6 +226,11 @@ where
while !self.task_client.is_shutdown() {
tokio::select! {
biased;
_ = self.task_client.recv() => {
tracing::trace!("InputMessageListener: Received shutdown");
break;
}
input_msg = self.input_receiver.recv() => match input_msg {
Some(input_msg) => {
self.on_input_message(input_msg).await;
@@ -232,9 +240,7 @@ where
break;
}
},
_ = self.task_client.recv() => {
tracing::trace!("InputMessageListener: Received shutdown");
}
}
}
self.task_client.recv_timeout().await;
@@ -298,29 +298,44 @@ where
let mut sent_notification_listener = self.sent_notification_listener;
let mut action_controller = self.action_controller;
spawn_future(async move {
acknowledgement_listener.run().await;
debug!("The acknowledgement listener has finished execution!");
});
spawn_future!(
async move {
acknowledgement_listener.run().await;
debug!("The acknowledgement listener has finished execution!");
},
"AcknowledgementController::AcknowledgementListener"
);
spawn_future(async move {
input_message_listener.run().await;
debug!("The input listener has finished execution!");
});
spawn_future!(
async move {
input_message_listener.run().await;
debug!("The input listener has finished execution!");
},
"AcknowledgementController::InputMessageListener"
);
spawn_future(async move {
retransmission_request_listener.run(packet_type).await;
debug!("The retransmission request listener has finished execution!");
});
spawn_future!(
async move {
retransmission_request_listener.run(packet_type).await;
debug!("The retransmission request listener has finished execution!");
},
"AcknowledgementController::RetransmissionRequestListener"
);
spawn_future(async move {
sent_notification_listener.run().await;
debug!("The sent notification listener has finished execution!");
});
spawn_future!(
async move {
sent_notification_listener.run().await;
debug!("The sent notification listener has finished execution!");
},
"AcknowledgementController::SentNotificationListener"
);
spawn_future(async move {
action_controller.run().await;
debug!("The controller has finished execution!");
});
spawn_future!(
async move {
action_controller.run().await;
debug!("The controller has finished execution!");
},
"AcknowledgementController::ActionController"
);
}
}
@@ -179,6 +179,11 @@ where
while !self.task_client.is_shutdown() {
tokio::select! {
biased;
_ = self.task_client.recv() => {
tracing::trace!("RetransmissionRequestListener: Received shutdown");
break;
}
timed_out_ack = self.request_receiver.next() => match timed_out_ack {
Some(timed_out_ack) => self.on_retransmission_request(timed_out_ack, packet_type).await,
None => {
@@ -186,9 +191,7 @@ where
break;
}
},
_ = self.task_client.recv() => {
tracing::trace!("RetransmissionRequestListener: Received shutdown");
}
}
}
self.task_client.recv_timeout().await;
@@ -35,6 +35,9 @@ pub enum PreparationError {
#[error(transparent)]
NymTopologyError(#[from] NymTopologyError),
#[error("message wasn't split into any fragments!")]
EmptyFragments,
#[error("message too long for a single SURB, splitting into {fragments} fragments.")]
MessageTooLongForSingleSurb { fragments: usize },
@@ -320,6 +323,16 @@ where
});
}
if fragment.is_empty() {
error!("CRITICAL FAILURE: our split message didn't result in any sendable fragments");
return Err(SurbWrappedPreparationError {
source: PreparationError::EmptyFragments,
returned_surbs: Some(vec![reply_surb]),
});
}
// SAFETY: we just checked we have one fragment
#[allow(clippy::unwrap_used)]
let chunk = fragment.pop().unwrap();
let chunk_clone = chunk.clone();
let prepared_fragment = self
@@ -535,6 +548,7 @@ where
pending_acks.push(pending_ack);
}
drop(topology_permit);
self.insert_pending_acks(pending_acks);
self.forward_messages(real_messages, lane).await;
@@ -657,6 +671,7 @@ where
.zip(reply_surbs.into_iter())
.map(|(fragment, reply_surb)| {
// unwrap here is fine as we know we have a valid topology
#[allow(clippy::unwrap_used)]
self.message_preparer
.prepare_reply_chunk_for_sending(
fragment,
@@ -716,17 +731,21 @@ where
// tells real message sender (with the poisson timer) to send this to the mix network
pub(crate) async fn forward_messages(
&self,
&mut self,
messages: Vec<RealMessage>,
transmission_lane: TransmissionLane,
) {
if let Err(err) = self
.real_message_sender
.send((messages, transmission_lane))
.await
{
if !self.task_client.is_shutdown_poll() {
error!("Failed to forward messages to the real message sender: {err}");
tokio::select! {
biased;
_ = self.task_client.recv() => {
trace!("received shutdown while attempting to forward mixnet messages");
}
sending_res = self.real_message_sender.send((messages, transmission_lane)) => {
if sending_res.is_err() {
error!(
"failed to forward mixnet messages due to closed channel (outside of shutdown!)"
);
}
}
}
}
@@ -224,14 +224,20 @@ impl RealMessagesController<OsRng> {
let ack_control = self.ack_control;
let mut reply_control = self.reply_control;
spawn_future(async move {
out_queue_control.run().await;
debug!("The out queue controller has finished execution!");
});
spawn_future(async move {
reply_control.run().await;
debug!("The reply controller has finished execution!");
});
spawn_future!(
async move {
out_queue_control.run().await;
debug!("The out queue controller has finished execution!");
},
"RealMessagesController::OutQueueControl)"
);
spawn_future!(
async move {
reply_control.run().await;
debug!("The reply controller has finished execution!");
},
"RealMessagesController::ReplyController"
);
ack_control.start(packet_type);
}
@@ -249,6 +249,8 @@ where
}
};
// SAFETY: our topology must be valid at this point
#[allow(clippy::expect_used)]
(
generate_loop_cover_packet(
&mut self.rng,
@@ -278,17 +280,33 @@ where
}
};
if let Err(err) = self.mix_tx.send(vec![next_message]).await {
if !self.task_client.is_shutdown_poll() {
tracing::error!("Failed to send: {err}");
let sending_res = tokio::select! {
biased;
_ = self.task_client.recv() => {
trace!("received shutdown signal while attempting to send mix message");
return
}
sending_res = self.mix_tx.send(vec![next_message]) => {
sending_res
}
};
match sending_res {
Err(_) => {
if !self.task_client.is_shutdown_poll() {
tracing::error!(
"failed to send mixnet packet due to closed channel (outside of shutdown!)"
);
}
}
Ok(_) => {
let event = if fragment_id.is_some() {
PacketStatisticsEvent::RealPacketSent(packet_size)
} else {
PacketStatisticsEvent::CoverPacketSent(packet_size)
};
self.stats_tx.report(event.into());
}
} else {
let event = if fragment_id.is_some() {
PacketStatisticsEvent::RealPacketSent(packet_size)
} else {
PacketStatisticsEvent::CoverPacketSent(packet_size)
};
self.stats_tx.report(event.into());
}
// notify ack controller about sending our message only after we actually managed to push it
@@ -439,6 +457,8 @@ where
tracing::trace!("handling real_messages: size: {}", real_messages.len());
self.transmission_buffer.store(&conn_id, real_messages);
// SAFETY: we just stored the message
#[allow(clippy::expect_used)]
let real_next = self.pop_next_message().expect("Just stored one");
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
@@ -487,6 +507,8 @@ where
// First store what we got for the given connection id
self.transmission_buffer.store(&conn_id, real_messages);
// SAFETY: we just stored the message
#[allow(clippy::expect_used)]
let real_next = self.pop_next_message().expect("we just added one");
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
@@ -198,6 +198,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
}
}
#[allow(clippy::panic)]
async fn disconnect_sender(&mut self) {
let mut guard = self.inner.lock().await;
if guard.message_sender.is_none() {
@@ -208,6 +209,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
guard.message_sender = None;
}
#[allow(clippy::panic)]
async fn connect_sender(&mut self, sender: ReconstructedMessagesSender) {
let mut guard = self.inner.lock().await;
if guard.message_sender.is_some() {
@@ -599,14 +601,20 @@ impl<R: MessageReceiver + Clone + Send + 'static> ReceivedMessagesBufferControll
let mut fragmented_message_receiver = self.fragmented_message_receiver;
let mut request_receiver = self.request_receiver;
spawn_future(async move {
match fragmented_message_receiver.run().await {
Ok(_) => {}
Err(e) => error!("{e}"),
}
});
spawn_future(async move {
request_receiver.run().await;
});
spawn_future!(
async move {
match fragmented_message_receiver.run().await {
Ok(_) => {}
Err(e) => error!("{e}"),
}
},
"ReceivedMessagesBufferController::FragmentedMessageReceiver"
);
spawn_future!(
async move {
request_receiver.run().await;
},
"ReceivedMessagesBufferController::RequestReceiver"
);
}
}
@@ -155,8 +155,9 @@ where
data: Vec<Arc<PendingAcknowledgement>>,
) {
trace!("re-inserting pending retransmissions for {recipient}");
// the underlying entry MUST exist as we've just got data from there
// SAFETY: the underlying entry MUST exist as we've just got data from there
// and we hold a mut reference
#[allow(clippy::expect_used)]
let map_entry = &mut self
.surb_senders
.get_mut(recipient)
@@ -429,6 +430,7 @@ where
.pop_at_most_n_next_messages_at_random(amount)
}
#[allow(clippy::panic)]
async fn try_clear_pending_queue(&mut self, target: AnonymousSenderTag) {
trace!("trying to clear pending queue");
let available_surbs = self.surbs_storage.available_surbs(&target);
@@ -165,9 +165,12 @@ impl StatisticsControl {
}
pub(crate) fn start(mut self) {
spawn_future(async move {
self.run().await;
})
spawn_future!(
async move {
self.run().await;
},
"StatisticsControl"
)
}
pub(crate) fn create_and_start(
@@ -126,7 +126,7 @@ impl TopologyAccessor {
.map(|p| p.topology.clone())
}
pub async fn current_route_provider(&self) -> Option<RwLockReadGuard<NymRouteProvider>> {
pub async fn current_route_provider(&self) -> Option<RwLockReadGuard<'_, NymRouteProvider>> {
let provider = self.inner.topology.read().await;
if provider.topology.is_empty() {
None
@@ -145,36 +145,39 @@ impl TopologyRefresher {
}
pub fn start(mut self) {
spawn_future(async move {
debug!("Started TopologyRefresher with graceful shutdown support");
spawn_future!(
async move {
debug!("Started TopologyRefresher with graceful shutdown support");
#[cfg(not(target_arch = "wasm32"))]
let mut interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
self.refresh_rate,
));
#[cfg(not(target_arch = "wasm32"))]
let mut interval = tokio_stream::wrappers::IntervalStream::new(
tokio::time::interval(self.refresh_rate),
);
#[cfg(target_arch = "wasm32")]
let mut interval =
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
#[cfg(target_arch = "wasm32")]
let mut interval =
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
// We already have an initial topology, so no need to refresh it immediately.
// My understanding is that js setInterval does not fire immediately, so it's not
// needed there.
#[cfg(not(target_arch = "wasm32"))]
interval.next().await;
// We already have an initial topology, so no need to refresh it immediately.
// My understanding is that js setInterval does not fire immediately, so it's not
// needed there.
#[cfg(not(target_arch = "wasm32"))]
interval.next().await;
while !self.task_client.is_shutdown() {
tokio::select! {
_ = interval.next() => {
self.try_refresh().await;
},
_ = self.task_client.recv() => {
tracing::trace!("TopologyRefresher: Received shutdown");
},
while !self.task_client.is_shutdown() {
tokio::select! {
_ = interval.next() => {
self.try_refresh().await;
},
_ = self.task_client.recv() => {
tracing::trace!("TopologyRefresher: Received shutdown");
},
}
}
}
self.task_client.recv_timeout().await;
tracing::debug!("TopologyRefresher: Exiting");
})
self.task_client.recv_timeout().await;
tracing::debug!("TopologyRefresher: Exiting");
},
"TopologyRefresher"
)
}
}
@@ -2,8 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait;
use nym_mixnet_contract_common::EpochRewardedSet;
use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider};
use nym_topology::NymTopology;
use nym_validator_client::nym_api::NymApiClientExt;
use rand::prelude::SliceRandom;
use rand::thread_rng;
use std::cmp::min;
@@ -39,30 +41,43 @@ impl Config {
pub struct NymApiTopologyProvider {
config: Config,
validator_client: nym_validator_client::client::NymApiClient,
validator_client: nym_http_api_client::Client,
nym_api_urls: Vec<Url>,
currently_used_api: usize,
use_bincode: bool,
}
impl NymApiTopologyProvider {
pub fn new(
config: impl Into<Config>,
mut nym_api_urls: Vec<Url>,
mut validator_client: nym_validator_client::client::NymApiClient,
validator_client: nym_http_api_client::Client,
) -> Self {
nym_api_urls.shuffle(&mut thread_rng());
validator_client.change_nym_api(nym_api_urls[0].clone());
NymApiTopologyProvider {
let mut provider = NymApiTopologyProvider {
config: config.into(),
validator_client,
nym_api_urls,
currently_used_api: 0,
}
use_bincode: true,
};
// Set all API URLs - the client will try them in order with automatic failover
provider.validator_client.change_base_urls(
provider
.nym_api_urls
.iter()
.map(|u| u.clone().into())
.collect(),
);
provider
}
pub fn disable_bincode(&mut self) {
self.validator_client.use_bincode = false;
self.use_bincode = false;
// Note: The unified client doesn't support toggling bincode after creation.
// This would require recreating the client without bincode.
// For now, we'll track the preference but it won't take effect.
warn!("Disabling bincode on existing client is not currently supported");
}
fn use_next_nym_api(&mut self) {
@@ -72,8 +87,19 @@ impl NymApiTopologyProvider {
}
self.currently_used_api = (self.currently_used_api + 1) % self.nym_api_urls.len();
self.validator_client
.change_nym_api(self.nym_api_urls[self.currently_used_api].clone())
// Provide all URLs starting from the next one in rotation order
// This enables automatic failover to other endpoints
let rotated_urls: Vec<_> = self
.nym_api_urls
.iter()
.cycle()
.skip(self.currently_used_api)
.take(self.nym_api_urls.len())
.map(|u| u.clone().into())
.collect();
self.validator_client.change_base_urls(rotated_urls)
}
async fn get_current_compatible_topology(&mut self) -> Option<NymTopology> {
@@ -99,8 +125,13 @@ impl NymApiTopologyProvider {
.filter(|n| n.performance.round_to_integer() >= self.config.min_node_performance())
.collect::<Vec<_>>();
NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new())
.with_skimmed_nodes(&nodes_filtered)
let epoch_rewarded_set: EpochRewardedSet = rewarded_set.into();
NymTopology::new(
metadata.to_topology_metadata(),
epoch_rewarded_set,
Vec::new(),
)
.with_skimmed_nodes(&nodes_filtered)
} else {
// if we're not using extended topology, we're only getting active set mixnodes and gateways
@@ -148,8 +179,13 @@ impl NymApiTopologyProvider {
}
}
NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new())
.with_skimmed_nodes(&nodes)
let epoch_rewarded_set: EpochRewardedSet = rewarded_set.into();
NymTopology::new(
metadata.to_topology_metadata(),
epoch_rewarded_set,
Vec::new(),
)
.with_skimmed_nodes(&nodes)
};
if !topology.is_minimally_routable() {
+15 -1
View File
@@ -7,7 +7,9 @@ use nym_gateway_client::error::GatewayClientError;
use nym_topology::node::RoutingNodeError;
use nym_topology::{NodeId, NymTopologyError};
use nym_validator_client::nym_api::error::NymAPIError;
use nym_validator_client::nyxd::error::NyxdError;
use nym_validator_client::ValidatorClientError;
use rand::distributions::WeightedError;
use std::error::Error;
use std::path::PathBuf;
@@ -230,7 +232,19 @@ pub enum ClientCoreError {
UnexpectedKeyUpgrade { gateway_id: String },
#[error("failed to derive keys from master key")]
HkdfDerivationError {},
HkdfDerivationError,
#[error("missing url for constructing RPC client")]
RpcClientMissingUrl,
#[error("provided nym network details were malformed: {source}")]
InvalidNetworkDetails { source: NyxdError },
#[error("failed to construct RPC client: {source}")]
RpcClientCreationFailure { source: NyxdError },
#[error("failed to select valid gateway due to incomputable latency")]
GatewaySelectionFailure { source: WeightedError },
}
impl From<tungstenite::Error> for ClientCoreError {
+69 -10
View File
@@ -7,7 +7,8 @@ use futures::{SinkExt, StreamExt};
use nym_crypto::asymmetric::ed25519;
use nym_gateway_client::GatewayClient;
use nym_topology::node::RoutingNode;
use nym_validator_client::client::IdentityKeyRef;
use nym_validator_client::client::{IdentityKeyRef, NymApiClientExt};
use nym_validator_client::nym_nodes::SkimmedNodesWithMetadata;
use nym_validator_client::UserAgent;
use rand::{seq::SliceRandom, Rng};
#[cfg(unix)]
@@ -83,6 +84,48 @@ struct GatewayWithLatency<'a, G: ConnectableGateway> {
latency: Duration,
}
// Helper to collect all pages of entry nodes - replicates NymApiClient's convenience method
async fn get_all_basic_entry_nodes_with_metadata(
client: &nym_http_api_client::Client,
use_bincode: bool,
) -> Result<SkimmedNodesWithMetadata, ClientCoreError> {
// Get first page to obtain metadata
let mut page = 0;
let res = client
.get_basic_entry_assigned_nodes_v2(false, Some(page), None, use_bincode)
.await?;
let mut nodes = res.nodes.data;
let metadata = res.metadata;
if res.nodes.pagination.total == nodes.len() {
return Ok(SkimmedNodesWithMetadata::new(nodes, metadata));
}
page += 1;
// Collect remaining pages
loop {
let mut res = client
.get_basic_entry_assigned_nodes_v2(false, Some(page), None, use_bincode)
.await?;
if !metadata.consistency_check(&res.metadata) {
return Err(ClientCoreError::ValidatorClientError(
nym_validator_client::ValidatorClientError::InconsistentPagedMetadata,
));
}
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
}
impl<'a, G: ConnectableGateway> GatewayWithLatency<'a, G> {
fn new(gateway: &'a G, latency: Duration) -> Self {
GatewayWithLatency { gateway, latency }
@@ -99,16 +142,32 @@ pub async fn gateways_for_init<R: Rng>(
let nym_api = nym_apis
.choose(rng)
.ok_or(ClientCoreError::ListOfNymApisIsEmpty)?;
let client = if let Some(user_agent) = user_agent {
nym_validator_client::client::NymApiClient::new_with_user_agent(nym_api.clone(), user_agent)
} else {
nym_validator_client::client::NymApiClient::new(nym_api.clone())
};
// Use the unified HTTP client directly with optional user agent
let mut builder = nym_http_api_client::Client::builder(nym_api.clone())
.map_err(|e| {
ClientCoreError::ValidatorClientError(
nym_validator_client::ValidatorClientError::NymAPIError { source: e },
)
})?
.with_bincode(); // Use bincode for better performance
if let Some(user_agent) = user_agent {
builder = builder.with_user_agent(user_agent);
}
let client = builder
.build::<nym_validator_client::models::RequestError>()
.map_err(|e| {
ClientCoreError::ValidatorClientError(
nym_validator_client::ValidatorClientError::NymAPIError { source: e },
)
})?;
tracing::debug!("Fetching list of gateways from: {nym_api}");
let gateways = client
.get_all_basic_entry_assigned_nodes_with_metadata()
// Use our helper to handle pagination
let gateways = get_all_basic_entry_nodes_with_metadata(&client, true)
.await?
.nodes;
info!("nym api reports {} gateways", gateways.len());
@@ -148,7 +207,7 @@ async fn connect(endpoint: &str) -> Result<WsConn, ClientCoreError> {
JSWebsocket::new(endpoint).map_err(|_| ClientCoreError::GatewayJsConnectionFailure)
}
async fn measure_latency<G>(gateway: &G) -> Result<GatewayWithLatency<G>, ClientCoreError>
async fn measure_latency<G>(gateway: &G) -> Result<GatewayWithLatency<'_, G>, ClientCoreError>
where
G: ConnectableGateway,
{
@@ -245,7 +304,7 @@ pub async fn choose_gateway_by_latency<R: Rng, G: ConnectableGateway + Clone>(
let gateways_with_latency = gateways_with_latency.lock().await;
let chosen = gateways_with_latency
.choose_weighted(rng, |item| 1. / item.latency.as_secs_f32())
.expect("invalid selection weight!");
.map_err(|source| ClientCoreError::GatewaySelectionFailure { source })?;
info!(
"chose gateway {} with average latency of {:?}",
+38 -2
View File
@@ -18,18 +18,54 @@ pub use nym_topology::{
};
#[cfg(target_arch = "wasm32")]
pub(crate) fn spawn_future<F>(future: F)
pub fn spawn_future<F>(future: F)
where
F: Future<Output = ()> + 'static,
{
wasm_bindgen_futures::spawn_local(future);
}
// TODO: expose similar API to the rest of the codebase,
// perhaps with some simple trait for a task to define its name
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn spawn_future<F>(future: F)
#[track_caller]
pub fn spawn_future<F>(future: F)
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
tokio::spawn(future);
}
#[cfg(not(target_arch = "wasm32"))]
#[track_caller]
pub fn spawn_named_future<F>(future: F, name: &str)
where
F: Future + Send + 'static,
F::Output: Send + 'static,
{
cfg_if::cfg_if! {if #[cfg(tokio_unstable)] {
#[allow(clippy::expect_used)]
tokio::task::Builder::new().name(name).spawn(future).expect("failed to spawn future");
} else {
let _ = name;
tracing::debug!(r#"the underlying binary hasn't been built with `RUSTFLAGS="--cfg tokio_unstable"` - the future naming won't do anything"#);
spawn_future(future);
}}
}
#[macro_export]
macro_rules! spawn_future {
($future:expr) => {{
$crate::spawn_future($future)
}};
($future:expr, $name:expr) => {{
cfg_if::cfg_if! {if #[cfg(not(target_arch = "wasm32"))] {
$crate::spawn_named_future($future, $name)
} else {
let _ = $name;
$crate::spawn_future($future)
}}
}};
}
@@ -30,6 +30,7 @@ optional = true
path = "../../../sqlx-pool-guard"
[build-dependencies]
anyhow = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
+7 -4
View File
@@ -2,23 +2,24 @@
// SPDX-License-Identifier: Apache-2.0
#[tokio::main]
async fn main() {
async fn main() -> anyhow::Result<()> {
#[cfg(feature = "fs-surb-storage")]
{
use anyhow::Context;
use sqlx::{Connection, SqliteConnection};
use std::env;
let out_dir = env::var("OUT_DIR").unwrap();
let out_dir = env::var("OUT_DIR")?;
let database_path = format!("{out_dir}/fs-surbs-example.sqlite");
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
.await
.expect("Failed to create SQLx database connection");
.context("Failed to create SQLx database connection")?;
sqlx::migrate!("./fs_surbs_migrations")
.run(&mut conn)
.await
.expect("Failed to perform SQLx migrations");
.context("Failed to perform SQLx migrations")?;
#[cfg(target_family = "unix")]
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
@@ -28,4 +29,6 @@ async fn main() {
// not a valid windows path... but hey, it works...
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
}
Ok(())
}
@@ -201,7 +201,7 @@ impl<C, St> GatewayClient<C, St> {
#[cfg(not(target_arch = "wasm32"))]
pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> {
debug!(
"Attemting to establish connection to gateway at: {}",
"Attempting to establish connection to gateway at: {}",
self.gateway_address
);
let (ws_stream, _) = connect_async(
@@ -337,7 +337,7 @@ impl PartiallyDelegatedHandle {
// check if the split stream didn't error out
let receive_res = stream_receiver
.try_recv()
.expect("stream sender was somehow dropped without sending anything!");
.map_err(|_| GatewayClientError::ConnectionAbruptlyClosed)?;
if let Some(res) = receive_res {
let _res = res?;
@@ -5,8 +5,8 @@ use crate::nyxd::{self, NyxdClient};
use crate::signing::direct_wallet::DirectSecp256k1HdWallet;
use crate::signing::signer::{NoSigner, OfflineSigner};
use crate::{
nym_api, DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient,
ReqwestRpcClient, ValidatorClientError,
DirectSigningReqwestRpcValidatorClient, QueryReqwestRpcValidatorClient, ReqwestRpcClient,
ValidatorClientError,
};
use nym_api_requests::ecash::models::{
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
@@ -153,7 +153,7 @@ impl Config {
pub struct Client<C, S = NoSigner> {
// ideally they would have been read-only, but unfortunately rust doesn't have such features
// #[deprecated(note = "please use `nym_api_client` instead")]
pub nym_api: nym_api::Client,
pub nym_api: nym_http_api_client::Client,
// pub nym_api_client: NymApiClient,
pub nyxd: NyxdClient<C, S>,
}
@@ -214,7 +214,7 @@ impl Client<ReqwestRpcClient> {
impl<C> Client<C> {
pub fn new_with_rpc_client(config: Config, rpc_client: C) -> Self {
let nym_api_client = nym_api::Client::new(config.api_url.clone(), None);
let nym_api_client = nym_http_api_client::Client::new(config.api_url.clone(), None);
Client {
nym_api: nym_api_client,
@@ -228,7 +228,7 @@ impl<C, S> Client<C, S> {
where
S: OfflineSigner,
{
let nym_api_client = nym_api::Client::new(config.api_url.clone(), None);
let nym_api_client = nym_http_api_client::Client::new(config.api_url.clone(), None);
Client {
nym_api: nym_api_client,
@@ -385,38 +385,25 @@ impl<C, S> Client<C, S> {
}
}
/// DEPRECATED: Use nym_http_api_client::Client with from_network() or with_bincode() instead
#[deprecated(
since = "1.2.0",
note = "Use nym_http_api_client::Client::from_network() or ClientBuilder::with_bincode() instead"
)]
#[derive(Clone)]
pub struct NymApiClient {
pub use_bincode: bool,
pub nym_api: nym_api::Client,
pub nym_api: nym_http_api_client::Client,
// TODO: perhaps if we really need it at some (currently I don't see any reasons for it)
// we could re-implement the communication with the REST API on port 1317
}
impl From<nym_api::Client> for NymApiClient {
fn from(nym_api: nym_api::Client) -> Self {
NymApiClient {
use_bincode: false,
nym_api,
}
}
}
// we have to allow the use of deprecated method here as they're calling the deprecated trait methods
#[allow(deprecated)]
impl NymApiClient {
pub fn new(api_url: Url) -> Self {
let nym_api = nym_api::Client::new(api_url, None);
NymApiClient {
use_bincode: true,
nym_api,
}
}
#[cfg(not(target_arch = "wasm32"))]
pub fn new_with_timeout(api_url: Url, timeout: std::time::Duration) -> Self {
let nym_api = nym_api::Client::new(api_url, Some(timeout));
let nym_api = nym_http_api_client::Client::new(api_url, Some(timeout));
NymApiClient {
use_bincode: true,
@@ -431,7 +418,7 @@ impl NymApiClient {
}
pub fn new_with_user_agent(api_url: Url, user_agent: impl Into<UserAgent>) -> Self {
let nym_api = nym_api::Client::builder::<_, ValidatorClientError>(api_url)
let nym_api = nym_http_api_client::Client::builder::<_, ValidatorClientError>(api_url)
.expect("invalid api url")
.with_user_agent(user_agent.into())
.build::<ValidatorClientError>()
@@ -719,10 +706,11 @@ impl NymApiClient {
pub async fn partial_expiration_date_signatures(
&self,
expiration_date: Option<Date>,
epoch_id: Option<EpochId>,
) -> Result<PartialExpirationDateSignatureResponse, ValidatorClientError> {
Ok(self
.nym_api
.partial_expiration_date_signatures(expiration_date)
.partial_expiration_date_signatures(expiration_date, epoch_id)
.await?)
}
@@ -739,10 +727,11 @@ impl NymApiClient {
pub async fn global_expiration_date_signatures(
&self,
expiration_date: Option<Date>,
epoch_id: Option<EpochId>,
) -> Result<AggregatedExpirationDateSignatureResponse, ValidatorClientError> {
Ok(self
.nym_api
.global_expiration_date_signatures(expiration_date)
.global_expiration_date_signatures(expiration_date, epoch_id)
.await?)
}
@@ -3,7 +3,6 @@
use crate::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
use crate::nyxd::error::NyxdError;
use crate::NymApiClient;
use nym_coconut_dkg_common::types::{EpochId, NodeIndex};
use nym_coconut_dkg_common::verification_key::ContractVKShare;
use nym_compact_ecash::error::CompactEcashError;
@@ -15,7 +14,7 @@ use url::Url;
// TODO: it really doesn't feel like this should live in this crate.
#[derive(Clone)]
pub struct EcashApiClient {
pub api_client: NymApiClient,
pub api_client: nym_http_api_client::Client,
pub verification_key: VerificationKeyAuth,
pub node_id: NodeIndex,
pub cosmos_address: cosmrs::AccountId,
@@ -25,10 +24,10 @@ impl Display for EcashApiClient {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[id: {}] {} @ {}",
"[id: {}] {} @ {:?}",
self.node_id,
self.cosmos_address,
self.api_client.api_url()
self.api_client.base_urls()
)
}
}
@@ -60,6 +59,9 @@ pub enum EcashApiError {
source: CompactEcashError,
},
#[error("failed to create API client: {0}")]
ClientError(String),
#[error("the provided account address is malformed: {source}")]
MalformedAccountAddress {
#[from]
@@ -89,8 +91,16 @@ impl TryFrom<ContractVKShare> for EcashApiClient {
// In non-client applications this resolver can cause warning logs about H2 connection
// failure. This indicates that the long lived https connection was closed by the remote
// peer and the resolver will have to reconnect. It should not impact actual functionality
let api_client = nym_http_api_client::Client::builder::<
_,
nym_api_requests::models::RequestError,
>(url_address)
.map_err(|e| EcashApiError::ClientError(e.to_string()))?
.build::<nym_api_requests::models::RequestError>()
.map_err(|e| EcashApiError::ClientError(e.to_string()))?;
Ok(EcashApiClient {
api_client: NymApiClient::new(url_address),
api_client,
verification_key: VerificationKeyAuth::try_from_bs58(&share.share)?,
node_id: share.node_index,
cosmos_address: share.owner.as_str().parse()?,
@@ -1,7 +1,8 @@
use crate::nym_api::NymApiClientExt;
use crate::nyxd::contract_traits::MixnetQueryClient;
use crate::nyxd::error::NyxdError;
use crate::nyxd::Config as ClientConfig;
use crate::{NymApiClient, QueryHttpRpcNyxdClient, ValidatorClientError};
use crate::{QueryHttpRpcNyxdClient, ValidatorClientError};
use colored::Colorize;
use core::fmt;
use itertools::Itertools;
@@ -87,8 +88,19 @@ fn setup_connection_tests<H: BuildHasher + 'static>(
}
});
let api_connection_test_clients = api_urls.map(|(network, url)| {
ClientForConnectionTest::Api(network, url.clone(), NymApiClient::new(url))
let api_connection_test_clients = api_urls.filter_map(|(network, url)| {
match nym_http_api_client::Client::builder(url.clone())
.and_then(|b| b.build::<nym_api_requests::models::RequestError>())
{
Ok(client) => Some(ClientForConnectionTest::Api(network, url, client)),
Err(err) => {
eprintln!(
"Failed to create API client for {}: {err}",
network.network_name
);
None
}
}
});
nyxd_connection_test_clients.chain(api_connection_test_clients)
@@ -160,7 +172,7 @@ async fn test_nyxd_connection(
async fn test_nym_api_connection(
network: NymNetworkDetails,
url: &Url,
client: &NymApiClient,
client: &nym_http_api_client::Client,
) -> ConnectionResult {
let result = match timeout(
Duration::from_secs(CONNECTION_TEST_TIMEOUT_SEC),
@@ -186,7 +198,7 @@ async fn test_nym_api_connection(
enum ClientForConnectionTest {
Nyxd(NymNetworkDetails, Url, Box<QueryHttpRpcNyxdClient>),
Api(NymNetworkDetails, Url, NymApiClient),
Api(NymNetworkDetails, Url, nym_http_api_client::Client),
}
impl ClientForConnectionTest {
@@ -14,7 +14,6 @@ pub mod signing;
pub use crate::error::ValidatorClientError;
pub use crate::rpc::reqwest::ReqwestRpcClient;
pub use crate::signing::direct_wallet::DirectSecp256k1HdWallet;
pub use client::NymApiClient;
pub use client::{Client, Config, EcashApiClient};
pub use nym_api_requests::*;
pub use nym_http_api_client::UserAgent;
@@ -3,19 +3,22 @@
use crate::nym_api::error::NymAPIError;
use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG};
use crate::nym_nodes::SkimmedNodesWithMetadata;
use async_trait::async_trait;
use nym_api_requests::ecash::models::{
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationResponse,
IssuedTicketbooksChallengeCommitmentRequest, IssuedTicketbooksChallengeCommitmentResponse,
IssuedTicketbooksDataRequest, IssuedTicketbooksDataResponse, IssuedTicketbooksForCountResponse,
IssuedTicketbooksForResponse, VerifyEcashTicketBody,
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashSignerStatusResponse,
EcashTicketVerificationResponse, IssuedTicketbooksChallengeCommitmentRequest,
IssuedTicketbooksChallengeCommitmentResponse, IssuedTicketbooksDataRequest,
IssuedTicketbooksDataResponse, IssuedTicketbooksForCountResponse, IssuedTicketbooksForResponse,
VerifyEcashTicketBody,
};
use nym_api_requests::ecash::VerificationKeyResponse;
use nym_api_requests::models::{
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainStatusResponse,
KeyRotationInfoResponse, LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody,
NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
ChainStatusResponse, KeyRotationInfoResponse, LegacyDescribedMixNode, NodePerformanceResponse,
NodeRefreshBody, NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
SignerInformationResponse,
};
use nym_api_requests::nym_nodes::{
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1,
@@ -35,7 +38,7 @@ pub use nym_api_requests::{
MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse,
StakeSaturationResponse, UptimeResponse,
},
nym_nodes::{CachedNodesResponse, SemiSkimmedNode, SkimmedNode},
nym_nodes::{CachedNodesResponse, SemiSkimmedNode, SemiSkimmedNodesWithMetadata, SkimmedNode},
NymNetworkDetailsResponse,
};
use nym_contracts_common::IdentityKey;
@@ -47,8 +50,8 @@ use time::format_description::BorrowedFormatItem;
use time::Date;
use tracing::instrument;
use crate::ValidatorClientError;
pub use nym_coconut_dkg_common::types::EpochId;
pub use nym_http_api_client::Client;
pub mod error;
pub mod routes;
@@ -60,6 +63,9 @@ pub fn rfc_3339_date() -> Vec<BorrowedFormatItem<'static>> {
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait NymApiClientExt: ApiClient {
/// Get the current API URL being used by the client
fn api_url(&self) -> &url::Url;
async fn health(&self) -> Result<ApiHealthResponse, NymAPIError> {
self.get_json(
&[
@@ -239,6 +245,162 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn get_current_rewarded_set(&self) -> Result<RewardedSetResponse, NymAPIError> {
self.get_rewarded_set().await
}
async fn get_all_basic_nodes_with_metadata(
&self,
) -> Result<SkimmedNodesWithMetadata, NymAPIError> {
// unroll first loop iteration in order to obtain the metadata
let mut page = 0;
let res = self
.get_basic_nodes_v2(false, Some(page), None, true)
.await?;
let mut nodes = res.nodes.data;
let metadata = res.metadata;
if res.nodes.pagination.total == nodes.len() {
return Ok(SkimmedNodesWithMetadata::new(nodes, metadata));
}
page += 1;
loop {
let mut res = self
.get_basic_nodes_v2(false, Some(page), None, true)
.await?;
if !metadata.consistency_check(&res.metadata) {
// Create a custom error for inconsistent metadata
return Err(NymAPIError::EndpointFailure {
status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
error: nym_api_requests::models::RequestError::new(
"Inconsistent paged metadata",
),
});
}
nodes.append(&mut res.nodes.data);
if nodes.len() >= res.nodes.pagination.total {
break;
} else {
page += 1
}
}
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
}
async fn get_all_basic_active_mixing_assigned_nodes_with_metadata(
&self,
) -> Result<SkimmedNodesWithMetadata, NymAPIError> {
// Get all mixing nodes that are in the active/rewarded set
let mut page = 0;
let res = self
.get_basic_active_mixing_assigned_nodes_v2(false, Some(page), None, false)
.await?;
let metadata = res.metadata;
let mut nodes = res.nodes.data;
if res.nodes.pagination.total == nodes.len() {
return Ok(SkimmedNodesWithMetadata::new(nodes, metadata));
}
page += 1;
loop {
let res = self
.get_basic_active_mixing_assigned_nodes_v2(false, Some(page), None, false)
.await?;
if !metadata.consistency_check(&res.metadata) {
return Err(NymAPIError::EndpointFailure {
status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
error: nym_api_requests::models::RequestError::new(
"Inconsistent paged metadata",
),
});
}
nodes.append(&mut res.nodes.data.clone());
// Check if we've got all nodes
if nodes.len() >= res.nodes.pagination.total {
break;
} else {
page += 1;
}
}
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
}
async fn get_all_basic_entry_assigned_nodes_with_metadata(
&self,
) -> Result<SkimmedNodesWithMetadata, NymAPIError> {
// Get all nodes that can act as entry gateways
let mut page = 0;
let res = self
.get_basic_entry_assigned_nodes_v2(false, Some(page), None, false)
.await?;
let metadata = res.metadata;
let mut nodes = res.nodes.data;
if res.nodes.pagination.total == nodes.len() {
return Ok(SkimmedNodesWithMetadata::new(nodes, metadata));
}
page += 1;
loop {
let res = self
.get_basic_entry_assigned_nodes_v2(false, Some(page), None, false)
.await?;
if !metadata.consistency_check(&res.metadata) {
return Err(NymAPIError::EndpointFailure {
status: reqwest::StatusCode::INTERNAL_SERVER_ERROR,
error: nym_api_requests::models::RequestError::new(
"Inconsistent paged metadata",
),
});
}
nodes.append(&mut res.nodes.data.clone());
// Check if we've got all nodes
if nodes.len() >= res.nodes.pagination.total {
break;
} else {
page += 1;
}
}
Ok(SkimmedNodesWithMetadata::new(nodes, metadata))
}
async fn get_all_described_nodes(&self) -> Result<Vec<NymNodeDescription>, NymAPIError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut descriptions = Vec::new();
loop {
let mut res = self.get_nodes_described(Some(page), None).await?;
descriptions.append(&mut res.data);
if descriptions.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(descriptions)
}
#[tracing::instrument(level = "debug", skip_all)]
async fn get_nym_nodes(
&self,
@@ -266,6 +428,25 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn get_all_bonded_nym_nodes(&self) -> Result<Vec<NymNodeDetails>, ValidatorClientError> {
// TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere
let mut page = 0;
let mut bonds = Vec::new();
loop {
let mut res = self.get_nym_nodes(Some(page), None).await?;
bonds.append(&mut res.data);
if bonds.len() < res.pagination.total {
page += 1
} else {
break;
}
}
Ok(bonds)
}
#[deprecated]
#[tracing::instrument(level = "debug", skip_all)]
async fn get_basic_mixnodes(&self) -> Result<CachedNodesResponse<SkimmedNode>, NymAPIError> {
@@ -1101,8 +1282,9 @@ pub trait NymApiClientExt: ApiClient {
async fn partial_expiration_date_signatures(
&self,
expiration_date: Option<Date>,
epoch_id: Option<EpochId>,
) -> Result<PartialExpirationDateSignatureResponse, NymAPIError> {
let params = match expiration_date {
let mut params = match expiration_date {
None => Vec::new(),
Some(exp) => vec![(
ecash::EXPIRATION_DATE_PARAM,
@@ -1110,6 +1292,10 @@ pub trait NymApiClientExt: ApiClient {
)],
};
if let Some(epoch_id) = epoch_id {
params.push((ecash::EPOCH_ID_PARAM, epoch_id.to_string()));
}
self.get_json(
&[
routes::V1_API_VERSION,
@@ -1146,8 +1332,9 @@ pub trait NymApiClientExt: ApiClient {
async fn global_expiration_date_signatures(
&self,
expiration_date: Option<Date>,
epoch_id: Option<EpochId>,
) -> Result<AggregatedExpirationDateSignatureResponse, NymAPIError> {
let params = match expiration_date {
let mut params = match expiration_date {
None => Vec::new(),
Some(exp) => vec![(
ecash::EXPIRATION_DATE_PARAM,
@@ -1155,6 +1342,10 @@ pub trait NymApiClientExt: ApiClient {
)],
};
if let Some(epoch_id) = epoch_id {
params.push((ecash::EPOCH_ID_PARAM, epoch_id.to_string()));
}
self.get_json(
&[
routes::V1_API_VERSION,
@@ -1331,6 +1522,22 @@ pub trait NymApiClientExt: ApiClient {
.await
}
async fn get_chain_blocks_status(&self) -> Result<ChainBlocksStatusResponse, NymAPIError> {
self.get_json("/v1/network/chain-blocks-status", NO_PARAMS)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_signer_status(&self) -> Result<EcashSignerStatusResponse, NymAPIError> {
self.get_json("/v1/ecash/signer-status", NO_PARAMS).await
}
#[instrument(level = "debug", skip(self))]
async fn get_signer_information(&self) -> Result<SignerInformationResponse, NymAPIError> {
self.get_json("/v1/api-status/signer-information", NO_PARAMS)
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_key_rotation_info(&self) -> Result<KeyRotationInfoResponse, NymAPIError> {
self.get_json(
@@ -1343,8 +1550,49 @@ pub trait NymApiClientExt: ApiClient {
)
.await
}
/// Method to change the base API URLs being used by the client
fn change_base_urls(&mut self, urls: Vec<url::Url>);
/// Retrieve expanded information for all bonded nodes on the network
async fn get_all_expanded_nodes(&self) -> Result<SemiSkimmedNodesWithMetadata, NymAPIError> {
// Unroll the first iteration to get the metadata
let mut page = 0;
let res = self.get_expanded_nodes(false, Some(page), None).await?;
let mut nodes = res.nodes.data;
let metadata = res.metadata;
if res.nodes.pagination.total == nodes.len() {
return Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata));
}
page += 1;
loop {
let mut res = self.get_expanded_nodes(false, Some(page), None).await?;
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
}
}
// Client is already nym_http_api_client::Client (re-exported above), so just one impl needed
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl NymApiClientExt for Client {}
impl NymApiClientExt for nym_http_api_client::Client {
fn api_url(&self) -> &url::Url {
self.current_url().as_ref()
}
fn change_base_urls(&mut self, urls: Vec<url::Url>) {
self.change_base_urls(urls.into_iter().map(|u| u.into()).collect());
}
}
@@ -8,11 +8,11 @@ use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use cosmrs::AccountId;
use cosmwasm_std::Addr;
use nym_coconut_dkg_common::dealer::RegisteredDealerDetails;
use nym_coconut_dkg_common::types::{ChunkIndex, NodeIndex, StateAdvanceResponse};
use serde::Deserialize;
use tracing::trace;
use nym_coconut_dkg_common::dealer::RegisteredDealerDetails;
pub use nym_coconut_dkg_common::{
dealer::{DealerDetailsResponse, PagedDealerIndexResponse, PagedDealerResponse},
dealing::{
@@ -21,7 +21,9 @@ pub use nym_coconut_dkg_common::{
},
msg::QueryMsg as DkgQueryMsg,
types::{DealerDetails, DealingIndex, Epoch, EpochId, EpochState, State},
verification_key::{ContractVKShare, PagedVKSharesResponse, VkShareResponse},
verification_key::{
ContractVKShare, PagedVKSharesResponse, VerificationKeyShare, VkShareResponse,
},
};
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -139,12 +139,22 @@ impl NyxdClient<HttpClient> {
})
}
pub fn connect_with_network_details<U>(
endpoint: U,
network_details: NymNetworkDetails,
) -> Result<QueryHttpRpcNyxdClient, NyxdError>
where
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
{
let config = Config::try_from_nym_network_details(&network_details)?;
Self::connect(config, endpoint)
}
pub fn connect_to_default_env<U>(endpoint: U) -> Result<QueryHttpRpcNyxdClient, NyxdError>
where
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
{
let config = Config::try_from_nym_network_details(&NymNetworkDetails::new_from_env())?;
Self::connect(config, endpoint)
Self::connect_with_network_details(endpoint, NymNetworkDetails::new_from_env())
}
}
+1
View File
@@ -38,6 +38,7 @@ cosmrs = { workspace = true }
cosmwasm-std = { workspace = true }
nym-validator-client = { path = "../client-libs/validator-client" }
nym-http-api-client = { path = "../http-api-client" }
nym-bin-common = { path = "../../common/bin-common", features = ["output_format"] }
nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] }
nym-network-defaults = { path = "../network-defaults" }
+1 -1
View File
@@ -2,12 +2,12 @@
// SPDX-License-Identifier: Apache-2.0
use crate::context::errors::ContextError;
pub use nym_http_api_client::Client as NymApiClient;
use nym_network_defaults::{
setup_env,
var_names::{MIXNET_CONTRACT_ADDRESS, NYM_API, NYXD, VESTING_CONTRACT_ADDRESS},
NymNetworkDetails,
};
pub use nym_validator_client::nym_api::Client as NymApiClient;
use nym_validator_client::nyxd::{self, AccountId, NyxdClient};
use nym_validator_client::{
DirectSigningHttpRpcNyxdClient, DirectSigningHttpRpcValidatorClient, QueryHttpRpcNyxdClient,
+1 -1
View File
@@ -86,7 +86,7 @@ pub async fn execute(args: Args) -> anyhow::Result<()> {
anyhow!("ticketbook got incorrectly imported - the master verification key is missing")
})?;
let expiration_signatures = persistent_storage
.get_expiration_date_signatures(expiration_date)
.get_expiration_date_signatures(expiration_date, epoch_id)
.await?
.ok_or_else(|| {
anyhow!(
@@ -120,7 +120,7 @@ async fn issue_to_file(args: Args, client: SigningClient) -> anyhow::Result<()>
if args.include_expiration_date_signatures {
let signatures = credentials_store
.get_expiration_date_signatures(expiration_date)
.get_expiration_date_signatures(expiration_date, epoch_id)
.await?
.ok_or(anyhow!("missing expiration date signatures!"))?;
+3
View File
@@ -4,6 +4,7 @@
use clap::{Args, Subcommand};
pub mod ecash;
pub mod nyx;
#[derive(Debug, Args)]
#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)]
@@ -16,4 +17,6 @@ pub struct Internal {
pub enum InternalCommands {
/// Ecash related internal commands
Ecash(ecash::InternalEcash),
Nyx(nyx::InternalNyx),
}
@@ -0,0 +1,116 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::context::SigningClient;
use anyhow::bail;
use clap::Parser;
use nym_mixnet_contract_common::nym_node::Role;
use nym_mixnet_contract_common::reward_params::NodeRewardingParameters;
use nym_mixnet_contract_common::{
EpochRewardedSet, EpochState, NodeId, RewardingParams, RoleAssignment,
};
use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt;
use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient};
use rand::prelude::*;
use rand::thread_rng;
#[derive(Debug, Parser)]
pub struct Args {}
fn choose_new_nodes(
params: &RewardingParams,
rewarded_set: &EpochRewardedSet,
role: Role,
) -> Vec<NodeId> {
let mut rng = thread_rng();
match role {
Role::EntryGateway => rewarded_set
.assignment
.entry_gateways
.choose_multiple(&mut rng, params.rewarded_set.entry_gateways as usize)
.copied()
.collect(),
Role::Layer1 => rewarded_set
.assignment
.layer1
.choose_multiple(&mut rng, params.rewarded_set.mixnodes as usize / 3)
.copied()
.collect(),
Role::Layer2 => rewarded_set
.assignment
.layer2
.choose_multiple(&mut rng, params.rewarded_set.mixnodes as usize / 3)
.copied()
.collect(),
Role::Layer3 => rewarded_set
.assignment
.layer3
.choose_multiple(&mut rng, params.rewarded_set.mixnodes as usize / 3)
.copied()
.collect(),
Role::ExitGateway => rewarded_set
.assignment
.exit_gateways
.choose_multiple(&mut rng, params.rewarded_set.exit_gateways as usize)
.copied()
.collect(),
Role::Standby => rewarded_set
.assignment
.standby
.choose_multiple(&mut rng, params.rewarded_set.standby as usize)
.copied()
.collect(),
}
}
pub async fn force_advance_epoch(_: Args, client: SigningClient) -> anyhow::Result<()> {
let current_epoch = client.get_current_interval_details().await?;
let epoch_status = client.get_current_epoch_status().await?;
if epoch_status.being_advanced_by.as_str() != client.address().to_string() {
bail!(
"this client is not authorised to perform any epoch operations. we need {}",
client.address()
)
}
let rewarding_params = client.get_rewarding_parameters().await?;
let current_rewarded_set = client.get_rewarded_set().await?;
if !current_epoch.is_current_epoch_over {
println!("the current epoch is not over yet - there's nothing to do")
}
// is this most efficient? no. but it's simple
loop {
let epoch_status = client.get_current_epoch_status().await?;
match epoch_status.state {
EpochState::InProgress => break,
EpochState::Rewarding { final_node_id, .. } => {
println!("rewarding {final_node_id} with big fat 0...");
client
.reward_node(
final_node_id,
NodeRewardingParameters::new(Default::default(), Default::default()),
None,
)
.await?;
}
EpochState::ReconcilingEvents => {
println!("trying to reconcile events...");
client.reconcile_epoch_events(None, None).await?;
}
EpochState::RoleAssignment { next } => {
let nodes = choose_new_nodes(&rewarding_params, &current_rewarded_set, next);
println!("assigning {nodes:?} as {next}");
client
.assign_roles(RoleAssignment { role: next, nodes }, None)
.await?;
}
}
}
Ok(())
}
+19
View File
@@ -0,0 +1,19 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::{Args, Subcommand};
pub mod force_advance_epoch;
#[derive(Debug, Args)]
#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)]
pub struct InternalNyx {
#[clap(subcommand)]
pub command: InternalNyxCommands,
}
#[derive(Debug, Subcommand)]
pub enum InternalNyxCommands {
/// Attempt to force advance the current epoch
ForceAdvanceEpoch(force_advance_epoch::Args),
}
@@ -241,7 +241,7 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) {
let node_id = row.node_id.clone().parse::<u32>().unwrap();
let coins: Vec<Coin> = vec![];
undelegation_msgs.push((ExecuteMsg::Undelegate { node_id }, coins));
undelegation_table.add_row(&[row.node_id.clone()]);
undelegation_table.add_row(std::slice::from_ref(&row.node_id));
if row.amount.amount > 0 {
delegation_msgs
@@ -188,7 +188,7 @@ impl<C> ContractTesterBuilder<C> {
*self.app.api()
}
pub fn querier(&self) -> QuerierWrapper {
pub fn querier(&self) -> QuerierWrapper<'_> {
self.app.wrap()
}
}
@@ -57,7 +57,7 @@ pub trait NodeBond {
fn is_unbonding(&self) -> bool;
fn identity(&self) -> IdentityKeyRef;
fn identity(&self) -> IdentityKeyRef<'_>;
fn original_pledge(&self) -> &Coin;
@@ -125,7 +125,7 @@ impl NodeBond for MixNodeBond {
self.is_unbonding
}
fn identity(&self) -> IdentityKeyRef {
fn identity(&self) -> IdentityKeyRef<'_> {
self.identity()
}
@@ -178,7 +178,7 @@ impl NodeBond for NymNodeBond {
self.is_unbonding
}
fn identity(&self) -> IdentityKeyRef {
fn identity(&self) -> IdentityKeyRef<'_> {
self.identity()
}
@@ -58,7 +58,7 @@ impl<'a> PrimaryKey<'a> for Role {
type Suffix = <u8 as PrimaryKey<'a>>::Suffix;
type SuperSuffix = <u8 as PrimaryKey<'a>>::SuperSuffix;
fn key(&self) -> Vec<Key> {
fn key(&self) -> Vec<Key<'_>> {
// I'm not sure why it wasn't possible to delegate the call to
// `(*self as u8).key()` directly...
// I guess because of the `Key::Ref(&'a [u8])` variant?
@@ -86,6 +86,25 @@ impl IntervalRewardParams {
pub fn to_inline_json(&self) -> String {
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
}
pub fn active_node_work(&self, standby_node_work: Decimal) -> WorkFactor {
self.active_set_work_factor * standby_node_work
}
pub fn standby_node_work(
&self,
rewarded_set_size: Decimal,
standby_set_size: Decimal,
) -> WorkFactor {
let f = self.active_set_work_factor;
let k = rewarded_set_size;
let one = Decimal::one();
// nodes in reserve
let k_r = standby_set_size;
one / (f * k - (f - one) * k_r)
}
}
/// Parameters used for reward calculation.
@@ -109,18 +128,15 @@ pub struct RewardingParams {
impl RewardingParams {
pub fn active_node_work(&self) -> WorkFactor {
self.interval.active_set_work_factor * self.standby_node_work()
let standby_work = self.standby_node_work();
self.interval.active_node_work(standby_work)
}
pub fn standby_node_work(&self) -> WorkFactor {
let f = self.interval.active_set_work_factor;
let k = self.dec_rewarded_set_size();
let one = Decimal::one();
// nodes in reserve
let k_r = self.dec_standby_set_size();
one / (f * k - (f - one) * k_r)
let rewarded_set_size = self.dec_rewarded_set_size();
let standby_set_size = self.dec_standby_set_size();
self.interval
.standby_node_work(rewarded_set_size, standby_set_size)
}
pub fn rewarded_set_size(&self) -> u32 {
@@ -3,6 +3,7 @@
use crate::config_score::{ConfigScoreParams, OutdatedVersionWeights, VersionScoreFormulaParams};
use crate::nym_node::Role;
use crate::reward_params::RewardedSetParams;
use crate::EpochId;
use contracts_common::Percent;
use cosmwasm_schema::cw_serde;
@@ -85,7 +86,11 @@ impl RewardedSet {
}
pub fn rewarded_set_size(&self) -> usize {
self.active_set_size() + self.standby.len()
self.active_set_size() + self.standby_set_size()
}
pub fn standby_set_size(&self) -> usize {
self.standby.len()
}
pub fn get_role(&self, node_id: NodeId) -> Option<Role> {
@@ -110,6 +115,13 @@ impl RewardedSet {
}
None
}
pub fn matches_parameters(&self, params: RewardedSetParams) -> bool {
self.entry_gateways.len() <= params.entry_gateways as usize
&& self.exit_gateways.len() <= params.exit_gateways as usize
&& self.layer1.len() + self.layer2.len() + self.layer3.len() <= params.mixnodes as usize
&& self.standby.len() <= params.standby as usize
}
}
#[cw_serde]
+2
View File
@@ -3,6 +3,7 @@ name = "nym-credential-storage"
version = "0.1.0"
edition = "2021"
license.workspace = true
rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -33,6 +34,7 @@ features = ["rt-multi-thread", "net", "signal", "fs"]
[build-dependencies]
anyhow = { workspace = true }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"sqlite",
+13 -4
View File
@@ -3,22 +3,29 @@
* SPDX-License-Identifier: Apache-2.0
*/
use anyhow::Context;
use sqlx::{Connection, SqliteConnection};
use std::env;
#[tokio::main]
async fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
async fn main() -> anyhow::Result<()> {
let out_dir = env::var("OUT_DIR")?;
let database_path = format!("{out_dir}/coconut-credential-example.sqlite");
// remove the db file if it already existed from previous build
// in case it was from a different branch
if std::fs::exists(&database_path)? {
std::fs::remove_file(&database_path)?;
}
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
.await
.expect("Failed to create SQLx database connection");
.context("Failed to create SQLx database connection")?;
sqlx::migrate!("./migrations")
.run(&mut conn)
.await
.expect("Failed to perform SQLx migrations");
.context("Failed to perform SQLx migrations")?;
#[cfg(target_family = "unix")]
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
@@ -27,4 +34,6 @@ async fn main() {
// for some strange reason we need to add a leading `/` to the windows path even though it's
// not a valid windows path... but hey, it works...
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
Ok(())
}
@@ -0,0 +1,123 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
-- 1. add temporary `epoch_id` column
ALTER TABLE pending_issuance
ADD COLUMN epoch_id INTEGER;
-- 2. populate the value
UPDATE pending_issuance
SET epoch_id = (SELECT epoch_id
FROM expiration_date_signatures
WHERE expiration_date_signatures.expiration_date = pending_issuance.expiration_date);
-- 3. create new expiration_date_signatures table (with changed constraints)
CREATE TABLE expiration_date_signatures_new
(
expiration_date DATE NOT NULL,
epoch_id INTEGER NOT NULL,
serialization_revision INTEGER NOT NULL,
-- combined signatures for all tuples issued for given day
serialised_signatures BLOB NOT NULL,
PRIMARY KEY (epoch_id, expiration_date)
);
-- 4. migrate the data
INSERT INTO expiration_date_signatures_new (expiration_date, epoch_id, serialization_revision, serialised_signatures)
SELECT expiration_date, epoch_id, serialization_revision, serialised_signatures
FROM expiration_date_signatures;
-- 5. drop and recreate the table references (due to new FK)
-- 5.1.
-- (data for ticketbooks that have an associated deposit, but failed to get issued)
CREATE TABLE pending_issuance_new
(
deposit_id INTEGER NOT NULL PRIMARY KEY,
-- introduce a way for us to introduce breaking changes in serialization of data
serialization_revision INTEGER NOT NULL,
pending_ticketbook_data BLOB NOT NULL UNIQUE,
-- for each ticketbook we MUST have corresponding expiration date signatures
expiration_date DATE NOT NULL,
epoch_id INTEGER NOT NULL,
-- for each ticketbook we MUST have corresponding expiration date signatures
FOREIGN KEY (epoch_id, expiration_date) REFERENCES expiration_date_signatures_new (epoch_id, expiration_date)
);
INSERT INTO pending_issuance_new (deposit_id, serialization_revision, pending_ticketbook_data, expiration_date,
epoch_id)
SELECT deposit_id, serialization_revision, pending_ticketbook_data, expiration_date, epoch_id
FROM pending_issuance;
-- 5.2.
CREATE TABLE ecash_ticketbook_new
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
-- introduce a way for us to introduce breaking changes in serialization of data
serialization_revision INTEGER NOT NULL,
-- the type of the associated ticketbook
ticketbook_type TEXT NOT NULL,
-- the actual crypto data of the ticketbook (wallet, keys, etc.)
ticketbook_data BLOB NOT NULL UNIQUE,
-- for each ticketbook we MUST have corresponding expiration date signatures
expiration_date DATE NOT NULL,
-- for each ticketbook we MUST have corresponding coin index signatures
epoch_id INTEGER NOT NULL,
-- the initial number of tickets the wallet has been created for
total_tickets INTEGER NOT NULL,
-- how many tickets have been used so far (the `l` value of the wallet)
used_tickets INTEGER NOT NULL,
-- FOREIGN KEYS:
-- for each ticketbook we MUST have corresponding coin index signatures
FOREIGN KEY (epoch_id) REFERENCES coin_indices_signatures (epoch_id),
-- for each ticketbook we MUST have corresponding expiration date signatures
FOREIGN KEY (expiration_date, epoch_id) REFERENCES expiration_date_signatures_new (expiration_date, epoch_id)
);
INSERT INTO ecash_ticketbook_new (id, serialization_revision, ticketbook_type, ticketbook_data, expiration_date,
epoch_id, total_tickets, used_tickets)
SELECT id,
serialization_revision,
ticketbook_type,
ticketbook_data,
expiration_date,
epoch_id,
total_tickets,
used_tickets
FROM ecash_ticketbook;
-- 6. finally swap out the old tables
-- drop old tables
DROP TABLE pending_issuance;
DROP TABLE ecash_ticketbook;
DROP TABLE expiration_date_signatures;
-- rename new tables
ALTER TABLE pending_issuance_new
RENAME TO pending_issuance;
ALTER TABLE ecash_ticketbook_new
RENAME TO ecash_ticketbook;
ALTER TABLE expiration_date_signatures_new
RENAME TO expiration_date_signatures;
@@ -28,7 +28,7 @@ struct EcashCredentialManagerInner {
pending: HashMap<i64, RetrievedPendingTicketbook>,
master_vk: HashMap<u64, VerificationKeyAuth>,
coin_indices_sigs: HashMap<u64, Vec<AnnotatedCoinIndexSignature>>,
expiration_date_sigs: HashMap<Date, Vec<AnnotatedExpirationDateSignature>>,
expiration_date_sigs: HashMap<(u64, Date), Vec<AnnotatedExpirationDateSignature>>,
_next_id: i64,
}
@@ -242,10 +242,14 @@ impl MemoryEcachTicketbookManager {
pub(crate) async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: u64,
) -> Option<Vec<AnnotatedExpirationDateSignature>> {
let guard = self.inner.read().await;
guard.expiration_date_sigs.get(&expiration_date).cloned()
guard
.expiration_date_sigs
.get(&(epoch_id, expiration_date))
.cloned()
}
pub(crate) async fn insert_expiration_date_signatures(
@@ -254,8 +258,9 @@ impl MemoryEcachTicketbookManager {
) {
let mut guard = self.inner.write().await;
guard
.expiration_date_sigs
.insert(sigs.expiration_date, sigs.signatures.clone());
guard.expiration_date_sigs.insert(
(sigs.epoch_id, sigs.expiration_date),
sigs.signatures.clone(),
);
}
}
@@ -39,7 +39,7 @@ impl SqliteEcashTicketbookManager {
Ok(())
}
pub(crate) async fn begin_storage_tx(&self) -> Result<Transaction<Sqlite>, sqlx::Error> {
pub(crate) async fn begin_storage_tx(&self) -> Result<Transaction<'_, Sqlite>, sqlx::Error> {
self.connection_pool.begin().await
}
@@ -260,15 +260,17 @@ impl SqliteEcashTicketbookManager {
pub(crate) async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: i64,
) -> Result<Option<RawExpirationDateSignatures>, sqlx::Error> {
sqlx::query_as!(
RawExpirationDateSignatures,
r#"
SELECT epoch_id as "epoch_id: u32", serialised_signatures, serialization_revision as "serialization_revision: u8"
SELECT serialised_signatures, serialization_revision as "serialization_revision: u8"
FROM expiration_date_signatures
WHERE expiration_date = ?
WHERE expiration_date = ? AND epoch_id = ?
"#,
expiration_date
expiration_date,
epoch_id
)
.fetch_optional(&*self.connection_pool)
.await
@@ -166,10 +166,11 @@ impl Storage for EphemeralStorage {
async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: u64,
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError> {
Ok(self
.storage_manager
.get_expiration_date_signatures(expiration_date)
.get_expiration_date_signatures(expiration_date, epoch_id)
.await)
}
-1
View File
@@ -60,7 +60,6 @@ pub struct StoredPendingTicketbook {
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
pub struct RawExpirationDateSignatures {
pub epoch_id: u32,
pub serialised_signatures: Vec<u8>,
pub serialization_revision: u8,
}
@@ -325,10 +325,11 @@ impl Storage for PersistentStorage {
async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: u64,
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError> {
let Some(raw) = self
.storage_manager
.get_expiration_date_signatures(expiration_date)
.get_expiration_date_signatures(expiration_date, epoch_id as i64)
.await?
else {
return Ok(None);
+1
View File
@@ -92,6 +92,7 @@ pub trait Storage: Clone + Send + Sync {
async fn get_expiration_date_signatures(
&self,
expiration_date: Date,
epoch_id: u64,
) -> Result<Option<Vec<AnnotatedExpirationDateSignature>>, Self::StorageError>;
async fn insert_expiration_date_signatures(
@@ -14,7 +14,7 @@ use nym_api_requests::ecash::models::{BatchRedeemTicketsBody, VerifyEcashTicketB
use nym_credentials_interface::Bandwidth;
use nym_credentials_interface::{ClientTicket, TicketType};
use nym_validator_client::coconut::EcashApiError;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nym_api::{EpochId, NymApiClientExt};
use nym_validator_client::nyxd::contract_traits::{
EcashSigningClient, MultisigQueryClient, MultisigSigningClient, PagedMultisigQueryClient,
};
@@ -354,7 +354,7 @@ impl CredentialHandler {
Err(err) => {
error!("failed to send ticket {ticket_id} for verification to ecash signer '{client}': {err}. if we don't reach quorum, we'll retry later");
Err(EcashTicketError::ApiFailure(EcashApiError::NymApi {
source: err,
source: nym_validator_client::ValidatorClientError::NymAPIError { source: err },
}))
}
}
@@ -39,7 +39,7 @@ impl traits::EcashManager for EcashManager {
async fn verification_key(
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError> {
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, EcashTicketError> {
self.shared_state.verification_key(epoch_id).await
}
@@ -231,7 +231,7 @@ impl traits::EcashManager for MockEcashManager {
async fn verification_key(
&self,
_epoch_id: EpochId,
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError> {
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, EcashTicketError> {
Ok(self.verfication_key.read().await)
}
@@ -125,7 +125,7 @@ impl SharedState {
async fn set_epoch_data(
&self,
epoch_id: EpochId,
) -> Result<RwLockWriteGuard<BTreeMap<EpochId, EpochState>>, EcashTicketError> {
) -> Result<RwLockWriteGuard<'_, BTreeMap<EpochId, EpochState>>, EcashTicketError> {
let Some(threshold) = self.threshold(epoch_id).await? else {
return Err(EcashTicketError::DKGThresholdUnavailable { epoch_id });
};
@@ -186,7 +186,7 @@ impl SharedState {
pub(crate) async fn api_clients(
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<Vec<EcashApiClient>>, EcashTicketError> {
) -> Result<RwLockReadGuard<'_, Vec<EcashApiClient>>, EcashTicketError> {
let guard = self.epoch_data.read().await;
// the key was already in the map
@@ -212,7 +212,7 @@ impl SharedState {
pub(crate) async fn verification_key(
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError> {
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, EcashTicketError> {
let guard = self.epoch_data.read().await;
// the key was already in the map
@@ -235,11 +235,11 @@ impl SharedState {
}))
}
pub(crate) async fn start_tx(&self) -> RwLockWriteGuard<DirectSigningHttpRpcNyxdClient> {
pub(crate) async fn start_tx(&self) -> RwLockWriteGuard<'_, DirectSigningHttpRpcNyxdClient> {
self.nyxd_client.write().await
}
pub(crate) async fn start_query(&self) -> RwLockReadGuard<DirectSigningHttpRpcNyxdClient> {
pub(crate) async fn start_query(&self) -> RwLockReadGuard<'_, DirectSigningHttpRpcNyxdClient> {
self.nyxd_client.read().await
}
@@ -12,7 +12,7 @@ pub trait EcashManager {
async fn verification_key(
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError>;
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, EcashTicketError>;
fn storage(&self) -> Box<dyn BandwidthGatewayStorage + Send + Sync>;
async fn check_payment(
&self,
+16
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::ecash::traits::EcashManager;
use async_trait::async_trait;
use bandwidth_storage_manager::BandwidthStorageManager;
use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime};
use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType};
@@ -139,3 +140,18 @@ impl CredentialVerifier {
.await)
}
}
#[async_trait]
pub trait TicketVerifier {
/// Verify that the ticket is valid and cryptographically correct.
/// If the verification succeeds, also increase the bandwidth with the ticket's
/// amount and return the latest available bandwidth
async fn verify(&mut self) -> Result<i64>;
}
#[async_trait]
impl TicketVerifier for CredentialVerifier {
async fn verify(&mut self) -> Result<i64> {
self.verify().await
}
}
+1
View File
@@ -15,6 +15,7 @@ bls12_381 = { workspace = true, default-features = false }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
strum = { workspace = true, features = ["derive"] }
strum_macros = { workspace = true }
time = { workspace = true, features = ["serde"] }
utoipa = { workspace = true }
rand = { workspace = true }
+3 -3
View File
@@ -229,9 +229,9 @@ impl From<PayInfo> for NymPayInfo {
Serialize,
Deserialize,
Hash,
strum::Display,
strum::EnumString,
strum::EnumIter,
strum_macros::Display,
strum_macros::EnumString,
strum_macros::EnumIter,
)]
#[serde(rename_all = "kebab-case")]
#[strum(serialize_all = "kebab-case")]
+1
View File
@@ -22,6 +22,7 @@ nym-ecash-time = { path = "../ecash-time", features = ["expiration"] }
nym-credentials-interface = { path = "../credentials-interface" }
nym-crypto = { path = "../crypto" }
nym-api-requests = { path = "../../nym-api/nym-api-requests" }
nym-http-api-client = { path = "../http-api-client" }
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" }
nym-network-defaults = { path = "../network-defaults" }
@@ -15,7 +15,7 @@ use nym_credentials_interface::{
use nym_crypto::asymmetric::ed25519;
use nym_ecash_contract_common::deposit::DepositId;
use nym_ecash_time::{ecash_default_expiration_date, ecash_today, EcashTime};
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nym_api::{EpochId, NymApiClientExt};
use serde::{Deserialize, Serialize};
use time::Date;
@@ -108,7 +108,7 @@ impl IssuanceTicketBook {
signing_request.withdrawal_request.clone(),
self.deposit_id,
request_signature,
signing_request.ecash_pub_key.clone(),
signing_request.ecash_pub_key,
signing_request.expiration_date,
signing_request.ticketbook_type,
)
@@ -116,7 +116,7 @@ impl IssuanceTicketBook {
pub async fn obtain_blinded_credential(
&self,
client: &nym_validator_client::client::NymApiClient,
client: &nym_http_api_client::Client,
request_body: &BlindSignRequestBody,
) -> Result<BlindedSignature, Error> {
let server_response = client.blind_sign(request_body).await?;
@@ -179,7 +179,7 @@ impl IssuanceTicketBook {
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
pub async fn obtain_partial_ticketbook_credential(
&self,
client: &nym_validator_client::client::NymApiClient,
client: &nym_http_api_client::Client,
signer_index: u64,
validator_vk: &VerificationKeyAuth,
signing_data: CredentialSigningData,
+2 -1
View File
@@ -10,6 +10,7 @@ use nym_credentials_interface::{
VerificationKeyAuth, WalletSignatures,
};
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};
@@ -51,7 +52,7 @@ pub async fn obtain_expiration_date_signatures(
for ecash_api_client in ecash_api_clients.iter() {
match ecash_api_client
.api_client
.partial_expiration_date_signatures(None)
.partial_expiration_date_signatures(None, None)
.await
{
Ok(signature) => {
+4 -1
View File
@@ -4,7 +4,7 @@
use crate::ecash::bandwidth::issued::CURRENT_SERIALIZATION_REVISION;
use nym_credentials_interface::CompactEcashError;
use nym_crypto::asymmetric::x25519::KeyRecoveryError;
use nym_validator_client::ValidatorClientError;
use nym_validator_client::{nym_api::error::NymAPIError, ValidatorClientError};
use thiserror::Error;
#[derive(Debug, Error)]
@@ -37,6 +37,9 @@ pub enum Error {
#[error("Ran into a validator client error - {0}")]
ValidatorClientError(#[from] ValidatorClientError),
#[error("Nym API request failed - {0}")]
NymAPIError(#[from] NymAPIError),
#[error("Bandwidth operation overflowed. {0}")]
BandwidthOverflow(String),
+1
View File
@@ -11,6 +11,7 @@ repository = { workspace = true }
aes-gcm-siv = { workspace = true, optional = true }
aes = { workspace = true, optional = true }
aead = { workspace = true, optional = true }
base64.workspace = true
bs58 = { workspace = true }
blake3 = { workspace = true, features = ["traits-preview"], optional = true }
ctr = { workspace = true, optional = true }
@@ -1,6 +1,7 @@
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use base64::Engine;
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use std::fmt::{self, Debug, Display, Formatter};
use std::str::FromStr;
@@ -158,6 +159,15 @@ impl PublicKey {
.map_err(|source| KeyRecoveryError::MalformedPublicKeyString { source })?;
Self::from_bytes(&bytes)
}
pub fn from_base64(s: &str) -> Option<Self> {
let bytes = base64::engine::general_purpose::STANDARD.decode(s).ok()?;
Self::from_bytes(&bytes).ok()
}
pub fn to_base64(&self) -> String {
base64::engine::general_purpose::STANDARD.encode(self.as_bytes())
}
}
impl FromStr for PublicKey {
@@ -0,0 +1,27 @@
[package]
name = "nym-ecash-signer-check-types"
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]
semver = { workspace = true }
serde = { workspace = true, features = ["derive"] }
url = { workspace = true }
thiserror = { workspace = true }
time = { workspace = true }
tracing = { workspace = true }
utoipa = { workspace = true }
nym-coconut-dkg-common = { path = "../cosmwasm-smart-contracts/coconut-dkg" }
nym-crypto = { path = "../crypto", features = ["asymmetric"] }
[lints]
workspace = true
@@ -0,0 +1,97 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_coconut_dkg_common::dealer::DealerDetails;
use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare};
use nym_crypto::asymmetric::ed25519;
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use url::Url;
use utoipa::ToSchema;
#[derive(Debug, Error)]
pub enum MalformedDealer {
#[error("dealer at {dealer_url} has provided invalid ed25519 pubkey: {source}")]
InvalidDealerPubkey {
dealer_url: String,
source: Ed25519RecoveryError,
},
#[error("dealer at {dealer_url} has provided invalid announce url: {source}")]
InvalidDealerAddress {
dealer_url: String,
source: url::ParseError,
},
}
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub struct RawDealerInformation {
pub announce_address: String,
pub owner_address: String,
pub node_index: u64,
pub public_key: String,
pub verification_key_share: Option<VerificationKeyShare>,
pub share_verified: bool,
}
impl RawDealerInformation {
pub fn new(
dealer_details: &DealerDetails,
contract_share: Option<&ContractVKShare>,
) -> RawDealerInformation {
RawDealerInformation {
announce_address: dealer_details.announce_address.clone(),
owner_address: dealer_details.address.to_string(),
node_index: dealer_details.assigned_index,
public_key: dealer_details.ed25519_identity.clone(),
verification_key_share: contract_share.map(|s| s.share.clone()),
share_verified: contract_share.map(|s| s.verified).unwrap_or(false),
}
}
pub fn parse(&self) -> Result<DealerInformation, MalformedDealer> {
Ok(DealerInformation {
announce_address: self.announce_address.parse().map_err(|source| {
MalformedDealer::InvalidDealerAddress {
dealer_url: self.announce_address.clone(),
source,
}
})?,
owner_address: self.owner_address.clone(),
node_index: self.node_index,
public_key: self.public_key.parse().map_err(|source| {
MalformedDealer::InvalidDealerPubkey {
dealer_url: self.announce_address.clone(),
source,
}
})?,
verification_key_share: self.verification_key_share.clone(),
share_verified: self.share_verified,
})
}
}
#[derive(Debug)]
pub struct DealerInformation {
pub announce_address: Url,
pub owner_address: String,
pub node_index: u64,
pub public_key: ed25519::PublicKey,
// no need to parse it into the full type as it doesn't get us anything
pub verification_key_share: Option<VerificationKeyShare>,
pub share_verified: bool,
}
impl From<DealerInformation> for RawDealerInformation {
fn from(d: DealerInformation) -> Self {
RawDealerInformation {
announce_address: d.announce_address.to_string(),
owner_address: d.owner_address,
node_index: d.node_index,
public_key: d.public_key.to_base58_string(),
verification_key_share: d.verification_key_share,
share_verified: d.share_verified,
}
}
}
@@ -0,0 +1,127 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_coconut_dkg_common::types::EpochId;
use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
use nym_crypto::asymmetric::ed25519;
use std::time::Duration;
use time::OffsetDateTime;
use tracing::{debug, warn};
pub trait Verifiable {
fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool;
}
pub trait TimestampedResponse {
fn timestamp(&self) -> OffsetDateTime;
}
pub trait LegacyChainResponse {
fn chain_synced(&self, now: OffsetDateTime, stall_threshold: Duration) -> bool;
}
pub trait ChainResponse: Verifiable + TimestampedResponse {
fn chain_synced(&self) -> bool;
fn chain_available(
&self,
pub_key: &ed25519::PublicKey,
now: OffsetDateTime,
stale_response_threshold: Duration,
) -> bool {
if !self.verify_signature(pub_key) {
warn!("failed signature verification on chain status response");
return false;
}
// we rely on information provided from the api itself AS LONG AS it's not too outdated
if self.timestamp() + stale_response_threshold < now {
return false;
}
self.chain_synced()
}
}
pub trait LegacySignerResponse {
fn signer_identity(&self) -> &str;
fn signer_verification_key(&self) -> &Option<String>;
fn unprovable_signing_available(
&self,
pub_key: &ed25519::PublicKey,
expected_verification_key: Option<VerificationKeyShare>,
share_verified: bool,
) -> bool {
if self.signer_identity() != pub_key.to_base58_string() {
warn!("mismatched identity key on the legacy response");
return false;
}
// the contract share hasn't been verified yet, so we're probably in the middle of DKG
// thus if there's a bit of desync in the state, it's fine
if !share_verified {
return true;
}
if self.signer_verification_key() != &expected_verification_key {
warn!("mismatched [ecash] verification key on the legacy response");
return false;
}
true
}
}
pub trait SignerResponse: Verifiable + TimestampedResponse {
fn has_signing_keys(&self) -> bool;
fn signer_disabled(&self) -> bool;
fn is_ecash_signer(&self) -> bool;
fn dkg_ecash_epoch_id(&self) -> EpochId;
fn provable_signing_available(
&self,
pub_key: &ed25519::PublicKey,
dkg_epoch_id: EpochId,
now: OffsetDateTime,
stale_response_threshold: Duration,
) -> bool {
if !self.verify_signature(pub_key) {
warn!("failed signature verification on chain status response");
return false;
}
// we rely on information provided from the api itself AS LONG AS it's not too outdated
if self.timestamp() + stale_response_threshold < now {
return false;
}
if !self.has_signing_keys() {
debug!("missing signing keys");
return false;
}
if self.signer_disabled() {
debug!("signer functionalities explicitly disabled");
return false;
}
if !self.is_ecash_signer() {
debug!("signer doesn't recognise it's a signer for this epoch");
return false;
}
if dkg_epoch_id != self.dkg_ecash_epoch_id() {
debug!(
"mismatched dkg epoch id. current: {dkg_epoch_id}, signer's: {}",
self.dkg_ecash_epoch_id()
);
return false;
}
true
}
}
@@ -0,0 +1,6 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod dealer_information;
pub mod helper_traits;
pub mod status;
@@ -0,0 +1,303 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::dealer_information::RawDealerInformation;
use crate::helper_traits::{
ChainResponse, LegacyChainResponse, LegacySignerResponse, SignerResponse,
};
use nym_coconut_dkg_common::types::EpochId;
use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
use nym_crypto::asymmetric::ed25519;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::OffsetDateTime;
use utoipa::ToSchema;
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
// the reason for generics is not to remove duplication of code,
// but because without them, we'd be having problems with circular dependencies,
// i.e. nym-api-requests depending on ecash-signer-check-types and
// ecash-signer-check-types needing nym-api-requests
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub enum Status<L, T> {
/// The API, even though it reports correct version, did not response to the status query
Unreachable,
/// The API is running an outdated version that does not expose the required endpoint
Outdated,
/// Response to the legacy (unsigned) status query
ReachableLegacy { response: Box<L> },
/// Response to the current (signed) status query
Reachable { response: Box<T> },
}
impl<L, T> Status<L, T>
where
L: LegacyChainResponse,
T: ChainResponse,
{
pub fn chain_available(&self, pub_key: ed25519::PublicKey) -> bool {
let now = OffsetDateTime::now_utc();
match self {
Status::Unreachable | Status::Outdated => false,
Status::ReachableLegacy { response } => {
response.chain_synced(now, CHAIN_STALL_THRESHOLD)
}
Status::Reachable { response } => {
response.chain_available(&pub_key, now, STALE_RESPONSE_THRESHOLD)
}
}
}
pub fn chain_provably_stalled(&self, pub_key: ed25519::PublicKey) -> bool {
let now = OffsetDateTime::now_utc();
match self {
Status::Unreachable | Status::Outdated | Status::ReachableLegacy { .. } => false,
Status::Reachable { response } => {
!response.chain_available(&pub_key, now, STALE_RESPONSE_THRESHOLD)
}
}
}
pub fn chain_unprovably_stalled(&self) -> bool {
let now = OffsetDateTime::now_utc();
match self {
Status::Unreachable | Status::Outdated | Status::Reachable { .. } => false,
Status::ReachableLegacy { response } => {
!response.chain_synced(now, CHAIN_STALL_THRESHOLD)
}
}
}
}
impl<L, T> Status<L, T>
where
L: LegacySignerResponse,
T: SignerResponse,
{
pub fn signing_available(
&self,
pub_key: ed25519::PublicKey,
dkg_epoch_id: u64,
expected_verification_key: Option<VerificationKeyShare>,
share_verified: bool,
) -> bool {
let now = OffsetDateTime::now_utc();
match self {
Status::Unreachable | Status::Outdated => false,
Status::ReachableLegacy { response } => response.unprovable_signing_available(
&pub_key,
expected_verification_key,
share_verified,
),
Status::Reachable { response } => response.provable_signing_available(
&pub_key,
dkg_epoch_id,
now,
STALE_RESPONSE_THRESHOLD,
),
}
}
pub fn signing_provably_unavailable(
&self,
pub_key: ed25519::PublicKey,
dkg_epoch_id: EpochId,
) -> bool {
let now = OffsetDateTime::now_utc();
match self {
Status::Unreachable | Status::Outdated | Status::ReachableLegacy { .. } => false,
Status::Reachable { response } => !response.provable_signing_available(
&pub_key,
dkg_epoch_id,
now,
STALE_RESPONSE_THRESHOLD,
),
}
}
pub fn signing_unprovably_unavailable(
&self,
pub_key: ed25519::PublicKey,
expected_verification_key: Option<VerificationKeyShare>,
share_verified: bool,
) -> bool {
match self {
Status::Unreachable | Status::Outdated | Status::Reachable { .. } => false,
Status::ReachableLegacy { response } => !response.unprovable_signing_available(
&pub_key,
expected_verification_key,
share_verified,
),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub struct SignerResult<LS, TS, LC, TC> {
pub dkg_epoch_id: u64,
pub information: RawDealerInformation,
pub status: SignerStatus<LS, TS, LC, TC>,
}
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC> {
pub fn signer_unreachable(&self) -> bool {
matches!(self.status, SignerStatus::Unreachable)
}
pub fn malformed_details(&self) -> bool {
self.information.parse().is_err()
}
}
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC>
where
LC: LegacyChainResponse,
TC: ChainResponse,
{
pub fn unknown_chain_status(&self) -> bool {
let Ok(_) = self.information.parse() else {
return true;
};
if let SignerStatus::Tested { .. } = &self.status {
return false;
}
true
}
pub fn chain_available(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result
.local_chain_status
.chain_available(parsed_info.public_key)
}
pub fn chain_provably_stalled(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result
.local_chain_status
.chain_provably_stalled(parsed_info.public_key)
}
pub fn chain_unprovably_stalled(&self) -> bool {
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result.local_chain_status.chain_unprovably_stalled()
}
}
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC>
where
LS: LegacySignerResponse,
TS: SignerResponse,
{
pub fn unknown_signing_status(&self) -> bool {
let Ok(_) = self.information.parse() else {
return true;
};
if let SignerStatus::Tested { .. } = &self.status {
return false;
}
true
}
pub fn signing_available(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result.signing_status.signing_available(
parsed_info.public_key,
self.dkg_epoch_id,
parsed_info.verification_key_share,
parsed_info.share_verified,
)
}
pub fn signing_provably_unavailable(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result
.signing_status
.signing_provably_unavailable(parsed_info.public_key, self.dkg_epoch_id)
}
pub fn signing_unprovably_unavailable(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result.signing_status.signing_unprovably_unavailable(
parsed_info.public_key,
parsed_info.verification_key_share,
parsed_info.share_verified,
)
}
}
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub enum SignerStatus<LS, TS, LC, TC> {
Unreachable,
ProvidedInvalidDetails,
Tested {
result: SignerTestResult<LS, TS, LC, TC>,
},
}
impl<LS, TS, LC, TC> SignerStatus<LS, TS, LC, TC> {
pub fn with_details(
self,
information: impl Into<RawDealerInformation>,
dkg_epoch_id: u64,
) -> SignerResult<LS, TS, LC, TC> {
SignerResult {
dkg_epoch_id,
status: self,
information: information.into(),
}
}
}
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
pub struct SignerTestResult<LS, TS, LC, TC> {
pub reported_version: String,
pub signing_status: Status<LS, TS>,
pub local_chain_status: Status<LC, TC>,
}
+28
View File
@@ -0,0 +1,28 @@
[package]
name = "nym-ecash-signer-check"
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]
futures = { workspace = true }
thiserror = { workspace = true }
semver = { workspace = true }
tokio = { workspace = true, features = ["time"] }
tracing = { workspace = true }
url = { workspace = true }
nym-validator-client = { path = "../client-libs/validator-client" }
nym-network-defaults = { path = "../network-defaults" }
nym-ecash-signer-check-types = { path = "../ecash-signer-check-types" }
nym-http-api-client = { path = "../http-api-client" }
[lints]
workspace = true
@@ -0,0 +1,231 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{LocalChainStatus, SignerCheckError, SigningStatus, TypedSignerResult};
use nym_ecash_signer_check_types::dealer_information::RawDealerInformation;
use nym_ecash_signer_check_types::status::{SignerStatus, SignerTestResult};
use nym_validator_client::models::BinaryBuildInformationOwned;
use nym_validator_client::nym_api::NymApiClientExt;
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
ContractVKShare, DealerDetails,
};
use std::time::Duration;
use tracing::{error, warn};
use url::Url;
pub(crate) mod chain_status {
// Dorina
pub(crate) const MINIMUM_VERSION_LEGACY: semver::Version = semver::Version::new(1, 1, 51);
// Gruyere
pub(crate) const MINIMUM_VERSION: semver::Version = semver::Version::new(1, 1, 64);
}
pub(crate) mod signing_status {
// Magura (possibly earlier)
pub(crate) const MINIMUM_LEGACY_VERSION: semver::Version = semver::Version::new(1, 1, 46);
// Gruyere
pub(crate) const MINIMUM_VERSION: semver::Version = semver::Version::new(1, 1, 64);
}
struct ClientUnderTest {
api_client: nym_http_api_client::Client,
build_info: Option<BinaryBuildInformationOwned>,
}
impl ClientUnderTest {
pub(crate) fn new(api_url: &Url) -> Result<Self, SignerCheckError> {
// The builder should not fail with a valid URL that's already parsed
// If it does fail, it's an internal error that we can't recover from
let api_client = nym_http_api_client::Client::builder(api_url.clone())?.build()?;
Ok(ClientUnderTest {
api_client,
build_info: None,
})
}
pub(crate) async fn try_retrieve_build_information(&mut self) -> bool {
match tokio::time::timeout(Duration::from_secs(5), self.api_client.build_information())
.await
{
Ok(Ok(build_information)) => {
self.build_info = Some(build_information);
true
}
Ok(Err(err)) => {
warn!("{}: failed to retrieve build information: {err}. the signer is most likely down", self.api_client.current_url());
false
}
Err(_timeout) => {
warn!(
"{}: timed out while attempting to retrieve build information",
self.api_client.current_url()
);
false
}
}
}
pub(crate) fn version(&self) -> Option<semver::Version> {
self.build_info.as_ref().and_then(|build_info| {
build_info
.build_version
.parse()
.inspect_err(|err| {
error!(
"ecash signer '{}' reports invalid version {}: {err}",
self.api_client.current_url(),
build_info.build_version
)
})
.ok()
})
}
pub(crate) fn supports_legacy_signing_status_query(&self) -> bool {
let Some(version) = self.version() else {
return false;
};
version >= signing_status::MINIMUM_LEGACY_VERSION
}
pub(crate) fn supports_signing_status_query(&self) -> bool {
let Some(version) = self.version() else {
return false;
};
version >= signing_status::MINIMUM_VERSION
}
pub(crate) fn supports_chain_status_query(&self) -> bool {
let Some(version) = self.version() else {
return false;
};
version >= chain_status::MINIMUM_VERSION
}
pub(crate) fn supports_legacy_chain_status_query(&self) -> bool {
let Some(version) = self.version() else {
return false;
};
version >= chain_status::MINIMUM_VERSION_LEGACY
}
pub(crate) async fn check_local_chain(&self) -> LocalChainStatus {
// check if it at least supports legacy query
if !self.supports_legacy_chain_status_query() {
return LocalChainStatus::Outdated;
}
// check if it supports the current query
if self.supports_chain_status_query() {
return match self.api_client.get_chain_blocks_status().await {
Ok(status) => LocalChainStatus::Reachable {
response: Box::new(status),
},
Err(err) => {
warn!(
"{}: failed to retrieve local chain status: {err}",
self.api_client.current_url()
);
LocalChainStatus::Unreachable
}
};
}
// fallback to the legacy query
match self.api_client.get_chain_status().await {
Ok(status) => LocalChainStatus::ReachableLegacy {
response: Box::new(status),
},
Err(err) => {
warn!(
"{}: failed to retrieve [legacy] local chain status: {err}",
self.api_client.current_url()
);
LocalChainStatus::Unreachable
}
}
}
pub(crate) async fn check_signing_status(&self) -> SigningStatus {
// check if it at least supports legacy query
if !self.supports_legacy_signing_status_query() {
return SigningStatus::Outdated;
}
// check if it supports the current query
if self.supports_signing_status_query() {
return match self.api_client.get_signer_status().await {
Ok(response) => SigningStatus::Reachable {
response: Box::new(response),
},
Err(err) => {
warn!(
"{}: failed to retrieve signer chain status: {err}",
self.api_client.current_url()
);
SigningStatus::Unreachable
}
};
}
// fallback to the legacy query
match self.api_client.get_signer_information().await {
Ok(status) => SigningStatus::ReachableLegacy {
response: Box::new(status),
},
Err(err) => {
warn!(
"{}: failed to retrieve [legacy] signer chain status: {err}",
self.api_client.current_url()
);
// NOTE: this might equally mean the signing is disabled
SigningStatus::Unreachable
}
}
}
}
pub(crate) async fn check_client(
dealer_details: DealerDetails,
dkg_epoch: u64,
contract_share: Option<&ContractVKShare>,
) -> TypedSignerResult {
let dealer_information = RawDealerInformation::new(&dealer_details, contract_share);
// 7. attempt to construct client instances out of them
let Ok(parsed_information) = dealer_information.parse() else {
return SignerStatus::ProvidedInvalidDetails.with_details(dealer_information, dkg_epoch);
};
let mut client = match ClientUnderTest::new(&parsed_information.announce_address) {
Ok(client) => client,
Err(err) => {
error!("failed to create client instance: {err}");
return SignerStatus::Unreachable.with_details(dealer_information, dkg_epoch);
}
};
// 8. check basic connection status - can you retrieve build information?
if !client.try_retrieve_build_information().await {
return SignerStatus::Unreachable.with_details(dealer_information, dkg_epoch);
}
// 9. check perceived chain status
let local_chain_status = client.check_local_chain().await;
// 10. check signer status
let signing_status = client.check_signing_status().await;
SignerStatus::Tested {
result: SignerTestResult {
reported_version: client.version().map(|v| v.to_string()).unwrap_or_default(),
signing_status,
local_chain_status,
},
}
.with_details(dealer_information, dkg_epoch)
}
@@ -0,0 +1,80 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::SignerCheckError;
use nym_crypto::asymmetric::ed25519;
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
ContractVKShare, DealerDetails, VerificationKeyShare,
};
use url::Url;
#[derive(Debug)]
pub struct RawDealerInformation {
pub announce_address: String,
pub owner_address: String,
pub node_index: u64,
pub public_key: String,
pub verification_key_share: Option<VerificationKeyShare>,
pub share_verified: bool,
}
impl RawDealerInformation {
pub fn new(
dealer_details: &DealerDetails,
contract_share: Option<&ContractVKShare>,
) -> RawDealerInformation {
RawDealerInformation {
announce_address: dealer_details.announce_address.clone(),
owner_address: dealer_details.address.to_string(),
node_index: dealer_details.assigned_index,
public_key: dealer_details.ed25519_identity.clone(),
verification_key_share: contract_share.map(|s| s.share.clone()),
share_verified: contract_share.map(|s| s.verified).unwrap_or(false),
}
}
pub fn parse(&self) -> Result<DealerInformation, SignerCheckError> {
Ok(DealerInformation {
announce_address: self.announce_address.parse().map_err(|source| {
SignerCheckError::InvalidDealerAddress {
dealer_url: self.announce_address.clone(),
source,
}
})?,
owner_address: self.owner_address.clone(),
node_index: self.node_index,
public_key: self.announce_address.parse().map_err(|source| {
SignerCheckError::InvalidDealerPubkey {
dealer_url: self.announce_address.clone(),
source,
}
})?,
verification_key_share: self.verification_key_share.clone(),
share_verified: self.share_verified,
})
}
}
#[derive(Debug)]
pub struct DealerInformation {
pub announce_address: Url,
pub owner_address: String,
pub node_index: u64,
pub public_key: ed25519::PublicKey,
// no need to parse it into the full type as it doesn't get us anything
pub verification_key_share: Option<VerificationKeyShare>,
pub share_verified: bool,
}
impl From<DealerInformation> for RawDealerInformation {
fn from(d: DealerInformation) -> Self {
RawDealerInformation {
announce_address: d.announce_address.to_string(),
owner_address: d.owner_address,
node_index: d.node_index,
public_key: d.public_key.to_base58_string(),
verification_key_share: d.verification_key_share,
share_verified: d.share_verified,
}
}
}
+31
View File
@@ -0,0 +1,31 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_http_api_client::HttpClientError;
use nym_validator_client::nyxd::error::NyxdError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SignerCheckError {
#[error("failed to connect to nyxd chain due to invalid connection details: {source}")]
InvalidNyxdConnectionDetails { source: NyxdError },
#[error("failed to query the DKG contract: {source}")]
DKGContractQueryFailure { source: NyxdError },
#[error("failed to build client: {source}")]
HttpClient {
#[from]
source: HttpClientError,
},
}
impl SignerCheckError {
pub fn invalid_nyxd_connection_details(source: NyxdError) -> Self {
Self::InvalidNyxdConnectionDetails { source }
}
pub fn dkg_contract_query_failure(source: NyxdError) -> Self {
Self::DKGContractQueryFailure { source }
}
}
+127
View File
@@ -0,0 +1,127 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client_check::check_client;
use futures::stream::{FuturesUnordered, StreamExt};
use nym_network_defaults::NymNetworkDetails;
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
use nym_validator_client::QueryHttpRpcNyxdClient;
use std::collections::HashMap;
use url::Url;
pub use error::SignerCheckError;
use nym_ecash_signer_check_types::status::{SignerResult, Status};
use nym_validator_client::ecash::models::EcashSignerStatusResponse;
use nym_validator_client::models::{
ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse,
};
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
ContractVKShare, DealerDetails, Epoch,
};
mod client_check;
pub mod error;
pub type TypedSignerResult = SignerResult<
SignerInformationResponse,
EcashSignerStatusResponse,
ChainStatusResponse,
ChainBlocksStatusResponse,
>;
pub type LocalChainStatus = Status<ChainStatusResponse, ChainBlocksStatusResponse>;
pub type SigningStatus = Status<SignerInformationResponse, EcashSignerStatusResponse>;
pub struct SignersTestResult {
pub threshold: Option<u64>,
pub results: Vec<TypedSignerResult>,
}
pub async fn check_signers(
rpc_endpoint: Url,
// details such as denoms, prefixes, etc.
network_details: NymNetworkDetails,
) -> Result<SignersTestResult, SignerCheckError> {
// 1. create nyx client instance
let client = QueryHttpRpcNyxdClient::connect_with_network_details(
rpc_endpoint.as_str(),
network_details,
)
.map_err(SignerCheckError::invalid_nyxd_connection_details)?;
check_signers_with_client(&client).await
}
pub struct DkgDetails {
pub dkg_epoch: Epoch,
pub threshold: Option<u64>,
pub network_dealers: Vec<DealerDetails>,
pub submitted_shared: HashMap<u64, ContractVKShare>,
}
pub async fn check_signers_with_client<C>(client: &C) -> Result<SignersTestResult, SignerCheckError>
where
C: DkgQueryClient + Sync,
{
let dkg_details = dkg_details_with_client(client).await?;
check_known_dealers(dkg_details).await
}
pub async fn dkg_details_with_client<C>(client: &C) -> Result<DkgDetails, SignerCheckError>
where
C: DkgQueryClient + Sync,
{
// 2. retrieve current dkg epoch
let dkg_epoch = client
.get_current_epoch()
.await
.map_err(SignerCheckError::dkg_contract_query_failure)?;
// 3. retrieve the dkg threshold as reference point
let threshold = client
.get_epoch_threshold(dkg_epoch.epoch_id)
.await
.map_err(SignerCheckError::dkg_contract_query_failure)?;
// 4. retrieve information on current DKG dealers (i.e. eligible signers)
let dealers = client
.get_all_current_dealers()
.await
.map_err(SignerCheckError::dkg_contract_query_failure)?;
// 5. retrieve their published keys (if available)
let shares: HashMap<_, _> = client
.get_all_verification_key_shares(dkg_epoch.epoch_id)
.await
.map_err(SignerCheckError::dkg_contract_query_failure)?
.into_iter()
.map(|share| (share.node_index, share))
.collect();
Ok(DkgDetails {
dkg_epoch,
threshold,
network_dealers: dealers,
submitted_shared: shares,
})
}
pub async fn check_known_dealers(
dkg_details: DkgDetails,
) -> Result<SignersTestResult, SignerCheckError> {
// 6. for each dealer attempt to perform the checks
let results = dkg_details
.network_dealers
.into_iter()
.map(|d| {
let share = dkg_details.submitted_shared.get(&d.assigned_index);
check_client(d, dkg_details.dkg_epoch.epoch_id, share)
})
.collect::<FuturesUnordered<_>>()
.collect::<Vec<_>>()
.await;
Ok(SignersTestResult {
threshold: dkg_details.threshold,
results,
})
}
+73
View File
@@ -0,0 +1,73 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::chain_status::LocalChainStatus;
use crate::dealer_information::RawDealerInformation;
use crate::signing_status::SigningStatus;
use std::time::Duration;
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
#[derive(Debug)]
pub struct SignerResult {
pub dkg_epoch_id: u64,
pub information: RawDealerInformation,
pub status: SignerStatus,
}
impl SignerResult {
pub fn chain_available(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result.local_chain_status.available(parsed_info.public_key)
}
pub fn signer_available(&self) -> bool {
let Ok(parsed_info) = self.information.parse() else {
return false;
};
let SignerStatus::Tested { result } = &self.status else {
return false;
};
result.signing_status.available(
parsed_info.public_key,
self.dkg_epoch_id,
parsed_info.verification_key_share,
parsed_info.share_verified,
)
}
}
#[derive(Debug)]
pub enum SignerStatus {
Unreachable,
ProvidedInvalidDetails,
Tested { result: SignerTestResult },
}
impl SignerStatus {
pub fn with_details(
self,
information: impl Into<RawDealerInformation>,
dkg_epoch_id: u64,
) -> SignerResult {
SignerResult {
dkg_epoch_id,
status: self,
information: information.into(),
}
}
}
#[derive(Debug)]
pub struct SignerTestResult {
pub reported_version: String,
pub signing_status: SigningStatus,
pub local_chain_status: LocalChainStatus,
}
+3
View File
@@ -47,4 +47,7 @@ workspace = true
default-features = false
[dev-dependencies]
anyhow = { workspace = true }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" } # we need specific imports in tests
nym-test-utils = { path = "../test-utils" }
tokio = { workspace = true, features = ["full"] }
+1 -1
View File
@@ -89,7 +89,7 @@ mod tests {
.unwrap();
let blind_sig = issue(
keypair.secret_key(),
sig_req.ecash_pub_key.clone(),
sig_req.ecash_pub_key,
&sig_req.withdrawal_request,
expiration_date.ecash_unix_timestamp(),
issuance.ticketbook_type().encode(),
@@ -109,3 +109,85 @@ GATEWAY -> CLIENT
DONE(status)
*/
#[cfg(test)]
mod tests {
use super::*;
use crate::ClientControlRequest;
use futures::StreamExt;
use nym_test_utils::helpers::u64_seeded_rng;
use nym_test_utils::mocks::stream_sink::mock_streams;
use nym_test_utils::traits::{Leak, Timeboxed, TimeboxedSpawnable};
use tokio::join;
use tungstenite::Message;
#[tokio::test]
async fn basic_handshake() -> anyhow::Result<()> {
use anyhow::Context as _;
// solve the lifetime issue by just leaking the contents of the boxes
// which is perfectly fine in test
let client_rng = u64_seeded_rng(42).leak();
let gateway_rng = u64_seeded_rng(69).leak();
let client_keys = ed25519::KeyPair::new(client_rng).leak();
let gateway_keys = ed25519::KeyPair::new(gateway_rng).leak();
let (client_ws, gateway_ws) = mock_streams::<Message>();
// we need streams that return Result<Message, WsError>
let client_ws = client_ws.map(Ok);
let gateway_ws = gateway_ws.map(Ok);
let client_ws = client_ws.leak();
let gateway_ws = gateway_ws.leak();
let handshake_client = client_handshake(
client_rng,
client_ws,
client_keys,
*gateway_keys.public_key(),
false,
true,
TaskClient::dummy(),
);
let client_fut = handshake_client.spawn_timeboxed();
// we need to receive the first message so that it could be propagated to the gateway side of the handshake
let ClientControlRequest::RegisterHandshakeInitRequest {
protocol_version: _,
data,
} = (gateway_ws.next())
.timeboxed()
.await
.context("timeout")?
.context("no message!")??
.into_text()?
.parse::<ClientControlRequest>()?
else {
panic!("bad message")
};
let init_msg = data;
let handshake_gateway = gateway_handshake(
gateway_rng,
gateway_ws,
gateway_keys,
init_msg,
TaskClient::dummy(),
);
let gateway_fut = handshake_gateway.spawn_timeboxed();
let (client, gateway) = join!(client_fut, gateway_fut);
let client_key = client???;
let gateway_key = gateway???;
// ensure the created keys are the same
assert_eq!(client_key, gateway_key);
Ok(())
}
}
+2
View File
@@ -7,6 +7,7 @@ homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[dependencies]
sqlx = { workspace = true, features = [
@@ -27,6 +28,7 @@ nym-statistics-common = { path = "../statistics" }
[build-dependencies]
anyhow = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
+13 -4
View File
@@ -1,22 +1,29 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use anyhow::Context;
use sqlx::{Connection, SqliteConnection};
use std::env;
#[tokio::main]
async fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
async fn main() -> anyhow::Result<()> {
let out_dir = env::var("OUT_DIR")?;
let database_path = format!("{out_dir}/gateway-stats-example.sqlite");
// remove the db file if it already existed from previous build
// in case it was from a different branch
if std::fs::exists(&database_path)? {
std::fs::remove_file(&database_path)?;
}
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
.await
.expect("Failed to create SQLx database connection");
.context("Failed to create SQLx database connection")?;
sqlx::migrate!("./migrations")
.run(&mut conn)
.await
.expect("Failed to perform SQLx migrations");
.context("Failed to perform SQLx migrations")?;
#[cfg(target_family = "unix")]
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
@@ -25,4 +32,6 @@ async fn main() {
// for some strange reason we need to add a leading `/` to the windows path even though it's
// not a valid windows path... but hey, it works...
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
Ok(())
}

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