Compare commits

...

100 Commits

Author SHA1 Message Date
Jędrzej Stuczyński 944fc27ef6 add additional leniency in ticketbook requests 2026-05-28 11:05:24 +01:00
Jędrzej Stuczyński 3853c0f0c9 reduce concurrency during quorum check tests 2026-05-27 16:36:13 +01:00
Jędrzej Stuczyński 97f79381b9 NS: don't return nodes with 0 performance 2026-05-27 14:36:02 +01:00
benedettadavico 25eba09b92 update changelog 2026-05-27 11:00:31 +02:00
Jędrzej Stuczyński a8cecb1200 additional logs for quorum failures + increase staleness threshold 2026-05-26 14:05:24 +01:00
Jędrzej Stuczyński 4e52e9bf77 fixed invalid ticket rejection 2026-05-26 10:58:05 +01:00
Jędrzej Stuczyński cf55e2fe86 fix nym-api config deserialisation + clippy 2026-05-26 10:18:39 +01:00
Jędrzej Stuczyński dc0835f1f3 more logs, timeouts and general duct taping 2026-05-26 08:45:46 +01:00
Jędrzej Stuczyński b5a8b9d283 change default netstack download timeout to 30s 2026-05-23 20:50:11 +01:00
Jędrzej Stuczyński a395167139 more logs and going insane 2026-05-23 20:23:39 +01:00
Jędrzej Stuczyński 6b98c168fc dont assign nodes with 0 performance 2026-05-23 20:00:43 +01:00
Jędrzej Stuczyński 4645de3eb5 add logs on failure to submit testrun results 2026-05-23 17:18:45 +01:00
Jędrzej Stuczyński e6dd670b16 quroum checker logs + temp: increase of DefaultBodyLimit 2026-05-23 16:40:43 +01:00
Jędrzej Stuczyński dc48750271 ensure sufficient number of tickets before performing testrun assignment 2026-05-23 14:26:49 +01:00
Jędrzej Stuczyński 46c67440bb Mixnode stress testing (#6575)
* Squashing the mix stress testing branch (#6575)

reduced chain watcher per block log severity

update network monitors contract semver to 1.0.0

fix build issues

fix mixnet client dropping initial packet on egress reconnection

adjusted logs for network monitor agent

changed default testing interval to 2h

refresh NM contract information

explicit return type for batch submission

for mixnet listener task to get scheduled before beginning connectivity test

make sure to always use canonical ip for network monitor noise keys

feat: NMv3: make agents decide egress port (#6746)

add config v12->v13 config migration for nym nodes

fix formatting in wallet types

simplified client config creation

remove other swagger redirect

removed swagger redirect on /swagger/ route

log version info on startup

add workflows, contract address, and dockerfile

bugfix: use correct endpoints when setting up orchestrator (#6733)

clippy

adjust DEFAULT_MIN_STRESS_TESTED_NODES ratio

expose route with new performance metrics

fixes and additional docs

use stress testing scores

stub for usage of stress testing scores

stub traits

added new fields to nym-api config controlling usage of stress test data

guard against duplicate packets

prevent usage of chain_authorisation_check_max_attempts with value of 0

make sure duplicate results cant be inserted into the db

submit test results from orchestrator on an interval

docs and fixes

nym-api side of handling result submission

stubs for submitting results

NM orchestrator verifying nym-api result submission permissions

NM orchestrator to update announced key on startup

allow NM orchestrator to announce its identity key to the contract

stubs within nym-api for accepting NMv3 results

added additional metrics

docs

bugfixes + making sure to only assign mixnode testruns

fixed node refresher to only retrieve mixnodes and add additional metrics

topology metrics

defined basic prometheus metrics

authorised endpoint for returning prometheus data

create initial stub for prometheus metrics

post rebasing fixes

adjusted routes

missing implementation for storage getters

a lot of new stubs and db accessors

stubs for results endpoints

update utoipa tags for agent rountes

shared auth between metrics and results

moved stale results eviction into the interval.tick branch

refactor and comments

create background process to evict stale data

include sphinx packet delay as part of the stats

fix mock construction

add median to the calculated latency distribution

remove unused imports

cleanup

performing testrun and submitting the results

assigning testruns to requesting agents

basic stub for http server for the NMv3 orchestrator

chore: rename existing 'NetworkMonitorAgent' to 'NodeStressTester'

make sure to use canonical ips within the noise config

fixed contract tests

cargo fmt

additional comments and unit tests

contract and nym-node support of NM agents being run on the same host

basic unit tests

refactoring

make agents retrieve mix port assignment from the orchestrator

provide sensible defaults to CLI arguments

stub the initial structure for the agent

chore: remove redundant import

missed tick behaviour

removed redundant mutex

removed redundant try_get_client

reuse existing constant for default nymnode port

add node refresher for periodic scraping of bonded nym-node details

- NodeRefresher periodically queries the mixnet contract for all bonded
  nodes and probes each node's HTTP API for host information, sphinx keys,
  noise keys, and key rotation IDs
- Extract NymNodeApiClientRetriever into nym-node-requests with port
  probing, identity verification, and host information signature checking
- Add clone_query_client on NyxdClient so the refresher can hold its own
  query client without locking the signing client
- Batch upsert for nym_node rows (single transaction instead of per-row)
- Reuse the new helpers in nym-api's node_describe_cache

ensure assignment of testrun begins an IMMEDIATE tx

construction of the orchestrator struct

initial set of cli args

make sure to not assign testable nodes too often

very initial database structure and cli

fixed construction of RoutableNetworkMonitors

remove redundant constructor for NoiseNode

forbid 0-nonsense config values

add type safety for test route construction

moved lioness and arrayref to workspace deps

fixed dockerfile build

always use canonical addresses in RoutableNetworkMonitors

fixed old contract formatting issues

removed redundant into() call

network monitor agent fixes

additional logs

config unit tests

more docs

standalone stress testing invocation

further refactoring and changes

refactor testing loop and return valid test result upon completion

initial sending/receiving test loop

generating reusable sphinx headers

additional structure for receiving ingress packets

initial scaffolding for NMv3 agent

added validation of x25519 noise key

removed unstable call to 'is_multiple_of'

remove calls to from_octets as they're unavailable in pre 1.91

additional docs/comments

propagating noise information about NM for mixnet routing

pass full socket address of the agent into the contract storage

feat: store noise keys alongside ip addresses within the contract

removed redundant comment

ensure NM packets can only go to NM

PR review comments

added additional docs

allow NM to replay packets + fix replay prometheus metrics

propagate information about nm agent to connection handler

updated nym-node config migration

feat: introduced nym-node websocket subscription for keeping updated list of NM agents

allow admin to also revoke monitor agents

remove agents upon orchestrator removal

fixed schema generation and regenerated the contract schema

removed rustc restriction on contracts-common

added client methods for interacting with the contract

added unit tests for contract methods

implemented logic of the network monitors contract

create initial structure for network monitors contract

start mix stress testing topic branch

* make nym-node default to the new blockstream rpc/ws node cluster

* reduced mixnet-client log severity

* set network monitors contract address for mainnet
2026-05-22 09:43:20 +01:00
benedettadavico e5cd9fd69e bump versions 2026-05-21 17:24:08 +02:00
Jack Wampler 21c14c0df0 Re-order default API urls for network details - waterloo (#6799) 2026-05-20 08:48:07 +01:00
Simon Wicky 87c236a927 ipr version revert on develop (#6772) 2026-05-15 09:42:30 +02:00
Andrej Mihajlov b501ddd534 Migrate to hickory 0.26.1 (#6751)
* Migrate to hickory 0.26.1
2026-05-08 12:25:07 -06:00
Tommy Verrall e9f6d1d47a Merge pull request #6738 from nymtech/dependabot/cargo/nym-wallet/openssl-0.10.79
Bump openssl from 0.10.72 to 0.10.79 in /nym-wallet
2026-05-06 19:27:31 +02:00
Tommy Verrall 52b4490e80 Merge pull request #6741 from nymtech/fix/issue-windows
Fix windows open log viewer
2026-05-06 17:13:12 +02:00
Tommy Verrall 7b30c83f9a Linting
- Add changelog for future release notes
2026-05-06 17:03:25 +02:00
Tommy Verrall 4aabb4ed56 Fix windows open log viewer
- There was tendency where webview would just freeze on windows, lets ensure this doesn't happen.
2026-05-06 16:55:58 +02:00
Tommy Verrall b14c28a462 Add environment variable for Windows signing 2026-05-06 16:25:13 +02:00
Tommy Verrall 664782c0c6 Merge pull request #6740 from nymtech/feature/nym-wallet-delegation-log-hardening
Delegation query cache, log webview streaming, HTTPS webviews
2026-05-06 16:02:38 +02:00
Tommy Verrall aeb2f1f0f6 Okay mr Rabbit
- PR comments by mr rabbit were valid
2026-05-06 15:45:15 +02:00
Tommy Verrall 268ba36700 Update changelog 2026-05-06 15:41:55 +02:00
Tommy Verrall c4df05157a Tighten delegation summary query keys and rewards context
- Remove redeemAllRewards / TRewardsTransaction from rewards context
- Use a dedicated React Query key when no client address is set
2026-05-06 15:35:02 +02:00
Tommy Verrall 09548a9aa9 Delegation query cache, log webview streaming, HTTPS webviews
- Use_https_scheme(true) on log window builder
- Delegation data is loaded and refreshed via TanStack Query
2026-05-06 15:19:13 +02:00
Tommy Verrall 78b796bf24 Merge pull request #6681 from nymtech/feature/wallet-investigation
Nym Wallet: deps updates, clipboard/updater/, icon, polishing...
2026-05-06 15:02:41 +02:00
import this f5ab7b3eb6 [DOCs/operators]: Release notes for v2026.9-venaco (#6739)
* add changelog notes

* bump up version

* semi-atomated data update

* fix spacing
2026-05-06 14:12:24 +02:00
Tommy Verrall 9cf679dadb Fix default workspace packages 2026-05-06 10:38:28 +02:00
Tommy Verrall 97a382520c Pin ESLint 8 and align @typescript-eslint to restore yarn lint
- Fix CI yarn lint after ESLint 9 switched to flat config by default while the repo still uses legacy .eslintrc / eslintConfig. Add Yarn resolutions for eslint@8.57.1 and a single @typescript-eslint@5.62.0 line so parser and typescript-estree stay in sync
2026-05-06 10:21:39 +02:00
Tommy Verrall f87ce06865 Pin ESLint 8 and align @typescript-eslint to restore yarn lint
- Fix CI yarn lint after ESLint 9 switched to flat config by default while the repo still uses legacy .eslintrc / eslintConfig. Add Yarn resolutions for eslint@8.57.1 and a single @typescript-eslint@5.62.0 line so parser and typescript-estree stay in sync
2026-05-06 10:20:09 +02:00
Tommy Verrall 6095215a73 Merge branch 'develop' into feature/wallet-investigation 2026-05-06 09:49:30 +02:00
benedetta davico 8c6ff79cd1 Merge pull request #6736 from nymtech/master
Keep branches synced
2026-05-06 08:23:38 +02:00
dependabot[bot] 16678537f7 Bump openssl from 0.10.72 to 0.10.79 in /nym-wallet
Bumps [openssl](https://github.com/rust-openssl/rust-openssl) from 0.10.72 to 0.10.79.
- [Release notes](https://github.com/rust-openssl/rust-openssl/releases)
- [Commits](https://github.com/rust-openssl/rust-openssl/compare/openssl-v0.10.72...openssl-v0.10.79)

---
updated-dependencies:
- dependency-name: openssl
  dependency-version: 0.10.79
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-06 06:03:47 +00:00
benedetta davico ae877e3867 Merge pull request #6735 from nymtech/release/2026.9-venaco
Merge release/2026.9-venaco to master
2026-05-06 08:00:15 +02:00
benedetta davico 21479bfb80 Merge pull request #6734 from nymtech/release/2026.9-venaco
Merge release/2026.9-venaco to develop
2026-05-06 08:00:08 +02:00
benedettadavico f84de25302 update changelog 2026-05-06 07:16:42 +02:00
benedetta davico db8edfe752 Merge pull request #6729 from nymtech/bdq/add-workflows
add workflows for NM3
2026-05-05 10:01:27 +02:00
benedettadavico 73edf28f39 add workflows for NM3 2026-05-05 08:26:23 +02:00
benedetta davico d23a42f7f5 credential proxy pool (#6726)
* fix?

* version

* unit test

* additional logs for stalled deposits

---------

Co-authored-by: benedettadavico <benedettadavico@users.noreply.github.com>
Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
2026-05-01 09:21:28 +01:00
mfahampshire d0f2c08cd1 Move pricing into table format (#6722) 2026-05-01 07:17:30 +00:00
benedetta davico 5599987d89 Merge pull request #6723 from nymtech/bdq/bump-cred-proxy
Bump cred proxy version
2026-04-30 18:01:50 +02:00
rachyandco a93763d73b Merge pull request #6688 from nymtech/fix/dns-ttl-reduce-cache-and-shuffle-ips
Title: fix(dns): shuffle resolved IPs
2026-04-30 16:21:22 +02:00
benedetta davico 8e8b6f4467 Bump version to 0.3.1 in Cargo.toml 2026-04-30 15:00:53 +02:00
mfahampshire 7feeed41d5 tweak subheading (#6721) 2026-04-30 09:09:49 +00:00
mfahampshire e9a20653b8 Components autogenerate + BYON section in NymVPNCLI docs (#6719) 2026-04-30 08:56:29 +00:00
Jędrzej Stuczyński 9438691506 Merge pull request #6718 from nymtech/chore/ipr-clippy
chore: made sphinx version threshold assertion a compile time check
2026-04-30 09:43:44 +01:00
p17o 84a4924e77 nym-node-setup: plumb HOST_SSH_PORT through tunnel manager, CLI, and env setup (#6633)
* network-tunnel-manager: make SSH port configurable

* Rename SSH_PORT to HOST_SSH_PORT.

* setup: plumb HOST_SSH_PORT through env and CLI

* setup-env-vars: persist HOST_SSH_PORT in env.sh

---------

Co-authored-by: p17o <p17o>
2026-04-30 07:10:21 +00:00
rachyandco 49277310ba Apply suggestion from @jmwample
Co-authored-by: Jack Wampler <jmwample@users.noreply.github.com>
2026-04-30 05:57:09 +02:00
rachyandco 944d2eb7d5 Apply suggestion from @jmwample
Co-authored-by: Jack Wampler <jmwample@users.noreply.github.com>
2026-04-30 05:56:43 +02:00
rachyandco bfaf17540e Apply suggestion from @jmwample
Co-authored-by: Jack Wampler <jmwample@users.noreply.github.com>
2026-04-30 05:56:09 +02:00
rachyandco 6dbc4efbd9 Apply suggestion from @jmwample
Co-authored-by: Jack Wampler <jmwample@users.noreply.github.com>
2026-04-30 05:55:39 +02:00
p17o cabbeaf1bf Handle split IPv4/IPv6 uplinks in network-tunnel-manager (#6640)
* Handle separate IPv4 and IPv6 uplink interfaces in network-tunnel-manager

* check_forward_chain() now checks IPv6 and is less brittle overall; missing IPv6 uplink detection now degrades to a loud warning plus partial IPv4-only setup rather than hard-failing early

* fix typos; fix UDP port 4443 being configured but not tested

---------

Co-authored-by: p17o <p17o>
2026-04-29 15:42:29 +00:00
benedettadavico e554f1e0ad bump versions 2026-04-28 15:02:40 +02:00
benedetta davico 62a4a2ed70 Merge pull request #6710 from nymtech/bdq/versioning-fix 2026-04-28 10:01:52 +02:00
benedetta davico caad74c73d Merge pull request #6713 from nymtech/bdq/nym-binaries-ci
update CI runners
2026-04-27 15:39:44 +02:00
benedettadavico 917993d8fb clean 2026-04-27 12:17:31 +02:00
benedettadavico 1451db39e6 warn 2026-04-27 11:27:41 +02:00
benedettadavico f13a2a6c06 change to warn level 2026-04-27 10:45:42 +02:00
benedetta davico ce39fb6675 Update publish-nym-binaries.yml 2026-04-27 10:20:10 +02:00
benedettadavico 02a926b74a addressing comments 2026-04-27 10:10:08 +02:00
benedetta davico 54ba710ea0 Change CI platform from ubuntu-22.04 to arc-ubuntu-22.04 2026-04-27 09:33:57 +02:00
benedettadavico 2653d12e55 fix ipr msg, and unit tests 2026-04-24 16:07:49 +02:00
benedettadavico f94d6d51cf adding debugging traces 2026-04-24 14:11:19 +02:00
Andrej Mihajlov a0116f9aec Merge pull request #6708 from nymtech/am/lazy-init-dns
Only init SHARED_CLIENT if requested
2026-04-23 18:36:57 +02:00
Tommy Verrall aaa8ee9d53 Revert "Merge remote-tracking branch 'origin/develop' into chore/eslint-9-flat-config-migration"
This reverts commit ab0f6af4b9, reversing
changes made to cca19f36c2.
2026-04-23 15:42:27 +01:00
Tommy Verrall ab0f6af4b9 Merge remote-tracking branch 'origin/develop' into chore/eslint-9-flat-config-migration 2026-04-23 15:40:01 +01:00
Tommy Verrall 7669d0933f Migrate ESLint config to flat config for ESLint 9
Dependabot bumped eslint to ^9 across the lint-scoped TS packages but did
not migrate the legacy .eslintrc.* configs, breaking CI lint on develop.

Behavior preserved: yarn lint passes locally with the same effective rule
coverage as the pre-bump setup. Pre-existing warnings in nym-wallet and
mui-theme are unchanged. Orphan .eslintrc files in sdk/typescript outside
the lerna lint scope are left untouched.
2026-04-23 15:39:11 +01:00
Andrej Mihajlov 50433fe265 Only init SHARED_CLIENT if requested 2026-04-23 16:29:02 +02:00
benedettadavico 42aade29eb more v9 fixes 2026-04-23 13:28:17 +02:00
benedettadavico 9f26759b8d v9 bugfix 2026-04-23 13:28:17 +02:00
benedettadavico 9e642c6354 v9 bugfix 2026-04-23 13:28:16 +02:00
mfahampshire cca19f36c2 Remove unused header (#6699) 2026-04-22 09:35:59 +00:00
Merve 17894880e0 Changelog urda (#6698)
* test identity

* changelog update
2026-04-22 08:41:12 +00:00
benedetta davico a99b8348d7 Merge pull request #6697 from nymtech/release/2026.8-urda
merge release/2026.8-urda to master
2026-04-21 13:14:31 +02:00
benedetta davico ef6fc82c39 Merge pull request #6696 from nymtech/release/2026.8-urda
final merge todevelop
2026-04-21 13:14:23 +02:00
benedettadavico 0c83ae2408 duplicate description 2026-04-21 12:06:23 +02:00
benedetta davico 92490731e7 Merge pull request #6691 from nymtech/merge/release/2026.8-urda
Merge release/2026.8 urda
2026-04-20 14:28:34 +02:00
benedettadavico 0ce93e366e Merge branch 'release/2026.8-urda' into develop 2026-04-20 14:10:23 +02:00
benedettadavico 0d031875f6 merge conflicts 2026-04-20 14:07:38 +02:00
benedettadavico e6103e4c43 update changelog 2026-04-20 13:58:11 +02:00
Rachyandco bf85e9eb79 fix(dns): reduce positive TTL to 60s and shuffle resolved IPs
The 1800s minimum TTL defeated CDN failover mechanisms (e.g. Fastly
  publishes 30–60s A-record TTLs specifically to signal when edge nodes
  are removed). Dead IPs were cached for up to 30 minutes with no
  way for the client to recover without a restart.

  - Drop DEFAULT_POSITIVE_LOOKUP_CACHE_TTL from 1800s to 60s so that
    CDN-signalled failovers take effect within a minute
  - Shuffle resolved IPs on each lookup so retries cycle through all
    available edge nodes rather than hitting the same dead address
  - Add invalidate_preresolve_entry / invalidate_preresolve_for API
    for callers that want targeted per-host cache eviction on hard
    connection failures
2026-04-19 23:03:26 +02:00
Tommy Verrall 3f1e04ebd4 Fix ubuntu CI issues - update readme 2026-04-17 19:41:13 +02:00
Tommy Verrall d4c5131bcb Attempt at windows CI 2026-04-17 19:21:42 +02:00
Tommy Verrall ef1c1b50d5 More CI fixes 2026-04-17 18:49:41 +02:00
Tommy Verrall 23b745d353 Fix windows workflow 2026-04-17 18:41:25 +02:00
Tommy Verrall 3dc94cc85a Send Max UX, shared address helper, CI and desktop packaging
- Wallet CI, Tauri, webpack, routes
- Send and Sahred UI
- Wallet app build and readme
2026-04-17 14:41:20 +02:00
Tommy Verrall a4c4345257 More linting 2026-04-17 13:30:17 +02:00
Tommy Verrall a0fb92cf17 More clippy and linting 2026-04-17 12:36:20 +02:00
Tommy Verrall 52cc77356e Adjust lefthook for usage on GUI's like GitKraken 2026-04-17 12:19:58 +02:00
Tommy Verrall a671084f4e Linting and fixing CI 2026-04-17 12:18:57 +02:00
Tommy Verrall 3ae986acc8 Tauri prod CSP for Emotion/MUI and window maximize ACL
- Tauri was injecting nonces/hashes into style-src, which disables
'unsafe-inline' and blocked Emotion/MUI runtime <style> tags.
- Grant core:window:allow-maximize so frontend maximize() passes ACL.
- Add node-status and explorer helpers plus chart mappers; Jest coverage
- NodeOperatorInsights on BondedNymNode; optional API moniker/location
- Shared MUI Emotion cache (speedy: false) and CacheProvider wiring
- SendInputModal: amount/recipient validation timing; memoized fee check
- AuthLayout refresh; NodeTable overflow-x; Bonding error title typo fix
2026-04-17 12:05:01 +02:00
Tommy Verrall 754994ba01 Removing the misleading log tag and adding a brief comment. 2026-04-17 12:05:01 +02:00
Tommy Verrall 33b181b26b Fix routing for main window, loading modal, and error polish 2026-04-17 12:05:01 +02:00
Tommy Verrall 809559e6dc Nym Wallet: deps updates, clipboard/updater/, icon, polishing...
This rolls together desktop wallet hardening, UX polish, and operational fixes we have been carrying in the branch. The goal is safer defaults, less noisy background behaviour.

Security
- Tighten the Tauri CSP for production and keep connect-src aligned with real needs.
- Add a safe URL opener path (allowlisted schemes / validation) so user-influenced links do not become an open redirect surface.
- Replace unwrap usage in mixnet account flows with proper errors and propagation.
- Add an internal threat-model note so future changes keep the same assumptions explicit.

Clipboard and desktop
- Add a window-level Tauri clipboard hook for normal inputs, with clear exclusions for
  currency fields, auth-sensitive paste, and opt-in replace-paste fields.
- Wire an Edit menu (cut, copy, paste, select all) where it helps, and keep behaviour
  consistent with the hook.
- Deduplicate clipboard field props and satisfy ESLint on optional paste handlers.
Updater and vesting operations
- Treat legacy static updater JSON (missing per-platform signatures) as a soft failure with a clear warning, instead of erroring the version check IPC
- Cut vesting polling spam when the chain has no vesting account for the address, and map vesting "no account" to a dedicated BackendError for stable handling on the client.
- Move high-frequency vesting query logs to debug and keep removed-query stubs at warn.

Icons and first-run chrome
- Regenerate macOS/Windows icon assets from a padded 1024 master so dock and switcher visual weight matches other apps; add a small script to regenerate from app-icon-source.png.
- Default the app to dark mode, paint the HTML shell and webview background in the same dark base colour

Housekeeping
- Mock app context defaults to dark for consistency with the new baseline.
Validation run locally where relevant: Rust check, TypeScript check, ESLint, and icon
regeneration script smoke run.

- Remove storybook and old webdriver tests too
2026-04-17 12:05:00 +02:00
benedettadavico e32c042c8d version bump 2026-04-17 11:03:03 +02:00
benedetta davico 00cc2f215a Merge pull request #6647 from nymtech/release/2026.7-tola
Merge release/2026.7-tola
2026-04-09 14:18:23 +02:00
508 changed files with 30324 additions and 12043 deletions
@@ -36,7 +36,7 @@ jobs:
strategy:
fail-fast: false
matrix:
platform: [ubuntu-22.04]
platform: [arc-ubuntu-22.04]
runs-on: ${{ matrix.platform }}
env:
@@ -0,0 +1,41 @@
name: ci-nym-wallet-frontend
on:
pull_request:
paths:
- 'nym-wallet/**'
- '.github/workflows/ci-nym-wallet-frontend.yml'
jobs:
types-lint:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v4
with:
node-version-file: nym-wallet/.nvmrc
cache: yarn
cache-dependency-path: yarn.lock
- name: Install dependencies
run: yarn install --network-timeout 100000
- name: Build TypeScript packages (wallet depends on @nymproject/types, etc.)
run: yarn build:types
- name: Build @nymproject/mui-theme and @nymproject/react (wallet imports subpaths)
run: yarn build:packages
- name: Typecheck nym-wallet
run: yarn --cwd nym-wallet tsc
- name: Lint nym-wallet
run: yarn --cwd nym-wallet lint
- name: Yarn audit (workspace lockfile; informational)
run: yarn audit --level critical
continue-on-error: true
- name: Unit tests (nym-wallet)
run: yarn --cwd nym-wallet test
+16
View File
@@ -41,6 +41,9 @@ jobs:
sed -i.bak '1s/^/\[profile.dev\]\ndebug = false\n\n/' Cargo.toml
git diff
- name: Ensure nym-wallet/dist exists for Tauri
run: mkdir -p nym-wallet/dist
- name: Build all binaries
uses: actions-rs/cargo@v1
with:
@@ -71,3 +74,16 @@ jobs:
with:
command: clippy
args: --manifest-path nym-wallet/Cargo.toml --workspace --all-features --all-targets -- -D warnings
- name: Install cargo-audit
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-audit --locked
- name: Cargo audit (nym-wallet workspace)
uses: actions-rs/cargo@v1
with:
command: audit
working-directory: nym-wallet
continue-on-error: true
@@ -1,53 +0,0 @@
name: ci-nym-wallet-storybook
on:
pull_request:
paths:
- 'nym-wallet/**'
- '.github/workflows/ci-nym-wallet-storybook.yml'
jobs:
build:
runs-on: arc-linux-latest-dind
steps:
- uses: actions/checkout@v6
- name: Install rsync
run: sudo apt-get install rsync
continue-on-error: true
- uses: rlespinasse/github-slug-action@v3.x
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup yarn
run: npm install -g yarn
- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ vars.REQUIRED_RUSTC_VERSION }}
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Build dependencies
run: yarn && yarn build
- name: Build storybook
run: yarn storybook:build
working-directory: ./nym-wallet
- name: Deploy branch to CI www (storybook)
continue-on-error: true
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
ARGS: "-rltgoDzvO --delete"
SOURCE: "nym-wallet/storybook-static/"
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/wallet-${{ env.GITHUB_REF_SLUG }}
EXCLUDE: "/dist/, /node_modules/"
@@ -21,7 +21,7 @@ jobs:
uses: actions/checkout@v6
- name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
run: sudo apt-get update && sudo apt-get install -y libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
if: matrix.os == 'ubuntu-22.04'
- name: Install rust toolchain
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-22.04
- os: arc-ubuntu-22.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
@@ -26,7 +26,7 @@ jobs:
- name: Node
uses: actions/setup-node@v4
with:
node-version: 21
node-version: 22.13.0
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
@@ -30,7 +30,7 @@ jobs:
- name: Node
uses: actions/setup-node@v4
with:
node-version: 21
node-version: 22.13.0
cache: 'yarn'
- name: Install Rust toolchain
@@ -72,6 +72,41 @@ jobs:
find target/release/bundle -type d -name "*appimage*" -o -name "*AppImage*" || echo "No AppImage directories found"
find target/release/bundle -name "*.AppImage" -o -name "*.appimage" || echo "No AppImage files found"
fi
- name: Inspect AppImage (hook + bundled graphics libs)
shell: bash
run: |
set -euo pipefail
APPIMAGE_REL=$(find target/release/bundle -name '*.AppImage' | head -n 1)
if [ -z "${APPIMAGE_REL}" ]; then
echo "No AppImage under target/release/bundle"
exit 1
fi
APPIMAGE_ABS="${GITHUB_WORKSPACE}/nym-wallet/${APPIMAGE_REL}"
chmod +x "${APPIMAGE_ABS}"
EXTRACT_DIR=$(mktemp -d)
cd "${EXTRACT_DIR}"
"${APPIMAGE_ABS}" --appimage-extract
# Tauri only stages appimage "files" under /usr/ into the AppDir; paths like /apprun-hooks/ never reach the image.
# Wayland + WEBKIT_DISABLE_DMABUF_RENDERER defaults are applied in main() instead (see configure_linux_wayland_defaults).
HOOK=$(find squashfs-root -name '99-nym-wayland.sh' 2>/dev/null | head -n 1)
if [ -n "${HOOK}" ]; then
echo "Found legacy apprun hook at ${HOOK}"
else
echo "No apprun-hooks/99-nym-wayland.sh (expected): Wayland defaults are set in-process."
fi
find squashfs-root/usr/lib -maxdepth 6 \
\( -name 'libwayland-client.so*' -o -name 'libEGL.so*' -o -name 'libgbm.so*' \) \
2>/dev/null | sort > "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt"
wc -l "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt"
head -50 "${GITHUB_WORKSPACE}/nym-wallet/appimage-bundled-graphics-libs.txt" || true
- name: Upload AppImage graphics lib inventory
uses: actions/upload-artifact@v6
with:
name: nym-wallet-appimage-lib-inventory
path: nym-wallet/appimage-bundled-graphics-libs.txt
retention-days: 30
- name: Create AppImage tarball if needed
run: |
+63 -26
View File
@@ -26,6 +26,9 @@ jobs:
outputs:
release_tag: ${{ github.ref_name }}
env:
SIGN_WINDOWS: ${{ github.event_name == 'release' || inputs.sign }}
steps:
- uses: actions/checkout@v6
@@ -37,48 +40,82 @@ jobs:
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v2
# No cache:yarn here: setup-node needs yarn on PATH to populate the cache, but this runner
# only gets yarn from the step below.
- name: Node
uses: actions/setup-node@v4
with:
node-version: 21
node-version: 22.13.0
- name: Install Yarn (classic)
shell: bash
run: npm install -g yarn@1.22.22
- name: Strip Authenticode thumbprint (avoid signtool on runner)
working-directory: nym-wallet/src-tauri
if: ${{ env.SIGN_WINDOWS == 'true' || (github.event_name == 'workflow_dispatch' && !inputs.sign) }}
shell: bash
run: |
set -euo pipefail
if ! command -v yq >/dev/null 2>&1; then
echo "yq is required on this runner to edit tauri.conf.json"
exit 1
fi
yq eval --inplace '
del(.bundle.windows.certificateThumbprint) |
del(.bundle.windows.digestAlgorithm) |
del(.bundle.windows.timestampUrl)
' tauri.conf.json
- name: Download EV CodeSignTool from ssl.com
working-directory: nym-wallet/src-tauri
if: ${{ inputs.sign }}
if: env.SIGN_WINDOWS == 'true'
shell: bash
run: |
curl -L0 https://www.ssl.com/download/codesigntool-for-linux-and-macos/ -o codesigntool.zip
unzip codesigntool.zip
- name: Get EV certificate credential id
working-directory: nym-wallet/src-tauri
if: ${{ inputs.sign }}
if: env.SIGN_WINDOWS == 'true'
id: get_credential_ids
shell: bash
run: |
echo "SSL_COM_CREDENTIAL_ID=$(./CodeSignTool.sh get_credential_ids -username=${{ secrets.SSL_COM_USERNAME }} -password=${{ secrets.SSL_COM_PASSWORD }} | sed -n '1!p' | sed 's/- //')" >> "$GITHUB_OUTPUT"
- name: Add custom sign command to tauri.conf.json
working-directory: nym-wallet/src-tauri
if: ${{ inputs.sign }}
if: env.SIGN_WINDOWS == 'true'
shell: bash
env:
SSL_SIGN_USER: ${{ secrets.SSL_COM_USERNAME }}
SSL_SIGN_PASS: ${{ secrets.SSL_COM_PASSWORD }}
SSL_SIGN_CRED: ${{ steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
SSL_SIGN_TOTP: ${{ secrets.SSL_COM_TOTP_SECRET }}
run: |
yq eval --inplace '.bundle.windows +=
{
"signCommand": {
"cmd": "C:\Program Files\Git\bin\bash.EXE",
"args": [
"/c/actions-runner/_work/nym/nym/nym-wallet/src-tauri/CodeSignTool.sh",
"sign",
"-username ${{ secrets.SSL_COM_USERNAME }}",
"-password ${{ secrets.SSL_COM_PASSWORD }}",
"-credential_id ${{ steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}",
"-totp_secret ${{ secrets.SSL_COM_TOTP_SECRET }}",
"-program_name NymWallet",
"-input_file_path",
"%1",
"-override"
]
set -euo pipefail
if ! command -v cygpath >/dev/null 2>&1; then
echo "cygpath not found; install Git for Windows or use bash from Git SDK"
exit 1
fi
export SCRIPT_UNIX="$(cygpath -u "$GITHUB_WORKSPACE/nym-wallet/src-tauri/CodeSignTool.sh")"
yq eval --inplace '
.bundle.windows += {
"signCommand": {
"cmd": "C:/Program Files/Git/bin/bash.exe",
"args": [
strenv(SCRIPT_UNIX),
"sign",
("-username " + strenv(SSL_SIGN_USER)),
("-password " + strenv(SSL_SIGN_PASS)),
("-credential_id " + strenv(SSL_SIGN_CRED)),
("-totp_secret " + strenv(SSL_SIGN_TOTP)),
"-program_name NymWallet",
"-input_file_path",
"%1",
"-override"
]
}
}
}' tauri.conf.json
' tauri.conf.json
- name: Install project dependencies
shell: bash
run: cd .. && yarn --network-timeout 100000
@@ -93,10 +130,10 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
SSL_COM_USERNAME: ${{ inputs.sign && secrets.SSL_COM_USERNAME }}
SSL_COM_PASSWORD: ${{ inputs.sign && secrets.SSL_COM_PASSWORD }}
SSL_COM_CREDENTIAL_ID: ${{ inputs.sign && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
SSL_COM_TOTP_SECRET: ${{ inputs.sign && secrets.SSL_COM_TOTP_SECRET }}
SSL_COM_USERNAME: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_USERNAME }}
SSL_COM_PASSWORD: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_PASSWORD }}
SSL_COM_CREDENTIAL_ID: ${{ env.SIGN_WINDOWS == 'true' && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
SSL_COM_TOTP_SECRET: ${{ env.SIGN_WINDOWS == 'true' && secrets.SSL_COM_TOTP_SECRET }}
run: |
echo "Starting build process..."
yarn build
@@ -167,4 +204,4 @@ jobs:
needs: publish-tauri
with:
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
secrets: inherit
secrets: inherit
@@ -0,0 +1,61 @@
name: Build and upload Network Monitor Agent container to harbor.nymte.ch
on:
workflow_dispatch:
inputs:
release_image:
description: 'Tag image as a release (prefix with golden-)'
required: true
default: false
type: boolean
env:
WORKING_DIRECTORY: "nym-network-monitor-v3/nym-network-monitor-agent"
CONTAINER_NAME: "network-monitor-agent"
jobs:
build-container:
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
- name: Checkout repo
uses: actions/checkout@v6
- name: Configure git identity
run: |
git config --global user.email "lawrence@nymtech.net"
git config --global user.name "Lawrence Stalder"
- name: Get version from Cargo.toml
id: get_version
run: |
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
echo "result=$VERSION" >> $GITHUB_OUTPUT
- name: Set GIT_TAG variable
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
- name: Initialize RELEASE_TAG
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
- name: Set RELEASE_TAG for release
if: github.event.inputs.release_image == 'true'
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
- name: Set IMAGE_NAME_AND_TAGS variable
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $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: Build and push image to Harbor
run: |
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
@@ -0,0 +1,57 @@
name: Build and upload Network Monitor Orchestrator container to harbor.nymte.ch
on:
workflow_dispatch:
inputs:
release_image:
description: 'Tag image as a release (prefix with golden-)'
required: true
default: false
type: boolean
env:
WORKING_DIRECTORY: "nym-network-monitor-v3/nym-network-monitor-orchestrator"
CONTAINER_NAME: "network-monitor-orchestrator"
jobs:
build-container:
runs-on: arc-linux-latest-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
- name: Checkout repo
uses: actions/checkout@v6
- name: Configure git identity
run: |
git config --global user.email "lawrence@nymtech.net"
git config --global user.name "Lawrence Stalder"
- name: Get version from Cargo.toml
id: get_version
run: |
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
echo "result=$VERSION" >> $GITHUB_OUTPUT
- name: Initialize RELEASE_TAG
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
- name: Set RELEASE_TAG for release
if: github.event.inputs.release_image == 'true'
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
- name: Set IMAGE_NAME_AND_TAGS variable
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
- name: Log image name
run: echo "RELEASE_TAG='$RELEASE_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
- name: Build and push image to Harbor
run: |
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
+1
View File
@@ -27,6 +27,7 @@ v6-topology.json
/explorer/public/downloads/mixmining.json
/explorer/public/downloads/topology.json
/nym-wallet/dist/*
/nym-wallet/appimage-bundled-graphics-libs.txt
/clients/validator/examples/nym-driver-example/current-contract.txt
validator-api/v4.json
validator-api/v6.json
+74
View File
@@ -4,6 +4,80 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2026.10-waterloo] (2026-05-27)
- Re-order default API urls for network details - Waterloo release ([#6799])
- [bugfix] IPR v8<->v9 mismatch on Waterloo ([#6772])
- Migrate to hickory 0.26.1 ([#6751])
- add workflows for NM3 ([#6729])
- credential proxy pool ([#6726])
- chore: made sphinx version threshold assertion a compile time check ([#6718])
- Feat/nmv3 updated performance calculation ([#6714])
- feat: NMv3: submission of stress testing result into nym-api ([#6709])
- feat: NMv3: Prometheus metrics for network monitor ([#6693])
- feat: NMv3: add read-only results API to orchestrator ([#6689])
- feat: NMv3: Eviction of stale testrun data ([#6685])
- feat: NMv3: Wire up testrun assignment and result submission flow ([#6680])
- feat: NMv3: Support multiple network monitor agents per host ([#6679])
- Feat/nmv3 agent announcement ([#6673])
- add node refresher for periodic scraping of bonded nym-node details ([#6626])
- Feat/nmv3 orchestrator queue ([#6597])
- feat: network monitor agent - standalone node stress-testing ([#6582])
- [feat] propagate NM agent noise keys to nym-node routing ([#6577])
- start mix stress testing topic branch ([#6575])
- Feat/nmv3 agents subscription ([#6567])
- Feat/nmv3 agents contract ([#6555])
[#6799]: https://github.com/nymtech/nym/pull/6799
[#6772]: https://github.com/nymtech/nym/pull/6772
[#6751]: https://github.com/nymtech/nym/pull/6751
[#6729]: https://github.com/nymtech/nym/pull/6729
[#6726]: https://github.com/nymtech/nym/pull/6726
[#6718]: https://github.com/nymtech/nym/pull/6718
[#6714]: https://github.com/nymtech/nym/pull/6714
[#6709]: https://github.com/nymtech/nym/pull/6709
[#6693]: https://github.com/nymtech/nym/pull/6693
[#6689]: https://github.com/nymtech/nym/pull/6689
[#6685]: https://github.com/nymtech/nym/pull/6685
[#6680]: https://github.com/nymtech/nym/pull/6680
[#6679]: https://github.com/nymtech/nym/pull/6679
[#6673]: https://github.com/nymtech/nym/pull/6673
[#6626]: https://github.com/nymtech/nym/pull/6626
[#6597]: https://github.com/nymtech/nym/pull/6597
[#6582]: https://github.com/nymtech/nym/pull/6582
[#6577]: https://github.com/nymtech/nym/pull/6577
[#6575]: https://github.com/nymtech/nym/pull/6575
[#6567]: https://github.com/nymtech/nym/pull/6567
[#6555]: https://github.com/nymtech/nym/pull/6555
## [2026.9-venaco] (2026-05-06)
- Fix for v9 IPR ([#6710])
- Only init SHARED_CLIENT if requested ([#6708])
- Fixes to crates and CI ([#6686])
- Return ipv6 addresses as well ([#6684])
- Fix invalid ticket spend ([#6683])
- Block non-public IPR/NR checks ([#6670])
[#6710]: https://github.com/nymtech/nym/pull/6710
[#6708]: https://github.com/nymtech/nym/pull/6708
[#6686]: https://github.com/nymtech/nym/pull/6686
[#6684]: https://github.com/nymtech/nym/pull/6684
[#6683]: https://github.com/nymtech/nym/pull/6683
[#6670]: https://github.com/nymtech/nym/pull/6670
## [2026.8-urda] (2026-04-20)
- Include all gateways in the returned list ([#6649])
- Optimize GW probe in NS agent ([#6636])
- Max/sdk docrs ([#6566])
- Max/sdk stream wrapper ([#6320])
[#6649]: https://github.com/nymtech/nym/pull/6649
[#6636]: https://github.com/nymtech/nym/pull/6636
[#6566]: https://github.com/nymtech/nym/pull/6566
[#6320]: https://github.com/nymtech/nym/pull/6320
## [2026.7-tola] (2026-04-07)
- Simon/ecash contract serde fix ([#6634])
Generated
+307 -51
View File
@@ -57,7 +57,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
@@ -253,7 +253,7 @@ checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
dependencies = [
"base64ct",
"blake2 0.10.6",
"cpufeatures",
"cpufeatures 0.2.17",
"password-hash",
]
@@ -1309,7 +1309,18 @@ checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
name = "chacha20"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601"
dependencies = [
"cfg-if",
"cpufeatures 0.3.0",
"rand_core 0.10.1",
]
[[package]]
@@ -1319,7 +1330,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"chacha20 0.9.1",
"cipher",
"poly1305",
"zeroize",
@@ -1831,6 +1842,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
dependencies = [
"libc",
]
[[package]]
name = "crc"
version = "3.3.0"
@@ -2095,7 +2115,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"curve25519-dalek-derive",
"digest 0.10.7",
"fiat-crypto",
@@ -2799,18 +2819,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "enum-as-inner"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn 2.0.106",
]
[[package]]
name = "env_filter"
version = "0.1.3"
@@ -3271,6 +3279,7 @@ dependencies = [
"cfg-if",
"libc",
"r-efi",
"rand_core 0.10.1",
"wasip2",
"wasip3",
]
@@ -3630,26 +3639,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ebdb29d2ea9ed0083cd8cece49bbd968021bd99b0849edb4a9a7ee0fdf6a4e0"
[[package]]
name = "hickory-proto"
version = "0.25.2"
name = "hickory-net"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8a6fe56c0038198998a6f217ca4e7ef3a5e51f46163bd6dd60b5c71ca6c6502"
checksum = "e2295ed2f9c31e471e1428a8f88a3f0e1f4b27c15049592138d1eebe9c35b183"
dependencies = [
"async-trait",
"bytes",
"cfg-if",
"data-encoding",
"enum-as-inner",
"futures-channel",
"futures-io",
"futures-util",
"h2 0.4.11",
"hickory-proto",
"http 1.3.1",
"idna",
"ipnet",
"once_cell",
"rand 0.9.2",
"ring",
"jni 0.22.4",
"rand 0.10.1",
"rustls 0.23.37",
"thiserror 2.0.12",
"tinyvec",
@@ -3657,31 +3665,56 @@ dependencies = [
"tokio-rustls 0.26.2",
"tracing",
"url",
"webpki-roots 0.26.11",
"webpki-roots 1.0.2",
]
[[package]]
name = "hickory-proto"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bab31817bfb44672a252e97fe81cd0c18d1b2cf892108922f6818820df8c643"
dependencies = [
"data-encoding",
"idna",
"ipnet",
"jni 0.22.4",
"once_cell",
"prefix-trie",
"rand 0.10.1",
"ring",
"thiserror 2.0.12",
"tinyvec",
"tracing",
"url",
]
[[package]]
name = "hickory-resolver"
version = "0.25.2"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc62a9a99b0bfb44d2ab95a7208ac952d31060efc16241c87eaf36406fecf87a"
checksum = "f0d58d28879ceecde6607729660c2667a081ccdc082e082675042793960f178c"
dependencies = [
"cfg-if",
"futures-util",
"hickory-net",
"hickory-proto",
"ipconfig",
"ipnet",
"jni 0.22.4",
"moka",
"ndk-context",
"once_cell",
"parking_lot",
"rand 0.9.2",
"rand 0.10.1",
"resolv-conf",
"rustls 0.23.37",
"smallvec",
"system-configuration 0.7.0",
"thiserror 2.0.12",
"tokio",
"tokio-rustls 0.26.2",
"tracing",
"webpki-roots 0.26.11",
"webpki-roots 1.0.2",
]
[[package]]
@@ -4409,6 +4442,9 @@ name = "ipnet"
version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
dependencies = [
"serde",
]
[[package]]
name = "ipnetwork"
@@ -4503,19 +4539,68 @@ dependencies = [
"cesu8",
"cfg-if",
"combine",
"jni-sys",
"jni-sys 0.3.0",
"log",
"thiserror 1.0.69",
"walkdir",
"windows-sys 0.45.0",
]
[[package]]
name = "jni"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [
"cfg-if",
"combine",
"jni-macros",
"jni-sys 0.4.1",
"log",
"simd_cesu8",
"thiserror 2.0.12",
"walkdir",
"windows-link 0.2.1",
]
[[package]]
name = "jni-macros"
version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
"proc-macro2",
"quote",
"rustc_version 0.4.1",
"simd_cesu8",
"syn 2.0.106",
]
[[package]]
name = "jni-sys"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
[[package]]
name = "jni-sys"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
dependencies = [
"jni-sys-macros",
]
[[package]]
name = "jni-sys-macros"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
dependencies = [
"quote",
"syn 2.0.106",
]
[[package]]
name = "jobserver"
version = "0.1.33"
@@ -4582,7 +4667,7 @@ version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653"
dependencies = [
"cpufeatures",
"cpufeatures 0.2.17",
]
[[package]]
@@ -5359,6 +5444,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "ndk-context"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b"
[[package]]
name = "netlink-packet-core"
version = "0.8.1"
@@ -5635,7 +5726,7 @@ dependencies = [
[[package]]
name = "nym-api"
version = "1.1.77"
version = "1.1.80"
dependencies = [
"anyhow",
"async-trait",
@@ -5880,7 +5971,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.74"
version = "1.1.77"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -5963,7 +6054,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.74"
version = "1.1.77"
dependencies = [
"bs58",
"clap",
@@ -6273,7 +6364,7 @@ dependencies = [
[[package]]
name = "nym-credential-proxy"
version = "0.3.0"
version = "0.3.2-rc"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -6472,6 +6563,8 @@ dependencies = [
"serde",
"thiserror 2.0.12",
"time",
"tokio",
"wasmtimer",
"zeroize",
]
@@ -6926,6 +7019,7 @@ dependencies = [
"bytes",
"cfg-if",
"encoding_rs",
"fastrand",
"hickory-resolver",
"http 1.3.1",
"inventory",
@@ -7028,6 +7122,7 @@ dependencies = [
"bytes",
"futures",
"nym-ip-packet-requests",
"nym-lp",
"nym-sdk",
"thiserror 2.0.12",
"tokio",
@@ -7325,7 +7420,6 @@ version = "1.20.4"
dependencies = [
"cargo_metadata 0.19.2",
"dotenvy",
"log",
"regex",
"schemars 0.8.22",
"serde",
@@ -7370,9 +7464,103 @@ dependencies = [
"utoipa-swagger-ui",
]
[[package]]
name = "nym-network-monitor-agent"
version = "1.0.2"
dependencies = [
"anyhow",
"arrayref",
"clap",
"futures",
"hkdf",
"humantime",
"lioness",
"nym-bin-common",
"nym-crypto",
"nym-network-monitor-orchestrator-requests",
"nym-noise",
"nym-pemstore",
"nym-sphinx-addressing",
"nym-sphinx-framing",
"nym-sphinx-params",
"nym-sphinx-types",
"nym-task",
"nym-test-utils",
"rand 0.8.5",
"sha2 0.10.9",
"time",
"tokio",
"tokio-util",
"tracing",
"url",
"x25519-dalek",
"zeroize",
]
[[package]]
name = "nym-network-monitor-orchestrator"
version = "1.0.2"
dependencies = [
"anyhow",
"axum 0.7.9",
"clap",
"futures",
"humantime",
"nym-api-requests",
"nym-bin-common",
"nym-crypto",
"nym-http-api-common",
"nym-metrics",
"nym-network-defaults",
"nym-network-monitor-orchestrator-requests",
"nym-node-requests",
"nym-task",
"nym-test-utils",
"nym-validator-client",
"rand 0.8.5",
"sqlx",
"strum 0.28.0",
"thiserror 2.0.12",
"time",
"tokio",
"tracing",
"url",
"utoipa",
"utoipa-swagger-ui",
"utoipauto",
"zeroize",
]
[[package]]
name = "nym-network-monitor-orchestrator-requests"
version = "1.20.4"
dependencies = [
"anyhow",
"humantime-serde",
"nym-crypto",
"nym-http-api-client",
"serde",
"time",
"tracing",
"utoipa",
"zeroize",
]
[[package]]
name = "nym-network-monitors-contract-common"
version = "1.20.4"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"schemars 0.8.22",
"serde",
"thiserror 2.0.12",
]
[[package]]
name = "nym-network-requester"
version = "1.1.75"
version = "1.1.78"
dependencies = [
"addr",
"anyhow",
@@ -7422,7 +7610,7 @@ dependencies = [
[[package]]
name = "nym-node"
version = "1.29.0"
version = "1.32.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -7491,6 +7679,7 @@ dependencies = [
"nym-verloc",
"nym-wireguard",
"nym-wireguard-types",
"nyxd-scraper-shared",
"opentelemetry",
"opentelemetry_sdk",
"rand 0.8.5",
@@ -7541,6 +7730,7 @@ dependencies = [
"nym-exit-policy",
"nym-http-api-client",
"nym-kkt-ciphersuite",
"nym-network-defaults",
"nym-noise-keys",
"nym-test-utils",
"nym-upgrade-mode-check",
@@ -7559,7 +7749,7 @@ dependencies = [
[[package]]
name = "nym-node-status-agent"
version = "2.0.0"
version = "2.0.1-rc3"
dependencies = [
"anyhow",
"clap",
@@ -7580,7 +7770,7 @@ dependencies = [
[[package]]
name = "nym-node-status-api"
version = "4.6.1"
version = "4.6.2-rc10"
dependencies = [
"ammonia",
"anyhow",
@@ -7767,7 +7957,7 @@ name = "nym-outfox"
version = "1.20.4"
dependencies = [
"blake3",
"chacha20",
"chacha20 0.9.1",
"chacha20poly1305",
"fastrand",
"sphinx-packet",
@@ -7974,7 +8164,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.74"
version = "1.1.77"
dependencies = [
"bs58",
"clap",
@@ -8479,6 +8669,7 @@ dependencies = [
"nym-mixnet-contract-common",
"nym-multisig-contract-common",
"nym-network-defaults",
"nym-network-monitors-contract-common",
"nym-performance-contract-common",
"nym-serde-helpers",
"nym-vesting-contract-common",
@@ -8772,7 +8963,7 @@ dependencies = [
[[package]]
name = "nymvisor"
version = "0.1.39"
version = "0.1.42"
dependencies = [
"anyhow",
"bytes",
@@ -9449,7 +9640,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf"
dependencies = [
"cpufeatures",
"cpufeatures 0.2.17",
"opaque-debug 0.3.1",
"universal-hash",
]
@@ -9461,7 +9652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"opaque-debug 0.3.1",
"universal-hash",
]
@@ -9540,6 +9731,17 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c"
[[package]]
name = "prefix-trie"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cf6e3177f0684016a5c209b00882e15f8bdd3f3bb48f0491df10cd102d0c6e7"
dependencies = [
"either",
"ipnet",
"num-traits",
]
[[package]]
name = "pretty_assertions"
version = "1.4.1"
@@ -9837,6 +10039,17 @@ dependencies = [
"rand_core 0.9.3",
]
[[package]]
name = "rand"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2e8e8bcc7961af1fdac401278c6a831614941f6164ee3bf4ce61b7edb162207"
dependencies = [
"chacha20 0.10.0",
"getrandom 0.4.1",
"rand_core 0.10.1",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
@@ -9875,6 +10088,12 @@ dependencies = [
"getrandom 0.3.3",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "rand_distr"
version = "0.4.3"
@@ -10004,7 +10223,7 @@ dependencies = [
"serde_json",
"serde_urlencoded",
"sync_wrapper 0.1.2",
"system-configuration",
"system-configuration 0.5.1",
"tokio",
"tokio-rustls 0.24.1",
"tower-service",
@@ -10427,7 +10646,7 @@ checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"jni 0.21.1",
"log",
"once_cell",
"rustls 0.23.37",
@@ -11014,7 +11233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
@@ -11026,7 +11245,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
dependencies = [
"block-buffer 0.9.0",
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest 0.9.0",
"opaque-debug 0.3.1",
]
@@ -11038,7 +11257,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
@@ -11119,6 +11338,22 @@ version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simd_cesu8"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
dependencies = [
"rustc_version 0.4.1",
"simdutf8",
]
[[package]]
name = "simdutf8"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
[[package]]
name = "siphasher"
version = "0.3.11"
@@ -11720,7 +11955,18 @@ checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7"
dependencies = [
"bitflags 1.3.2",
"core-foundation 0.9.4",
"system-configuration-sys",
"system-configuration-sys 0.5.0",
]
[[package]]
name = "system-configuration"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b"
dependencies = [
"bitflags 2.9.1",
"core-foundation 0.9.4",
"system-configuration-sys 0.6.0",
]
[[package]]
@@ -11733,6 +11979,16 @@ dependencies = [
"libc",
]
[[package]]
name = "system-configuration-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "tagptr"
version = "0.2.0"
+13 -3
View File
@@ -44,6 +44,7 @@ members = [
"common/cosmwasm-smart-contracts/nym-performance-contract",
"common/cosmwasm-smart-contracts/nym-pool-contract",
"common/cosmwasm-smart-contracts/vesting-contract",
"common/cosmwasm-smart-contracts/network-monitors-contract",
"common/credential-proxy",
"common/credential-storage",
"common/credential-utils",
@@ -176,6 +177,8 @@ members = [
"integration-tests",
"common/nym-kkt-ciphersuite",
"common/nym-kkt-context",
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
"nym-network-monitor-v3/nym-network-monitor-agent", "nym-network-monitor-v3/nym-network-monitor-orchestrator-requests",
]
default-members = [
@@ -192,6 +195,8 @@ default-members = [
"service-providers/network-requester",
"tools/nymvisor",
"nym-registration-client",
"nym-network-monitor-v3/nym-network-monitor-orchestrator",
"nym-network-monitor-v3/nym-network-monitor-agent",
"tools/internal/localnet-orchestrator"
]
@@ -280,8 +285,8 @@ getrandom03 = { package = "getrandom", version = "=0.3.3" }
glob = "0.3"
handlebars = "3.5.5"
hex = "0.4.3"
hickory-proto = "0.25.2"
hickory-resolver = "0.25.2"
hickory-proto = "0.26.1"
hickory-resolver = "0.26.1"
hkdf = "0.12.3"
hmac = "0.12.1"
http = "1"
@@ -402,6 +407,10 @@ zeroize = "1.7.0"
prometheus = { version = "0.14.0" }
# recreating lioness
# we don't care about particular versions - just pull whatever is used by sphinx
lioness = "*"
arrayref = "*"
# libcrux
libcrux-kem = "0.0.7"
@@ -411,7 +420,7 @@ libcrux-chacha20poly1305 = "0.0.7"
libcrux-psq = "0.0.8"
libcrux-ml-kem = "0.0.8"
libcrux-sha3 = "0.0.8"
libcrux-traits = "0.0.8"
libcrux-traits = "0.0.6"
# Workspace dep definitions required by crates.io publication - we need a workspace version since `cargo workspaces` doesn't work with path imports from crate manifests
nym-api-requests = { version = "1.20.4", path = "nym-api/nym-api-requests" }
@@ -507,6 +516,7 @@ nym-types = { version = "1.20.4", path = "common/types" }
nym-upgrade-mode-check = { version = "1.20.4", path = "common/upgrade-mode-check" }
nym-validator-client = { version = "1.20.4", path = "common/client-libs/validator-client", default-features = false }
nym-vesting-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/vesting-contract" }
nym-network-monitors-contract-common = { version = "1.20.4", path = "common/cosmwasm-smart-contracts/network-monitors-contract" }
nym-verloc = { version = "1.20.4", path = "common/verloc" }
nym-wireguard = { version = "1.20.4", path = "common/wireguard" }
nym-wireguard-types = { version = "1.20.4", path = "common/wireguard-types" }
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-client"
description = "Implementation of the Nym Client"
version = "1.1.74"
version = "1.1.77"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
edition = "2021"
license.workspace = true
+1 -1
View File
@@ -1,7 +1,7 @@
[package]
name = "nym-socks5-client"
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
version = "1.1.74"
version = "1.1.77"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
edition = "2021"
license.workspace = true
@@ -240,7 +240,7 @@ mod nonwasm_sealed {
impl GatewaySender for LocalGateway {
async fn send_mix_packet(&mut self, packet: MixPacket) -> Result<(), ErasedGatewayError> {
self.packet_forwarder
.forward_packet(packet)
.forward_client_packet_without_delay(packet)
.map_err(erase_err)
}
}
@@ -34,3 +34,4 @@ client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
[dev-dependencies]
nym-crypto = { workspace = true }
rand = { workspace = true }
tokio = { workspace = true, features = ["macros", "io-util", "rt", "rt-multi-thread"] }
+277 -69
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use dashmap::DashMap;
use futures::StreamExt;
use futures::{SinkExt, StreamExt};
use nym_noise::config::NoiseConfig;
use nym_noise::upgrade_noise_initiator;
use nym_sphinx::forwarding::packet::MixPacket;
@@ -14,6 +14,7 @@ use std::ops::Deref;
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::net::TcpStream;
use tokio::sync::mpsc;
use tokio::sync::mpsc::error::TrySendError;
@@ -90,13 +91,17 @@ impl Deref for ActiveConnections {
pub struct ConnectionSender {
channel: mpsc::Sender<FramedNymPacket>,
current_reconnection_attempt: Arc<AtomicU32>,
// Identifies the `ManagedConnection` task currently owning this entry; used
// to ensure drop-time eviction only fires on the still-owning task.
handle_token: Arc<()>,
}
impl ConnectionSender {
fn new(channel: mpsc::Sender<FramedNymPacket>) -> Self {
fn new(channel: mpsc::Sender<FramedNymPacket>, handle_token: Arc<()>) -> Self {
ConnectionSender {
channel,
current_reconnection_attempt: Arc::new(AtomicU32::new(0)),
handle_token,
}
}
}
@@ -107,6 +112,31 @@ struct ManagedConnection {
message_receiver: ReceiverStream<FramedNymPacket>,
connection_timeout: Duration,
current_reconnection: Arc<AtomicU32>,
active_connections: ActiveConnections,
handle_token: Arc<()>,
}
// Evicts the cache entry on task exit (only if still owned by this task).
// Without this, a stale `ConnectionSender` survives after the peer disconnects
// and the next outbound packet is silently swallowed by the dead TCP.
struct EvictOnDrop {
active_connections: ActiveConnections,
address: SocketAddr,
handle_token: Arc<()>,
}
impl Drop for EvictOnDrop {
fn drop(&mut self) {
let address = self.address;
let handle_token = &self.handle_token;
self.active_connections.remove_if(&address, |_, sender| {
Arc::ptr_eq(&sender.handle_token, handle_token)
});
trace!(
peer = %address,
"managed connection task exited; evicted owning cache entry"
);
}
}
impl ManagedConnection {
@@ -116,6 +146,8 @@ impl ManagedConnection {
message_receiver: mpsc::Receiver<FramedNymPacket>,
connection_timeout: Duration,
current_reconnection: Arc<AtomicU32>,
active_connections: ActiveConnections,
handle_token: Arc<()>,
) -> Self {
ManagedConnection {
address,
@@ -123,72 +155,30 @@ impl ManagedConnection {
message_receiver: ReceiverStream::new(message_receiver),
connection_timeout,
current_reconnection,
active_connections,
handle_token,
}
}
async fn run(self) {
let address = self.address;
let _evict_guard = EvictOnDrop {
active_connections: self.active_connections,
address,
handle_token: self.handle_token,
};
let reconnection_attempt = self.current_reconnection.load(Ordering::Acquire);
let connect_start = tokio::time::Instant::now();
let connection_fut = TcpStream::connect(address);
let conn = match tokio::time::timeout(self.connection_timeout, connection_fut).await {
Ok(stream_res) => match stream_res {
Ok(stream) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
debug!(
peer = %address,
connect_ms,
"Managed to establish connection to {}", self.address
);
let noise_start = tokio::time::Instant::now();
let noise_stream =
match upgrade_noise_initiator(stream, &self.noise_config).await {
Ok(noise_stream) => noise_stream,
Err(err) => {
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
warn!(
event = "connection.failed.noise",
peer = %address,
error = %err,
connect_ms,
noise_handshake_ms,
reconnection_attempt,
exit_reason = "noise_error",
"Failed to perform Noise initiator handshake with {address}"
);
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
return;
}
};
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
self.current_reconnection.store(0, Ordering::Release);
debug!(
peer = %address,
connect_ms,
noise_handshake_ms,
"Noise initiator handshake completed for {:?}", address
);
Framed::new(noise_stream, NymCodec)
}
Err(err) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
warn!(
event = "connection.failed.connect",
peer = %address,
error = %err,
connect_ms,
reconnection_attempt,
exit_reason = "connect_error",
"failed to establish connection to {address}"
);
return;
}
},
// 1. attempt to establish the connection with timeout
let maybe_stream = match tokio::time::timeout(self.connection_timeout, connection_fut).await
{
Ok(stream) => stream,
Err(_) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
warn!(
debug!(
event = "connection.failed.timeout",
peer = %address,
timeout_ms = self.connection_timeout.as_millis() as u64,
@@ -203,21 +193,133 @@ impl ManagedConnection {
}
};
if let Err(err) = self.message_receiver.map(Ok).forward(conn).await {
warn!(
event = "connection.forward_error",
peer = %address,
error = %err,
exit_reason = "forward_error",
"Failed to forward packets to {address}: {err}"
);
}
// 2. check if it actually succeeded
let stream = match maybe_stream {
Ok(stream) => stream,
Err(err) => {
let connect_ms = connect_start.elapsed().as_millis() as u64;
debug!(
event = "connection.failed.connect",
peer = %address,
error = %err,
connect_ms,
reconnection_attempt,
exit_reason = "connect_error",
"failed to establish connection to {address}"
);
return;
}
};
let connect_ms = connect_start.elapsed().as_millis() as u64;
debug!(
peer = %address,
exit_reason = "sender_dropped",
"connection manager to {address} finished"
connect_ms,
"Managed to establish connection to {}", self.address
);
// 3. perform noise handshake (if applicable)
let noise_start = tokio::time::Instant::now();
let noise_stream = match upgrade_noise_initiator(stream, &self.noise_config).await {
Ok(noise_stream) => noise_stream,
Err(err) => {
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
debug!(
event = "connection.failed.noise",
peer = %address,
error = %err,
connect_ms,
noise_handshake_ms,
reconnection_attempt,
exit_reason = "noise_error",
"Failed to perform Noise initiator handshake with {address}"
);
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
return;
}
};
let noise_handshake_ms = noise_start.elapsed().as_millis() as u64;
self.current_reconnection.store(0, Ordering::Release);
debug!(
peer = %address,
connect_ms,
noise_handshake_ms,
"Noise initiator handshake completed for {:?}", address
);
let conn = Framed::new(noise_stream, NymCodec);
// 4. start handling the framed stream
run_io_loop(conn, self.message_receiver, address).await;
}
}
// The connection is unidirectional (send-only); we read from it solely to
// notice peer FIN/RST while idle so we can evict the cache entry before the
// next outbound send finds it stale.
async fn run_io_loop<T>(
conn: Framed<T, NymCodec>,
mut receiver: ReceiverStream<FramedNymPacket>,
address: SocketAddr,
) where
T: AsyncRead + AsyncWrite + Unpin,
{
let (mut sink, mut stream) = conn.split();
loop {
tokio::select! {
msg = stream.next() => {
match msg {
None => {
debug!(
peer = %address,
exit_reason = "peer_closed",
"peer closed mixnet connection to {address}"
);
break;
}
Some(Err(err)) => {
debug!(
event = "connection.read_error",
peer = %address,
error = %err,
exit_reason = "read_error",
"read error on mixnet connection to {address}: {err}"
);
break;
}
Some(Ok(_)) => {
trace!(
peer = %address,
"unexpected inbound packet on mixnet connection to {address}; discarding"
);
}
}
}
outgoing = receiver.next() => {
match outgoing {
None => {
debug!(
peer = %address,
exit_reason = "sender_dropped",
"connection manager to {address} finished"
);
break;
}
Some(packet) => {
if let Err(err) = sink.send(packet).await {
debug!(
event = "connection.forward_error",
peer = %address,
error = %err,
exit_reason = "forward_error",
"Failed to forward packet to {address}: {err}"
);
break;
}
}
}
}
}
}
}
@@ -264,13 +366,18 @@ impl Client {
sender.try_send(pending_packet).unwrap();
}
// Ownership token for the task we're about to spawn; lets it tell
// on exit whether the cache entry still names it.
let handle_token = Arc::new(());
// if we already tried to connect to `address` before, grab the current attempt count
let current_reconnection_attempt =
if let Some(mut existing) = self.active_connections.get_mut(&address) {
existing.channel = sender;
existing.handle_token = Arc::clone(&handle_token);
Arc::clone(&existing.current_reconnection_attempt)
} else {
let new_entry = ConnectionSender::new(sender);
let new_entry = ConnectionSender::new(sender, Arc::clone(&handle_token));
let current_attempt = Arc::clone(&new_entry.current_reconnection_attempt);
self.active_connections.insert(address, new_entry);
current_attempt
@@ -285,6 +392,7 @@ impl Client {
let connections_count = self.connections_count.clone();
let noise_config = self.noise_config.clone();
let active_connections = self.active_connections.clone();
tokio::spawn(async move {
// before executing the manager, wait for what was specified, if anything
if let Some(backoff) = backoff {
@@ -299,6 +407,8 @@ impl Client {
receiver,
initial_connection_timeout,
current_reconnection_attempt,
active_connections,
handle_token,
)
.run()
.await;
@@ -428,4 +538,102 @@ mod tests {
client.config.maximum_reconnection_backoff
);
}
fn test_addr() -> SocketAddr {
"127.0.0.1:1".parse().unwrap()
}
fn insert_with_token(
active: &ActiveConnections,
addr: SocketAddr,
token: Arc<()>,
) -> mpsc::Receiver<FramedNymPacket> {
let (tx, rx) = mpsc::channel(1);
active.insert(addr, ConnectionSender::new(tx, token));
rx
}
#[test]
fn evict_on_drop_removes_entry_when_token_still_matches() {
let active = ActiveConnections::default();
let addr = test_addr();
let token = Arc::new(());
let _rx = insert_with_token(&active, addr, Arc::clone(&token));
assert!(active.get(&addr).is_some());
{
let _guard = EvictOnDrop {
active_connections: active.clone(),
address: addr,
handle_token: token,
};
}
assert!(
active.get(&addr).is_none(),
"owning task's drop should evict the entry"
);
}
#[test]
fn evict_on_drop_preserves_entry_replaced_by_newer_make_connection() {
// Simulates the race: old task's run() has returned, but before its
// drop guard fires, a concurrent `make_connection` replaced the
// entry's channel + handle_token with a fresh task's token.
let active = ActiveConnections::default();
let addr = test_addr();
let old_token = Arc::new(());
let new_token = Arc::new(());
let _rx_new = insert_with_token(&active, addr, Arc::clone(&new_token));
{
let _guard = EvictOnDrop {
active_connections: active.clone(),
address: addr,
handle_token: old_token,
};
}
assert!(
active.get(&addr).is_some(),
"old task's drop must not clobber the newer entry"
);
}
#[tokio::test]
async fn io_loop_exits_when_peer_closes_idle_connection() {
// The fix's second half: while no packets are flowing, peer FIN/RST
// must still be observed so the cache entry can be evicted before the
// next send finds it stale.
let (a, b) = tokio::io::duplex(64);
let conn = Framed::new(a, NymCodec);
let (_tx, rx) = mpsc::channel(1);
let task = tokio::spawn(run_io_loop(conn, ReceiverStream::new(rx), test_addr()));
// Simulate peer closing both directions of the connection.
drop(b);
tokio::time::timeout(Duration::from_secs(1), task)
.await
.expect("io_loop must notice peer close while idle")
.expect("io_loop task must not panic");
}
#[tokio::test]
async fn io_loop_exits_when_sender_dropped() {
let (a, _b) = tokio::io::duplex(64);
let conn = Framed::new(a, NymCodec);
let (tx, rx) = mpsc::channel(1);
let task = tokio::spawn(run_io_loop(conn, ReceiverStream::new(rx), test_addr()));
drop(tx);
tokio::time::timeout(Duration::from_secs(1), task)
.await
.expect("io_loop must exit when the upstream sender is dropped")
.expect("io_loop task must not panic");
}
}
@@ -21,12 +21,16 @@ impl From<mpsc::UnboundedSender<PacketToForward>> for MixForwardingSender {
}
impl MixForwardingSender {
pub fn forward_packet(&self, packet: impl Into<PacketToForward>) -> Result<(), SendError> {
pub fn forward_packet(&self, packet: PacketToForward) -> Result<(), SendError> {
self.0
.unbounded_send(packet.into())
.unbounded_send(packet)
.map_err(|err| err.into_send_error())
}
pub fn forward_client_packet_without_delay(&self, packet: MixPacket) -> Result<(), SendError> {
self.forward_packet(PacketToForward::client_packet_without_delay(packet))
}
#[allow(clippy::len_without_is_empty)]
pub fn len(&self) -> usize {
self.0.len()
@@ -38,35 +42,23 @@ pub type MixForwardingReceiver = mpsc::UnboundedReceiver<PacketToForward>;
pub struct PacketToForward {
pub packet: MixPacket,
pub forward_delay_target: Option<Instant>,
}
impl From<MixPacket> for PacketToForward {
fn from(packet: MixPacket) -> Self {
PacketToForward::new_no_delay(packet)
}
}
impl From<(MixPacket, Option<Instant>)> for PacketToForward {
fn from((packet, delay_until): (MixPacket, Option<Instant>)) -> Self {
PacketToForward::new(packet, delay_until)
}
}
impl From<(MixPacket, Instant)> for PacketToForward {
fn from((packet, delay_until): (MixPacket, Instant)) -> Self {
PacketToForward::new(packet, Some(delay_until))
}
pub network_monitor_packet: bool,
}
impl PacketToForward {
pub fn new(packet: MixPacket, forward_delay_target: Option<Instant>) -> Self {
pub fn new(
packet: MixPacket,
forward_delay_target: Option<Instant>,
network_monitor_packet: bool,
) -> Self {
PacketToForward {
packet,
forward_delay_target,
network_monitor_packet,
}
}
pub fn new_no_delay(packet: MixPacket) -> Self {
Self::new(packet, None)
pub fn client_packet_without_delay(packet: MixPacket) -> Self {
Self::new(packet, None, false)
}
}
@@ -26,6 +26,7 @@ nym-ecash-contract-common = { workspace = true }
nym-multisig-contract-common = { workspace = true }
nym-group-contract-common = { workspace = true }
nym-performance-contract-common = { workspace = true }
nym-network-monitors-contract-common = { workspace = true }
nym-serde-helpers = { workspace = true, features = ["hex", "base64"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
@@ -104,6 +104,14 @@ impl TryFrom<NymNetworkDetails> for Config {
}
impl Config {
pub fn new(nyxd_url: Url, api_url: Url, nyxd_config: nyxd::Config) -> Self {
Config {
api_url,
nyxd_url,
nyxd_config,
}
}
pub fn try_from_nym_network_details(
details: &NymNetworkDetails,
) -> Result<Self, ValidatorClientError> {
@@ -114,6 +122,15 @@ impl Config {
.map(|url| Url::parse(url))
.collect::<Result<Vec<_>, _>>()?;
if let Some(nym_api_urls) = details.nym_api_urls.as_ref() {
api_url.extend(
nym_api_urls
.iter()
.map(|url| url.url.parse())
.collect::<Result<Vec<_>, _>>()?,
);
}
if api_url.is_empty() {
return Err(ValidatorClientError::NoAPIUrlAvailable);
}
@@ -15,11 +15,15 @@ use nym_api_requests::ecash::models::{
VerifyEcashTicketBody,
};
use nym_api_requests::ecash::VerificationKeyResponse;
use nym_api_requests::models::network_monitor::{
KnownNetworkMonitorResponse, StressTestBatchSubmission,
};
use nym_api_requests::models::{
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
ChainStatusResponse, KeyRotationInfoResponse, NodePerformanceResponse, NodeRefreshBody,
NymNodeDescriptionV1, NymNodeDescriptionV2, PerformanceHistoryResponse, RewardedSetResponse,
SignerInformationResponse,
AnnotationResponseV1, ApiHealthResponse, BinaryBuildInformationOwned,
ChainBlocksStatusResponse, ChainStatusResponse, KeyRotationInfoResponse,
NodePerformanceResponse, NodeRefreshBody, NymNodeDescriptionV1, NymNodeDescriptionV2,
PerformanceHistoryResponse, RewardedSetResponse, SignerInformationResponse,
StressTestBatchSubmissionResponse,
};
use nym_api_requests::pagination::PaginatedResponse;
use nym_http_api_client::{ApiClient, NO_PARAMS};
@@ -976,7 +980,7 @@ pub trait NymApiClientExt: ApiClient {
async fn get_node_annotation(
&self,
node_id: NodeId,
) -> Result<AnnotationResponse, NymAPIError> {
) -> Result<AnnotationResponseV1, NymAPIError> {
self.get_json(
&[
routes::V1_API_VERSION,
@@ -1359,6 +1363,53 @@ pub trait NymApiClientExt: ApiClient {
Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
}
/// Queries the nym-api for whether a particular ed25519 identity key is currently recognised
/// as an authorised network monitor permitted to submit stress testing results.
///
/// `identity_key` is expected to be the base58-encoded form of the ed25519 public key.
#[instrument(level = "debug", skip(self))]
async fn get_known_network_monitor(
&self,
identity_key: IdentityKeyRef<'_>,
) -> Result<KnownNetworkMonitorResponse, NymAPIError> {
self.get_json(
&[
routes::V3_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::STRESS_TESTING,
routes::STRESS_TESTING_KNOWN_MONITORS,
identity_key,
],
NO_PARAMS,
)
.await
}
/// Submit a signed batch of stress-testing results to nym-api on behalf of a network monitor
/// orchestrator.
///
/// The caller is expected to have produced `request` via
/// `StressTestBatchSubmissionContent::new(...)` and signed it with the orchestrator's ed25519
/// key; nym-api will reject submissions that are stale, replayed, unauthorised, or whose
/// signature fails to verify.
#[instrument(level = "debug", skip(self, request))]
async fn submit_stress_testing_results(
&self,
request: &StressTestBatchSubmission,
) -> Result<StressTestBatchSubmissionResponse, NymAPIError> {
self.post_json(
&[
routes::V3_API_VERSION,
routes::NYM_NODES_ROUTES,
routes::STRESS_TESTING,
routes::STRESS_TESTING_BATCH_SUBMIT,
],
NO_PARAMS,
request,
)
.await
}
}
// Client is already nym_http_api_client::Client (re-exported above), so just one impl needed
@@ -49,6 +49,9 @@ pub mod nym_nodes {
pub const NYM_NODES_REWARDED_SET: &str = "rewarded-set";
pub const NYM_NODES_REFRESH_DESCRIBED: &str = "refresh-described";
pub const BY_ADDRESSES: &str = "by-addresses";
pub const STRESS_TESTING: &str = "stress-testing";
pub const STRESS_TESTING_KNOWN_MONITORS: &str = "known-monitors";
pub const STRESS_TESTING_BATCH_SUBMIT: &str = "batch-submit";
}
pub const STATUS_ROUTES: &str = "status";
@@ -13,6 +13,7 @@ pub mod ecash_query_client;
pub mod group_query_client;
pub mod mixnet_query_client;
pub mod multisig_query_client;
pub mod network_monitors_query_client;
pub mod performance_query_client;
pub mod vesting_query_client;
@@ -22,6 +23,7 @@ pub mod ecash_signing_client;
pub mod group_signing_client;
pub mod mixnet_signing_client;
pub mod multisig_signing_client;
pub mod network_monitors_signing_client;
pub mod performance_signing_client;
pub mod vesting_signing_client;
@@ -31,6 +33,9 @@ pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient};
pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient};
pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient};
pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient};
pub use network_monitors_query_client::{
NetworkMonitorsQueryClient, PagedNetworkMonitorsQueryClient,
};
pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
@@ -40,6 +45,7 @@ pub use ecash_signing_client::EcashSigningClient;
pub use group_signing_client::GroupSigningClient;
pub use mixnet_signing_client::MixnetSigningClient;
pub use multisig_signing_client::MultisigSigningClient;
pub use network_monitors_signing_client::NetworkMonitorsSigningClient;
pub use performance_signing_client::PerformanceSigningClient;
pub use vesting_signing_client::VestingSigningClient;
@@ -49,6 +55,7 @@ pub trait NymContractsProvider {
fn mixnet_contract_address(&self) -> Option<&AccountId>;
fn vesting_contract_address(&self) -> Option<&AccountId>;
fn performance_contract_address(&self) -> Option<&AccountId>;
fn network_monitors_contract_address(&self) -> Option<&AccountId>;
// coconut-related
fn ecash_contract_address(&self) -> Option<&AccountId>;
@@ -62,6 +69,7 @@ pub struct TypedNymContracts {
pub mixnet_contract_address: Option<AccountId>,
pub vesting_contract_address: Option<AccountId>,
pub performance_contract_address: Option<AccountId>,
pub network_monitors_contract_address: Option<AccountId>,
pub ecash_contract_address: Option<AccountId>,
pub group_contract_address: Option<AccountId>,
@@ -86,6 +94,10 @@ impl TryFrom<NymContracts> for TypedNymContracts {
.performance_contract_address
.map(|addr| addr.parse())
.transpose()?,
network_monitors_contract_address: value
.network_monitors_contract_address
.map(|addr| addr.parse())
.transpose()?,
ecash_contract_address: value
.ecash_contract_address
.map(|addr| addr.parse())
@@ -0,0 +1,107 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::collect_paged;
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::error::NyxdError;
use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use nym_network_monitors_contract_common::{
AuthorisedNetworkMonitor, AuthorisedNetworkMonitorOrchestratorsResponse,
AuthorisedNetworkMonitorsPagedResponse, QueryMsg as NetworkMonitorsQueryMsg,
};
use serde::Deserialize;
use std::net::SocketAddr;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait NetworkMonitorsQueryClient {
async fn query_network_monitors_contract<T>(
&self,
query: NetworkMonitorsQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>;
async fn get_admin(&self) -> Result<cw_controllers::AdminResponse, NyxdError> {
self.query_network_monitors_contract(NetworkMonitorsQueryMsg::Admin {})
.await
}
async fn get_network_monitor_orchestrators(
&self,
) -> Result<AuthorisedNetworkMonitorOrchestratorsResponse, NyxdError> {
self.query_network_monitors_contract(
NetworkMonitorsQueryMsg::NetworkMonitorOrchestrators {},
)
.await
}
async fn get_network_monitor_agents_paged(
&self,
start_next_after: Option<SocketAddr>,
limit: Option<u32>,
) -> Result<AuthorisedNetworkMonitorsPagedResponse, NyxdError> {
self.query_network_monitors_contract(NetworkMonitorsQueryMsg::NetworkMonitorAgents {
start_next_after,
limit,
})
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait PagedNetworkMonitorsQueryClient: NetworkMonitorsQueryClient {
async fn get_all_network_monitor_agents(
&self,
) -> Result<Vec<AuthorisedNetworkMonitor>, NyxdError> {
collect_paged!(self, get_network_monitor_agents_paged, authorised)
}
}
#[async_trait]
impl<T> PagedNetworkMonitorsQueryClient for T where T: NetworkMonitorsQueryClient {}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> NetworkMonitorsQueryClient for C
where
C: CosmWasmClient + NymContractsProvider + Send + Sync,
{
async fn query_network_monitors_contract<T>(
&self,
query: NetworkMonitorsQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
let contract_address = &self
.network_monitors_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("network monitors contract"))?;
self.query_contract_smart(contract_address, &query).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_query_variants_are_covered<C: NetworkMonitorsQueryClient + Send + Sync>(
client: C,
msg: NetworkMonitorsQueryMsg,
) {
match msg {
NetworkMonitorsQueryMsg::Admin {} => client.get_admin().ignore(),
NetworkMonitorsQueryMsg::NetworkMonitorOrchestrators {} => {
client.get_network_monitor_orchestrators().ignore()
}
NetworkMonitorsQueryMsg::NetworkMonitorAgents { .. } => {
client.get_network_monitor_agents_paged(None, None).ignore()
}
};
}
}
@@ -0,0 +1,205 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
use crate::nyxd::error::NyxdError;
use crate::nyxd::{Coin, Fee, SigningCosmWasmClient};
use crate::signing::signer::OfflineSigner;
use async_trait::async_trait;
use nym_network_monitors_contract_common::ExecuteMsg as NetworkMonitorsExecuteMsg;
use std::net::SocketAddr;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait NetworkMonitorsSigningClient {
async fn execute_network_monitors_contract(
&self,
fee: Option<Fee>,
msg: NetworkMonitorsExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError>;
async fn update_admin(
&self,
admin: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::UpdateAdmin { admin };
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::UpdateAdmin".into(),
vec![],
)
.await
}
async fn authorise_network_monitor_orchestrator(
&self,
address: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address };
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitorOrchestrator".into(),
vec![],
)
.await
}
/// Announce (or rotate) the ed25519 identity key of the calling network monitor orchestrator.
///
/// The caller must already be an authorised orchestrator; the contract validates that
/// `identity_key` is a well-formed base-58 encoding of a 32-byte ed25519 public key.
async fn update_orchestrator_identity_key(
&self,
identity_key: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::UpdateOrchestratorIdentityKey { key: identity_key };
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::UpdateOrchestratorIdentityKey".into(),
vec![],
)
.await
}
async fn revoke_network_monitor_orchestrator(
&self,
address: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::RevokeNetworkMonitorOrchestrator { address };
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::RevokeNetworkMonitorOrchestrator".into(),
vec![],
)
.await
}
async fn authorise_network_monitor(
&self,
mixnet_address: SocketAddr,
bs58_x25519_noise: String,
noise_version: u8,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitor {
mixnet_address,
bs58_x25519_noise,
noise_version,
};
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::AuthoriseNetworkMonitor".into(),
vec![],
)
.await
}
async fn revoke_network_monitor(
&self,
address: SocketAddr,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::RevokeNetworkMonitor { address };
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::RevokeNetworkMonitor".into(),
vec![],
)
.await
}
async fn revoke_all_network_monitors(
&self,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
let msg = NetworkMonitorsExecuteMsg::RevokeAllNetworkMonitors;
self.execute_network_monitors_contract(
fee,
msg,
"NetworkMonitorsExecuteMsg::RevokeAllNetworkMonitors".into(),
vec![],
)
.await
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> NetworkMonitorsSigningClient for C
where
C: SigningCosmWasmClient + NymContractsProvider + Sync,
NyxdError: From<<Self as OfflineSigner>::Error>,
{
async fn execute_network_monitors_contract(
&self,
fee: Option<Fee>,
msg: NetworkMonitorsExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError> {
let contract_address = &self
.network_monitors_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("network monitors contract"))?;
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let signer_address = &self.signer_addresses()[0];
self.execute(signer_address, contract_address, &msg, fee, memo, funds)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
use nym_network_monitors_contract_common::ExecuteMsg;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_execute_variants_are_covered<C: NetworkMonitorsSigningClient + Send + Sync>(
client: C,
msg: NetworkMonitorsExecuteMsg,
) {
match msg {
NetworkMonitorsExecuteMsg::UpdateAdmin { admin } => {
client.update_admin(admin, None).ignore()
}
ExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address } => client
.authorise_network_monitor_orchestrator(address, None)
.ignore(),
ExecuteMsg::UpdateOrchestratorIdentityKey { key } => {
client.update_orchestrator_identity_key(key, None).ignore()
}
ExecuteMsg::RevokeNetworkMonitorOrchestrator { address } => client
.revoke_network_monitor_orchestrator(address, None)
.ignore(),
ExecuteMsg::AuthoriseNetworkMonitor {
mixnet_address: address,
bs58_x25519_noise,
noise_version,
} => client
.authorise_network_monitor(address, bs58_x25519_noise, noise_version, None)
.ignore(),
ExecuteMsg::RevokeNetworkMonitor { address } => {
client.revoke_network_monitor(address, None).ignore()
}
ExecuteMsg::RevokeAllNetworkMonitors => {
client.revoke_all_network_monitors(None).ignore()
}
};
}
}
@@ -36,7 +36,7 @@ pub mod logs;
pub mod module_traits;
pub mod types;
#[derive(Debug)]
#[derive(Debug, Clone)]
pub(crate) struct SigningClientOptions {
gas_price: GasPrice,
simulated_gas_multiplier: f32,
@@ -80,6 +80,17 @@ impl<C, S> MaybeSigningClient<C, S> {
opts,
}
}
pub(crate) fn clone_query_client(&self) -> MaybeSigningClient<C, NoSigner>
where
C: Clone,
{
MaybeSigningClient {
client: self.client.clone(),
signer: Default::default(),
opts: self.opts.clone(),
}
}
}
#[cfg(feature = "http-client")]
@@ -24,6 +24,8 @@ use async_trait::async_trait;
use cosmrs::tendermint::{abci, evidence::Evidence, Genesis};
use cosmrs::tx::{Raw, SignDoc};
use cosmwasm_std::Addr;
use nym_contracts_common::build_information::CONTRACT_BUILD_INFO_STORAGE_KEY;
use nym_contracts_common::ContractBuildInformation;
use nym_network_defaults::{ChainDetails, NymNetworkDetails};
use serde::{de::DeserializeOwned, Serialize};
use std::fmt::Debug;
@@ -40,6 +42,7 @@ pub use crate::nyxd::{
fee::Fee,
};
pub use crate::rpc::TendermintRpcClient;
pub use bip39;
pub use coin::Coin;
pub use cosmrs::{
bank::MsgSend,
@@ -70,14 +73,19 @@ pub use tendermint_rpc::{
Paging, Request, Response, SimpleRequest,
};
pub use nym_ecash_contract_common;
pub use nym_mixnet_contract_common;
pub use nym_multisig_contract_common;
pub use nym_network_monitors_contract_common;
pub use nym_performance_contract_common;
pub use nym_vesting_contract_common;
#[cfg(feature = "http-client")]
use crate::http_client;
#[cfg(feature = "http-client")]
use crate::{DirectSigningHttpRpcNyxdClient, QueryHttpRpcNyxdClient};
#[cfg(feature = "http-client")]
use cosmrs::rpc::{HttpClient, HttpClientUrl};
use nym_contracts_common::build_information::CONTRACT_BUILD_INFO_STORAGE_KEY;
use nym_contracts_common::ContractBuildInformation;
pub mod coin;
pub mod contract_traits;
@@ -262,6 +270,16 @@ impl<C, S> NyxdClient<C, S> {
}
}
pub fn clone_query_client(&self) -> NyxdClient<C>
where
C: Clone,
{
NyxdClient {
client: self.client.clone_query_client(),
config: self.config.clone(),
}
}
pub fn current_config(&self) -> &Config {
&self.config
}
@@ -289,6 +307,10 @@ impl<C, S> NyxdClient<C, S> {
pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) {
self.config.simulated_gas_multiplier = multiplier;
}
pub fn get_nym_contracts(&self) -> TypedNymContracts {
self.config.contracts.clone()
}
}
impl<C, S> NymContractsProvider for NyxdClient<C, S> {
@@ -303,6 +325,12 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
fn performance_contract_address(&self) -> Option<&AccountId> {
self.config.contracts.performance_contract_address.as_ref()
}
fn network_monitors_contract_address(&self) -> Option<&AccountId> {
self.config
.contracts
.network_monitors_contract_address
.as_ref()
}
fn ecash_contract_address(&self) -> Option<&AccountId> {
self.config.contracts.ecash_contract_address.as_ref()
@@ -0,0 +1,26 @@
[package]
name = "nym-network-monitors-contract-common"
description = "Common library for the Nym Network Monitors contract"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
readme.workspace = true
version.workspace = true
[dependencies]
thiserror = { workspace = true }
serde = { workspace = true }
schemars = { workspace = true }
cosmwasm-std = { workspace = true }
cosmwasm-schema = { workspace = true }
cw-controllers = { workspace = true }
[features]
schema = []
[lints]
workspace = true
@@ -0,0 +1,8 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod storage_keys {
pub const CONTRACT_ADMIN: &str = "contract-admin";
pub const AUTHORISED_ORCHESTRATORS: &str = "authorised-orchestrators";
pub const AUTHORISED_NETWORK_MONITORS: &str = "authorised-network-monitors";
}
@@ -0,0 +1,30 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Addr;
use cw_controllers::AdminError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum NetworkMonitorsContractError {
#[error("could not perform contract migration: {comment}")]
FailedMigration { comment: String },
#[error(transparent)]
Admin(#[from] AdminError),
#[error("unauthorised")]
Unauthorized,
#[error("address {addr} is not an authorised orchestrator")]
NotAnOrchestrator { addr: Addr },
#[error("Failed to recover x25519 public key from its base58 representation: {0}")]
MalformedX25519AgentNoiseKey(String),
#[error("Failed to recover ed25519 public key from its base58 representation: {0}")]
MalformedEd25519OrchestratorIdentityKey(String),
#[error(transparent)]
StdErr(#[from] cosmwasm_std::StdError),
}
@@ -0,0 +1,11 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod constants;
pub mod error;
pub mod msg;
pub mod types;
pub use error::*;
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
pub use types::*;
@@ -0,0 +1,78 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
use std::net::SocketAddr;
#[cfg(feature = "schema")]
use crate::{
AuthorisedNetworkMonitorOrchestratorsResponse, AuthorisedNetworkMonitorsPagedResponse,
};
#[cw_serde]
pub struct InstantiateMsg {
/// Address of the initial network monitor orchestrator.
pub orchestrator_address: String,
}
#[cw_serde]
pub enum ExecuteMsg {
/// Change the admin
UpdateAdmin { admin: String },
/// Authorise new network monitor orchestrator
AuthoriseNetworkMonitorOrchestrator { address: String },
/// Attempt to update the announced identity key of this orchestrator
UpdateOrchestratorIdentityKey { key: String },
/// Revoke network monitor orchestrator authorisation.
RevokeNetworkMonitorOrchestrator { address: String },
/// Authorise new network monitor (or renew authorisation)
/// granting additional privileges when sending mixnet packets to Nym nodes.
AuthoriseNetworkMonitor {
/// Mixnet address of the agent.
/// The underlying ip address is going to be used as ingress to the nodes,
/// and the full socket address announces the egress and the association with the noise key
mixnet_address: SocketAddr,
/// Base-58 encoded noise key of the agent.
bs58_x25519_noise: String,
/// Version of the noise protocol used by the agent.
noise_version: u8,
},
/// Revoke network monitor authorisation.
RevokeNetworkMonitor { address: SocketAddr },
/// Revoke all network monitor authorisations.
RevokeAllNetworkMonitors,
}
#[cw_serde]
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
pub enum QueryMsg {
#[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))]
Admin {},
// no need for pagination as we don't expect even a double digit of those
#[cfg_attr(
feature = "schema",
returns(AuthorisedNetworkMonitorOrchestratorsResponse)
)]
NetworkMonitorOrchestrators {},
#[cfg_attr(feature = "schema", returns(AuthorisedNetworkMonitorsPagedResponse))]
NetworkMonitorAgents {
/// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.
start_next_after: Option<SocketAddr>,
/// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.
limit: Option<u32>,
},
}
#[cw_serde]
pub struct MigrateMsg {}
@@ -0,0 +1,53 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Timestamp};
use std::net::SocketAddr;
pub type OrchestratorAddress = Addr;
#[cw_serde]
pub struct AuthorisedNetworkMonitorOrchestrator {
/// The address associated with the network monitor orchestrator.
pub address: Addr,
/// Base-58 encoded identity key of the orchestrator, announced by the orchestrator itself
/// on startup.
pub identity_key: Option<String>,
/// Timestamp of when the network monitor was authorised.
pub authorised_at: Timestamp,
}
#[cw_serde]
pub struct AuthorisedNetworkMonitor {
/// Mixnet address of the agent.
/// The underlying ip address is going to be used as ingress to the nodes,
/// and the full socket address announces the egress and the association with the noise key
pub mixnet_address: SocketAddr,
/// The address of the orchestrator that authorised the network monitor agent.
pub authorised_by: OrchestratorAddress,
/// Timestamp of when the network monitor was authorised.
pub authorised_at: Timestamp,
/// Base-58 encoded noise key of the agent.
pub bs58_x25519_noise: String,
/// Version of the noise protocol used by the agent.
pub noise_version: u8,
}
#[cw_serde]
pub struct AuthorisedNetworkMonitorOrchestratorsResponse {
pub authorised: Vec<AuthorisedNetworkMonitorOrchestrator>,
}
#[cw_serde]
pub struct AuthorisedNetworkMonitorsPagedResponse {
pub authorised: Vec<AuthorisedNetworkMonitor>,
pub start_next_after: Option<SocketAddr>,
}
@@ -13,8 +13,9 @@ use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime;
use tokio::sync::Mutex as AsyncMutex;
use tokio::time::Instant;
use tokio_util::sync::CancellationToken;
use tracing::{debug, info, instrument, warn};
use tracing::{debug, error, info, instrument, warn};
use uuid::Uuid;
pub use helpers::{BufferedDeposit, PerformedDeposits, make_deposits_request, split_deposits};
@@ -146,9 +147,14 @@ impl DepositsBuffer {
// if we're here, we know we're below the threshold
fn maybe_refill_deposits(&self) {
if let Some(mut guard) = self.inner.deposits_refill_task.try_get_new_task_guard() {
if let Some((mut guard, completion_guard)) =
self.inner.deposits_refill_task.try_get_new_task_guard()
{
let this = self.clone();
*guard = Some(tokio::spawn(async move { this.refill_deposits().await }));
*guard = Some(tokio::spawn(async move {
let _completion_guard = completion_guard;
this.refill_deposits().await
}));
}
}
@@ -179,6 +185,8 @@ impl DepositsBuffer {
requested_on: OffsetDateTime,
client_pubkey: PublicKeyUser,
) -> Result<BufferedDeposit, CredentialProxyError> {
let wait_start = Instant::now();
let mut i = 0;
loop {
tokio::time::sleep(Duration::from_millis(500)).await;
if let Some(buffered_deposit) = self.inner.unused_deposits.lock().await.pop() {
@@ -195,6 +203,15 @@ impl DepositsBuffer {
// make sure there's always a task working in the background in case deposits get used up too quickly
self.maybe_refill_deposits()
}
i += 1;
let elapsed = wait_start.elapsed();
if elapsed > Duration::from_secs(5) && i % 10 == 0 {
warn!("we've been waiting for over 5s to make a deposit - something is wrong!")
} else if elapsed > Duration::from_secs(10) && i % 5 == 0 {
error!(
"we've been waiting for over 10s to make a deposit - something is SERIOUSLY wrong!"
)
}
}
}
@@ -3,12 +3,22 @@
use crate::error::CredentialProxyError;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex as StdMutex, MutexGuard};
use std::sync::{Arc, Mutex as StdMutex, MutexGuard};
use tokio::task::JoinHandle;
use tracing::{debug, error};
pub(super) type RefillTaskResult = Result<(), CredentialProxyError>;
pub(super) struct InProgressGuard {
in_progress: Arc<AtomicBool>,
}
impl Drop for InProgressGuard {
fn drop(&mut self) {
self.in_progress.store(false, Ordering::SeqCst);
}
}
#[derive(Default)]
pub(super) struct RefillTask {
// note that we can only have a single transaction in progress (or it'd mess up with our sequence numbers)
@@ -16,7 +26,7 @@ pub(super) struct RefillTask {
// we'll have to increase the number of deposits per transaction
join_handle: StdMutex<Option<JoinHandle<RefillTaskResult>>>,
in_progress: AtomicBool,
in_progress: Arc<AtomicBool>,
}
impl RefillTask {
@@ -28,9 +38,15 @@ impl RefillTask {
.is_ok()
}
/// Returns `None` if a refill is already in progress. On success, returns the
/// join-handle guard (to store the new `JoinHandle` into) and an [`InProgressGuard`]
/// that **must be moved into the spawned task** — it resets the flag when dropped.
pub(super) fn try_get_new_task_guard(
&self,
) -> Option<MutexGuard<'_, Option<JoinHandle<RefillTaskResult>>>> {
) -> Option<(
MutexGuard<'_, Option<JoinHandle<RefillTaskResult>>>,
InProgressGuard,
)> {
// sanity check for concurrent request
if !self.try_set_in_progress() {
debug!("another task has already started deposit refill request");
@@ -48,7 +64,11 @@ impl RefillTask {
}
}
Some(guard)
let completion_guard = InProgressGuard {
in_progress: Arc::clone(&self.in_progress),
};
Some((guard, completion_guard))
}
pub(super) fn take_task_join_handle(&self) -> Option<JoinHandle<RefillTaskResult>> {
@@ -56,3 +76,34 @@ impl RefillTask {
self.join_handle.lock().expect("mutex got poisoned").take()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn in_progress_resets_after_guard_drop() {
let task = RefillTask::default();
let (guard, completion_guard) = task.try_get_new_task_guard().unwrap();
drop(guard);
assert!(task.try_get_new_task_guard().is_none());
drop(completion_guard);
assert!(task.try_get_new_task_guard().is_some());
}
#[test]
fn in_progress_resets_on_panic() {
let task = RefillTask::default();
let (guard, completion_guard) = task.try_get_new_task_guard().unwrap();
drop(guard);
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _g = completion_guard;
panic!("simulated refill task panic");
}));
assert!(task.try_get_new_task_guard().is_some());
}
}
+47 -4
View File
@@ -27,6 +27,9 @@ pub struct QuorumStateChecker {
cancellation_token: CancellationToken,
check_interval: Duration,
quorum_state: QuorumState,
/// indicates whether the last check has been a failure
last_failed: bool,
}
impl QuorumStateChecker {
@@ -42,6 +45,7 @@ impl QuorumStateChecker {
quorum_state: QuorumState {
available: Arc::new(Default::default()),
},
last_failed: false,
};
// first check MUST succeed, otherwise we shouldn't start
@@ -57,6 +61,7 @@ impl QuorumStateChecker {
}
async fn check_quorum_state(&self) -> Result<bool, CredentialProxyError> {
info!("checking the current quorum state");
let client_guard = self.client.query_chain().await;
// split the operation as we only need to hold the reference to chain client for the first part
@@ -64,7 +69,8 @@ impl QuorumStateChecker {
let dkg_details = dkg_details_with_client(client_guard.deref()).await?;
drop(client_guard);
let res = check_known_dealers(dkg_details).await?;
let res = check_known_dealers(dkg_details, 4).await?;
info!("there are {} known DKG dealers", res.results.len());
let Some(signing_threshold) = res.threshold else {
warn!(
@@ -76,15 +82,36 @@ impl QuorumStateChecker {
let mut working_issuer = 0;
for result in res.results {
let dealer = &result.information;
let info = format!("[id: {}] @ {}", dealer.node_index, dealer.announce_address);
if result.chain_available() && result.signing_available() {
info!("✅ {info} is fully available");
working_issuer += 1;
} else if !result.chain_available() && !result.signing_available() {
warn!("❌ {info} is not available for both chain and signing");
} else if !result.chain_available() {
warn!("❌ {info} is not available for chain");
} else {
warn!("❌ {info} is not available for signing");
}
}
Ok((working_issuer as u64) >= signing_threshold)
let available = (working_issuer as u64) >= signing_threshold;
if available {
info!(
"✅ Quorum state is available with {working_issuer} out of {signing_threshold} issuers"
)
} else {
error!(
"❌ Quorum state is not available with {working_issuer} out of {signing_threshold} issuers"
)
}
Ok(available)
}
pub async fn run_forever(self) {
pub async fn run_forever(mut self) {
info!("starting quorum state checker");
loop {
tokio::select! {
@@ -94,7 +121,23 @@ impl QuorumStateChecker {
}
_ = tokio::time::sleep(self.check_interval) => {
match self.check_quorum_state().await {
Ok(available) => self.quorum_state.available.store(available, Ordering::SeqCst),
Ok(available) => {
let previous = self.quorum_state.available.load(Ordering::SeqCst);
// only update the quorum state to a failed state if we've had two consecutive failures
if available {
if !previous {
info!("quorum recovered");
}
self.quorum_state.available.store(true, Ordering::SeqCst);
} else if self.last_failed {
if previous {
warn!("quorum became unavailable after 2 consecutive failed checks");
}
self.quorum_state.available.store(false, Ordering::SeqCst);
}
self.last_failed = !available;
},
Err(err) => error!("failed to check current quorum state: {err}"),
}
}
+8
View File
@@ -36,5 +36,13 @@ nym-ecash-contract-common = { workspace = true }
nym-network-defaults = { workspace = true }
nym-serde-helpers = { workspace = true, features = ["date"] }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
workspace = true
features = ["time"]
[target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer]
workspace = true
features = ["tokio"]
[dev-dependencies]
rand = { workspace = true }
@@ -6,6 +6,7 @@ use crate::ecash::bandwidth::serialiser::VersionedSerialise;
use crate::ecash::bandwidth::CredentialSigningData;
use crate::ecash::utils::cred_exp_date;
use crate::error::Error;
use log::{debug, warn};
use nym_api_requests::ecash::BlindSignRequestBody;
use nym_credentials_interface::{
aggregate_wallets, generate_keypair_user_from_seed, issue_verify, withdrawal_request,
@@ -17,8 +18,15 @@ use nym_ecash_contract_common::deposit::DepositId;
use nym_ecash_time::{ecash_default_expiration_date, ecash_today, EcashTime};
use nym_validator_client::nym_api::{EpochId, NymApiClientExt};
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::Date;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time::sleep;
#[cfg(target_arch = "wasm32")]
use wasmtimer::tokio::sleep;
pub use nym_validator_client::nyxd::{Coin, Hash};
#[derive(Serialize, Deserialize)]
@@ -192,6 +200,49 @@ impl IssuanceTicketBook {
Ok(unblinded_signature)
}
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
pub async fn obtain_partial_ticketbook_credential_with_retries(
&self,
client: &nym_http_api_client::Client,
signer_index: u64,
validator_vk: &VerificationKeyAuth,
signing_data: CredentialSigningData,
max_attempts: usize,
) -> Result<PartialWallet, Error> {
let Some(client_url) = client.base_urls().first() else {
return Err(Error::CredentialShareObtainFailed);
};
let mut last_err = None;
for attempt in 0..max_attempts {
if attempt > 0 {
sleep(Duration::from_millis(500 * attempt as u64)).await;
}
debug!(
"attempt {} / {max_attempts} to obtain partial ticketbook credential from {client_url}",
attempt + 1,
);
match self
.obtain_partial_ticketbook_credential(
client,
signer_index,
validator_vk,
signing_data.clone(),
)
.await
{
Ok(partial_wallet) => return Ok(partial_wallet),
Err(err) => {
warn!(
"attempt {} / {max_attempts} to obtain partial ticketbook credential from {client_url} failed: {err}",
attempt + 1,
);
last_err = Some(err);
}
}
}
Err(last_err.unwrap_or(Error::CredentialShareObtainFailed))
}
// ideally this would have been generic over credential type, but we really don't need secp256k1 keys for bandwidth vouchers
pub async fn obtain_partial_ticketbook_credential(
&self,
+9 -1
View File
@@ -137,6 +137,8 @@ pub async fn obtain_aggregate_wallet(
ecash_api_clients: &[EcashApiClient],
threshold: u64,
) -> Result<WalletSignatures, Error> {
const MAX_ATTEMPTS: usize = 2;
if ecash_api_clients.len() < threshold as usize {
return Err(Error::NoValidatorsAvailable);
}
@@ -154,11 +156,12 @@ pub async fn obtain_aggregate_wallet(
);
match voucher
.obtain_partial_ticketbook_credential(
.obtain_partial_ticketbook_credential_with_retries(
&ecash_api_client.api_client,
ecash_api_client.node_id,
&ecash_api_client.verification_key,
request.clone(),
MAX_ATTEMPTS,
)
.await
{
@@ -167,6 +170,11 @@ pub async fn obtain_aggregate_wallet(
warn!("failed to obtain partial credential from API {ecash_api_client}: {err}",);
}
};
// we got sufficient number of shares
if wallets.len() >= threshold as usize {
break;
}
}
if wallets.len() < threshold as usize {
return Err(Error::NotEnoughShares);
+3
View File
@@ -63,6 +63,9 @@ pub enum Error {
#[error("failed to create a secp256k1 signature")]
Secp256k1SignFailure,
#[error("failed to obtain a valid credential share")]
CredentialShareObtainFailed,
}
impl From<NymAPIError> for Error {
+2
View File
@@ -31,3 +31,5 @@ pub use aes_gcm_siv::{Aes128GcmSiv, Aes256GcmSiv};
pub use blake3;
#[cfg(feature = "stream_cipher")]
pub use ctr;
#[cfg(feature = "hashing")]
pub use sha2;
@@ -36,6 +36,7 @@ pub trait ChainResponse: Verifiable + TimestampedResponse {
// we rely on information provided from the api itself AS LONG AS it's not too outdated
if self.timestamp() + stale_response_threshold < now {
warn!("chain status response is stale");
return false;
}
self.chain_synced()
@@ -96,26 +97,27 @@ pub trait SignerResponse: Verifiable + TimestampedResponse {
// we rely on information provided from the api itself AS LONG AS it's not too outdated
if self.timestamp() + stale_response_threshold < now {
warn!("stale signer response");
return false;
}
if !self.has_signing_keys() {
debug!("missing signing keys");
warn!("missing signing keys");
return false;
}
if self.signer_disabled() {
debug!("signer functionalities explicitly disabled");
warn!("signer functionalities are explicitly disabled");
return false;
}
if !self.is_ecash_signer() {
debug!("signer doesn't recognise it's a signer for this epoch");
warn!("signer doesn't recognise it's a signer for this epoch");
return false;
}
if dkg_epoch_id != self.dkg_ecash_epoch_id() {
debug!(
warn!(
"mismatched dkg epoch id. current: {dkg_epoch_id}, signer's: {}",
self.dkg_ecash_epoch_id()
);
@@ -11,10 +11,11 @@ use nym_crypto::asymmetric::ed25519;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::OffsetDateTime;
use tracing::warn;
use utoipa::ToSchema;
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(10 * 60);
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(10 * 60);
// the reason for generics is not to remove duplication of code,
// but because without them, we'd be having problems with circular dependencies,
@@ -188,6 +189,7 @@ where
};
let SignerStatus::Tested { result } = &self.status else {
warn!("no valid chain response");
return false;
};
result
@@ -239,6 +241,7 @@ where
};
let SignerStatus::Tested { result } = &self.status else {
warn!("no valid signer response");
return false;
};
result.signing_status.signing_available(
@@ -195,9 +195,9 @@ impl ClientUnderTest {
pub(crate) async fn check_client(
dealer_details: DealerDetails,
dkg_epoch: u64,
contract_share: Option<&ContractVKShare>,
contract_share: Option<ContractVKShare>,
) -> TypedSignerResult {
let dealer_information = RawDealerInformation::new(&dealer_details, contract_share);
let dealer_information = RawDealerInformation::new(&dealer_details, contract_share.as_ref());
// 7. attempt to construct client instances out of them
let Ok(parsed_information) = dealer_information.parse() else {
+16 -12
View File
@@ -2,7 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use crate::client_check::check_client;
use futures::stream::{FuturesUnordered, StreamExt};
use futures::stream;
use futures::stream::StreamExt;
use nym_ecash_signer_check_types::status::{SignerResult, Status};
use nym_network_defaults::NymNetworkDetails;
use nym_validator_client::QueryHttpRpcNyxdClient;
@@ -65,7 +66,7 @@ where
C: DkgQueryClient + Sync,
{
let dkg_details = dkg_details_with_client(client).await?;
check_known_dealers(dkg_details).await
check_known_dealers(dkg_details, None).await
}
pub async fn dkg_details_with_client<C>(client: &C) -> Result<DkgDetails, SignerCheckError>
@@ -109,18 +110,21 @@ where
pub async fn check_known_dealers(
dkg_details: DkgDetails,
concurrency: impl Into<Option<usize>>,
) -> Result<SignersTestResult, SignerCheckError> {
// 6. for each dealer attempt to perform the checks
let results = dkg_details
.network_dealers
.into_iter()
.map(|d| {
let share = dkg_details.submitted_shared.get(&d.assigned_index);
check_client(d, dkg_details.dkg_epoch.epoch_id, share)
})
.collect::<FuturesUnordered<_>>()
.collect::<Vec<_>>()
.await;
let epoch_id = dkg_details.dkg_epoch.epoch_id;
let submitted = dkg_details.submitted_shared;
let dealers = dkg_details.network_dealers.len();
let tasks = dkg_details.network_dealers.into_iter().map(move |d| {
let share = submitted.get(&d.assigned_index).cloned();
check_client(d, epoch_id, share)
});
let limit = concurrency.into().filter(|&n| n > 0).unwrap_or(dealers);
let results = stream::iter(tasks).buffer_unordered(limit).collect().await;
Ok(SignersTestResult {
threshold: dkg_details.threshold,
+1
View File
@@ -36,6 +36,7 @@ thiserror = { workspace = true }
tracing = { workspace = true }
itertools = { workspace = true }
inventory = { workspace = true }
fastrand = { workspace = true }
tokio = { workspace = true, features = ["rt", "macros", "time"] }
rustls = { workspace=true }
# used for decoding text responses (they were already implicitly included)
+78 -128
View File
@@ -55,9 +55,8 @@ use std::{
use hickory_resolver::{
TokioResolver,
config::{NameServerConfig, NameServerConfigGroup, ResolverConfig, ResolverOpts},
lookup_ip::LookupIpIntoIter,
name_server::TokioConnectionProvider,
config::{CLOUDFLARE, NameServerConfig, QUAD9, ResolverConfig, ResolverOpts},
net::{NetError, runtime::TokioRuntimeProvider},
};
use once_cell::sync::OnceCell;
use reqwest::dns::{Addrs, Name, Resolve, Resolving};
@@ -113,7 +112,7 @@ pub enum ResolveError {
#[error("invalid name: {0}")]
InvalidNameError(String),
#[error("hickory-dns resolver error: {0}")]
ResolveError(#[from] hickory_resolver::ResolveError),
ResolveError(#[from] NetError),
#[error("high level lookup timed out")]
Timeout,
#[error("hostname not found in static lookup table")]
@@ -123,7 +122,10 @@ pub enum ResolveError {
impl ResolveError {
/// Returns true if the error is a timeout.
pub fn is_timeout(&self) -> bool {
matches!(self, ResolveError::Timeout)
matches!(
self,
ResolveError::Timeout | ResolveError::ResolveError(NetError::Timeout)
)
}
}
@@ -167,18 +169,17 @@ impl Resolve for HickoryDnsResolver {
fn resolve(&self, name: Name) -> Resolving {
let use_system = self.use_system.load(std::sync::atomic::Ordering::Relaxed);
let use_shared = self.use_shared;
let resolver = if use_system {
match self
.system_resolver
let result = if use_system {
self.system_resolver
.get_or_try_init(|| HickoryDnsResolver::new_resolver_system(use_shared))
{
Ok(r) => r.clone(),
Err(e) => return Box::pin(return_err(e)),
}
} else {
self.state
.get_or_init(|| HickoryDnsResolver::new_resolver(use_shared))
.clone()
.get_or_try_init(|| HickoryDnsResolver::new_resolver(use_shared))
};
let resolver = match result {
Ok(r) => r.clone(),
Err(err) => return Box::pin(return_err(err)),
};
let maybe_static = self.static_base.clone();
@@ -227,9 +228,11 @@ async fn resolve(
let primary_err = match resolve_fut.await {
Err(_) => ResolveError::Timeout,
Ok(Ok(lookup)) => {
let addrs: Addrs = Box::new(SocketAddrs {
iter: lookup.into_iter(),
});
// Shuffle so that successive connection attempts cycle through all
// returned IPs rather than always hitting the same first address.
let mut ips = Vec::from_iter(lookup.iter());
fastrand::shuffle(&mut ips);
let addrs: Addrs = Box::new(ips.into_iter().map(|ip| SocketAddr::new(ip, 0)));
return Ok(addrs);
}
Ok(Err(e)) => {
@@ -256,18 +259,6 @@ async fn resolve(
Err(primary_err)
}
struct SocketAddrs {
iter: LookupIpIntoIter,
}
impl Iterator for SocketAddrs {
type Item = SocketAddr;
fn next(&mut self) -> Option<Self::Item> {
self.iter.next().map(|ip_addr| SocketAddr::new(ip_addr, 0))
}
}
impl HickoryDnsResolver {
/// Returns an instance of the shared resolver.
pub fn shared() -> Self {
@@ -288,7 +279,7 @@ impl HickoryDnsResolver {
.clone()
} else {
self.state
.get_or_init(|| HickoryDnsResolver::new_resolver(self.use_shared))
.get_or_try_init(|| HickoryDnsResolver::new_resolver(self.use_shared))?
.clone()
};
@@ -311,11 +302,11 @@ impl HickoryDnsResolver {
}
}
fn new_resolver(use_shared: bool) -> TokioResolver {
fn new_resolver(use_shared: bool) -> Result<TokioResolver, ResolveError> {
// using a closure here is slightly gross, but this makes sure that if the
// lazy-init returns an error it can be handled by the client
if use_shared {
SHARED_RESOLVER.state.get_or_init(new_resolver).clone()
SHARED_RESOLVER.state.get_or_try_init(new_resolver).cloned()
} else {
new_resolver()
}
@@ -367,7 +358,7 @@ impl HickoryDnsResolver {
/// Clear entries from the static table that would return entries during the pre-resolve stage.
/// This means that all lookups will attempt to use the network resolver again before the static
/// table is consulted.
///
///
/// Entries elevated to pre-resolve from fallback (added from default or using
/// [`set_fallback`]`) will have their cache timeout cleared. Entries added directly to
/// pre-resolve (using [`Self::set_static_preresolve`]) will be removed.
@@ -438,20 +429,7 @@ impl HickoryDnsResolver {
/// Get the list of currently available nameserver configs.
pub fn all_configured_name_servers(&self) -> Vec<NameServerConfig> {
default_nameserver_group().to_vec()
}
/// Get the list of currently used nameserver configs.
pub fn active_name_servers(&self) -> Vec<NameServerConfig> {
if !self.use_shared {
return self
.state
.get()
.map(|r| r.config().name_servers().to_vec())
.unwrap_or(self.all_configured_name_servers());
}
SHARED_RESOLVER.active_name_servers()
default_nameserver_group()
}
/// Do a trial resolution using each nameserver individually to test which are working and which
@@ -477,65 +455,60 @@ impl HickoryDnsResolver {
///
/// Caches successfully resolved addresses for 30 minutes to prevent continual use of remote lookup.
/// This resolver is intended to be used for OUR API endpoints that do not rapidly rotate IPs.
fn new_resolver() -> TokioResolver {
fn new_resolver() -> Result<TokioResolver, ResolveError> {
let name_servers = default_nameserver_group_ipv4_only();
configure_and_build_resolver(name_servers)
}
fn configure_and_build_resolver<G>(name_servers: G) -> TokioResolver
where
G: Into<NameServerConfigGroup>,
{
fn configure_and_build_resolver(
name_servers: Vec<NameServerConfig>,
) -> Result<TokioResolver, ResolveError> {
let options = HickoryDnsResolver::default_options();
let name_servers: NameServerConfigGroup = name_servers.into();
info!("building new configured resolver");
debug!("configuring resolver with {options:?}, {name_servers:?}");
let config = ResolverConfig::from_parts(None, Vec::new(), name_servers);
let mut resolver_builder =
TokioResolver::builder_with_config(config, TokioConnectionProvider::default());
TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
resolver_builder = resolver_builder.with_options(options);
resolver_builder.build()
Ok(resolver_builder.build()?)
}
fn filter_ipv4(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
fn filter_ipv4(nameservers: impl IntoIterator<Item = NameServerConfig>) -> Vec<NameServerConfig> {
nameservers
.as_ref()
.iter()
.filter(|ns| ns.socket_addr.is_ipv4())
.cloned()
.into_iter()
.filter(|ns| ns.ip.is_ipv4())
.collect()
}
#[allow(unused)]
fn filter_ipv6(nameservers: impl AsRef<[NameServerConfig]>) -> Vec<NameServerConfig> {
fn filter_ipv6(nameservers: impl IntoIterator<Item = NameServerConfig>) -> Vec<NameServerConfig> {
nameservers
.as_ref()
.iter()
.filter(|ns| ns.socket_addr.is_ipv6())
.cloned()
.into_iter()
.filter(|ns| ns.ip.is_ipv6())
.collect()
}
#[allow(unused)]
fn default_nameserver_group() -> NameServerConfigGroup {
let mut name_servers = NameServerConfigGroup::quad9_tls();
name_servers.merge(NameServerConfigGroup::quad9_https());
name_servers.merge(NameServerConfigGroup::cloudflare_tls());
name_servers.merge(NameServerConfigGroup::cloudflare_https());
name_servers
fn default_nameserver_group() -> Vec<NameServerConfig> {
QUAD9
.tls()
.chain(QUAD9.https())
.chain(CLOUDFLARE.tls())
.chain(CLOUDFLARE.https())
.collect()
}
fn default_nameserver_group_ipv4_only() -> NameServerConfigGroup {
filter_ipv4(&default_nameserver_group() as &[NameServerConfig]).into()
fn default_nameserver_group_ipv4_only() -> Vec<NameServerConfig> {
filter_ipv4(default_nameserver_group())
}
#[allow(unused)]
fn default_nameserver_group_ipv6_only() -> NameServerConfigGroup {
filter_ipv6(&default_nameserver_group() as &[NameServerConfig]).into()
fn default_nameserver_group_ipv6_only() -> Vec<NameServerConfig> {
filter_ipv6(default_nameserver_group())
}
/// Create a new resolver with the default configuration, which reads from the system DNS config
@@ -550,7 +523,7 @@ fn new_resolver_system() -> Result<TokioResolver, ResolveError> {
resolver_builder = resolver_builder.with_options(options);
Ok(resolver_builder.build())
Ok(resolver_builder.build()?)
}
fn new_default_static_fallback() -> StaticResolver {
@@ -577,7 +550,7 @@ async fn trial_nameservers_inner(
async fn trial_lookup(name_server: NameServerConfig, query: &str) -> Result<(), ResolveError> {
debug!("running ns trial {name_server:?} query={query}");
let resolver = configure_and_build_resolver(vec![name_server]);
let resolver = configure_and_build_resolver(vec![name_server])?;
match tokio::time::timeout(DEFAULT_OVERALL_LOOKUP_TIMEOUT, resolver.ipv4_lookup(query)).await {
Ok(Ok(_)) => Ok(()),
@@ -590,8 +563,10 @@ async fn trial_lookup(name_server: NameServerConfig, query: &str) -> Result<(),
mod test {
use super::*;
use itertools::Itertools;
use std::collections::HashMap;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::{
collections::HashMap,
net::{IpAddr, Ipv4Addr, Ipv6Addr},
};
/// IP addresses guaranteed to fail attempts to resolve
///
@@ -670,26 +645,16 @@ mod test {
let mut ns_ips = GUARANTEED_BROKEN_IPS_1.to_vec();
ns_ips.push(good_cf_ip);
let broken_ns_https = NameServerConfigGroup::from_ips_https(
&ns_ips,
443,
"cloudflare-dns.com".to_string(),
true,
);
let domain = Arc::<str>::from("cloudflare-dns.com");
let path = Arc::<str>::from("/dns-query");
let broken_ns_https = GUARANTEED_BROKEN_IPS_1
.iter()
.chain([&good_cf_ip])
.map(|ip| NameServerConfig::https(*ip, domain.clone(), Some(path.clone())))
.collect::<Vec<_>>();
let inner = configure_and_build_resolver(broken_ns_https);
// create a new resolver that won't mess with the shared resolver used by other tests
let resolver = HickoryDnsResolver {
use_shared: false,
state: Arc::new(OnceCell::with_value(inner)),
static_base: Some(Default::default()),
..Default::default()
};
let name_servers = resolver.state.get().unwrap().config().name_servers();
for (ns, result) in trial_nameservers_inner(name_servers).await {
if ns.socket_addr.ip() == good_cf_ip {
for (ns, result) in trial_nameservers_inner(&broken_ns_https).await {
if ns.ip == good_cf_ip {
assert!(result.is_ok())
} else {
assert!(result.is_err())
@@ -705,21 +670,20 @@ mod test {
fn build_broken_resolver() -> Result<TokioResolver, ResolveError> {
info!("building new faulty resolver");
let mut broken_ns_group = NameServerConfigGroup::from_ips_tls(
GUARANTEED_BROKEN_IPS_1,
853,
"cloudflare-dns.com".to_string(),
true,
);
let broken_ns_https = NameServerConfigGroup::from_ips_https(
GUARANTEED_BROKEN_IPS_1,
443,
"cloudflare-dns.com".to_string(),
true,
);
broken_ns_group.merge(broken_ns_https);
let domain = Arc::<str>::from("cloudflare-dns.com");
let path = Arc::<str>::from("/dns-query");
let broken_ns_group = GUARANTEED_BROKEN_IPS_1
.iter()
.map(|ip| NameServerConfig::tls(*ip, domain.clone()))
.chain(
GUARANTEED_BROKEN_IPS_1
.iter()
.map(|ip| NameServerConfig::https(*ip, domain.clone(), Some(path.clone())))
.collect::<Vec<_>>(),
)
.collect::<Vec<_>>();
Ok(configure_and_build_resolver(broken_ns_group))
configure_and_build_resolver(broken_ns_group)
}
#[tokio::test]
@@ -740,7 +704,7 @@ mod test {
build_broken_resolver()?;
let domain = "ifconfig.me";
let result = resolver.resolve_str(domain).await;
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
assert!(result.is_err_and(|e| e.is_timeout()));
let duration = time_start.elapsed();
assert!(duration < resolver.overall_dns_timeout + Duration::from_secs(1));
@@ -774,25 +738,11 @@ mod test {
// unsuccessful lookup - primary times out, and not in static table
let domain = "non-existent.nymtech.net";
let result = resolver.resolve_str(domain).await;
assert!(result.is_err_and(|e| matches!(e, ResolveError::Timeout)));
assert!(result.is_err_and(|e| e.is_timeout()));
Ok(())
}
#[test]
fn default_resolver_uses_ipv4_only_nameservers() {
let resolver = HickoryDnsResolver::thread_resolver();
resolver
.active_name_servers()
.iter()
.all(|cfg| cfg.socket_addr.is_ipv4());
SHARED_RESOLVER
.active_name_servers()
.iter()
.all(|cfg| cfg.socket_addr.is_ipv4());
}
#[tokio::test]
#[cfg(any())] // #[ignore] we run --ignore in CI/CD assuming it just means slow -_-
// This test impacts the state of the shared resolver and as such is disabled to avoid
+21 -15
View File
@@ -141,9 +141,7 @@
use http::header::USER_AGENT;
pub use inventory;
pub use reqwest;
pub use reqwest::ClientBuilder as ReqwestClientBuilder;
pub use reqwest::StatusCode;
pub use reqwest::{self, ClientBuilder as ReqwestClientBuilder, StatusCode};
use std::error::Error;
pub mod registry;
@@ -152,19 +150,21 @@ use crate::path::RequestPath;
use async_trait::async_trait;
use bytes::Bytes;
use cfg_if::cfg_if;
use http::HeaderMap;
use http::header::{ACCEPT, CONTENT_TYPE};
use http::{
HeaderMap,
header::{ACCEPT, CONTENT_TYPE},
};
use itertools::Itertools;
use mime::Mime;
use reqwest::header::HeaderValue;
use reqwest::{RequestBuilder, Response};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use reqwest::{RequestBuilder, Response, header::HeaderValue};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
#[cfg(not(target_arch = "wasm32"))]
use std::io::ErrorKind;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use std::{
fmt::Display,
sync::atomic::{AtomicUsize, Ordering},
time::Duration,
};
use thiserror::Error;
use tracing::{debug, instrument, warn};
@@ -1152,7 +1152,10 @@ impl ApiClientCore for Client {
#[cfg(target_arch = "wasm32")]
let response: Result<Response, HttpClientError> = {
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
let client = self
.reqwest_client
.as_ref()
.unwrap_or_else(|| &*SHARED_CLIENT);
Ok(
wasmtimer::tokio::timeout(self.request_timeout, client.execute(req))
.await
@@ -1162,7 +1165,10 @@ impl ApiClientCore for Client {
#[cfg(not(target_arch = "wasm32"))]
let response = {
let client = self.reqwest_client.as_ref().unwrap_or(&*SHARED_CLIENT);
let client = self
.reqwest_client
.as_ref()
.unwrap_or_else(|| &*SHARED_CLIENT);
client.execute(req).await
};
@@ -1268,7 +1274,7 @@ pub(crate) fn might_be_network_interference(err: &reqwest::Error) -> bool {
} else if let Some(_tls_err) = e.downcast_ref::<rustls::Error>() {
// try downcast to TLS error
return true;
} else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::ResolveError>() {
} else if let Some(resolve_err) = e.downcast_ref::<hickory_resolver::net::NetError>() {
// try downcast to DNS error
return resolve_err.is_nx_domain();
} else {
+2
View File
@@ -10,7 +10,9 @@ fn sanitize_fragment(segment: &str) -> &str {
segment.trim_matches(|c: char| c.is_whitespace() || c == '/')
}
/// Defines a path that can be used to make a request to an API.
pub trait RequestPath: Debug {
/// Sanitise the request path by removing empty segments and trimming whitespace and slashes
fn to_sanitized_segments(&self) -> Vec<&str>;
}
+8
View File
@@ -12,6 +12,14 @@ pub mod v7;
pub mod v8;
pub mod v9;
/// Highest IPR protocol version that is allowed to be sent as a **non-stream** mixnet payload
/// (i.e. not wrapped in `LpFrameKind::SphinxStream`).
pub const MAX_NON_STREAM_VERSION: u8 = v8::VERSION;
/// First IPR protocol version that **requires** the SphinxStream (LP) transport for non-stream
/// mixnet sends, matching the node-side enforcement in `ip-packet-router`.
pub const SPHINX_STREAM_VERSION_THRESHOLD: u8 = v9::VERSION;
// version 3: initial version
// version 4: IPv6 support
// version 5: Add severity level to info response
+4 -5
View File
@@ -17,11 +17,10 @@ exclude = ["build.rs"]
[dependencies]
dotenvy = { workspace = true, optional = true }
log = { workspace = true, optional = true }
schemars = { workspace = true, features = ["preserve_order"], optional = true }
serde = { workspace = true, features = ["derive"], optional = true }
serde_json = {workspace = true, optional = true }
tracing = {workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
tracing = { workspace = true, optional = true }
url = { workspace = true, optional = true }
utoipa = { workspace = true, optional = true }
@@ -30,9 +29,9 @@ utoipa = { workspace = true, optional = true }
[features]
default = ["env", "network"]
env = ["dotenvy", "log", "serde_json", "tracing"]
env = ["dotenvy", "serde_json", "tracing"]
network = ["schemars", "serde", "url"]
utoipa = [ "dep:utoipa" ]
utoipa = ["dep:utoipa"]
[build-dependencies]
regex = { workspace = true }
+8 -4
View File
@@ -27,16 +27,20 @@ fn print_env_vars_with_keys_in_file<P: AsRef<Path> + Copy>(config_env_file: P) {
.expect("Invalid path to environment configuration file");
for item in items {
let (key, val) = item.expect("Invalid item in environment configuration file");
log::debug!("{key}: {val}");
tracing::debug!("{key}: {val}");
}
}
pub fn env_configured() -> bool {
std::env::var(var_names::CONFIGURED).is_ok()
}
pub fn setup_env<P: AsRef<Path>>(config_env_file: Option<P>) {
match std::env::var(var_names::CONFIGURED) {
// if the configuration is not already set in the env vars
Err(std::env::VarError::NotPresent) => {
if let Some(config_env_file) = &config_env_file {
log::debug!(
tracing::debug!(
"Loading environment variables from {:?}",
config_env_file.as_ref()
);
@@ -47,12 +51,12 @@ pub fn setup_env<P: AsRef<Path>>(config_env_file: Option<P>) {
// if nothing is set, the use mainnet defaults
// if the user has not set `CONFIGURED`, then even if they set any of the env variables,
// overwrite them
log::debug!("Loading mainnet defaults");
tracing::debug!("Loading mainnet defaults");
crate::mainnet::export_to_env();
}
}
Err(_) => {
log::debug!("Environment variables already set. Using them");
tracing::debug!("Environment variables already set. Using them");
crate::mainnet::export_to_env()
}
_ => {
+24 -5
View File
@@ -22,6 +22,8 @@ pub const VESTING_CONTRACT_ADDRESS: &str =
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
// /\ TODO: this has to be updated once the contract is deployed
pub const NETWORK_MONITORS_CONTRACT_ADDRESS: &str =
"n1m3a2ltkjqud8mkmrpqvgllrtv2p4r6js6qwl7p8cqkzrq8jg6e2qwqgl8z";
pub const ECASH_CONTRACT_ADDRESS: &str =
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
pub const GROUP_CONTRACT_ADDRESS: &str =
@@ -36,6 +38,10 @@ pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772
pub const NYXD_URL: &str = "https://rpc.nymtech.net";
pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket";
// cluster of lite rpc nodes (not part of consensus, aggressive pruning, no archival state)
pub const NYXD_QUERY_LITE: &str = "https://blockstream.nymtech.net";
pub const NYXD_WS_LITE: &str = "wss://blockstream.nymtech.net/websocket";
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
#[cfg(feature = "network")]
pub const NYM_APIS: &[ApiUrlConst] = &[
@@ -43,10 +49,6 @@ pub const NYM_APIS: &[ApiUrlConst] = &[
url: NYM_API,
front_hosts: None,
},
ApiUrlConst {
url: "https://nym-frontdoor.vercel.app/api/",
front_hosts: Some(&["vercel.app", "vercel.com"]),
},
ApiUrlConst {
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
@@ -68,7 +70,7 @@ pub const UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY: &str =
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
ApiUrlConst {
url: NYM_VPN_API,
front_hosts: Some(&["vercel.app", "vercel.com"]),
front_hosts: None,
},
ApiUrlConst {
url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/",
@@ -137,6 +139,11 @@ pub fn read_parsed_var_if_not_default<T: std::str::FromStr>(
.map(std::str::FromStr::from_str)
}
#[cfg(feature = "env")]
pub fn read_parsed_var<T: std::str::FromStr>(var: &str) -> Result<T, T::Err> {
std::env::var(var).unwrap_or_default().parse()
}
#[cfg(all(feature = "env", feature = "network"))]
pub fn export_to_env() {
use crate::var_names;
@@ -167,6 +174,14 @@ pub fn export_to_env() {
var_names::COCONUT_DKG_CONTRACT_ADDRESS,
COCONUT_DKG_CONTRACT_ADDRESS,
);
set_var_to_default(
var_names::PERFORMANCE_CONTRACT_ADDRESS,
PERFORMANCE_CONTRACT_ADDRESS,
);
set_var_to_default(
var_names::NETWORK_MONITORS_CONTRACT_ADDRESS,
NETWORK_MONITORS_CONTRACT_ADDRESS,
);
set_var_to_default(
var_names::REWARDING_VALIDATOR_ADDRESS,
REWARDING_VALIDATOR_ADDRESS,
@@ -186,6 +201,8 @@ pub fn export_to_env() {
var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
);
set_var_to_default(var_names::NYXD_QUERY_LITE, NYXD_QUERY_LITE);
set_var_to_default(var_names::NYXD_WS_LITE, NYXD_WS_LITE);
}
#[cfg(all(feature = "env", feature = "network"))]
@@ -237,4 +254,6 @@ pub fn export_to_env_if_not_set() {
var_names::UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
UPGRADE_MODE_ATTESTER_ED25519_BS58_PUBKEY,
);
set_var_conditionally_to_default(var_names::NYXD_QUERY_LITE, NYXD_QUERY_LITE);
set_var_conditionally_to_default(var_names::NYXD_WS_LITE, NYXD_WS_LITE);
}
+37 -3
View File
@@ -39,6 +39,8 @@ pub struct NymContracts {
pub vesting_contract_address: Option<String>,
#[serde(default)]
pub performance_contract_address: Option<String>,
#[serde(default)]
pub network_monitors_contract_address: Option<String>,
pub ecash_contract_address: Option<String>,
pub group_contract_address: Option<String>,
pub multisig_contract_address: Option<String>,
@@ -72,6 +74,15 @@ pub struct ApiUrl {
pub front_hosts: Option<Vec<String>>,
}
impl From<Url> for ApiUrl {
fn from(value: Url) -> Self {
ApiUrl {
url: value.to_string(),
front_hosts: None,
}
}
}
#[derive(Copy, Clone, Debug, Serialize)]
pub struct ApiUrlConst<'a> {
pub url: &'a str,
@@ -178,6 +189,10 @@ impl NymNetworkDetails {
.with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
.with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS))
.with_performance_contract(get_optional_env(var_names::PERFORMANCE_CONTRACT_ADDRESS))
.with_network_monitors_contract(get_optional_env(
var_names::NETWORK_MONITORS_CONTRACT_ADDRESS,
))
.with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API))
.with_nym_vpn_api_urls(nym_vpn_api_urls)
.with_nym_api_urls(nym_api_urls)
@@ -199,6 +214,9 @@ impl NymNetworkDetails {
performance_contract_address: parse_optional_str(
mainnet::PERFORMANCE_CONTRACT_ADDRESS,
),
network_monitors_contract_address: parse_optional_str(
mainnet::NETWORK_MONITORS_CONTRACT_ADDRESS,
),
ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS),
group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS),
multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS),
@@ -226,7 +244,7 @@ impl NymNetworkDetails {
fn set_optional_var(var_name: &str, value: Option<String>) {
if let Some(value) = value {
unsafe {set_var(var_name, value)}
unsafe { set_var(var_name, value) }
}
}
unsafe {
@@ -364,15 +382,31 @@ impl NymNetworkDetails {
self
}
#[must_use]
pub fn with_performance_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
self.contracts.performance_contract_address = contract.map(Into::into);
self
}
#[must_use]
pub fn with_network_monitors_contract<S: Into<String>>(mut self, contract: Option<S>) -> Self {
self.contracts.network_monitors_contract_address = contract.map(Into::into);
self
}
#[must_use]
pub fn with_nym_vpn_api_url<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
self.nym_vpn_api_url = endpoint.map(Into::into);
self
}
pub fn set_nym_api_urls<U: Into<ApiUrl>>(&mut self, urls: Vec<U>) {
self.nym_api_urls = Some(urls.into_iter().map(Into::into).collect());
}
#[must_use]
pub fn with_nym_api_urls(mut self, urls: Vec<ApiUrl>) -> Self {
self.nym_api_urls = Some(urls);
pub fn with_nym_api_urls<U: Into<ApiUrl>>(mut self, urls: Vec<U>) -> Self {
self.set_nym_api_urls(urls);
self
}
+4
View File
@@ -18,11 +18,15 @@ pub const ECASH_CONTRACT_ADDRESS: &str = "ECASH_CONTRACT_ADDRESS";
pub const GROUP_CONTRACT_ADDRESS: &str = "GROUP_CONTRACT_ADDRESS";
pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS";
pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS";
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "PERFORMANCE_CONTRACT_ADDRESS";
pub const NETWORK_MONITORS_CONTRACT_ADDRESS: &str = "NETWORK_MONITORS_CONTRACT_ADDRESS";
pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
pub const NYXD: &str = "NYXD";
pub const NYM_API: &str = "NYM_API";
pub const NYM_APIS: &str = "NYM_APIS";
pub const NYXD_WEBSOCKET: &str = "NYXD_WS";
pub const NYXD_QUERY_LITE: &str = "NYXD_LITE";
pub const NYXD_WS_LITE: &str = "NYXD_WS_LITE";
pub const EXIT_POLICY_URL: &str = "EXIT_POLICY";
pub const NYM_VPN_API: &str = "NYM_VPN_API";
pub const NYM_VPN_APIS: &str = "NYM_VPN_APIS";
+385 -44
View File
@@ -1,19 +1,19 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use arc_swap::ArcSwap;
use nym_crypto::asymmetric::x25519;
use snow::params::NoiseParams;
use std::{
collections::HashMap,
net::{IpAddr, SocketAddr},
sync::Arc,
time::Duration,
};
use arc_swap::ArcSwap;
use nym_crypto::asymmetric::x25519;
use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1};
use snow::params::NoiseParams;
use strum_macros::{EnumIter, FromRepr};
use tokio::sync::{Mutex, MutexGuard};
pub use nym_noise_keys::{NoiseVersion, VersionedNoiseKeyV1};
#[derive(Default, Debug, Clone, Copy, EnumIter, FromRepr, Eq, PartialEq)]
#[repr(u8)]
@@ -53,38 +53,125 @@ impl NoisePattern {
}
}
#[derive(Debug, Default)]
struct SocketAddrToKey {
inner: ArcSwap<HashMap<SocketAddr, VersionedNoiseKeyV1>>,
}
// SW NOTE : Only for phased upgrade. To remove once we decide all nodes have to support Noise
#[derive(Debug, Default)]
struct IpAddrToVersion {
inner: ArcSwap<HashMap<IpAddr, NoiseVersion>>,
}
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone)]
pub struct NoiseNetworkView {
keys: Arc<SocketAddrToKey>,
support: Arc<IpAddrToVersion>,
inner: Arc<NoiseNetworkViewInner>,
}
impl NoiseNetworkView {
pub fn new_empty() -> Self {
NoiseNetworkView {
keys: Default::default(),
support: Default::default(),
/// Inner state of [`NoiseNetworkView`], shared behind an `Arc`.
///
/// # Concurrency model
///
/// Reads (on the packet-processing hot path) use `ArcSwap` and are fully lock-free.
/// Writers must first acquire `update_lock` to serialise concurrent updates, then call
/// `swap_view` to atomically publish the new map. The lock is intentionally *not* wrapping
/// the map itself so that readers are never blocked.
#[derive(Debug)]
struct NoiseNetworkViewInner {
update_lock: Mutex<()>,
nodes: ArcSwap<HashMap<IpAddr, NoiseNode>>,
}
/// A node in the noise network map, keyed by IP address.
///
/// A single IP can correspond to either one nym-node (which has a single noise key)
/// or one-or-more network monitor agents (each with its own port and noise key).
/// The two variants have independent lifecycles: nym-node entries come from the
/// nym-api topology refresher, while agent entries come from blockchain events.
#[derive(Debug, Clone)]
pub enum NoiseNode {
NymNode { key: VersionedNoiseKeyV1 },
// due to the structure of network monitor agents,
// it is possible to have multiple destinations with the same host ip address,
// but a different noise key.
// however, we are also guaranteed all of those are going to have a unique port.
// note: we're not storing it in a map, since at maximum we might have maybe 20 or so
// entries under a single ip address and linear look-up of a vec is faster than the overhead of a hashmap
NetworkMonitorAgent { nodes: Vec<NetworkMonitorAgentNode> },
}
impl NoiseNode {
pub fn new_nym_node(key: VersionedNoiseKeyV1) -> Self {
NoiseNode::NymNode { key }
}
pub fn new_agent(socket_addr: SocketAddr, key: VersionedNoiseKeyV1) -> Self {
NoiseNode::NetworkMonitorAgent {
nodes: vec![NetworkMonitorAgentNode {
port: socket_addr.port(),
key,
}],
}
}
pub fn swap_view(&self, new: HashMap<SocketAddr, VersionedNoiseKeyV1>) {
let noise_support = new
.iter()
.map(|(s_addr, key)| (s_addr.ip(), key.supported_version))
.collect::<HashMap<_, _>>();
self.keys.inner.store(Arc::new(new));
self.support.inner.store(Arc::new(noise_support));
pub fn is_nym_node(&self) -> bool {
matches!(self, NoiseNode::NymNode { .. })
}
}
/// A single network monitor agent identified by its port on a shared host.
///
/// Multiple agents may share an IP address but are guaranteed to have unique ports.
#[derive(Debug, Clone)]
pub struct NetworkMonitorAgentNode {
pub port: u16,
pub key: VersionedNoiseKeyV1,
}
impl NoiseNetworkView {
pub fn new(nodes: HashMap<IpAddr, NoiseNode>) -> Self {
// ensure we're always storing canonical IPs
NoiseNetworkView {
inner: Arc::new(NoiseNetworkViewInner {
update_lock: Mutex::new(()),
nodes: ArcSwap::from_pointee(
nodes
.into_iter()
.map(|(k, v)| (k.to_canonical(), v))
.collect(),
),
}),
}
}
pub fn new_empty() -> Self {
Self::new(Default::default())
}
/// Build a noise view pre-populated with network monitor agents (used at startup).
pub fn new_with_agents(agents: HashMap<IpAddr, Vec<NetworkMonitorAgentNode>>) -> Self {
let mut nodes = HashMap::new();
for (ip, agent_nodes) in agents {
nodes.insert(ip, NoiseNode::NetworkMonitorAgent { nodes: agent_nodes });
}
Self::new(nodes)
}
pub async fn get_update_permit(&self) -> MutexGuard<'_, ()> {
self.inner.update_lock.lock().await
}
/// Atomically replace the noise key map.
///
/// # Precondition
///
/// The caller **must** hold the permit returned by [`NoiseNetworkView::get_update_permit`].
/// Passing the `MutexGuard` by value enforces this at the type level — the guard is dropped
/// (releasing the lock) only after the swap completes, preventing torn writes from concurrent
/// update calls.
pub fn swap_view(&self, _permit: MutexGuard<'_, ()>, new: HashMap<IpAddr, NoiseNode>) {
// defensive: ensure stored keys are always canonical so lookups (which canonicalise)
// always match. callers should still canonicalise before assembling `new` to keep
// collision resolution deterministic.
let canonical = new
.into_iter()
.map(|(k, v)| (k.to_canonical(), v))
.collect();
self.inner.nodes.store(Arc::new(canonical));
}
pub fn all_nodes(&self) -> HashMap<IpAddr, NoiseNode> {
self.inner.nodes.load().as_ref().clone()
}
}
@@ -126,20 +213,38 @@ impl NoiseConfig {
self
}
pub(crate) fn get_noise_key(&self, s_address: &SocketAddr) -> Option<VersionedNoiseKeyV1> {
self.network.keys.inner.load().get(s_address).copied()
/// Look up the noise key for a specific remote socket address.
///
/// Used on the **initiator** path where we need the responder's public key
/// to start the handshake. For nym-nodes the port is ignored (one key per IP);
/// for network monitor agents, the port disambiguates which agent's key to use.
pub(crate) fn get_noise_key(&self, address: SocketAddr) -> Option<VersionedNoiseKeyV1> {
let ip_to_check = address.ip().to_canonical();
let nodes = self.network.inner.nodes.load();
// Resolve the noise key for `address` from a loaded snapshot of the node map.
// For [`NoiseNode::NymNode`] entries the port is irrelevant — only the IP is matched.
// For [`NoiseNode::NetworkMonitorAgent`] entries the port selects the specific agent.
match nodes.get(&ip_to_check)? {
NoiseNode::NymNode { key } => Some(*key),
NoiseNode::NetworkMonitorAgent { nodes } => {
let port = address.port();
nodes.iter().find(|n| n.port == port).map(|n| n.key)
}
}
}
// Only for phased update
//SW This can lead to some troubles if two nodes share the same IP and one support Noise but not the other.
// This in only for the progressive update though and there is no workaround
pub(crate) fn get_noise_support(&self, ip_addr: IpAddr) -> Option<NoiseVersion> {
let plain_ip_support = self.network.support.inner.load().get(&ip_addr).copied();
// SW default bind address being [::]:1789, it can happen that a responder sees the ipv6-mapped address of the initiator, this check for that
let canonical_ip = &ip_addr.to_canonical();
let canonical_ip_support = self.network.support.inner.load().get(canonical_ip).copied();
plain_ip_support.or(canonical_ip_support)
/// Check whether a remote IP is known to support noise.
/// Used on the responder path where we don't need the remote's key
/// (the initiator sends it during the handshake).
// note: in the case of network monitor agents, it must hold
// that ALL agents on given host support it (or don't support it)
pub(crate) fn supports_noise(&self, ip_addr: IpAddr) -> bool {
self.network
.inner
.nodes
.load()
.contains_key(&ip_addr.to_canonical())
}
}
@@ -169,4 +274,240 @@ mod tests {
}
}
}
mod noise_key_lookup {
use super::super::*;
use nym_crypto::asymmetric::x25519;
use nym_noise_keys::NoiseVersion;
use nym_test_utils::helpers::deterministic_rng;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::Arc;
use std::time::Duration;
fn dummy_key(seed: u8) -> VersionedNoiseKeyV1 {
VersionedNoiseKeyV1 {
supported_version: NoiseVersion::V1,
x25519_pubkey: x25519::PublicKey::from([seed; 32]),
}
}
fn make_config(nodes: HashMap<IpAddr, NoiseNode>) -> NoiseConfig {
NoiseConfig::new(
Arc::new(x25519::KeyPair::new(&mut deterministic_rng())),
NoiseNetworkView::new(nodes),
Duration::from_secs(5),
)
}
// -- get_noise_key tests --
#[test]
fn nym_node_key_returned_regardless_of_port() {
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
let key = dummy_key(1);
let config = make_config(HashMap::from([(ip, NoiseNode::new_nym_node(key))]));
// any port should resolve to the same key
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 1000)), Some(key));
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 9999)), Some(key));
}
#[test]
fn agent_key_resolved_by_port() {
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
let key_a = dummy_key(1);
let key_b = dummy_key(2);
let node = NoiseNode::NetworkMonitorAgent {
nodes: vec![
NetworkMonitorAgentNode {
port: 1000,
key: key_a,
},
NetworkMonitorAgentNode {
port: 2000,
key: key_b,
},
],
};
let config = make_config(HashMap::from([(ip, node)]));
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 1000)), Some(key_a));
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 2000)), Some(key_b));
}
#[test]
fn agent_unknown_port_returns_none() {
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
let node = NoiseNode::NetworkMonitorAgent {
nodes: vec![NetworkMonitorAgentNode {
port: 1000,
key: dummy_key(1),
}],
};
let config = make_config(HashMap::from([(ip, node)]));
assert!(config.get_noise_key(SocketAddr::new(ip, 9999)).is_none());
}
#[test]
fn completely_unknown_address_returns_none() {
let config = make_config(HashMap::new());
assert!(config
.get_noise_key(SocketAddr::new(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4)), 80))
.is_none());
}
#[test]
fn canonical_ipv6_fallback_for_nym_node() {
// register under the plain IPv4 address
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
let key = dummy_key(1);
let config = make_config(HashMap::from([(v4, NoiseNode::new_nym_node(key))]));
// query with the IPv4-mapped IPv6 form (::ffff:1.2.3.4)
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
assert_eq!(
config.get_noise_key(SocketAddr::new(v6_mapped, 1789)),
Some(key)
);
}
#[test]
fn canonical_ipv6_fallback_for_agent() {
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
let key = dummy_key(1);
let node = NoiseNode::NetworkMonitorAgent {
nodes: vec![NetworkMonitorAgentNode { port: 1000, key }],
};
let config = make_config(HashMap::from([(v4, node)]));
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
assert_eq!(
config.get_noise_key(SocketAddr::new(v6_mapped, 1000)),
Some(key)
);
// wrong port still returns None even with the fallback
assert!(config
.get_noise_key(SocketAddr::new(v6_mapped, 9999))
.is_none());
}
// -- supports_noise tests --
#[test]
fn supports_noise_true_for_nym_node() {
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
let config = make_config(HashMap::from([(ip, NoiseNode::new_nym_node(dummy_key(1)))]));
assert!(config.supports_noise(ip));
}
#[test]
fn supports_noise_true_for_agent_ip() {
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
let node = NoiseNode::NetworkMonitorAgent {
nodes: vec![NetworkMonitorAgentNode {
port: 1000,
key: dummy_key(1),
}],
};
let config = make_config(HashMap::from([(ip, node)]));
assert!(config.supports_noise(ip));
}
#[test]
fn supports_noise_false_for_unknown_ip() {
let config = make_config(HashMap::new());
assert!(!config.supports_noise(IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4))));
}
#[test]
fn supports_noise_canonical_ipv6_fallback() {
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
let config = make_config(HashMap::from([(v4, NoiseNode::new_nym_node(dummy_key(1)))]));
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
assert!(config.supports_noise(v6_mapped));
}
// -- new_with_agents test --
#[test]
fn new_with_agents_builds_correct_view() {
let ip = IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1));
let key_a = dummy_key(1);
let key_b = dummy_key(2);
let agents = HashMap::from([(
ip,
vec![
NetworkMonitorAgentNode {
port: 1000,
key: key_a,
},
NetworkMonitorAgentNode {
port: 2000,
key: key_b,
},
],
)]);
let config = NoiseConfig::new(
Arc::new(x25519::KeyPair::new(&mut deterministic_rng())),
NoiseNetworkView::new_with_agents(agents),
Duration::from_secs(5),
);
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 1000)), Some(key_a));
assert_eq!(config.get_noise_key(SocketAddr::new(ip, 2000)), Some(key_b));
assert!(config.supports_noise(ip));
}
// -- swap_view canonicalisation test --
// Regression: an agent registered via blockchain events flows through `swap_view` (called
// from `NetworkMonitorAgentsModule::new_agent` and from the periodic network refresher).
// If a non-canonical (IPv4-mapped IPv6) key reaches `swap_view`, lookups via
// `supports_noise` (which canonicalises) used to miss, producing the
// "can't speak Noise yet, falling back to TCP" warning despite the agent being correctly
// authorised in the routing filter.
#[tokio::test]
async fn swap_view_canonicalises_non_canonical_keys() {
let view = NoiseNetworkView::new_empty();
let v4 = IpAddr::V4(Ipv4Addr::new(1, 2, 3, 4));
let v6_mapped = IpAddr::V6(Ipv4Addr::new(1, 2, 3, 4).to_ipv6_mapped());
let mut nodes = HashMap::new();
// intentionally insert under the IPv4-mapped form — what a buggy caller might do
nodes.insert(
v6_mapped,
NoiseNode::NetworkMonitorAgent {
nodes: vec![NetworkMonitorAgentNode {
port: 1000,
key: dummy_key(1),
}],
},
);
let permit = view.get_update_permit().await;
view.swap_view(permit, nodes);
let config = NoiseConfig::new(
Arc::new(x25519::KeyPair::new(&mut deterministic_rng())),
view,
Duration::from_secs(5),
);
// lookup via either form must succeed
assert!(config.supports_noise(v4));
assert!(config.supports_noise(v6_mapped));
assert!(config
.get_noise_key(SocketAddr::new(v6_mapped, 1000))
.is_some());
}
}
}
+6
View File
@@ -15,6 +15,12 @@ pub enum Connection<C> {
Noise(#[pin] Box<NoiseStream<C>>),
}
impl<C> Connection<C> {
pub fn is_noise(&self) -> bool {
matches!(self, Connection::Noise(_))
}
}
impl<C> AsyncRead for Connection<C>
where
C: AsyncRead + AsyncWrite + Unpin,
+2 -2
View File
@@ -66,7 +66,7 @@ pub async fn upgrade_noise_initiator(
Error::Prereq(Prerequisite::RemotePublicKey)
})?;
let Some(key) = config.get_noise_key(&responder_addr) else {
let Some(key) = config.get_noise_key(responder_addr) else {
warn!("{responder_addr} can't speak Noise yet, falling back to TCP");
return Ok(Connection::Raw(conn));
};
@@ -106,7 +106,7 @@ pub async fn upgrade_noise_responder(
};
// if responder doesn't announce noise support, we fallback to tcp
if config.get_noise_support(initiator_addr.ip()).is_none() {
if !config.supports_noise(initiator_addr.ip()) {
warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",);
return Ok(Connection::Raw(conn));
};
@@ -110,6 +110,12 @@ pub enum PacketProcessingError {
PacketReplay,
}
impl PacketProcessingError {
pub fn is_replay(&self) -> bool {
matches!(self, PacketProcessingError::PacketReplay)
}
}
pub struct PartialyUnwrappedPacketWithKeyRotation {
pub packet: PartiallyUnwrappedPacket,
pub used_key_rotation: u32,
+1 -1
View File
@@ -29,7 +29,7 @@ pub use sphinx_packet::{
packet::builder::DEFAULT_PAYLOAD_SIZE,
payload::{
PAYLOAD_OVERHEAD_SIZE, Payload,
key::{PayloadKey, PayloadKeySeed},
key::{PayloadKey, PayloadKeySeed, derive_payload_key},
},
route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier},
surb::{SURB, SURBMaterial},
+3 -2
View File
@@ -7,8 +7,9 @@ use nyxd_scraper_shared::NyxdScraper;
pub use nyxd_scraper_shared::constants;
pub use nyxd_scraper_shared::error::ScraperError;
pub use nyxd_scraper_shared::{
BlockModule, MsgModule, NyxdScraperTransaction, ParsedTransactionResponse, PruningOptions,
PruningStrategy, StartingBlockOpts, TxModule,
BlockModule, DecodedMessage, MsgModule, NyxdScraperTransaction, ParsedTransactionDetails,
ParsedTransactionResponse, PruningOptions, PruningStrategy, StartingBlockOpts, TxModule,
parse_msg,
};
pub use storage::models;
@@ -9,7 +9,7 @@ use std::str::FromStr;
// replicate behaviour of `CosmosMessageAddressesParser` from juno
pub(crate) fn parse_addresses_from_events(tx: &ParsedTransactionResponse) -> Vec<String> {
let mut addresses: Vec<String> = Vec::new();
for event in &tx.tx_result.events {
for event in &tx.tx_details.tx_result.events {
for attribute in &event.attributes {
let Ok(value) = attribute.value_str() else {
continue;
@@ -147,6 +147,7 @@ impl PostgresStorageTransaction {
for chain_tx in txs {
// bdjuno style, base64 encode them
let signatures = chain_tx
.tx_details
.tx
.signatures
.iter()
@@ -154,12 +155,14 @@ impl PostgresStorageTransaction {
.collect();
let messages = chain_tx
.parsed_messages
.decoded_messages
.values()
.map(|msg| &msg.decoded_content)
.cloned()
.collect::<Vec<_>>();
let signer_infos = chain_tx
.tx_details
.tx
.auth_info
.signer_infos
@@ -167,28 +170,28 @@ impl PostgresStorageTransaction {
.map(|info| proto::cosmos::tx::v1beta1::SignerInfo::from(info.clone()))
.collect::<Vec<_>>();
let hash = chain_tx.hash.to_string();
let height = chain_tx.height.into();
let index = chain_tx.index as i32;
let hash = chain_tx.tx_details.hash.to_string();
let height = chain_tx.tx_details.height().into();
let index = chain_tx.tx_details.index as i32;
let log = serde_json::to_value(chain_tx.tx_result.log.clone())
let log = serde_json::to_value(chain_tx.tx_details.tx_result.log.clone())
.inspect_err(|e| error!(hash, height, index, "Failed to parse logs: {e}"))
.unwrap_or_default();
let events = &chain_tx.tx_result.events;
let events = &chain_tx.tx_details.tx_result.events;
insert_transaction(
hash,
height,
index,
chain_tx.tx_result.code.is_ok(),
chain_tx.tx_details.tx_result.code.is_ok(),
serde_json::Value::Array(messages),
chain_tx.tx.body.memo.clone(),
chain_tx.tx_details.tx.body.memo.clone(),
signatures,
serde_json::to_value(signer_infos)?,
serde_json::to_value(&chain_tx.tx.auth_info.fee)?,
chain_tx.tx_result.gas_wanted,
chain_tx.tx_result.gas_used,
chain_tx.tx_result.log.clone(),
serde_json::to_value(&chain_tx.tx_details.tx.auth_info.fee)?,
chain_tx.tx_details.tx_result.gas_wanted,
chain_tx.tx_details.tx_result.gas_used,
chain_tx.tx_details.tx_result.log.clone(),
json!(log),
json!(events),
self.inner.as_mut(),
@@ -207,17 +210,20 @@ impl PostgresStorageTransaction {
for chain_tx in txs {
let involved_addresses = parse_addresses_from_events(chain_tx);
for (index, msg) in chain_tx.tx.body.messages.iter().enumerate() {
let parsed_message = chain_tx.parsed_messages.get(&index);
for (index, msg) in chain_tx.tx_details.tx.body.messages.iter().enumerate() {
let parsed_message = chain_tx
.decoded_messages
.get(&index)
.map(|msg| &msg.decoded_content);
let value = serde_json::to_value(parsed_message)?;
insert_message(
chain_tx.hash.to_string(),
chain_tx.tx_details.hash.to_string(),
index as i64,
msg.type_url.clone(),
value,
involved_addresses.clone(),
chain_tx.height.into(),
chain_tx.tx_details.height().into(),
self.inner.as_mut(),
)
.await?
@@ -33,9 +33,9 @@ impl TxModule for FancyTxModule {
async fn handle_tx(&mut self, tx: &ParsedTransactionResponse) -> Result<(), ScraperError> {
println!(
"✨ got new tx for height {}: {} ({} msgs)",
tx.block.header.height,
tx.hash,
tx.parsed_messages.len()
tx.tx_details.height(),
tx.tx_details.hash,
tx.tx_details.tx.body.messages.len()
);
Ok(())
@@ -281,7 +281,7 @@ where
&mut self,
block: BlockToProcess,
) -> Result<(), ScraperError> {
info!("processing block at height {}", block.height);
debug!("processing block at height {}", block.height);
let full_info = self
.rpc_client
@@ -291,8 +291,13 @@ where
if let Some(tx_info) = &full_info.transactions {
debug!("this block has {} transaction(s)", tx_info.len());
for tx in tx_info {
debug!("{} has {} message(s)", tx.hash, tx.tx.body.messages.len());
for (index, msg) in tx.tx.body.messages.iter().enumerate() {
let details = &tx.tx_details;
debug!(
"{} has {} message(s)",
details.hash,
details.tx.body.messages.len()
);
for (index, msg) in details.tx.body.messages.iter().enumerate() {
debug!("{index}: {:?}", msg.type_url)
}
}
@@ -315,11 +320,24 @@ where
for tx_module in &mut self.tx_modules {
tx_module.handle_tx(block_tx).await?;
}
let tx_details = &block_tx.tx_details;
// the ones concerned with individual messages
for (index, msg) in block_tx.tx.body.messages.iter().enumerate() {
for (index, msg) in tx_details.tx.body.messages.iter().enumerate() {
let Some(decoded) = block_tx.decoded_messages.get(&index) else {
warn!(
"height: {} tx: {} tx_index: {}, msg_index: {index}: message failed to get decoded",
tx_details.height(),
tx_details.hash,
tx_details.index,
);
continue;
};
for msg_module in &mut self.msg_modules {
if msg.type_url == msg_module.type_url() {
msg_module.handle_msg(index, msg, block_tx).await?
msg_module
.handle_msg(index, msg, decoded, tx_details)
.await?
}
}
}
@@ -7,9 +7,16 @@ use tendermint::{Block, Hash, abci, block, tx};
use tendermint_rpc::endpoint::{block as block_endpoint, block_results, validators};
use tendermint_rpc::event::{Event, EventData};
// just get all everything out of tx::Response, but parse raw `tx` bytes
/// Message decoded from the raw transaction and converted into json.
/// Note that it might have gone through additional processing as set by the `MessageRegistry`
#[derive(Clone, Debug)]
pub struct ParsedTransactionResponse {
pub struct DecodedMessage {
pub type_url: String,
pub decoded_content: serde_json::Value,
}
#[derive(Clone, Debug)]
pub struct ParsedTransactionDetails {
/// The hash of the transaction.
///
/// Deserialized from a hex-encoded string (there is a discrepancy between
@@ -17,8 +24,6 @@ pub struct ParsedTransactionResponse {
/// the Tendermint RPC).
pub hash: Hash,
pub height: block::Height,
pub index: u32,
pub tx_result: abci::types::ExecTxResult,
@@ -27,13 +32,23 @@ pub struct ParsedTransactionResponse {
pub proof: Option<tx::Proof>,
pub parsed_messages: BTreeMap<usize, serde_json::Value>,
pub parsed_message_urls: BTreeMap<usize, String>,
pub block: Block,
}
impl ParsedTransactionDetails {
pub fn height(&self) -> block::Height {
self.block.header.height
}
}
// just get all everything out of tx::Response, but parse raw `tx` bytes
#[derive(Clone, Debug)]
pub struct ParsedTransactionResponse {
pub tx_details: ParsedTransactionDetails,
pub decoded_messages: BTreeMap<usize, DecodedMessage>,
}
#[derive(Debug)]
pub struct FullBlockInformation {
/// Basic block information, including its signers.
+1 -3
View File
@@ -82,10 +82,8 @@ pub enum ScraperError {
source: cosmrs::ErrorReport,
},
#[error("could not parse msg in tx {hash} at index {index} into {type_url}: {source}")]
#[error("could not parse msg of type {type_url}: {source}")]
MsgParseFailure {
hash: Hash,
index: usize,
type_url: String,
#[source]
source: cosmrs::ErrorReport,
+1 -1
View File
@@ -47,7 +47,7 @@ pub fn validator_consensus_address(id: account::Id) -> Result<AccountId, Malform
}
pub fn tx_gas_sum(txs: &[ParsedTransactionResponse]) -> i64 {
txs.iter().map(|tx| tx.tx_result.gas_used).sum()
txs.iter().map(|tx| tx.tx_details.tx_result.gas_used).sum()
}
pub fn validator_info(
+4 -2
View File
@@ -15,12 +15,14 @@ pub(crate) mod subscriber;
pub mod watcher;
pub use block_processor::pruning::{PruningOptions, PruningStrategy};
pub use block_processor::types::ParsedTransactionResponse;
pub use block_processor::types::{
DecodedMessage, ParsedTransactionDetails, ParsedTransactionResponse,
};
pub use cosmos_module::{
CosmosModule,
message_registry::{MessageRegistry, default_message_registry},
};
pub use cosmrs::Any;
pub use modules::{BlockModule, MsgModule, TxModule};
pub use modules::{BlockModule, MsgModule, TxModule, parse_msg};
pub use scraper::{Config, NyxdScraper, StartingBlockOpts};
pub use storage::{NyxdScraperStorage, NyxdScraperTransaction};
@@ -6,5 +6,5 @@ mod msg_module;
mod tx_module;
pub use block_module::BlockModule;
pub use msg_module::MsgModule;
pub use msg_module::{MsgModule, parse_msg};
pub use tx_module::TxModule;
@@ -1,11 +1,47 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::block_processor::types::ParsedTransactionResponse;
use crate::block_processor::types::{DecodedMessage, ParsedTransactionDetails};
use crate::error::ScraperError;
use async_trait::async_trait;
use cosmrs::Any;
use cosmrs::tx::Msg;
/// Parse a protobuf `Any` message into a strongly typed Cosmos message.
///
/// # Example
///
/// ```rust,ignore
/// let execute_msg: MsgExecuteContract = parse_msg(msg)?;
/// ```
///
/// # Errors
///
/// Returns `ScraperError::MsgParseFailure` if:
/// - The type URL doesn't match the expected type
/// - The protobuf bytes are malformed
/// - The message schema is incompatible with this version of the code
pub fn parse_msg<T: Msg>(msg: &Any) -> Result<T, ScraperError> {
T::from_any(msg).map_err(|source| ScraperError::MsgParseFailure {
type_url: msg.type_url.clone(),
source,
})
}
/// Trait for modules that process specific message types from blockchain transactions.
///
/// # Parameters
///
/// - `index`: Position of this message within the transaction (0-based)
/// - `msg`: Raw protobuf message (use `parse_msg()` to decode)
/// - `decoded_msg`: Pre-decoded JSON representation (may be None for unsupported types)
/// - `tx`: Transaction details including block height, hash, and execution result
///
/// # Error Handling
///
/// - Return `Err` for critical failures that should stop block processing
/// - Return `Ok(())` for non-critical errors (e.g., unexpected contract schema)
/// - Log warnings for debugging without propagating errors
#[async_trait]
pub trait MsgModule {
fn type_url(&self) -> String;
@@ -14,6 +50,7 @@ pub trait MsgModule {
&mut self,
index: usize,
msg: &Any,
tx: &ParsedTransactionResponse,
decoded_msg: &DecodedMessage,
tx: &ParsedTransactionDetails,
) -> Result<(), ScraperError>;
}
+20 -16
View File
@@ -2,11 +2,11 @@
// SPDX-License-Identifier: Apache-2.0
use crate::block_processor::types::{
BlockToProcess, FullBlockInformation, ParsedTransactionResponse,
BlockToProcess, DecodedMessage, FullBlockInformation, ParsedTransactionResponse,
};
use crate::error::ScraperError;
use crate::helpers::tx_hash;
use crate::{Any, MessageRegistry, default_message_registry};
use crate::{Any, MessageRegistry, ParsedTransactionDetails, default_message_registry};
use futures::StreamExt;
use futures::future::join3;
use std::collections::BTreeMap;
@@ -77,8 +77,7 @@ impl RpcClient {
) -> Result<Vec<ParsedTransactionResponse>, ScraperError> {
let mut transactions = Vec::with_capacity(raw_transactions.len());
for raw_tx in raw_transactions {
let mut parsed_messages = BTreeMap::new();
let mut parsed_message_urls = BTreeMap::new();
let mut decoded_messages = BTreeMap::new();
let tx = cosmrs::Tx::from_bytes(&raw_tx.tx).map_err(|source| {
ScraperError::TxParseFailure {
hash: raw_tx.hash,
@@ -87,22 +86,27 @@ impl RpcClient {
})?;
for (index, msg) in tx.body.messages.iter().enumerate() {
if let Some(value) = self.decode_or_skip(msg) {
parsed_messages.insert(index, value);
parsed_message_urls.insert(index, msg.type_url.clone());
if let Some(decoded_content) = self.decode_or_skip(msg) {
decoded_messages.insert(
index,
DecodedMessage {
type_url: msg.type_url.clone(),
decoded_content,
},
);
}
}
transactions.push(ParsedTransactionResponse {
hash: raw_tx.hash,
height: raw_tx.height,
index: raw_tx.index,
tx_result: raw_tx.tx_result,
tx,
proof: raw_tx.proof,
parsed_messages,
parsed_message_urls,
block: block.clone(),
tx_details: ParsedTransactionDetails {
hash: raw_tx.hash,
index: raw_tx.index,
tx_result: raw_tx.tx_result,
tx,
proof: raw_tx.proof,
block: block.clone(),
},
decoded_messages,
})
}
Ok(transactions)
+10 -2
View File
@@ -14,15 +14,16 @@ use tracing::info;
use url::Url;
pub struct WatcherConfig {
/// Url to the websocket endpoint of a validator, for example `wss://rpc.nymtech.net/websocket`
/// Url to the websocket endpoint of a validator, for example, `wss://rpc.nymtech.net/websocket`
pub websocket_url: Url,
/// Url to the rpc endpoint of a validator, for example `https://rpc.nymtech.net/`
/// Url to the rpc endpoint of a validator, for example, `https://rpc.nymtech.net/`
pub rpc_url: Url,
}
pub struct NyxdWatcherBuilder {
config: WatcherConfig,
custom_shutdown: CancellationToken,
block_modules: Vec<Box<dyn BlockModule + Send>>,
tx_modules: Vec<Box<dyn TxModule + Send>>,
@@ -33,12 +34,19 @@ impl NyxdWatcherBuilder {
pub fn new(config: WatcherConfig) -> Self {
NyxdWatcherBuilder {
config,
custom_shutdown: CancellationToken::new(),
block_modules: vec![],
tx_modules: vec![],
msg_modules: vec![],
}
}
#[must_use]
pub fn with_custom_shutdown(mut self, token: CancellationToken) -> Self {
self.custom_shutdown = token;
self
}
#[must_use]
pub fn with_block_module<M: BlockModule + Send + 'static>(mut self, module: M) -> Self {
self.block_modules.push(Box::new(module));
+3 -2
View File
@@ -7,8 +7,9 @@ use nyxd_scraper_shared::NyxdScraper;
pub use nyxd_scraper_shared::constants;
pub use nyxd_scraper_shared::error::ScraperError;
pub use nyxd_scraper_shared::{
BlockModule, MsgModule, NyxdScraperTransaction, ParsedTransactionResponse, PruningOptions,
PruningStrategy, StartingBlockOpts, TxModule,
BlockModule, DecodedMessage, MsgModule, NyxdScraperTransaction, ParsedTransactionDetails,
ParsedTransactionResponse, PruningOptions, PruningStrategy, StartingBlockOpts, TxModule,
parse_msg,
};
pub use storage::models;
@@ -132,15 +132,15 @@ impl SqliteStorageTransaction {
for chain_tx in txs {
insert_transaction(
chain_tx.hash.to_string(),
chain_tx.height.into(),
chain_tx.index as i64,
chain_tx.tx_result.code.is_ok(),
chain_tx.tx.body.messages.len() as i64,
chain_tx.tx.body.memo.clone(),
chain_tx.tx_result.gas_wanted,
chain_tx.tx_result.gas_used,
chain_tx.tx_result.log.clone(),
chain_tx.tx_details.hash.to_string(),
chain_tx.tx_details.height().into(),
chain_tx.tx_details.index as i64,
chain_tx.tx_details.tx_result.code.is_ok(),
chain_tx.tx_details.tx.body.messages.len() as i64,
chain_tx.tx_details.tx.body.memo.clone(),
chain_tx.tx_details.tx_result.gas_wanted,
chain_tx.tx_details.tx_result.gas_used,
chain_tx.tx_details.tx_result.log.clone(),
self.0.as_mut(),
)
.await?;
@@ -156,12 +156,12 @@ impl SqliteStorageTransaction {
debug!("persisting messages");
for chain_tx in txs {
for (index, msg) in chain_tx.tx.body.messages.iter().enumerate() {
for (index, msg) in chain_tx.tx_details.tx.body.messages.iter().enumerate() {
insert_message(
chain_tx.hash.to_string(),
chain_tx.tx_details.hash.to_string(),
index as i64,
msg.type_url.clone(),
chain_tx.height.into(),
chain_tx.tx_details.height().into(),
self.0.as_mut(),
)
.await?
+28
View File
@@ -1180,6 +1180,22 @@ dependencies = [
"rand_chacha",
]
[[package]]
name = "network-monitors"
version = "1.0.0"
dependencies = [
"anyhow",
"bs58",
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"cw-storage-plus",
"cw2",
"nym-contracts-common",
"nym-contracts-common-testing",
"nym-network-monitors-contract-common",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
@@ -1456,6 +1472,18 @@ dependencies = [
"regex",
]
[[package]]
name = "nym-network-monitors-contract-common"
version = "1.20.4"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"schemars",
"serde",
"thiserror 2.0.12",
]
[[package]]
name = "nym-pemstore"
version = "1.20.4"
+5 -3
View File
@@ -10,6 +10,7 @@ members = [
"multisig/cw4-group",
"vesting",
"performance",
"network-monitors",
]
[workspace.package]
@@ -19,6 +20,8 @@ homepage = "https://nymtech.net"
documentation = "https://nymtech.net"
edition = "2021"
license = "Apache-2.0"
rust-version = "1.86.0"
readme = "../README.md"
[profile.release]
opt-level = 3
@@ -67,6 +70,7 @@ nym-ecash-contract-common = "1.20.4"
nym-group-contract-common = "1.20.4"
nym-mixnet-contract-common = "1.20.4"
nym-multisig-contract-common = "1.20.4"
nym-network-monitors-contract-common = "1.20.4"
nym-network-defaults = { version = "1.20.4", default-features = false }
nym-performance-contract-common = "1.20.4"
nym-pool-contract-common = "1.20.4"
@@ -94,8 +98,6 @@ unimplemented = "deny"
unreachable = "deny"
# For local development, import via path instead of crates.io, e.g.
# [patch.crates-io]
# nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" }
[patch.crates-io]
nym-network-monitors-contract-common = { path = "../common/cosmwasm-smart-contracts/network-monitors-contract" }
nym-ecash-contract-common = { path = "../common/cosmwasm-smart-contracts/ecash-contract" }
+4
View File
@@ -0,0 +1,4 @@
[alias]
wasm = "build --release --lib --target wasm32-unknown-unknown"
unit-test = "test --lib"
schema = "run --bin schema --features=schema-gen"
+43
View File
@@ -0,0 +1,43 @@
[package]
name = "network-monitors"
description = "CosmWasm smart contract storing information on Nym network monitors"
version = "1.0.0"
authors.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
rust-version.workspace = true
readme.workspace = true
publish = false
[[bin]]
name = "schema"
required-features = ["schema-gen"]
[lib]
name = "network_monitors_contract"
crate-type = ["cdylib", "rlib"]
[dependencies]
bs58 = { workspace = true }
cosmwasm-std = { workspace = true }
cw2 = { workspace = true }
cw-storage-plus = { workspace = true }
cw-controllers = { workspace = true }
cosmwasm-schema = { workspace = true, optional = true }
nym-network-monitors-contract-common = { workspace = true }
nym-contracts-common = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
nym-contracts-common-testing = { workspace = true }
[features]
schema-gen = ["nym-network-monitors-contract-common/schema", "cosmwasm-schema"]
[lints]
workspace = true
+5
View File
@@ -0,0 +1,5 @@
wasm:
RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown
generate-schema:
cargo schema
@@ -0,0 +1,368 @@
{
"contract_name": "network-monitors",
"contract_version": "0.1.0",
"idl_version": "1.0.0",
"instantiate": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "InstantiateMsg",
"type": "object",
"required": [
"orchestrator_address"
],
"properties": {
"orchestrator_address": {
"description": "Address of the initial network monitor orchestrator.",
"type": "string"
}
},
"additionalProperties": false
},
"execute": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ExecuteMsg",
"oneOf": [
{
"description": "Change the admin",
"type": "object",
"required": [
"update_admin"
],
"properties": {
"update_admin": {
"type": "object",
"required": [
"admin"
],
"properties": {
"admin": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Authorise new network monitor orchestrator",
"type": "object",
"required": [
"authorise_network_monitor_orchestrator"
],
"properties": {
"authorise_network_monitor_orchestrator": {
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Revoke network monitor orchestrator authorisation.",
"type": "object",
"required": [
"revoke_network_monitor_orchestrator"
],
"properties": {
"revoke_network_monitor_orchestrator": {
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Authorise new network monitor (or renew authorisation) granting additional privileges when sending mixnet packets to Nym nodes.",
"type": "object",
"required": [
"authorise_network_monitor"
],
"properties": {
"authorise_network_monitor": {
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"type": "string",
"format": "ip"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Revoke network monitor authorisation.",
"type": "object",
"required": [
"revoke_network_monitor"
],
"properties": {
"revoke_network_monitor": {
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"type": "string",
"format": "ip"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Revoke all network monitor authorisations.",
"type": "string",
"enum": [
"revoke_all_network_monitors"
]
}
]
},
"query": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "QueryMsg",
"oneOf": [
{
"type": "object",
"required": [
"admin"
],
"properties": {
"admin": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"network_monitor_orchestrators"
],
"properties": {
"network_monitor_orchestrators": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"network_monitor_agents"
],
"properties": {
"network_monitor_agents": {
"type": "object",
"properties": {
"limit": {
"description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_next_after": {
"description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.",
"type": [
"string",
"null"
],
"format": "ip"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
]
},
"migrate": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MigrateMsg",
"type": "object",
"additionalProperties": false
},
"sudo": null,
"responses": {
"admin": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AdminResponse",
"description": "Returned from Admin.query_admin()",
"type": "object",
"properties": {
"admin": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
},
"network_monitor_agents": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AuthorisedNetworkMonitorsPagedResponse",
"type": "object",
"required": [
"authorised"
],
"properties": {
"authorised": {
"type": "array",
"items": {
"$ref": "#/definitions/AuthorisedNetworkMonitor"
}
},
"start_next_after": {
"type": [
"string",
"null"
],
"format": "ip"
}
},
"additionalProperties": false,
"definitions": {
"Addr": {
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
},
"AuthorisedNetworkMonitor": {
"type": "object",
"required": [
"address",
"authorised_at",
"authorised_by"
],
"properties": {
"address": {
"description": "The Ip address associated with the network monitor agent.",
"type": "string",
"format": "ip"
},
"authorised_at": {
"description": "Timestamp of when the network monitor was authorised.",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
},
"authorised_by": {
"description": "The address of the orchestrator that authorised the network monitor agent.",
"allOf": [
{
"$ref": "#/definitions/Addr"
}
]
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
}
}
},
"network_monitor_orchestrators": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AuthorisedNetworkMonitorOrchestratorsResponse",
"type": "object",
"required": [
"authorised"
],
"properties": {
"authorised": {
"type": "array",
"items": {
"$ref": "#/definitions/AuthorisedNetworkMonitorOrchestrator"
}
}
},
"additionalProperties": false,
"definitions": {
"Addr": {
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
},
"AuthorisedNetworkMonitorOrchestrator": {
"type": "object",
"required": [
"address",
"authorised_at"
],
"properties": {
"address": {
"description": "The address associated with the network monitor orchestrator.",
"allOf": [
{
"$ref": "#/definitions/Addr"
}
]
},
"authorised_at": {
"description": "Timestamp of when the network monitor was authorised.",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
}
}
}
}
}
@@ -0,0 +1,125 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ExecuteMsg",
"oneOf": [
{
"description": "Change the admin",
"type": "object",
"required": [
"update_admin"
],
"properties": {
"update_admin": {
"type": "object",
"required": [
"admin"
],
"properties": {
"admin": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Authorise new network monitor orchestrator",
"type": "object",
"required": [
"authorise_network_monitor_orchestrator"
],
"properties": {
"authorise_network_monitor_orchestrator": {
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Revoke network monitor orchestrator authorisation.",
"type": "object",
"required": [
"revoke_network_monitor_orchestrator"
],
"properties": {
"revoke_network_monitor_orchestrator": {
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"type": "string"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Authorise new network monitor (or renew authorisation) granting additional privileges when sending mixnet packets to Nym nodes.",
"type": "object",
"required": [
"authorise_network_monitor"
],
"properties": {
"authorise_network_monitor": {
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"type": "string",
"format": "ip"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Revoke network monitor authorisation.",
"type": "object",
"required": [
"revoke_network_monitor"
],
"properties": {
"revoke_network_monitor": {
"type": "object",
"required": [
"address"
],
"properties": {
"address": {
"type": "string",
"format": "ip"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"description": "Revoke all network monitor authorisations.",
"type": "string",
"enum": [
"revoke_all_network_monitors"
]
}
]
}
@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "InstantiateMsg",
"type": "object",
"required": [
"orchestrator_address"
],
"properties": {
"orchestrator_address": {
"description": "Address of the initial network monitor orchestrator.",
"type": "string"
}
},
"additionalProperties": false
}
@@ -0,0 +1,6 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MigrateMsg",
"type": "object",
"additionalProperties": false
}
@@ -0,0 +1,64 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "QueryMsg",
"oneOf": [
{
"type": "object",
"required": [
"admin"
],
"properties": {
"admin": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"network_monitor_orchestrators"
],
"properties": {
"network_monitor_orchestrators": {
"type": "object",
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"network_monitor_agents"
],
"properties": {
"network_monitor_agents": {
"type": "object",
"properties": {
"limit": {
"description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.",
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_next_after": {
"description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.",
"type": [
"string",
"null"
],
"format": "ip"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
]
}
@@ -0,0 +1,15 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AdminResponse",
"description": "Returned from Admin.query_admin()",
"type": "object",
"properties": {
"admin": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
}
@@ -0,0 +1,74 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AuthorisedNetworkMonitorsPagedResponse",
"type": "object",
"required": [
"authorised"
],
"properties": {
"authorised": {
"type": "array",
"items": {
"$ref": "#/definitions/AuthorisedNetworkMonitor"
}
},
"start_next_after": {
"type": [
"string",
"null"
],
"format": "ip"
}
},
"additionalProperties": false,
"definitions": {
"Addr": {
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
},
"AuthorisedNetworkMonitor": {
"type": "object",
"required": [
"address",
"authorised_at",
"authorised_by"
],
"properties": {
"address": {
"description": "The Ip address associated with the network monitor agent.",
"type": "string",
"format": "ip"
},
"authorised_at": {
"description": "Timestamp of when the network monitor was authorised.",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
},
"authorised_by": {
"description": "The address of the orchestrator that authorised the network monitor agent.",
"allOf": [
{
"$ref": "#/definitions/Addr"
}
]
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
}
}
}
@@ -0,0 +1,61 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "AuthorisedNetworkMonitorOrchestratorsResponse",
"type": "object",
"required": [
"authorised"
],
"properties": {
"authorised": {
"type": "array",
"items": {
"$ref": "#/definitions/AuthorisedNetworkMonitorOrchestrator"
}
}
},
"additionalProperties": false,
"definitions": {
"Addr": {
"description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.",
"type": "string"
},
"AuthorisedNetworkMonitorOrchestrator": {
"type": "object",
"required": [
"address",
"authorised_at"
],
"properties": {
"address": {
"description": "The address associated with the network monitor orchestrator.",
"allOf": [
{
"$ref": "#/definitions/Addr"
}
]
},
"authorised_at": {
"description": "Timestamp of when the network monitor was authorised.",
"allOf": [
{
"$ref": "#/definitions/Timestamp"
}
]
}
},
"additionalProperties": false
},
"Timestamp": {
"description": "A point in time in nanosecond precision.\n\nThis type can represent times from 1970-01-01T00:00:00Z to 2554-07-21T23:34:33Z.\n\n## Examples\n\n``` # use cosmwasm_std::Timestamp; let ts = Timestamp::from_nanos(1_000_000_202); assert_eq!(ts.nanos(), 1_000_000_202); assert_eq!(ts.seconds(), 1); assert_eq!(ts.subsec_nanos(), 202);\n\nlet ts = ts.plus_seconds(2); assert_eq!(ts.nanos(), 3_000_000_202); assert_eq!(ts.seconds(), 3); assert_eq!(ts.subsec_nanos(), 202); ```",
"allOf": [
{
"$ref": "#/definitions/Uint64"
}
]
},
"Uint64": {
"description": "A thin wrapper around u64 that is using strings for JSON encoding/decoding, such that the full u64 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u64` to get the value out:\n\n``` # use cosmwasm_std::Uint64; let a = Uint64::from(42u64); assert_eq!(a.u64(), 42);\n\nlet b = Uint64::from(70u32); assert_eq!(b.u64(), 70); ```",
"type": "string"
}
}
}
@@ -0,0 +1,14 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::write_api;
use nym_network_monitors_contract_common::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
fn main() {
write_api! {
instantiate: InstantiateMsg,
query: QueryMsg,
execute: ExecuteMsg,
migrate: MigrateMsg,
}
}
+179
View File
@@ -0,0 +1,179 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::queries::{
query_admin, query_network_monitor_agents, query_network_monitor_orchestrators,
};
use crate::storage::NETWORK_MONITORS_CONTRACT_STORAGE;
use crate::transactions::{
try_authorise_network_monitor, try_authorise_network_monitor_orchestrator,
try_revoke_all_network_monitors, try_revoke_network_monitor,
try_revoke_network_monitor_orchestrator, try_update_contract_admin,
try_update_orchestrator_identity_key,
};
use cosmwasm_std::{
entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response,
};
use nym_contracts_common::set_build_information;
use nym_network_monitors_contract_common::{
ExecuteMsg, InstantiateMsg, MigrateMsg, NetworkMonitorsContractError, QueryMsg,
};
const CONTRACT_NAME: &str = "crate:nym-network-monitors-contract";
const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION");
#[entry_point]
pub fn instantiate(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: InstantiateMsg,
) -> Result<Response, NetworkMonitorsContractError> {
cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
set_build_information!(deps.storage)?;
let orchestrator = deps.api.addr_validate(&msg.orchestrator_address)?;
NETWORK_MONITORS_CONTRACT_STORAGE.initialise(deps, env, info.sender, orchestrator)?;
Ok(Response::default())
}
#[entry_point]
pub fn execute(
deps: DepsMut,
env: Env,
info: MessageInfo,
msg: ExecuteMsg,
) -> Result<Response, NetworkMonitorsContractError> {
match msg {
ExecuteMsg::UpdateAdmin { admin } => try_update_contract_admin(deps, info, admin),
ExecuteMsg::AuthoriseNetworkMonitorOrchestrator { address } => {
try_authorise_network_monitor_orchestrator(deps, env, info, address)
}
ExecuteMsg::UpdateOrchestratorIdentityKey { key } => {
try_update_orchestrator_identity_key(deps, info, key)
}
ExecuteMsg::RevokeNetworkMonitorOrchestrator { address } => {
try_revoke_network_monitor_orchestrator(deps, info, address)
}
ExecuteMsg::AuthoriseNetworkMonitor {
mixnet_address: address,
bs58_x25519_noise,
noise_version,
} => try_authorise_network_monitor(
deps,
env,
info,
address,
bs58_x25519_noise,
noise_version,
),
ExecuteMsg::RevokeNetworkMonitor { address } => {
try_revoke_network_monitor(deps, info, address)
}
ExecuteMsg::RevokeAllNetworkMonitors => try_revoke_all_network_monitors(deps, info),
}
}
#[entry_point]
pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> Result<Binary, NetworkMonitorsContractError> {
match msg {
QueryMsg::Admin {} => Ok(to_json_binary(&query_admin(deps)?)?),
QueryMsg::NetworkMonitorOrchestrators {} => {
Ok(to_json_binary(&query_network_monitor_orchestrators(deps)?)?)
}
QueryMsg::NetworkMonitorAgents {
start_next_after,
limit,
} => Ok(to_json_binary(&query_network_monitor_agents(
deps,
start_next_after,
limit,
)?)?),
}
}
#[entry_point]
pub fn migrate(
deps: DepsMut,
_env: Env,
_msg: MigrateMsg,
) -> Result<Response, NetworkMonitorsContractError> {
set_build_information!(deps.storage)?;
cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
Ok(Default::default())
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
mod contract_instantiation {
use super::*;
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};
use cosmwasm_std::Addr;
#[test]
fn sets_contract_admin_to_the_message_sender() -> anyhow::Result<()> {
let mut deps = mock_dependencies();
let env = mock_env();
let init_msg = InstantiateMsg {
orchestrator_address: deps.api.addr_make("foo").to_string(),
};
let some_sender = deps.api.addr_make("some_sender");
instantiate(
deps.as_mut(),
env,
message_info(&some_sender, &[]),
init_msg,
)?;
NETWORK_MONITORS_CONTRACT_STORAGE
.contract_admin
.assert_admin(deps.as_ref(), &some_sender)?;
Ok(())
}
#[test]
fn sets_the_initial_orchestrator() -> anyhow::Result<()> {
let mut deps = mock_dependencies();
let env = mock_env();
let admin = deps.api.addr_make("some_sender");
let bad_addr = "foo".to_string();
let good_addr = deps.api.addr_make("foo").to_string();
let bad_init_msg = InstantiateMsg {
orchestrator_address: bad_addr.clone(),
};
let good_init_msg = InstantiateMsg {
orchestrator_address: good_addr.clone(),
};
let res = instantiate(
deps.as_mut(),
env.clone(),
message_info(&admin, &[]),
bad_init_msg,
);
assert!(res.is_err());
let is_orchestrator = NETWORK_MONITORS_CONTRACT_STORAGE
.is_orchestrator(deps.as_ref(), &Addr::unchecked(&good_addr))?;
assert!(!is_orchestrator);
instantiate(deps.as_mut(), env, message_info(&admin, &[]), good_init_msg)?;
let is_orchestrator = NETWORK_MONITORS_CONTRACT_STORAGE
.is_orchestrator(deps.as_ref(), &Addr::unchecked(&good_addr))?;
assert!(is_orchestrator);
Ok(())
}
}
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod contract;
pub mod queries;
pub mod queued_migrations;
pub mod storage;
pub mod transactions;
#[cfg(test)]
pub mod testing;
+345
View File
@@ -0,0 +1,345 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::storage::{retrieval_limits, AgentStorageKey, NETWORK_MONITORS_CONTRACT_STORAGE};
use cosmwasm_std::{Deps, StdResult};
use cw_controllers::AdminResponse;
use cw_storage_plus::Bound;
use nym_network_monitors_contract_common::{
AuthorisedNetworkMonitorOrchestratorsResponse, AuthorisedNetworkMonitorsPagedResponse,
NetworkMonitorsContractError,
};
use std::net::SocketAddr;
pub fn query_admin(deps: Deps) -> Result<AdminResponse, NetworkMonitorsContractError> {
NETWORK_MONITORS_CONTRACT_STORAGE
.contract_admin
.query_admin(deps)
.map_err(Into::into)
}
// no need for pagination as we don't expect even a double digit of those
pub fn query_network_monitor_orchestrators(
deps: Deps,
) -> Result<AuthorisedNetworkMonitorOrchestratorsResponse, NetworkMonitorsContractError> {
let authorised = NETWORK_MONITORS_CONTRACT_STORAGE
.authorised_orchestrators
.range(deps.storage, None, None, cosmwasm_std::Order::Ascending)
.map(|record| record.map(|(_, details)| details))
.collect::<StdResult<Vec<_>>>()?;
Ok(AuthorisedNetworkMonitorOrchestratorsResponse { authorised })
}
pub fn query_network_monitor_agents(
deps: Deps,
start_after: Option<SocketAddr>,
limit: Option<u32>,
) -> Result<AuthorisedNetworkMonitorsPagedResponse, NetworkMonitorsContractError> {
let limit = limit
.unwrap_or(retrieval_limits::AGENTS_DEFAULT_LIMIT)
.min(retrieval_limits::AGENTS_MAX_LIMIT) as usize;
let start = start_after.map(|addr| Bound::exclusive(AgentStorageKey::from(addr)));
let authorised = NETWORK_MONITORS_CONTRACT_STORAGE
.authorised_agents
.range(deps.storage, start, None, cosmwasm_std::Order::Ascending)
.take(limit)
.map(|record| record.map(|(_, details)| details))
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = authorised.last().map(|last| last.mixnet_address);
Ok(AuthorisedNetworkMonitorsPagedResponse {
authorised,
start_next_after,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(test)]
mod admin_query {
use crate::queries::query_admin;
use crate::testing::init_contract_tester;
use nym_contracts_common_testing::{AdminExt, ChainOpts, ContractOpts, RandExt};
use nym_network_monitors_contract_common::ExecuteMsg;
#[test]
fn returns_current_admin() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let initial_admin = test.admin_unchecked();
// initial
let res = query_admin(test.deps())?;
assert_eq!(res.admin, Some(initial_admin.to_string()));
let new_admin = test.generate_account();
// sanity check
assert_ne!(initial_admin, new_admin);
// after update
test.execute_msg(
initial_admin.clone(),
&ExecuteMsg::UpdateAdmin {
admin: new_admin.to_string(),
},
)?;
let updated_admin = query_admin(test.deps())?;
assert_eq!(updated_admin.admin, Some(new_admin.to_string()));
Ok(())
}
}
#[cfg(test)]
mod network_monitor_orchestrators_query {
use super::*;
use crate::testing::{init_contract_tester, NetworkMonitorsContractTesterExt};
use nym_contracts_common_testing::{AdminExt, ContractOpts};
use nym_network_monitors_contract_common::ExecuteMsg;
#[test]
fn returns_empty_list_when_there_are_no_extra_orchestrators() -> anyhow::Result<()> {
// make sure to start with an empty state
let mut test = init_contract_tester();
test.remove_all_orchestrators();
let res = query_network_monitor_orchestrators(test.deps())?;
assert!(res.authorised.is_empty());
Ok(())
}
#[test]
fn returns_all_authorised_orchestrators() -> anyhow::Result<()> {
// make sure to start with an empty state
let mut test = init_contract_tester();
test.remove_all_orchestrators();
let orchestrator1 = test.add_orchestrator()?;
let orchestrator2 = test.add_orchestrator()?;
let orchestrator3 = test.add_orchestrator()?;
let res = query_network_monitor_orchestrators(test.deps())?;
assert_eq!(res.authorised.len(), 3);
assert!(res.authorised.iter().any(|o| o.address == orchestrator1));
assert!(res.authorised.iter().any(|o| o.address == orchestrator2));
assert!(res.authorised.iter().any(|o| o.address == orchestrator3));
Ok(())
}
#[test]
fn does_not_return_revoked_orchestrators() -> anyhow::Result<()> {
// make sure to start with an empty state
let mut test = init_contract_tester();
test.remove_all_orchestrators();
let orchestrator1 = test.add_orchestrator()?;
let orchestrator2 = test.add_orchestrator()?;
test.execute_raw(
test.admin_unchecked(),
ExecuteMsg::RevokeNetworkMonitorOrchestrator {
address: orchestrator1.to_string(),
},
)?;
let res = query_network_monitor_orchestrators(test.deps())?;
assert!(!res.authorised.iter().any(|o| o.address == orchestrator1));
assert!(res.authorised.iter().any(|o| o.address == orchestrator2));
Ok(())
}
#[test]
fn returns_entries_in_ascending_order() -> anyhow::Result<()> {
// make sure to start with an empty state
let mut test = init_contract_tester();
test.remove_all_orchestrators();
test.add_orchestrator()?;
test.add_orchestrator()?;
test.add_orchestrator()?;
let res = query_network_monitor_orchestrators(test.deps())?;
assert!(res
.authorised
.windows(2)
.all(|window| window[0].address <= window[1].address));
Ok(())
}
}
#[cfg(test)]
mod network_monitor_agents_query {
use super::*;
use crate::testing::{
init_contract_tester, storage_socket_comp, NetworkMonitorsContract,
NetworkMonitorsContractTesterExt,
};
use nym_contracts_common_testing::{ContractOpts, ContractTester};
use std::net::SocketAddr;
fn storage_sorted_addresses(
test: &mut ContractTester<NetworkMonitorsContract>,
n: usize,
) -> Vec<SocketAddr> {
let mut ips = Vec::new();
for _ in 0..n {
ips.push(test.random_socket());
}
ips.sort_by(|a, b| storage_socket_comp(*a, *b));
ips
}
#[test]
fn returns_empty_response_when_no_agents_are_authorised() -> anyhow::Result<()> {
let test = init_contract_tester();
let res = query_network_monitor_agents(test.deps(), None, None)?;
assert!(res.authorised.is_empty());
assert_eq!(res.start_next_after, None);
Ok(())
}
#[test]
fn returns_all_authorised_agents_below_default_limit() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let agents = storage_sorted_addresses(&mut test, 5);
for agent in &agents {
test.add_dummy_agent(*agent)
}
let res = query_network_monitor_agents(test.deps(), None, None)?;
assert_eq!(res.authorised.len(), agents.len());
assert_eq!(res.start_next_after, agents.last().copied());
for agent in &agents {
assert!(res.authorised.iter().any(|a| a.mixnet_address == *agent));
}
Ok(())
}
#[test]
fn respects_explicit_limit() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let agents = storage_sorted_addresses(&mut test, 5);
for agent in &agents {
test.add_dummy_agent(*agent)
}
let res = query_network_monitor_agents(test.deps(), None, Some(2))?;
assert_eq!(res.authorised.len(), 2);
assert_eq!(res.authorised[0].mixnet_address, agents[0]);
assert_eq!(res.authorised[1].mixnet_address, agents[1]);
assert_eq!(res.start_next_after, Some(agents[1]));
Ok(())
}
#[test]
fn respects_start_after_for_pagination() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let agents = storage_sorted_addresses(&mut test, 5);
for agent in &agents {
test.add_dummy_agent(*agent)
}
let res = query_network_monitor_agents(test.deps(), Some(agents[1]), Some(2))?;
assert_eq!(res.authorised.len(), 2);
assert_eq!(res.authorised[0].mixnet_address, agents[2]);
assert_eq!(res.authorised[1].mixnet_address, agents[3]);
assert_eq!(res.start_next_after, Some(agents[3]));
Ok(())
}
#[test]
fn caps_limit_at_maximum() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let total = retrieval_limits::AGENTS_MAX_LIMIT as usize + 20;
let agents = storage_sorted_addresses(&mut test, total);
for agent in &agents {
test.add_dummy_agent(*agent)
}
let res = query_network_monitor_agents(
test.deps(),
None,
Some(retrieval_limits::AGENTS_MAX_LIMIT + 1),
)?;
assert_eq!(
res.authorised.len(),
retrieval_limits::AGENTS_MAX_LIMIT as usize
);
assert_eq!(
res.start_next_after,
Some(agents[retrieval_limits::AGENTS_MAX_LIMIT as usize - 1])
);
Ok(())
}
#[test]
fn start_next_after_is_none_for_empty_page() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let agents = storage_sorted_addresses(&mut test, 3);
for agent in &agents {
test.add_dummy_agent(*agent)
}
let res = query_network_monitor_agents(test.deps(), Some(agents[2]), Some(10))?;
assert!(res.authorised.is_empty());
assert_eq!(res.start_next_after, None);
Ok(())
}
#[test]
fn returns_entries_in_ascending_order() -> anyhow::Result<()> {
let mut test = init_contract_tester();
let agents = storage_sorted_addresses(&mut test, 6);
for agent in &agents {
test.add_dummy_agent(*agent)
}
let res = query_network_monitor_agents(test.deps(), None, None)?;
assert!(res.authorised.windows(2).all(|window| storage_socket_comp(
window[0].mixnet_address,
window[1].mixnet_address
)
.is_le()));
Ok(())
}
}
}
@@ -0,0 +1,2 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,188 @@
// Copyright 2026 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::contract::{execute, instantiate, migrate, query};
use cosmwasm_std::{Addr, Order};
use nym_contracts_common_testing::{
mock_dependencies, AdminExt, ChainOpts, CommonStorageKeys, ContractFn, ContractOpts,
ContractTester, DenomExt, PermissionedFn, QueryFn, RandExt, Rng, RngCore, TestableNymContract,
};
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
use crate::storage::NetworkMonitorsStorage;
use nym_network_monitors_contract_common::constants::storage_keys;
use nym_network_monitors_contract_common::{
ExecuteMsg, InstantiateMsg, MigrateMsg, NetworkMonitorsContractError, QueryMsg,
};
pub struct NetworkMonitorsContract;
impl TestableNymContract for NetworkMonitorsContract {
const NAME: &'static str = "nym-network-monitors-contract";
type InitMsg = InstantiateMsg;
type ExecuteMsg = ExecuteMsg;
type QueryMsg = QueryMsg;
type MigrateMsg = MigrateMsg;
type ContractError = NetworkMonitorsContractError;
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
instantiate
}
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
execute
}
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
query
}
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
migrate
}
fn base_init_msg() -> Self::InitMsg {
let deps = mock_dependencies();
InstantiateMsg {
orchestrator_address: deps.api.addr_make("initial-dummy-orchestrator").to_string(),
}
}
}
pub fn init_contract_tester() -> ContractTester<NetworkMonitorsContract> {
NetworkMonitorsContract::init()
.with_common_storage_key(CommonStorageKeys::Admin, storage_keys::CONTRACT_ADMIN)
}
pub trait NetworkMonitorsContractTesterExt:
ContractOpts<
ExecuteMsg = ExecuteMsg,
QueryMsg = QueryMsg,
ContractError = NetworkMonitorsContractError,
> + ChainOpts
+ AdminExt
+ DenomExt
+ RandExt
{
fn add_orchestrator(&mut self) -> Result<Addr, NetworkMonitorsContractError> {
let admin = self.admin_unchecked();
let addr = self.generate_account();
self.execute_raw(
admin,
ExecuteMsg::AuthoriseNetworkMonitorOrchestrator {
address: addr.to_string(),
},
)?;
Ok(addr)
}
fn remove_all_orchestrators(&mut self) {
let orchestrators = self.all_orchestrators();
for orchestrator in orchestrators {
self.execute_raw(
self.admin_unchecked(),
ExecuteMsg::RevokeNetworkMonitorOrchestrator {
address: orchestrator.to_string(),
},
)
.unwrap();
}
}
fn add_dummy_agent(&mut self, agent: SocketAddr) {
let orchestrators = self.all_orchestrators();
let orchestrator = match orchestrators.first() {
Some(orchestrator) => orchestrator.clone(),
None => self.add_orchestrator().unwrap().clone(),
};
self.execute_raw(
orchestrator,
ExecuteMsg::AuthoriseNetworkMonitor {
mixnet_address: agent,
bs58_x25519_noise: "11111111111111111111111111111111".to_string(),
noise_version: 1,
},
)
.unwrap();
}
fn random_ipv4(&mut self) -> IpAddr {
let rng = self.raw_rng();
IpAddr::V4(Ipv4Addr::new(rng.gen(), rng.gen(), rng.gen(), rng.gen()))
}
fn random_ipv6(&mut self) -> IpAddr {
let rng = self.raw_rng();
IpAddr::V6(Ipv6Addr::new(
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
rng.gen(),
))
}
fn random_ip(&mut self) -> IpAddr {
let rng = self.raw_rng();
// toss a coin, if even => ipv4, if odd => ipv6
if rng.next_u32() % 2 == 0 {
self.random_ipv4()
} else {
self.random_ipv6()
}
}
fn random_socket_ipv4(&mut self) -> SocketAddr {
let port = self.raw_rng().gen();
SocketAddr::new(self.random_ipv4(), port)
}
fn random_socket_ipv6(&mut self) -> SocketAddr {
let port = self.raw_rng().gen();
SocketAddr::new(self.random_ipv6(), port)
}
fn random_socket(&mut self) -> SocketAddr {
let port = self.raw_rng().gen();
SocketAddr::new(self.random_ip(), port)
}
fn all_agents(&self) -> Vec<SocketAddr> {
NetworkMonitorsStorage::new()
.authorised_agents
.range(self.storage(), None, None, Order::Ascending)
.map(|record| record.unwrap().1.mixnet_address)
.collect()
}
fn all_orchestrators(&self) -> Vec<Addr> {
NetworkMonitorsStorage::new()
.authorised_orchestrators
.range(self.storage(), None, None, Order::Ascending)
.map(|record| record.unwrap().0)
.collect()
}
}
impl NetworkMonitorsContractTesterExt for ContractTester<NetworkMonitorsContract> {}
/// Compare SocketAddrs in the same order as the storage key encoding.
///
/// Storage keys are: `[0, ip_len] [ip_octets...] [port_be_bytes]`
/// This means IPv4 (len=4) always sorts before IPv6 (len=16),
/// within the same type keys sort by IP octets then by port.
pub(crate) fn storage_socket_comp(a: SocketAddr, b: SocketAddr) -> std::cmp::Ordering {
let ip_ord = match (a.ip(), b.ip()) {
(IpAddr::V4(a), IpAddr::V4(b)) => a.octets().cmp(&b.octets()),
(IpAddr::V6(a), IpAddr::V6(b)) => a.octets().cmp(&b.octets()),
// length prefix [0, 4] < [0, 16] so all IPv4 sorts before all IPv6
(IpAddr::V4(_), IpAddr::V6(_)) => std::cmp::Ordering::Less,
(IpAddr::V6(_), IpAddr::V4(_)) => std::cmp::Ordering::Greater,
};
ip_ord.then(a.port().cmp(&b.port()))
}
File diff suppressed because it is too large Load Diff

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