Compare commits

...

111 Commits

Author SHA1 Message Date
nikss31 2c717c0ebd Created an RTT Tester Client 2026-01-07 22:49:06 +00:00
Jędrzej Stuczyński d73b7b7127 chore: remove support for legacy mixnode within the performance contract (#6205) 2025-11-14 15:04:59 +00:00
Jędrzej Stuczyński 440aadf124 chore: updated default endpoint for retrieving attestation.json (#6207) 2025-11-14 15:04:51 +00:00
Jędrzej Stuczyński d126d8e5a0 feat: upgrade mode: VPN adjustments (#6189)
* placeholder handling of wg registration with upgrade mode token

* include upgrade mode credentials as part of credential storage

* introduce helper for decoding JWT payload

* expose methods for removing emergency credentials from the storage

* don't allow duplicate emergency credentials with the same content

* added authenticator ClientMessage for upgrade mode check

* retrieve credentials with longest expiration first

* post rebasing fixes

* fixed gateway config

* feat: allow specifying minimum node performance for client init

* nym-node UM improvements

* fixed upgrade mode bandwidth on initial authentication

* fix: logs and thresholds

* expose attestation information from nym-node http api

* additional logs

* post rebasing fixes

* make @simonwicky happy by removing empty lines in emergency_credential table definition

* chore: remove '_' prefix for internal counters within in-mem ecash storage

* improved import of 'UpgradeModeState' within the nym-node

* use explicit time dependency within credential-storage

* re-order imports within the gateway-client

* moved 'AvailableBandwidth' definition to the monorepo
2025-11-14 13:34:36 +00:00
Jędrzej Stuczyński 6b2bb3029b feat: merge intermediate upgrade mode changes (#6174)
* squashing feat: merge intermediate upgrade mode changes #6174 to more easily resolve merge conflicts during rebasing

added additional v2 query for metadata endpoint for requesting upgrade mode recheck

added additional message to v6 authenticator to request explicit upgrade mode recheck

clippy

test fixes due to updated keys

updated assertion for upgrading v1 top up request to v2

compare attester public key against the expected value within the credential proxy

use pre-generated attestation public keys within nym-nodes

remove version deprecation

bugfix: default bandwidth response for authenticator

expose upgrade mode information in authenticator responses

adding tests for new v2 server

passing upgrade mode information in metadata endpoint

v2 wireguard private metadata

bugfix: make sure to immediately poll for attestation after spawning task

fix gateway probe and remove code duplication for finalizing registration

squashing before rebasing

post rebasing fixes

AuthenticatorVersion helpers

additional nits

allow unwraps in mocks

fixed linux build

clippy

integrating upgrade mode into authenticator

fixed build after adding wrappers to response types

conditionally updating peer handle bandwidth

cleanup

negotiate initial protocol during registration

change auth to use highest protocol

handler for JWT message

dont meter client bandwidth in upgrade mode

handling recheck requests

sending information about upgrade_mode on client messages

gateway watching for upgrade mode attestation

wip: gateways to disable bandwidth metering on upgrade mode

* fixed ServerResponse deserialisation

* fixed incorrect swagger path for upgrade mode check endpoint

* moved upgrade mode endpoint out of bandwidth routes

* chore: remove unused error variant

* removed re-export of UpgradeModeAttestation from credentials-interface

* chore: define single source of truth for minimum bandwidth threshold value

* moved type definitions out of traits.rs

* updated v6 versioning to point to niolo release instead

* fixed incorrect error mapping
2025-11-14 13:13:15 +00:00
benedetta davico 4dcc568ec2 Merge pull request #6204 from nymtech/master
merging master to develop to maintain sync
2025-11-14 11:21:13 +01:00
benedetta davico 468835e3a2 Merge pull request #6199 from nymtech/release/2025.20-leerdammer
Release/2025.20 leerdammer
2025-11-14 10:36:23 +01:00
benedetta davico 28a866e26d Merge pull request #6198 from nymtech/release/2025.20-leerdammer
Release/2025.20 leerdammer
2025-11-14 10:36:11 +01:00
Jędrzej Stuczyński 350d244032 bugfix: fix credential proxy upgrade mode attestation url arg (#6202)
this includes bringing over changes introduced in #6174
2025-11-14 08:19:21 +00:00
Jack Wampler 17ca000782 HTTP API resilience enable & domain rotation conditions (#6200)
* http url fallback conditions

* include changes and tests for fronted

* Allow for explicit DNS error Handling in HTTP client  (#6201)

when sending http reqs add manual DNS so we can handle errors directly

* Address PR nits

---------

Co-authored-by: durch <durch@users.noreply.github.com>
2025-11-14 08:59:36 +01:00
Drazen Urch aac983d922 Remove debug feature from http-macro spec in gateway probe (#6195) 2025-11-13 14:18:29 +01:00
mfahampshire 577675bab3 Remove old conceptsoverview page + move index to proper place in sidebar (#6196) 2025-11-13 11:38:54 +00:00
mfahampshire ec015618cd update gw probe to point @ monorepo (#6194)
* update gw probe to point @ monorepo

* add funded nyx account info
2025-11-13 11:02:45 +00:00
mfahampshire fa40acbeca fixed broken link (#6193) 2025-11-12 15:12:38 +00:00
import this 386e1790dd [DOCs/operators]: Release notes for v2025.20 leerdammer (#6191)
* release notes

* bump up nym-node docs version

* add dev tools

* scrape stats and clean
2025-11-12 12:32:13 +00:00
mfahampshire d07f9c8fad Max/docs new structure (#6188)
* rework of sdk docs

* update integration docs + bit of overall restructure

* remove debug logger from tool
2025-11-12 11:03:28 +00:00
Tommy Verrall 0dc071daeb Merge pull request #6179 from nymtech/dns-debug
DNS relibility and troubleshooting
2025-11-12 11:01:20 +01:00
benedettadavico babf113fe5 update changelog 2025-11-12 08:39:48 +01:00
jmwample 10951d4cd3 clippy, fmt, minor fix 2025-11-11 10:40:25 -07:00
mfahampshire 872c25bfcc Use hardcoded devrel gw for examples to get around CSP (#6187)
* Use hardcoded devrel gw for examples to get around CSP

* remove comment
2025-11-11 16:22:41 +00:00
jmwample 5acce42c64 add some staic hosts and switch server strategy 2025-11-11 09:14:26 -07:00
mfahampshire 4848d081d0 Max/tweak ts sdk actions (#6185)
* add taskset to wasm release build commands

* bump taskset cores
2025-11-11 10:19:55 +00:00
mfahampshire b3452ede77 add wss to prod csp (#6183)
* add wss to csp
2025-11-10 20:48:02 +00:00
import this a44819b14c [DOCs/operators]: Cleanup (#6184)
* cleanup

* add ipr abbrs

* syntax fix

* syntax fix

* fix link path

* QUIC non-root warning

* syntax fix

* update stats

* address comment

* fix url path
2025-11-10 14:20:15 +00:00
mfahampshire 5455110810 removed warning from TSSDK (#6182)
* removed warning from TSSDK

* tweak
2025-11-10 12:21:20 +00:00
Mark Sinclair a0178d28f7 Typescript SDK 1.4.1 (#6146)
* wasm: mix-fetch: remove harbour master client and use Nym API client

* wasm: mix-fetch: fix up internal tester

* Release Typescript SDK v1.4.1

* remove bump version tool from workspace

* ts-sdk: contract clients: update and re-run autogen

* ts: fix linting errors

* update go

* pin minimatch typings to fix lint errors

* bump versions to rc

* Update publish-sdk-npm.yml

* Update publish-sdk-npm.yml

* Update publish-sdk-npm.yml

* Update publish-sdk-npm.yml

* try disable typedoc because of minimatch errors

* bump versions to rc0

* limit packages published to only wasm clients

* TS SDK 1.4.1-rc1

* simplify version dependencies and add dist to dev mode

* add back version complexity for CI

* TS SDK 1.4.1-rc2

* ts-sdk: fix minimatch dependency and correct casing on `selfAddress` function call

* wasm: rename `main` to `main_js` to avoid collision errors in exporting main from tests

see https://github.com/wasm-bindgen/wasm-bindgen/issues/2206

* improve wasm js tests

* TS SDK 1.4.1-rc3

* update example docs

* TS SDK 1.4.1 release

* update docs dependencies to SDK 1.4.1

* update yarn lock file after TS SDK 1.4.1 publish

* Update ci-lint-typescript.yml

* Update ci-lint-typescript.yml

* Update ci-nym-wallet-storybook.yml

* Bump node tester version and add additional yarn install step to fix linting

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
2025-11-07 21:17:42 +00:00
import this 3e42160426 [Docs/operators]: Performance measurement pages (#6177)
* initialise wg perf docs

* fix paths to absolute

* fix paths to absolute

* vpn coloring guide

* improve quic script dwl way

* refactor operators menu structure

* create probe-details page

* wg syntax change

* modified time

* fix link paths

* remove redundancy

* fix comments & bump stats
2025-11-07 15:41:59 +00:00
jmwample 2f752a6c42 fix things related to interface changes 2025-11-06 18:37:50 -07:00
Jack Wampler 806f807f02 Implement Static DNS fallback (#6178) 2025-11-06 16:46:39 -07:00
Jack Wampler 1400db6156 DNS Reliability Fixes (#6175) 2025-11-06 12:37:27 -07:00
Simon Wicky 673a3e45d3 [bugfix] Distinguish authenticator errors by credential spent (#6176)
* distinguish authenticator errors by credential spent

* nitpicking fixes

* fix CI to see those changes

* error naming
2025-11-06 18:07:07 +01:00
Jędrzej Stuczyński d9c2f6ebda Feature/credential proxy jwt (#5957)
* squashed feature/credential-proxy-jwt [#5957]

post rebasing fixes

clippy

changed obtain-async endpoint to conditionally return jwt instead of pending zk-nym

watching for the attestation file and issuing jwt

* changed attestation starting time serialisation into rfc3339

* including authorised JWT issuers in attestation

* reduce attestation retrieval error log
2025-11-03 16:42:39 +00:00
import this e24e094711 [DOCs/operators]: Cleanup (#6170)
* fix quic docs steps

* update stats

* fix typo

* quic bridge update
2025-10-31 13:33:24 +00:00
benedettadavico 0d7487f530 bump versions 2025-10-31 13:24:27 +01:00
Jędrzej Stuczyński 378f32e6d7 disconnect mixnet client if registration fails (#6169)
Co-authored-by: Simon Wicky <simon@nymtech.net>
2025-10-31 12:07:22 +00:00
Jędrzej Stuczyński 3e9b8d237f Merge pull request #6168 from nymtech/chore/clippy-1.91
chore: resolve clippy 1.91 warnings
2025-10-31 12:00:55 +00:00
Jędrzej Stuczyński f5a4dbc555 removed useless use of vec! 2025-10-31 11:42:42 +00:00
Jędrzej Stuczyński 4480534e4d derived Default impl for EpochState 2025-10-31 11:39:58 +00:00
Jędrzej Stuczyński d355f9d752 Merge pull request #6167 from nymtech/master
master -> develop
2025-10-31 11:38:15 +00:00
Jędrzej Stuczyński 9f3a370496 Merge pull request #6166 from nymtech/merge/release/2025.19-kase-cherry-picked
release/2025.19 kase into master
2025-10-31 11:28:51 +00:00
Jędrzej Stuczyński e4adc5d954 Merge pull request #6165 from nymtech/release/2025.19-kase-cherry-picked
release/2025.19 kase into develop
2025-10-31 11:28:44 +00:00
Jędrzej Stuczyński 00373b70e2 Merge branch 'master' into merge/release/2025.19-kase-cherry-picked 2025-10-31 10:54:29 +00:00
benedettadavico 65f2017422 update changelog 2025-10-31 10:47:36 +00:00
benedettadavico 192f258463 update workflow 2025-10-31 10:47:34 +00:00
Tommy Verrall a5eee7444b Merge pull request #6143 from nymtech/bugfix/mix-tx-closed-v2
Bugfix: Add circuit breaker
2025-10-31 10:47:20 +00:00
benedettadavico 6abd7e7352 bump versions 2025-10-31 10:47:10 +00:00
Jędrzej Stuczyński 3306ca5357 merge 'master' into 'develop' 2025-10-30 17:43:42 +00:00
Jędrzej Stuczyński 9c2ccead0e Merge branch 'master' into merge/master/develop 2025-10-30 17:30:39 +00:00
import this b7aeb51362 [DOCs/operators]: Release notes for v2025.19 kase (#6157)
* add release and operators notes

* bump up version

* fix location in csv to USA

* bump up stats

* typo fix
2025-10-30 16:21:04 +00:00
benedetta davico e9e725defe Merge pull request #6093 from nymtech/bugfix/ns-api-node-custom-http-api-port
ns-api: fix scraping bug when operator specifies custom node HTTP API port in bond
2025-10-30 16:49:02 +01:00
import this c74494a21d [Feature/operators]: QUIC bridge deployment script v2 (#6145)
* new quick deployment script

* docs tweak

* update script to use .deb postinst

* final clean - ready to go

* correct nym-node config dir search with a fallback
2025-10-30 12:22:55 +00:00
Simon Wicky 54f6c98c22 remove unused deps (#6151) 2025-10-29 11:48:49 +01:00
Simon Wicky 846484bbb4 use typed builder (#6150) 2025-10-27 17:49:50 +01:00
Tommy Verrall fb3f5501ba Merge pull request #6143 from nymtech/bugfix/mix-tx-closed-v2
Bugfix: Add circuit breaker
2025-10-27 16:45:41 +01:00
Tommy Verrall e8a607f520 Merge pull request #6149 from nymtech/simon/tommy_too_quick
tommy is too quick
2025-10-27 13:52:55 +01:00
Simon Wicky f5f6df9eaf tommy is too quick 2025-10-27 13:50:49 +01:00
Tommy Verrall c647ab5605 Merge pull request #6148 from nymtech/simon/registration_client_timeout
configurable mixnet client startup timeout
2025-10-27 13:47:48 +01:00
Simon Wicky 416c21a42e configurable mixnet client startup timeout 2025-10-27 13:35:46 +01:00
Simon Wicky fd5a95fa4d allow overwriting existing sdk shutdown manager 2025-10-24 16:17:29 +02:00
Simon Wicky c61df79182 typo 2025-10-24 14:11:56 +02:00
Simon Wicky 08559a7660 calling for shutdown from the MixTrafficController 2025-10-24 14:07:15 +02:00
Jędrzej Stuczyński 6dce55a99b using same hierarchy of trackers for client shutdown control 2025-10-24 14:03:18 +02:00
Tommy Verrall bc0b89b31c Internal comments 2025-10-24 12:44:10 +02:00
Tommy Verrall 67c32faa11 Fix comments 2025-10-23 19:22:26 +02:00
Tommy Verrall aa0d15ee67 Better message to come in the PR description 2025-10-23 19:06:27 +02:00
p17o 4f0974fcf1 Update quic_bridge_deployment.sh for IPv4 and .deb package (#6138)
Updated ping commands to explicitly use IPv4 and adjusted file permission checks with sudo. Changed the forward address prompt to specify IPv4 and modified the binary download process to fetch and install the latest .deb release URL automatically.
2025-10-22 15:46:23 +00:00
Jędrzej Stuczyński bd2174641e bugfix: update internal owner address in transferred share (#6139) 2025-10-22 10:42:26 +01:00
Tommy Verrall 59b62fabc9 Merge pull request #6134 from nymtech/feature/domain-fronting-v2
Domain fronting
2025-10-22 11:08:21 +02:00
Tommy Verrall e6f4bae895 Last failing test - fix 2025-10-21 19:34:20 +02:00
Tommy Verrall d71742af32 Use explicit Vec<ApiUrl> handling in BaseClientBuilder
- Replace NymNetworkDetails with explicit API URL handling
- Fix deprecated from_network() usage and improve fallback logic
- Add URL validation and remove unused backwards compatibility
2025-10-21 19:15:24 +02:00
Tommy Verrall 3b7c07e249 Actually commit the recommended changes 2025-10-21 18:12:38 +02:00
Tommy Verrall 3b429dde69 Fix broken tests in CI 2025-10-21 16:29:26 +02:00
Tommy Verrall 3a29c296da Replace deprecated from_network() with new_with_fronted_urls() 2025-10-21 16:05:41 +02:00
Tommy Verrall 8544c54f8f Merge develop into feature/domain-fronting-v2
- Use new_with_fronted_urls() for explicit domain fronting
- Deprecate from_network() in favor of explicit method
2025-10-21 15:58:20 +02:00
Jędrzej Stuczyński 9f9639950b feat: expose more explicit new_with_fronted_urls builder for http API client (#6136) 2025-10-21 14:47:58 +01:00
Jędrzej Stuczyński 111a0b20b6 bugfix: update stored epoch share when changing ownership (#6135) 2025-10-21 14:10:20 +01:00
Tommy Verrall 67b300d0b8 Fix new_from_env() to populate nym_api_urls for domain fronting 2025-10-21 12:22:51 +02:00
Tommy Verrall f6800aff0a fix all clippy messages 2025-10-21 11:32:47 +02:00
Tommy Verrall 0c7c927ca2 Add more tests for retry logic 2025-10-21 11:32:47 +02:00
Tommy Verrall a69c8b1660 Fix confusing tracing logs 2025-10-21 11:32:47 +02:00
Tommy Verrall f3ea310a46 Fix retries - this is working 2025-10-21 11:32:46 +02:00
Tommy Verrall 92f9ff035d Add configuration-based domain fronting support
Changes:
- Add network_details field to BaseClientBuilder (optional, backwards compatible)
- Add with_network_details() method for opt-in domain fronting
- Update construct_nym_api_client() to use from_network() when network_details provided
- Enable network-defaults feature in nym-client-core Cargo.toml
- SDK passes network_details to BaseClientBuilder
2025-10-21 11:32:46 +02:00
Mark Sinclair 17d11f201e change migration and bump version 2025-10-17 15:04:53 +01:00
Mark Sinclair fef7e42eb4 bump version to rc 2025-10-17 14:56:02 +01:00
Mark Sinclair ceeeb6211b add tracing output 2025-10-17 14:52:59 +01:00
Mark Sinclair cd77b1032f clippy 2025-10-17 14:48:40 +01:00
Mark Sinclair 6bbb14f12f save custom_http_port to db 2025-10-17 14:48:40 +01:00
Mark Sinclair de8030d85a allow NS API to run once for scraping for troubleshooting and debugging 2025-10-17 14:48:40 +01:00
Mark Sinclair e18e64bf21 wip 2025-10-17 14:48:40 +01:00
Mark Sinclair a50c9ac3fb ns-api: fix scraping bug when operator specifies custom node HTTP API port in bond 2025-10-17 14:48:39 +01:00
benedetta davico 2235a6e147 Merge pull request #6113 from nymtech/release/2025.18-jarlsberg
Merge release/2025.18-jarlsberg to master
2025-10-15 10:22:16 +02:00
benedettadavico db6defa122 update changelog 2025-10-14 12:07:26 +02:00
Jędrzej Stuczyński df7768dec0 Bugfix/bloomfilters purge (#6089)
* remove all old bloomfilters upon starting binary

* remove old bloomfilter file upon purging secondary data
2025-10-06 14:02:32 +01:00
benedettadavico f3a449b7cc bump versions 2025-10-06 14:38:00 +02:00
benedetta davico cf21593ffa Merge pull request #6080 from nymtech/release/2025.17-isabirra
Merge release/2025.17-isabirra to master
2025-10-02 16:06:41 +02:00
benedetta davico f0d8dabb9f Merge pull request #6042 from nymtech/release/2025.16-halloumi
Merge release/2025.16-halloumi to master
2025-09-17 14:20:19 +02:00
benedetta davico f105bcbafe Merge pull request #5968 from nymtech/release/2025.15-gruyere
merge gruyere to master
2025-08-21 12:20:35 +02:00
benedetta davico dc0f4af2c1 Merge pull request #5937 from nymtech/release/2025.14-feta 2025-08-13 11:12:19 +02:00
benedetta davico 2a621e07a8 Merge pull request #5907 from nymtech/release/2025.13-emmental
Merge release/2025.13-emmental to master
2025-07-22 16:23:44 +02:00
benedetta davico 485aeebabd Merge pull request #5886 from nymtech/release/2025.12-dolcelatte
Merge release/2025.12-dolcelatte to master
2025-07-09 15:25:16 +02:00
benedetta davico 3b726bada9 Merge pull request #5839 from nymtech/release/2025.11-cheddar
merge release/2025.11-cheddar to master
2025-06-11 13:09:43 +02:00
benedetta davico 1d1b2e17d2 Merge pull request #5807 from nymtech/release/2025.10-brie 2025-05-28 09:38:15 +02:00
benedetta davico b5b2dbdfd8 Merge pull request #5776 from nymtech/release/2025.9-appenzeller
Release/2025.9-appenzeller to master
2025-05-16 13:23:10 +02:00
benedetta davico 82806f47d8 Merge pull request #5735 from nymtech/release/2025.8-tourist
Merge release/2025.8-tourist to master
2025-05-05 12:11:39 +02:00
benedetta davico c6f85cf23e Merge pull request #5727 from nymtech/release/2025.7-tex
Merge tex to master
2025-04-22 10:50:43 +02:00
benedetta davico ed8de7234d Merge pull request #5672 from nymtech/release/2025.6-chuckles
Merge release/2025.6-chuckles into master
2025-04-02 10:34:51 +02:00
benedetta davico e25d83b047 Merge pull request #5641 from nymtech/release/2025.5-chokito
Merge chokito to master
2025-03-24 10:14:50 +01:00
Jędrzej Stuczyński 9974d480b5 Merge pull request #5574 from nymtech/release/2025.4-dorina-patched
Release/2025.4-dorina-patched to master
2025-03-11 10:37:06 +00:00
benedetta davico 2211f13cdd Merge pull request #5551 from nymtech/release/2025.4-dorina
Merge release/2025.4-dorina to master
2025-03-04 13:55:27 +01:00
benedetta davico 4505f18a02 Merge pull request #5485 from nymtech/release/2025.3-ruta
Release/2025.3 ruta to master
2025-02-18 10:08:08 +01:00
benedetta davico a717a18948 Merge pull request #5430 from nymtech/release/2025.2-hu
Merge release/2025.2-hu to master
2025-02-06 13:58:55 +01:00
470 changed files with 19193 additions and 12182 deletions
+3
View File
@@ -8,10 +8,13 @@ on:
- 'gateway/**'
- 'integrations/**'
- 'nym-api/**'
- 'nym-authenticator-client/**'
- 'nym-credential-proxy/**'
- 'nym-ip-packet-client/**'
- 'nym-network-monitor/**'
- 'nym-node/**'
- 'nym-node-status-api/**'
- 'nym-registration-client/**'
- 'nym-statistics-api/**'
- 'nym-outfox/**'
- 'nym-validator-rewarder/**'
+10 -9
View File
@@ -6,16 +6,14 @@ on:
paths:
- "ts-packages/**"
- "sdk/typescript/**"
- "nym-connect/desktop/src/**"
- "nym-connect/desktop/package.json"
- "nym-wallet/src/**"
- "nym-wallet/package.json"
- "explorer/**"
- "explorer-v2/**"
- ".github/workflows/ci-lint-typescript.yml"
jobs:
build:
runs-on: ubuntu-22.04
runs-on: arc-linux-latest
env:
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
@@ -25,6 +23,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup yarn
run: npm install -g yarn
@@ -37,14 +36,12 @@ jobs:
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Install wasm-opt
uses: ./.github/actions/install-wasm-opt
with:
version: '116'
run: cargo install wasm-opt
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.23.7"
go-version: "1.24.6"
- name: Install
run: yarn
@@ -52,7 +49,11 @@ jobs:
- name: Build packages
run: yarn build:ci
- name: Install again
run: yarn
- name: Lint
run: yarn lint
- name: Typecheck with tsc
run: yarn tsc
@@ -8,7 +8,7 @@ on:
jobs:
build:
runs-on: custom-linux
runs-on: arc-linux-latest
steps:
- uses: actions/checkout@v4
+7 -4
View File
@@ -4,7 +4,7 @@ on:
jobs:
publish:
runs-on: arc-ubuntu-22.04
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -17,10 +17,13 @@ jobs:
- name: Setup yarn
run: npm install -g yarn
- name: Install Rust stable
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
components: rustfmt, clippy
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
@@ -29,9 +32,9 @@ jobs:
run: cargo install wasm-opt
- name: Set up Go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "1.23.7"
go-version: "1.24.6"
- name: Install dependencies
run: yarn
+3 -30
View File
@@ -3,11 +3,6 @@ name: Build and upload Node Status agent container to harbor.nymte.ch
on:
workflow_dispatch:
inputs:
gateway_probe_git_ref:
type: string
default: nym-vpn-core-v1.4.0
required: true
description: Which gateway probe git ref to build the image with
release_image:
description: 'Tag image as a release'
required: true
@@ -43,16 +38,6 @@ jobs:
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
run: |
GATEWAY_PROBE_GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }}
GIT_REF_SLUG="${GATEWAY_PROBE_GIT_REF//\//-}"
echo "git_ref=${GIT_REF_SLUG}" >> $GITHUB_OUTPUT
- 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: Initialize RELEASE_TAG
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
@@ -61,24 +46,12 @@ jobs:
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
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
- name: New env vars
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
# - name: Remove existing tag if exists
# run: |
# if git rev-parse $${{ env.GIT_TAG }} >/dev/null 2>&1; then
# git push --delete origin $${{ env.GIT_TAG }}
# git tag -d $${{ env.GIT_TAG }}
# fi
# - name: Create tag
# run: |
# git tag -a $${{ env.GIT_TAG }} -m "Version ${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}"
# git push origin $${{ env.GIT_TAG }}
run: echo "RELEASE_TAG='$RELEASE_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
- name: BuildAndPushImageOnHarbor
run: |
docker build --build-arg GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }} -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
+72
View File
@@ -4,6 +4,78 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.20-leerdammer] (2025-11-12)
- Max/tweak ts sdk actions ([#6185])
- chore: resolve clippy 1.91 warnings ([#6168])
- [chore] Remove unused dependencies ([#6151])
- Use typed-builder for registration client builder config ([#6150])
- tommy is too quick ([#6149])
- configurable mixnet client startup timeout ([#6148])
- [Feature/operators]: QUIC bridge deployment script v2 ([#6145])
- Bugfix: Add circuit breaker ([#6143])
- bugfix: update internal owner address in transferred share ([#6139])
- Update quic_bridge_deployment.sh for IPv4 and .deb package ([#6138])
- feat: expose more explicit new_with_fronted_urls builder for http API client ([#6136])
- bugfix: update stored epoch share when changing ownership ([#6135])
- Domain fronting ([#6134])
- bugfix: update stored epoch share when changing announce address ([#6131])
[#6185]: https://github.com/nymtech/nym/pull/6185
[#6168]: https://github.com/nymtech/nym/pull/6168
[#6151]: https://github.com/nymtech/nym/pull/6151
[#6150]: https://github.com/nymtech/nym/pull/6150
[#6149]: https://github.com/nymtech/nym/pull/6149
[#6148]: https://github.com/nymtech/nym/pull/6148
[#6145]: https://github.com/nymtech/nym/pull/6145
[#6143]: https://github.com/nymtech/nym/pull/6143
[#6139]: https://github.com/nymtech/nym/pull/6139
[#6138]: https://github.com/nymtech/nym/pull/6138
[#6136]: https://github.com/nymtech/nym/pull/6136
[#6135]: https://github.com/nymtech/nym/pull/6135
[#6134]: https://github.com/nymtech/nym/pull/6134
[#6131]: https://github.com/nymtech/nym/pull/6131
## [2025.19-kase] (2025-10-30)
- update ns agent workflow ([#6154])
- Cherry pick - request #6143 from nymtech/bugfix/mix-tx-closed-v2 ([#6153])
- bugfix: nym-credential-proxy query params parsing regression ([#6121])
- bugfix: revert some dep updates introduced in #6043 ([#6120])
- Skip ipv6 metadata endpoint request ([#6118])
- update to no longer use 1mb files ([#6117])
- chore: restore pending dkg contract state migration ([#6116])
- Revert "Propagate cancel token to mixnet client" ([#6115])
- Update dirs to 6.0 ([#6109])
- Propagate cancel token to mixnet client ([#6105])
- bugfix: retrieve and update ticketbook in the same query ([#6101])
- bugfix: include network name in the default gateway probe config path ([#6100])
- Bugfix/incompatibility fixes ([#6099])
- [DOCs/operators] QUIC deployment script & docs ([#6098])
- bugfix: testnet manager 02sql migration ([#6096])
- feat: move gateway probe to monorepo (and update to rust edition 2024) ([#6094])
- bugfix: use custom topology provider for list of init gateways ([#6092])
- Max/fix wasm client + build commands ([#6043])
[#6154]: https://github.com/nymtech/nym/pull/6154
[#6153]: https://github.com/nymtech/nym/pull/6153
[#6121]: https://github.com/nymtech/nym/pull/6121
[#6120]: https://github.com/nymtech/nym/pull/6120
[#6118]: https://github.com/nymtech/nym/pull/6118
[#6117]: https://github.com/nymtech/nym/pull/6117
[#6116]: https://github.com/nymtech/nym/pull/6116
[#6115]: https://github.com/nymtech/nym/pull/6115
[#6109]: https://github.com/nymtech/nym/pull/6109
[#6105]: https://github.com/nymtech/nym/pull/6105
[#6101]: https://github.com/nymtech/nym/pull/6101
[#6100]: https://github.com/nymtech/nym/pull/6100
[#6099]: https://github.com/nymtech/nym/pull/6099
[#6098]: https://github.com/nymtech/nym/pull/6098
[#6096]: https://github.com/nymtech/nym/pull/6096
[#6094]: https://github.com/nymtech/nym/pull/6094
[#6092]: https://github.com/nymtech/nym/pull/6092
[#6043]: https://github.com/nymtech/nym/pull/6043
## [2025.18-jarlsberg] (2025-10-14)
- ns-api: add descriptions to dVPN gateway responses ([#6102])
Generated
+81 -42
View File
@@ -133,9 +133,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "ammonia"
version = "4.1.1"
version = "4.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6b346764dd0814805de8abf899fe03065bcee69bb1a4771c785817e39f3978f"
checksum = "17e913097e1a2124b46746c980134e8c954bc17a6a59bb3fde96f088d126dde6"
dependencies = [
"cssparser",
"html5ever",
@@ -2262,7 +2262,7 @@ dependencies = [
"libc",
"option-ext",
"redox_users",
"windows-sys 0.59.0",
"windows-sys 0.60.2",
]
[[package]]
@@ -2579,7 +2579,7 @@ dependencies = [
[[package]]
name = "extension-storage"
version = "1.4.0-rc.0"
version = "1.4.1"
dependencies = [
"bip39",
"console_error_panic_hook",
@@ -4478,7 +4478,7 @@ dependencies = [
[[package]]
name = "mix-fetch-wasm"
version = "1.4.0-rc.0"
version = "1.4.1"
dependencies = [
"async-trait",
"futures",
@@ -4488,6 +4488,7 @@ dependencies = [
"nym-ordered-buffer",
"nym-service-providers-common",
"nym-socks5-requests",
"nym-validator-client",
"rand 0.8.5",
"serde",
"serde-wasm-bindgen 0.6.5",
@@ -4824,7 +4825,7 @@ dependencies = [
[[package]]
name = "nym-api"
version = "1.1.67"
version = "1.1.69"
dependencies = [
"anyhow",
"async-trait",
@@ -4987,6 +4988,7 @@ dependencies = [
"nym-network-defaults",
"nym-service-provider-requests-common",
"nym-sphinx",
"nym-test-utils",
"nym-wireguard-types",
"rand 0.8.5",
"semver 1.0.26",
@@ -4994,6 +4996,7 @@ dependencies = [
"sha2 0.10.9",
"strum_macros",
"thiserror 2.0.12",
"tracing",
"x25519-dalek",
]
@@ -5002,21 +5005,16 @@ name = "nym-bandwidth-controller"
version = "0.1.0"
dependencies = [
"async-trait",
"bip39",
"log",
"nym-credential-storage",
"nym-credentials",
"nym-credentials-interface",
"nym-crypto",
"nym-ecash-contract-common",
"nym-ecash-time",
"nym-network-defaults",
"nym-task",
"nym-validator-client",
"rand 0.8.5",
"thiserror 2.0.12",
"url",
"zeroize",
]
[[package]]
@@ -5050,7 +5048,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.64"
version = "1.1.66"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -5133,7 +5131,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.64"
version = "1.1.66"
dependencies = [
"bs58",
"clap",
@@ -5206,6 +5204,7 @@ dependencies = [
"nym-task",
"nym-topology",
"nym-validator-client",
"once_cell",
"rand 0.8.5",
"rand_chacha 0.3.1",
"serde",
@@ -5281,9 +5280,25 @@ dependencies = [
"tracing",
]
[[package]]
name = "nym-client-rtt-tester"
version = "0.1.0"
dependencies = [
"nym-client-core",
"nym-config",
"nym-crypto",
"nym-network-defaults",
"nym-sdk",
"nym-sphinx",
"nym-task",
"nym-topology",
"tokio",
"tracing",
]
[[package]]
name = "nym-client-wasm"
version = "1.4.0-rc.0"
version = "1.4.1"
dependencies = [
"anyhow",
"futures",
@@ -5462,8 +5477,11 @@ dependencies = [
"nym-crypto",
"nym-ecash-contract-common",
"nym-ecash-signer-check",
"nym-http-api-client",
"nym-http-api-common",
"nym-network-defaults",
"nym-pemstore",
"nym-upgrade-mode-check",
"nym-validator-client",
"rand 0.8.5",
"reqwest 0.12.22",
@@ -5534,6 +5552,7 @@ dependencies = [
"nym-http-api-client",
"nym-http-api-common",
"nym-serde-helpers",
"nym-upgrade-mode-check",
"reqwest 0.12.22",
"schemars 0.8.22",
"serde",
@@ -5565,6 +5584,7 @@ dependencies = [
"sqlx",
"sqlx-pool-guard",
"thiserror 2.0.12",
"time",
"tokio",
"zeroize",
]
@@ -5600,12 +5620,13 @@ dependencies = [
"nym-api-requests",
"nym-credentials",
"nym-credentials-interface",
"nym-crypto",
"nym-ecash-contract-common",
"nym-gateway-requests",
"nym-gateway-storage",
"nym-task",
"nym-upgrade-mode-check",
"nym-validator-client",
"rand 0.8.5",
"si-scale",
"thiserror 2.0.12",
"time",
@@ -5645,6 +5666,7 @@ dependencies = [
"nym-compact-ecash",
"nym-ecash-time",
"nym-network-defaults",
"nym-upgrade-mode-check",
"rand 0.8.5",
"serde",
"strum",
@@ -5661,6 +5683,7 @@ dependencies = [
"aead",
"aes",
"aes-gcm-siv",
"anyhow",
"base64 0.22.1",
"blake3",
"bs58",
@@ -5674,10 +5697,12 @@ dependencies = [
"jwt-simple",
"nym-pemstore",
"nym-sphinx-types",
"nym-test-utils",
"rand 0.8.5",
"rand_chacha 0.3.1",
"serde",
"serde_bytes",
"serde_json",
"sha2 0.10.9",
"subtle-encoding",
"thiserror 2.0.12",
@@ -5792,7 +5817,6 @@ dependencies = [
name = "nym-gateway"
version = "1.1.36"
dependencies = [
"anyhow",
"async-trait",
"bincode",
"bip39",
@@ -5803,7 +5827,6 @@ dependencies = [
"futures",
"ipnetwork",
"mock_instant",
"nym-api-requests",
"nym-authenticator-requests",
"nym-client-core",
"nym-credential-verification",
@@ -5816,7 +5839,6 @@ dependencies = [
"nym-id",
"nym-ip-packet-router",
"nym-mixnet-client",
"nym-mixnode-common",
"nym-network-defaults",
"nym-network-requester",
"nym-node-metrics",
@@ -5826,20 +5848,18 @@ dependencies = [
"nym-statistics-common",
"nym-task",
"nym-topology",
"nym-types",
"nym-upgrade-mode-check",
"nym-validator-client",
"nym-wireguard",
"nym-wireguard-private-metadata-server",
"nym-wireguard-types",
"rand 0.8.5",
"serde",
"sha2 0.10.9",
"thiserror 2.0.12",
"time",
"tokio",
"tokio-stream",
"tokio-tungstenite",
"tokio-util",
"tracing",
"url",
"zeroize",
@@ -6050,6 +6070,7 @@ dependencies = [
"thiserror 2.0.12",
"tokio",
"tracing",
"tracing-subscriber",
"url",
"wasmtimer",
]
@@ -6354,7 +6375,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.1.65"
version = "1.1.67"
dependencies = [
"addr",
"anyhow",
@@ -6404,7 +6425,7 @@ dependencies = [
[[package]]
name = "nym-node"
version = "1.19.0"
version = "1.21.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -6435,6 +6456,7 @@ dependencies = [
"nym-bin-common",
"nym-client-core-config-types",
"nym-config",
"nym-credential-verification",
"nym-crypto",
"nym-gateway",
"nym-gateway-stats-storage",
@@ -6507,13 +6529,13 @@ version = "0.1.0"
dependencies = [
"async-trait",
"celes",
"humantime",
"humantime-serde",
"nym-bin-common",
"nym-crypto",
"nym-exit-policy",
"nym-http-api-client",
"nym-noise-keys",
"nym-upgrade-mode-check",
"nym-wireguard-types",
"rand_chacha 0.3.1",
"schemars 0.8.22",
@@ -6524,6 +6546,7 @@ dependencies = [
"thiserror 2.0.12",
"time",
"tokio",
"url",
"utoipa",
]
@@ -6546,7 +6569,7 @@ dependencies = [
[[package]]
name = "nym-node-status-api"
version = "4.0.10"
version = "4.0.11-rc1"
dependencies = [
"ammonia",
"anyhow",
@@ -6642,7 +6665,7 @@ dependencies = [
[[package]]
name = "nym-node-tester-wasm"
version = "1.3.0-rc.0"
version = "1.3.1"
dependencies = [
"futures",
"js-sys",
@@ -6798,6 +6821,7 @@ dependencies = [
"tokio",
"tokio-util",
"tracing",
"typed-builder",
"url",
]
@@ -6930,7 +6954,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.64"
version = "1.1.66"
dependencies = [
"bs58",
"clap",
@@ -7377,12 +7401,14 @@ dependencies = [
"jwt-simple",
"nym-crypto",
"nym-http-api-client",
"nym-test-utils",
"reqwest 0.12.22",
"serde",
"serde_json",
"thiserror 2.0.12",
"time",
"tracing",
"utoipa",
]
[[package]]
@@ -7562,17 +7588,10 @@ dependencies = [
name = "nym-wireguard"
version = "0.1.0"
dependencies = [
"async-trait",
"base64 0.22.1",
"bincode",
"chrono",
"dashmap",
"defguard_wireguard_rs",
"dyn-clone",
"futures",
"ip_network",
"log",
"nym-authenticator-requests",
"nym-credential-verification",
"nym-credentials-interface",
"nym-crypto",
@@ -7583,11 +7602,9 @@ dependencies = [
"nym-task",
"nym-wireguard-types",
"thiserror 2.0.12",
"time",
"tokio",
"tokio-stream",
"tracing",
"x25519-dalek",
]
[[package]]
@@ -7639,15 +7656,20 @@ version = "1.0.0"
dependencies = [
"async-trait",
"axum",
"futures",
"nym-credential-verification",
"nym-credentials-interface",
"nym-crypto",
"nym-http-api-client",
"nym-http-api-common",
"nym-upgrade-mode-check",
"nym-wireguard",
"nym-wireguard-private-metadata-client",
"nym-wireguard-private-metadata-server",
"nym-wireguard-private-metadata-shared",
"time",
"tokio",
"tower 0.5.2",
"tower-http 0.5.2",
"utoipa",
]
@@ -7657,10 +7679,7 @@ name = "nym-wireguard-types"
version = "0.1.0"
dependencies = [
"base64 0.22.1",
"log",
"nym-config",
"nym-crypto",
"nym-network-defaults",
"rand 0.8.5",
"serde",
"thiserror 2.0.12",
@@ -7669,7 +7688,7 @@ dependencies = [
[[package]]
name = "nymvisor"
version = "0.1.29"
version = "0.1.31"
dependencies = [
"anyhow",
"bytes",
@@ -7823,9 +7842,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.109"
version = "0.9.110"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
checksum = "0a9f0075ba3c21b09f8e8b2026584b1d18d49388648f2fbbf3c97ea8deced8e2"
dependencies = [
"cc",
"libc",
@@ -11391,6 +11410,26 @@ dependencies = [
"utf-8",
]
[[package]]
name = "typed-builder"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d0dd654273fc253fde1df4172c31fb6615cf8b041d3a4008a028ef8b1119e66"
dependencies = [
"typed-builder-macro",
]
[[package]]
name = "typed-builder-macro"
version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016c26257f448222014296978b2c8456e2cad4de308c35bdb1e383acd569ef5b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "typenum"
version = "1.18.0"
+9 -13
View File
@@ -18,6 +18,7 @@ resolver = "2"
members = [
"clients/native",
"clients/native/websocket-requests",
"clients/rtt-tester",
"clients/socks5",
"common/async-file-watcher",
"common/authenticator-requests",
@@ -150,7 +151,7 @@ members = [
"tools/internal/contract-state-importer/importer-cli",
"tools/internal/contract-state-importer/importer-contract",
"tools/internal/mixnet-connectivity-check",
# "tools/internal/sdk-version-bump",
# "tools/internal/sdk-version-bump",
"tools/internal/ssl-inject",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
@@ -171,6 +172,7 @@ members = [
default-members = [
"clients/native",
"clients/socks5",
"nym-authenticator-client",
"nym-api",
"nym-credential-proxy/nym-credential-proxy",
"nym-node",
@@ -215,7 +217,6 @@ base64 = "0.22.1"
base85rs = "0.1.3"
bincode = "1.3.3"
bip39 = { version = "2.0.0", features = ["zeroize"] }
bit-vec = "0.7.0" # can we unify those?
bitvec = "1.0.0"
blake3 = "1.7.0"
bloomfilter = "3.0.1"
@@ -243,13 +244,11 @@ criterion = "0.5"
csv = "1.3.1"
ctr = "0.9.1"
cupid = "0.6.1"
curve25519-dalek = "4.1"
dashmap = "5.5.3"
# We want https://github.com/DefGuard/wireguard-rs/pull/64 , but there's no crates.io release being pushed out anymore
defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", rev = "v0.4.7" }
digest = "0.10.7"
dirs = "6.0"
doc-comment = "0.3"
dotenvy = "0.15.6"
dyn-clone = "1.0.19"
ecdsa = "0.16"
@@ -265,11 +264,8 @@ futures = "0.3.31"
futures-util = "0.3"
generic-array = "0.14.7"
getrandom = "0.2.10"
getset = "0.1.5"
handlebars = "3.5.5"
headers = "0.4.0"
hex = "0.4.3"
hex-literal = "0.3.3"
hickory-resolver = "0.25"
hkdf = "0.12.3"
hmac = "0.12.1"
@@ -293,12 +289,10 @@ lazy_static = "1.5.0"
ledger-transport = "0.10.0"
ledger-transport-hid = "0.10.0"
log = "0.4"
maxminddb = "0.23.0"
mime = "0.3.17"
moka = { version = "0.12", features = ["future"] }
nix = "0.27.1"
notify = "5.1.0"
okapi = "0.7.0"
once_cell = "1.21.3"
opentelemetry = "0.19.0"
opentelemetry-jaeger = "0.18.0"
@@ -307,7 +301,6 @@ pem = "0.8"
petgraph = "0.6.5"
pin-project = "1.1"
pnet_packet = "0.35.0"
pin-project-lite = "0.2.16"
publicsuffix = "2.3.0"
proc_pidinfo = "0.1.3"
quote = "1"
@@ -315,13 +308,10 @@ rand = "0.8.5"
rand_chacha = "0.3"
rand_core = "0.6.3"
rand_distr = "0.4"
rand_pcg = "0.3.1"
rand_seeder = "0.2.3"
rayon = "1.5.1"
regex = "1.10.6"
reqwest = { version = "0.12.15", default-features = false }
rs_merkle = "1.5.0"
safer-ffi = "0.1.13"
schemars = "0.8.22"
semver = "1.0.26"
serde = "1.0.219"
@@ -368,6 +358,7 @@ tracing-indicatif = "0.3.9"
tracing-test = "0.2.5"
ts-rs = "10.1.0"
tungstenite = { version = "0.20.1", default-features = false }
typed-builder = "0.23.0"
uniffi = "0.29.2"
uniffi_build = "0.29.0"
url = "2.5"
@@ -464,6 +455,11 @@ opt-level = 'z'
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] }
[workspace.lints.clippy]
suspicious = "deny"
complexity = "deny"
perf = "deny"
style = "deny"
unwrap_used = "deny"
expect_used = "deny"
todo = "deny"
+4 -4
View File
@@ -107,16 +107,16 @@ sdk-wasm-build:
$(MAKE) -C nym-browser-extension/storage wasm-pack
$(MAKE) -C wasm/client
$(MAKE) -C wasm/node-tester
# $(MAKE) -C wasm/mix-fetch
$(MAKE) -C wasm/mix-fetch
$(MAKE) -C wasm/zknym-lib
# $(MAKE) -C wasm/full-nym-wasm
# run this from npm/yarn to ensure tools are in the path, e.g. yarn build:sdk from root of repo
sdk-typescript-build:
npx lerna run --scope @nymproject/sdk build --stream
# npx lerna run --scope @nymproject/mix-fetch build --stream
# npx lerna run --scope @nymproject/node-tester build --stream
# yarn --cwd sdk/typescript/codegen/contract-clients build
npx lerna run --scope @nymproject/mix-fetch build --stream
npx lerna run --scope @nymproject/node-tester build --stream
yarn --cwd sdk/typescript/codegen/contract-clients build
# NOTE: These targets are part of the main workspace (but not as wasm32-unknown-unknown)
WASM_CRATES = extension-storage nym-client-wasm nym-node-tester-wasm zknym-lib
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.64"
version = "1.1.66"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+22
View File
@@ -0,0 +1,22 @@
[package]
name = "nym-client-rtt-tester"
version = "0.1.0"
edition = "2021"
description = "RTT testing client built using nym-client-core"
license.workspace = true
[[bin]]
name = "nym-client-rtt-tester"
path = "src/main.rs"
[dependencies]
tokio = { workspace = true, features = ["full"] }
tracing = { workspace = true }
nym-client-core = { path = "../../common/client-core" }
nym-network-defaults = { path = "../../common/network-defaults" }
nym-sphinx = { path = "../../common/nymsphinx" }
nym-topology = { path = "../../common/topology" }
nym-config = { path = "../../common/config" }
nym-task = { path = "../../common/task" }
nym-crypto = { path = "../../common/crypto" }
nym-sdk = { path = "../../sdk/rust/nym-sdk" }
+466
View File
@@ -0,0 +1,466 @@
use nym_sdk::mixnet;
use nym_sdk::mixnet::MixnetMessageSender;
use nym_client_core::client::rtt_analyzer::{RttAnalyzer, RttConfig, RttEvent, RttPattern};
use nym_sdk::DebugConfig;
use tokio::io::{self, AsyncBufReadExt};
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
// ============================================================
// 1. Start RTT Analyzer + background worker
// ============================================================
let _analyzer = RttAnalyzer::new();
let tx = RttAnalyzer::producer().expect("Analyzer was not initialized!");
// ============================================================
// 2. Build mixnet client
// ============================================================
let mut debug = DebugConfig::default();
// Disable ALL Poisson & cover streams
debug.traffic.disable_main_poisson_packet_distribution = true;
debug.cover_traffic.disable_loop_cover_traffic_stream = false;
let client = mixnet::MixnetClientBuilder::new_ephemeral()
.debug_config(debug)
.build()
.unwrap();
let client = client.connect_to_mixnet().await.unwrap();
let our_address = client.nym_address();
println!("Our client nym address is: {our_address}");
// ============================================================
// 3. Ask the user for RTT TEST configuration
// ============================================================
let config = ask_user_for_rtt_config().await;
println!("\nStarting RTT test with:");
println!(" packets_per_route = {}", config.packets_per_route);
println!(" pattern = {:?}", config.pattern);
println!(" delay (ms) = {}", config.inter_route_delay_ms);
// START THE TEST
let _ = client
.send_rtt_test(*our_address, None, tx.clone(), config)
.await
.unwrap();
// ============================================================
// 4. Background listener for incoming messages
// ============================================================
tokio::spawn({
let mut client = client;
async move {
loop {
if client.wait_for_messages().await.is_some() {
//I should do something here to shutdown
}
sleep(Duration::from_millis(10)).await;
}
}
});
// ============================================================
// 5. Main input loop
// ============================================================
let stdin = io::BufReader::new(io::stdin());
let mut lines = stdin.lines();
println!("Type 'menu' to show RTT commands.");
loop {
if let Ok(Some(input)) = lines.next_line().await {
let input = input.trim().to_lowercase();
if input == "menu" {
show_menu_and_handle_choice(&tx).await;
}
}
sleep(Duration::from_millis(50)).await;
}
}
// =====================================================================
// ASK USER FOR RTT TEST SETTINGS AT PROGRAM START
// =====================================================================
async fn ask_user_for_rtt_config() -> RttConfig {
let stdin = io::BufReader::new(io::stdin());
let mut lines = stdin.lines();
println!("\n========== RTT TEST CONFIGURATION ==========");
// -----------------------------
// Ask for packets per route
// -----------------------------
println!("Enter number of packets per route: ");
let packets = read_u32_from_stdin(&mut lines).await;
// -----------------------------
// Ask for pattern: Burst / RR
// -----------------------------
println!("Choose pattern:");
println!(" 1) Burst");
println!(" 2) Round Robin");
let pattern = loop {
let input = read_string(&mut lines).await;
match input.as_str() {
"1" => break RttPattern::Burst,
"2" => break RttPattern::RoundRobin,
_ => println!("Invalid choice! Please type 1 or 2:"),
}
};
// -----------------------------
// Ask for delay between packets
// -----------------------------
println!("Enter delay between packets (ms): ");
let delay = read_u64_from_stdin(&mut lines).await;
// Build Config
RttConfig {
packets_per_route: packets,
pattern,
inter_route_delay_ms: delay,
}
}
// =====================================================================
// Util functions for reading typed input
// =====================================================================
async fn read_string(lines: &mut tokio::io::Lines<io::BufReader<io::Stdin>>) -> String {
loop {
if let Ok(Some(line)) = lines.next_line().await {
let trimmed = line.trim().to_string();
if !trimmed.is_empty() {
return trimmed;
}
}
println!("Please type a value:");
}
}
async fn read_u32_from_stdin(lines: &mut tokio::io::Lines<io::BufReader<io::Stdin>>) -> u32 {
loop {
if let Ok(Some(line)) = lines.next_line().await {
if let Ok(num) = line.trim().parse::<u32>() {
return num;
}
}
println!("Invalid number, try again:");
}
}
async fn read_u64_from_stdin(lines: &mut tokio::io::Lines<io::BufReader<io::Stdin>>) -> u64 {
loop {
if let Ok(Some(line)) = lines.next_line().await {
if let Ok(num) = line.trim().parse::<u64>() {
return num;
}
}
println!("Invalid number, try again:");
}
}
// =====================================================================
// MENU HANDLER (FULL VERSION WITH HELP / DOCS)
// =====================================================================
async fn show_menu_and_handle_choice(tx: &tokio::sync::mpsc::Sender<RttEvent>) {
println!("\n======================== RTT MENU ========================");
println!("1) Print global RTT statistics");
println!("2) Write statistics to CSV file");
println!("3) Print route details by ROUTE INDEX");
println!("4) Print route details by ROUTE NODES STRING");
println!("5) Print routes with AVG RTT above threshold");
println!("6) Print routes with ANY RTT above threshold");
println!("7) Help (Show all commands & how to use them)");
println!("8) Write CSV and generate RTT histogram(s) with Python");
println!("9) Show overall experiment completion percentage");
println!("===========================================================");
print!("Select option: ");
use std::io::Write;
std::io::stdout().flush().unwrap();
let mut input = String::new();
let _ = std::io::stdin().read_line(&mut input);
let choice = input.trim();
match choice {
// -------------------- 1. PRINT GLOBAL STATS --------------------
"1" => {
let _ = tx.send(RttEvent::PrintStats).await;
}
// -------------------- 2. WRITE STATS ---------------------------
"2" => {
print!("Enter file path: ");
std::io::stdout().flush().unwrap();
let mut path = String::new();
let _ = std::io::stdin().read_line(&mut path);
let path = path.trim().to_string();
let _ = tx.send(RttEvent::WriteStats { path }).await;
}
// -------------------- 3. PRINT ROUTE DETAILS -------------------
"3" => {
print!("Enter route index (0-based): ");
std::io::stdout().flush().unwrap();
let mut s = String::new();
let _ = std::io::stdin().read_line(&mut s);
if let Ok(index) = s.trim().parse::<usize>() {
let _ = tx
.send(RttEvent::PrintRouteDetail { route_index: index })
.await;
} else {
println!("Invalid index.");
}
}
// -------------------- 4. PRINT STATS BY NODE STRING -----------
"4" => {
println!("Enter Node String EXACTLY as stored.");
println!("Example format:");
println!(" <base58_node1> > <base58_node2> > <base58_node3>");
print!("Nodes: ");
std::io::stdout().flush().unwrap();
let mut nodes = String::new();
let _ = std::io::stdin().read_line(&mut nodes);
let nodes = nodes.trim().to_string();
let _ = tx.send(RttEvent::PrintRouteStatsByNodes { nodes }).await;
}
// -------------------- 5. AVG ABOVE THRESHOLD ------------------
"5" => {
print!("Enter threshold in ms: ");
std::io::stdout().flush().unwrap();
let mut s = String::new();
let _ = std::io::stdin().read_line(&mut s);
if let Ok(th) = s.trim().parse::<u128>() {
let _ = tx
.send(RttEvent::PrintRoutesWithAvgAbove { threshold_ms: th })
.await;
} else {
println!("Invalid number.");
}
}
// -------------------- 6. ANY ABOVE THRESHOLD ------------------
"6" => {
print!("Enter threshold in ms: ");
std::io::stdout().flush().unwrap();
let mut s = String::new();
let _ = std::io::stdin().read_line(&mut s);
if let Ok(th) = s.trim().parse::<u128>() {
let _ = tx
.send(RttEvent::PrintRoutesWithAnyAbove { threshold_ms: th })
.await;
} else {
println!("Invalid number.");
}
}
// -------------------- 7. HELP --------------------------------
"7" => {
print_help();
}
"8" => {
// Ask for CSV path
print!("Enter CSV output path (e.g. rtt_stats.csv): ");
std::io::stdout().flush().unwrap();
let mut path = String::new();
let _ = std::io::stdin().read_line(&mut path);
let path = path.trim().to_string();
// Sub-menu for histogram mode
println!("\nHistogram mode:");
println!(" 1) One plot with ALL RTT samples (including outliers)");
println!(" 2) One plot with INLIERS only (RTT <= cutoff)");
println!(" 3) TWO plots: one for INLIERS and one for OUTLIERS");
print!("Select mode: ");
std::io::stdout().flush().unwrap();
let mut mode_input = String::new();
let _ = std::io::stdin().read_line(&mut mode_input);
let mode_choice = mode_input.trim();
let outlier_mode = match mode_choice {
// 1) All RTTs
"1" => "all".to_string(),
// 2) Only inliers, ask for cutoff in seconds
"2" => {
print!("Enter cutoff in seconds (e.g. 1.0 for 1 second): ");
std::io::stdout().flush().unwrap();
let mut c = String::new();
let _ = std::io::stdin().read_line(&mut c);
let cutoff = c.trim();
cutoff.to_string() // e.g. "1.0"
}
// 3) Two plots: inliers + outliers (both)
"3" => {
print!("Enter cutoff in seconds (e.g. 1.0 for 1 second): ");
std::io::stdout().flush().unwrap();
let mut c = String::new();
let _ = std::io::stdin().read_line(&mut c);
let cutoff = c.trim();
// Encode as 'both:<cutoff>' so Python can understand it
format!("both:{cutoff}")
}
_ => {
println!("Invalid mode, aborting histogram generation.");
return;
}
};
let _ = tx
.send(RttEvent::WriteStatsAndPlot { path, outlier_mode })
.await;
}
"9" => {
// Send an event to the RTT analyzer to compute and print progress
let _ = tx.send(RttEvent::PrintExperimentProgress).await;
}
_ => println!("Invalid selection."),
}
}
fn print_help() {
println!("\n======================== RTT HELP ========================\n");
println!("This tool allows you to perform detailed RTT analysis over all mixnet routes.");
println!("The client sends RTT probe traffic through every candidate route,");
println!("and the RTT analyzer collects per-route statistics in the background.\n");
println!("Main commands (from the RTT menu):\n");
println!(" 1) Print global RTT statistics");
println!(" Prints one summary line per route:");
println!(" - route index");
println!(" - packets sent (including retransmissions)");
println!(" - number of ACKs");
println!(" - number of timeouts");
println!(" - average RTT (computed over all stored RTT samples, in ms)");
println!();
println!(" 2) Write stats to CSV file");
println!(" Writes one line per route to a CSV file on disk.");
println!(" Current CSV columns:");
println!(" route,sent,acks,timeouts,avg_rtt");
println!(" route : numeric route index");
println!(" sent : how many FragmentSent events were recorded");
println!(" acks : how many FragmentAckReceived events were recorded");
println!(" timeouts : how many FragmentAckExpired events were recorded");
println!(" avg_rtt : average RTT (in milliseconds) from all RTT samples");
println!();
println!(" 3) Print route details BY ROUTE INDEX");
println!(" Input: a 0-based route index.");
println!(" Output for that route:");
println!(" - node list (base58 identities) in order: Node1 > Node2 > Node3");
println!(" - ALL RTT samples recorded for that route (each sample shown in ms)");
println!(" This is useful when you already know the route index and");
println!(" want to inspect exactly how it behaves packet by packet.");
println!();
println!(" 4) Print route details BY NODE STRING");
println!(" Input format must match exactly what the analyzer stored, for example:");
println!(" <node1_base58> > <node2_base58> > <node3_base58>");
println!(" If a route with that node sequence exists, the tool will:");
println!(" - print the matching route index");
println!(" - print the full per-route detail (same as option 3).");
println!(" This is useful when you have a specific mixnode combination");
println!(" (e.g. a slow or suspicious path) and want its statistics.");
println!();
println!(" 5) Print routes with AVERAGE RTT ABOVE a threshold");
println!(" You provide a threshold in milliseconds (e.g. 150).");
println!(" The tool will:");
println!(" - compute avg RTT for each route");
println!(" - select only routes where avg RTT > threshold");
println!(" - print detailed info for each matching route (nodes + RTT samples).");
println!(" Use this to quickly find generally slow routes.");
println!();
println!(" 6) Print routes with ANY RTT ABOVE a threshold");
println!(" You provide a threshold in milliseconds (e.g. 500).");
println!(" For each route, if at least one RTT sample exceeds the threshold,");
println!(" that route is printed with full details.");
println!(" Use this to find routes that occasionally spike very high,");
println!(" even if their average RTT is still acceptable.");
println!();
println!(" 7) Show experiment progress (percentage completed)");
println!(" Uses the stored experiment configuration (total_routes, packets_per_route)");
println!(" plus the number of RTT samples recorded so far to estimate:");
println!(" completion = received_samples / (total_routes * packets_per_route)");
println!(" The result is printed as a percentage (0%100%).");
println!(" This tells you roughly how far the RTT experiment has progressed.");
println!();
println!(" 8) Write stats AND generate histogram(s) via Python");
println!(" This command will:");
println!(" 1) Write the current route statistics to a CSV file (same as option 2).");
println!(" 2) Call the Python script 'rtt_histogram.py' to visualize RTTs.");
println!();
println!(" When prompted, you will provide two things:");
println!(" - CSV file path (where to save the stats)");
println!(" - outlier_mode string, which controls which histograms are generated:");
println!();
println!("\"all\"");
println!(" Use ALL avg_rtt values from the CSV.");
println!(
" Result: a single histogram containing every route's avg RTT (in seconds)."
);
println!();
println!("\"<cutoff>\" (numeric, in seconds, e.g. \"1.0\")");
println!(" Only keep avg_rtt <= cutoff.");
println!(" Result: a single histogram with INLIERS only (values <= cutoff).");
println!(
" Example: \"1.0\" keeps everything at or below 1.0s and drops slower routes."
);
println!();
println!("\"both:<cutoff>\" (e.g. \"both:1.0\")");
println!(" Split the data into two sets:");
println!(" - inliers : avg_rtt <= cutoff");
println!(" - outliers : avg_rtt > cutoff");
println!(" Result: TWO histograms are generated:");
println!(" 1) Distribution of inliers");
println!(" 2) Distribution of outliers");
println!(
" This helps visually compare the \"normal\" routes and the very slow ones."
);
println!();
println!("Helpful notes:");
println!(" • RTT samples are computed when a FragmentReceived event arrives.");
println!(" For each fragment that may be retransmitted, the analyzer stores");
println!(" multiple send times and receive times, and pairs them in order");
println!(" to compute multiple RTT values for that fragment if needed.");
println!("\"sent\" in the stats includes retransmissions as well, so it may be");
println!(" higher than packets_per_route for unstable routes.");
println!("===========================================================\n");
}
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.64"
version = "1.1.66"
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"
+7
View File
@@ -16,6 +16,7 @@ serde = { workspace = true, features = ["derive"] }
semver = { workspace = true }
strum_macros = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true }
nym-credentials-interface = { path = "../credentials-interface" }
nym-crypto = { path = "../crypto", features = ["asymmetric"] }
@@ -29,7 +30,13 @@ hmac = { workspace = true, optional = true }
sha2 = { workspace = true, optional = true }
x25519-dalek = { workspace = true, features = ["static_secrets"] }
[dev-dependencies]
nym-test-utils = { path = "../test-utils" }
[features]
default = ["verify"]
# this is moved to a separate feature as we really need clients to import it (especially, *cough*, wasm)
verify = ["hmac", "sha2"]
[lints]
workspace = true
@@ -6,9 +6,11 @@ use nym_wireguard_types::PeerPublicKey;
use crate::{
AuthenticatorVersion, Error,
latest::registration::IpPair,
traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, Versionable},
v2, v3, v4, v5,
traits::{
FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, UpgradeModeMessage,
Versionable,
},
v2, v3, v4, v5, v6,
};
// This is very redundant with AuthenticatorRequest and I reckon they could be smooshed.
@@ -19,6 +21,293 @@ pub enum ClientMessage {
Final(Box<dyn FinalMessage + Send + Sync + 'static>),
Query(Box<dyn QueryBandwidthMessage + Send + Sync + 'static>),
TopUp(Box<dyn TopUpMessage + Send + Sync + 'static>),
UpgradeModeCheck(Box<dyn UpgradeModeMessage + Send + Sync + 'static>),
}
pub struct SerialisedRequest {
pub bytes: Vec<u8>,
pub request_id: u64,
}
impl SerialisedRequest {
pub fn new(bytes: Vec<u8>, request_id: u64) -> Self {
Self { bytes, request_id }
}
}
impl ClientMessage {
fn serialise_v1(&self) -> Result<SerialisedRequest, Error> {
Err(Error::UnsupportedVersion)
}
fn serialise_v2(&self, reply_to: Recipient) -> Result<SerialisedRequest, Error> {
use v2::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ip: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?
.into(),
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message
.credential()
.and_then(|c| c.credential.into_zk_nym())
.map(|c| *c),
},
reply_to,
);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) =
AuthenticatorRequest::new_query_request(query_message.pub_key(), reply_to);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
_ => Err(Error::UnsupportedMessage),
}
}
fn serialise_v3(&self, reply_to: Recipient) -> Result<SerialisedRequest, Error> {
use v3::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ip: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?
.into(),
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message
.credential()
.and_then(|c| c.credential.into_zk_nym())
.map(|c| *c),
},
reply_to,
);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) =
AuthenticatorRequest::new_query_request(query_message.pub_key(), reply_to);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(
TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
},
reply_to,
);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
_ => Err(Error::UnsupportedMessage),
}
}
fn serialise_v4(&self, reply_to: Recipient) -> Result<SerialisedRequest, Error> {
use v4::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ips: IpPair {
ipv4: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?,
ipv6: final_message
.gateway_client_ipv6()
.ok_or(Error::UnsupportedMessage)?,
},
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message
.credential()
.and_then(|c| c.credential.into_zk_nym())
.map(|c| *c),
},
reply_to,
);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) =
AuthenticatorRequest::new_query_request(query_message.pub_key(), reply_to);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(
TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
},
reply_to,
);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
_ => Err(Error::UnsupportedMessage),
}
}
fn serialise_v5(&self) -> Result<SerialisedRequest, Error> {
use v5::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage {
pub_key: init_message.pub_key(),
});
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ips: IpPair {
ipv4: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?,
ipv6: final_message
.gateway_client_ipv6()
.ok_or(Error::UnsupportedMessage)?,
},
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message
.credential()
.and_then(|c| c.credential.into_zk_nym())
.map(|c| *c),
});
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) = AuthenticatorRequest::new_query_request(query_message.pub_key());
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
});
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
_ => Err(Error::UnsupportedMessage),
}
}
fn serialise_v6(&self) -> Result<SerialisedRequest, Error> {
use v6::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage, IpPair},
request::AuthenticatorRequest,
topup::TopUpMessage,
upgrade_mode_check::UpgradeModeCheckRequest,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage {
pub_key: init_message.pub_key(),
});
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ips: IpPair {
ipv4: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?,
ipv6: final_message
.gateway_client_ipv6()
.ok_or(Error::UnsupportedMessage)?,
},
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
});
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) = AuthenticatorRequest::new_query_request(query_message.pub_key());
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
});
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
ClientMessage::UpgradeModeCheck(upgrade_mode_check) => {
// currently JWT is the only emergency credential option
let Some(upgrade_mode_jwt) =
upgrade_mode_check.upgrade_mode_global_attestation_jwt()
else {
return Err(Error::conversion(
"no valid known upgrade mode check variants",
));
};
let msg = UpgradeModeCheckRequest::UpgradeModeJwt {
token: upgrade_mode_jwt,
};
let (req, id) = AuthenticatorRequest::new_upgrade_mode_check_request(msg);
Ok(SerialisedRequest::new(req.to_bytes()?, id))
}
}
}
}
impl ClientMessage {
@@ -27,7 +316,7 @@ impl ClientMessage {
match self {
Self::Final(msg) => msg.credential().is_some(),
Self::TopUp(_) => true,
Self::Initial(_) | Self::Query(_) => false,
Self::Initial(_) | Self::Query(_) | Self::UpgradeModeCheck(_) => false,
}
}
@@ -37,208 +326,18 @@ impl ClientMessage {
ClientMessage::Final(msg) => msg.version(),
ClientMessage::Query(msg) => msg.version(),
ClientMessage::TopUp(msg) => msg.version(),
ClientMessage::UpgradeModeCheck(msg) => msg.version(),
}
}
pub fn bytes(&self, reply_to: Recipient) -> Result<(Vec<u8>, u64), Error> {
pub fn bytes(&self, reply_to: Recipient) -> Result<SerialisedRequest, Error> {
match self.version() {
AuthenticatorVersion::V1 => Err(Error::UnsupportedVersion),
AuthenticatorVersion::V2 => {
use v2::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ip: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?
.into(),
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) = AuthenticatorRequest::new_query_request(
query_message.pub_key(),
reply_to,
);
Ok((req.to_bytes()?, id))
}
_ => Err(Error::UnsupportedMessage),
}
}
AuthenticatorVersion::V3 => {
use v3::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ip: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?
.into(),
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) = AuthenticatorRequest::new_query_request(
query_message.pub_key(),
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(
TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
}
}
AuthenticatorVersion::V4 => {
use v4::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(
InitMessage {
pub_key: init_message.pub_key(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(
FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ips: IpPair {
ipv4: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?,
ipv6: final_message
.gateway_client_ipv6()
.ok_or(Error::UnsupportedMessage)?,
}
.into(),
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) = AuthenticatorRequest::new_query_request(
query_message.pub_key(),
reply_to,
);
Ok((req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(
TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
},
reply_to,
);
Ok((req.to_bytes()?, id))
}
}
}
AuthenticatorVersion::V5 => {
use v5::{
registration::{ClientMac, FinalMessage, GatewayClient, InitMessage},
request::AuthenticatorRequest,
topup::TopUpMessage,
};
match self {
ClientMessage::Initial(init_message) => {
let (req, id) = AuthenticatorRequest::new_initial_request(InitMessage {
pub_key: init_message.pub_key(),
});
Ok((req.to_bytes()?, id))
}
ClientMessage::Final(final_message) => {
let (req, id) = AuthenticatorRequest::new_final_request(FinalMessage {
gateway_client: GatewayClient {
pub_key: final_message.gateway_client_pub_key(),
private_ips: IpPair {
ipv4: final_message
.gateway_client_ipv4()
.ok_or(Error::UnsupportedMessage)?,
ipv6: final_message
.gateway_client_ipv6()
.ok_or(Error::UnsupportedMessage)?,
},
mac: ClientMac::new(final_message.gateway_client_mac()),
},
credential: final_message.credential(),
});
Ok((req.to_bytes()?, id))
}
ClientMessage::Query(query_message) => {
let (req, id) =
AuthenticatorRequest::new_query_request(query_message.pub_key());
Ok((req.to_bytes()?, id))
}
ClientMessage::TopUp(top_up_message) => {
let (req, id) = AuthenticatorRequest::new_topup_request(TopUpMessage {
pub_key: top_up_message.pub_key(),
credential: top_up_message.credential(),
});
Ok((req.to_bytes()?, id))
}
}
}
AuthenticatorVersion::V1 => self.serialise_v1(),
AuthenticatorVersion::V2 => self.serialise_v2(reply_to),
AuthenticatorVersion::V3 => self.serialise_v3(reply_to),
AuthenticatorVersion::V4 => self.serialise_v4(reply_to),
AuthenticatorVersion::V5 => self.serialise_v5(),
AuthenticatorVersion::V6 => self.serialise_v6(),
AuthenticatorVersion::UNKNOWN => Err(Error::UnknownVersion),
}
}
@@ -247,7 +346,7 @@ impl ClientMessage {
use AuthenticatorVersion::*;
match self.version() {
V1 | V2 | V3 | V4 => false,
V5 => true,
V5 | V6 => true,
UNKNOWN => true,
}
}
@@ -1,6 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::fmt::Display;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -37,3 +38,13 @@ pub enum Error {
#[error(transparent)]
Bincode(#[from] bincode::Error),
}
impl Error {
pub fn conversion(msg: impl Into<String>) -> Self {
Error::Conversion(msg.into())
}
pub fn conversion_display(msg: impl Display) -> Self {
Error::Conversion(msg.to_string())
}
}
+3 -1
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
pub mod client_message;
pub mod models;
pub mod request;
pub mod response;
pub mod traits;
@@ -10,13 +11,14 @@ pub mod v2;
pub mod v3;
pub mod v4;
pub mod v5;
pub mod v6;
mod error;
mod util;
mod version;
pub use error::Error;
pub use v5 as latest;
pub use v6 as latest;
pub use version::AuthenticatorVersion;
pub const CURRENT_VERSION: u8 = latest::VERSION;
@@ -0,0 +1,58 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_credentials_interface::{
BandwidthCredential, CredentialSpendingData, TicketType, UnknownTicketType,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)]
pub enum CurrentUpgradeModeStatus {
Enabled,
Disabled,
// everything pre-v6
Unknown,
}
impl CurrentUpgradeModeStatus {
pub fn is_enabled(&self) -> bool {
matches!(self, CurrentUpgradeModeStatus::Enabled)
}
}
impl From<bool> for CurrentUpgradeModeStatus {
fn from(value: bool) -> Self {
if value {
CurrentUpgradeModeStatus::Enabled
} else {
CurrentUpgradeModeStatus::Disabled
}
}
}
impl From<CurrentUpgradeModeStatus> for Option<bool> {
fn from(value: CurrentUpgradeModeStatus) -> Self {
match value {
CurrentUpgradeModeStatus::Enabled => Some(true),
CurrentUpgradeModeStatus::Disabled => Some(false),
CurrentUpgradeModeStatus::Unknown => None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct BandwidthClaim {
pub credential: BandwidthCredential,
pub kind: TicketType,
}
impl TryFrom<CredentialSpendingData> for BandwidthClaim {
type Error = UnknownTicketType;
fn try_from(credential: CredentialSpendingData) -> Result<Self, Self::Error> {
Ok(BandwidthClaim {
kind: TicketType::try_from_encoded(credential.payment.t_type)?,
credential: BandwidthCredential::from(credential),
})
}
}
+51 -2
View File
@@ -4,8 +4,10 @@
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use nym_sphinx::addressing::Recipient;
use crate::traits::{FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage};
use crate::{v1, v2, v3, v4, v5};
use crate::traits::{
FinalMessage, InitMessage, QueryBandwidthMessage, TopUpMessage, UpgradeModeMessage,
};
use crate::{v1, v2, v3, v4, v5, v6};
#[derive(Debug)]
pub enum AuthenticatorRequest {
@@ -33,6 +35,11 @@ pub enum AuthenticatorRequest {
reply_to: Option<Recipient>,
request_id: u64,
},
CheckUpgradeMode {
msg: Box<dyn UpgradeModeMessage + Send + Sync + 'static>,
protocol: Protocol,
request_id: u64,
},
}
impl From<v1::request::AuthenticatorRequest> for AuthenticatorRequest {
@@ -202,3 +209,45 @@ impl From<v5::request::AuthenticatorRequest> for AuthenticatorRequest {
}
}
}
impl From<v6::request::AuthenticatorRequest> for AuthenticatorRequest {
fn from(value: v6::request::AuthenticatorRequest) -> Self {
match value.data {
v6::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial {
msg: Box::new(init_message),
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
},
v6::request::AuthenticatorRequestData::Final(final_message) => Self::Final {
msg: final_message,
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
},
v6::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => {
Self::QueryBandwidth {
msg: Box::new(peer_public_key),
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
}
}
v6::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => {
Self::TopUpBandwidth {
msg: top_up_message,
protocol: value.protocol,
reply_to: None,
request_id: value.request_id,
}
}
v6::request::AuthenticatorRequestData::CheckUpgradeMode(upgrade_mode_check_msg) => {
Self::CheckUpgradeMode {
msg: Box::new(upgrade_mode_check_msg),
protocol: value.protocol,
request_id: value.request_id,
}
}
}
}
}
+49 -2
View File
@@ -1,11 +1,12 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::CurrentUpgradeModeStatus;
use crate::traits::{
Id, PendingRegistrationResponse, RegisteredResponse, RemainingBandwidthResponse,
TopUpBandwidthResponse,
TopUpBandwidthResponse, UpgradeModeStatus,
};
use crate::{v2, v3, v4, v5};
use crate::{v2, v3, v4, v5, v6};
#[derive(Debug)]
pub enum AuthenticatorResponse {
@@ -13,6 +14,29 @@ pub enum AuthenticatorResponse {
Registered(Box<dyn RegisteredResponse + Send + Sync + 'static>),
RemainingBandwidth(Box<dyn RemainingBandwidthResponse + Send + Sync + 'static>),
TopUpBandwidth(Box<dyn TopUpBandwidthResponse + Send + Sync + 'static>),
UpgradeMode(Box<dyn UpgradeModeStatus + Send + Sync + 'static>),
}
impl UpgradeModeStatus for AuthenticatorResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
match self {
AuthenticatorResponse::PendingRegistration(pending_registration_response) => {
pending_registration_response.upgrade_mode_status()
}
AuthenticatorResponse::Registered(registered_response) => {
registered_response.upgrade_mode_status()
}
AuthenticatorResponse::RemainingBandwidth(remaining_bandwidth_response) => {
remaining_bandwidth_response.upgrade_mode_status()
}
AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => {
top_up_bandwidth_response.upgrade_mode_status()
}
AuthenticatorResponse::UpgradeMode(upgrade_mode_response) => {
upgrade_mode_response.upgrade_mode_status()
}
}
}
}
impl Id for AuthenticatorResponse {
@@ -28,6 +52,7 @@ impl Id for AuthenticatorResponse {
AuthenticatorResponse::TopUpBandwidth(top_up_bandwidth_response) => {
top_up_bandwidth_response.id()
}
AuthenticatorResponse::UpgradeMode(upgrade_mode_response) => upgrade_mode_response.id(),
}
}
}
@@ -104,3 +129,25 @@ impl From<v5::response::AuthenticatorResponse> for AuthenticatorResponse {
}
}
}
impl From<v6::response::AuthenticatorResponse> for AuthenticatorResponse {
fn from(value: v6::response::AuthenticatorResponse) -> Self {
match value.data {
v6::response::AuthenticatorResponseData::PendingRegistration(
pending_registration_response,
) => Self::PendingRegistration(Box::new(pending_registration_response)),
v6::response::AuthenticatorResponseData::Registered(registered_response) => {
Self::Registered(Box::new(registered_response))
}
v6::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response,
) => Self::RemainingBandwidth(Box::new(remaining_bandwidth_response)),
v6::response::AuthenticatorResponseData::TopUpBandwidth(top_up_bandwidth_response) => {
Self::TopUpBandwidth(Box::new(top_up_bandwidth_response))
}
v6::response::AuthenticatorResponseData::UpgradeMode(upgrade_mode_check_response) => {
Self::UpgradeMode(Box::new(upgrade_mode_check_response))
}
}
}
}
+456 -34
View File
@@ -1,15 +1,15 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::latest::registration::IpPair;
use crate::models::{BandwidthClaim, CurrentUpgradeModeStatus};
use crate::{AuthenticatorVersion, Error, v1, v2, v3, v4, v5, v6};
use nym_credentials_interface::CredentialSpendingData;
use nym_crypto::asymmetric::x25519;
use nym_wireguard_types::PeerPublicKey;
use std::fmt;
use std::net::{Ipv4Addr, Ipv6Addr};
use nym_credentials_interface::CredentialSpendingData;
use nym_crypto::asymmetric::x25519::PrivateKey;
use nym_wireguard_types::PeerPublicKey;
use crate::latest::registration::IpPair;
use crate::{AuthenticatorVersion, Error, v1, v2, v3, v4, v5};
use tracing::error;
pub trait Versionable {
fn version(&self) -> AuthenticatorVersion;
@@ -51,6 +51,12 @@ impl Versionable for v5::registration::InitMessage {
}
}
impl Versionable for v6::registration::InitMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V6
}
}
impl Versionable for v2::registration::FinalMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V2
@@ -75,6 +81,12 @@ impl Versionable for v5::registration::FinalMessage {
}
}
impl Versionable for v6::registration::FinalMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V6
}
}
impl Versionable for PeerPublicKey {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V3
@@ -98,6 +110,158 @@ impl Versionable for v5::topup::TopUpMessage {
AuthenticatorVersion::V5
}
}
impl Versionable for v6::topup::TopUpMessage {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V6
}
}
impl Versionable for v6::upgrade_mode_check::UpgradeModeCheckRequest {
fn version(&self) -> AuthenticatorVersion {
AuthenticatorVersion::V6
}
}
pub trait UpgradeModeStatus: Id + fmt::Debug {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus;
}
impl UpgradeModeStatus for v1::response::PendingRegistrationResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v1::response::RegisteredResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v1::response::RemainingBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v2::response::PendingRegistrationResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v2::response::RegisteredResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v2::response::RemainingBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v3::response::PendingRegistrationResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v3::response::RegisteredResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v3::response::RemainingBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v3::response::TopUpBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v4::response::PendingRegistrationResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v4::response::RegisteredResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v4::response::RemainingBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v4::response::TopUpBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v5::response::PendingRegistrationResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v5::response::RegisteredResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v5::response::RemainingBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v5::response::TopUpBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
CurrentUpgradeModeStatus::Unknown
}
}
impl UpgradeModeStatus for v6::response::PendingRegistrationResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
self.upgrade_mode_enabled.into()
}
}
impl UpgradeModeStatus for v6::response::RegisteredResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
self.upgrade_mode_enabled.into()
}
}
impl UpgradeModeStatus for v6::response::RemainingBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
self.upgrade_mode_enabled.into()
}
}
impl UpgradeModeStatus for v6::response::TopUpBandwidthResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
self.upgrade_mode_enabled.into()
}
}
impl UpgradeModeStatus for v6::response::UpgradeModeResponse {
fn upgrade_mode_status(&self) -> CurrentUpgradeModeStatus {
self.upgrade_mode_enabled.into()
}
}
pub trait InitMessage: Versionable + fmt::Debug {
fn pub_key(&self) -> PeerPublicKey;
@@ -133,14 +297,20 @@ impl InitMessage for v5::registration::InitMessage {
}
}
impl InitMessage for v6::registration::InitMessage {
fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
}
pub trait FinalMessage: Versionable + fmt::Debug {
fn gateway_client_pub_key(&self) -> PeerPublicKey;
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error>;
fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error>;
fn private_ips(&self) -> IpPair;
fn gateway_client_ipv4(&self) -> Option<Ipv4Addr>;
fn gateway_client_ipv6(&self) -> Option<Ipv6Addr>;
fn gateway_client_mac(&self) -> Vec<u8>;
fn credential(&self) -> Option<CredentialSpendingData>;
fn credential(&self) -> Option<BandwidthClaim>;
}
impl FinalMessage for v1::GatewayClient {
@@ -148,7 +318,7 @@ impl FinalMessage for v1::GatewayClient {
self.pub_key
}
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> {
self.verify(private_key, nonce)
}
@@ -171,7 +341,7 @@ impl FinalMessage for v1::GatewayClient {
self.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
fn credential(&self) -> Option<BandwidthClaim> {
None
}
}
@@ -181,7 +351,7 @@ impl FinalMessage for v2::registration::FinalMessage {
self.gateway_client.pub_key
}
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> {
self.gateway_client.verify(private_key, nonce)
}
@@ -204,8 +374,12 @@ impl FinalMessage for v2::registration::FinalMessage {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
fn credential(&self) -> Option<BandwidthClaim> {
self.credential.clone().and_then(|c| {
c.try_into()
.inspect_err(|err| error!("credential conversion error: {err}"))
.ok()
})
}
}
@@ -214,7 +388,7 @@ impl FinalMessage for v3::registration::FinalMessage {
self.gateway_client.pub_key
}
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> {
self.gateway_client.verify(private_key, nonce)
}
@@ -237,8 +411,12 @@ impl FinalMessage for v3::registration::FinalMessage {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
fn credential(&self) -> Option<BandwidthClaim> {
self.credential.clone().and_then(|c| {
c.try_into()
.inspect_err(|err| error!("credential conversion error: {err}"))
.ok()
})
}
}
@@ -247,7 +425,42 @@ impl FinalMessage for v4::registration::FinalMessage {
self.gateway_client.pub_key
}
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> {
self.gateway_client.verify(private_key, nonce)
}
fn private_ips(&self) -> IpPair {
// v4 -> v5 -> v6
v5::registration::IpPair::from(self.gateway_client.private_ips).into()
}
fn gateway_client_ipv4(&self) -> Option<Ipv4Addr> {
Some(self.gateway_client.private_ips.ipv4)
}
fn gateway_client_ipv6(&self) -> Option<Ipv6Addr> {
Some(self.gateway_client.private_ips.ipv6)
}
fn gateway_client_mac(&self) -> Vec<u8> {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<BandwidthClaim> {
self.credential.clone().and_then(|c| {
c.try_into()
.inspect_err(|err| error!("credential conversion error: {err}"))
.ok()
})
}
}
impl FinalMessage for v5::registration::FinalMessage {
fn gateway_client_pub_key(&self) -> PeerPublicKey {
self.gateway_client.pub_key
}
fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> {
self.gateway_client.verify(private_key, nonce)
}
@@ -267,17 +480,21 @@ impl FinalMessage for v4::registration::FinalMessage {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
self.credential.clone()
fn credential(&self) -> Option<BandwidthClaim> {
self.credential.clone().and_then(|c| {
c.try_into()
.inspect_err(|err| error!("credential conversion error: {err}"))
.ok()
})
}
}
impl FinalMessage for v5::registration::FinalMessage {
impl FinalMessage for v6::registration::FinalMessage {
fn gateway_client_pub_key(&self) -> PeerPublicKey {
self.gateway_client.pub_key
}
fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
fn verify(&self, private_key: &x25519::PrivateKey, nonce: u64) -> Result<(), Error> {
self.gateway_client.verify(private_key, nonce)
}
@@ -297,7 +514,7 @@ impl FinalMessage for v5::registration::FinalMessage {
self.gateway_client.mac.to_vec()
}
fn credential(&self) -> Option<CredentialSpendingData> {
fn credential(&self) -> Option<BandwidthClaim> {
self.credential.clone()
}
}
@@ -347,10 +564,42 @@ impl TopUpMessage for v5::topup::TopUpMessage {
}
}
impl TopUpMessage for v6::topup::TopUpMessage {
fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
fn credential(&self) -> CredentialSpendingData {
self.credential.clone()
}
}
pub trait UpgradeModeMessage: Versionable + fmt::Debug {
// the idea is to expose different types of emergency credentials here,
// like upgrade mode JWT, emergency threshold credential issued by signers, etc.
fn upgrade_mode_global_attestation_jwt(&self) -> Option<String>;
}
impl UpgradeModeMessage for v6::upgrade_mode_check::UpgradeModeCheckRequest {
fn upgrade_mode_global_attestation_jwt(&self) -> Option<String> {
use v6::upgrade_mode_check::UpgradeModeCheckRequest;
match self {
UpgradeModeCheckRequest::UpgradeModeJwt { token } => Some(token.clone()),
}
}
}
pub trait Id {
fn id(&self) -> u64;
}
impl Id for v1::response::PendingRegistrationResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v2::response::PendingRegistrationResponse {
fn id(&self) -> u64 {
self.request_id
@@ -375,6 +624,18 @@ impl Id for v5::response::PendingRegistrationResponse {
}
}
impl Id for v6::response::PendingRegistrationResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v1::response::RegisteredResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v2::response::RegisteredResponse {
fn id(&self) -> u64 {
self.request_id
@@ -399,6 +660,18 @@ impl Id for v5::response::RegisteredResponse {
}
}
impl Id for v6::response::RegisteredResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v1::response::RemainingBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v2::response::RemainingBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
@@ -423,6 +696,12 @@ impl Id for v5::response::RemainingBandwidthResponse {
}
}
impl Id for v6::response::RemainingBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v3::response::TopUpBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
@@ -441,11 +720,28 @@ impl Id for v5::response::TopUpBandwidthResponse {
}
}
pub trait PendingRegistrationResponse: Id + fmt::Debug {
impl Id for v6::response::TopUpBandwidthResponse {
fn id(&self) -> u64 {
self.request_id
}
}
impl Id for v6::response::UpgradeModeResponse {
fn id(&self) -> u64 {
self.request_id
}
}
pub trait PendingRegistrationResponse: Id + UpgradeModeStatus + fmt::Debug {
fn nonce(&self) -> u64;
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error>;
fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error>;
fn pub_key(&self) -> PeerPublicKey;
fn private_ips(&self) -> IpPair;
fn finalise_registration(
&self,
private_key: &x25519::PrivateKey,
credential: Option<BandwidthClaim>,
) -> Box<dyn FinalMessage + Send + Sync>;
}
impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse {
@@ -453,7 +749,7 @@ impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse {
self.reply.nonce
}
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> {
fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
@@ -464,6 +760,22 @@ impl PendingRegistrationResponse for v2::response::PendingRegistrationResponse {
fn private_ips(&self) -> IpPair {
self.reply.gateway_data.private_ip.into()
}
fn finalise_registration(
&self,
private_key: &x25519::PrivateKey,
credential: Option<BandwidthClaim>,
) -> Box<dyn FinalMessage + Send + Sync> {
Box::new(v2::registration::FinalMessage {
gateway_client: v2::registration::GatewayClient::new(
private_key,
self.pub_key().inner(),
self.private_ips().ipv4.into(),
self.nonce(),
),
credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)),
})
}
}
impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse {
@@ -471,7 +783,7 @@ impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse {
self.reply.nonce
}
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> {
fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
@@ -482,6 +794,22 @@ impl PendingRegistrationResponse for v3::response::PendingRegistrationResponse {
fn private_ips(&self) -> IpPair {
self.reply.gateway_data.private_ip.into()
}
fn finalise_registration(
&self,
private_key: &x25519::PrivateKey,
credential: Option<BandwidthClaim>,
) -> Box<dyn FinalMessage + Send + Sync> {
Box::new(v3::registration::FinalMessage {
gateway_client: v3::registration::GatewayClient::new(
private_key,
self.pub_key().inner(),
self.private_ips().ipv4.into(),
self.nonce(),
),
credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)),
})
}
}
impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse {
@@ -489,7 +817,42 @@ impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse {
self.reply.nonce
}
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> {
fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.gateway_data.pub_key
}
fn private_ips(&self) -> IpPair {
// v4 -> v5 -> v6
v5::registration::IpPair::from(self.reply.gateway_data.private_ips).into()
}
fn finalise_registration(
&self,
private_key: &x25519::PrivateKey,
credential: Option<BandwidthClaim>,
) -> Box<dyn FinalMessage + Send + Sync> {
Box::new(v4::registration::FinalMessage {
gateway_client: v4::registration::GatewayClient::new(
private_key,
self.pub_key().inner(),
self.reply.gateway_data.private_ips,
self.nonce(),
),
credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)),
})
}
}
impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse {
fn nonce(&self) -> u64 {
self.reply.nonce
}
fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
@@ -500,14 +863,30 @@ impl PendingRegistrationResponse for v4::response::PendingRegistrationResponse {
fn private_ips(&self) -> IpPair {
self.reply.gateway_data.private_ips.into()
}
fn finalise_registration(
&self,
private_key: &x25519::PrivateKey,
credential: Option<BandwidthClaim>,
) -> Box<dyn FinalMessage + Send + Sync> {
Box::new(v5::registration::FinalMessage {
gateway_client: v5::registration::GatewayClient::new(
private_key,
self.pub_key().inner(),
self.reply.gateway_data.private_ips,
self.nonce(),
),
credential: credential.and_then(|b| b.credential.into_zk_nym().map(|c| *c)),
})
}
}
impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse {
impl PendingRegistrationResponse for v6::response::PendingRegistrationResponse {
fn nonce(&self) -> u64 {
self.reply.nonce
}
fn verify(&self, gateway_key: &PrivateKey) -> std::result::Result<(), Error> {
fn verify(&self, gateway_key: &x25519::PrivateKey) -> Result<(), Error> {
self.reply.gateway_data.verify(gateway_key, self.nonce())
}
@@ -518,9 +897,25 @@ impl PendingRegistrationResponse for v5::response::PendingRegistrationResponse {
fn private_ips(&self) -> IpPair {
self.reply.gateway_data.private_ips
}
fn finalise_registration(
&self,
private_key: &x25519::PrivateKey,
credential: Option<BandwidthClaim>,
) -> Box<dyn FinalMessage + Send + Sync> {
Box::new(v6::registration::FinalMessage {
gateway_client: v6::registration::GatewayClient::new(
private_key,
self.pub_key().inner(),
self.reply.gateway_data.private_ips,
self.nonce(),
),
credential,
})
}
}
pub trait RegisteredResponse: Id + fmt::Debug {
pub trait RegisteredResponse: Id + UpgradeModeStatus + fmt::Debug {
fn private_ips(&self) -> IpPair;
fn pub_key(&self) -> PeerPublicKey;
fn wg_port(&self) -> u16;
@@ -555,7 +950,8 @@ impl RegisteredResponse for v3::response::RegisteredResponse {
}
impl RegisteredResponse for v4::response::RegisteredResponse {
fn private_ips(&self) -> IpPair {
self.reply.private_ips.into()
// v4 -> v5 -> v6
v5::registration::IpPair::from(self.reply.private_ips).into()
}
fn pub_key(&self) -> PeerPublicKey {
@@ -568,6 +964,20 @@ impl RegisteredResponse for v4::response::RegisteredResponse {
}
impl RegisteredResponse for v5::response::RegisteredResponse {
fn private_ips(&self) -> IpPair {
self.reply.private_ips.into()
}
fn pub_key(&self) -> PeerPublicKey {
self.reply.pub_key
}
fn wg_port(&self) -> u16 {
self.reply.wg_port
}
}
impl RegisteredResponse for v6::response::RegisteredResponse {
fn private_ips(&self) -> IpPair {
self.reply.private_ips
}
@@ -581,7 +991,7 @@ impl RegisteredResponse for v5::response::RegisteredResponse {
}
}
pub trait RemainingBandwidthResponse: Id + fmt::Debug {
pub trait RemainingBandwidthResponse: Id + UpgradeModeStatus + fmt::Debug {
fn available_bandwidth(&self) -> Option<i64>;
}
@@ -609,7 +1019,13 @@ impl RemainingBandwidthResponse for v5::response::RemainingBandwidthResponse {
}
}
pub trait TopUpBandwidthResponse: Id + fmt::Debug {
impl RemainingBandwidthResponse for v6::response::RemainingBandwidthResponse {
fn available_bandwidth(&self) -> Option<i64> {
self.reply.as_ref().map(|r| r.available_bandwidth)
}
}
pub trait TopUpBandwidthResponse: Id + UpgradeModeStatus + fmt::Debug {
fn available_bandwidth(&self) -> i64;
}
@@ -630,3 +1046,9 @@ impl TopUpBandwidthResponse for v5::response::TopUpBandwidthResponse {
self.reply.available_bandwidth
}
}
impl TopUpBandwidthResponse for v6::response::TopUpBandwidthResponse {
fn available_bandwidth(&self) -> i64 {
self.reply.available_bandwidth
}
}
@@ -48,7 +48,7 @@ pub struct RegistrationData {
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RegistredData {
pub struct RegisteredData {
pub pub_key: PeerPublicKey,
pub private_ip: IpAddr,
pub wg_port: u16,
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData};
use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData};
use nym_sphinx::addressing::Recipient;
use serde::{Deserialize, Serialize};
@@ -34,7 +34,7 @@ impl AuthenticatorResponse {
}
pub fn new_registered(
registred_data: RegistredData,
registred_data: RegisteredData,
reply_to: Recipient,
request_id: u64,
) -> Self {
@@ -108,7 +108,7 @@ pub struct PendingRegistrationResponse {
pub struct RegisteredResponse {
pub request_id: u64,
pub reply_to: Recipient,
pub reply: RegistredData,
pub reply: RegisteredData,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
@@ -154,8 +154,8 @@ impl From<v2::registration::RegistrationData> for v1::registration::Registration
}
}
impl From<v2::registration::RegistredData> for v1::registration::RegistredData {
fn from(value: v2::registration::RegistredData) -> Self {
impl From<v2::registration::RegisteredData> for v1::registration::RegisteredData {
fn from(value: v2::registration::RegisteredData) -> Self {
Self {
pub_key: value.pub_key,
private_ip: value.private_ip,
@@ -58,7 +58,7 @@ pub struct RegistrationData {
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RegistredData {
pub struct RegisteredData {
pub pub_key: PeerPublicKey,
pub private_ip: IpAddr,
pub wg_port: u16,
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData};
use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData};
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use nym_sphinx::addressing::Recipient;
use serde::{Deserialize, Serialize};
@@ -38,7 +38,7 @@ impl AuthenticatorResponse {
}
pub fn new_registered(
registred_data: RegistredData,
registred_data: RegisteredData,
reply_to: Recipient,
request_id: u64,
) -> Self {
@@ -118,7 +118,7 @@ pub struct PendingRegistrationResponse {
pub struct RegisteredResponse {
pub request_id: u64,
pub reply_to: Recipient,
pub reply: RegistredData,
pub reply: RegisteredData,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -299,8 +299,8 @@ impl From<v2::registration::RegistrationData> for v3::registration::Registration
}
}
impl From<v3::registration::RegistredData> for v2::registration::RegistredData {
fn from(value: v3::registration::RegistredData) -> Self {
impl From<v3::registration::RegisteredData> for v2::registration::RegisteredData {
fn from(value: v3::registration::RegisteredData) -> Self {
Self {
pub_key: value.pub_key,
private_ip: value.private_ip,
@@ -309,8 +309,8 @@ impl From<v3::registration::RegistredData> for v2::registration::RegistredData {
}
}
impl From<v2::registration::RegistredData> for v3::registration::RegistredData {
fn from(value: v2::registration::RegistredData) -> Self {
impl From<v2::registration::RegisteredData> for v3::registration::RegisteredData {
fn from(value: v2::registration::RegisteredData) -> Self {
Self {
pub_key: value.pub_key,
private_ip: value.private_ip,
@@ -674,7 +674,7 @@ mod tests {
let pub_key = PeerPublicKey::new(PublicKey::from([0; 32]));
let private_ip = IpAddr::from_str("10.10.10.10").unwrap();
let wg_port = 51822;
let registred_data = v2::registration::RegistredData {
let registred_data = v2::registration::RegisteredData {
pub_key,
private_ip,
wg_port,
@@ -701,7 +701,7 @@ mod tests {
v3::response::AuthenticatorResponseData::Registered(v3::response::RegisteredResponse {
request_id,
reply_to,
reply: v3::registration::RegistredData {
reply: v3::registration::RegisteredData {
wg_port,
pub_key,
private_ip
@@ -715,7 +715,7 @@ mod tests {
let pub_key = PeerPublicKey::new(PublicKey::from([0; 32]));
let private_ip = IpAddr::from_str("10.10.10.10").unwrap();
let wg_port = 51822;
let registred_data = v3::registration::RegistredData {
let registred_data = v3::registration::RegisteredData {
pub_key,
private_ip,
wg_port,
@@ -742,7 +742,7 @@ mod tests {
v2::response::AuthenticatorResponseData::Registered(v2::response::RegisteredResponse {
request_id,
reply_to,
reply: v2::registration::RegistredData {
reply: v2::registration::RegisteredData {
wg_port,
pub_key,
private_ip
@@ -58,7 +58,7 @@ pub struct RegistrationData {
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RegistredData {
pub struct RegisteredData {
pub pub_key: PeerPublicKey,
pub private_ip: IpAddr,
pub wg_port: u16,
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData};
use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData};
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use nym_sphinx::addressing::Recipient;
use serde::{Deserialize, Serialize};
@@ -38,7 +38,7 @@ impl AuthenticatorResponse {
}
pub fn new_registered(
registred_data: RegistredData,
registred_data: RegisteredData,
reply_to: Recipient,
request_id: u64,
) -> Self {
@@ -139,7 +139,7 @@ pub struct PendingRegistrationResponse {
pub struct RegisteredResponse {
pub request_id: u64,
pub reply_to: Recipient,
pub reply: RegistredData,
pub reply: RegisteredData,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -262,8 +262,8 @@ impl From<v4::response::TopUpBandwidthResponse> for v3::response::TopUpBandwidth
}
}
impl From<v3::registration::RegistredData> for v4::registration::RegistredData {
fn from(value: v3::registration::RegistredData) -> Self {
impl From<v3::registration::RegisteredData> for v4::registration::RegisteredData {
fn from(value: v3::registration::RegisteredData) -> Self {
Self {
pub_key: value.pub_key,
private_ips: value.private_ip.into(),
@@ -272,8 +272,8 @@ impl From<v3::registration::RegistredData> for v4::registration::RegistredData {
}
}
impl From<v4::registration::RegistredData> for v3::registration::RegistredData {
fn from(value: v4::registration::RegistredData) -> Self {
impl From<v4::registration::RegisteredData> for v3::registration::RegisteredData {
fn from(value: v4::registration::RegisteredData) -> Self {
Self {
pub_key: value.pub_key,
private_ip: value.private_ips.ipv4.into(),
@@ -565,7 +565,7 @@ mod tests {
let private_ips =
v4::registration::IpPair::new(ipv4, Ipv6Addr::from_str("fc01::a0a").unwrap());
let wg_port = 51822;
let registred_data = v3::registration::RegistredData {
let registred_data = v3::registration::RegisteredData {
pub_key,
private_ip: ipv4.into(),
wg_port,
@@ -592,7 +592,7 @@ mod tests {
v4::response::AuthenticatorResponseData::Registered(v4::response::RegisteredResponse {
request_id,
reply_to,
reply: v4::registration::RegistredData {
reply: v4::registration::RegisteredData {
wg_port,
pub_key,
private_ips
@@ -608,7 +608,7 @@ mod tests {
let private_ips =
v4::registration::IpPair::new(ipv4, Ipv6Addr::from_str("fc01::10").unwrap());
let wg_port = 51822;
let registred_data = v4::registration::RegistredData {
let registred_data = v4::registration::RegisteredData {
pub_key,
private_ips,
wg_port,
@@ -635,7 +635,7 @@ mod tests {
v3::response::AuthenticatorResponseData::Registered(v3::response::RegisteredResponse {
request_id,
reply_to,
reply: v3::registration::RegistredData {
reply: v3::registration::RegisteredData {
wg_port,
pub_key,
private_ip: ipv4.into()
@@ -110,7 +110,7 @@ pub struct RegistrationData {
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RegistredData {
pub struct RegisteredData {
pub pub_key: PeerPublicKey,
pub private_ips: IpPair,
pub wg_port: u16,
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData};
use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData};
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use nym_sphinx::addressing::Recipient;
use serde::{Deserialize, Serialize};
@@ -38,7 +38,7 @@ impl AuthenticatorResponse {
}
pub fn new_registered(
registred_data: RegistredData,
registred_data: RegisteredData,
reply_to: Recipient,
request_id: u64,
) -> Self {
@@ -139,7 +139,7 @@ pub struct PendingRegistrationResponse {
pub struct RegisteredResponse {
pub request_id: u64,
pub reply_to: Recipient,
pub reply: RegistredData,
pub reply: RegisteredData,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -186,8 +186,8 @@ impl From<v4::response::TopUpBandwidthResponse> for v5::response::TopUpBandwidth
}
}
impl From<v4::registration::RegistredData> for v5::registration::RegistredData {
fn from(value: v4::registration::RegistredData) -> Self {
impl From<v4::registration::RegisteredData> for v5::registration::RegisteredData {
fn from(value: v4::registration::RegisteredData) -> Self {
Self {
pub_key: value.pub_key,
private_ips: value.private_ips.into(),
@@ -405,7 +405,7 @@ mod tests {
let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap();
let private_ips = v4::registration::IpPair::new(ipv4, ipv6);
let wg_port = 51822;
let registred_data = v4::registration::RegistredData {
let registred_data = v4::registration::RegisteredData {
pub_key,
private_ips,
wg_port,
@@ -431,7 +431,7 @@ mod tests {
upgraded_msg.data,
v5::response::AuthenticatorResponseData::Registered(v5::response::RegisteredResponse {
request_id,
reply: v5::registration::RegistredData {
reply: v5::registration::RegisteredData {
wg_port,
pub_key,
private_ips: v5::registration::IpPair::new(ipv4, ipv6)
@@ -108,7 +108,7 @@ pub struct RegistrationData {
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RegistredData {
pub struct RegisteredData {
pub pub_key: PeerPublicKey,
pub private_ips: IpPair,
pub wg_port: u16,
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData};
use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData};
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use serde::{Deserialize, Serialize};
@@ -32,7 +32,7 @@ impl AuthenticatorResponse {
}
}
pub fn new_registered(registred_data: RegistredData, request_id: u64) -> Self {
pub fn new_registered(registred_data: RegisteredData, request_id: u64) -> Self {
Self {
protocol: Protocol {
service_provider_type: ServiceProviderType::Authenticator,
@@ -116,7 +116,7 @@ pub struct PendingRegistrationResponse {
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct RegisteredResponse {
pub request_id: u64,
pub reply: RegistredData,
pub reply: RegisteredData,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
@@ -0,0 +1,441 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{v5, v6};
impl TryFrom<v5::request::AuthenticatorRequest> for v6::request::AuthenticatorRequest {
type Error = crate::Error;
fn try_from(
authenticator_request: v5::request::AuthenticatorRequest,
) -> Result<Self, Self::Error> {
Ok(Self {
protocol: v6::PROTOCOL,
data: authenticator_request.data.try_into()?,
request_id: authenticator_request.request_id,
})
}
}
impl TryFrom<v5::request::AuthenticatorRequestData> for v6::request::AuthenticatorRequestData {
type Error = crate::Error;
fn try_from(
authenticator_request_data: v5::request::AuthenticatorRequestData,
) -> Result<Self, Self::Error> {
match authenticator_request_data {
v5::request::AuthenticatorRequestData::Initial(init_msg) => Ok(
v6::request::AuthenticatorRequestData::Initial(init_msg.into()),
),
v5::request::AuthenticatorRequestData::Final(final_msg) => Ok(
v6::request::AuthenticatorRequestData::Final(Box::new((*final_msg).try_into()?)),
),
v5::request::AuthenticatorRequestData::QueryBandwidth(pub_key) => Ok(
v6::request::AuthenticatorRequestData::QueryBandwidth(pub_key),
),
v5::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => Ok(
v6::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message.into()),
),
}
}
}
impl From<v5::registration::InitMessage> for v6::registration::InitMessage {
fn from(init_msg: v5::registration::InitMessage) -> Self {
Self {
pub_key: init_msg.pub_key,
}
}
}
impl TryFrom<v5::registration::FinalMessage> for v6::registration::FinalMessage {
type Error = crate::Error;
fn try_from(final_msg: v5::registration::FinalMessage) -> Result<Self, Self::Error> {
Ok(Self {
gateway_client: final_msg.gateway_client.into(),
credential: final_msg
.credential
.map(TryInto::try_into)
.transpose()
.map_err(Self::Error::conversion_display)?,
})
}
}
impl From<v5::registration::GatewayClient> for v6::registration::GatewayClient {
fn from(gateway_client: v5::registration::GatewayClient) -> Self {
Self {
pub_key: gateway_client.pub_key,
private_ips: gateway_client.private_ips.into(),
mac: gateway_client.mac.into(),
}
}
}
impl From<v6::registration::GatewayClient> for v5::registration::GatewayClient {
fn from(gateway_client: v6::registration::GatewayClient) -> Self {
Self {
pub_key: gateway_client.pub_key,
private_ips: gateway_client.private_ips.into(),
mac: gateway_client.mac.into(),
}
}
}
impl From<v5::registration::ClientMac> for v6::registration::ClientMac {
fn from(client_mac: v5::registration::ClientMac) -> Self {
Self::new((*client_mac).clone())
}
}
impl From<v6::registration::ClientMac> for v5::registration::ClientMac {
fn from(client_mac: v6::registration::ClientMac) -> Self {
Self::new((*client_mac).clone())
}
}
impl From<Box<v5::topup::TopUpMessage>> for Box<v6::topup::TopUpMessage> {
fn from(top_up_message: Box<v5::topup::TopUpMessage>) -> Self {
Box::new(v6::topup::TopUpMessage {
pub_key: top_up_message.pub_key,
credential: top_up_message.credential,
})
}
}
impl From<v5::response::AuthenticatorResponse> for v6::response::AuthenticatorResponse {
fn from(value: v5::response::AuthenticatorResponse) -> Self {
Self {
protocol: v6::PROTOCOL,
data: value.data.into(),
}
}
}
impl From<v5::response::AuthenticatorResponseData> for v6::response::AuthenticatorResponseData {
fn from(authenticator_response_data: v5::response::AuthenticatorResponseData) -> Self {
match authenticator_response_data {
v5::response::AuthenticatorResponseData::PendingRegistration(pending_response) => {
v6::response::AuthenticatorResponseData::PendingRegistration(
pending_response.into(),
)
}
v5::response::AuthenticatorResponseData::Registered(registered_response) => {
v6::response::AuthenticatorResponseData::Registered(registered_response.into())
}
v5::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response,
) => v6::response::AuthenticatorResponseData::RemainingBandwidth(
remaining_bandwidth_response.into(),
),
v5::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response) => {
v6::response::AuthenticatorResponseData::TopUpBandwidth(top_up_response.into())
}
}
}
}
impl From<v5::response::RegisteredResponse> for v6::response::RegisteredResponse {
fn from(value: v5::response::RegisteredResponse) -> Self {
Self {
request_id: value.request_id,
reply: value.reply.into(),
upgrade_mode_enabled: false,
}
}
}
impl From<v5::response::PendingRegistrationResponse> for v6::response::PendingRegistrationResponse {
fn from(value: v5::response::PendingRegistrationResponse) -> Self {
Self {
request_id: value.request_id,
reply: value.reply.into(),
upgrade_mode_enabled: false,
}
}
}
impl From<v5::registration::RegistrationData> for v6::registration::RegistrationData {
fn from(value: v5::registration::RegistrationData) -> Self {
Self {
nonce: value.nonce,
gateway_data: value.gateway_data.into(),
wg_port: value.wg_port,
}
}
}
impl From<v6::registration::RegistrationData> for v5::registration::RegistrationData {
fn from(value: v6::registration::RegistrationData) -> Self {
Self {
nonce: value.nonce,
gateway_data: value.gateway_data.into(),
wg_port: value.wg_port,
}
}
}
impl From<v5::response::RemainingBandwidthResponse> for v6::response::RemainingBandwidthResponse {
fn from(value: v5::response::RemainingBandwidthResponse) -> Self {
Self {
request_id: value.request_id,
reply: value.reply.map(Into::into),
upgrade_mode_enabled: false,
}
}
}
impl From<v5::response::TopUpBandwidthResponse> for v6::response::TopUpBandwidthResponse {
fn from(value: v5::response::TopUpBandwidthResponse) -> Self {
Self {
request_id: value.request_id,
reply: value.reply.into(),
upgrade_mode_enabled: false,
}
}
}
impl From<v5::registration::RegisteredData> for v6::registration::RegisteredData {
fn from(value: v5::registration::RegisteredData) -> Self {
Self {
pub_key: value.pub_key,
private_ips: value.private_ips.into(),
wg_port: value.wg_port,
}
}
}
impl From<v5::registration::RemainingBandwidthData> for v6::registration::RemainingBandwidthData {
fn from(value: v5::registration::RemainingBandwidthData) -> Self {
Self {
available_bandwidth: value.available_bandwidth,
}
}
}
impl From<v5::registration::IpPair> for v6::registration::IpPair {
fn from(value: v5::registration::IpPair) -> Self {
Self {
ipv4: value.ipv4,
ipv6: value.ipv6,
}
}
}
impl From<v6::registration::IpPair> for v5::registration::IpPair {
fn from(value: v6::registration::IpPair) -> Self {
Self {
ipv4: value.ipv4,
ipv6: value.ipv6,
}
}
}
#[cfg(test)]
mod tests {
use std::{
net::{Ipv4Addr, Ipv6Addr},
str::FromStr,
};
use nym_credentials_interface::{BandwidthCredential, CredentialSpendingData, TicketType};
use nym_crypto::asymmetric::x25519::PrivateKey;
use nym_wireguard_types::PeerPublicKey;
use x25519_dalek::PublicKey;
use super::*;
use crate::models::BandwidthClaim;
use crate::{util::tests::CREDENTIAL_BYTES, v5};
#[test]
fn upgrade_initial_req() {
let pub_key = PeerPublicKey::new(PublicKey::from([0; 32]));
let (msg, _) = v5::request::AuthenticatorRequest::new_initial_request(
v5::registration::InitMessage::new(pub_key),
);
let upgraded_msg = v6::request::AuthenticatorRequest::try_from(msg).unwrap();
assert_eq!(upgraded_msg.protocol, v6::PROTOCOL);
assert_eq!(
upgraded_msg.data,
v6::request::AuthenticatorRequestData::Initial(v6::registration::InitMessage {
pub_key
})
);
}
#[test]
fn upgrade_final_req() {
let mut rng = rand::thread_rng();
let local_secret = PrivateKey::new(&mut rng);
let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng);
let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap();
let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap();
let ips = v5::registration::IpPair::new(ipv4, ipv6);
let nonce = 42;
let gateway_client = v5::registration::GatewayClient::new(
&local_secret,
(&remote_secret).into(),
ips,
nonce,
);
let credential = CredentialSpendingData::try_from_bytes(&CREDENTIAL_BYTES).unwrap();
let final_message = v5::registration::FinalMessage {
gateway_client: gateway_client.clone(),
credential: Some(credential.clone()),
};
let (msg, _) = v5::request::AuthenticatorRequest::new_final_request(final_message);
let upgraded_msg = v6::request::AuthenticatorRequest::try_from(msg).unwrap();
assert_eq!(upgraded_msg.protocol, v6::PROTOCOL);
assert_eq!(
upgraded_msg.data,
v6::request::AuthenticatorRequestData::Final(Box::new(
v6::registration::FinalMessage {
gateway_client: v6::registration::GatewayClient::new(
&local_secret,
(&remote_secret).into(),
v6::registration::IpPair::new(ipv4, ipv6),
nonce
),
credential: Some(BandwidthClaim {
credential: BandwidthCredential::ZkNym(Box::new(credential)),
kind: TicketType::V1MixnetEntry,
})
}
))
);
}
#[test]
fn upgrade_query_req() {
let pub_key = PeerPublicKey::new(PublicKey::from([0; 32]));
let (msg, _) = v5::request::AuthenticatorRequest::new_query_request(pub_key);
let upgraded_msg = v6::request::AuthenticatorRequest::try_from(msg).unwrap();
assert_eq!(upgraded_msg.protocol, v6::PROTOCOL);
assert_eq!(
upgraded_msg.data,
v6::request::AuthenticatorRequestData::QueryBandwidth(pub_key)
);
}
#[test]
fn upgrade_pending_reg_resp() {
let mut rng = rand::thread_rng();
let local_secret = PrivateKey::new(&mut rng);
let remote_secret = x25519_dalek::StaticSecret::random_from_rng(&mut rng);
let ipv4 = Ipv4Addr::from_str("10.10.10.10").unwrap();
let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap();
let ips = v5::registration::IpPair::new(ipv4, ipv6);
let nonce = 42;
let wg_port = 51822;
let gateway_data = v5::registration::GatewayClient::new(
&local_secret,
(&remote_secret).into(),
ips,
nonce,
);
let registration_data = v5::registration::RegistrationData {
nonce,
gateway_data,
wg_port,
};
let request_id = 123;
let msg = v5::response::AuthenticatorResponse::new_pending_registration_success(
registration_data,
request_id,
);
let upgraded_msg = v6::response::AuthenticatorResponse::from(msg);
assert_eq!(upgraded_msg.protocol, v6::PROTOCOL);
assert_eq!(
upgraded_msg.data,
v6::response::AuthenticatorResponseData::PendingRegistration(
v6::response::PendingRegistrationResponse {
request_id,
reply: v6::registration::RegistrationData {
nonce,
gateway_data: v6::registration::GatewayClient::new(
&local_secret,
(&remote_secret).into(),
v6::registration::IpPair::new(ipv4, ipv6),
nonce
),
wg_port
},
upgrade_mode_enabled: false,
}
)
);
}
#[test]
fn upgrade_registered_resp() {
let pub_key = PeerPublicKey::new(PublicKey::from([0; 32]));
let ipv4 = Ipv4Addr::from_str("10.1.10.10").unwrap();
let ipv6 = Ipv6Addr::from_str("fc01::a0a").unwrap();
let private_ips = v5::registration::IpPair::new(ipv4, ipv6);
let wg_port = 51822;
let registered_data = v5::registration::RegisteredData {
pub_key,
private_ips,
wg_port,
};
let request_id = 123;
let msg = v5::response::AuthenticatorResponse::new_registered(registered_data, request_id);
let upgraded_msg = v6::response::AuthenticatorResponse::from(msg);
assert_eq!(upgraded_msg.protocol, v6::PROTOCOL);
assert_eq!(
upgraded_msg.data,
v6::response::AuthenticatorResponseData::Registered(v6::response::RegisteredResponse {
request_id,
reply: v6::registration::RegisteredData {
wg_port,
pub_key,
private_ips: v6::registration::IpPair::new(ipv4, ipv6)
},
upgrade_mode_enabled: false,
})
);
}
#[test]
fn upgrade_remaining_bandwidth_resp() {
let available_bandwidth = 42;
let remaining_bandwidth_data = Some(v5::registration::RemainingBandwidthData {
available_bandwidth,
});
let request_id = 123;
let msg = v5::response::AuthenticatorResponse::new_remaining_bandwidth(
remaining_bandwidth_data,
request_id,
);
let upgraded_msg = v6::response::AuthenticatorResponse::from(msg);
assert_eq!(upgraded_msg.protocol, v6::PROTOCOL);
assert_eq!(
upgraded_msg.data,
v6::response::AuthenticatorResponseData::RemainingBandwidth(
v6::response::RemainingBandwidthResponse {
request_id,
reply: Some(v6::registration::RemainingBandwidthData {
available_bandwidth,
}),
upgrade_mode_enabled: false,
}
)
);
}
}
@@ -0,0 +1,15 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
pub mod conversion;
pub mod registration;
pub mod request;
pub mod response;
pub mod topup;
pub mod upgrade_mode_check;
pub const VERSION: u8 = 6;
pub const PROTOCOL: Protocol = Protocol::new(VERSION, ServiceProviderType::Authenticator);
@@ -0,0 +1,287 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::Error;
use crate::models::BandwidthClaim;
use base64::{Engine, engine::general_purpose};
use nym_network_defaults::constants::{WG_TUN_DEVICE_IP_ADDRESS_V4, WG_TUN_DEVICE_IP_ADDRESS_V6};
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::time::SystemTime;
use std::{fmt, ops::Deref, str::FromStr};
#[cfg(feature = "verify")]
use hmac::{Hmac, Mac};
#[cfg(feature = "verify")]
use nym_crypto::asymmetric::x25519::{PrivateKey, PublicKey};
#[cfg(feature = "verify")]
use sha2::Sha256;
pub type PendingRegistrations = HashMap<PeerPublicKey, RegistrationData>;
pub type PrivateIPs = HashMap<IpPair, Taken>;
#[cfg(feature = "verify")]
pub type HmacSha256 = Hmac<Sha256>;
pub type Nonce = u64;
pub type Taken = Option<SystemTime>;
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IpPair {
pub ipv4: Ipv4Addr,
pub ipv6: Ipv6Addr,
}
impl IpPair {
pub fn new(ipv4: Ipv4Addr, ipv6: Ipv6Addr) -> Self {
IpPair { ipv4, ipv6 }
}
}
impl From<(Ipv4Addr, Ipv6Addr)> for IpPair {
fn from((ipv4, ipv6): (Ipv4Addr, Ipv6Addr)) -> Self {
IpPair { ipv4, ipv6 }
}
}
impl fmt::Display for IpPair {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "({}, {})", self.ipv4, self.ipv6)
}
}
impl From<IpAddr> for IpPair {
fn from(value: IpAddr) -> Self {
let (before_last_byte, last_byte) = match value {
IpAddr::V4(ipv4_addr) => (ipv4_addr.octets()[2], ipv4_addr.octets()[3]),
IpAddr::V6(ipv6_addr) => (ipv6_addr.octets()[14], ipv6_addr.octets()[15]),
};
let last_bytes = ((before_last_byte as u16) << 8) | last_byte as u16;
let ipv4 = Ipv4Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[0],
WG_TUN_DEVICE_IP_ADDRESS_V4.octets()[1],
before_last_byte,
last_byte,
);
let ipv6 = Ipv6Addr::new(
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[0],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[1],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[2],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[3],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[4],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[5],
WG_TUN_DEVICE_IP_ADDRESS_V6.segments()[6],
last_bytes,
);
IpPair::new(ipv4, ipv6)
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct InitMessage {
/// Base64 encoded x25519 public key
pub pub_key: PeerPublicKey,
}
impl InitMessage {
pub fn new(pub_key: PeerPublicKey) -> Self {
InitMessage { pub_key }
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct FinalMessage {
/// Gateway client data
pub gateway_client: GatewayClient,
/// Ecash credential
pub credential: Option<BandwidthClaim>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RegistrationData {
pub nonce: u64,
pub gateway_data: GatewayClient,
pub wg_port: u16,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RegisteredData {
pub pub_key: PeerPublicKey,
pub private_ips: IpPair,
pub wg_port: u16,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct RemainingBandwidthData {
pub available_bandwidth: i64,
}
/// Client that wants to register sends its PublicKey bytes mac digest encrypted with a DH shared secret.
/// Gateway/Nym node can then verify pub_key payload using the same process
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct GatewayClient {
/// Base64 encoded x25519 public key
pub pub_key: PeerPublicKey,
/// Assigned private IPs (v4 and v6)
pub private_ips: IpPair,
/// Sha256 hmac on the data (alongside the prior nonce)
pub mac: ClientMac,
}
impl GatewayClient {
#[cfg(feature = "verify")]
pub fn new(
local_secret: &PrivateKey,
remote_public: x25519_dalek::PublicKey,
private_ips: IpPair,
nonce: u64,
) -> Self {
let local_public = PublicKey::from(local_secret);
let remote_public = PublicKey::from(remote_public);
let dh = local_secret.diffie_hellman(&remote_public);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
let mut mac = HmacSha256::new_from_slice(&dh[..])
.expect("x25519 shared secret is always 32 bytes long");
mac.update(local_public.as_bytes());
mac.update(private_ips.to_string().as_bytes());
mac.update(&nonce.to_le_bytes());
GatewayClient {
pub_key: PeerPublicKey::new(local_public.into()),
private_ips,
mac: ClientMac(mac.finalize().into_bytes().to_vec()),
}
}
// Reusable secret should be gateways Wireguard PK
// Client should perform this step when generating its payload, using its own WG PK
#[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
let mut mac = HmacSha256::new_from_slice(dh.as_bytes())
.expect("x25519 shared secret is always 32 bytes long");
mac.update(self.pub_key.as_bytes());
mac.update(self.private_ips.to_string().as_bytes());
mac.update(&nonce.to_le_bytes());
mac.verify_slice(&self.mac)
.map_err(|source| Error::FailedClientMacVerification {
client: self.pub_key.to_string(),
source,
})
}
pub fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
}
// TODO: change the inner type into generic array of size HmacSha256::OutputSize
// TODO2: rely on our internal crypto/hmac
#[derive(Debug, Clone, PartialEq)]
pub struct ClientMac(Vec<u8>);
impl fmt::Display for ClientMac {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", general_purpose::STANDARD.encode(&self.0))
}
}
impl From<Vec<u8>> for ClientMac {
fn from(v: Vec<u8>) -> Self {
ClientMac(v)
}
}
impl ClientMac {
#[allow(dead_code)]
pub fn new(mac: Vec<u8>) -> Self {
ClientMac(mac)
}
}
impl Deref for ClientMac {
type Target = Vec<u8>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromStr for ClientMac {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mac_bytes: Vec<u8> =
general_purpose::STANDARD
.decode(s)
.map_err(|source| Error::MalformedClientMac {
mac: s.to_string(),
source,
})?;
Ok(ClientMac(mac_bytes))
}
}
impl Serialize for ClientMac {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let encoded_key = general_purpose::STANDARD.encode(self.0.clone());
serializer.serialize_str(&encoded_key)
}
}
impl<'de> Deserialize<'de> for ClientMac {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let encoded_key = String::deserialize(deserializer)?;
ClientMac::from_str(&encoded_key).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
use nym_crypto::asymmetric::x25519;
use nym_test_utils::helpers::deterministic_rng;
#[test]
fn create_ip_pair() {
let ipv4: IpAddr = Ipv4Addr::from_str("10.1.10.50").unwrap().into();
let ipv6: IpAddr = Ipv6Addr::from_str("fc01::0a32").unwrap().into();
assert_eq!(IpPair::from(ipv4), IpPair::from(ipv6));
}
#[test]
#[cfg(feature = "verify")]
fn client_request_roundtrip() {
let mut rng = deterministic_rng();
let gateway_key_pair = x25519::KeyPair::new(&mut rng);
let client_key_pair = x25519::KeyPair::new(&mut rng);
let nonce = 1234567890;
let client = GatewayClient::new(
client_key_pair.private_key(),
x25519_dalek::PublicKey::from(gateway_key_pair.public_key().to_bytes()),
IpPair::new("10.0.0.42".parse().unwrap(), "fc00::42".parse().unwrap()),
nonce,
);
assert!(client.verify(gateway_key_pair.private_key(), nonce).is_ok())
}
}
@@ -0,0 +1,135 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::{
PROTOCOL,
registration::{FinalMessage, InitMessage},
topup::TopUpMessage,
upgrade_mode_check::UpgradeModeCheckRequest,
};
use nym_service_provider_requests_common::Protocol;
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
use crate::make_bincode_serializer;
fn generate_random() -> u64 {
use rand::RngCore;
let mut rng = rand::rngs::OsRng;
rng.next_u64()
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct AuthenticatorRequest {
pub protocol: Protocol,
pub data: AuthenticatorRequestData,
pub request_id: u64,
}
impl AuthenticatorRequest {
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(&message.message)
}
pub fn new_initial_request(init_message: InitMessage) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: PROTOCOL,
data: AuthenticatorRequestData::Initial(init_message),
request_id,
},
request_id,
)
}
pub fn new_final_request(final_message: FinalMessage) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: PROTOCOL,
data: AuthenticatorRequestData::Final(Box::new(final_message)),
request_id,
},
request_id,
)
}
pub fn new_query_request(peer_public_key: PeerPublicKey) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: PROTOCOL,
data: AuthenticatorRequestData::QueryBandwidth(peer_public_key),
request_id,
},
request_id,
)
}
pub fn new_topup_request(top_up_message: TopUpMessage) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: PROTOCOL,
data: AuthenticatorRequestData::TopUpBandwidth(Box::new(top_up_message)),
request_id,
},
request_id,
)
}
pub fn new_upgrade_mode_check_request(message: UpgradeModeCheckRequest) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: PROTOCOL,
data: AuthenticatorRequestData::CheckUpgradeMode(message),
request_id,
},
request_id,
)
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum AuthenticatorRequestData {
Initial(InitMessage),
Final(Box<FinalMessage>),
QueryBandwidth(PeerPublicKey),
TopUpBandwidth(Box<TopUpMessage>),
CheckUpgradeMode(UpgradeModeCheckRequest),
}
#[cfg(test)]
mod tests {
use super::super::VERSION;
use super::*;
use nym_service_provider_requests_common::ServiceProviderType;
use std::str::FromStr;
#[test]
fn check_first_bytes_protocol() {
let version = VERSION;
let data = AuthenticatorRequest {
protocol: Protocol {
version,
service_provider_type: ServiceProviderType::Authenticator,
},
data: AuthenticatorRequestData::Initial(InitMessage::new(
PeerPublicKey::from_str("yvNUDpT5l7W/xDhiu6HkqTHDQwbs/B3J5UrLmORl1EQ=").unwrap(),
)),
request_id: 1,
};
let bytes = *data.to_bytes().unwrap().first_chunk::<2>().unwrap();
assert_eq!(bytes, [version, ServiceProviderType::Authenticator as u8]);
}
}
@@ -0,0 +1,153 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{RegisteredData, RegistrationData, RemainingBandwidthData};
use nym_service_provider_requests_common::Protocol;
use serde::{Deserialize, Serialize};
use crate::make_bincode_serializer;
use super::PROTOCOL;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct AuthenticatorResponse {
pub protocol: Protocol,
pub data: AuthenticatorResponseData,
}
impl AuthenticatorResponse {
pub fn new_pending_registration_success(
registration_data: RegistrationData,
request_id: u64,
upgrade_mode_enabled: bool,
) -> Self {
Self {
protocol: PROTOCOL,
data: AuthenticatorResponseData::PendingRegistration(PendingRegistrationResponse {
reply: registration_data,
request_id,
upgrade_mode_enabled,
}),
}
}
pub fn new_registered(
registered_data: RegisteredData,
request_id: u64,
upgrade_mode_enabled: bool,
) -> Self {
Self {
protocol: PROTOCOL,
data: AuthenticatorResponseData::Registered(RegisteredResponse {
reply: registered_data,
request_id,
upgrade_mode_enabled,
}),
}
}
pub fn new_remaining_bandwidth(
remaining_bandwidth_data: Option<RemainingBandwidthData>,
request_id: u64,
upgrade_mode_enabled: bool,
) -> Self {
Self {
protocol: PROTOCOL,
data: AuthenticatorResponseData::RemainingBandwidth(RemainingBandwidthResponse {
reply: remaining_bandwidth_data,
request_id,
upgrade_mode_enabled,
}),
}
}
pub fn new_topup_bandwidth(
remaining_bandwidth_data: RemainingBandwidthData,
request_id: u64,
upgrade_mode_enabled: bool,
) -> Self {
Self {
protocol: PROTOCOL,
data: AuthenticatorResponseData::TopUpBandwidth(TopUpBandwidthResponse {
reply: remaining_bandwidth_data,
request_id,
upgrade_mode_enabled,
}),
}
}
pub fn new_upgrade_mode_check(request_id: u64, upgrade_mode_enabled: bool) -> Self {
Self {
protocol: PROTOCOL,
data: AuthenticatorResponseData::UpgradeMode(UpgradeModeResponse {
request_id,
upgrade_mode_enabled,
}),
}
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
}
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(&message.message)
}
pub fn id(&self) -> Option<u64> {
match &self.data {
AuthenticatorResponseData::PendingRegistration(response) => Some(response.request_id),
AuthenticatorResponseData::Registered(response) => Some(response.request_id),
AuthenticatorResponseData::RemainingBandwidth(response) => Some(response.request_id),
AuthenticatorResponseData::TopUpBandwidth(response) => Some(response.request_id),
AuthenticatorResponseData::UpgradeMode(response) => Some(response.request_id),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum AuthenticatorResponseData {
PendingRegistration(PendingRegistrationResponse),
Registered(RegisteredResponse),
RemainingBandwidth(RemainingBandwidthResponse),
TopUpBandwidth(TopUpBandwidthResponse),
UpgradeMode(UpgradeModeResponse),
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct PendingRegistrationResponse {
pub request_id: u64,
pub reply: RegistrationData,
pub upgrade_mode_enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct RegisteredResponse {
pub request_id: u64,
pub reply: RegisteredData,
pub upgrade_mode_enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct RemainingBandwidthResponse {
pub request_id: u64,
pub reply: Option<RemainingBandwidthData>,
pub upgrade_mode_enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct TopUpBandwidthResponse {
pub request_id: u64,
pub reply: RemainingBandwidthData,
pub upgrade_mode_enabled: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct UpgradeModeResponse {
pub request_id: u64,
pub upgrade_mode_enabled: bool,
}
@@ -0,0 +1,15 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_credentials_interface::CredentialSpendingData;
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct TopUpMessage {
/// Base64 encoded x25519 public key
pub pub_key: PeerPublicKey,
/// Ecash credential
pub credential: CredentialSpendingData,
}
@@ -0,0 +1,12 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum UpgradeModeCheckRequest {
/// Attempt to request upgrade mode recheck via the JWT issued as the result of
/// global attestation.json being published
UpgradeModeJwt { token: String },
}
+21 -5
View File
@@ -1,7 +1,7 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::{v1, v2, v3, v4, v5};
use super::{v1, v2, v3, v4, v5, v6};
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
#[derive(Copy, Clone, Debug, PartialEq, strum_macros::Display)]
@@ -22,11 +22,15 @@ pub enum AuthenticatorVersion {
/// introduced in dorina-patched release (1.6.1)
V5,
/// introduced in niolo release (1.23.0)
V6,
/// an unknown, future, variant that can be present if running outdated software
UNKNOWN,
}
impl AuthenticatorVersion {
pub const LATEST: Self = Self::V5;
pub const LATEST: Self = Self::V6;
pub const fn release_version(&self) -> semver::Version {
match self {
@@ -35,6 +39,7 @@ impl AuthenticatorVersion {
AuthenticatorVersion::V3 => semver::Version::new(1, 1, 10),
AuthenticatorVersion::V4 => semver::Version::new(1, 2, 0),
AuthenticatorVersion::V5 => semver::Version::new(1, 6, 1),
AuthenticatorVersion::V6 => semver::Version::new(1, 23, 0),
AuthenticatorVersion::UNKNOWN => semver::Version::new(0, 0, 0),
}
}
@@ -54,6 +59,8 @@ impl From<Protocol> for AuthenticatorVersion {
AuthenticatorVersion::V4
} else if value.version == v5::VERSION {
AuthenticatorVersion::V5
} else if value.version == v6::VERSION {
AuthenticatorVersion::V6
} else {
AuthenticatorVersion::UNKNOWN
}
@@ -72,6 +79,8 @@ impl From<u8> for AuthenticatorVersion {
AuthenticatorVersion::V4
} else if value == v5::VERSION {
AuthenticatorVersion::V5
} else if value == v6::VERSION {
AuthenticatorVersion::V6
} else {
AuthenticatorVersion::UNKNOWN
}
@@ -126,11 +135,14 @@ impl From<semver::Version> for AuthenticatorVersion {
if semver < AuthenticatorVersion::V5.release_version() {
return Self::V4;
}
// if provided version is higher (or equal) to release version of V5,
// we return the latest (i.e. v5)
if semver < AuthenticatorVersion::V6.release_version() {
return Self::V5;
}
// if provided version is higher (or equal) to release version of V6,
// we return the latest (i.e. v6)
debug_assert_eq!(
Self::V5,
Self::V6,
Self::LATEST,
"a new AuthenticatorVersion variant has been introduced without adjusting the `From<semver::Version>` trait"
);
@@ -191,5 +203,9 @@ mod tests {
assert_eq!(AuthenticatorVersion::V5, "1.7.0".into());
assert_eq!(AuthenticatorVersion::V5, "1.16.11".into());
assert_eq!(AuthenticatorVersion::V5, "1.17.0".into());
assert_eq!(AuthenticatorVersion::V5, "1.22.0".into());
assert_eq!(AuthenticatorVersion::V6, "1.23.0".into());
assert_eq!(AuthenticatorVersion::V6, "1.23.1".into());
assert_eq!(AuthenticatorVersion::V6, "1.24.0".into());
}
}
+1 -6
View File
@@ -7,21 +7,16 @@ license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = { workspace = true }
bip39 = { workspace = true }
async-trait = { workspace = true }
log = { workspace = true }
rand = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
zeroize = { workspace = true }
nym-credential-storage = { path = "../credential-storage" }
nym-credentials = { path = "../credentials" }
nym-credentials-interface = { path = "../credentials-interface" }
nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "stream_cipher", "aes", "hashing"] }
nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" }
nym-ecash-time = { path = "../ecash-time" }
nym-network-defaults = { path = "../network-defaults" }
nym-task = { path = "../task" }
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
+3
View File
@@ -21,6 +21,9 @@ pub enum BandwidthControllerError {
#[error("There was a credential storage error - {0}")]
CredentialStorageError(Box<dyn std::error::Error + Send + Sync>),
#[error("retrieved upgrade mode token is not a valid String")]
MalformedUpgradeModeToken,
#[error("the credential storage does not contain any usable credentials")]
NoCredentialsAvailable,
+14 -1
View File
@@ -12,7 +12,7 @@ use crate::utils::{
ApiClientsWrapper,
};
use log::error;
use nym_credential_storage::models::RetrievedTicketbook;
use nym_credential_storage::models::{EmergencyCredential, RetrievedTicketbook};
use nym_credential_storage::storage::Storage;
use nym_credentials::ecash::bandwidth::CredentialSpendingData;
use nym_credentials_interface::{
@@ -220,6 +220,19 @@ impl<C, St: Storage> BandwidthController<C, St> {
}
}
}
pub async fn get_emergency_credential(
&self,
typ: &str,
) -> Result<Option<EmergencyCredential>, BandwidthControllerError>
where
<St as Storage>::StorageError: Send + Sync + 'static,
{
self.storage
.get_emergency_credential(typ)
.await
.map_err(BandwidthControllerError::credential_storage_error)
}
}
impl<C, St> Clone for BandwidthController<C, St>
+17
View File
@@ -11,6 +11,9 @@ use crate::{error::BandwidthControllerError, BandwidthController, PreparedCreden
pub const DEFAULT_TICKETS_TO_SPEND: u32 = 1;
// TODO: this does not really belong here
pub const UPGRADE_MODE_JWT_TYPE: &str = "UPGRADE_MODE_JWT";
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait BandwidthTicketProvider: Send + Sync {
@@ -20,6 +23,8 @@ pub trait BandwidthTicketProvider: Send + Sync {
gateway_id: ed25519::PublicKey,
tickets_to_spend: u32,
) -> Result<PreparedCredential, BandwidthControllerError>;
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError>;
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -39,4 +44,16 @@ where
self.prepare_ecash_ticket(ticket_type, gateway_id.to_bytes(), tickets_to_spend)
.await
}
async fn get_upgrade_mode_token(&self) -> Result<Option<String>, BandwidthControllerError> {
let Some(emergency_credential) =
self.get_emergency_credential(UPGRADE_MODE_JWT_TYPE).await?
else {
return Ok(None);
};
// upgrade mode credential is just a simple stringified JWT
let token = String::from_utf8(emergency_credential.data.content)
.map_err(|_| BandwidthControllerError::MalformedUpgradeModeToken)?;
Ok(Some(token))
}
}
+3 -1
View File
@@ -29,6 +29,8 @@ time = { workspace = true }
tokio = { workspace = true, features = ["sync", "macros"] }
tracing = { workspace = true }
zeroize = { workspace = true }
once_cell = "1.19"
# internal
nym-id = { path = "../nym-id" }
@@ -36,7 +38,7 @@ nym-bandwidth-controller = { path = "../bandwidth-controller" }
nym-crypto = { path = "../crypto" }
nym-gateway-client = { path = "../client-libs/gateway-client" }
nym-gateway-requests = { path = "../gateway-requests" }
nym-http-api-client = { path = "../http-api-client" }
nym-http-api-client = { path = "../http-api-client", features = ["network-defaults"] }
nym-nonexhaustive-delayqueue = { path = "../nonexhaustive-delayqueue" }
nym-sphinx = { path = "../nymsphinx" }
nym-statistics-common = { path = "../statistics" }
@@ -81,6 +81,10 @@ pub struct CommonClientInitArgs {
#[cfg_attr(feature = "cli", clap(long, hide = true))]
pub enabled_credentials_mode: Option<bool>,
/// Change the default minimum node performance used during initial node selection filtering.
#[cfg_attr(feature = "cli", clap(long, hide = true))]
pub minimum_gateway_performance: Option<u8>,
/// Mostly debug-related option to increase default traffic rate so that you would not need to
/// modify config post init
#[cfg_attr(feature = "cli", clap(long, hide = true))]
@@ -173,10 +177,14 @@ where
})?;
hardcoded_topology.entry_capable_nodes().cloned().collect()
} else {
let minimum_performance = common_args
.minimum_gateway_performance
.unwrap_or(core.debug.topology.minimum_gateway_performance);
crate::init::helpers::gateways_for_init(
&core.client.nym_api_urls,
user_agent,
core.debug.topology.minimum_gateway_performance,
minimum_performance,
core.debug.topology.ignore_ingress_epoch_role,
None,
)
+134 -16
View File
@@ -73,6 +73,10 @@ use url::Url;
#[cfg(debug_assertions)]
use wasm_utils::console_log;
/// Default number of retries for Nym API requests when using network details with domain fronting.
/// This allows the client to try alternative URLs if the primary endpoint is unavailable.
const DEFAULT_NYM_API_RETRIES: usize = 3;
#[cfg(all(
not(target_arch = "wasm32"),
feature = "fs-surb-storage",
@@ -212,6 +216,9 @@ pub struct BaseClientBuilder<C, S: MixnetClientStorage> {
client_store: S,
dkg_query_client: Option<C>,
// Optional API URLs for domain fronting support
nym_api_urls: Option<Vec<nym_network_defaults::ApiUrl>>,
wait_for_gateway: bool,
custom_topology_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
custom_gateway_transceiver: Option<Box<dyn GatewayTransceiver + Send>>,
@@ -241,6 +248,7 @@ where
config: base_config,
client_store,
dkg_query_client,
nym_api_urls: None,
wait_for_gateway: false,
custom_topology_provider: None,
custom_gateway_transceiver: None,
@@ -263,6 +271,16 @@ where
self
}
/// Set Nym API URLs for domain fronting support.
///
/// When provided, the client will use these API URLs (which include front_hosts)
/// to construct HTTP clients with domain fronting enabled.
#[must_use]
pub fn with_nym_api_urls(mut self, nym_api_urls: Vec<nym_network_defaults::ApiUrl>) -> Self {
self.nym_api_urls = Some(nym_api_urls);
self
}
#[must_use]
pub fn with_forget_me(mut self, forget_me: &ForgetMe) -> Self {
self.config.debug.forget_me = *forget_me;
@@ -783,7 +801,7 @@ where
event_tx,
);
let mix_tx = mix_traffic_controller.mix_rx();
let mix_tx = mix_traffic_controller.mix_tx();
let client_tx = mix_traffic_controller.client_tx();
shutdown_tracker.try_spawn_named(
@@ -863,21 +881,67 @@ where
}
fn construct_nym_api_client(
nym_api_urls: Option<&Vec<nym_network_defaults::ApiUrl>>,
config: &Config,
user_agent: Option<UserAgent>,
) -> Result<nym_http_api_client::Client, ClientCoreError> {
tracing::debug!(
"construct_nym_api_client called with nym_api_urls: {}",
nym_api_urls.is_some()
);
// If API URLs are provided, use new_with_fronted_urls() which handles domain fronting
if let Some(nym_api_urls) = nym_api_urls {
if nym_api_urls.is_empty() {
tracing::warn!("Provided nym_api_urls is empty, falling back to config endpoints");
} else {
tracing::info!(
"Building nym-api client from provided URLs (with domain fronting support): {} URLs",
nym_api_urls.len()
);
let mut builder =
nym_http_api_client::ClientBuilder::new_with_fronted_urls(nym_api_urls.clone())
.map_err(ClientCoreError::from)?
.with_retries(DEFAULT_NYM_API_RETRIES);
if let Some(user_agent) = user_agent {
builder = builder.with_user_agent(user_agent);
}
return builder.build().map_err(ClientCoreError::from);
}
}
// Fallback to basic client for backwards compatibility
tracing::debug!("Building basic nym-api HTTP client from config endpoints");
let mut nym_api_urls = config.get_nym_api_endpoints();
if nym_api_urls.is_empty() {
tracing::warn!("No API endpoints configured in config, this may cause issues");
}
nym_api_urls.shuffle(&mut thread_rng());
let mut builder = nym_http_api_client::Client::builder(nym_api_urls[0].clone())
.map_err(ClientCoreError::from)?;
// Convert config URLs to ApiUrl format for consistency
let api_urls: Vec<nym_network_defaults::ApiUrl> = nym_api_urls
.into_iter()
.map(|url| nym_network_defaults::ApiUrl {
url: url.to_string(),
front_hosts: None,
})
.collect();
tracing::debug!("Using {} config API endpoints", api_urls.len());
let mut builder = nym_http_api_client::ClientBuilder::new_with_fronted_urls(api_urls)
.map_err(ClientCoreError::from)?
.with_retries(DEFAULT_NYM_API_RETRIES)
.with_bincode();
if let Some(user_agent) = user_agent {
builder = builder.with_user_agent(user_agent);
}
builder = builder.with_bincode();
builder.build().map_err(ClientCoreError::from)
}
@@ -940,8 +1004,8 @@ where
// Create a shutdown tracker for this client - either as a child of provided tracker
// or get one from the registry
let shutdown_tracker = match self.shutdown {
Some(parent_tracker) => parent_tracker.child_tracker(),
None => nym_task::get_sdk_shutdown_tracker()?,
Some(parent_tracker) => parent_tracker.clone(),
None => nym_task::create_sdk_shutdown_tracker()?,
};
Self::start_event_control(self.event_tx, event_receiver, &shutdown_tracker);
@@ -961,7 +1025,11 @@ 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.nym_api_urls.as_ref(),
&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(
@@ -976,7 +1044,7 @@ where
self.user_agent.clone(),
generate_client_stats_id(*self_address.identity()),
input_sender.clone(),
&shutdown_tracker.child_tracker(),
&shutdown_tracker.clone(),
);
// needs to be started as the first thing to block if required waiting for the gateway
@@ -986,7 +1054,7 @@ where
shared_topology_accessor.clone(),
self_address.gateway(),
self.wait_for_gateway,
&shutdown_tracker.child_tracker(),
&shutdown_tracker.clone(),
)
.await?;
@@ -1006,7 +1074,7 @@ where
stats_reporter.clone(),
#[cfg(unix)]
self.connection_fd_callback,
&shutdown_tracker.child_tracker(),
&shutdown_tracker.clone(),
)
.await?;
let gateway_ws_fd = gateway_transceiver.ws_fd();
@@ -1014,7 +1082,7 @@ where
let reply_storage = Self::setup_persistent_reply_storage(
reply_storage_backend,
key_rotation_config,
&shutdown_tracker.child_tracker(),
&shutdown_tracker.clone(),
)
.await?;
@@ -1025,7 +1093,7 @@ where
reply_storage.key_storage(),
reply_controller_sender.clone(),
stats_reporter.clone(),
&shutdown_tracker.child_tracker(),
&shutdown_tracker.clone(),
);
// The message_sender is the transmitter for any component generating sphinx packets
@@ -1035,7 +1103,7 @@ where
let (message_sender, client_request_sender) = Self::start_mix_traffic_controller(
gateway_transceiver,
&shutdown_tracker.child_tracker(),
&shutdown_tracker.clone(),
EventSender(event_sender),
);
@@ -1066,7 +1134,7 @@ where
shared_lane_queue_lengths.clone(),
client_connection_rx,
stats_reporter.clone(),
&shutdown_tracker.child_tracker(),
&shutdown_tracker.clone(),
);
if !self
@@ -1082,7 +1150,7 @@ where
shared_topology_accessor.clone(),
message_sender,
stats_reporter.clone(),
&shutdown_tracker.child_tracker(),
&shutdown_tracker.clone(),
);
}
@@ -1136,3 +1204,53 @@ pub struct BaseClient {
pub forget_me: ForgetMe,
pub remember_me: RememberMe,
}
#[cfg(test)]
mod tests {
use super::*;
use nym_network_defaults::{ApiUrl, NymNetworkDetails};
#[test]
fn test_network_details_with_multiple_urls() {
// Verify that network details can be configured with multiple API URLs
let mut network_details = NymNetworkDetails::new_empty();
network_details.nym_api_urls = Some(vec![
ApiUrl {
url: "https://validator.nymtech.net/api/".to_string(),
front_hosts: None,
},
ApiUrl {
url: "https://nym-frontdoor.vercel.app/api/".to_string(),
front_hosts: Some(vec!["vercel.app".to_string(), "vercel.com".to_string()]),
},
]);
assert_eq!(network_details.nym_api_urls.as_ref().unwrap().len(), 2);
assert!(network_details.nym_api_urls.as_ref().unwrap()[1]
.front_hosts
.is_some());
}
#[test]
fn test_network_details_with_front_hosts() {
// Verify that ApiUrl can store domain fronting configuration
let api_url = ApiUrl {
url: "https://nym-frontdoor.vercel.app/api/".to_string(),
front_hosts: Some(vec!["vercel.app".to_string(), "vercel.com".to_string()]),
};
assert_eq!(api_url.url, "https://nym-frontdoor.vercel.app/api/");
assert_eq!(api_url.front_hosts.as_ref().unwrap().len(), 2);
assert!(api_url
.front_hosts
.as_ref()
.unwrap()
.contains(&"vercel.app".to_string()));
}
#[test]
fn test_default_nym_api_retries_constant() {
// Verify the retry constant is set correctly
assert_eq!(DEFAULT_NYM_API_RETRIES, 3);
}
}
@@ -205,7 +205,7 @@ impl LoopCoverTrafficStream<OsRng> {
TrySendError::Full(_) => {
// This isn't a problem, if the channel is full means we're already sending the
// max amount of messages downstream can handle.
tracing::debug!("Failed to send cover message - channel full");
tracing::trace!("Failed to send cover message - channel full");
}
TrySendError::Closed(_) => {
tracing::warn!("Failed to send cover message - channel closed");
@@ -1,11 +1,13 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::rtt_analyzer::{RttConfig, RttEvent};
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_sphinx::params::PacketType;
use nym_task::connections::TransmissionLane;
use tokio::sync::mpsc::Sender;
pub type InputMessageSender = tokio::sync::mpsc::Sender<InputMessage>;
pub type InputMessageReceiver = tokio::sync::mpsc::Receiver<InputMessage>;
@@ -46,6 +48,13 @@ pub enum InputMessage {
lane: TransmissionLane,
max_retransmissions: Option<u32>,
},
RunRTTTest {
recipient: Recipient,
lane: TransmissionLane,
max_retransmissions: Option<u32>,
sender: Sender<RttEvent>,
config: RttConfig,
},
/// Attempt to use our internally received and stored `ReplySurb` to send the message back
/// to specified recipient whilst not knowing its full identity (or even gateway).
@@ -150,6 +159,7 @@ impl InputMessage {
match self {
InputMessage::Regular { lane, .. }
| InputMessage::Anonymous { lane, .. }
| InputMessage::RunRTTTest { lane, .. }
| InputMessage::Reply { lane, .. }
| InputMessage::Premade { lane, .. } => lane,
InputMessage::MessageWrapper { message, .. } => message.lane(),
@@ -166,6 +176,10 @@ impl InputMessage {
max_retransmissions: m,
..
}
| InputMessage::RunRTTTest {
max_retransmissions: m,
..
}
| InputMessage::Reply {
max_retransmissions: m,
..
@@ -20,7 +20,10 @@ pub mod transceiver;
// We remind ourselves that 32 x 32kb = 1024kb, a reasonable size for a network buffer.
pub const MIX_MESSAGE_RECEIVER_BUFFER_SIZE: usize = 32;
const MAX_FAILURE_COUNT: usize = 100;
/// Reduced from 100 to 20 to fail fast (~1-2 seconds instead of ~6 seconds).
/// If we can't send 20 packets in a row, the gateway is unreachable.
const MAX_FAILURE_COUNT: usize = 20;
// that's also disgusting.
pub struct Empty;
@@ -84,7 +87,7 @@ impl MixTrafficController {
self.client_tx.clone()
}
pub fn mix_rx(&self) -> BatchMixMessageSender {
pub fn mix_tx(&self) -> BatchMixMessageSender {
self.mix_tx.clone()
}
@@ -156,6 +159,11 @@ impl MixTrafficController {
// Do we need to handle the embedded mixnet client case
// separately?
self.event_tx.send(MixnetClientEvent::Traffic(MixTrafficEvent::FailedSendingSphinx));
// IMO it shouldn't be signalled from there but it is how it is
// TODO : report the failure upwards and shutdown from upwards
// Gateway is dead, we have to shut down currently
error!("Signalling shutdown from the MixTrafficController");
self.shutdown_token.cancel();
break;
}
}
+1
View File
@@ -11,6 +11,7 @@ pub mod mix_traffic;
pub mod real_messages_control;
pub mod received_buffer;
pub mod replies;
pub mod rtt_analyzer;
pub mod statistics_control;
pub mod topology_control;
pub(crate) mod transmission_buffer;
@@ -4,6 +4,7 @@
use super::action_controller::{AckActionSender, Action};
use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender};
use crate::client::rtt_analyzer::{RttAnalyzer, RttEvent};
use futures::StreamExt;
use nym_gateway_client::AcknowledgementReceiver;
use nym_sphinx::{
@@ -12,6 +13,7 @@ use nym_sphinx::{
};
use nym_task::ShutdownToken;
use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use tracing::*;
/// Module responsible for listening for any data resembling acknowledgements from the network
@@ -38,7 +40,11 @@ impl AcknowledgementListener {
}
}
async fn on_ack(&mut self, ack_content: Vec<u8>) {
async fn on_ack(
&mut self,
ack_content: Vec<u8>,
rtt_producer: Option<tokio::sync::mpsc::Sender<RttEvent>>,
) {
trace!("Received an ack");
self.stats_tx
.report(PacketStatisticsEvent::AckReceived(ack_content.len()).into());
@@ -62,6 +68,16 @@ impl AcknowledgementListener {
return;
}
if let Some(ref producer) = rtt_producer {
if let Ok(duration) = SystemTime::now().duration_since(UNIX_EPOCH) {
let now = duration.as_millis();
let _ = producer.try_send(RttEvent::FragmentAckReceived {
fragment_id: frag_id.set_id().to_string(),
timestamp: now,
});
}
}
trace!("Received {frag_id} from the mix network");
self.stats_tx
.report(PacketStatisticsEvent::RealAckReceived(ack_content.len()).into());
@@ -70,15 +86,20 @@ impl AcknowledgementListener {
.unbounded_send(Action::new_remove(frag_id));
}
async fn handle_ack_receiver_item(&mut self, item: Vec<Vec<u8>>) {
async fn handle_ack_receiver_item(
&mut self,
item: Vec<Vec<u8>>,
rtt_producer: Option<tokio::sync::mpsc::Sender<RttEvent>>,
) {
// realistically we would only be getting one ack at the time
for ack in item {
self.on_ack(ack).await;
self.on_ack(ack, rtt_producer.clone()).await;
}
}
pub(crate) async fn run(&mut self, shutdown_token: ShutdownToken) {
debug!("Started AcknowledgementListener with graceful shutdown support");
let rtt_producer = RttAnalyzer::producer();
loop {
tokio::select! {
@@ -88,7 +109,7 @@ impl AcknowledgementListener {
break;
}
acks = self.ack_receiver.next() => match acks {
Some(acks) => self.handle_ack_receiver_item(acks).await,
Some(acks) => self.handle_ack_receiver_item(acks,rtt_producer.clone()).await,
None => {
tracing::trace!("AcknowledgementListener: Stopping since channel closed");
break;
@@ -3,6 +3,7 @@
use super::PendingAcknowledgement;
use crate::client::real_messages_control::acknowledgement_control::RetransmissionRequestSender;
use crate::client::rtt_analyzer::{RttAnalyzer, RttEvent};
use futures::channel::mpsc;
use futures::StreamExt;
use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue, QueueKey};
@@ -16,6 +17,7 @@ use tracing::*;
pub(crate) type AckActionSender = mpsc::UnboundedSender<Action>;
pub(crate) type AckActionReceiver = mpsc::UnboundedReceiver<Action>;
use std::time::{SystemTime, UNIX_EPOCH};
// The actual data being sent off as well as potential key to the delay queue
type PendingAckEntry = (Arc<PendingAcknowledgement>, Option<QueueKey>);
@@ -207,8 +209,22 @@ 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>) {
fn handle_expired_ack_timer(
&mut self,
expired_ack: Expired<FragmentIdentifier>,
rtt_producer: Option<tokio::sync::mpsc::Sender<RttEvent>>,
) {
let frag_id = expired_ack.into_inner();
if let Some(ref producer) = rtt_producer {
if let Ok(duration) = SystemTime::now().duration_since(UNIX_EPOCH) {
let now: u128 = duration.as_millis();
let _ = producer.try_send(RttEvent::FragmentAckExpired {
fragment_id: frag_id.set_id().to_string(),
timestamp: now,
});
}
}
trace!("{frag_id} has expired");
@@ -244,7 +260,7 @@ impl ActionController {
pub(crate) async fn run(&mut self, shutdown_token: ShutdownToken) {
debug!("Started ActionController with graceful shutdown support");
let rtt_producer = RttAnalyzer::producer();
loop {
tokio::select! {
biased;
@@ -262,7 +278,7 @@ impl ActionController {
}
},
expired_ack = self.pending_acks_timers.next() => match expired_ack {
Some(expired_ack) => self.handle_expired_ack_timer(expired_ack),
Some(expired_ack) => self.handle_expired_ack_timer(expired_ack,rtt_producer.clone()),
None => {
tracing::trace!("ActionController: Stopping since ack channel closed");
break;
@@ -5,6 +5,7 @@ use crate::client::inbound_messages::{InputMessage, InputMessageReceiver};
use crate::client::real_messages_control::message_handler::MessageHandler;
use crate::client::real_messages_control::real_traffic_stream::RealMessage;
use crate::client::replies::reply_controller::ReplyControllerSender;
use crate::client::rtt_analyzer::{RttConfig, RttEvent};
use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::forwarding::packet::MixPacket;
@@ -12,6 +13,7 @@ use nym_sphinx::params::PacketType;
use nym_task::connections::TransmissionLane;
use nym_task::ShutdownToken;
use rand::{CryptoRng, Rng};
use tokio::sync::mpsc::Sender;
use tracing::*;
/// Module responsible for dealing with the received messages: splitting them, creating acknowledgements,
@@ -111,7 +113,30 @@ where
warn!("failed to send a repliable message - {err}")
}
}
async fn run_rtt_test(
&mut self,
recipient: Recipient,
lane: TransmissionLane,
packet_type: PacketType,
max_retransmissions: Option<u32>,
sender: Sender<RttEvent>,
config: RttConfig,
) {
if let Err(err) = self
.message_handler
.try_run_rtt_test(
recipient,
lane,
packet_type,
max_retransmissions,
sender,
config,
)
.await
{
warn!("failed to send a repliable message - {err}")
}
}
#[allow(clippy::panic)]
async fn on_input_message(&mut self, msg: InputMessage) {
match msg {
@@ -147,6 +172,23 @@ where
)
.await
}
InputMessage::RunRTTTest {
recipient,
lane,
max_retransmissions,
sender,
config,
} => {
self.run_rtt_test(
recipient,
lane,
PacketType::Mix,
max_retransmissions,
sender,
config,
)
.await
}
InputMessage::Reply {
recipient_tag,
data,
@@ -176,6 +218,23 @@ where
)
.await
}
InputMessage::RunRTTTest {
recipient,
lane,
max_retransmissions,
sender,
config,
} => {
self.run_rtt_test(
recipient,
lane,
PacketType::Mix,
max_retransmissions,
sender,
config,
)
.await
}
InputMessage::Anonymous {
recipient,
data,
@@ -8,6 +8,7 @@ use crate::client::real_messages_control::real_traffic_stream::{
use crate::client::real_messages_control::{AckActionSender, Action};
use crate::client::replies::reply_controller::MaxRetransmissions;
use crate::client::replies::reply_storage::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags};
use crate::client::rtt_analyzer::{RttConfig, RttEvent, RttPattern};
use crate::client::topology_control::{TopologyAccessor, TopologyReadPermit};
use nym_client_core_surb_storage::RetrievedReplySurb;
use nym_sphinx::acknowledgements::AckKey;
@@ -27,6 +28,8 @@ use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use thiserror::Error;
use tokio::sync::mpsc::Sender;
use tokio::time::sleep;
use tracing::{debug, error, info, trace, warn};
// TODO: move that error elsewhere since it seems to be contaminating different files
@@ -555,6 +558,97 @@ where
Ok(())
}
pub(crate) async fn try_split_and_send_non_reply_rtt_message(
&mut self,
sender_tag: AnonymousSenderTag,
recipient: Recipient,
lane: TransmissionLane,
packet_type: PacketType,
topology: &NymRouteProvider,
max_retransmissions: Option<u32>,
route_index: usize,
sender: Sender<RttEvent>,
) -> Result<(), PreparationError> {
debug!("Sending RTT test message on route index {route_index} with packet type {packet_type:?}");
// Construct the base message
let message = NymMessage::new_repliable(RepliableMessage::new_data(
self.config.use_legacy_sphinx_format,
Vec::new(),
sender_tag,
Vec::new(),
));
debug_assert!(!matches!(message, NymMessage::Reply(_)));
let packet_size = if packet_type == PacketType::Outfox {
PacketSize::OutfoxRegularPacket
} else {
self.optimal_packet_size(&message)
};
trace!("Using packet size {packet_size:?}");
// ✅ Drop the read lock before mutably borrowing self
// Prepare fragments from message
let fragments = self
.message_preparer
.pad_and_split_message(message, packet_size);
if fragments.len() > 1 {
println!(
"[RTT TEST] Warning: message was split into {} fragments",
fragments.len()
);
}
let mut pending_acks = Vec::with_capacity(fragments.len());
let mut real_messages = Vec::with_capacity(fragments.len());
for fragment in &fragments {
let prepared_fragment = self
.message_preparer
.prepare_chunk_for_sending_with_deterministic_route(
fragment.clone(),
&topology,
&self.config.ack_key,
&recipient,
packet_type,
route_index,
)?;
let _ = sender.try_send(RttEvent::RouteUsed {
route_index,
fragment_id: (fragment.fragment_identifier().set_id().to_string()),
});
let real_message = RealMessage::new(
prepared_fragment.mix_packet,
Some(fragment.fragment_identifier().clone()),
);
let pending_ack = PendingAcknowledgement::new_known(
fragment.clone(),
prepared_fragment.total_delay,
recipient,
max_retransmissions,
);
real_messages.push(real_message);
pending_acks.push(pending_ack);
}
// Record ACKs and forward messages for *this route only*
self.insert_pending_acks(pending_acks);
self.forward_messages(real_messages, lane).await;
// // Optional: small delay to avoid flooding
// sleep(Duration::from_millis(200)).await;
Ok(())
}
pub(crate) async fn try_send_additional_reply_surbs(
&mut self,
recipient: Recipient,
@@ -633,6 +727,142 @@ where
Ok(())
}
// Helper: sends ONE RTT packet on ONE specific route.
// Rust requires this to be a standalone async function (not an async closure),
// because async closures cannot borrow local variables safely.
async fn send_packet_on_route(
&mut self,
recipient: &Recipient,
num_reply_surbs: u32,
lane: TransmissionLane,
packet_type: PacketType,
topology: &NymRouteProvider,
max_retransmissions: Option<u32>,
route_index: usize,
sender: &Sender<RttEvent>,
) -> Result<(), SurbWrappedPreparationError> {
let sender_tag = self.get_or_create_sender_tag(recipient);
// Prepare reply SURBs
let reply_surbs = self.generate_reply_surbs(num_reply_surbs as usize).await?;
let reply_keys = reply_surbs
.iter()
.map(|s| *s.encryption_key())
.collect::<Vec<_>>();
// Send message on the given route
self.try_split_and_send_non_reply_rtt_message(
sender_tag,
recipient.clone(),
lane,
packet_type,
topology,
max_retransmissions,
route_index,
sender.clone(),
)
.await?;
// Store reply keys after sending
self.reply_key_storage.insert_multiple(reply_keys);
Ok(())
}
pub(crate) async fn try_run_rtt_test(
&mut self,
recipient: Recipient,
lane: TransmissionLane,
packet_type: PacketType,
max_retransmissions: Option<u32>,
sender: Sender<RttEvent>,
config: RttConfig,
) -> Result<(), SurbWrappedPreparationError> {
debug!("Starting RTT test using pattern {:?}", config.pattern);
// Load topology
let topology_permit = self.topology_access.get_read_permit().await;
let mut topology = self.get_topology(&topology_permit)?.clone();
let route_strings = topology
.topology
.initialize_static_mixnodes_for_rtt_testing()?;
let total_routes = topology.topology.all_mix_routes.len();
drop(topology_permit);
// =====================================================
// SEND ROUTE STRINGS TO RTT ANALYZER USING try_send()
// =====================================================
for (route_index, nodes_string) in route_strings {
let _ = sender.try_send(RttEvent::RouteNodes {
route_index,
nodes: nodes_string,
});
}
sender
.send(RttEvent::ExperimentConfiguration {
total_routes: total_routes as usize,
per_route_sent: config.packets_per_route as usize,
})
.await
.unwrap();
// ==============================================================
// PATTERN: BURST
// Send packets_per_route packets on each route sequentially
// ==============================================================
if let RttPattern::Burst = config.pattern {
for route in 0..total_routes {
for _ in 0..config.packets_per_route {
self.send_packet_on_route(
&recipient,
0,
lane,
packet_type,
&topology,
max_retransmissions,
route,
&sender,
)
.await?;
// Optional delay between packets on the same route
if config.inter_route_delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(config.inter_route_delay_ms))
.await;
}
}
}
return Ok(());
}
// ==============================================================
// PATTERN: ROUND ROBIN
// Send packets in cycles: 1 on route 0, 1 on route 1, ..., repeat
// ==============================================================
if let RttPattern::RoundRobin = config.pattern {
for _cycle in 0..config.packets_per_route {
for route in 0..total_routes {
self.send_packet_on_route(
&recipient,
0,
lane,
packet_type,
&topology,
max_retransmissions,
route,
&sender,
)
.await?;
if config.inter_route_delay_ms > 0 {
tokio::time::sleep(Duration::from_millis(config.inter_route_delay_ms))
.await;
}
}
}
return Ok(());
}
Ok(())
}
pub(crate) async fn try_prepare_single_chunk_for_sending(
&mut self,
@@ -4,6 +4,7 @@
use self::sending_delay_controller::SendingDelayController;
use crate::client::mix_traffic::BatchMixMessageSender;
use crate::client::real_messages_control::acknowledgement_control::SentPacketNotificationSender;
use crate::client::rtt_analyzer::{RttAnalyzer, RttEvent};
use crate::client::topology_control::TopologyAccessor;
use crate::client::transmission_buffer::TransmissionBuffer;
use crate::config;
@@ -21,6 +22,7 @@ use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, C
use nym_task::connections::{
ConnectionCommand, ConnectionCommandReceiver, ConnectionId, LaneQueueLengths, TransmissionLane,
};
use nym_task::ShutdownToken;
use rand::{CryptoRng, Rng};
use std::pin::Pin;
@@ -224,7 +226,11 @@ where
}
}
async fn on_message(&mut self, next_message: StreamMessage) {
async fn on_message(
&mut self,
next_message: StreamMessage,
rtt_producer: Option<tokio::sync::mpsc::Sender<RttEvent>>,
) {
trace!("created new message");
let (next_message, fragment_id, packet_size) = match next_message {
@@ -271,6 +277,21 @@ where
)
}
StreamMessage::Real(real_message) => {
if let Some(ref producer) = rtt_producer {
if let Some(fragment_id_local) = real_message.fragment_id {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
let _ = producer.try_send(RttEvent::FragmentSent {
fragment_id: (fragment_id_local.set_id().to_string()),
timestamp: (now),
});
}
}
let packet_size = real_message.packet_size();
(
real_message.mix_packet,
@@ -298,6 +319,8 @@ where
"failed to send mixnet packet due to closed channel (outside of shutdown!)"
);
}
// Early return to avoid further processing when channel is closed
return;
}
Ok(_) => {
let event = if fragment_id.is_some() {
@@ -582,7 +605,7 @@ where
pub(crate) async fn run(&mut self) {
debug!("Started OutQueueControl with graceful shutdown support");
let rtt_producer = RttAnalyzer::producer();
// avoid borrow on self
let shutdown_token = self.shutdown_token.clone();
#[cfg(not(target_arch = "wasm32"))]
@@ -600,7 +623,7 @@ where
self.log_status();
}
next_message = self.next() => if let Some(next_message) = next_message {
self.on_message(next_message).await;
self.on_message(next_message,rtt_producer.clone()).await;
} else {
tracing::trace!("OutQueueControl: Stopping since channel closed");
break;
@@ -5,6 +5,7 @@ use crate::client::helpers::get_time_now;
use crate::client::replies::{
reply_controller::ReplyControllerSender, reply_storage::SentReplyKeys,
};
use crate::client::rtt_analyzer::{RttAnalyzer, RttEvent};
use futures::channel::mpsc;
use futures::lock::Mutex;
use futures::StreamExt;
@@ -55,6 +56,7 @@ struct ReceivedMessagesBufferInner<R: MessageReceiver> {
// Periodically check for stale buffers to clean up
last_stale_check: crate::client::helpers::Instant,
rtt_producer: Option<tokio::sync::mpsc::Sender<RttEvent>>,
}
impl<R: MessageReceiver> ReceivedMessagesBufferInner<R> {
@@ -81,7 +83,20 @@ impl<R: MessageReceiver> ReceivedMessagesBufferInner<R> {
warn!("failed to recover fragment from raw data: {err}. The whole underlying message might be corrupted and unrecoverable!");
return None;
}
Ok(frag) => frag,
Ok(frag) => {
if let Some(ref producer) = self.rtt_producer {
use std::time::{SystemTime, UNIX_EPOCH};
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
let _ = producer.try_send(RttEvent::FragmentReceived {
fragment_id: frag.id().to_string(),
timestamp: now,
});
}
frag
}
};
if self.recently_reconstructed.contains(&fragment.id()) {
@@ -181,6 +196,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
reply_controller_sender: ReplyControllerSender,
stats_tx: ClientStatsSender,
shutdown_token: ShutdownToken,
rtt_producer: Option<tokio::sync::mpsc::Sender<RttEvent>>,
) -> Self {
ReceivedMessagesBuffer {
inner: Arc::new(Mutex::new(ReceivedMessagesBufferInner {
@@ -191,6 +207,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
recently_reconstructed: HashSet::new(),
stats_tx,
last_stale_check: get_time_now(),
rtt_producer: rtt_producer.clone(),
})),
reply_key_storage,
reply_controller_sender,
@@ -585,6 +602,7 @@ impl<R: MessageReceiver + Clone + Send + 'static> ReceivedMessagesBufferControll
reply_controller_sender,
metrics_reporter,
shutdown_token.clone(),
RttAnalyzer::producer(),
);
ReceivedMessagesBufferController {
@@ -0,0 +1,514 @@
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufWriter, Write};
use once_cell::sync::OnceCell;
use std::process::Command;
use std::sync::Mutex;
use tokio::sync::mpsc::{self, Sender};
use tokio::task::JoinHandle;
#[derive(Debug, Clone)]
pub enum RttPattern {
Burst,
RoundRobin,
}
#[derive(Debug, Clone)]
pub struct RttConfig {
pub packets_per_route: u32,
pub pattern: RttPattern,
pub inter_route_delay_ms: u64,
}
#[derive(Debug, Clone)]
pub enum RttEvent {
RouteUsed {
route_index: usize,
fragment_id: String,
},
FragmentSent {
fragment_id: String,
timestamp: u128,
},
FragmentAckReceived {
fragment_id: String,
timestamp: u128,
},
FragmentAckExpired {
fragment_id: String,
timestamp: u128,
},
FragmentReceived {
fragment_id: String,
timestamp: u128,
},
RouteNodes {
route_index: usize,
nodes: String,
},
ExperimentConfiguration {
total_routes: usize,
per_route_sent: usize,
},
PrintRouteDetail {
route_index: usize,
},
PrintRouteStatsByNodes {
nodes: String,
},
PrintRoutesWithAvgAbove {
threshold_ms: u128,
},
PrintRoutesWithAnyAbove {
threshold_ms: u128,
},
PrintStats,
WriteStats {
path: String,
},
WriteStatsAndPlot {
path: String,
outlier_mode: String, // "all" or cutoff() seconds (e.g. "1.0"))
},
PrintExperimentProgress,
}
pub struct StoredRouteSummary {
pub total_routes: usize,
pub per_route_sent: usize,
}
static PRODUCER: OnceCell<Mutex<Option<Sender<RttEvent>>>> = OnceCell::new();
#[derive(Default, Debug)]
pub struct RouteStats {
pub sent: u32,
pub acks: u32,
pub timeouts: u32,
pub rtts: Vec<u128>,
}
pub struct RttAnalyzer {
/// fragment_id → (route, Vec<sent_times>)
fragments: HashMap<String, (usize, Vec<u128>)>,
/// fragment_id → Vec<recv_times>
receive_times: HashMap<String, Vec<u128>>,
/// fragment_id → last ack
ack_times: HashMap<String, u128>,
route_stats: HashMap<usize, RouteStats>,
route_summary: Option<StoredRouteSummary>,
route_nodes: HashMap<usize, String>,
consumer_handle: JoinHandle<()>,
}
impl RttAnalyzer {
pub fn consumer_handle(&self) -> &JoinHandle<()> {
&self.consumer_handle
}
pub fn new() -> Self {
let (tx, mut rx) = mpsc::channel(80000);
PRODUCER
.set(Mutex::new(Some(tx.clone())))
.expect("PRODUCER already initialized");
let handle = tokio::spawn(async move {
let mut analyzer = RttAnalyzer {
fragments: HashMap::new(),
receive_times: HashMap::new(),
ack_times: HashMap::new(),
route_stats: HashMap::new(),
route_summary: None,
route_nodes: HashMap::new(),
consumer_handle: tokio::spawn(async {}),
};
while let Some(event) = rx.recv().await {
analyzer.process(event);
}
println!("RTT Analyzer consumer exited");
});
Self {
fragments: HashMap::new(),
receive_times: HashMap::new(),
ack_times: HashMap::new(),
route_stats: HashMap::new(),
route_summary: None,
route_nodes: HashMap::new(),
consumer_handle: handle,
}
}
pub fn producer() -> Option<Sender<RttEvent>> {
let lock = PRODUCER.get()?.lock().unwrap();
lock.clone()
}
pub fn process(&mut self, event: RttEvent) {
match event {
// -------------------------
// FIRST USE OF A FRAGMENT
// -------------------------
RttEvent::RouteUsed {
route_index,
fragment_id,
} => {
self.fragments
.insert(fragment_id.clone(), (route_index, Vec::new()));
}
// -------------------------
// RETRANSMISSION → append new sent time
// -------------------------
RttEvent::FragmentSent {
fragment_id,
timestamp,
} => {
if let Some((_route, sent_list)) = self.fragments.get_mut(&fragment_id) {
sent_list.push(timestamp);
self.route_stats.entry(*_route).or_default().sent += 1;
}
}
// -------------------------
// ACK RECEIVED
// -------------------------
RttEvent::FragmentAckReceived {
fragment_id,
timestamp,
} => {
if let Some((route, _)) = self.fragments.get(&fragment_id) {
let stats = self.route_stats.entry(*route).or_default();
stats.acks += 1;
}
self.ack_times.insert(fragment_id, timestamp);
}
// -------------------------
// ACK TIMEOUT
// -------------------------
RttEvent::FragmentAckExpired { fragment_id, .. } => {
if let Some((route, _)) = self.fragments.get(&fragment_id) {
self.route_stats.entry(*route).or_default().timeouts += 1;
}
}
RttEvent::WriteStatsAndPlot { path, outlier_mode } => {
// 1) write the csv
if let Err(e) = self.write_csv(&path) {
eprintln!("Failed to write CSV: {}", e);
return;
}
// 2) Call the Python script
if let Err(e) = Self::run_histogram_script(&path, &outlier_mode) {
eprintln!("Failed to run histogram script: {}", e);
}
}
// -------------------------
// PACKET RECEIVED → compute RTTs
// -------------------------
RttEvent::FragmentReceived {
fragment_id,
timestamp,
} => {
// Append receive time
let recv_list = self.receive_times.entry(fragment_id.clone()).or_default();
recv_list.push(timestamp);
// Lookup route + sent times
if let Some((route, sent_list)) = self.fragments.get(&fragment_id) {
let recv_list = self.receive_times.get(&fragment_id).unwrap();
// Index of the *newly added* receive time
let idx = recv_list.len() - 1;
/*
Maybe we can put a retransmission flag and not counting the new RTTs and only the basic N?
*/
//println!("Fragment id: {} Sent list length: {} Recv list length:{}",fragment_id,sent_list.len(),recv_list.len());
// Check if we have a matching sent timestamp
if idx < sent_list.len() {
let sent_ts = sent_list[idx];
let rtt = recv_list[idx] - sent_ts;
self.route_stats.entry(*route).or_default().rtts.push(rtt);
}
}
}
RttEvent::RouteNodes { route_index, nodes } => {
self.route_nodes.insert(route_index, nodes);
}
RttEvent::PrintStats => self.print_stats(),
RttEvent::WriteStats { path } => {
if let Err(e) = self.write_csv(&path) {
eprintln!("Failed to write CSV: {}", e)
}
}
RttEvent::ExperimentConfiguration {
total_routes,
per_route_sent,
} => {
self.route_summary = Some(StoredRouteSummary {
total_routes,
per_route_sent,
});
}
RttEvent::PrintExperimentProgress => {
self.print_experiment_progress();
}
RttEvent::PrintRouteDetail { route_index } => {
self.print_route_detail(route_index);
}
RttEvent::PrintRoutesWithAvgAbove { threshold_ms } => {
self.print_routes_with_avg_above(threshold_ms);
}
RttEvent::PrintRoutesWithAnyAbove { threshold_ms } => {
self.print_routes_with_any_above(threshold_ms);
}
RttEvent::PrintRouteStatsByNodes { nodes } => {
self.print_route_by_nodes(nodes);
}
}
}
// ---------------------- PRINT FUNCTIONS (unchanged) ----------------------
pub fn print_stats(&self) {
println!("\n================ Route RTT Statistics ================");
for (route, stats) in self.route_stats.iter() {
let avg_rtt = if !stats.rtts.is_empty() {
stats.rtts.iter().sum::<u128>() as f64 / stats.rtts.len() as f64
} else {
0.0
};
println!(
"Route {:5} | Sent {:4} | ACKs {:4} | Timeouts {:4} | Avg RTT {:8.2}",
route, stats.sent, stats.acks, stats.timeouts, avg_rtt
);
}
println!("======================================================\n");
}
pub fn write_csv(&self, path: &str) -> std::io::Result<()> {
let mut writer = BufWriter::new(File::create(path)?);
writer.write_all(b"route,sent,acks,timeouts,avg_rtt\n")?;
for (route, stats) in &self.route_stats {
let avg_rtt = if !stats.rtts.is_empty() {
stats.rtts.iter().sum::<u128>() as f64 / stats.rtts.len() as f64
} else {
0.0
};
writer.write_all(
format!(
"{},{},{},{},{:.2}\n",
route, stats.sent, stats.acks, stats.timeouts, avg_rtt
)
.as_bytes(),
)?;
}
Ok(())
}
pub fn print_route_detail(&self, route_index: usize) {
println!(
"\n================ Route #{} Details ================\n",
route_index
);
if let Some(nodes) = self.route_nodes.get(&route_index) {
println!(" Route Nodes:");
for (i, node) in nodes.split(" > ").enumerate() {
println!(" • Node {}: {}", i + 1, node);
}
}
if let Some(stats) = self.route_stats.get(&route_index) {
println!("\n RTT Values:");
for (i, rtt) in stats.rtts.iter().enumerate() {
println!(" [{:3}] {} ms", i, rtt);
}
}
println!("======================================================\n");
}
fn run_histogram_script(csv_path: &str, outlier_mode: &str) -> std::io::Result<()> {
let status = Command::new("python")
.arg("rtt_histogram.py") // path του script
.arg(csv_path)
.arg(outlier_mode)
.status()?;
if !status.success() {
eprintln!("Python histogram script exited with status: {}", status);
}
Ok(())
}
pub fn print_routes_with_avg_above(&self, threshold_ms: u128) {
println!(
"\n======= Routes with AVG RTT > {} ms =======\n",
threshold_ms
);
let mut matches: Vec<usize> = self
.route_stats
.iter()
.filter_map(|(route, stats)| {
if stats.rtts.is_empty() {
return None;
}
let avg = stats.rtts.iter().sum::<u128>() as f64 / stats.rtts.len() as f64;
if (avg as u128) > threshold_ms {
Some(*route)
} else {
None
}
})
.collect();
matches.sort();
for route in matches {
self.print_route_detail(route);
}
println!("====================================================\n");
}
/// Prints overall experiment completion percentage.
///
/// It uses:
/// - self.route_summary.total_routes
/// - self.route_summary.per_route_sent
/// to compute how many packets were planned in total.
///
/// Then it sums, over all routes:
/// - how many packets were actually sent (RouteStats.sent)
/// - how many packets have a completed RTT sample (RouteStats.rtts.len())
///
/// Finally it prints:
/// - total expected packets
/// - total sent packets and percentage
/// - total completed RTT packets and percentage
pub fn print_experiment_progress(&self) {
println!("\n=========== RTT Experiment Progress ===========");
// Check if experiment configuration is available
let summary = match &self.route_summary {
Some(s) => s,
None => {
println!("No experiment configuration stored (route_summary is None).");
println!("You must send an ExperimentConfiguration event first.");
println!("==============================================\n");
return;
}
};
let total_routes = summary.total_routes;
let per_route_sent = summary.per_route_sent;
// Total number of packets that were planned for the whole experiment
let expected_total: usize = total_routes.saturating_mul(per_route_sent);
if expected_total == 0 {
println!("Experiment configuration has zero expected packets.");
println!("==============================================\n");
return;
}
// Sum how many packets were actually sent and how many have a measured RTT
let mut sent_total: usize = 0;
let mut received_total: usize = 0;
for (_route_idx, stats) in &self.route_stats {
// 'sent' counts how many times we called FragmentSent for this route
sent_total += std::cmp::min(stats.sent, per_route_sent as u32) as usize;
// Each RTT entry corresponds to one packet for which we have both send and receive time
let route_recv = std::cmp::min(stats.rtts.len(), per_route_sent);
received_total += route_recv;
}
let sent_pct = (sent_total as f64 / expected_total as f64) * 100.0;
let recv_pct = (received_total as f64 / expected_total as f64) * 100.0;
println!("Total routes configured : {}", total_routes);
println!("Packets per route (planned) : {}", per_route_sent);
println!("Total expected packets : {}", expected_total);
println!("---------------------------------------------");
println!(
"Total sent packets : {} ({:.2}%)",
sent_total, sent_pct
);
println!(
"Total completed RTT packets : {} ({:.2}%)",
received_total, recv_pct
);
println!("==============================================\n");
}
pub fn print_routes_with_any_above(&self, threshold_ms: u128) {
println!(
"\n======= Routes with ANY RTT > {} ms =======\n",
threshold_ms
);
let mut matches: Vec<usize> = self
.route_stats
.iter()
.filter_map(|(route, stats)| {
if stats.rtts.iter().any(|&x| x > threshold_ms) {
Some(*route)
} else {
None
}
})
.collect();
matches.sort();
for route in matches {
self.print_route_detail(route);
}
println!("====================================================\n");
}
pub fn print_route_by_nodes(&self, nodes: String) {
println!("\n========== Searching route by nodes ==========\n");
let mut routes: Vec<(usize, &String)> =
self.route_nodes.iter().map(|(k, v)| (*k, v)).collect();
routes.sort_by_key(|(idx, _)| *idx);
for (route, stored) in routes {
if *stored == nodes {
println!("Found route {}!", route);
self.print_route_detail(route);
return;
}
}
println!("No route found with nodes: {}", nodes);
println!("=============================================\n");
}
}
+1 -1
View File
@@ -45,7 +45,7 @@ pub enum ClientCoreError {
#[cfg(not(target_arch = "wasm32"))]
#[error("resolution failed: {0}")]
ResolutionFailed(#[from] nym_http_api_client::HickoryDnsError),
ResolutionFailed(#[from] nym_http_api_client::ResolveError),
#[error("no gateways on network")]
NoGatewaysOnNetwork,
+2 -2
View File
@@ -151,7 +151,7 @@ pub async fn gateways_for_init(
}
let retry_count = retry_count.unwrap_or(DEFAULT_NYM_API_RETRIES);
let mut builder = nym_http_api_client::ClientBuilder::new_with_urls(nym_api_urls.clone())
let mut builder = nym_http_api_client::ClientBuilder::new_with_urls(nym_api_urls.clone())?
.with_retries(retry_count)
.with_bincode();
@@ -441,7 +441,7 @@ mod tests {
#[test]
fn test_multiple_urls_prepared_for_retries() {
let urls = vec![
let urls = [
Url::parse("https://api1.nym.com").unwrap(),
Url::parse("https://api2.nym.com").unwrap(),
Url::parse("https://api3.nym.com").unwrap(),
@@ -30,7 +30,6 @@ pub(crate) async fn connect_async(
resolver
.resolve_str(domain)
.await?
.into_iter()
.map(|a| SocketAddr::new(a, port))
.collect()
}
@@ -88,3 +88,6 @@ features = ["js"]
[features]
wasm = []
[lints]
workspace = true
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use si_scale::helpers::bibytes2;
use std::fmt::{Display, Formatter};
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
use std::sync::Arc;
use std::time::Duration;
@@ -26,6 +27,39 @@ pub struct ClientBandwidth {
inner: Arc<ClientBandwidthInner>,
}
// simple helper for logging purposes to accommodate 'unknown' case
pub(crate) enum UpgradeModeEnabledWrapper {
True,
False,
Unknown,
}
impl From<Option<bool>> for UpgradeModeEnabledWrapper {
fn from(value: Option<bool>) -> Self {
match value {
Some(true) => UpgradeModeEnabledWrapper::True,
Some(false) => UpgradeModeEnabledWrapper::False,
None => UpgradeModeEnabledWrapper::Unknown,
}
}
}
impl From<bool> for UpgradeModeEnabledWrapper {
fn from(value: bool) -> Self {
Some(value).into()
}
}
impl Display for UpgradeModeEnabledWrapper {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
UpgradeModeEnabledWrapper::True => write!(f, "true"),
UpgradeModeEnabledWrapper::False => write!(f, "false"),
UpgradeModeEnabledWrapper::Unknown => write!(f, "unknown"),
}
}
}
struct ClientBandwidthInner {
/// the actual bandwidth amount (in bytes) available
available: AtomicI64,
@@ -71,26 +105,41 @@ impl ClientBandwidth {
self.inner.available.load(Ordering::Acquire)
}
pub(crate) fn maybe_log_bandwidth(&self, now: Option<OffsetDateTime>) {
pub(crate) fn maybe_log_bandwidth(
&self,
now: Option<OffsetDateTime>,
upgrade_mode: impl Into<UpgradeModeEnabledWrapper>,
) {
let last = self.last_logged();
let now = now.unwrap_or_else(OffsetDateTime::now_utc);
if last + Duration::from_secs(10) < now {
self.log_bandwidth(Some(now))
self.log_bandwidth(Some(now), upgrade_mode)
}
}
pub(crate) fn log_bandwidth(&self, now: Option<OffsetDateTime>) {
pub(crate) fn log_bandwidth(
&self,
now: Option<OffsetDateTime>,
upgrade_mode: impl Into<UpgradeModeEnabledWrapper>,
) {
let now = now.unwrap_or_else(OffsetDateTime::now_utc);
let upgrade_mode = upgrade_mode.into();
let remaining = self.remaining();
let remaining_bi2 = bibytes2(remaining as f64);
if remaining < 0 {
tracing::warn!("OUT OF BANDWIDTH. remaining: {remaining_bi2}");
tracing::warn!(
"OUT OF BANDWIDTH. remaining: {remaining_bi2}. in 'upgrade mode': {upgrade_mode}"
);
} else if remaining < 1_000_000 {
tracing::info!("remaining bandwidth: {remaining_bi2}");
tracing::info!(
"remaining bandwidth: {remaining_bi2}. in 'upgrade mode': {upgrade_mode}"
);
} else {
tracing::debug!("remaining bandwidth: {remaining_bi2}");
tracing::trace!(
"remaining bandwidth: {remaining_bi2}. in 'upgrade mode': {upgrade_mode}"
);
}
self.inner
@@ -98,26 +147,35 @@ impl ClientBandwidth {
.store(now.unix_timestamp(), Ordering::Relaxed)
}
pub(crate) fn update_and_maybe_log(&self, remaining: i64) {
pub(crate) fn update_and_maybe_log(
&self,
remaining: i64,
upgrade_mode: impl Into<UpgradeModeEnabledWrapper>,
) {
let now = OffsetDateTime::now_utc();
self.inner.available.store(remaining, Ordering::Release);
self.inner
.last_updated_ts
.store(now.unix_timestamp(), Ordering::Relaxed);
self.maybe_log_bandwidth(Some(now))
self.maybe_log_bandwidth(Some(now), upgrade_mode)
}
pub(crate) fn update_and_log(&self, remaining: i64) {
pub(crate) fn update_and_log(
&self,
remaining: i64,
upgrade_mode: impl Into<UpgradeModeEnabledWrapper>,
) {
let now = OffsetDateTime::now_utc();
self.inner.available.store(remaining, Ordering::Release);
self.inner
.last_updated_ts
.store(now.unix_timestamp(), Ordering::Relaxed);
self.log_bandwidth(Some(now))
self.log_bandwidth(Some(now), upgrade_mode)
}
fn last_logged(&self) -> OffsetDateTime {
// SAFETY: this value is always populated with valid timestamps
#[allow(clippy::unwrap_used)]
OffsetDateTime::from_unix_timestamp(self.inner.last_logged_ts.load(Ordering::Relaxed))
.unwrap()
}
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::error::GatewayClientError;
use nym_network_defaults::TicketTypeRepr::V1MixnetEntry;
use nym_credentials_interface::DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD;
use si_scale::helpers::bibytes2;
use std::time::Duration;
@@ -103,7 +103,7 @@ impl BandwidthTickets {
// 20% of entry ticket value
pub const DEFAULT_REMAINING_BANDWIDTH_THRESHOLD: i64 =
(V1MixnetEntry.bandwidth_value() / 5) as i64;
DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD;
pub const DEFAULT_CUTOFF_REMAINING_BANDWIDTH_THRESHOLD: Option<i64> = None;
@@ -20,9 +20,9 @@ use nym_credentials_interface::TicketType;
use nym_crypto::asymmetric::ed25519;
use nym_gateway_requests::registration::handshake::client_handshake;
use nym_gateway_requests::{
BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt,
GatewayRequestsError, SensitiveServerResponse, ServerResponse, SharedGatewayKey,
SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
BandwidthResponse, BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersion,
GatewayProtocolVersionExt, GatewayRequestsError, SensitiveServerResponse, ServerResponse,
SharedGatewayKey, SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION,
};
use nym_sphinx::forwarding::packet::MixPacket;
use nym_statistics_common::clients::connection::ConnectionStatsEvent;
@@ -101,8 +101,7 @@ pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
bandwidth_controller: Option<BandwidthController<C, St>>,
stats_reporter: ClientStatsSender,
// currently unused (but populated)
negotiated_protocol: Option<u8>,
negotiated_protocol: Option<GatewayProtocolVersion>,
// Callback on the fd as soon as the connection has been established
#[cfg(unix)]
@@ -166,10 +165,12 @@ impl<C, St> GatewayClient<C, St> {
}
#[cfg(not(target_arch = "wasm32"))]
#[allow(clippy::unreachable)]
async fn _close_connection(&mut self) -> Result<(), GatewayClientError> {
match std::mem::replace(&mut self.connection, SocketState::NotConnected) {
SocketState::Available(mut socket) => Ok((*socket).close(None).await?),
SocketState::PartiallyDelegated(_) => {
// SAFETY: this is only called after the caller has already recovered the connection
unreachable!("this branch should have never been reached!")
}
_ => Ok(()), // no need to do anything in those cases
@@ -177,6 +178,7 @@ impl<C, St> GatewayClient<C, St> {
}
#[cfg(target_arch = "wasm32")]
#[allow(clippy::unreachable)]
async fn _close_connection(&mut self) -> Result<(), GatewayClientError> {
match std::mem::replace(&mut self.connection, SocketState::NotConnected) {
SocketState::Available(socket) => {
@@ -184,6 +186,7 @@ impl<C, St> GatewayClient<C, St> {
Ok(())
}
SocketState::PartiallyDelegated(_) => {
// SAFETY: this is only called after the caller has already recovered the connection
unreachable!("this branch should have never been reached!")
}
_ => Ok(()), // no need to do anything in those cases
@@ -458,43 +461,16 @@ impl<C, St> GatewayClient<C, St> {
}
}
fn check_gateway_protocol(
&self,
gateway_protocol: Option<u8>,
) -> Result<(), GatewayClientError> {
debug!("gateway protocol: {gateway_protocol:?}, ours: {CURRENT_PROTOCOL_VERSION}");
// right now there are no failure cases here, but this might change in the future
match gateway_protocol {
None => {
warn!("the gateway we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0");
// note: in +1.2.0 we will have to return a hard error here
Ok(())
}
Some(v) if v > CURRENT_PROTOCOL_VERSION => {
let err = GatewayClientError::IncompatibleProtocol {
gateway: Some(v),
current: CURRENT_PROTOCOL_VERSION,
};
error!("{err}");
Err(err)
}
Some(_) => {
debug!("the gateway is using exactly the same (or older) protocol version as we are. We're good to continue!");
Ok(())
}
}
}
async fn register(
&mut self,
derive_aes256_gcm_siv_key: bool,
supported_gateway_protocol: Option<GatewayProtocolVersion>,
) -> Result<(), GatewayClientError> {
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
let derive_aes256_gcm_siv_key = supported_gateway_protocol.supports_aes256_gcm_siv();
debug_assert!(self.connection.is_available());
log::debug!(
"registering with gateway. using legacy key derivation: {}",
@@ -505,14 +481,13 @@ impl<C, St> GatewayClient<C, St> {
// and putting it into the GatewayClient struct would be a hassle
let mut rng = OsRng;
let shared_key = match &mut self.connection {
let handshake_result = match &mut self.connection {
SocketState::Available(ws_stream) => client_handshake(
&mut rng,
ws_stream,
self.local_identity.as_ref(),
self.gateway_identity,
self.cfg.bandwidth.require_tickets,
derive_aes256_gcm_siv_key,
supported_gateway_protocol,
#[cfg(not(target_arch = "wasm32"))]
self.shutdown_token.clone(),
)
@@ -521,26 +496,31 @@ impl<C, St> GatewayClient<C, St> {
_ => return Err(GatewayClientError::ConnectionInInvalidState),
}?;
let (authentication_status, gateway_protocol) = match self.read_control_response().await? {
let authentication_status = match self.read_control_response().await? {
ServerResponse::Register {
protocol_version,
status,
} => (status, protocol_version),
upgrade_mode,
..
} => {
if upgrade_mode {
warn!("the system is currently undergoing an upgrade. some of its functionalities might be unstable")
}
status
}
ServerResponse::Error { message } => {
return Err(GatewayClientError::GatewayError(message))
}
other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
};
self.check_gateway_protocol(gateway_protocol)?;
self.authenticated = authentication_status;
if self.authenticated {
self.shared_key = Some(Arc::new(shared_key));
self.shared_key = Some(Arc::new(handshake_result.derived_key));
}
// populate the negotiated protocol for future uses
self.negotiated_protocol = gateway_protocol;
self.negotiated_protocol = Some(handshake_result.negotiated_protocol);
Ok(())
}
@@ -623,13 +603,24 @@ impl<C, St> GatewayClient<C, St> {
protocol_version,
status,
bandwidth_remaining,
upgrade_mode,
} => {
self.check_gateway_protocol(protocol_version)?;
if protocol_version.is_future_version() {
// SAFETY: future version is always defined
#[allow(clippy::unwrap_used)]
let version = protocol_version.unwrap();
error!("the gateway insists on using v{version} protocol which is not supported by this client");
return Err(GatewayClientError::AuthenticationFailure);
}
self.authenticated = status;
self.bandwidth.update_and_maybe_log(bandwidth_remaining);
self.bandwidth
.update_and_maybe_log(bandwidth_remaining, upgrade_mode);
self.negotiated_protocol = protocol_version;
log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}");
if upgrade_mode {
warn!("the system is currently undergoing an upgrade. some of its functionalities might be unstable")
}
Ok(())
}
@@ -650,7 +641,7 @@ impl<C, St> GatewayClient<C, St> {
.public_key()
.derive_destination_address();
let msg = ClientControlRequest::new_authenticate(
let msg = ClientControlRequest::new_legacy_authenticate(
self_address,
shared_key,
self.cfg.bandwidth.require_tickets,
@@ -659,25 +650,40 @@ impl<C, St> GatewayClient<C, St> {
.await
}
async fn authenticate_v2(&mut self) -> Result<(), GatewayClientError> {
async fn authenticate_v2(
&mut self,
requested_protocol_version: GatewayProtocolVersion,
) -> Result<(), GatewayClientError> {
debug!("using v2 authentication");
let Some(shared_key) = self.shared_key.as_ref() else {
return Err(GatewayClientError::NoSharedKeyAvailable);
};
let msg = ClientControlRequest::new_authenticate_v2(shared_key, &self.local_identity)?;
let msg = ClientControlRequest::new_authenticate_v2(
shared_key,
&self.local_identity,
requested_protocol_version,
)?;
self.send_authenticate_request_and_handle_response(msg)
.await
}
async fn authenticate(&mut self, use_v2: bool) -> Result<(), GatewayClientError> {
async fn authenticate(
&mut self,
supported_gateway_protocol: Option<GatewayProtocolVersion>,
) -> Result<(), GatewayClientError> {
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
debug!("authenticating with gateway");
if use_v2 {
self.authenticate_v2().await
if supported_gateway_protocol.supports_authenticate_v2() {
// use the highest possible protocol version the gateway has announced support for
// SAFETY: if announced protocol supports auth v2, it means it's properly set
#[allow(clippy::unwrap_used)]
self.authenticate_v2(supported_gateway_protocol.unwrap())
.await
} else {
self.authenticate_v1().await
}
@@ -708,9 +714,12 @@ impl<C, St> GatewayClient<C, St> {
}
};
debug!("supported gateway protocol: {gw_protocol:?}");
let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv();
let supports_auth_v2 = gw_protocol.supports_authenticate_v2();
let supports_key_rotation_info = gw_protocol.supports_key_rotation_packet();
let supports_upgrade_mode = gw_protocol.supports_upgrade_mode();
if !supports_aes_gcm_siv {
warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV");
@@ -721,6 +730,16 @@ impl<C, St> GatewayClient<C, St> {
if !supports_key_rotation_info {
warn!("this gateway is on an old version that doesn't support key rotation packets")
}
if !supports_upgrade_mode {
warn!("this gateway is on an old version that doesn't support upgrade mode")
}
let gw_protocol = if gw_protocol.is_future_version() {
warn!("we're running outdated software as gateway is announcing protocol {gw_protocol:?} whilst we're using {}. we're going to attempt to downgrade", GatewayProtocolVersion::CURRENT);
Some(GatewayProtocolVersion::CURRENT)
} else {
gw_protocol
};
if self.authenticated {
debug!("Already authenticated");
@@ -735,10 +754,11 @@ impl<C, St> GatewayClient<C, St> {
}
if self.shared_key.is_some() {
self.authenticate(supports_auth_v2).await?;
self.authenticate(gw_protocol).await?;
if self.authenticated {
// if we are authenticated it means we MUST have an associated shared_key
#[allow(clippy::unwrap_used)]
let shared_key = self.shared_key.as_ref().unwrap();
let requires_key_upgrade = shared_key.is_legacy() && supports_aes_gcm_siv;
@@ -751,9 +771,10 @@ impl<C, St> GatewayClient<C, St> {
Err(GatewayClientError::AuthenticationFailure)
}
} else {
self.register(supports_aes_gcm_siv).await?;
self.register(gw_protocol).await?;
// if registration didn't return an error, we MUST have an associated shared key
#[allow(clippy::unwrap_used)]
let shared_key = self.shared_key.as_ref().unwrap();
// we're always registering with the highest supported protocol,
@@ -783,51 +804,81 @@ impl<C, St> GatewayClient<C, St> {
}
}
async fn claim_ecash_bandwidth(
async fn wait_for_bandwidth_response(
&mut self,
credential: CredentialSpendingData,
) -> Result<(), GatewayClientError> {
let msg = ClientControlRequest::new_enc_ecash_credential(
credential,
self.shared_key.as_ref().unwrap(),
)?;
let bandwidth_remaining = match self
msg: ClientControlRequest,
) -> Result<BandwidthResponse, GatewayClientError> {
let response = match self
.send_websocket_message_with_non_send_response(msg)
.await?
{
ServerResponse::Bandwidth { available_total } => Ok(available_total),
ServerResponse::Bandwidth(response) => {
if response.upgrade_mode {
info!("the system is currently undergoing an upgrade. our bandwidth shouldn't have been metered")
}
Ok(response)
}
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
ServerResponse::TypedError { error } => {
Err(GatewayClientError::TypedGatewayError(error))
}
other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
}?;
Ok(response)
}
async fn claim_ecash_bandwidth(
&mut self,
credential: CredentialSpendingData,
) -> Result<(), GatewayClientError> {
// SAFETY: claiming ecash bandwidth is called as part of `claim_bandwidth` which
// ensures the shared key is defined
#[allow(clippy::unwrap_used)]
let msg = ClientControlRequest::new_enc_ecash_credential(
credential,
self.shared_key.as_ref().unwrap(),
)?;
let response = self.wait_for_bandwidth_response(msg).await?;
// TODO: create tracing span
info!("managed to claim ecash bandwidth");
self.bandwidth.update_and_log(bandwidth_remaining);
self.bandwidth
.update_and_log(response.available_total, response.upgrade_mode);
Ok(())
}
pub async fn send_upgrade_mode_jwt(&mut self, token: String) -> Result<(), GatewayClientError> {
let msg = ClientControlRequest::new_upgrade_mode_jwt(token);
let response = self.wait_for_bandwidth_response(msg).await?;
// if gateway rejected our jwt, we would have returned an error
info!("gateway has accepted our jwt");
if !response.upgrade_mode {
error!("but we're not in upgrade mode - something is wrong!");
return Err(GatewayClientError::UnexpectedUpgradeModeState);
}
self.bandwidth
.update_and_log(response.available_total, response.upgrade_mode);
Ok(())
}
async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> {
let msg = ClientControlRequest::ClaimFreeTestnetBandwidth;
let bandwidth_remaining = match self
.send_websocket_message_with_non_send_response(msg)
.await?
{
ServerResponse::Bandwidth { available_total } => Ok(available_total),
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
}?;
let response = self.wait_for_bandwidth_response(msg).await?;
info!("managed to claim testnet bandwidth");
self.bandwidth.update_and_log(bandwidth_remaining);
self.bandwidth
.update_and_log(response.available_total, response.upgrade_mode);
Ok(())
}
fn unchecked_bandwidth_controller(&self) -> &BandwidthController<C, St> {
// this is an unchecked method
#[allow(clippy::unwrap_used)]
self.bandwidth_controller.as_ref().unwrap()
}
@@ -919,6 +970,7 @@ impl<C, St> GatewayClient<C, St> {
BinaryRequest::ForwardSphinx { packet }
};
#[allow(clippy::expect_used)]
req.into_ws_message(
self.shared_key
.as_ref()
@@ -1025,6 +1077,8 @@ impl<C, St> GatewayClient<C, St> {
self.send_with_reconnection_on_failure(msg).await
}
// SAFETY: this method is only called when the connection is in `PartiallyDelegated` state
#[allow(clippy::unreachable)]
async fn recover_socket_connection(&mut self) -> Result<(), GatewayClientError> {
if self.connection.is_available() {
return Ok(());
@@ -1054,6 +1108,7 @@ impl<C, St> GatewayClient<C, St> {
return Err(GatewayClientError::ConnectionInInvalidState);
}
#[allow(clippy::expect_used)]
let partially_delegated =
match std::mem::replace(&mut self.connection, SocketState::Invalid) {
SocketState::Available(conn) => {
@@ -1069,7 +1124,13 @@ impl<C, St> GatewayClient<C, St> {
self.shutdown_token.clone(),
)
}
_ => unreachable!(),
other => {
error!(
"attempted to start mixnet listener whilst the connection is in {} state!",
other.name()
);
return Err(GatewayClientError::ConnectionInInvalidState);
}
};
self.connection = SocketState::PartiallyDelegated(partially_delegated);
@@ -1082,8 +1143,7 @@ impl<C, St> GatewayClient<C, St> {
}
// if we're reconnecting, because we lost connection, we need to re-authenticate the connection
self.authenticate(self.negotiated_protocol.supports_authenticate_v2())
.await?;
self.authenticate(self.negotiated_protocol).await?;
// this call is NON-blocking
self.start_listening_for_mixnet_messages()?;
@@ -39,7 +39,6 @@ pub(crate) async fn connect_async(
resolver
.resolve_str(domain)
.await?
.into_iter()
.map(|a| SocketAddr::new(a, port))
.collect()
}
@@ -54,7 +54,7 @@ pub enum GatewayClientError {
#[cfg(not(target_arch = "wasm32"))]
#[error("resolution failed: {0}")]
ResolutionFailed(#[from] nym_http_api_client::HickoryDnsError),
ResolutionFailed(#[from] nym_http_api_client::ResolveError),
#[error("No shared key was provided or obtained")]
NoSharedKeyAvailable,
@@ -128,6 +128,9 @@ pub enum GatewayClientError {
"this operation couldn't be completed as the program is in the process of shutting down"
)]
ShutdownInProgress,
#[error("the system is an unexpected upgrade mode state")]
UnexpectedUpgradeModeState,
}
impl From<WsError> for GatewayClientError {
@@ -35,6 +35,7 @@ impl PacketRouter {
}
}
#[allow(clippy::panic)]
pub fn route_mixnet_messages(
&self,
received_messages: Vec<Vec<u8>>,
@@ -54,6 +55,7 @@ impl PacketRouter {
Ok(())
}
#[allow(clippy::panic)]
pub fn route_acks(&self, received_acks: Vec<Vec<u8>>) -> Result<(), GatewayClientError> {
if let Err(err) = self.ack_sender.unbounded_send(received_acks) {
// check if the failure is due to the shutdown being in progress and thus the receiver channel
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::bandwidth::ClientBandwidth;
use crate::client::config::BandwidthTickets;
use crate::error::GatewayClientError;
use crate::packet_router::PacketRouter;
use crate::traits::GatewayPacketRouter;
@@ -10,7 +11,9 @@ use futures::channel::oneshot;
use futures::stream::{SplitSink, SplitStream};
use futures::{SinkExt, StreamExt};
use nym_gateway_requests::shared_key::SharedGatewayKey;
use nym_gateway_requests::{SensitiveServerResponse, ServerResponse, SimpleGatewayRequestsError};
use nym_gateway_requests::{
SendResponse, SensitiveServerResponse, ServerResponse, SimpleGatewayRequestsError,
};
use nym_task::ShutdownToken;
use si_scale::helpers::bibytes2;
use std::os::raw::c_int as RawFd;
@@ -154,11 +157,12 @@ impl PartiallyDelegatedRouter {
fn handle_text_message(&self, text: String) -> Result<(), GatewayClientError> {
// if we fail to deserialise the response, return a hard error. we can't handle garbage
match ServerResponse::try_from(text).map_err(|_| GatewayClientError::MalformedResponse)? {
ServerResponse::Send {
ServerResponse::Send(SendResponse {
remaining_bandwidth,
} => {
upgrade_mode,
}) => {
self.client_bandwidth
.update_and_maybe_log(remaining_bandwidth);
.update_and_maybe_log(remaining_bandwidth, upgrade_mode);
Ok(())
}
ServerResponse::Error { message } => {
@@ -174,7 +178,20 @@ impl PartiallyDelegatedRouter {
let available_bi2 = bibytes2(available as f64);
let required_bi2 = bibytes2(required as f64);
warn!("run out of bandwidth when attempting to send the message! we got {available_bi2} available, but needed at least {required_bi2} to send the previous message");
self.client_bandwidth.update_and_log(available);
// if we run out of bandwidth (and tried to send reasonable amount of data),
// the upgrade mode is implicitly disabled, as otherwise we would have been
// to proceed
let upgrade_mode = if available
< BandwidthTickets::DEFAULT_REMAINING_BANDWIDTH_THRESHOLD
{
Some(false)
} else {
// we were attempting to send a lot of data at once
// - we have no certainty about upgrade mode at this point
None
};
self.client_bandwidth
.update_and_log(available, upgrade_mode);
// UNIMPLEMENTED: we should stop sending messages until we recover bandwidth
Ok(())
}
@@ -327,6 +344,7 @@ impl PartiallyDelegatedHandle {
Ok(self.sink_half.send_all(&mut send_stream).await?)
}
#[allow(clippy::panic)]
pub(crate) async fn merge(self) -> Result<WsConn, GatewayClientError> {
let (mut stream_receiver, notify) = self.delegated_stream;
@@ -355,8 +373,10 @@ impl PartiallyDelegatedHandle {
// in receive_res
.map_err(|_| GatewayClientError::ConnectionAbruptlyClosed)?;
let stream = stream_results?;
// the error is thrown when trying to reunite sink and stream that did not originate
// from the same split which is impossible to happen here
#[allow(clippy::unwrap_used)]
Ok(self.sink_half.reunite(stream).unwrap())
}
}
@@ -387,4 +407,13 @@ impl SocketState {
SocketState::Available(_) | SocketState::PartiallyDelegated(_)
)
}
pub(crate) fn name(&self) -> &'static str {
match self {
SocketState::Available(_) => "available",
SocketState::PartiallyDelegated(_) => "partially delegated",
SocketState::NotConnected => "not connected",
SocketState::Invalid => "invalid",
}
}
}
@@ -241,23 +241,28 @@ impl Epoch {
//
// Note: It's important that the variant ordering is not changed otherwise it would mess up the derived `PartialOrd`
#[cw_serde]
#[derive(Copy)]
#[derive(Copy, Default)]
pub enum EpochState {
#[default]
WaitingInitialisation,
PublicKeySubmission { resharing: bool },
DealingExchange { resharing: bool },
VerificationKeySubmission { resharing: bool },
VerificationKeyValidation { resharing: bool },
VerificationKeyFinalization { resharing: bool },
PublicKeySubmission {
resharing: bool,
},
DealingExchange {
resharing: bool,
},
VerificationKeySubmission {
resharing: bool,
},
VerificationKeyValidation {
resharing: bool,
},
VerificationKeyFinalization {
resharing: bool,
},
InProgress,
}
impl Default for EpochState {
fn default() -> Self {
Self::WaitingInitialisation
}
}
impl Display for EpochState {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
+1 -1
View File
@@ -8,7 +8,7 @@ async fn main() -> anyhow::Result<()> {
use sqlx::{Connection, SqliteConnection};
use std::env;
let out_dir = env::var("OUT_DIR")?;
let out_dir = env::var("OUT_DIR").context("missing OUT_DIR env variable")?;
let database_path = format!("{out_dir}/nym-credential-proxy-example.sqlite");
// remove the db file if it already existed from previous build
+26
View File
@@ -1,6 +1,7 @@
// Copyright 2025 Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_crypto::asymmetric::ed25519;
use nym_ecash_signer_check::SignerCheckError;
use nym_validator_client::coconut::EcashApiError;
use nym_validator_client::nym_api::{EpochId, error::NymAPIError};
@@ -127,6 +128,13 @@ pub enum CredentialProxyError {
#[error("failed to create deposit")]
DepositFailure,
#[error("failed to load jwt signing key from {path}: {err}")]
JWTSigningKeyLoadFailure {
path: String,
#[source]
err: std::io::Error,
},
#[error("can't obtain sufficient number of credential shares due to unavailable quorum")]
UnavailableSigningQuorum,
@@ -161,6 +169,24 @@ pub enum CredentialProxyError {
device_id: String,
credential_id: String,
},
#[error(
"the attestation check url has not been provided through either the CLI nor the default .env config"
)]
AttestationCheckUrlNotSet,
#[error("the provided attester public key is malformed: {source}")]
MalformedAttestationCheckUrl { source: url::ParseError },
#[error(
"the attester public key has not been provided through either the CLI nor the default .env config"
)]
AttesterPublicKeyNotSet,
#[error("the provided attester public key is malformed: {source}")]
MalformedAttesterPublicKey {
source: ed25519::Ed25519RecoveryError,
},
}
impl From<NymAPIError> for CredentialProxyError {
@@ -7,9 +7,9 @@ use crate::ticketbook_manager::TicketbookManager;
use nym_compact_ecash::Base58;
use nym_credential_proxy_requests::api::v1::ticketbook::models::{
CurrentEpochResponse, DepositResponse, GlobalDataParams, MasterVerificationKeyResponse,
PartialVerificationKey, PartialVerificationKeysResponse, TicketbookAsyncRequest,
TicketbookObtainParams, TicketbookRequest, TicketbookWalletSharesAsyncResponse,
TicketbookWalletSharesResponse,
ObtainTicketBookSharesAsyncResponse, PartialVerificationKey, PartialVerificationKeysResponse,
TicketbookAsyncRequest, TicketbookObtainParams, TicketbookRequest,
TicketbookWalletSharesAsyncResponse, TicketbookWalletSharesResponse,
};
use time::OffsetDateTime;
use tracing::{Instrument, Level, error, info, span, warn};
@@ -65,7 +65,7 @@ impl TicketbookManager {
uuid: Uuid,
request: TicketbookAsyncRequest,
params: TicketbookObtainParams,
) -> Result<TicketbookWalletSharesAsyncResponse, CredentialProxyError> {
) -> Result<ObtainTicketBookSharesAsyncResponse, CredentialProxyError> {
let requested_on = OffsetDateTime::now_utc();
let span = span!(Level::INFO, "[async] obtain ticketboook", uuid = %uuid);
async move {
@@ -110,7 +110,7 @@ impl TicketbookManager {
}
// 4. in the meantime, return the id to the user
Ok(TicketbookWalletSharesAsyncResponse { id, uuid })
Ok(TicketbookWalletSharesAsyncResponse { id, uuid }.into())
}
.instrument(span)
.await
+1
View File
@@ -14,6 +14,7 @@ bincode = { workspace = true, optional = true }
log = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true, features = ["derive"], optional = true }
time = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
zeroize = { workspace = true, features = ["zeroize_derive"] }
@@ -0,0 +1,17 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
CREATE TABLE emergency_credential
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
type TEXT NOT NULL,
-- don't define any strict schema on the content as it might be implementation-dependant
content BLOB NOT NULL,
expiration TIMESTAMP WITHOUT TIME ZONE
);
-- no point in allowing duplicate data
CREATE UNIQUE INDEX emergency_credential_unique_type_content
ON emergency_credential (type, content);
@@ -1,7 +1,10 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook};
use crate::models::{
BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent,
RetrievedPendingTicketbook, RetrievedTicketbook,
};
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature;
use nym_compact_ecash::VerificationKeyAuth;
@@ -22,6 +25,12 @@ pub struct MemoryEcachTicketbookManager {
inner: Arc<RwLock<EcashCredentialManagerInner>>,
}
#[derive(Default)]
struct InternalIdCounters {
next_ticketbook_id: i64,
next_emergency_credential_id: i64,
}
#[derive(Default)]
struct EcashCredentialManagerInner {
ticketbooks: HashMap<i64, RetrievedTicketbook>,
@@ -29,13 +38,22 @@ struct EcashCredentialManagerInner {
master_vk: HashMap<u64, VerificationKeyAuth>,
coin_indices_sigs: HashMap<u64, Vec<AnnotatedCoinIndexSignature>>,
expiration_date_sigs: HashMap<(u64, Date), Vec<AnnotatedExpirationDateSignature>>,
_next_id: i64,
emergency_credentials: HashMap<String, Vec<EmergencyCredential>>,
// internal counters emulating assignment of an increasing id to new inserted database entries
internal_counters: InternalIdCounters,
}
impl EcashCredentialManagerInner {
fn next_id(&mut self) -> i64 {
let next = self._next_id;
self._next_id += 1;
fn next_ticketbook_id(&mut self) -> i64 {
let next = self.internal_counters.next_ticketbook_id;
self.internal_counters.next_ticketbook_id += 1;
next
}
fn next_emergency_credential_id(&mut self) -> i64 {
let next = self.internal_counters.next_emergency_credential_id;
self.internal_counters.next_emergency_credential_id += 1;
next
}
}
@@ -170,7 +188,7 @@ impl MemoryEcachTicketbookManager {
used_tickets: u32,
) {
let mut guard = self.inner.write().await;
let id = guard.next_id();
let id = guard.next_ticketbook_id();
#[allow(clippy::unwrap_used)]
let mut nasty_clone = hack_clone_ticketbook(ticketbook);
@@ -277,4 +295,41 @@ impl MemoryEcachTicketbookManager {
sigs.signatures.clone(),
);
}
pub(crate) async fn get_emergency_credential(&self, typ: &str) -> Option<EmergencyCredential> {
let guard = self.inner.read().await;
guard.emergency_credentials.get(typ)?.first().cloned()
}
pub(crate) async fn insert_emergency_credential(
&self,
credential: &EmergencyCredentialContent,
) {
let mut guard = self.inner.write().await;
let id = guard.next_emergency_credential_id();
guard
.emergency_credentials
.entry(credential.typ.clone())
.or_default()
.push(EmergencyCredential {
id,
data: credential.clone(),
});
}
pub(crate) async fn remove_emergency_credential(&self, id: i64) {
let mut guard = self.inner.write().await;
guard.emergency_credentials.retain(|_, credentials| {
credentials.retain(|c| c.id != id);
!credentials.is_empty()
})
}
pub(crate) async fn remove_emergency_credentials_of_type(&self, typ: &str) {
let mut guard = self.inner.write().await;
guard.emergency_credentials.remove(typ);
}
}
@@ -2,8 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
use crate::models::{
BasicTicketbookInformation, RawCoinIndexSignatures, RawExpirationDateSignatures,
RawVerificationKey, StoredIssuedTicketbook, StoredPendingTicketbook,
BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent,
RawCoinIndexSignatures, RawExpirationDateSignatures, RawVerificationKey,
StoredIssuedTicketbook, StoredPendingTicketbook,
};
use nym_ecash_time::Date;
use sqlx::{Executor, Sqlite, Transaction};
@@ -305,6 +306,74 @@ impl SqliteEcashTicketbookManager {
.await?;
Ok(())
}
pub(crate) async fn get_emergency_credential(
&self,
typ: &str,
) -> Result<Option<EmergencyCredential>, sqlx::Error> {
sqlx::query_as(
r#"
SELECT *
FROM emergency_credential
WHERE type = ?
AND (expiration IS NULL OR expiration > CURRENT_TIMESTAMP)
ORDER BY expiration DESC NULLS LAST
LIMIT 1
"#,
)
.bind(typ)
.fetch_optional(&*self.connection_pool)
.await
}
pub(crate) async fn insert_emergency_credential(
&self,
credential: &EmergencyCredentialContent,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO emergency_credential
(type, content, expiration)
VALUES (?, ?, ?)
ON CONFLICT(type, content) DO NOTHING;
"#,
credential.typ,
credential.content,
credential.expiration,
)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn remove_emergency_credential(&self, id: i64) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
DELETE FROM emergency_credential
WHERE id = ?
"#,
id
)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn remove_emergency_credentials_of_type(
&self,
typ: &str,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
DELETE FROM emergency_credential
WHERE type = ?
"#,
typ
)
.execute(&*self.connection_pool)
.await?;
Ok(())
}
}
pub(crate) async fn get_next_unspent_ticketbook<'a, E>(
@@ -3,7 +3,10 @@
use crate::backends::memory::MemoryEcachTicketbookManager;
use crate::error::StorageError;
use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook};
use crate::models::{
BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent,
RetrievedPendingTicketbook, RetrievedTicketbook,
};
use crate::storage::Storage;
use async_trait::async_trait;
use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature;
@@ -218,6 +221,38 @@ impl Storage for EphemeralStorage {
.await;
Ok(())
}
async fn get_emergency_credential(
&self,
typ: &str,
) -> Result<Option<EmergencyCredential>, Self::StorageError> {
Ok(self.storage_manager.get_emergency_credential(typ).await)
}
async fn insert_emergency_credential(
&self,
credential: &EmergencyCredentialContent,
) -> Result<(), Self::StorageError> {
self.storage_manager
.insert_emergency_credential(credential)
.await;
Ok(())
}
async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError> {
self.storage_manager.remove_emergency_credential(id).await;
Ok(())
}
async fn remove_emergency_credentials_of_type(
&self,
typ: &str,
) -> Result<(), Self::StorageError> {
self.storage_manager
.remove_emergency_credentials_of_type(typ)
.await;
Ok(())
}
}
#[cfg(test)]
@@ -246,7 +281,7 @@ mod tests {
let _exp_date_sigs = generate_expiration_date_signatures(
sig_req.expiration_date.ecash_unix_timestamp(),
&[signing_keys.secret_key()],
&vec![signing_keys.verification_key()],
&[signing_keys.verification_key()],
&signing_keys.verification_key(),
&[1],
)?;
@@ -263,7 +298,7 @@ mod tests {
let wallet = issuance.aggregate_signature_shares(
&signing_keys.verification_key(),
&vec![partial_wallet],
&[partial_wallet],
sig_req,
)?;
+18
View File
@@ -3,6 +3,7 @@
use nym_credentials::{IssuanceTicketBook, IssuedTicketBook};
use nym_ecash_time::Date;
use time::OffsetDateTime;
use zeroize::{Zeroize, ZeroizeOnDrop};
pub struct RetrievedTicketbook {
@@ -78,3 +79,20 @@ pub struct RawVerificationKey {
pub serialised_key: Vec<u8>,
pub serialization_revision: u8,
}
#[derive(Clone, Debug)]
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
pub struct EmergencyCredential {
pub id: i64,
#[cfg_attr(not(target_arch = "wasm32"), sqlx(flatten))]
pub data: EmergencyCredentialContent,
}
#[derive(Clone, Debug)]
#[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))]
pub struct EmergencyCredentialContent {
#[cfg_attr(not(target_arch = "wasm32"), sqlx(rename = "type"))]
pub typ: String,
pub content: Vec<u8>,
pub expiration: Option<OffsetDateTime>,
}
@@ -3,6 +3,7 @@
mod legacy_helpers;
use crate::models::{EmergencyCredential, EmergencyCredentialContent};
use crate::{
backends::sqlite::{
get_next_unspent_ticketbook, increase_used_ticketbook_tickets, SqliteEcashTicketbookManager,
@@ -401,4 +402,36 @@ impl Storage for PersistentStorage {
.await?;
Ok(())
}
async fn get_emergency_credential(
&self,
typ: &str,
) -> Result<Option<EmergencyCredential>, Self::StorageError> {
Ok(self.storage_manager.get_emergency_credential(typ).await?)
}
async fn insert_emergency_credential(
&self,
credential: &EmergencyCredentialContent,
) -> Result<(), Self::StorageError> {
self.storage_manager
.insert_emergency_credential(credential)
.await?;
Ok(())
}
async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError> {
self.storage_manager.remove_emergency_credential(id).await?;
Ok(())
}
async fn remove_emergency_credentials_of_type(
&self,
typ: &str,
) -> Result<(), Self::StorageError> {
self.storage_manager
.remove_emergency_credentials_of_type(typ)
.await?;
Ok(())
}
}
+21 -1
View File
@@ -1,7 +1,10 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook};
use crate::models::{
BasicTicketbookInformation, EmergencyCredential, EmergencyCredentialContent,
RetrievedPendingTicketbook, RetrievedTicketbook,
};
use async_trait::async_trait;
use nym_compact_ecash::VerificationKeyAuth;
use nym_credentials::ecash::bandwidth::serialiser::keys::EpochVerificationKey;
@@ -108,4 +111,21 @@ pub trait Storage: Clone + Send + Sync {
&self,
signatures: &AggregatedExpirationDateSignatures,
) -> Result<(), Self::StorageError>;
async fn get_emergency_credential(
&self,
typ: &str,
) -> Result<Option<EmergencyCredential>, Self::StorageError>;
async fn insert_emergency_credential(
&self,
credential: &EmergencyCredentialContent,
) -> Result<(), Self::StorageError>;
async fn remove_emergency_credential(&self, id: i64) -> Result<(), Self::StorageError>;
async fn remove_emergency_credentials_of_type(
&self,
typ: &str,
) -> Result<(), Self::StorageError>;
}
+2 -1
View File
@@ -17,7 +17,6 @@ cosmwasm-std = { workspace = true }
cw-utils = { workspace = true }
dyn-clone = { workspace = true }
futures = { workspace = true }
rand = { workspace = true }
si-scale = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
@@ -27,8 +26,10 @@ tracing = { workspace = true }
nym-api-requests = { path = "../../nym-api/nym-api-requests" }
nym-credentials = { path = "../credentials" }
nym-credentials-interface = { path = "../credentials-interface" }
nym-crypto = { path = "../crypto", features = ["asymmetric"] }
nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" }
nym-gateway-requests = { path = "../gateway-requests" }
nym-gateway-storage = { path = "../gateway-storage" }
nym-task = { path = "../task" }
nym-validator-client = { path = "../client-libs/validator-client" }
nym-upgrade-mode-check = { path = "../upgrade-mode-check" }
@@ -6,7 +6,6 @@ use crate::ClientBandwidth;
use crate::error::*;
use nym_credentials::ecash::utils::ecash_today;
use nym_credentials_interface::Bandwidth;
use nym_gateway_requests::ServerResponse;
use nym_gateway_storage::traits::BandwidthGatewayStorage;
use si_scale::helpers::bibytes2;
use time::OffsetDateTime;
@@ -66,7 +65,7 @@ impl BandwidthStorageManager {
Ok(())
}
pub async fn handle_claim_testnet_bandwidth(&mut self) -> Result<ServerResponse> {
pub async fn handle_claim_testnet_bandwidth(&mut self) -> Result<i64> {
debug!("handling testnet bandwidth request");
if self.only_coconut_credentials {
@@ -76,8 +75,7 @@ impl BandwidthStorageManager {
self.increase_bandwidth(FREE_TESTNET_BANDWIDTH_VALUE, ecash_today())
.await?;
let available_total = self.client_bandwidth.available().await;
Ok(ServerResponse::Bandwidth { available_total })
Ok(available_total)
}
#[instrument(skip_all)]
@@ -96,7 +94,7 @@ impl BandwidthStorageManager {
let available_bi2 = bibytes2(available_bandwidth as f64);
let required_bi2 = bibytes2(required_bandwidth as f64);
debug!(available = available_bi2, required = required_bi2);
trace!(available = available_bi2, required = required_bi2);
self.consume_bandwidth(required_bandwidth).await?;
let remaining_bandwidth = self.client_bandwidth.available().await;
@@ -47,6 +47,25 @@ pub enum Error {
UnknownTicketType(#[from] nym_credentials_interface::UnknownTicketType),
}
impl Error {
pub fn is_out_of_bandwidth(&self) -> bool {
matches!(self, Error::OutOfBandwidth { .. })
}
}
pub trait OutOfBandwidthResultExt {
fn is_out_of_bandwidth(&self) -> bool;
}
impl<T> OutOfBandwidthResultExt for Result<T> {
fn is_out_of_bandwidth(&self) -> bool {
match &self {
Ok(_) => false,
Err(err) => err.is_out_of_bandwidth(),
}
}
}
impl From<EcashTicketError> for Error {
fn from(err: EcashTicketError) -> Self {
// don't expose storage issue details to the user
@@ -13,11 +13,13 @@ use tracing::*;
pub use client_bandwidth::*;
pub use error::*;
pub use upgrade_mode::UpgradeModeState;
pub mod bandwidth_storage_manager;
mod client_bandwidth;
pub mod ecash;
pub mod error;
pub mod upgrade_mode;
pub struct CredentialVerifier {
credential: CredentialSpendingRequest,
@@ -0,0 +1,293 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
use nym_crypto::asymmetric::ed25519;
use nym_upgrade_mode_check::{
CREDENTIAL_PROXY_JWT_ISSUER, UpgradeModeAttestation, validate_upgrade_mode_jwt,
};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
use std::time::Duration;
use thiserror::Error;
use time::OffsetDateTime;
use tokio::sync::{Notify, RwLock};
use tracing::{debug, error, info};
#[derive(Debug, Error)]
pub enum UpgradeModeEnableError {
#[error("too soon to perform another upgrade mode attestation check")]
TooManyRecheckRequests,
#[error("provided upgrade mode JWT is invalid: {0}")]
InvalidUpgradeModeJWT(#[from] nym_upgrade_mode_check::UpgradeModeCheckError),
#[error("the upgrade mode attestation does not appear to have been published")]
AttestationNotPublished,
#[error("the provided upgrade mode attestation is different from the published one")]
MismatchedUpgradeModeAttestation,
}
// the idea behind this is as follows:
// it's been relatively a long time since the watcher last performed its checks (since it's in 'regular' mode)
// and some client has just sent a JWT. we have to retrieve most recent information in case upgrade mode
// has just been enabled, and we haven't learned about it yet
#[derive(Clone)]
pub struct UpgradeModeCheckRequestSender(Option<UnboundedSender<CheckRequest>>);
impl UpgradeModeCheckRequestSender {
pub fn new(sender: UnboundedSender<CheckRequest>) -> Self {
UpgradeModeCheckRequestSender(Some(sender))
}
pub fn new_empty() -> Self {
Self(None)
}
pub(crate) fn send_request(&self, on_done: Arc<Notify>) {
let Some(ref inner) = self.0 else {
// make sure the caller gets notified so it doesn't wait forever
on_done.notify_waiters();
return;
};
if let Err(not_sent) = inner.unbounded_send(CheckRequest { on_done }) {
debug!("failed to send upgrade mode check request - {not_sent}");
// make sure the caller gets notified so it doesn't wait forever
not_sent.into_inner().on_done.notify_waiters();
}
}
}
pub type UpgradeModeCheckRequestReceiver = UnboundedReceiver<CheckRequest>;
pub struct CheckRequest {
on_done: Arc<Notify>,
}
impl CheckRequest {
pub fn finalize(self) {
self.on_done.notify_waiters();
}
}
#[derive(Clone, Copy)]
pub struct UpgradeModeCheckConfig {
/// The minimum duration since the last explicit check to allow creation of separate request.
pub min_staleness_recheck: Duration,
}
/// Full upgrade mode information, that apart from boolean flag indicating the state
/// and the attestation information, includes channel connection to relevant
/// attestation watcher to request state rechecks
#[derive(Clone)]
pub struct UpgradeModeDetails {
pub(crate) config: UpgradeModeCheckConfig,
pub(crate) request_checker: UpgradeModeCheckRequestSender,
pub(crate) state: UpgradeModeState,
}
impl UpgradeModeDetails {
pub fn new(
config: UpgradeModeCheckConfig,
request_checker: UpgradeModeCheckRequestSender,
state: UpgradeModeState,
) -> Self {
UpgradeModeDetails {
config,
request_checker,
state,
}
}
pub fn state(&self) -> &UpgradeModeState {
&self.state
}
pub fn enabled(&self) -> bool {
self.state.upgrade_mode_enabled()
}
fn since_last_query(&self) -> Duration {
self.state.since_last_query()
}
pub fn can_request_recheck(&self) -> bool {
self.since_last_query() > self.config.min_staleness_recheck
}
// explicitly request state update. this is only called when upgrade mode is NOT enabled,
// and client has sent a JWT instead of ticket
async fn request_recheck(&self) -> bool {
// send request
let on_done = Arc::new(Notify::new());
self.request_checker.send_request(on_done.clone());
// wait for response - note, if we fail to send, notification will be sent regardless,
// so that we wouldn't get stuck in here
on_done.notified().await;
// check the state again
self.enabled()
}
pub async fn try_enable_via_received_jwt(
&self,
token: String,
) -> Result<(), UpgradeModeEnableError> {
// see if it's viable to perform another expedited check
if !self.can_request_recheck() {
return Err(UpgradeModeEnableError::TooManyRecheckRequests);
}
// first validate whether the received JWT is even valid
let attestation = validate_upgrade_mode_jwt(&token, Some(CREDENTIAL_PROXY_JWT_ISSUER))?;
// send request to revalidate internal state
// this will, among other things, pull fresh attestation from the configured endpoint
// and also verify required signatures (and pubkeys)
self.request_recheck().await;
// not strictly necessary, but check if provided attestation actually matches the one retrieved
// (if any)
let Some(retrieved_attestation) = self.state.attestation().await else {
return Err(UpgradeModeEnableError::AttestationNotPublished);
};
if retrieved_attestation != attestation {
return Err(UpgradeModeEnableError::MismatchedUpgradeModeAttestation);
}
// note: if attestation has been returned, it means we're definitely in upgrade mode
// (otherwise it wouldn't have existed in the state)
info!("managed to initialise upgrade mode through received JWT");
Ok(())
}
}
/// Detailed upgrade mode information, that apart from boolean flag,
/// also includes, if applicable, the associated attestation
#[derive(Clone)]
pub struct UpgradeModeState {
inner: Arc<UpgradeModeStateInner>,
}
/// Just a shareable flag to indicate whether upgrade mode is enabled or disabled
#[derive(Clone, Default)]
pub struct UpgradeModeStatus(Arc<AtomicBool>);
impl UpgradeModeStatus {
pub fn enabled(&self) -> bool {
self.0.load(Ordering::Acquire)
}
pub fn enable(&self) {
self.0.store(true, Ordering::Relaxed);
}
pub fn disable(&self) {
self.0.store(false, Ordering::Release);
}
}
impl UpgradeModeState {
pub fn new(attester_public_key: ed25519::PublicKey) -> UpgradeModeState {
UpgradeModeState {
inner: Arc::new(UpgradeModeStateInner {
expected_attester_public_key: attester_public_key,
expected_attestation: RwLock::new(None),
last_queried_ts: AtomicI64::new(OffsetDateTime::UNIX_EPOCH.unix_timestamp()),
status: UpgradeModeStatus(Arc::new(AtomicBool::new(false))),
}),
}
}
pub fn attester_pubkey(&self) -> ed25519::PublicKey {
self.inner.expected_attester_public_key
}
pub async fn attestation(&self) -> Option<UpgradeModeAttestation> {
self.inner.expected_attestation.read().await.clone()
}
pub async fn try_set_expected_attestation(
&self,
expected_attestation: Option<UpgradeModeAttestation>,
) {
// make sure to only enable upgrade mode flag AFTER we have written the expected value
// (or still hold the exclusive lock as in this instance)
let mut guard = self.inner.expected_attestation.write().await;
// ensure that the attestation had been signed with the expected key
if let Some(attestation) = expected_attestation.as_ref() {
if attestation.content.attester_public_key != self.inner.expected_attester_public_key {
self.update_last_queried(OffsetDateTime::now_utc());
return;
}
self.enable_upgrade_mode()
} else {
self.disable_upgrade_mode()
}
self.update_last_queried(OffsetDateTime::now_utc());
*guard = expected_attestation;
}
pub fn upgrade_mode_status(&self) -> UpgradeModeStatus {
self.inner.status.clone()
}
pub fn upgrade_mode_enabled(&self) -> bool {
self.inner.status.enabled()
}
pub fn enable_upgrade_mode(&self) {
self.inner.status.enable()
}
pub fn disable_upgrade_mode(&self) {
self.inner.status.disable()
}
pub fn last_queried(&self) -> OffsetDateTime {
// SAFETY: the stored value here is always a valid unix timestamp
#[allow(clippy::unwrap_used)]
OffsetDateTime::from_unix_timestamp(self.inner.last_queried_ts.load(Ordering::Acquire))
.unwrap()
}
pub fn update_last_queried(&self, queried_at: OffsetDateTime) {
self.inner
.last_queried_ts
.store(queried_at.unix_timestamp(), Ordering::Release);
}
pub fn since_last_query(&self) -> Duration {
(OffsetDateTime::now_utc() - self.last_queried())
.try_into()
.unwrap_or_else(|_| {
error!("somehow our last query for upgrade mode was in the future!");
Duration::ZERO
})
}
}
struct UpgradeModeStateInner {
/// Expected public key of the entity issuing upgrade mode attestations.
expected_attester_public_key: ed25519::PublicKey,
/// Contents of the published upgrade mode attestation, as queried by this node
expected_attestation: RwLock<Option<UpgradeModeAttestation>>,
/// timestamp indicating last time this node has queried for the current upgrade mode attestation
/// it is used to determine if an additional expedited query should be made in case client sends a JWT
/// whilst this node is not aware of the upgrade mode
last_queried_ts: AtomicI64,
/// flag indicating whether upgrade mode is currently enabled. this is to perform cheap checks
/// that avoid having to acquire the lock
// (and dealing with the async consequences of that)
status: UpgradeModeStatus,
}
+2
View File
@@ -23,3 +23,5 @@ rand = { workspace = true }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
nym-ecash-time = { path = "../ecash-time" }
nym-network-defaults = { path = "../network-defaults" }
nym-upgrade-mode-check = { path = "../upgrade-mode-check" }
+29
View File
@@ -30,6 +30,35 @@ pub use nym_compact_ecash::{
};
pub use nym_ecash_time::{EcashTime, ecash_today};
pub use nym_network_defaults::TicketTypeRepr;
use nym_network_defaults::TicketTypeRepr::V1MixnetEntry;
/// Default bandwidth amount under which [mixnet] clients will attempt to send additional zk-nyms
/// to increase their allowance.
// currently defined as 20% of entry ticket value
// clients are, of course, free to override this value
pub const DEFAULT_MIXNET_REQUEST_BANDWIDTH_THRESHOLD: i64 =
(V1MixnetEntry.bandwidth_value() / 5) as i64;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub enum BandwidthCredential {
ZkNym(Box<CredentialSpendingData>),
UpgradeModeJWT { token: String },
}
impl BandwidthCredential {
pub fn into_zk_nym(self) -> Option<Box<CredentialSpendingData>> {
match self {
BandwidthCredential::ZkNym(credential) => Some(credential),
_ => None,
}
}
}
impl From<CredentialSpendingData> for BandwidthCredential {
fn from(credential: CredentialSpendingData) -> Self {
Self::ZkNym(Box::new(credential))
}
}
#[derive(Debug, Clone)]
pub struct CredentialSigningData {
+4
View File
@@ -36,7 +36,11 @@ nym-sphinx-types = { path = "../nymsphinx/types", version = "0.2.0", default-fea
nym-pemstore = { path = "../../common/pemstore", version = "0.3.0" }
[dev-dependencies]
anyhow = { workspace = true }
rand_chacha = { workspace = true }
serde_json = { workspace = true }
nym-test-utils = { path = "../test-utils" }
[features]
default = []
@@ -17,6 +17,51 @@ pub mod bs58_ed25519_pubkey {
}
}
pub mod vec_bs58_ed25519_pubkey {
use super::*;
use serde::{Deserialize, Deserializer, Serializer, ser::SerializeSeq};
pub fn serialize<S: Serializer>(
keys: &Vec<PublicKey>,
serializer: S,
) -> Result<S::Ok, S::Error> {
let mut seq = serializer.serialize_seq(Some(keys.len()))?;
for key in keys {
seq.serialize_element(&Bs58KeyWrapper(*key))?;
}
seq.end()
}
pub fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<PublicKey>, D::Error> {
let wrapped = Vec::<Bs58KeyWrapper>::deserialize(deserializer)?;
Ok(wrapped.into_iter().map(|k| k.0).collect())
}
struct Bs58KeyWrapper(PublicKey);
impl serde::Serialize for Bs58KeyWrapper {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
bs58_ed25519_pubkey::serialize(&self.0, serializer)
}
}
impl<'de> Deserialize<'de> for Bs58KeyWrapper {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
Ok(Bs58KeyWrapper(bs58_ed25519_pubkey::deserialize(
deserializer,
)?))
}
}
}
pub mod bs58_ed25519_signature {
use crate::asymmetric::ed25519::Signature;
use serde::{Deserialize, Deserializer, Serializer};
@@ -33,3 +78,53 @@ pub mod bs58_ed25519_signature {
Signature::from_base58_string(s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
use jwt_simple::reexports::{anyhow, serde_json};
use nym_test_utils::helpers::deterministic_rng;
use serde::{Deserialize, Serialize};
#[test]
fn vec_bs58_ed25519_pubkey_json() -> anyhow::Result<()> {
#[derive(Serialize, Deserialize, Debug, PartialEq)]
struct KeysWrapper(#[serde(with = "vec_bs58_ed25519_pubkey")] Vec<PublicKey>);
use crate::asymmetric::ed25519;
let mut rng = deterministic_rng();
let empty = KeysWrapper(vec![]);
let single_key = KeysWrapper(vec![PublicKey::from_base58_string(
"Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt",
)?]);
let three_keys = KeysWrapper(vec![
ed25519::KeyPair::new(&mut rng).public_key,
ed25519::KeyPair::new(&mut rng).public_key,
ed25519::KeyPair::new(&mut rng).public_key,
]);
let se_empty = serde_json::to_string(&empty)?;
let se_single_key = serde_json::to_string(&single_key)?;
let se_three_keys = serde_json::to_string(&three_keys)?;
assert_eq!(se_empty, r#"[]"#);
assert_eq!(
se_single_key,
r#"["Be9wH7xuXBRJAuV1pC7MALZv6a61RvWQ3SypsNarqTt"]"#
);
assert_eq!(
se_three_keys,
r#"["HmgHDV79LpnEaSUp8QZQwSroxVvS4RewF7yM9e7qu8y3","311xRh859qCd5MVqoPRCoNx26eYhLknGwtjzkkTJFGhf","A5BMp8WJ6Uk91U4JpWRv2Bc6X35AaRaSEy8QEWeAkaBv"]"#
);
let empty_de = serde_json::from_str::<KeysWrapper>(&se_empty)?;
let single_key_de = serde_json::from_str::<KeysWrapper>(&se_single_key)?;
let three_keys_de = serde_json::from_str::<KeysWrapper>(&se_three_keys)?;
assert_eq!(empty, empty_de);
assert_eq!(single_key, single_key_de);
assert_eq!(three_keys, three_keys_de);
Ok(())
}
}
+3
View File
@@ -51,3 +51,6 @@ 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"] }
[lints]
workspace = true
+53 -13
View File
@@ -19,7 +19,9 @@ pub use shared_key::{
SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey,
};
pub const CURRENT_PROTOCOL_VERSION: u8 = EMBEDDED_KEY_ROTATION_INFO_VERSION;
pub type GatewayProtocolVersion = u8;
pub const CURRENT_PROTOCOL_VERSION: GatewayProtocolVersion = UPGRADE_MODE_VERSION;
/// Defines the current version of the communication protocol between gateway and clients.
/// It has to be incremented for any breaking change.
@@ -29,35 +31,73 @@ pub const CURRENT_PROTOCOL_VERSION: u8 = EMBEDDED_KEY_ROTATION_INFO_VERSION;
// 3 - change to AES-GCM-SIV and non-zero IVs
// 4 - introduction of v2 authentication protocol to prevent reply attacks
// 5 - add key rotation information to the serialised mix packet
pub const INITIAL_PROTOCOL_VERSION: u8 = 1;
pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2;
pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3;
pub const AUTHENTICATE_V2_PROTOCOL_VERSION: u8 = 4;
pub const EMBEDDED_KEY_ROTATION_INFO_VERSION: u8 = 5;
// 6 - support for 'upgrade mode'
pub const INITIAL_PROTOCOL_VERSION: GatewayProtocolVersion = 1;
pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: GatewayProtocolVersion = 2;
pub const AES_GCM_SIV_PROTOCOL_VERSION: GatewayProtocolVersion = 3;
pub const AUTHENTICATE_V2_PROTOCOL_VERSION: GatewayProtocolVersion = 4;
pub const EMBEDDED_KEY_ROTATION_INFO_VERSION: GatewayProtocolVersion = 5;
pub const UPGRADE_MODE_VERSION: GatewayProtocolVersion = 6;
// TODO: could using `Mac` trait here for OutputSize backfire?
// Should hmac itself be exposed, imported and used instead?
pub type LegacyGatewayMacSize = <GatewayIntegrityHmacAlgorithm as OutputSizeUser>::OutputSize;
pub trait GatewayProtocolVersionExt {
const CURRENT: GatewayProtocolVersion = CURRENT_PROTOCOL_VERSION;
fn supports_aes256_gcm_siv(&self) -> bool;
fn supports_authenticate_v2(&self) -> bool;
fn supports_key_rotation_packet(&self) -> bool;
fn supports_upgrade_mode(&self) -> bool;
fn is_future_version(&self) -> bool;
}
impl GatewayProtocolVersionExt for Option<u8> {
impl GatewayProtocolVersionExt for Option<GatewayProtocolVersion> {
fn supports_aes256_gcm_siv(&self) -> bool {
let Some(protocol) = *self else { return false };
protocol >= AES_GCM_SIV_PROTOCOL_VERSION
let Some(protocol) = self else { return false };
protocol.supports_aes256_gcm_siv()
}
fn supports_authenticate_v2(&self) -> bool {
let Some(protocol) = *self else { return false };
protocol >= AUTHENTICATE_V2_PROTOCOL_VERSION
let Some(protocol) = self else { return false };
protocol.supports_authenticate_v2()
}
fn supports_key_rotation_packet(&self) -> bool {
let Some(protocol) = *self else { return false };
protocol >= EMBEDDED_KEY_ROTATION_INFO_VERSION
let Some(protocol) = self else { return false };
protocol.supports_key_rotation_packet()
}
fn supports_upgrade_mode(&self) -> bool {
let Some(protocol) = self else { return false };
protocol.supports_upgrade_mode()
}
fn is_future_version(&self) -> bool {
let Some(protocol) = self else { return false };
protocol.is_future_version()
}
}
impl GatewayProtocolVersionExt for GatewayProtocolVersion {
fn supports_aes256_gcm_siv(&self) -> bool {
*self >= AES_GCM_SIV_PROTOCOL_VERSION
}
fn supports_authenticate_v2(&self) -> bool {
*self >= AUTHENTICATE_V2_PROTOCOL_VERSION
}
fn supports_key_rotation_packet(&self) -> bool {
*self >= EMBEDDED_KEY_ROTATION_INFO_VERSION
}
fn supports_upgrade_mode(&self) -> bool {
*self >= UPGRADE_MODE_VERSION
}
fn is_future_version(&self) -> bool {
*self > CURRENT_PROTOCOL_VERSION
}
}
+3 -3
View File
@@ -82,7 +82,7 @@ mod tests {
let exp_date_sigs = generate_expiration_date_signatures(
sig_req.expiration_date.ecash_unix_timestamp(),
&[keypair.secret_key()],
&vec![keypair.verification_key()],
&[keypair.verification_key()],
&keypair.verification_key(),
&[keypair.index.unwrap()],
)
@@ -106,14 +106,14 @@ mod tests {
.unwrap();
let wallet = issuance
.aggregate_signature_shares(&keypair.verification_key(), &vec![partial_wallet], sig_req)
.aggregate_signature_shares(&keypair.verification_key(), &[partial_wallet], sig_req)
.unwrap();
let mut issued = issuance.into_issued_ticketbook(wallet, 1);
let coin_indices_signatures = generate_coin_indices_signatures(
nym_credentials_interface::ecash_parameters(),
&[keypair.secret_key()],
&vec![keypair.verification_key()],
&[keypair.verification_key()],
&keypair.verification_key(),
&[keypair.index.unwrap()],
)
@@ -3,10 +3,12 @@
use crate::registration::handshake::messages::{Finalization, GatewayMaterialExchange};
use crate::registration::handshake::state::State;
use crate::registration::handshake::SharedGatewayKey;
use crate::registration::handshake::HandshakeResult;
use crate::registration::handshake::{error::HandshakeError, WsItem};
use crate::{GatewayProtocolVersionExt, INITIAL_PROTOCOL_VERSION};
use futures::{Sink, Stream};
use rand::{CryptoRng, RngCore};
use tracing::info;
use tungstenite::Message as WsMessage;
impl<S, R> State<'_, S, R> {
@@ -25,10 +27,26 @@ impl<S, R> State<'_, S, R> {
// 2. wait for response with remote x25519 pubkey as well as encrypted signature
// <- g^y || AES(k, sig(gate_priv, (g^y || g^x)) || MAYBE_NONCE
let mid_res = self
let (mid_res, gateway_protocol) = self
.receive_handshake_message::<GatewayMaterialExchange>()
.await?;
// NEGOTIATE PROTOCOL
if gateway_protocol.is_future_version() {
// SAFETY: future version means it's greater than CURRENT, which is always a `Some`
#[allow(clippy::unwrap_used)]
return Err(HandshakeError::UnsupportedProtocol {
version: gateway_protocol.unwrap(),
});
}
let gateway_protocol = gateway_protocol.unwrap_or(INITIAL_PROTOCOL_VERSION);
// that should never happen, but we're fine with that outcome
if Some(gateway_protocol) != self.proposed_protocol_version() {
info!("the gateway insists on protocol version different from the one we suggested. it wants {gateway_protocol} whilst we wanted {:?}, however, we can support it", self.proposed_protocol_version());
self.set_protocol_version(gateway_protocol);
}
// 3. derive shared keys locally
// hkdf::<blake3>::(g^xy)
self.derive_shared_key(&mid_res.ephemeral_dh, maybe_hkdf_salt.as_deref());
@@ -42,14 +60,14 @@ impl<S, R> State<'_, S, R> {
self.send_handshake_data(materials).await?;
// 6. wait for remote confirmation of finalizing the handshake
let finalization = self.receive_handshake_message::<Finalization>().await?;
let (finalization, _) = self.receive_handshake_message::<Finalization>().await?;
finalization.ensure_success()?;
Ok(())
}
pub(crate) async fn perform_client_handshake(
mut self,
) -> Result<SharedGatewayKey, HandshakeError>
) -> Result<HandshakeResult, HandshakeError>
where
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin,
R: CryptoRng + RngCore,
@@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use crate::shared_key::SharedKeyUsageError;
use crate::GatewayProtocolVersion;
use crate::GatewayProtocolVersionExt;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -34,4 +36,10 @@ pub enum HandshakeError {
#[error("timed out waiting for a handshake message")]
Timeout,
#[error("Connection is in an invalid state - please send a bug report")]
ConnectionInInvalidState,
#[error("the gateway requests protocol version that's not supported by this client. it wants to use v{version} whilst we only understand up to v{}", GatewayProtocolVersion::CURRENT)]
UnsupportedProtocol { version: GatewayProtocolVersion },
}
@@ -5,9 +5,11 @@ use crate::registration::handshake::messages::{
HandshakeMessage, Initialisation, MaterialExchange,
};
use crate::registration::handshake::state::State;
use crate::registration::handshake::SharedGatewayKey;
use crate::registration::handshake::HandshakeResult;
use crate::registration::handshake::{error::HandshakeError, WsItem};
use crate::{GatewayProtocolVersion, GatewayProtocolVersionExt};
use futures::{Sink, Stream};
use tracing::{debug, warn};
use tungstenite::Message as WsMessage;
impl<S, R> State<'_, S, R> {
@@ -18,11 +20,39 @@ impl<S, R> State<'_, S, R> {
where
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin,
{
// NEGOTIATE PROTOCOL
// old clients were sending protocol version as defined by the following:
/*
fn request_protocol_version(&self) -> u8 {
if self.derive_aes256_gcm_siv_key {
AES_GCM_SIV_PROTOCOL_VERSION
} else if self.expects_credential_usage {
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION
} else {
INITIAL_PROTOCOL_VERSION
}
}
*/
// meaning the highest possible value they could have sent was `4` (AUTHENTICATE_V2_PROTOCOL_VERSION)
// so if we received anything higher than that, it means they understand negotiation.
// currently not strictly needed as we just blindly accept what they proposed,
// but will be needed in the future.
if self.proposed_protocol_version().is_future_version() {
// this should never happen in a non-malicious client as it should use at most whatever version this gateway has announced
self.set_protocol_version(GatewayProtocolVersion::CURRENT)
} else {
// currently we accept all protocols, i.e. legacy keys, aes128, etc. so we downgrade to whatever
// the client has proposed. this will change in the future
debug!(
"using the protocol version proposed by the client: {:?}",
self.proposed_protocol_version()
)
}
// 1. receive remote ed25519 pubkey alongside ephemeral x25519 pubkey and maybe a flag indicating non-legacy client
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_NON_LEGACY
let init_message = Initialisation::try_from_bytes(&raw_init_message)?;
self.update_remote_identity(init_message.identity);
self.set_aes256_gcm_siv_key_derivation(!init_message.is_legacy());
// 2. derive shared keys locally
// hkdf::<blake3>::(g^xy)
@@ -39,7 +69,12 @@ impl<S, R> State<'_, S, R> {
self.send_handshake_data(material).await?;
// 4. wait for the remote response with their own encrypted signature
let materials = self.receive_handshake_message::<MaterialExchange>().await?;
let (materials, client_protocol) =
self.receive_handshake_message::<MaterialExchange>().await?;
if client_protocol != self.proposed_protocol_version() {
warn!("the client hasn't accepted our proposed protocol version. we suggested {:?} while it returned {client_protocol:?}", self.proposed_protocol_version());
// TBD what to do here
}
// 5. verify the received signature using the locally derived keys
self.verify_remote_key_material(&materials, &init_message.ephemeral_dh)?;
@@ -54,7 +89,7 @@ impl<S, R> State<'_, S, R> {
pub(crate) async fn perform_gateway_handshake(
mut self,
raw_init_message: Vec<u8>,
) -> Result<SharedGatewayKey, HandshakeError>
) -> Result<HandshakeResult, HandshakeError>
where
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin,
{
@@ -24,13 +24,6 @@ pub struct Initialisation {
pub initiator_salt: Option<Vec<u8>>,
}
impl Initialisation {
#[cfg(not(target_arch = "wasm32"))]
pub fn is_legacy(&self) -> bool {
self.initiator_salt.is_none()
}
}
#[derive(Debug)]
pub struct MaterialExchange {
pub signature_ciphertext: Vec<u8>,
@@ -99,8 +92,9 @@ impl HandshakeMessage for Initialisation {
let identity = ed25519::PublicKey::from_bytes(&bytes[..ed25519::PUBLIC_KEY_LENGTH])
.map_err(|_| HandshakeError::MalformedRequest)?;
// this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE
// SAFETY: this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE
// which is impossible
#[allow(clippy::unwrap_used)]
let ephemeral_dh =
x25519::PublicKey::from_bytes(&bytes[ed25519::PUBLIC_KEY_LENGTH..legacy_len]).unwrap();
@@ -194,6 +188,7 @@ impl HandshakeMessage for GatewayMaterialExchange {
// this can only fail if the provided bytes have len different from PUBLIC_KEY_SIZE
// which is impossible
#[allow(clippy::unwrap_used)]
let ephemeral_dh =
x25519::PublicKey::from_bytes(&bytes[..x25519::PUBLIC_KEY_SIZE]).unwrap();
let materials = MaterialExchange::try_from_bytes(&bytes[x25519::PUBLIC_KEY_SIZE..])?;

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