Compare commits

...

62 Commits

Author SHA1 Message Date
Floriane TUERNAL SABOTINOV 190ddc0d7b Revert "handle_binary"
This reverts commit bc47fdfdfd.
2025-09-02 15:25:21 +02:00
Floriane TUERNAL SABOTINOV 7567b57553 Revert "missing span"
This reverts commit 281de87871.
2025-09-02 15:25:20 +02:00
Floriane TUERNAL SABOTINOV 3268aa63e6 Revert "initial_auth missing span"
This reverts commit d86a393412.
2025-09-02 15:25:19 +02:00
Floriane TUERNAL SABOTINOV 132a8c66f6 Revert "debug handle_request instrument"
This reverts commit f6dffa2a38.
2025-09-02 15:25:18 +02:00
Floriane TUERNAL SABOTINOV f932135799 Revert "rm noisy instrument"
This reverts commit 0fe5264231.
2025-09-02 15:25:16 +02:00
Floriane TUERNAL SABOTINOV 3b8b29a0ab Revert "add span propagation test"
This reverts commit 68ebe93ba5.
2025-09-02 15:25:15 +02:00
Floriane TUERNAL SABOTINOV cfec1781a8 Revert "packet forwarding spans"
This reverts commit fa02376403.
2025-09-02 15:25:11 +02:00
Floriane TUERNAL SABOTINOV 03280cd9b4 Revert "auth and gateway changes"
This reverts commit 7609d6ae4d.
2025-09-02 15:25:08 +02:00
Floriane TUERNAL SABOTINOV cd16239cf3 Revert "test lifetime span into tokio select"
This reverts commit 7fa5a778aa.
2025-09-02 15:24:52 +02:00
Floriane TUERNAL SABOTINOV 7fa5a778aa test lifetime span into tokio select 2025-08-27 14:12:15 +02:00
Floriane TUERNAL SABOTINOV 7609d6ae4d auth and gateway changes 2025-08-27 09:10:29 +02:00
Floriane TUERNAL SABOTINOV fa02376403 packet forwarding spans 2025-08-26 13:44:59 +02:00
Floriane TUERNAL SABOTINOV 68ebe93ba5 add span propagation test 2025-08-26 09:12:26 +02:00
Floriane TUERNAL SABOTINOV 0fe5264231 rm noisy instrument 2025-08-25 15:04:45 +02:00
Floriane TUERNAL SABOTINOV f6dffa2a38 debug handle_request instrument 2025-08-25 15:04:18 +02:00
Floriane TUERNAL SABOTINOV d86a393412 initial_auth missing span 2025-08-25 12:33:33 +02:00
Floriane TUERNAL SABOTINOV 281de87871 missing span 2025-08-25 11:37:24 +02:00
Floriane TUERNAL SABOTINOV bc47fdfdfd handle_binary 2025-08-22 15:38:56 +02:00
Mark Sinclair 05dd39699d add tracing event formatter with trace_id and span_id 2025-08-06 11:28:53 +01:00
mfahampshire 5cf97977a1 add feature flag to tracing-sub 2025-08-05 20:30:13 +02:00
Mark Sinclair a47a10fdde JSON log format 2025-08-05 18:29:18 +01:00
mfahampshire 2a8265371d try fix the propogation of trace_id across client-node v2 2025-08-05 19:09:42 +02:00
mfahampshire 0b7513ff55 try fix the propogation of trace_id across client-node 2025-08-05 18:43:15 +02:00
mfahampshire 312c51535f adding tracing to syslog 2025-08-05 18:10:56 +02:00
mfahampshire 54fb9ebfab try add traceID to syslogs 2025-08-05 17:54:05 +02:00
mfahampshire 3ec9c4d8d7 change to check authv2 2025-08-05 16:20:56 +02:00
mfahampshire 6d0b899648 yet more logging 2025-08-05 14:43:38 +02:00
mfahampshire bb5187eb0a warning logs (more) 2025-08-05 14:04:34 +02:00
mfahampshire 558df0975b warning logs 2025-08-05 12:11:46 +02:00
mfahampshire 952fcdb2b3 added tracing command for testing + extra logging 2025-08-05 11:21:31 +02:00
mfahampshire 7bc4a69a1c pull guard into main fn scope 2025-08-04 18:22:58 +02:00
mfahampshire dd1e46ec15 force connect to gateway 2025-08-04 14:29:14 +02:00
mfahampshire 8c219f0603 Made logging conditional on execution 2025-08-04 12:44:12 +02:00
mfahampshire 8c0addb987 moved telemetry to logging from mod 2025-08-02 00:27:31 +02:00
mfahampshire fc53eec241 add telemetry builder to node.run() 2025-08-01 17:17:14 +02:00
Mark Sinclair 12649627a5 tidy up 2025-08-01 13:58:17 +01:00
mfahampshire 99684e79e4 think its fixed 2025-08-01 14:53:18 +02:00
Mark Sinclair fc76c2d7a0 wip 2025-08-01 12:58:52 +01:00
Mark Sinclair 18f7ba92bb wip 2025-07-31 20:09:38 +01:00
import this 33339c085d [DOCs/operators]: Update ISP list (#5918)
* update ISP list

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

* add monero ports to local testing script

* console output snippet update
2025-07-30 09:43:39 +00:00
Jack Wampler 917f391948 Make DNS Resolver fallback optional (#5920)
default to no dns system fallback, but keep support
2025-07-29 11:00:24 -06:00
Jędrzej Stuczyński a4e674c98b basic zulip client for sending messages (#5913) 2025-07-24 16:22:35 +01:00
Bogdan-Ștefan Neacşu b975d08342 Remove old free credential handle (#5864)
* Set cached storage counters to 0 (#5812)

* Set cached storage counters to 0

* u64 to i64 log possible error

* Check addition too

Debug commit

Remove more data from wg storage peer

Put actual ticket type in storage

Simplify add peer

Finish rebase

Pass defguard Peer

Cache less data for consumption

GatewayStorage traits

Wg API trait

Mock test structures

Unit test for peer controller

EcashManager trait

Init test of Authenticator

Remove peer test

* Fix windows different API

* Use make_bincode_serializer like in other places

* Add log_slow_statements to gateway storage

* Use correct LevelFilter

* Fix clippy

* More win fix

* Win clippy

* Use two error variants more

* Use only one Arc<RwLock<T>> instead of many more

* Remove commented test

* Specific trait import
2025-07-23 17:07:12 +03:00
Jędrzej Stuczyński 8e44f9f07f chore: allow compatibility with 'CDLA-Permissive-2.0' (#5910) 2025-07-23 14:48:40 +01:00
benedetta davico 8461d085a5 Merge pull request #5906 from nymtech/release/2025.13-emmental
merge release/2025.13-emmental to develop
2025-07-22 16:23:29 +02:00
Drazen Urch af9f6e5ca0 Allow PG database backend (#5880)
* feat(db): add SQL query wrapper for PostgreSQL placeholder conversion

- Created query_wrapper module with functions to automatically convert
  SQLite ? placeholders to PostgreSQL $1, $2, ... format
- Updated build.rs to handle mutually exclusive feature flags
- Modified one query in mixnodes.rs as proof of concept
- Added type conversions for PostgreSQL compatibility (u32->i64, u16->i32)

This is a checkpoint commit before converting all queries to use the wrapper.

* feat(nym-node-status-api): add PostgreSQL database support via feature flags

Implement dual database support for SQLite and PostgreSQL through Cargo feature flags.
The implementation uses a query wrapper that automatically converts SQLite-style ?
placeholders to PostgreSQL-style $1, $2, ... placeholders at runtime.

Key changes:
- Add query wrapper functions that handle placeholder conversion
- Convert all sqlx::query\! macros to use wrapper functions
- Handle type conversions between databases (i64 vs i32)
- Add feature-gated implementations for database-specific SQL syntax
- Update Makefile with clippy targets for both database features
- Document database support in README

* feat(nym-node-status-agent): add multi-API support with random selection

Agents can now connect to multiple APIs and randomly select one for each testrun:
- Accept multiple --server arguments in format "address:port:auth_key"
- Randomly shuffle server list before attempting connections
- Try each server until a testrun is obtained
- Submit results back only to the API that provided the testrun
- Continue to next server if one is down or has no testruns available

* feat(nym-node-status): implement primary/secondary server architecture

- Agent now requests testruns only from primary server (first in list)
- Results are submitted to all configured servers in parallel
- Secondary servers accept external testruns via new v2 endpoint
- Added auto-creation of gateway and testrun records on secondary servers
- New database queries: get_or_create_gateway, insert_external_testrun
- Client library enhanced with submit_results_with_context method

* Bump Node status API version

* Fix build workdir

* Bump to 3.1.4

* Fix types and queries

* 3.1.6

* Fix gateway perf, bump 3.1.7

* NodeId -> i32, 3.1.8

* Bump agent version

* i64 -> i32

* Use image yq

* Migration and more types

* Update remaining JSONB columns

* Simplify server config

* Update build path

* Change delimiter

* bump agent

* Split up pg and sqlite builds

* More typing fixes, build-and-push script

* Fix Dockerfile-pg

* Bump node-status-api

* TYping

* Agent build script

* More logging around testruns

* Fail loudly on read errors

* Cleanup

* Debug get gateways query

* Fix get_gateways query

* Use pg cert, 3.1.16

* Submit regular results to primary server

* Bump freshenss cutoff

* Update Cargo.lock

* fix: resolve rebase conflicts and compilation errors

After rebasing onto develop, fixed several issues:
- Fixed borrowed data escapes error by using sqlx::query directly in transaction functions
- Removed unused imports and cleaned up code
- Maintained database-specific implementations for transaction functions

* fmt

* Make PG default to make lives easier

* Performance improvements for Explorer v2

* Fix sqlite build

* Fix PG migration

* Tests round 1

* DB tests

* More tests

* And some more tests

* And some more, more tests

* cargo fmt

* Fix some failing lints

* Fix lioness version problems

* Clippy in tests

---------

Co-authored-by: dynco-nym <173912580+dynco-nym@users.noreply.github.com>
2025-07-22 15:25:43 +02:00
import this a9ae2017f5 [DOCs/operators]: Release notes/v2025.13 emmental & NIP-3 announcement (#5908)
* initialise PR, add dev notes and bump node version

* add operators tool and update api stats
2025-07-22 12:10:43 +00:00
Bogdan-Ștefan Neacşu 09ebe7f9e9 Support mnemonic in the NS agent (#5883)
Co-authored-by: benedettadavico <benedetta.davico@gmail.com>
2025-07-22 14:21:12 +03:00
Andrej Mihajlov b72915c224 Merge pull request #5905 from nymtech/am/sqlx-guard-obtain-db-path-from-pool
sqlx-pool-guard: obtain filename from connect options
2025-07-22 11:57:55 +02:00
Andrej Mihajlov add3e864e3 sqlx-pool-guard: obtain filename from connect options 2025-07-22 11:09:39 +02:00
benedettadavico 578c9b0567 update changelog 2025-07-22 11:09:35 +02:00
Andrej Mihajlov 8f6f696f36 Merge pull request #5896 from nymtech/am/handle-table-allocate-more-memory 2025-07-22 11:09:11 +02:00
Jędrzej Stuczyński e9165763b6 Feature/dkg snapshot epoch (#5900)
* define storage item for holding historical DKG state

* make all epoch storage operations go through proxy functions

* make each saving action also apply to the historical item

* removed usage of update_epoch function

* test correct save heights

* exposed query for the epoch state at specified height

* regenerated contract schema

* restored default cw-plus behaviour as in hindsight it makes more sense
2025-07-21 17:32:57 +01:00
mfahampshire 6c1149708b GW Probe docs: Go dep. + new required mnemonic (#5897)
* add note on go dep

* updated -h and useage doc
2025-07-18 12:36:30 +00:00
Mark Sinclair aaf6931d78 nym-node-status-ui placeholder (#5902)
Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
2025-07-17 20:04:45 +01:00
Jędrzej Stuczyński 97804f2fe5 Feature/dkg epoch dealers query (#5899)
* feat: add GetEpochDealers and GetEpochDealersAddresses queries to the DKG contract

* extended DkgQueryClient with new queries

* updated contract schema

* unit tests
2025-07-17 12:26:01 +01:00
Jędrzej Stuczyński 802d9b69ca fix: don't allow mixnode running in exit mode (#5898)
* fix: don't allow mixnode running in exit mode

* fixed error message
2025-07-17 10:57:16 +01:00
Andrej Mihajlov 7313857bc8 Allocate more memory to account for a drift in handle table size in between calls 2025-07-16 13:29:45 +02:00
benedettadavico 779174ada5 update wallet changelog 2025-07-15 14:57:49 +02:00
benedettadavico 329ad83fc0 bump versions 2025-07-15 10:04:51 +02:00
280 changed files with 15429 additions and 5648 deletions
@@ -38,10 +38,9 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
run: |
yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
- name: cleanup-gateway-probe-ref
id: cleanup_gateway_probe_ref
+2 -3
View File
@@ -32,10 +32,9 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
run: |
yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
- name: Set GIT_TAG variable
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
+48
View File
@@ -4,6 +4,54 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.13-emmental] (2025-07-22)
- fix: don't allow mixnode running in exit mode ([#5898])
- fix contract build process in Makefile ([#5892])
- bugfix: ignore 'Send' responses when claiming bandwidth ([#5884])
- Update push-node-status-agent.yaml ([#5882])
- listen for shutdown signals during nym-node startup ([#5879])
- feat: forbid running mixnode + entry on the same node ([#5878])
- chore: 1.88 clippy ([#5877])
- Batch SQL writes for packet stats ([#5874])
- fix the broken link ([#5873])
- Set busy_timeout in sqlx ([#5872])
- feat: basic performance contract integration [within Nym API] ([#5871])
- scraper bugfix: ignore precommits from missing validators ([#5867])
- Return true remaining ([#5866])
- Make Mix hops optional for Mixnet Client SURBs ([#5861])
- Check gateway supported versions ([#5860])
- Add build info endpoints ([#5857])
- Clear out screaming logs ([#5856])
- fix removal of qa env ([#5855])
- Use display when printing paths ([#5853])
- feat: initial performance contract ([#5833])
- Security patches for the `dkg` crate ([#5828])
- HTTP Discovery objects & network defaults ([#5814])
[#5898]: https://github.com/nymtech/nym/pull/5898
[#5892]: https://github.com/nymtech/nym/pull/5892
[#5884]: https://github.com/nymtech/nym/pull/5884
[#5882]: https://github.com/nymtech/nym/pull/5882
[#5879]: https://github.com/nymtech/nym/pull/5879
[#5878]: https://github.com/nymtech/nym/pull/5878
[#5877]: https://github.com/nymtech/nym/pull/5877
[#5874]: https://github.com/nymtech/nym/pull/5874
[#5873]: https://github.com/nymtech/nym/pull/5873
[#5872]: https://github.com/nymtech/nym/pull/5872
[#5871]: https://github.com/nymtech/nym/pull/5871
[#5867]: https://github.com/nymtech/nym/pull/5867
[#5866]: https://github.com/nymtech/nym/pull/5866
[#5861]: https://github.com/nymtech/nym/pull/5861
[#5860]: https://github.com/nymtech/nym/pull/5860
[#5857]: https://github.com/nymtech/nym/pull/5857
[#5856]: https://github.com/nymtech/nym/pull/5856
[#5855]: https://github.com/nymtech/nym/pull/5855
[#5853]: https://github.com/nymtech/nym/pull/5853
[#5833]: https://github.com/nymtech/nym/pull/5833
[#5828]: https://github.com/nymtech/nym/pull/5828
[#5814]: https://github.com/nymtech/nym/pull/5814
## [2025.12-dolcelatte] (2025-07-07)
- bugfix: key-rotation + reply SURBs ([#5876])
+686
View File
@@ -0,0 +1,686 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Nym is a privacy platform that uses mixnet technology to protect against metadata surveillance. The platform consists of several key components:
- Mixnet nodes (mixnodes) for packet mixing
- Gateways (entry/exit points for the network)
- Clients for interacting with the network
- Network monitoring tools
- Validators for network consensus
- Various service providers and integrations
## Build Commands
### Rust Components
```bash
# Default build (debug)
cargo build
# Release build
cargo build --release
# Build a specific package
cargo build -p <package-name>
# Build main components
make build
# Build release versions of main binaries and contracts
make build-release
# Build specific binaries
make build-nym-cli
cargo build -p nym-node --release
cargo build -p nym-api --release
```
### Testing
```bash
# Run clippy, unit tests, and formatting
make test
# Run all tests including slow tests
make test-all
# Run clippy on all workspaces
make clippy
# Run unit tests for a specific package
cargo test -p <package-name>
# Run only expensive/ignored tests
cargo test --workspace -- --ignored
# Run API tests
dotenv -f envs/sandbox.env -- cargo test --test public-api-tests
# Run tests with specific log level
RUST_LOG=debug cargo test -p <package-name>
# Run specific test scripts
./nym-node/tests/test_apis.sh
./scripts/wireguard-exit-policy/exit-policy-tests.sh
```
### Linting and Formatting
```bash
# Run rustfmt on all code
make fmt
# Check formatting without modifying
cargo fmt --all -- --check
# Run clippy with all targets
cargo clippy --workspace --all-targets -- -D warnings
# TypeScript linting
yarn lint
yarn lint:fix
yarn types:lint:fix
# Check dependencies for security/licensing issues
cargo deny check
```
### WASM Components
```bash
# Build all WASM components
make sdk-wasm-build
# Build TypeScript SDK
yarn build:sdk
npx lerna run --scope @nymproject/sdk build --stream
# Build and test WASM components
make sdk-wasm
# Build specific WASM packages
cd wasm/client && make
cd wasm/mix-fetch && make
cd wasm/node-tester && make
```
### Contract Development
```bash
# Build all contracts
make contracts
# Build contracts in release mode
make build-release-contracts
# Generate contract schemas
make contract-schema
# Run wasm-opt on contracts
make wasm-opt-contracts
# Check contracts with cosmwasm-check
make cosmwasm-check-contracts
```
### Running Components
```bash
# Run nym-node as a mixnode
cargo run -p nym-node -- run --mode mixnode
# Run nym-node as a gateway
cargo run -p nym-node -- run --mode gateway
# Run the network monitor
cargo run -p nym-network-monitor
# Run the API server
cargo run -p nym-api
# Run with specific environment
dotenv -f envs/sandbox.env -- cargo run -p nym-api
# Start a local network
./scripts/localnet_start.sh
```
## Architecture
The Nym platform consists of various components organized as a monorepo:
1. **Core Mixnet Infrastructure**:
- `nym-node`: Core binary supporting mixnode and gateway modes
- `common/nymsphinx`: Implementation of the Sphinx packet format
- `common/topology`: Network topology management
- `common/types`: Shared data types across components
2. **Network Monitoring**:
- `nym-network-monitor`: Monitors the network's reliability and performance
- `nym-api`: API server for network stats and monitoring data
- Metrics tracking for nodes, routes, and overall network health
3. **Client Implementations**:
- `clients/native`: Native Rust client implementation
- `clients/socks5`: SOCKS5 proxy client for standard applications
- `wasm`: WebAssembly client implementations (for browsers)
- `nym-connect`: Desktop and mobile clients
4. **Blockchain & Smart Contracts**:
- `common/cosmwasm-smart-contracts`: Smart contract implementations
- `contracts`: CosmWasm contracts for the Nym network
- `common/ledger`: Blockchain integration
5. **Utilities & Tools**:
- `tools`: Various CLI tools and utilities
- `sdk`: SDKs for different languages and platforms
- `documentation`: Documentation generation and management
## Packet System
Nym uses a modified Sphinx packet format for its mixnet:
1. **Message Chunking**:
- Messages are divided into "sets" and "fragments"
- Each fragment fits in a single Sphinx packet
- The `common/nymsphinx/chunking` module handles message fragmentation
2. **Routing**:
- Packets traverse through 3 layers of mixnodes
- Routing information is encrypted in layers (onion routing)
- The final gateway receives and processes the messages
3. **Monitoring**:
- Monitoring system tracks packet delivery through the network
- Routes are analyzed for reliability statistics
- Node performance metrics are collected
## Network Protocol
Nym implements the Loopix mixnet design with several key privacy features:
1. **Continuous-time Mixing**:
- Each mixnode delays messages independently with an exponential distribution
- This creates random reordering of packets, destroying timing correlations
- Offers better anonymity properties than batch mixing approaches
2. **Cover Traffic**:
- Clients and nodes generate dummy "loop" packets that circulate through the network
- These packets are indistinguishable from real traffic
- Creates a baseline level of traffic that hides actual communication patterns
- Provides unobservability (hiding when and how much real traffic is being sent)
3. **Stratified Network Architecture**:
- Traffic flows through Entry Gateway → 3 Mixnode Layers → Exit Gateway
- Path selection is independent per-message (unlike Tor)
- Each node connects only to adjacent layers
4. **Anonymous Replies**:
- Single-Use Reply Blocks (SURBs) allow receiving messages without revealing identity
- Enables bidirectional communication while maintaining privacy
## Network Monitoring Architecture
The network monitoring system is a core component that measures mixnet reliability:
1. The `nym-network-monitor` sends test packets through the network
2. These packets follow predefined routes through multiple mixnodes
3. Metrics are collected about:
- Successful and failed packet deliveries
- Node reliability (percentage of successful packet handling)
- Route reliability (which specific route combinations work best)
4. Results are stored in the database and used by `nym-api` to:
- Present node performance statistics
- Determine network rewards
- Provide route selection guidance to clients
In the current branch, metrics collection is being enhanced with a fanout approach to submit to multiple API endpoints.
## Development Environment
### Required Dependencies
- Rust toolchain (stable, 1.80+)
- Node.js (v20+) and yarn for TypeScript components
- SQLite for local database development
- PostgreSQL for API database (optional, for full API functionality)
- CosmWasm tools for contract development
- For building contracts: `wasm-opt` tool from `binaryen`
- Python 3.8+ for some scripts
- Docker (optional, for containerized development)
- protoc (Protocol Buffers compiler) for some components
### Environment Configurations
The `envs/` directory contains pre-configured environments:
#### Available Environments
- **`local.env`**: Local development environment
- Points to local services (localhost)
- Uses test mnemonics and keys
- Ideal for testing without external dependencies
- **`sandbox.env`**: Sandbox test network
- Public test network with real nodes
- Test tokens available from faucet
- Contract addresses for sandbox deployment
- API: https://sandbox-nym-api1.nymtech.net
- **`mainnet.env`**: Production mainnet
- Real network with real tokens
- Production contract addresses
- API: https://validator.nymtech.net
- Use with caution!
- **`canary.env`**: Canary deployment
- Pre-release testing environment
- Tests new features before mainnet
- **`mainnet-local-api.env`**: Hybrid environment
- Uses mainnet contracts but local API
- Useful for API development against mainnet data
#### Key Environment Variables
```bash
# Network configuration
NETWORK_NAME=sandbox # Network identifier
BECH32_PREFIX=n # Address prefix (n for sandbox, n for mainnet)
NYM_API=https://sandbox-nym-api1.nymtech.net/api
NYXD=https://rpc.sandbox.nymtech.net
NYM_API_NETWORK=sandbox
# Contract addresses (network-specific)
MIXNET_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav
VESTING_CONTRACT_ADDRESS=n1unyuj8qnmygvzuex3dwmg9yzt9alhvyeat0uu0jedg2wj33efl5qackslz
# ... other contract addresses
# Mnemonic for testing (NEVER use in production)
MNEMONIC="clutch captain shoe salt awake harvest setup primary inmate ugly among become"
# API Keys and tokens
IPINFO_API_TOKEN=your_token_here
AUTHENTICATOR_PASSWORD=password_here
# Logging
RUST_LOG=info # Options: error, warn, info, debug, trace
RUST_BACKTRACE=1 # Enable backtraces
# Database
DATABASE_URL=postgresql://user:pass@localhost/nym_api
```
#### Using Environment Files
```bash
# Load environment and run command
dotenv -f envs/sandbox.env -- cargo run -p nym-api
# Export to shell
source envs/sandbox.env
# Use with make targets
dotenv -f envs/sandbox.env -- make run-api-tests
```
## Initial Setup
### First Time Setup
1. **Install Prerequisites**
```bash
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install Node.js and yarn
# Via nvm (recommended):
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install 20
npm install -g yarn
# Install build tools
# Ubuntu/Debian:
sudo apt-get install build-essential pkg-config libssl-dev protobuf-compiler libpq-dev
# macOS:
brew install protobuf postgresql
# Install wasm-opt for contract builds
npm install -g wasm-opt
# Add wasm target for Rust
rustup target add wasm32-unknown-unknown
```
2. **Clone and Setup Repository**
```bash
git clone https://github.com/nymtech/nym.git
cd nym/nym
# Install JavaScript dependencies
yarn install
# Build the project
make build
```
3. **Database Setup (Optional, for API development)**
```bash
# Install PostgreSQL
# Create database
createdb nym_api
# Run migrations (from nym-api directory)
cd nym-api
sqlx migrate run
```
### Quick Start
```bash
# Run a mixnode locally
dotenv -f envs/sandbox.env -- cargo run -p nym-node -- run --mode mixnode --id my-mixnode
# Run a gateway locally
dotenv -f envs/sandbox.env -- cargo run -p nym-node -- run --mode gateway --id my-gateway
# Run the API server
dotenv -f envs/sandbox.env -- cargo run -p nym-api
# Run a client
cargo run -p nym-client -- init --id my-client
cargo run -p nym-client -- run --id my-client
```
## CI/CD Pipeline
The project uses GitHub Actions for CI/CD with several key workflows:
1. **Build and Test**:
- `ci-build.yml`: Main build workflow for Rust components
- Tests are run on multiple platforms (Linux, Windows, macOS)
- Includes formatting check (rustfmt) and linting (clippy)
2. **Release Process**:
- Binary artifacts are published on release tags
- Multiple platform builds are created
3. **Documentation**:
- Documentation is automatically built and deployed
## Database Structure
The system uses SQLite databases with tables like:
- `mixnode_status`: Status information about mixnodes
- `gateway_status`: Status information about gateways
- `routes`: Route performance information (success/failure of specific paths)
- `monitor_run`: Information about monitoring test runs
## Development Workflows
### Running a Node
To run the mixnode or gateway:
```bash
# Run nym-node as a mixnode with specified identity
cargo run -p nym-node -- run --mode mixnode --id my-mixnode
# Run nym-node as a gateway
cargo run -p nym-node -- run --mode gateway --id my-gateway
```
### Configuration
Nodes can be configured with files in various locations:
- Command-line arguments
- Environment variables
- `.env` files specified with `--config-env-file`
### Monitoring
To monitor the health of your node:
- View logs for real-time information
- Use the node's HTTP API for status information
- Check the explorer for public node statistics
## Common Libraries
- `common/types`: Shared data types across all components
- `common/crypto`: Cryptographic primitives and wrappers
- `common/client-core`: Core client functionality
- `common/gateway-client`: Client-gateway communication
- `common/task`: Task management and concurrency utilities
- `common/nymsphinx`: Sphinx packet implementation for mixnet
- `common/topology`: Network topology management
- `common/credentials`: Credential system for privacy-preserving authentication
- `common/bandwidth-controller`: Bandwidth management and accounting
## Code Conventions
- Error handling: Use anyhow/thiserror for structured error handling
- Logging: Use the tracing framework for logging and diagnostics
- State management: Generally use Tokio/futures for async code
- Configuration: Use the config crate and env vars with defaults
- Database: Use sqlx for type-safe database queries
- Follow clippy recommendations and rustfmt formatting
- Use semantic commit messages: feat, fix, docs, refactor, test, chore
## When Making Changes
- Run `make test` before submitting PRs
- Follow Rust naming conventions
- Use `clippy` to check for common issues
- Update SQLx query caches when modifying DB queries: `cargo sqlx prepare`
- Consider backward compatibility for protocol changes
- Use lefthook pre-commit hooks for TypeScript formatting
- Run `cargo deny check` to verify dependency compliance
- Test against both sandbox and local environments when possible
- Update relevant documentation and CHANGELOG.md
## Development Tools
### Useful Cargo Commands
```bash
# Check for outdated dependencies
cargo outdated
# Analyze binary size
cargo bloat --release -p nym-node
# Generate dependency graph
cargo tree -p nym-api
# Run with instrumentation
cargo run --features profiling -p nym-node
# Check for security advisories
cargo audit
```
### Database Tools
```bash
# SQLx CLI for migrations
cargo install sqlx-cli
# Create new migration
cd nym-api && sqlx migrate add <migration_name>
# Prepare query metadata for offline compilation
cargo sqlx prepare --workspace
# View database schema
./nym-api/enter_db.sh
```
### Development Scripts
- `scripts/build_topology.py`: Generate network topology files
- `scripts/node_api_check.py`: Verify node API endpoints
- `scripts/network_tunnel_manager.sh`: Manage network tunnels
- `scripts/localnet_start.sh`: Start a local test network
- Various deployment scripts in `deployment/` for different environments
## Debugging
- Enable more verbose logging with the RUST_LOG environment variable:
```
RUST_LOG=debug,nym_node=trace cargo run -p nym-node -- run --mode mixnode
```
- Use the HTTP API endpoints for status information
- Check monitoring data in the database for network performance metrics
- For complex issues, use tracing tools to follow packet flow
- Enable backtraces: `RUST_BACKTRACE=full`
- For WASM debugging: Use browser developer tools with source maps
## Deployment and Advanced Configurations
### Deployment Structure
The `deployment/` directory contains Ansible playbooks and configurations for various deployment scenarios:
- **`aws/`**: AWS-specific deployment configurations
- **`mixnode/`**: Mixnode deployment playbooks
- **`gateway/`**: Gateway deployment playbooks
- **`validator/`**: Validator node deployment
- **`sandbox-v2/`**: Complete sandbox environment setup
- **`big-dipper-2/`**: Block explorer deployment
### Sandbox V2 Deployment
The sandbox-v2 deployment (`deployment/sandbox-v2/`) provides a complete test environment:
```bash
# Key playbooks:
- deploy.yaml # Main deployment orchestrator
- deploy-mixnodes.yaml # Deploy mixnodes
- deploy-gateways.yaml # Deploy gateways
- deploy-validators.yaml # Deploy validator nodes
- deploy-nym-api.yaml # Deploy API services
```
### Custom Environment Setup
To create a custom environment:
1. Copy an existing env file: `cp envs/sandbox.env envs/custom.env`
2. Modify the network endpoints and contract addresses
3. Update the `NETWORK_NAME` to your identifier
4. Set appropriate mnemonics and keys (use fresh ones for production!)
### Contract Addresses
Contract addresses are network-specific and defined in environment files:
- Mixnet contract: Manages mixnode/gateway registry
- Vesting contract: Handles token vesting schedules
- Coconut contracts: Privacy-preserving credentials
- Name service: Human-readable address mapping
- Ecash contract: Electronic cash functionality
### Local Network Setup
For a completely local network:
```bash
# Start local chain
./scripts/localnet_start.sh
# Deploy contracts
cd contracts
make deploy-local
# Start nodes with local config
dotenv -f envs/local.env -- cargo run -p nym-node -- run --mode mixnode
```
## Common Issues and Troubleshooting
### Database Issues
- When modifying database queries, you must update SQLx query caches:
```bash
cargo sqlx prepare
```
- If you see SQLx errors about missing query files, this is likely the cause
- For "database is locked" errors with SQLite, ensure only one process accesses the DB
- For PostgreSQL connection issues, verify DATABASE_URL and that the server is running
### API Connection Issues
- Check the environment variables pointing to the APIs (NYM_API, NYXD)
- Verify network connectivity and API health endpoints
- For authentication issues, check node keys and credentials
- Common endpoints to verify:
- API health: `$NYM_API/health`
- Chain status: `$NYXD/status`
- Contract info: `$NYXD/cosmwasm/wasm/v1/contract/$CONTRACT_ADDRESS`
### Build Problems
- Clean dependencies with `cargo clean` for a fresh build
- Check for compatible Rust version (1.80+ recommended)
- For smart contract builds, ensure wasm-opt is installed: `npm install -g wasm-opt`
- For cross-compilation issues, check target-specific dependencies
- WASM build issues: Ensure wasm32-unknown-unknown target is installed:
```bash
rustup target add wasm32-unknown-unknown
```
- For "cannot find -lpq" errors, install PostgreSQL development files:
```bash
# Ubuntu/Debian
sudo apt-get install libpq-dev
# macOS
brew install postgresql
```
### Environment Issues
- Contract address mismatches: Ensure you're using the correct environment file
- "Account sequence mismatch": The account nonce is out of sync, wait and retry
- Token decimal issues: Sandbox uses different decimal places than mainnet
- API version mismatches: Ensure your local API version matches the network
- "Insufficient funds": Get test tokens from faucet (sandbox) or check balance
- Gateway/mixnode bonding issues: Verify minimum stake requirements
## Working with Routes and Monitoring
1. Route monitoring metrics are stored in a `routes` table with:
- Layer node IDs (layer1, layer2, layer3, gw)
- Success flag (boolean)
- Timestamp
2. To analyze routes:
- Check `NetworkAccount` and `AccountingRoute` in `nym-network-monitor/src/accounting.rs`
- View monitoring logic in `common/nymsphinx/chunking/monitoring.rs`
- Observe how routes are submitted to the database in the `submit_accounting_routes_to_db` function
## Performance Optimization
### Profiling and Benchmarking
```bash
# Run benchmarks
cargo bench -p nym-node
# Profile with perf (Linux)
cargo build --release --features profiling
perf record --call-graph=dwarf ./target/release/nym-node run --mode mixnode
perf report
# Generate flamegraph
cargo install flamegraph
cargo flamegraph --bin nym-node -- run --mode mixnode
```
### Common Performance Considerations
- Use bounded channels for backpressure
- Batch database operations where possible
- Monitor memory usage with `RUST_LOG=nym_node::metrics=debug`
- Use connection pooling for database connections
- Consider using `jemalloc` for better memory allocation performance
Generated
+1251 -1009
View File
File diff suppressed because it is too large Load Diff
+11 -4
View File
@@ -99,7 +99,7 @@ members = [
"common/wasm/storage",
"common/wasm/utils",
"common/wireguard",
"common/wireguard-types",
"common/wireguard-types", "common/zulip-client",
"documentation/autodoc",
"gateway",
"nym-api",
@@ -234,6 +234,7 @@ digest = "0.10.7"
dirs = "5.0"
doc-comment = "0.3"
dotenvy = "0.15.6"
dyn-clone = "1.0.19"
ecdsa = "0.16"
ed25519-dalek = "2.1"
encoding_rs = "0.8.35"
@@ -280,8 +281,12 @@ nix = "0.27.1"
notify = "5.1.0"
okapi = "0.7.0"
once_cell = "1.21.3"
opentelemetry = "0.19.0"
opentelemetry-jaeger = "0.18.0"
opentelemetry = "0.30.0"
opentelemetry-otlp = "0.30.0"
opentelemetry-semantic-conventions = "0.30.0"
opentelemetry_sdk = "0.30.0"
opentelemetry-stdout = "0.30.0"
opentelemetry-jaeger = "0.22.0"
parking_lot = "0.12.3"
pem = "0.8"
petgraph = "0.6.5"
@@ -337,8 +342,10 @@ toml = "0.8.22"
tower = "0.5.2"
tower-http = "0.5.2"
tracing = "0.1.41"
tracing-core = "0.1.33"
tracing-log = "0.2"
tracing-opentelemetry = "0.19.0"
tracing-opentelemetry = "0.31.0"
tracing-serde = "0.2.0"
tracing-subscriber = "0.3.19"
tracing-tree = "0.2.2"
tracing-indicatif = "0.3.9"
+11
View File
@@ -0,0 +1,11 @@
find . -name "Cargo.toml" -not -path "./target/*" -exec grep -l "^name = " {} \; | xargs grep "^name = " | grep -v 'name = "nym-'
find . -name "Cargo.toml" \
-not -path "./nym-wallet/*" \
-not -path "./contracts/*" \
-not -path "./target/*" \
-not -name "./Cargo.toml" \
-exec sed -i 's/^version = ".*"/version.workspace = true/' {} \;
find . -name "Cargo.toml" -not -path "./target/*" -exec awk '/^\[package\]/{flag=1; next} flag && /^name = /{print FILENAME ":" $0; flag=0} /^\[/{flag=0}' {} \; | grep -v 'name = "nym-'
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.58"
version = "1.1.59"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.58"
version = "1.1.59"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
@@ -28,8 +28,6 @@ pub type HmacSha256 = Hmac<Sha256>;
pub type Nonce = u64;
pub type Taken = Option<SystemTime>;
pub const BANDWIDTH_CAP_PER_DAY: u64 = 250 * 1024 * 1024 * 1024; // 250 GB
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IpPair {
pub ipv4: Ipv4Addr,
+2 -2
View File
@@ -21,10 +21,10 @@ serde_json = { workspace = true, optional = true }
tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true }
tracing-tree = { workspace = true, optional = true }
tracing = { workspace = true, optional = true }
opentelemetry-jaeger = { workspace = true, features = ["rt-tokio", "collector_client", "isahc_collector_client"], optional = true }
opentelemetry-jaeger = { workspace = true, features = ["tokio", "collector_client", "isahc_collector_client"], optional = true }
tracing-opentelemetry = { workspace = true, optional = true }
utoipa = { workspace = true, optional = true }
opentelemetry = { workspace = true, features = ["rt-tokio"], optional = true }
opentelemetry = { workspace = true, optional = true }
[build-dependencies]
@@ -65,7 +65,7 @@ pub trait MixnetClientStorage {
fn gateway_details_store(&self) -> &Self::GatewaysDetailsStore;
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Ephemeral {
key_store: InMemEphemeralKeys,
reply_store: reply_storage::Empty,
@@ -22,7 +22,7 @@ mod test;
// use the old key after new one was issued.
// Remember that Arc<T> has Deref implementation for T
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct ClientKeys {
/// identity key associated with the client instance.
identity_keypair: Arc<ed25519::KeyPair>,
@@ -196,7 +196,7 @@ impl KeyStore for OnDiskKeys {
}
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct InMemEphemeralKeys {
keys: Arc<Mutex<ClientKeys>>,
}
@@ -37,7 +37,7 @@ impl StorageManager {
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.synchronous(SqliteSynchronous::Normal)
.auto_vacuum(SqliteAutoVacuum::Incremental)
.filename(&database_path)
.filename(database_path)
.create_if_missing(fresh)
.disable_statement_logging();
@@ -49,8 +49,7 @@ impl StorageManager {
}
};
let connection_pool =
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
let connection_pool = SqlitePoolGuard::new(connection_pool);
if let Err(err) = sqlx::migrate!("./fs_surbs_migrations")
.run(&*connection_pool)
@@ -41,6 +41,11 @@ pub trait DkgQueryClient {
self.query_dkg_contract(request).await
}
async fn get_epoch_at_height(&self, height: u64) -> Result<Option<Epoch>, NyxdError> {
let request = DkgQueryMsg::GetEpochStateAtHeight { height };
self.query_dkg_contract(request).await
}
async fn can_advance_state(&self) -> Result<StateAdvanceResponse, NyxdError> {
let request = DkgQueryMsg::CanAdvanceState {};
self.query_dkg_contract(request).await
@@ -87,6 +92,34 @@ pub trait DkgQueryClient {
self.query_dkg_contract(request).await
}
async fn get_epoch_dealers_paged(
&self,
epoch_id: EpochId,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<PagedDealerResponse, NyxdError> {
let request = DkgQueryMsg::GetEpochDealers {
epoch_id,
start_after,
limit,
};
self.query_dkg_contract(request).await
}
async fn get_epoch_dealers_addresses_paged(
&self,
epoch_id: EpochId,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<PagedDealerResponse, NyxdError> {
let request = DkgQueryMsg::GetEpochDealersAddresses {
epoch_id,
start_after,
limit,
};
self.query_dkg_contract(request).await
}
async fn get_dealer_indices_paged(
&self,
start_after: Option<String>,
@@ -208,6 +241,20 @@ pub trait PagedDkgQueryClient: DkgQueryClient {
collect_paged!(self, get_current_dealers_paged, dealers)
}
async fn get_all_epoch_dealers(
&self,
epoch_id: EpochId,
) -> Result<Vec<DealerDetails>, NyxdError> {
collect_paged!(self, get_epoch_dealers_paged, dealers, epoch_id)
}
async fn get_all_epoch_dealers_addresses(
&self,
epoch_id: EpochId,
) -> Result<Vec<DealerDetails>, NyxdError> {
collect_paged!(self, get_epoch_dealers_addresses_paged, dealers, epoch_id)
}
async fn get_all_dealer_indices(&self) -> Result<Vec<(Addr, NodeIndex)>, NyxdError> {
collect_paged!(self, get_dealer_indices_paged, indices)
}
@@ -257,6 +304,9 @@ mod tests {
match msg {
DkgQueryMsg::GetState {} => client.get_state().ignore(),
DkgQueryMsg::GetCurrentEpochState {} => client.get_current_epoch().ignore(),
DkgQueryMsg::GetEpochStateAtHeight { height } => {
client.get_epoch_at_height(height).ignore()
}
DkgQueryMsg::CanAdvanceState {} => client.can_advance_state().ignore(),
DkgQueryMsg::GetCurrentEpochThreshold {} => {
client.get_current_epoch_threshold().ignore()
@@ -276,6 +326,20 @@ mod tests {
DkgQueryMsg::GetCurrentDealers { limit, start_after } => client
.get_current_dealers_paged(start_after, limit)
.ignore(),
QueryMsg::GetEpochDealers {
epoch_id,
limit,
start_after,
} => client
.get_epoch_dealers_paged(epoch_id, start_after, limit)
.ignore(),
QueryMsg::GetEpochDealersAddresses {
epoch_id,
limit,
start_after,
} => client
.get_epoch_dealers_addresses_paged(epoch_id, start_after, limit)
.ignore(),
DkgQueryMsg::GetDealerIndices { limit, start_after } => {
client.get_dealer_indices_paged(start_after, limit).ignore()
}
@@ -55,6 +55,14 @@ impl DealerDetailsResponse {
}
}
#[cw_serde]
pub struct PagedDealerAddressesResponse {
pub dealers: Vec<Addr>,
/// Field indicating paging information for the following queries if the caller wishes to get further entries.
pub start_next_after: Option<Addr>,
}
#[cw_serde]
pub struct PagedDealerResponse {
pub dealers: Vec<DealerDetails>,
@@ -12,8 +12,8 @@ use cosmwasm_schema::cw_serde;
#[cfg(feature = "schema")]
use crate::{
dealer::{
DealerDetailsResponse, PagedDealerIndexResponse, PagedDealerResponse,
RegisteredDealerDetails,
DealerDetailsResponse, PagedDealerAddressesResponse, PagedDealerIndexResponse,
PagedDealerResponse, RegisteredDealerDetails,
},
dealing::{
DealerDealingsStatusResponse, DealingChunkResponse, DealingChunkStatusResponse,
@@ -84,6 +84,9 @@ pub enum QueryMsg {
#[cfg_attr(feature = "schema", returns(Epoch))]
GetCurrentEpochState {},
#[cfg_attr(feature = "schema", returns(Option<Epoch>))]
GetEpochStateAtHeight { height: u64 },
#[cfg_attr(feature = "schema", returns(u64))]
GetCurrentEpochThreshold {},
@@ -102,6 +105,20 @@ pub enum QueryMsg {
#[cfg_attr(feature = "schema", returns(DealerDetailsResponse))]
GetDealerDetails { dealer_address: String },
#[cfg_attr(feature = "schema", returns(PagedDealerAddressesResponse))]
GetEpochDealersAddresses {
epoch_id: EpochId,
limit: Option<u32>,
start_after: Option<String>,
},
#[cfg_attr(feature = "schema", returns(PagedDealerResponse))]
GetEpochDealers {
epoch_id: EpochId,
limit: Option<u32>,
start_after: Option<String>,
},
#[cfg_attr(feature = "schema", returns(PagedDealerResponse))]
GetCurrentDealers {
limit: Option<u32>,
@@ -63,7 +63,7 @@ impl PersistentStorage {
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.synchronous(SqliteSynchronous::Normal)
.auto_vacuum(SqliteAutoVacuum::Incremental)
.filename(&database_path)
.filename(database_path)
.create_if_missing(true)
.disable_statement_logging();
@@ -75,8 +75,7 @@ impl PersistentStorage {
}
};
let connection_pool =
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
let connection_pool = SqlitePoolGuard::new(connection_pool);
if let Err(err) = sqlx::migrate!("./migrations").run(&*connection_pool).await {
error!("Failed to perform migration on the SQLx database: {err}");
@@ -11,9 +11,11 @@ rust-version.workspace = true
readme.workspace = true
[dependencies]
async-trait = { workspace = true }
bs58 = { workspace = true }
cosmwasm-std = { workspace = true }
cw-utils = { workspace = true }
dyn-clone = { workspace = true }
futures = { workspace = true }
rand = { workspace = true }
si-scale = { workspace = true }
@@ -7,25 +7,36 @@ use crate::ClientBandwidth;
use nym_credentials::ecash::utils::ecash_today;
use nym_credentials_interface::Bandwidth;
use nym_gateway_requests::ServerResponse;
use nym_gateway_storage::GatewayStorage;
use nym_gateway_storage::traits::BandwidthGatewayStorage;
use si_scale::helpers::bibytes2;
use time::OffsetDateTime;
use tracing::*;
const FREE_TESTNET_BANDWIDTH_VALUE: Bandwidth = Bandwidth::new_unchecked(64 * 1024 * 1024 * 1024); // 64GB
#[derive(Clone)]
pub struct BandwidthStorageManager {
pub(crate) storage: GatewayStorage,
pub(crate) storage: Box<dyn BandwidthGatewayStorage + Send + Sync>,
pub(crate) client_bandwidth: ClientBandwidth,
pub(crate) client_id: i64,
pub(crate) bandwidth_cfg: BandwidthFlushingBehaviourConfig,
pub(crate) only_coconut_credentials: bool,
}
impl Clone for BandwidthStorageManager {
fn clone(&self) -> Self {
Self {
storage: dyn_clone::clone_box(&*self.storage),
client_bandwidth: self.client_bandwidth.clone(),
client_id: self.client_id,
bandwidth_cfg: self.bandwidth_cfg,
only_coconut_credentials: self.only_coconut_credentials,
}
}
}
impl BandwidthStorageManager {
pub fn new(
storage: GatewayStorage,
storage: Box<dyn BandwidthGatewayStorage + Send + Sync>,
client_bandwidth: ClientBandwidth,
client_id: i64,
bandwidth_cfg: BandwidthFlushingBehaviourConfig,
+123 -35
View File
@@ -2,12 +2,14 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::Error;
use async_trait::async_trait;
use credential_sender::CredentialHandler;
use credential_sender::CredentialHandlerConfig;
use error::EcashTicketError;
use futures::channel::mpsc::{self, UnboundedSender};
use nym_credentials::CredentialSpendingData;
use nym_credentials_interface::{ClientTicket, CompactEcashError, NymPayInfo, VerificationKeyAuth};
use nym_gateway_storage::traits::BandwidthGatewayStorage;
use nym_gateway_storage::GatewayStorage;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::DirectSigningHttpRpcNyxdClient;
@@ -20,6 +22,7 @@ pub mod credential_sender;
pub mod error;
mod helpers;
mod state;
pub mod traits;
pub const TIME_RANGE_SEC: i64 = 30;
@@ -31,44 +34,21 @@ pub struct EcashManager {
cred_sender: UnboundedSender<ClientTicket>,
}
impl EcashManager {
pub async fn new(
credential_handler_cfg: CredentialHandlerConfig,
nyxd_client: DirectSigningHttpRpcNyxdClient,
pk_bytes: [u8; 32],
shutdown: nym_task::TaskClient,
storage: GatewayStorage,
) -> Result<Self, Error> {
let shared_state = SharedState::new(nyxd_client, storage).await?;
let (cred_sender, cred_receiver) = mpsc::unbounded();
let cs =
CredentialHandler::new(credential_handler_cfg, cred_receiver, shared_state.clone())
.await?;
cs.start(shutdown);
Ok(EcashManager {
shared_state,
pk_bytes,
pay_infos: Default::default(),
cred_sender,
})
}
pub async fn verification_key(
#[async_trait]
impl traits::EcashManager for EcashManager {
async fn verification_key(
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError> {
self.shared_state.verification_key(epoch_id).await
}
pub fn storage(&self) -> &GatewayStorage {
&self.shared_state.storage
fn storage(&self) -> Box<dyn BandwidthGatewayStorage + Send + Sync> {
dyn_clone::clone_box(&*self.shared_state.storage)
}
//Check for duplicate pay_info, then check the payment, then insert pay_info if everything succeeded
pub async fn check_payment(
async fn check_payment(
&self,
credential: &CredentialSpendingData,
aggregated_verification_key: &VerificationKeyAuth,
@@ -88,6 +68,40 @@ impl EcashManager {
.await
}
fn async_verify(&self, ticket: ClientTicket) {
// TODO: I guess do something for shutdowns
let _ = self
.cred_sender
.unbounded_send(ticket)
.inspect_err(|_| error!("failed to send the client ticket for verification task"));
}
}
impl EcashManager {
pub async fn new(
credential_handler_cfg: CredentialHandlerConfig,
nyxd_client: DirectSigningHttpRpcNyxdClient,
pk_bytes: [u8; 32],
shutdown: nym_task::TaskClient,
storage: GatewayStorage,
) -> Result<Self, Error> {
let shared_state = SharedState::new(nyxd_client, Box::new(storage)).await?;
let (cred_sender, cred_receiver) = mpsc::unbounded();
let cs =
CredentialHandler::new(credential_handler_cfg, cred_receiver, shared_state.clone())
.await?;
cs.start(shutdown);
Ok(EcashManager {
shared_state,
pk_bytes,
pay_infos: Default::default(),
cred_sender,
})
}
pub async fn verify_pay_info(&self, pay_info: NymPayInfo) -> Result<usize, EcashTicketError> {
//Public key check
if pay_info.pk() != self.pk_bytes {
@@ -152,12 +166,86 @@ impl EcashManager {
inner.insert(index, pay_info);
Ok(())
}
}
pub fn async_verify(&self, ticket: ClientTicket) {
// TODO: I guess do something for shutdowns
let _ = self
.cred_sender
.unbounded_send(ticket)
.inspect_err(|_| error!("failed to send the client ticket for verification task"));
pub struct MockEcashManager {
verfication_key: tokio::sync::RwLock<VerificationKeyAuth>,
storage: Box<dyn BandwidthGatewayStorage + Send + Sync>,
}
impl MockEcashManager {
pub fn new(storage: Box<dyn BandwidthGatewayStorage + Send + Sync>) -> Self {
Self {
verfication_key: tokio::sync::RwLock::new(
VerificationKeyAuth::from_bytes(&[
129, 187, 76, 12, 1, 51, 46, 26, 132, 205, 148, 109, 140, 131, 50, 119, 45,
128, 51, 218, 106, 70, 181, 74, 244, 38, 162, 62, 42, 12, 5, 100, 7, 136, 32,
155, 18, 219, 195, 182, 3, 56, 168, 16, 93, 154, 249, 230, 16, 202, 90, 134,
246, 25, 98, 6, 175, 215, 188, 239, 71, 84, 66, 1, 43, 66, 197, 180, 216, 80,
55, 185, 140, 216, 14, 48, 244, 214, 20, 68, 106, 41, 48, 252, 188, 181, 231,
170, 23, 211, 215, 12, 91, 147, 47, 7, 4, 0, 0, 0, 0, 0, 0, 0, 174, 31, 237,
215, 159, 183, 71, 125, 90, 147, 84, 78, 49, 216, 66, 232, 92, 206, 41, 230,
239, 209, 211, 166, 131, 190, 148, 36, 225, 194, 146, 6, 120, 34, 194, 5, 154,
155, 234, 41, 191, 119, 227, 51, 91, 128, 151, 240, 129, 208, 253, 171, 234,
170, 71, 139, 251, 78, 49, 35, 218, 16, 77, 150, 177, 204, 83, 210, 67, 147,
66, 162, 58, 25, 96, 168, 61, 180, 92, 21, 18, 78, 194, 98, 176, 123, 122, 176,
81, 150, 187, 20, 64, 69, 0, 134, 142, 3, 84, 108, 3, 55, 107, 111, 73, 31, 46,
51, 225, 248, 202, 173, 194, 24, 104, 96, 31, 61, 24, 140, 220, 31, 176, 200,
30, 217, 66, 58, 11, 181, 158, 196, 179, 199, 177, 7, 210, 4, 119, 142, 149,
59, 3, 186, 145, 27, 230, 125, 230, 246, 197, 196, 119, 70, 239, 115, 99, 215,
63, 205, 63, 74, 108, 201, 42, 226, 150, 137, 3, 157, 45, 25, 163, 54, 107,
153, 61, 141, 64, 207, 139, 41, 203, 39, 36, 97, 181, 72, 206, 235, 221, 178,
171, 60, 4, 6, 170, 181, 213, 10, 216, 53, 28, 32, 33, 41, 224, 60, 247, 206,
137, 108, 251, 229, 234, 112, 65, 145, 124, 212, 125, 116, 154, 114, 2, 125,
202, 24, 25, 196, 219, 104, 200, 131, 133, 180, 39, 21, 144, 204, 8, 151, 218,
99, 64, 209, 47, 5, 42, 13, 214, 139, 54, 112, 224, 53, 238, 250, 56, 42, 105,
15, 21, 238, 99, 225, 79, 121, 104, 155, 230, 243, 133, 47, 39, 147, 98, 45,
113, 137, 200, 102, 151, 122, 174, 9, 250, 17, 138, 191, 129, 202, 244, 107,
75, 48, 141, 136, 89, 168, 124, 88, 174, 251, 17, 35, 146, 88, 76, 134, 102,
105, 204, 16, 176, 214, 63, 13, 170, 225, 250, 112, 7, 237, 161, 160, 15, 71,
10, 130, 137, 69, 186, 64, 223, 188, 5, 5, 228, 57, 214, 134, 247, 20, 171,
140, 43, 230, 57, 29, 127, 136, 169, 80, 14, 137, 130, 200, 205, 222, 81, 143,
40, 77, 68, 197, 91, 142, 91, 84, 164, 15, 133, 242, 149, 255, 173, 201, 108,
208, 23, 188, 230, 158, 146, 54, 198, 52, 148, 123, 202, 52, 222, 50, 4, 62,
211, 208, 176, 61, 104, 151, 227, 192, 224, 200, 132, 53, 187, 240, 254, 150,
60, 30, 140, 11, 63, 71, 12, 30, 233, 255, 144, 250, 16, 81, 38, 33, 9, 185,
195, 214, 0, 119, 117, 94, 100, 103, 144, 10, 189, 65, 113, 114, 192, 11, 177,
214, 223, 218, 36, 139, 183, 2, 206, 247, 245, 88, 62, 231, 183, 50, 46, 95,
202, 152, 82, 244, 80, 173, 192, 147, 51, 248, 46, 181, 194, 205, 233, 67, 144,
155, 250, 142, 124, 71, 9, 136, 142, 88, 29, 99, 222, 43, 181, 172, 120, 187,
179, 172, 240, 231, 57, 236, 195, 158, 182, 203, 19, 49, 220, 180, 212, 101,
105, 239, 58, 215, 0, 50, 100, 172, 29, 236, 170, 108, 129, 150, 5, 64, 238,
59, 50, 4, 21, 131, 197, 142, 191, 76, 101, 140, 133, 112, 38, 235, 113, 203,
22, 161, 204, 84, 73, 125, 219, 70, 62, 67, 119, 52, 130, 208, 180, 231, 78,
141, 181, 13, 207, 196, 126, 159, 70, 34, 195, 70,
])
.unwrap(),
),
storage: dyn_clone::clone_box(&*storage),
}
}
}
#[async_trait]
impl traits::EcashManager for MockEcashManager {
async fn verification_key(
&self,
_epoch_id: EpochId,
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError> {
Ok(self.verfication_key.read().await)
}
fn storage(&self) -> Box<dyn BandwidthGatewayStorage + Send + Sync> {
dyn_clone::clone_box(&*self.storage)
}
async fn check_payment(
&self,
_credential: &CredentialSpendingData,
_aggregated_verification_key: &VerificationKeyAuth,
) -> Result<(), EcashTicketError> {
Ok(())
}
fn async_verify(&self, _ticket: ClientTicket) {}
}
@@ -6,7 +6,7 @@ use crate::Error;
use cosmwasm_std::{from_json, CosmosMsg, WasmMsg};
use nym_credentials_interface::VerificationKeyAuth;
use nym_ecash_contract_common::msg::ExecuteMsg;
use nym_gateway_storage::GatewayStorage;
use nym_gateway_storage::traits::BandwidthGatewayStorage;
use nym_validator_client::coconut::all_ecash_api_clients;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::{
@@ -22,18 +22,28 @@ use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
use tracing::{error, trace, warn};
// state shared by different subtasks dealing with credentials
#[derive(Clone)]
pub(crate) struct SharedState {
pub(crate) nyxd_client: Arc<RwLock<DirectSigningHttpRpcNyxdClient>>,
pub(crate) address: AccountId,
pub(crate) epoch_data: Arc<RwLock<BTreeMap<EpochId, EpochState>>>,
pub(crate) storage: GatewayStorage,
pub(crate) storage: Box<dyn BandwidthGatewayStorage + Send + Sync>,
}
impl Clone for SharedState {
fn clone(&self) -> Self {
Self {
nyxd_client: self.nyxd_client.clone(),
address: self.address.clone(),
epoch_data: self.epoch_data.clone(),
storage: dyn_clone::clone_box(&*self.storage),
}
}
}
impl SharedState {
pub(crate) async fn new(
nyxd_client: DirectSigningHttpRpcNyxdClient,
storage: GatewayStorage,
storage: Box<dyn BandwidthGatewayStorage + Send + Sync>,
) -> Result<Self, Error> {
let address = nyxd_client.address();
@@ -0,0 +1,23 @@
use async_trait::async_trait;
use nym_credentials::CredentialSpendingData;
use nym_credentials_interface::{ClientTicket, VerificationKeyAuth};
use nym_gateway_storage::traits::BandwidthGatewayStorage;
use nym_validator_client::nym_api::EpochId;
use tokio::sync::RwLockReadGuard;
use crate::ecash::error::EcashTicketError;
#[async_trait]
pub trait EcashManager {
async fn verification_key(
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError>;
fn storage(&self) -> Box<dyn BandwidthGatewayStorage + Send + Sync>;
async fn check_payment(
&self,
credential: &CredentialSpendingData,
aggregated_verification_key: &VerificationKeyAuth,
) -> Result<(), EcashTicketError>;
fn async_verify(&self, ticket: ClientTicket);
}
+3 -3
View File
@@ -1,8 +1,8 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::ecash::traits::EcashManager;
use bandwidth_storage_manager::BandwidthStorageManager;
use ecash::EcashManager;
use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime};
use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType};
use nym_gateway_requests::models::CredentialSpendingRequest;
@@ -20,14 +20,14 @@ pub mod error;
pub struct CredentialVerifier {
credential: CredentialSpendingRequest,
ecash_verifier: Arc<EcashManager>,
ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
bandwidth_storage_manager: BandwidthStorageManager,
}
impl CredentialVerifier {
pub fn new(
credential: CredentialSpendingRequest,
ecash_verifier: Arc<EcashManager>,
ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
bandwidth_storage_manager: BandwidthStorageManager,
) -> Self {
CredentialVerifier {
+7 -1
View File
@@ -53,7 +53,7 @@ pub enum Ed25519RecoveryError {
}
/// Keypair for usage in ed25519 EdDSA.
#[derive(Debug, Zeroize, ZeroizeOnDrop)]
#[derive(Zeroize, ZeroizeOnDrop)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct KeyPair {
private_key: PrivateKey,
@@ -66,6 +66,12 @@ pub struct KeyPair {
index: u32,
}
impl Debug for KeyPair {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&self.public_key.to_base58_string(), f)
}
}
/// All keys will always have an index field populated this is to prevent anyone from figuring out if
/// the keys are derived or random, and alter their behaviour based on that.
impl KeyPair {
@@ -55,6 +55,12 @@ pub struct KeyPair {
pub(crate) public_key: PublicKey,
}
impl Debug for KeyPair {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
Display::fmt(&self.public_key.to_base58_string(), f)
}
}
impl KeyPair {
#[cfg(feature = "rand")]
pub fn new<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
@@ -227,6 +233,12 @@ impl AsRef<[u8]> for PublicKey {
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct PrivateKey(x25519_dalek::StaticSecret);
impl Debug for PrivateKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_base58_string())
}
}
impl Display for PrivateKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_base58_string())
+4 -1
View File
@@ -19,7 +19,6 @@ serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
strum = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true, features = ["log"] }
time = { workspace = true }
subtle = { workspace = true }
zeroize = { workspace = true }
@@ -34,6 +33,9 @@ nym-task = { path = "../task" }
nym-credentials = { path = "../credentials" }
nym-credentials-interface = { path = "../credentials-interface" }
opentelemetry = { workspace = true, features = ["trace"] }
tracing = { workspace = true, features = ["std", "attributes", "tracing-attributes"] }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
workspace = true
features = ["time"]
@@ -48,3 +50,4 @@ default-features = false
[dev-dependencies]
nym-compact-ecash = { path = "../nym_offline_compact_ecash" } # we need specific imports in tests
@@ -20,7 +20,7 @@ use rand::{thread_rng, CryptoRng, RngCore};
use std::any::{type_name, Any};
use std::str::FromStr;
use std::time::Duration;
use tracing::log::*;
use tracing::{error, trace};
use tungstenite::Message as WsMessage;
#[cfg(not(target_arch = "wasm32"))]
@@ -16,6 +16,9 @@ pub struct AuthenticateRequest {
pub content: AuthenticateRequestContent,
pub request_signature: ed25519::Signature,
#[serde(default)]
pub debug_trace_id: Option<String>,
}
impl AuthenticateRequest {
@@ -23,6 +26,7 @@ impl AuthenticateRequest {
protocol_version: u8,
shared_key: &SharedGatewayKey,
identity_keys: &ed25519::KeyPair,
debug_trace_id: Option<String>,
) -> Result<AuthenticateRequest, GatewayRequestsError> {
let content = AuthenticateRequestContent::new(
protocol_version,
@@ -35,6 +39,7 @@ impl AuthenticateRequest {
Ok(AuthenticateRequest {
content,
request_signature,
debug_trace_id,
})
}
@@ -12,8 +12,10 @@ use nym_credentials_interface::CredentialSpendingData;
use nym_crypto::asymmetric::ed25519;
use nym_sphinx::DestinationAddressBytes;
use nym_statistics_common::types::SessionType;
use opentelemetry::trace::TraceContextExt;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use tracing::{instrument, warn};
use tungstenite::Message;
pub mod authenticate;
@@ -76,6 +78,10 @@ pub enum ClientControlRequest {
address: String,
enc_address: String,
iv: String,
/// this is a trace id that is used in testing and performance verification
/// in mainnet, this will always be set to None
#[serde(default)]
debug_trace_id: Option<String>,
},
AuthenticateV2(Box<AuthenticateRequest>),
@@ -127,14 +133,24 @@ impl ClientControlRequest {
let nonce = shared_key.random_nonce_or_iv();
let ciphertext = shared_key.encrypt_naive(address.as_bytes_ref(), Some(&nonce))?;
let otel_context = opentelemetry::Context::current();
warn!("OTEL CONTEXT: {:?}", otel_context);
let span = otel_context.span();
let context = span.span_context();
let trace_id = context.trace_id();
warn!("TRACE_ID: {:?}", trace_id);
// panic!();
Ok(ClientControlRequest::Authenticate {
protocol_version,
address: address.as_base58_string(),
enc_address: bs58::encode(&ciphertext).into_string(),
iv: bs58::encode(&nonce).into_string(),
debug_trace_id: Some(trace_id.to_string()),
})
}
#[instrument]
pub fn new_authenticate_v2(
shared_key: &SharedGatewayKey,
identity_keys: &ed25519::KeyPair,
@@ -142,8 +158,21 @@ impl ClientControlRequest {
// if we're using v2 authentication, we must announce at least that protocol version
let protocol_version = AUTHENTICATE_V2_PROTOCOL_VERSION;
let otel_context = opentelemetry::Context::current();
warn!("OTEL CONTEXT: {:?}", otel_context);
let span = otel_context.span();
let context = span.span_context();
let trace_id = context.trace_id();
warn!("TRACE_ID: {:?}", trace_id);
// panic!();
Ok(ClientControlRequest::AuthenticateV2(Box::new(
AuthenticateRequest::new(protocol_version, shared_key, identity_keys)?,
AuthenticateRequest::new(
protocol_version,
shared_key,
identity_keys,
Some(trace_id.to_string()),
)?,
)))
}
+7
View File
@@ -9,8 +9,10 @@ edition.workspace = true
license.workspace = true
[dependencies]
async-trait = { workspace = true }
bincode = { workspace = true }
defguard_wireguard_rs = { workspace = true }
dyn-clone = { workspace = true }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"sqlite",
@@ -21,6 +23,7 @@ sqlx = { workspace = true, features = [
] }
time = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["sync"], optional = true }
tracing = { workspace = true }
nym-credentials-interface = { path = "../credentials-interface" }
@@ -35,3 +38,7 @@ sqlx = { workspace = true, features = [
"macros",
"migrate",
] }
[features]
default = []
mock = ["tokio"]
@@ -0,0 +1,19 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
DELETE FROM wireguard_peer WHERE client_id IS NULL;
CREATE TABLE wireguard_peer_new
(
public_key TEXT NOT NULL PRIMARY KEY UNIQUE,
allowed_ips BLOB NOT NULL,
client_id INTEGER REFERENCES clients(id) NOT NULL
);
INSERT INTO wireguard_peer_new (public_key, allowed_ips, client_id)
SELECT public_key, allowed_ips, client_id FROM wireguard_peer;
DROP TABLE wireguard_peer;
ALTER TABLE wireguard_peer_new RENAME TO wireguard_peer;
+13
View File
@@ -3,6 +3,8 @@
use std::str::FromStr;
use nym_credentials_interface::TicketType;
use crate::models::Client;
#[derive(Debug, PartialEq, sqlx::Type)]
@@ -15,6 +17,17 @@ pub enum ClientType {
ExitWireguard,
}
impl From<TicketType> for ClientType {
fn from(value: TicketType) -> Self {
match value {
TicketType::V1MixnetEntry => ClientType::EntryMixnet,
TicketType::V1MixnetExit => ClientType::ExitMixnet,
TicketType::V1WireguardEntry => ClientType::EntryWireguard,
TicketType::V1WireguardExit => ClientType::ExitWireguard,
}
}
}
impl FromStr for ClientType {
type Err = &'static str;
+14 -2
View File
@@ -20,6 +20,18 @@ pub enum GatewayStorageError {
#[error("the stored data associated with ticket {ticket_id} is malformed!")]
MalformedStoredTicketData { ticket_id: i64 },
#[error("Failed to convert from type of database: {0}")]
TypeConversion(String),
#[error("Failed to convert from type of database: {field_key}")]
TypeConversion { field_key: &'static str },
#[error("Serialization failure for {field_key}")]
Serialize {
field_key: &'static str,
source: bincode::Error,
},
#[error("Deserialization failure for {field_key}")]
Deserialize {
field_key: &'static str,
source: bincode::Error,
},
}
+77 -74
View File
@@ -1,6 +1,7 @@
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use async_trait::async_trait;
use bandwidth::BandwidthManager;
use clients::{ClientManager, ClientType};
use models::{
@@ -15,10 +16,10 @@ use sqlx::{
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
ConnectOptions,
};
use std::path::Path;
use std::{path::Path, time::Duration};
use tickets::TicketStorageManager;
use time::OffsetDateTime;
use tracing::{debug, error};
use tracing::{debug, error, log::LevelFilter};
pub mod bandwidth;
mod clients;
@@ -27,11 +28,21 @@ mod inboxes;
pub mod models;
mod shared_keys;
mod tickets;
pub mod traits;
mod wireguard_peers;
pub use error::GatewayStorageError;
pub use inboxes::InboxManager;
use crate::traits::{BandwidthGatewayStorage, InboxGatewayStorage, SharedKeyGatewayStorage};
fn make_bincode_serializer() -> impl bincode::Options {
use bincode::Options;
bincode::DefaultOptions::new()
.with_big_endian()
.with_varint_encoding()
}
// note that clone here is fine as upon cloning the same underlying pool will be used
#[derive(Clone)]
pub struct GatewayStorage {
@@ -71,6 +82,21 @@ impl GatewayStorage {
&self.wireguard_peer_manager
}
pub async fn handle_forget_me(
&self,
client_address: DestinationAddressBytes,
) -> Result<(), GatewayStorageError> {
let client_id = self.get_mixnet_client_id(client_address).await?;
self.inbox_manager()
.remove_messages_for_client(&client_address.as_base58_string())
.await?;
self.bandwidth_manager().remove_client(client_id).await?;
self.shared_key_manager()
.remove_shared_keys(&client_address.as_base58_string())
.await?;
Ok(())
}
/// Initialises `PersistentStorage` using the provided path.
///
/// # Arguments
@@ -92,6 +118,7 @@ impl GatewayStorage {
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.synchronous(SqliteSynchronous::Normal)
.auto_vacuum(SqliteAutoVacuum::Incremental)
.log_slow_statements(LevelFilter::Warn, Duration::from_millis(250))
.filename(database_path)
.create_if_missing(true)
.disable_statement_logging();
@@ -123,8 +150,9 @@ impl GatewayStorage {
}
}
impl GatewayStorage {
pub async fn get_mixnet_client_id(
#[async_trait]
impl SharedKeyGatewayStorage for GatewayStorage {
async fn get_mixnet_client_id(
&self,
client_address: DestinationAddressBytes,
) -> Result<i64, GatewayStorageError> {
@@ -134,22 +162,7 @@ impl GatewayStorage {
.await?)
}
pub async fn handle_forget_me(
&self,
client_address: DestinationAddressBytes,
) -> Result<(), GatewayStorageError> {
let client_id = self.get_mixnet_client_id(client_address).await?;
self.inbox_manager()
.remove_messages_for_client(&client_address.as_base58_string())
.await?;
self.bandwidth_manager().remove_client(client_id).await?;
self.shared_key_manager()
.remove_shared_keys(&client_address.as_base58_string())
.await?;
Ok(())
}
pub async fn insert_shared_keys(
async fn insert_shared_keys(
&self,
client_address: DestinationAddressBytes,
shared_keys: &SharedGatewayKey,
@@ -178,7 +191,7 @@ impl GatewayStorage {
Ok(client_id)
}
pub async fn get_shared_keys(
async fn get_shared_keys(
&self,
client_address: DestinationAddressBytes,
) -> Result<Option<PersistedSharedKeys>, GatewayStorageError> {
@@ -190,7 +203,7 @@ impl GatewayStorage {
}
#[allow(dead_code)]
pub async fn remove_shared_keys(
async fn remove_shared_keys(
&self,
client_address: DestinationAddressBytes,
) -> Result<(), GatewayStorageError> {
@@ -200,7 +213,7 @@ impl GatewayStorage {
Ok(())
}
pub async fn update_last_used_authentication_timestamp(
async fn update_last_used_authentication_timestamp(
&self,
client_id: i64,
last_used_authentication_timestamp: OffsetDateTime,
@@ -214,12 +227,15 @@ impl GatewayStorage {
Ok(())
}
pub async fn get_client(&self, client_id: i64) -> Result<Option<Client>, GatewayStorageError> {
async fn get_client(&self, client_id: i64) -> Result<Option<Client>, GatewayStorageError> {
let client = self.client_manager.get_client(client_id).await?;
Ok(client)
}
}
pub async fn store_message(
#[async_trait]
impl InboxGatewayStorage for GatewayStorage {
async fn store_message(
&self,
client_address: DestinationAddressBytes,
message: Vec<u8>,
@@ -230,7 +246,7 @@ impl GatewayStorage {
Ok(())
}
pub async fn retrieve_messages(
async fn retrieve_messages(
&self,
client_address: DestinationAddressBytes,
start_after: Option<i64>,
@@ -242,19 +258,22 @@ impl GatewayStorage {
Ok(messages)
}
pub async fn remove_messages(&self, ids: Vec<i64>) -> Result<(), GatewayStorageError> {
async fn remove_messages(&self, ids: Vec<i64>) -> Result<(), GatewayStorageError> {
for id in ids {
self.inbox_manager.remove_message(id).await?;
}
Ok(())
}
}
pub async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), GatewayStorageError> {
#[async_trait]
impl BandwidthGatewayStorage for GatewayStorage {
async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), GatewayStorageError> {
self.bandwidth_manager.insert_new_client(client_id).await?;
Ok(())
}
pub async fn set_expiration(
async fn set_expiration(
&self,
client_id: i64,
expiration: OffsetDateTime,
@@ -265,12 +284,12 @@ impl GatewayStorage {
Ok(())
}
pub async fn reset_bandwidth(&self, client_id: i64) -> Result<(), GatewayStorageError> {
async fn reset_bandwidth(&self, client_id: i64) -> Result<(), GatewayStorageError> {
self.bandwidth_manager.reset_bandwidth(client_id).await?;
Ok(())
}
pub async fn get_available_bandwidth(
async fn get_available_bandwidth(
&self,
client_id: i64,
) -> Result<Option<PersistedBandwidth>, GatewayStorageError> {
@@ -280,7 +299,7 @@ impl GatewayStorage {
.await?)
}
pub async fn increase_bandwidth(
async fn increase_bandwidth(
&self,
client_id: i64,
amount: i64,
@@ -291,7 +310,7 @@ impl GatewayStorage {
.await?)
}
pub async fn revoke_ticket_bandwidth(
async fn revoke_ticket_bandwidth(
&self,
ticket_id: i64,
amount: i64,
@@ -302,7 +321,7 @@ impl GatewayStorage {
.await?)
}
pub async fn decrease_bandwidth(
async fn decrease_bandwidth(
&self,
client_id: i64,
amount: i64,
@@ -313,7 +332,7 @@ impl GatewayStorage {
.await?)
}
pub async fn insert_epoch_signers(
async fn insert_epoch_signers(
&self,
epoch_id: i64,
signer_ids: Vec<i64>,
@@ -324,7 +343,7 @@ impl GatewayStorage {
Ok(())
}
pub async fn insert_received_ticket(
async fn insert_received_ticket(
&self,
client_id: i64,
received_at: OffsetDateTime,
@@ -344,11 +363,11 @@ impl GatewayStorage {
Ok(ticket_id)
}
pub async fn contains_ticket(&self, serial_number: &[u8]) -> Result<bool, GatewayStorageError> {
async fn contains_ticket(&self, serial_number: &[u8]) -> Result<bool, GatewayStorageError> {
Ok(self.ticket_manager.has_ticket_data(serial_number).await?)
}
pub async fn insert_ticket_verification(
async fn insert_ticket_verification(
&self,
ticket_id: i64,
signer_id: i64,
@@ -361,7 +380,7 @@ impl GatewayStorage {
Ok(())
}
pub async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> {
async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> {
// set the ticket as rejected
self.ticket_manager.set_rejected_ticket(ticket_id).await?;
@@ -372,7 +391,7 @@ impl GatewayStorage {
Ok(())
}
pub async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> {
async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> {
// 1. insert into verified table
self.ticket_manager
.insert_verified_ticket(ticket_id)
@@ -386,7 +405,7 @@ impl GatewayStorage {
Ok(())
}
pub async fn remove_verified_ticket_binary_data(
async fn remove_verified_ticket_binary_data(
&self,
ticket_id: i64,
) -> Result<(), GatewayStorageError> {
@@ -396,7 +415,7 @@ impl GatewayStorage {
Ok(())
}
pub async fn get_all_verified_tickets_with_sn(
async fn get_all_verified_tickets_with_sn(
&self,
) -> Result<Vec<VerifiedTicket>, GatewayStorageError> {
Ok(self
@@ -405,7 +424,7 @@ impl GatewayStorage {
.await?)
}
pub async fn get_all_proposed_tickets_with_sn(
async fn get_all_proposed_tickets_with_sn(
&self,
proposal_id: u32,
) -> Result<Vec<VerifiedTicket>, GatewayStorageError> {
@@ -415,7 +434,7 @@ impl GatewayStorage {
.await?)
}
pub async fn insert_redemption_proposal(
async fn insert_redemption_proposal(
&self,
tickets: &[VerifiedTicket],
proposal_id: u32,
@@ -438,7 +457,7 @@ impl GatewayStorage {
Ok(())
}
pub async fn clear_post_proposal_data(
async fn clear_post_proposal_data(
&self,
proposal_id: u32,
resolved_at: OffsetDateTime,
@@ -462,13 +481,11 @@ impl GatewayStorage {
Ok(())
}
pub async fn latest_proposal(&self) -> Result<Option<RedemptionProposal>, GatewayStorageError> {
async fn latest_proposal(&self) -> Result<Option<RedemptionProposal>, GatewayStorageError> {
Ok(self.ticket_manager.get_latest_redemption_proposal().await?)
}
pub async fn get_all_unverified_tickets(
&self,
) -> Result<Vec<ClientTicket>, GatewayStorageError> {
async fn get_all_unverified_tickets(&self) -> Result<Vec<ClientTicket>, GatewayStorageError> {
self.ticket_manager
.get_unverified_tickets()
.await?
@@ -477,21 +494,21 @@ impl GatewayStorage {
.collect()
}
pub async fn get_all_unresolved_proposals(&self) -> Result<Vec<i64>, GatewayStorageError> {
async fn get_all_unresolved_proposals(&self) -> Result<Vec<i64>, GatewayStorageError> {
Ok(self
.ticket_manager
.get_all_unresolved_redemption_proposal_ids()
.await?)
}
pub async fn get_votes(&self, ticket_id: i64) -> Result<Vec<i64>, GatewayStorageError> {
async fn get_votes(&self, ticket_id: i64) -> Result<Vec<i64>, GatewayStorageError> {
Ok(self
.ticket_manager
.get_verification_votes(ticket_id)
.await?)
}
pub async fn get_signers(&self, epoch_id: i64) -> Result<Vec<i64>, GatewayStorageError> {
async fn get_signers(&self, epoch_id: i64) -> Result<Vec<i64>, GatewayStorageError> {
Ok(self.ticket_manager.get_epoch_signers(epoch_id).await?)
}
@@ -500,34 +517,20 @@ impl GatewayStorage {
/// # Arguments
///
/// * `peer`: wireguard peer data to be stored
/// * `with_client_id`: if the peer should have a corresponding client_id
/// (created with entry wireguard ticket) or live without one (or with an
/// exiting one), for temporary backwards compatibility.
pub async fn insert_wireguard_peer(
async fn insert_wireguard_peer(
&self,
peer: &defguard_wireguard_rs::host::Peer,
with_client_id: bool,
) -> Result<Option<i64>, GatewayStorageError> {
client_type: ClientType,
) -> Result<i64, GatewayStorageError> {
let client_id = match self
.wireguard_peer_manager
.retrieve_peer(&peer.public_key.to_string())
.await?
{
Some(peer) => peer.client_id,
_ => {
if with_client_id {
Some(
self.client_manager
.insert_client(ClientType::EntryWireguard)
.await?,
)
} else {
None
}
}
None => self.client_manager.insert_client(client_type).await?,
};
let mut peer = WireguardPeer::from(peer.clone());
peer.client_id = client_id;
let peer = WireguardPeer::from_defguard_peer(peer.clone(), client_id)?;
self.wireguard_peer_manager.insert_peer(&peer).await?;
Ok(client_id)
}
@@ -537,7 +540,7 @@ impl GatewayStorage {
/// # Arguments
///
/// * `peer_public_key`: wireguard public key of the peer to be retrieved.
pub async fn get_wireguard_peer(
async fn get_wireguard_peer(
&self,
peer_public_key: &str,
) -> Result<Option<WireguardPeer>, GatewayStorageError> {
@@ -549,7 +552,7 @@ impl GatewayStorage {
}
/// Retrieves all wireguard peers.
pub async fn get_all_wireguard_peers(&self) -> Result<Vec<WireguardPeer>, GatewayStorageError> {
async fn get_all_wireguard_peers(&self) -> Result<Vec<WireguardPeer>, GatewayStorageError> {
let ret = self.wireguard_peer_manager.retrieve_all_peers().await?;
Ok(ret)
}
@@ -559,7 +562,7 @@ impl GatewayStorage {
/// # Arguments
///
/// * `peer_public_key`: wireguard public key of the peer to be removed.
pub async fn remove_wireguard_peer(
async fn remove_wireguard_peer(
&self,
peer_public_key: &str,
) -> Result<(), GatewayStorageError> {
+25 -67
View File
@@ -1,9 +1,7 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use std::time::SystemTime;
use crate::error::GatewayStorageError;
use crate::{error::GatewayStorageError, make_bincode_serializer};
use nym_credentials_interface::{AvailableBandwidth, ClientTicket, CredentialSpendingData};
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
use sqlx::FromRow;
@@ -112,35 +110,24 @@ impl TryFrom<UnverifiedTicketData> for ClientTicket {
#[derive(Debug, Clone, FromRow)]
pub struct WireguardPeer {
pub public_key: String,
pub preshared_key: Option<String>,
pub protocol_version: Option<i64>,
pub endpoint: Option<String>,
pub last_handshake: Option<OffsetDateTime>,
pub tx_bytes: i64,
pub rx_bytes: i64,
pub persistent_keepalive_interval: Option<i64>,
pub allowed_ips: Vec<u8>,
pub client_id: Option<i64>,
pub client_id: i64,
}
impl From<defguard_wireguard_rs::host::Peer> for WireguardPeer {
fn from(value: defguard_wireguard_rs::host::Peer) -> Self {
WireguardPeer {
impl WireguardPeer {
pub fn from_defguard_peer(
value: defguard_wireguard_rs::host::Peer,
client_id: i64,
) -> Result<Self, crate::error::GatewayStorageError> {
Ok(WireguardPeer {
public_key: value.public_key.to_string(),
preshared_key: value.preshared_key.as_ref().map(|k| k.to_string()),
protocol_version: value.protocol_version.map(|v| v as i64),
endpoint: value.endpoint.map(|e| e.to_string()),
last_handshake: value.last_handshake.map(OffsetDateTime::from),
tx_bytes: value.tx_bytes as i64,
rx_bytes: value.rx_bytes as i64,
persistent_keepalive_interval: value.persistent_keepalive_interval.map(|v| v as i64),
allowed_ips: bincode::Options::serialize(
bincode::DefaultOptions::new(),
&value.allowed_ips,
)
.unwrap_or_default(),
client_id: None,
}
allowed_ips: bincode::Options::serialize(make_bincode_serializer(), &value.allowed_ips)
.map_err(|source| crate::error::GatewayStorageError::Serialize {
field_key: "allowed_ips",
source,
})?,
client_id,
})
}
}
@@ -149,49 +136,20 @@ impl TryFrom<WireguardPeer> for defguard_wireguard_rs::host::Peer {
fn try_from(value: WireguardPeer) -> Result<Self, Self::Error> {
Ok(Self {
public_key: value
.public_key
.as_str()
.try_into()
.map_err(|e| Self::Error::TypeConversion(format!("public key {e}")))?,
preshared_key: value
.preshared_key
.as_deref()
.map(TryFrom::try_from)
.transpose()
.map_err(|e| Self::Error::TypeConversion(format!("preshared key {e}")))?,
protocol_version: value
.protocol_version
.map(TryFrom::try_from)
.transpose()
.map_err(|e| Self::Error::TypeConversion(format!("protocol version {e}")))?,
endpoint: value
.endpoint
.as_deref()
.map(|e| e.parse())
.transpose()
.map_err(|e| Self::Error::TypeConversion(format!("endpoint {e}")))?,
last_handshake: value.last_handshake.map(SystemTime::from),
tx_bytes: value
.tx_bytes
.try_into()
.map_err(|e| Self::Error::TypeConversion(format!("tx bytes {e}")))?,
rx_bytes: value
.rx_bytes
.try_into()
.map_err(|e| Self::Error::TypeConversion(format!("rx bytes {e}")))?,
persistent_keepalive_interval: value
.persistent_keepalive_interval
.map(TryFrom::try_from)
.transpose()
.map_err(|e| {
Self::Error::TypeConversion(format!("persistent keepalive interval {e}"))
})?,
public_key: value.public_key.as_str().try_into().map_err(|_| {
Self::Error::TypeConversion {
field_key: "public_key",
}
})?,
allowed_ips: bincode::Options::deserialize(
bincode::DefaultOptions::new(),
&value.allowed_ips,
)
.map_err(|e| Self::Error::TypeConversion(format!("allowed ips {e}")))?,
.map_err(|source| Self::Error::Deserialize {
field_key: "allowed_ips",
source,
})?,
..Default::default()
})
}
}
+511
View File
@@ -0,0 +1,511 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use async_trait::async_trait;
use nym_credentials_interface::ClientTicket;
use nym_gateway_requests::SharedGatewayKey;
use nym_sphinx::DestinationAddressBytes;
use time::OffsetDateTime;
use crate::{
clients::ClientType,
models::{
Client, PersistedBandwidth, PersistedSharedKeys, RedemptionProposal, StoredMessage,
VerifiedTicket, WireguardPeer,
},
GatewayStorageError,
};
#[async_trait]
pub trait SharedKeyGatewayStorage {
async fn get_mixnet_client_id(
&self,
client_address: DestinationAddressBytes,
) -> Result<i64, GatewayStorageError>;
async fn insert_shared_keys(
&self,
client_address: DestinationAddressBytes,
shared_keys: &SharedGatewayKey,
) -> Result<i64, GatewayStorageError>;
async fn get_shared_keys(
&self,
client_address: DestinationAddressBytes,
) -> Result<Option<PersistedSharedKeys>, GatewayStorageError>;
#[allow(dead_code)]
async fn remove_shared_keys(
&self,
client_address: DestinationAddressBytes,
) -> Result<(), GatewayStorageError>;
async fn update_last_used_authentication_timestamp(
&self,
client_id: i64,
last_used_authentication_timestamp: OffsetDateTime,
) -> Result<(), GatewayStorageError>;
async fn get_client(&self, client_id: i64) -> Result<Option<Client>, GatewayStorageError>;
}
#[async_trait]
pub trait InboxGatewayStorage {
async fn store_message(
&self,
client_address: DestinationAddressBytes,
message: Vec<u8>,
) -> Result<(), GatewayStorageError>;
async fn retrieve_messages(
&self,
client_address: DestinationAddressBytes,
start_after: Option<i64>,
) -> Result<(Vec<StoredMessage>, Option<i64>), GatewayStorageError>;
async fn remove_messages(&self, ids: Vec<i64>) -> Result<(), GatewayStorageError>;
}
#[async_trait]
pub trait BandwidthGatewayStorage: dyn_clone::DynClone {
async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), GatewayStorageError>;
async fn set_expiration(
&self,
client_id: i64,
expiration: OffsetDateTime,
) -> Result<(), GatewayStorageError>;
async fn reset_bandwidth(&self, client_id: i64) -> Result<(), GatewayStorageError>;
async fn get_available_bandwidth(
&self,
client_id: i64,
) -> Result<Option<PersistedBandwidth>, GatewayStorageError>;
async fn increase_bandwidth(
&self,
client_id: i64,
amount: i64,
) -> Result<i64, GatewayStorageError>;
async fn revoke_ticket_bandwidth(
&self,
ticket_id: i64,
amount: i64,
) -> Result<(), GatewayStorageError>;
async fn decrease_bandwidth(
&self,
client_id: i64,
amount: i64,
) -> Result<i64, GatewayStorageError>;
async fn insert_epoch_signers(
&self,
epoch_id: i64,
signer_ids: Vec<i64>,
) -> Result<(), GatewayStorageError>;
async fn insert_received_ticket(
&self,
client_id: i64,
received_at: OffsetDateTime,
serial_number: Vec<u8>,
data: Vec<u8>,
) -> Result<i64, GatewayStorageError>;
async fn contains_ticket(&self, serial_number: &[u8]) -> Result<bool, GatewayStorageError>;
async fn insert_ticket_verification(
&self,
ticket_id: i64,
signer_id: i64,
verified_at: OffsetDateTime,
accepted: bool,
) -> Result<(), GatewayStorageError>;
async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError>;
async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError>;
async fn remove_verified_ticket_binary_data(
&self,
ticket_id: i64,
) -> Result<(), GatewayStorageError>;
async fn get_all_verified_tickets_with_sn(
&self,
) -> Result<Vec<VerifiedTicket>, GatewayStorageError>;
async fn get_all_proposed_tickets_with_sn(
&self,
proposal_id: u32,
) -> Result<Vec<VerifiedTicket>, GatewayStorageError>;
async fn insert_redemption_proposal(
&self,
tickets: &[VerifiedTicket],
proposal_id: u32,
created_at: OffsetDateTime,
) -> Result<(), GatewayStorageError>;
async fn clear_post_proposal_data(
&self,
proposal_id: u32,
resolved_at: OffsetDateTime,
rejected: bool,
) -> Result<(), GatewayStorageError>;
async fn latest_proposal(&self) -> Result<Option<RedemptionProposal>, GatewayStorageError>;
async fn get_all_unverified_tickets(&self) -> Result<Vec<ClientTicket>, GatewayStorageError>;
async fn get_all_unresolved_proposals(&self) -> Result<Vec<i64>, GatewayStorageError>;
async fn get_votes(&self, ticket_id: i64) -> Result<Vec<i64>, GatewayStorageError>;
async fn get_signers(&self, epoch_id: i64) -> Result<Vec<i64>, GatewayStorageError>;
/// Insert a wireguard peer in the storage.
///
/// # Arguments
///
/// * `peer`: wireguard peer data to be stored
async fn insert_wireguard_peer(
&self,
peer: &defguard_wireguard_rs::host::Peer,
client_type: ClientType,
) -> Result<i64, GatewayStorageError>;
/// Tries to retrieve a particular peer with the given public key.
///
/// # Arguments
///
/// * `peer_public_key`: wireguard public key of the peer to be retrieved.
async fn get_wireguard_peer(
&self,
peer_public_key: &str,
) -> Result<Option<WireguardPeer>, GatewayStorageError>;
/// Retrieves all wireguard peers.
async fn get_all_wireguard_peers(&self) -> Result<Vec<WireguardPeer>, GatewayStorageError>;
/// Remove a wireguard peer from the storage.
///
/// # Arguments
///
/// * `peer_public_key`: wireguard public key of the peer to be removed.
async fn remove_wireguard_peer(&self, peer_public_key: &str)
-> Result<(), GatewayStorageError>;
}
#[cfg(feature = "mock")]
pub mod mock {
use std::{collections::HashMap, sync::Arc};
use tokio::sync::RwLock;
use super::*;
struct EcashSigner {
_epoch_id: i64,
_signer_id: i64,
}
struct ReceivedTicket {
client_id: i64,
_received_at: OffsetDateTime,
rejected: Option<bool>,
}
struct TicketData {
serial_number: Vec<u8>,
data: Option<Vec<u8>>,
}
struct TicketVerification {
_ticket_id: i64,
_signer_id: i64,
_verified_at: OffsetDateTime,
_accepted: bool,
}
#[derive(Default)]
pub struct MockGatewayStorage {
available_bandwidth: HashMap<i64, PersistedBandwidth>,
ecash_signers: Vec<EcashSigner>,
received_ticket: HashMap<i64, ReceivedTicket>,
ticket_data: HashMap<i64, TicketData>,
ticket_verification: HashMap<i64, TicketVerification>,
verified_tickets: Vec<i64>,
wireguard_peers: HashMap<String, WireguardPeer>,
clients: HashMap<i64, String>,
}
#[async_trait]
impl BandwidthGatewayStorage for Arc<RwLock<MockGatewayStorage>> {
async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), GatewayStorageError> {
self.write().await.available_bandwidth.insert(
client_id,
PersistedBandwidth {
client_id,
available: 0,
expiration: Some(OffsetDateTime::UNIX_EPOCH),
},
);
Ok(())
}
async fn set_expiration(
&self,
client_id: i64,
expiration: OffsetDateTime,
) -> Result<(), GatewayStorageError> {
if let Some(bw) = self.write().await.available_bandwidth.get_mut(&client_id) {
bw.expiration = Some(expiration);
}
Ok(())
}
async fn reset_bandwidth(&self, client_id: i64) -> Result<(), GatewayStorageError> {
if let Some(bw) = self.write().await.available_bandwidth.get_mut(&client_id) {
bw.available = 0;
bw.expiration = Some(OffsetDateTime::UNIX_EPOCH);
}
Ok(())
}
async fn get_available_bandwidth(
&self,
client_id: i64,
) -> Result<Option<PersistedBandwidth>, GatewayStorageError> {
Ok(self
.read()
.await
.available_bandwidth
.get(&client_id)
.cloned())
}
async fn increase_bandwidth(
&self,
client_id: i64,
amount: i64,
) -> Result<i64, GatewayStorageError> {
self.write()
.await
.available_bandwidth
.get_mut(&client_id)
.map(|bw| {
bw.available += amount;
bw.available
})
.ok_or(GatewayStorageError::InternalDatabaseError(
sqlx::Error::RowNotFound,
))
}
async fn revoke_ticket_bandwidth(
&self,
ticket_id: i64,
amount: i64,
) -> Result<(), GatewayStorageError> {
let mut guard = self.write().await;
if let Some(client_id) = guard
.received_ticket
.get(&ticket_id)
.map(|ticket| ticket.client_id)
{
if let Some(bw) = guard.available_bandwidth.get_mut(&client_id) {
bw.available -= amount;
}
}
Ok(())
}
async fn decrease_bandwidth(
&self,
client_id: i64,
amount: i64,
) -> Result<i64, GatewayStorageError> {
self.write()
.await
.available_bandwidth
.get_mut(&client_id)
.map(|bw| {
bw.available -= amount;
bw.available
})
.ok_or(GatewayStorageError::InternalDatabaseError(
sqlx::Error::RowNotFound,
))
}
async fn insert_epoch_signers(
&self,
_epoch_id: i64,
signer_ids: Vec<i64>,
) -> Result<(), GatewayStorageError> {
self.write()
.await
.ecash_signers
.extend(signer_ids.iter().map(|signer_id| EcashSigner {
_epoch_id,
_signer_id: *signer_id,
}));
Ok(())
}
async fn insert_received_ticket(
&self,
client_id: i64,
_received_at: OffsetDateTime,
serial_number: Vec<u8>,
data: Vec<u8>,
) -> Result<i64, GatewayStorageError> {
let mut guard = self.write().await;
let ticket_id = guard.received_ticket.len() as i64;
guard.received_ticket.insert(
ticket_id,
ReceivedTicket {
client_id,
_received_at,
rejected: None,
},
);
guard.ticket_data.insert(
ticket_id,
TicketData {
serial_number,
data: Some(data),
},
);
Ok(ticket_id)
}
async fn contains_ticket(&self, serial_number: &[u8]) -> Result<bool, GatewayStorageError> {
Ok(self
.read()
.await
.ticket_data
.values()
.any(|ticket_data| ticket_data.serial_number == serial_number))
}
async fn insert_ticket_verification(
&self,
_ticket_id: i64,
_signer_id: i64,
_verified_at: OffsetDateTime,
_accepted: bool,
) -> Result<(), GatewayStorageError> {
self.write().await.ticket_verification.insert(
_ticket_id,
TicketVerification {
_ticket_id,
_signer_id,
_verified_at,
_accepted,
},
);
Ok(())
}
async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> {
let mut guard = self.write().await;
if let Some(ticket) = guard.received_ticket.get_mut(&ticket_id) {
ticket.rejected = Some(true);
}
guard.ticket_data.remove(&ticket_id);
Ok(())
}
async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> {
let mut guard = self.write().await;
guard.verified_tickets.push(ticket_id);
guard.ticket_verification.remove(&ticket_id);
Ok(())
}
async fn remove_verified_ticket_binary_data(
&self,
ticket_id: i64,
) -> Result<(), GatewayStorageError> {
if let Some(ticket) = self.write().await.ticket_data.get_mut(&ticket_id) {
ticket.data = None;
}
Ok(())
}
async fn get_all_verified_tickets_with_sn(
&self,
) -> Result<Vec<VerifiedTicket>, GatewayStorageError> {
todo!()
}
async fn get_all_proposed_tickets_with_sn(
&self,
_proposal_id: u32,
) -> Result<Vec<VerifiedTicket>, GatewayStorageError> {
todo!()
}
async fn insert_redemption_proposal(
&self,
_tickets: &[VerifiedTicket],
_proposal_id: u32,
_created_at: OffsetDateTime,
) -> Result<(), GatewayStorageError> {
todo!()
}
async fn clear_post_proposal_data(
&self,
_proposal_id: u32,
_resolved_at: OffsetDateTime,
_rejected: bool,
) -> Result<(), GatewayStorageError> {
todo!()
}
async fn latest_proposal(&self) -> Result<Option<RedemptionProposal>, GatewayStorageError> {
todo!()
}
async fn get_all_unverified_tickets(
&self,
) -> Result<Vec<ClientTicket>, GatewayStorageError> {
todo!()
}
async fn get_all_unresolved_proposals(&self) -> Result<Vec<i64>, GatewayStorageError> {
todo!()
}
async fn get_votes(&self, _ticket_id: i64) -> Result<Vec<i64>, GatewayStorageError> {
todo!()
}
async fn get_signers(&self, _epoch_id: i64) -> Result<Vec<i64>, GatewayStorageError> {
todo!()
}
async fn insert_wireguard_peer(
&self,
peer: &defguard_wireguard_rs::host::Peer,
client_type: ClientType,
) -> Result<i64, GatewayStorageError> {
let mut guard = self.write().await;
let client_id =
if let Some(peer) = guard.wireguard_peers.get(&peer.public_key.to_string()) {
peer.client_id
} else {
let client_id = guard.clients.len() as i64;
guard.clients.insert(client_id, client_type.to_string());
client_id
};
guard.wireguard_peers.insert(
peer.public_key.to_string(),
WireguardPeer::from_defguard_peer(peer.clone(), client_id)?,
);
Ok(client_id)
}
async fn get_wireguard_peer(
&self,
peer_public_key: &str,
) -> Result<Option<WireguardPeer>, GatewayStorageError> {
Ok(self
.read()
.await
.wireguard_peers
.get(peer_public_key)
.cloned())
}
async fn get_all_wireguard_peers(&self) -> Result<Vec<WireguardPeer>, GatewayStorageError> {
todo!()
}
async fn remove_wireguard_peer(
&self,
peer_public_key: &str,
) -> Result<(), GatewayStorageError> {
self.write().await.wireguard_peers.remove(peer_public_key);
Ok(())
}
}
}
@@ -27,15 +27,18 @@ impl WgPeerManager {
pub(crate) async fn insert_peer(&self, peer: &WireguardPeer) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT OR IGNORE INTO wireguard_peer(public_key, preshared_key, protocol_version, endpoint, last_handshake, tx_bytes, rx_bytes, persistent_keepalive_interval, allowed_ips, client_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
INSERT OR IGNORE INTO wireguard_peer(public_key, allowed_ips, client_id)
VALUES (?, ?, ?);
UPDATE wireguard_peer
SET preshared_key = ?, protocol_version = ?, endpoint = ?, last_handshake = ?, tx_bytes = ?, rx_bytes = ?, persistent_keepalive_interval = ?, allowed_ips = ?, client_id = ?
SET allowed_ips = ?, client_id = ?
WHERE public_key = ?
"#,
peer.public_key, peer.preshared_key, peer.protocol_version, peer.endpoint, peer.last_handshake, peer.tx_bytes, peer.rx_bytes, peer.persistent_keepalive_interval, peer.allowed_ips, peer.client_id,
peer.preshared_key, peer.protocol_version, peer.endpoint, peer.last_handshake, peer.tx_bytes, peer.rx_bytes, peer.persistent_keepalive_interval, peer.allowed_ips, peer.client_id,
peer.public_key,
peer.allowed_ips,
peer.client_id,
peer.allowed_ips,
peer.client_id,
peer.public_key,
)
.execute(&self.connection_pool)
+57 -28
View File
@@ -6,9 +6,9 @@
//! The resolver itself is the set combination of the google, cloudflare, and quad9 endpoints
//! supporting DoH and DoT.
//!
//! This resolver implements a fallback mechanism where, should the DNS-over-TLS resolution fail, a
//! This resolver supports a fallback mechanism where, should the DNS-over-TLS resolution fail, a
//! followup resolution will be done using the hosts configured default (e.g. `/etc/resolve.conf` on
//! linux).
//! linux). This is disabled by default and can be enabled using [`enable_system_fallback`].
//!
//! Requires the `dns-over-https-rustls`, `webpki-roots` feature for the
//! `hickory-resolver` crate
@@ -93,14 +93,14 @@ pub struct HickoryDnsResolver {
// Tokio Runtime in initialization, so we must delay the actual
// construction of the resolver.
state: Arc<OnceCell<TokioResolver>>,
fallback: Arc<OnceCell<TokioResolver>>,
fallback: Option<Arc<OnceCell<TokioResolver>>>,
dont_use_shared: bool,
}
impl Resolve for HickoryDnsResolver {
fn resolve(&self, name: Name) -> Resolving {
let resolver = self.state.clone();
let fallback = self.fallback.clone();
let maybe_fallback = self.fallback.clone();
let independent = self.dont_use_shared;
Box::pin(async move {
let resolver = resolver.get_or_try_init(|| {
@@ -117,23 +117,30 @@ impl Resolve for HickoryDnsResolver {
let lookup = match resolver.lookup_ip(name.as_str()).await {
Ok(res) => res,
Err(e) => {
// on failure use the fall back system configured DNS resolver
if !e.is_no_records_found() {
warn!("primary DNS failed w/ error {e}: using system fallback");
}
let resolver = fallback.get_or_try_init(|| {
// 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 independent {
new_resolver_system()
} else {
Ok(SHARED_RESOLVER
.fallback
.get_or_try_init(new_resolver_system)?
.clone())
if let Some(ref fallback) = maybe_fallback {
// on failure use the fall back system configured DNS resolver
if !e.is_no_records_found() {
warn!("primary DNS failed w/ error {e}: using system fallback");
}
})?;
resolver.lookup_ip(name.as_str()).await?
let resolver = fallback.get_or_try_init(|| {
// 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 independent {
new_resolver_system()
} else {
Ok(SHARED_RESOLVER
.fallback
.as_ref()
.ok_or(e)? // if the shared resolver has no fallback return the original error
.get_or_try_init(new_resolver_system)?
.clone())
}
})?;
resolver.lookup_ip(name.as_str()).await?
} else {
return Err(e.into());
}
}
};
@@ -162,14 +169,17 @@ impl HickoryDnsResolver {
let lookup = match resolver.lookup_ip(name).await {
Ok(res) => res,
Err(e) => {
// on failure use the fall back system configured DNS resolver
if !e.is_no_records_found() {
warn!("primary DNS failed w/ error {e}: using system fallback");
if let Some(ref fallback) = self.fallback {
// on failure use the fall back system configured DNS resolver
if !e.is_no_records_found() {
warn!("primary DNS failed w/ error {e}: using system fallback");
}
let resolver = fallback.get_or_try_init(|| self.new_resolver_system())?;
resolver.lookup_ip(name).await?
} else {
return Err(e.into());
}
let resolver = self
.fallback
.get_or_try_init(|| self.new_resolver_system())?;
resolver.lookup_ip(name).await?
}
};
@@ -193,15 +203,34 @@ impl HickoryDnsResolver {
}
fn new_resolver_system(&self) -> Result<TokioResolver, HickoryDnsError> {
if self.dont_use_shared {
if self.dont_use_shared || SHARED_RESOLVER.fallback.is_none() {
new_resolver_system()
} else {
Ok(SHARED_RESOLVER
.fallback
.as_ref()
.unwrap()
.get_or_try_init(new_resolver_system)?
.clone())
}
}
/// Enable fallback to the system default resolver if the primary (DoX) resolver fails
pub fn enable_system_fallback(&mut self) -> Result<(), HickoryDnsError> {
self.fallback = Some(Default::default());
let _ = self
.fallback
.as_ref()
.unwrap()
.get_or_try_init(new_resolver_system)?;
Ok(())
}
/// Disable fallback resolution. If the primary resolver fails the error is
/// returned immediately
pub fn disable_system_fallback(&mut self) {
self.fallback = None;
}
}
/// Create a new resolver with a custom DoT based configuration. The options are overridden to look
+1 -1
View File
@@ -16,7 +16,7 @@ pub struct UserAgent {
pub version: String,
/// client platform
pub platform: String,
/// source commit version for the calling calling crate / subsystem
/// source commit version for the calling crate / subsystem
pub git_commit: String,
}
+1 -1
View File
@@ -12,7 +12,7 @@ use zeroize::{Zeroize, ZeroizeOnDrop};
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Zeroize, ZeroizeOnDrop)]
#[derive(Debug, Zeroize, ZeroizeOnDrop)]
pub struct AckKey(CipherKey<AckEncryptionAlgorithm>);
#[derive(Debug)]
+10
View File
@@ -11,11 +11,13 @@ license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = { workspace = true }
base64 = { workspace = true }
bincode = { workspace = true }
chrono = { workspace = true }
dashmap = { workspace = true }
defguard_wireguard_rs = { workspace = true }
dyn-clone = { workspace = true }
futures = { workspace = true }
# The latest version on crates.io at the time of writing this (6.0.0) has a
# version mismatch with x25519-dalek/curve25519-dalek that is resolved in the
@@ -37,3 +39,11 @@ nym-network-defaults = { path = "../network-defaults" }
nym-task = { path = "../task" }
nym-wireguard-types = { path = "../wireguard-types" }
nym-node-metrics = { path = "../../nym-node/nym-node-metrics" }
[dev-dependencies]
nym-gateway-storage = { path = "../gateway-storage", features = ["mock"] }
[features]
default = []
mock = ["nym-gateway-storage/mock"]
+4 -4
View File
@@ -3,18 +3,18 @@
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("traffic byte data needs to be increasing")]
InconsistentConsumedBytes,
#[error("{0}")]
Defguard(#[from] defguard_wireguard_rs::error::WireguardInterfaceError),
#[error("internal {0}")]
Internal(String),
#[error("storage should have the requested bandwidht entry")]
#[error("storage should have the requested bandwidth entry")]
MissingClientBandwidthEntry,
#[error("kernel should have the requested client entry: {0}")]
MissingClientKernelEntry(String),
#[error("{0}")]
GatewayStorage(#[from] nym_gateway_storage::error::GatewayStorageError),
+87 -29
View File
@@ -6,16 +6,15 @@
// #![warn(clippy::expect_used)]
// #![warn(clippy::unwrap_used)]
use defguard_wireguard_rs::WGApi;
use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask, WGApi, WireguardInterfaceApi};
use nym_crypto::asymmetric::x25519::KeyPair;
#[cfg(target_os = "linux")]
use nym_gateway_storage::GatewayStorage;
use nym_wireguard_types::Config;
use peer_controller::PeerControlRequest;
use std::sync::Arc;
use tokio::sync::mpsc::{self, Receiver, Sender};
#[cfg(target_os = "linux")]
use defguard_wireguard_rs::{host::Peer, key::Key, net::IpAddrMask};
#[cfg(target_os = "linux")]
use nym_network_defaults::constants::WG_TUN_BASE_NAME;
@@ -28,6 +27,81 @@ pub struct WgApiWrapper {
inner: WGApi,
}
impl WireguardInterfaceApi for WgApiWrapper {
fn create_interface(
&self,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
self.inner.create_interface()
}
fn assign_address(
&self,
address: &IpAddrMask,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
self.inner.assign_address(address)
}
fn configure_peer_routing(
&self,
peers: &[Peer],
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
self.inner.configure_peer_routing(peers)
}
#[cfg(not(target_os = "windows"))]
fn configure_interface(
&self,
config: &defguard_wireguard_rs::InterfaceConfiguration,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
self.inner.configure_interface(config)
}
#[cfg(target_os = "windows")]
fn configure_interface(
&self,
config: &defguard_wireguard_rs::InterfaceConfiguration,
dns: &[std::net::IpAddr],
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
self.inner.configure_interface(config, dns)
}
fn remove_interface(
&self,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
self.inner.remove_interface()
}
fn configure_peer(
&self,
peer: &Peer,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
self.inner.configure_peer(peer)
}
fn remove_peer(
&self,
peer_pubkey: &Key,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
self.inner.remove_peer(peer_pubkey)
}
fn read_interface_data(
&self,
) -> Result<
defguard_wireguard_rs::host::Host,
defguard_wireguard_rs::error::WireguardInterfaceError,
> {
self.inner.read_interface_data()
}
fn configure_dns(
&self,
dns: &[std::net::IpAddr],
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
self.inner.configure_dns(dns)
}
}
impl WgApiWrapper {
pub fn new(wg_api: WGApi) -> Self {
WgApiWrapper { inner: wg_api }
@@ -84,9 +158,9 @@ pub struct WireguardData {
/// Start wireguard device
#[cfg(target_os = "linux")]
pub async fn start_wireguard(
storage: nym_gateway_storage::GatewayStorage,
storage: GatewayStorage,
metrics: nym_node_metrics::NymNodeMetrics,
all_peers: Vec<nym_gateway_storage::models::WireguardPeer>,
peers: Vec<Peer>,
task_client: nym_task::TaskClient,
wireguard_data: WireguardData,
) -> Result<std::sync::Arc<WgApiWrapper>, Box<dyn std::error::Error + Send + Sync + 'static>> {
@@ -100,29 +174,13 @@ pub async fn start_wireguard(
let ifname = String::from(WG_TUN_BASE_NAME);
let wg_api = defguard_wireguard_rs::WGApi::new(ifname.clone(), false)?;
let mut peer_bandwidth_managers = HashMap::with_capacity(all_peers.len());
let peers = all_peers
.into_iter()
.map(Peer::try_from)
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.map(|mut peer| {
// since WGApi doesn't set those values on init, let's set them to 0
peer.rx_bytes = 0;
peer.tx_bytes = 0;
peer
})
.collect::<Vec<_>>();
let mut peer_bandwidth_managers = HashMap::with_capacity(peers.len());
for peer in peers.iter() {
let bandwidth_manager =
PeerController::generate_bandwidth_manager(storage.clone(), &peer.public_key)
.await?
.map(|bw_m| Arc::new(RwLock::new(bw_m)));
// Update storage with *x_bytes set to 0, as in kernel peers we can't set those values
// so we need to restart counting. Hopefully the bandwidth was counted in available_bandwidth
storage
.insert_wireguard_peer(peer, bandwidth_manager.is_some())
.await?;
let bandwidth_manager = Arc::new(RwLock::new(
PeerController::generate_bandwidth_manager(Box::new(storage.clone()), &peer.public_key)
.await?,
));
peer_bandwidth_managers.insert(peer.public_key.clone(), (bandwidth_manager, peer.clone()));
}
@@ -175,7 +233,7 @@ pub async fn start_wireguard(
let host = wg_api.read_interface_data()?;
let wg_api = std::sync::Arc::new(WgApiWrapper::new(wg_api));
let mut controller = PeerController::new(
storage,
Box::new(storage),
metrics,
wg_api.clone(),
host,
+176 -125
View File
@@ -8,14 +8,11 @@ use defguard_wireguard_rs::{
};
use futures::channel::oneshot;
use log::info;
use nym_authenticator_requests::latest::registration::{
RemainingBandwidthData, BANDWIDTH_CAP_PER_DAY,
};
use nym_credential_verification::{
bandwidth_storage_manager::BandwidthStorageManager, BandwidthFlushingBehaviourConfig,
ClientBandwidth,
};
use nym_gateway_storage::GatewayStorage;
use nym_gateway_storage::traits::BandwidthGatewayStorage;
use nym_node_metrics::NymNodeMetrics;
use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK;
use std::time::{Duration, SystemTime};
@@ -23,14 +20,12 @@ use std::{collections::HashMap, sync::Arc};
use tokio::sync::{mpsc, RwLock};
use tokio_stream::{wrappers::IntervalStream, StreamExt};
use crate::WgApiWrapper;
use crate::{error::Error, peer_handle::SharedBandwidthStorageManager};
use crate::{peer_handle::PeerHandle, peer_storage_manager::PeerStorageManager};
use crate::{peer_handle::PeerHandle, peer_storage_manager::CachedPeerManager};
pub enum PeerControlRequest {
AddPeer {
peer: Peer,
client_id: Option<i64>,
response_tx: oneshot::Sender<AddPeerControlResponse>,
},
RemovePeer {
@@ -41,10 +36,6 @@ pub enum PeerControlRequest {
key: Key,
response_tx: oneshot::Sender<QueryPeerControlResponse>,
},
QueryBandwidth {
key: Key,
response_tx: oneshot::Sender<QueryBandwidthControlResponse>,
},
GetClientBandwidth {
key: Key,
response_tx: oneshot::Sender<GetClientBandwidthControlResponse>,
@@ -64,17 +55,12 @@ pub struct QueryPeerControlResponse {
pub peer: Option<Peer>,
}
pub struct QueryBandwidthControlResponse {
pub success: bool,
pub bandwidth_data: Option<RemainingBandwidthData>,
}
pub struct GetClientBandwidthControlResponse {
pub client_bandwidth: Option<ClientBandwidth>,
}
pub struct PeerController {
storage: GatewayStorage,
storage: Box<dyn BandwidthGatewayStorage + Send + Sync>,
// we have "all" metrics of a node, but they're behind a single Arc pointer,
// so the overhead is minimal
@@ -83,9 +69,9 @@ pub struct PeerController {
// used to receive commands from individual handles too
request_tx: mpsc::Sender<PeerControlRequest>,
request_rx: mpsc::Receiver<PeerControlRequest>,
wg_api: Arc<WgApiWrapper>,
wg_api: Arc<dyn WireguardInterfaceApi + Send + Sync>,
host_information: Arc<RwLock<Host>>,
bw_storage_managers: HashMap<Key, Option<SharedBandwidthStorageManager>>,
bw_storage_managers: HashMap<Key, SharedBandwidthStorageManager>,
timeout_check_interval: IntervalStream,
task_client: nym_task::TaskClient,
}
@@ -93,11 +79,11 @@ pub struct PeerController {
impl PeerController {
#[allow(clippy::too_many_arguments)]
pub fn new(
storage: GatewayStorage,
storage: Box<dyn BandwidthGatewayStorage + Send + Sync>,
metrics: NymNodeMetrics,
wg_api: Arc<WgApiWrapper>,
wg_api: Arc<dyn WireguardInterfaceApi + Send + Sync>,
initial_host_information: Host,
bw_storage_managers: HashMap<Key, (Option<SharedBandwidthStorageManager>, Peer)>,
bw_storage_managers: HashMap<Key, (SharedBandwidthStorageManager, Peer)>,
request_tx: mpsc::Sender<PeerControlRequest>,
request_rx: mpsc::Receiver<PeerControlRequest>,
task_client: nym_task::TaskClient,
@@ -107,15 +93,11 @@ impl PeerController {
);
let host_information = Arc::new(RwLock::new(initial_host_information));
for (public_key, (bandwidth_storage_manager, peer)) in bw_storage_managers.iter() {
let peer_storage_manager = PeerStorageManager::new(
storage.clone(),
peer.clone(),
bandwidth_storage_manager.is_some(),
);
let cached_peer_manager = CachedPeerManager::new(peer);
let mut handle = PeerHandle::new(
public_key.clone(),
host_information.clone(),
peer_storage_manager,
cached_peer_manager,
bandwidth_storage_manager.clone(),
request_tx.clone(),
&task_client,
@@ -144,32 +126,11 @@ impl PeerController {
}
}
// Function that should be used for peer insertion, to handle both storage and kernel interaction
pub async fn add_peer(&self, peer: &Peer, client_id: Option<i64>) -> Result<(), Error> {
if client_id.is_none() {
self.storage.insert_wireguard_peer(peer, false).await?;
}
let ret: Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> =
self.wg_api.inner.configure_peer(peer);
if client_id.is_none() && ret.is_err() {
// Try to revert the insertion in storage
if self
.storage
.remove_wireguard_peer(&peer.public_key.to_string())
.await
.is_err()
{
log::error!("The storage has been corrupted. Wireguard peer {} will persist in storage indefinitely.", peer.public_key);
}
}
Ok(ret?)
}
// Function that should be used for peer removal, to handle both storage and kernel interaction
pub async fn remove_peer(&mut self, key: &Key) -> Result<(), Error> {
self.storage.remove_wireguard_peer(&key.to_string()).await?;
self.bw_storage_managers.remove(key);
let ret = self.wg_api.inner.remove_peer(key);
let ret = self.wg_api.remove_peer(key);
if ret.is_err() {
log::error!("Wireguard peer could not be removed from wireguard kernel module. Process should be restarted so that the interface is reset.");
}
@@ -177,50 +138,43 @@ impl PeerController {
}
pub async fn generate_bandwidth_manager(
storage: GatewayStorage,
storage: Box<dyn BandwidthGatewayStorage + Send + Sync>,
public_key: &Key,
) -> Result<Option<BandwidthStorageManager>, Error> {
if let Some(client_id) = storage
) -> Result<BandwidthStorageManager, Error> {
let client_id = storage
.get_wireguard_peer(&public_key.to_string())
.await?
.ok_or(Error::MissingClientBandwidthEntry)?
.client_id
{
let bandwidth = storage
.get_available_bandwidth(client_id)
.await?
.ok_or(Error::MissingClientBandwidthEntry)?;
Ok(Some(BandwidthStorageManager::new(
storage,
ClientBandwidth::new(bandwidth.into()),
client_id,
BandwidthFlushingBehaviourConfig::default(),
true,
)))
} else {
Ok(None)
}
.client_id;
let bandwidth = storage
.get_available_bandwidth(client_id)
.await?
.ok_or(Error::MissingClientBandwidthEntry)?;
Ok(BandwidthStorageManager::new(
storage,
ClientBandwidth::new(bandwidth.into()),
client_id,
BandwidthFlushingBehaviourConfig::default(),
true,
))
}
async fn handle_add_request(
&mut self,
peer: &Peer,
client_id: Option<i64>,
) -> Result<(), Error> {
self.add_peer(peer, client_id).await?;
let bandwidth_storage_manager =
Self::generate_bandwidth_manager(self.storage.clone(), &peer.public_key)
.await?
.map(|bw_m| Arc::new(RwLock::new(bw_m)));
let peer_storage_manager = PeerStorageManager::new(
self.storage.clone(),
peer.clone(),
bandwidth_storage_manager.is_some(),
);
async fn handle_add_request(&mut self, peer: &Peer) -> Result<(), Error> {
self.wg_api.configure_peer(peer)?;
let bandwidth_storage_manager = Arc::new(RwLock::new(
Self::generate_bandwidth_manager(
dyn_clone::clone_box(&*self.storage),
&peer.public_key,
)
.await?,
));
let cached_peer_manager = CachedPeerManager::new(peer);
let mut handle = PeerHandle::new(
peer.public_key.clone(),
self.host_information.clone(),
peer_storage_manager,
cached_peer_manager,
bandwidth_storage_manager.clone(),
self.request_tx.clone(),
&self.task_client,
@@ -228,7 +182,7 @@ impl PeerController {
self.bw_storage_managers
.insert(peer.public_key.clone(), bandwidth_storage_manager);
// try to immediately update the host information, to eliminate races
if let Ok(host_information) = self.wg_api.inner.read_interface_data() {
if let Ok(host_information) = self.wg_api.read_interface_data() {
*self.host_information.write().await = host_information;
}
let public_key = peer.public_key.clone();
@@ -248,35 +202,8 @@ impl PeerController {
.transpose()?)
}
async fn handle_query_bandwidth(
&self,
key: &Key,
) -> Result<Option<RemainingBandwidthData>, Error> {
let Some(bandwidth_storage_manager) = self.bw_storage_managers.get(key) else {
return Ok(None);
};
let available_bandwidth = if let Some(bandwidth_storage_manager) = bandwidth_storage_manager
{
bandwidth_storage_manager
.read()
.await
.available_bandwidth()
.await
} else {
let Some(peer) = self.host_information.read().await.peers.get(key).cloned() else {
// host information not updated yet
return Ok(None);
};
BANDWIDTH_CAP_PER_DAY.saturating_sub(peer.rx_bytes + peer.tx_bytes) as i64
};
Ok(Some(RemainingBandwidthData {
available_bandwidth,
}))
}
async fn handle_get_client_bandwidth(&self, key: &Key) -> Option<ClientBandwidth> {
if let Some(Some(bandwidth_storage_manager)) = self.bw_storage_managers.get(key) {
if let Some(bandwidth_storage_manager) = self.bw_storage_managers.get(key) {
Some(bandwidth_storage_manager.read().await.client_bandwidth())
} else {
None
@@ -362,7 +289,7 @@ impl PeerController {
loop {
tokio::select! {
_ = self.timeout_check_interval.next() => {
let Ok(host) = self.wg_api.inner.read_interface_data() else {
let Ok(host) = self.wg_api.read_interface_data() else {
log::error!("Can't read wireguard kernel data");
continue;
};
@@ -376,8 +303,8 @@ impl PeerController {
}
msg = self.request_rx.recv() => {
match msg {
Some(PeerControlRequest::AddPeer { peer, client_id, response_tx }) => {
let ret = self.handle_add_request(&peer, client_id).await;
Some(PeerControlRequest::AddPeer { peer, response_tx }) => {
let ret = self.handle_add_request(&peer).await;
if ret.is_ok() {
response_tx.send(AddPeerControlResponse { success: true }).ok();
} else {
@@ -396,14 +323,6 @@ impl PeerController {
response_tx.send(QueryPeerControlResponse { success: false, peer: None }).ok();
}
}
Some(PeerControlRequest::QueryBandwidth { key, response_tx }) => {
let ret = self.handle_query_bandwidth(&key).await;
if let Ok(bandwidth_data) = ret {
response_tx.send(QueryBandwidthControlResponse { success: true, bandwidth_data }).ok();
} else {
response_tx.send(QueryBandwidthControlResponse { success: false, bandwidth_data: None }).ok();
}
}
Some(PeerControlRequest::GetClientBandwidth { key, response_tx }) => {
let client_bandwidth = self.handle_get_client_bandwidth(&key).await;
response_tx.send(GetClientBandwidthControlResponse { client_bandwidth }).ok();
@@ -419,3 +338,135 @@ impl PeerController {
}
}
}
#[cfg(feature = "mock")]
#[derive(Default)]
struct MockWgApi {
peers: std::sync::RwLock<HashMap<Key, Peer>>,
}
#[cfg(feature = "mock")]
impl WireguardInterfaceApi for MockWgApi {
fn create_interface(
&self,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
todo!()
}
fn assign_address(
&self,
_address: &defguard_wireguard_rs::net::IpAddrMask,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
todo!()
}
fn configure_peer_routing(
&self,
_peers: &[Peer],
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
todo!()
}
#[cfg(not(target_os = "windows"))]
fn configure_interface(
&self,
_config: &defguard_wireguard_rs::InterfaceConfiguration,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
todo!()
}
#[cfg(target_os = "windows")]
fn configure_interface(
&self,
_config: &defguard_wireguard_rs::InterfaceConfiguration,
_dns: &[std::net::IpAddr],
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
todo!()
}
fn remove_interface(
&self,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
todo!()
}
fn configure_peer(
&self,
peer: &Peer,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
self.peers
.write()
.unwrap()
.insert(peer.public_key.clone(), peer.clone());
Ok(())
}
fn remove_peer(
&self,
peer_pubkey: &Key,
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
self.peers.write().unwrap().remove(peer_pubkey);
Ok(())
}
fn read_interface_data(
&self,
) -> Result<Host, defguard_wireguard_rs::error::WireguardInterfaceError> {
let mut host = Host::default();
host.peers = self.peers.read().unwrap().clone();
Ok(host)
}
fn configure_dns(
&self,
_dns: &[std::net::IpAddr],
) -> Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> {
todo!()
}
}
#[cfg(feature = "mock")]
pub fn start_controller(
request_tx: mpsc::Sender<PeerControlRequest>,
request_rx: mpsc::Receiver<PeerControlRequest>,
) -> (
Arc<RwLock<nym_gateway_storage::traits::mock::MockGatewayStorage>>,
nym_task::TaskManager,
) {
let storage = Arc::new(RwLock::new(
nym_gateway_storage::traits::mock::MockGatewayStorage::default(),
));
let wg_api = Arc::new(MockWgApi::default());
let task_manager = nym_task::TaskManager::default();
let mut peer_controller = PeerController::new(
Box::new(storage.clone()),
Default::default(),
wg_api,
Default::default(),
Default::default(),
request_tx,
request_rx,
task_manager.subscribe(),
);
tokio::spawn(async move { peer_controller.run().await });
(storage, task_manager)
}
#[cfg(feature = "mock")]
pub async fn stop_controller(mut task_manager: nym_task::TaskManager) {
task_manager.signal_shutdown().unwrap();
task_manager.wait_for_shutdown().await;
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn start_and_stop() {
let (request_tx, request_rx) = mpsc::channel(1);
let (_, task_manager) = start_controller(request_tx.clone(), request_rx);
stop_controller(task_manager).await;
}
}
+53 -81
View File
@@ -3,13 +3,10 @@
use crate::error::Error;
use crate::peer_controller::PeerControlRequest;
use crate::peer_storage_manager::PeerStorageManager;
use defguard_wireguard_rs::host::Peer;
use crate::peer_storage_manager::{CachedPeerManager, PeerInformation};
use defguard_wireguard_rs::{host::Host, key::Key};
use futures::channel::oneshot;
use nym_authenticator_requests::latest::registration::BANDWIDTH_CAP_PER_DAY;
use nym_credential_verification::bandwidth_storage_manager::BandwidthStorageManager;
use nym_gateway_storage::models::WireguardPeer;
use nym_task::TaskClient;
use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK;
use std::sync::Arc;
@@ -21,8 +18,8 @@ pub(crate) type SharedBandwidthStorageManager = Arc<RwLock<BandwidthStorageManag
pub struct PeerHandle {
public_key: Key,
host_information: Arc<RwLock<Host>>,
peer_storage_manager: PeerStorageManager,
bandwidth_storage_manager: Option<SharedBandwidthStorageManager>,
cached_peer: CachedPeerManager,
bandwidth_storage_manager: SharedBandwidthStorageManager,
request_tx: mpsc::Sender<PeerControlRequest>,
timeout_check_interval: IntervalStream,
task_client: TaskClient,
@@ -32,8 +29,8 @@ impl PeerHandle {
pub fn new(
public_key: Key,
host_information: Arc<RwLock<Host>>,
peer_storage_manager: PeerStorageManager,
bandwidth_storage_manager: Option<SharedBandwidthStorageManager>,
cached_peer: CachedPeerManager,
bandwidth_storage_manager: SharedBandwidthStorageManager,
request_tx: mpsc::Sender<PeerControlRequest>,
task_client: &TaskClient,
) -> Self {
@@ -45,7 +42,7 @@ impl PeerHandle {
PeerHandle {
public_key,
host_information,
peer_storage_manager,
cached_peer,
bandwidth_storage_manager,
request_tx,
timeout_check_interval,
@@ -69,14 +66,10 @@ impl PeerHandle {
Ok(success)
}
fn compute_spent_bandwidth(kernel_peer: &Peer, storage_peer: &WireguardPeer) -> Option<u64> {
let storage_peer_rx_bytes = u64::try_from(storage_peer.rx_bytes)
.inspect_err(|e| tracing::error!("Storage rx bytes could not be converted: {e}"))
.ok()?;
let storage_peer_tx_bytes = u64::try_from(storage_peer.tx_bytes)
.inspect_err(|e| tracing::error!("Storage tx bytes could not be converted: {e}"))
.ok()?;
fn compute_spent_bandwidth(
kernel_peer: PeerInformation,
cached_peer: PeerInformation,
) -> Option<u64> {
let kernel_total = kernel_peer
.rx_bytes
.checked_add(kernel_peer.tx_bytes)
@@ -88,21 +81,26 @@ impl PeerHandle {
);
None
})?;
let storage_total = storage_peer_rx_bytes
.checked_add(storage_peer_tx_bytes)
let cached_total = cached_peer
.rx_bytes
.checked_add(cached_peer.tx_bytes)
.or_else(|| {
tracing::error!("Overflow on storage adding bytes: {storage_peer_rx_bytes} + {storage_peer_tx_bytes}");
tracing::error!(
"Overflow on cached adding bytes: {} + {}",
cached_peer.rx_bytes,
cached_peer.tx_bytes
);
None
})?;
kernel_total.checked_sub(storage_total).or_else(|| {
tracing::error!("Overflow on spent bandwidth subtraction: kernel - storage = {kernel_total} - {storage_total}");
kernel_total.checked_sub(cached_total).or_else(|| {
tracing::error!("Overflow on spent bandwidth subtraction: kernel - cached = {kernel_total} - {cached_total}");
None
})
}
async fn active_peer(&mut self, kernel_peer: &Peer) -> Result<bool, Error> {
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
async fn active_peer(&mut self, kernel_peer: PeerInformation) -> Result<bool, Error> {
let Some(cached_peer) = self.cached_peer.get_peer() else {
log::debug!(
"Peer {:?} not in storage anymore, shutting down handle",
self.public_key
@@ -110,76 +108,51 @@ impl PeerHandle {
return Ok(false);
};
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
let spent_bandwidth = Self::compute_spent_bandwidth(kernel_peer, &storage_peer)
.unwrap_or_else(|| {
// if gateway restarted, the kernel values restart from 0
// and we should restart from 0 in storage as well
if let Some(peer_information) =
self.peer_storage_manager.peer_information.as_mut()
{
peer_information.force_sync = true;
peer_information.peer.rx_bytes = kernel_peer.rx_bytes;
peer_information.peer.tx_bytes = kernel_peer.tx_bytes;
}
0
})
.try_into()
.map_err(|_| Error::InconsistentConsumedBytes)?;
if spent_bandwidth > 0 {
self.peer_storage_manager.update_trx(kernel_peer);
if bandwidth_manager
.write()
.await
.try_use_bandwidth(spent_bandwidth)
.await
.is_err()
{
tracing::debug!(
"Peer {} is out of bandwidth, removing it",
kernel_peer.public_key.to_string()
);
let success = self.remove_peer().await?;
self.peer_storage_manager.remove_peer();
return Ok(!success);
}
}
} else {
let spent_bandwidth = kernel_peer.rx_bytes + kernel_peer.tx_bytes;
if spent_bandwidth >= BANDWIDTH_CAP_PER_DAY {
log::debug!(
"Peer {} doesn't have bandwidth anymore, removing it",
self.public_key
);
let success = self.remove_peer().await?;
return Ok(!success);
}
let spent_bandwidth = Self::compute_spent_bandwidth(kernel_peer, cached_peer)
.unwrap_or_default()
.try_into()
.inspect_err(|err| tracing::error!("Could not convert from u64 to i64: {err:?}"))
.unwrap_or_default();
self.cached_peer.update(kernel_peer);
if spent_bandwidth > 0
&& self
.bandwidth_storage_manager
.write()
.await
.try_use_bandwidth(spent_bandwidth)
.await
.is_err()
{
tracing::debug!(
"Peer {} is out of bandwidth, removing it",
self.public_key.to_string()
);
let success = self.remove_peer().await?;
self.cached_peer.remove_peer();
return Ok(!success);
}
Ok(true)
}
async fn continue_checking(&mut self) -> Result<bool, Error> {
let Some(kernel_peer) = self
let kernel_peer = self
.host_information
.read()
.await
.peers
.get(&self.public_key)
.cloned()
else {
// the host information hasn't beed updated yet
return Ok(true);
};
if !self.active_peer(&kernel_peer).await? {
.ok_or(Error::MissingClientKernelEntry(self.public_key.to_string()))?
.into();
if !self.active_peer(kernel_peer).await? {
log::debug!(
"Peer {:?} is not active anymore, shutting down handle",
self.public_key
);
Ok(false)
} else {
// Update storage values
self.peer_storage_manager.sync_storage_peer().await?;
Ok(true)
}
}
@@ -208,11 +181,10 @@ impl PeerHandle {
_ = self.task_client.recv() => {
log::trace!("PeerHandle: Received shutdown");
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
if let Err(e) = bandwidth_manager.write().await.sync_storage_bandwidth().await {
log::error!("Storage sync failed - {e}, unaccounted bandwidth might have been consumed");
}
if let Err(e) = self.bandwidth_storage_manager.write().await.sync_storage_bandwidth().await {
log::error!("Storage sync failed - {e}, unaccounted bandwidth might have been consumed");
}
log::trace!("PeerHandle: Finished shutdown");
}
}
+21 -91
View File
@@ -1,12 +1,8 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::Error;
use defguard_wireguard_rs::host::Peer;
use nym_gateway_storage::models::WireguardPeer;
use nym_gateway_storage::GatewayStorage;
use std::time::Duration;
use time::OffsetDateTime;
const DEFAULT_PEER_MAX_FLUSHING_RATE: Duration = Duration::from_secs(60 * 60 * 24); // 24h
const DEFAULT_PEER_MAX_DELTA_FLUSHING_AMOUNT: u64 = 512 * 1024 * 1024; // 512MB
@@ -29,116 +25,50 @@ impl Default for PeerFlushingBehaviourConfig {
}
}
pub struct PeerStorageManager {
pub(crate) storage: GatewayStorage,
pub struct CachedPeerManager {
pub(crate) peer_information: Option<PeerInformation>,
pub(crate) cfg: PeerFlushingBehaviourConfig,
pub(crate) with_client_id: bool,
}
impl PeerStorageManager {
pub(crate) fn new(storage: GatewayStorage, peer: Peer, with_client_id: bool) -> Self {
let peer_information = Some(PeerInformation::new(peer));
impl CachedPeerManager {
pub(crate) fn new(peer: &Peer) -> Self {
Self {
storage,
peer_information,
cfg: PeerFlushingBehaviourConfig::default(),
with_client_id,
peer_information: Some(peer.into()),
}
}
pub(crate) fn get_peer(&self) -> Option<WireguardPeer> {
pub(crate) fn get_peer(&self) -> Option<PeerInformation> {
self.peer_information
.as_ref()
.map(|p| p.peer.clone().into())
}
pub(crate) fn remove_peer(&mut self) {
self.peer_information = None;
}
pub(crate) fn update_trx(&mut self, kernel_peer: &Peer) {
pub(crate) fn update(&mut self, kernel_peer: PeerInformation) {
if let Some(peer_information) = self.peer_information.as_mut() {
peer_information.update_trx_bytes(kernel_peer.tx_bytes, kernel_peer.rx_bytes);
peer_information.update_trx_bytes(kernel_peer);
}
}
pub(crate) async fn sync_storage_peer(&mut self) -> Result<(), Error> {
let Some(peer_information) = self.peer_information.as_mut() else {
return Ok(());
};
if !peer_information.should_sync(self.cfg) {
return Ok(());
}
if self
.storage
.get_wireguard_peer(&peer_information.peer().public_key.to_string())
.await?
.is_none()
{
self.peer_information = None;
return Ok(());
}
self.storage
.insert_wireguard_peer(peer_information.peer(), self.with_client_id)
.await?;
peer_information.resync_peer_with_storage();
Ok(())
}
}
#[derive(Clone, Debug)]
#[derive(Clone, Copy, Debug)]
pub(crate) struct PeerInformation {
pub(crate) peer: Peer,
pub(crate) last_synced: OffsetDateTime,
pub(crate) tx_bytes: u64,
pub(crate) rx_bytes: u64,
}
pub(crate) bytes_delta_since_sync: u64,
pub(crate) force_sync: bool,
impl From<&Peer> for PeerInformation {
fn from(value: &Peer) -> Self {
Self {
tx_bytes: value.tx_bytes,
rx_bytes: value.rx_bytes,
}
}
}
impl PeerInformation {
pub fn new(peer: Peer) -> PeerInformation {
PeerInformation {
peer,
last_synced: OffsetDateTime::now_utc(),
bytes_delta_since_sync: 0,
force_sync: false,
}
}
pub(crate) fn should_sync(&self, cfg: PeerFlushingBehaviourConfig) -> bool {
if self.force_sync {
return true;
}
if self.bytes_delta_since_sync >= cfg.peer_max_delta_flushing_amount {
return true;
}
if self.last_synced + cfg.peer_max_flushing_rate < OffsetDateTime::now_utc()
&& self.bytes_delta_since_sync != 0
{
return true;
}
false
}
pub(crate) fn peer(&self) -> &Peer {
&self.peer
}
pub(crate) fn update_trx_bytes(&mut self, tx_bytes: u64, rx_bytes: u64) {
self.bytes_delta_since_sync += tx_bytes.saturating_sub(self.peer.tx_bytes)
+ rx_bytes.saturating_sub(self.peer.rx_bytes);
self.peer.tx_bytes = tx_bytes;
self.peer.rx_bytes = rx_bytes;
}
pub(crate) fn resync_peer_with_storage(&mut self) {
self.bytes_delta_since_sync = 0;
self.last_synced = OffsetDateTime::now_utc();
self.force_sync = false;
pub(crate) fn update_trx_bytes(&mut self, peer: PeerInformation) {
self.tx_bytes = peer.tx_bytes;
self.rx_bytes = peer.rx_bytes;
}
}
+31
View File
@@ -0,0 +1,31 @@
[package]
name = "zulip-client"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
[dependencies]
thiserror = { workspace = true }
itertools = { workspace = true }
url = { workspace = true, features = ["serde"] }
serde = { workspace = true, features = ["derive"] }
zeroize = { workspace = true }
nym-bin-common = { path = "../bin-common" }
nym-http-api-client = { path = "../http-api-client" }
reqwest = { workspace = true }
tracing = { workspace = true }
[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
serde_json = { workspace = true }
[lints]
workspace = true
+151
View File
@@ -0,0 +1,151 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
//! An incomplete Zulip API Client
//!
//! Currently, it serves a single purpose: to send a message to a server,
//! however, it could very easily be extended with additional functionalities.
//!
//! ## Sending Direct Message
//!
//! ```rust
//! # use zulip_client::{Client, ZulipClientError};
//! # use zulip_client::message::DirectMessage;
//! # async fn try_send() -> Result<(), ZulipClientError> {
//! let api_key = "your-api-key";
//! let email = "associated-email-address";
//! let server = "https://server-address.com";
//! let client = Client::builder(email, api_key, server)?.build()?;
//! // send to userid 12
//! client.send_message((12u32, "hello world")).await?;
//! // more concrete typing
//! client.send_message(DirectMessage::new(12, "hello world2")).await?;
//! # Ok(())
//! # }
//! ```
use crate::error::ZulipClientError;
use crate::message::{SendMessageResponse, SendableMessage};
use nym_bin_common::bin_info;
use nym_http_api_client::UserAgent;
use reqwest::{header, Method, RequestBuilder};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use tracing::trace;
use url::Url;
use zeroize::Zeroizing;
#[derive(Serialize, Deserialize)]
pub struct ClientConfig {
pub user_email: String,
pub api_key: String,
// TODO: introduce validation
pub user_agent: Option<String>,
pub server_url: Url,
}
pub struct Client {
server_url: Url,
api_key: Zeroizing<String>,
user_email: String,
inner_client: reqwest::Client,
}
fn default_user_agent() -> String {
UserAgent::from(bin_info!()).to_string()
}
impl Client {
const MESSAGES_ENDPOINT: &'static str = "/api/v1/messages";
pub fn builder(
user_email: impl Into<String>,
api_key: impl Into<String>,
server_url: impl Into<String>,
) -> Result<ClientBuilder, ZulipClientError> {
ClientBuilder::new(user_email, api_key, server_url)
}
pub fn new(config: ClientConfig) -> Result<Self, ZulipClientError> {
let builder = ClientBuilder::new(config.user_email, config.api_key, config.server_url)?;
match config.user_agent {
Some(user_agent) => builder.user_agent(user_agent).build(),
None => builder.build(),
}
}
pub async fn send_message(
&self,
msg: impl Into<SendableMessage>,
) -> Result<SendMessageResponse, ZulipClientError> {
let url = format!("{}{}", self.server_url, Self::MESSAGES_ENDPOINT);
self.build_request(Method::POST, Self::MESSAGES_ENDPOINT)
.form(&msg.into())
.send()
.await
.map_err(|source| ZulipClientError::RequestSendingFailure { source, url })?
.json()
.await
.map_err(|source| ZulipClientError::RequestDecodeFailure { source })
}
fn build_request(&self, method: Method, endpoint: &'static str) -> RequestBuilder {
let url = format!("{}{endpoint}", self.server_url);
trace!("posting to {url}");
self.inner_client
.request(method, url)
.basic_auth(&self.user_email, Some(self.api_key.to_string()))
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
}
}
pub struct ClientBuilder {
api_key: Zeroizing<String>,
user_email: String,
server_url: Url,
user_agent: Option<String>,
}
impl ClientBuilder {
pub fn new(
user_email: impl Into<String>,
api_key: impl Into<String>,
server_url: impl Into<String>,
) -> Result<Self, ZulipClientError> {
let server_url = server_url.into();
let parsed_url =
Url::from_str(&server_url).map_err(|source| ZulipClientError::MalformedServerUrl {
raw: server_url,
source,
})?;
Ok(ClientBuilder {
api_key: Zeroizing::new(api_key.into()),
user_email: user_email.into(),
server_url: parsed_url,
user_agent: None,
})
}
#[must_use]
pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
self.user_agent = Some(user_agent.into());
self
}
pub fn build(self) -> Result<Client, ZulipClientError> {
let user_agent = self.user_agent.unwrap_or_else(default_user_agent);
Ok(Client {
api_key: self.api_key,
server_url: self.server_url,
user_email: self.user_email,
inner_client: reqwest::ClientBuilder::new()
.user_agent(user_agent)
.build()
.map_err(|source| ZulipClientError::ClientBuildFailure { source })?,
})
}
}
+22
View File
@@ -0,0 +1,22 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ZulipClientError {
#[error("failed to send request to {url}: {source}")]
RequestSendingFailure { url: String, source: reqwest::Error },
#[error("failed to decode received response: {source}")]
RequestDecodeFailure { source: reqwest::Error },
#[error("failed to build internal client: {source}")]
ClientBuildFailure { source: reqwest::Error },
#[error("provided url ({raw}) is malformed: {source}")]
MalformedServerUrl {
raw: String,
source: url::ParseError,
},
}
+11
View File
@@ -0,0 +1,11 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
pub mod client;
pub mod error;
pub mod message;
pub type Id = u32;
pub use client::{Client, ClientBuilder};
pub use error::ZulipClientError;
+215
View File
@@ -0,0 +1,215 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::message::to::{ToChannel, ToDirect};
use serde::{Deserialize, Serialize};
pub mod to;
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "result")]
#[serde(rename_all = "snake_case")]
pub enum SendMessageResponse {
Success {
id: i64,
automatic_new_visibility_policy: Option<i64>,
msg: String,
},
Error {
code: String,
msg: String,
stream: Option<String>,
},
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type")]
pub enum SendableMessageContent {
// old name: 'private'
Direct {
// internally this is a list
to: String,
content: String,
},
// alternative name: 'channel'
Stream {
to: String,
topic: Option<String>,
content: String,
},
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SendableMessage {
#[serde(flatten)]
content: SendableMessageContent,
/// For clients supporting local echo, the event queue ID for the client.
/// If passed, `local_id` is required. If the message is successfully sent,
/// the server will include `local_id` in the message event that the client with this `queue_id`
/// will receive notifying it of the new message via `GET /events`.
/// This lets the client know unambiguously that it should replace the locally echoed message,
/// rather than adding this new message
/// (which would be correct if the user had sent the new message from another device).
/// example: "fb67bf8a-c031-47cc-84cf-ed80accacda8"
queue_id: Option<String>,
/// For clients supporting local echo, a unique string-format identifier chosen freely by the client;
/// the server will pass it back to the client without inspecting it, as described in the `queue_id` description.
/// example: "100.01"
local_id: Option<String>,
/// Whether the message should be initially marked read by its sender.
/// If unspecified, the server uses a heuristic based on the client name.
read_by_sender: bool,
}
impl SendableMessage {
pub fn new(content: impl Into<SendableMessageContent>) -> Self {
SendableMessage {
content: content.into(),
queue_id: None,
local_id: None,
read_by_sender: false,
}
}
#[must_use]
pub fn with_queue(mut self, queue_id: impl Into<String>, local_id: impl Into<String>) -> Self {
self.queue_id = Some(queue_id.into());
self.local_id = Some(local_id.into());
self
}
#[must_use]
pub fn read_by_sender(mut self, read_by_sender: bool) -> Self {
self.read_by_sender = read_by_sender;
self
}
}
pub type PrivateMessage = DirectMessage;
pub struct DirectMessage {
to: String,
content: String,
}
impl DirectMessage {
pub fn new(to: impl Into<ToDirect>, content: impl Into<String>) -> Self {
DirectMessage {
to: to.into().to_string(),
content: content.into(),
}
}
}
pub type ChannelMessage = StreamMessage;
pub struct StreamMessage {
to: String,
topic: Option<String>,
content: String,
}
impl StreamMessage {
pub fn new(
to: impl Into<ToChannel>,
content: impl Into<String>,
topic: Option<String>,
) -> Self {
StreamMessage {
to: to.into().to_string(),
topic,
content: content.into(),
}
}
pub fn no_topic(to: impl Into<ToChannel>, content: impl Into<String>) -> Self {
Self::new(to, content, None)
}
#[must_use]
pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
self.topic = Some(topic.into());
self
}
}
impl From<SendableMessageContent> for SendableMessage {
fn from(content: SendableMessageContent) -> Self {
SendableMessage::new(content)
}
}
impl From<DirectMessage> for SendableMessage {
fn from(msg: DirectMessage) -> Self {
SendableMessageContent::from(msg).into()
}
}
impl From<DirectMessage> for SendableMessageContent {
fn from(msg: DirectMessage) -> Self {
SendableMessageContent::Direct {
to: msg.to,
content: msg.content,
}
}
}
impl<T, S> From<(T, S)> for DirectMessage
where
T: Into<ToDirect>,
S: Into<String>,
{
fn from((to, content): (T, S)) -> Self {
DirectMessage::new(to, content)
}
}
impl<T, S> From<(T, S)> for SendableMessage
where
T: Into<ToDirect>,
S: Into<String>,
{
fn from((to, content): (T, S)) -> Self {
DirectMessage::new(to, content).into()
}
}
impl From<StreamMessage> for SendableMessage {
fn from(msg: StreamMessage) -> Self {
SendableMessageContent::from(msg).into()
}
}
impl From<StreamMessage> for SendableMessageContent {
fn from(msg: StreamMessage) -> Self {
SendableMessageContent::Stream {
to: msg.to,
topic: msg.topic,
content: msg.content,
}
}
}
impl<T, S> From<(T, S, Option<S>)> for StreamMessage
where
T: Into<ToChannel>,
S: Into<String>,
{
fn from((to, content, topic): (T, S, Option<S>)) -> Self {
StreamMessage::new(to, content, topic.map(Into::into))
}
}
impl<T, S> From<(T, S, Option<S>)> for SendableMessage
where
T: Into<ToChannel>,
S: Into<String>,
{
fn from(inner: (T, S, Option<S>)) -> Self {
StreamMessage::from(inner).into()
}
}
+128
View File
@@ -0,0 +1,128 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::Id;
use itertools::Itertools;
use std::fmt::Display;
// from the docs:
// For channel messages, either the name or integer ID of the channel.
// For direct messages, either a list containing integer user IDs
// or a list containing string Zulip API email addresses.
pub enum ToDirect {
ByIds(Vec<Id>),
ByNames(Vec<String>),
}
impl Display for ToDirect {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ToDirect::ByIds(ids) => write!(f, "[{}]", ids.iter().join(",")),
ToDirect::ByNames(names) => {
write!(f, "[{}]", names.join(","))
}
}
}
}
impl From<Vec<String>> for ToDirect {
fn from(names: Vec<String>) -> Self {
ToDirect::ByNames(names)
}
}
impl From<&[String]> for ToDirect {
fn from(names: &[String]) -> Self {
names.to_vec().into()
}
}
impl From<&[&str]> for ToDirect {
fn from(names: &[&str]) -> Self {
names
.iter()
.map(|s| s.to_string())
.collect::<Vec<_>>()
.into()
}
}
impl<const N: usize> From<&[&str; N]> for ToDirect {
fn from(names: &[&str; N]) -> Self {
names.as_slice().into()
}
}
impl From<Vec<&str>> for ToDirect {
fn from(names: Vec<&str>) -> Self {
names.as_slice().into()
}
}
impl From<String> for ToDirect {
fn from(name: String) -> Self {
ToDirect::ByNames(vec![name])
}
}
impl From<&str> for ToDirect {
fn from(name: &str) -> Self {
name.to_string().into()
}
}
impl From<Id> for ToDirect {
fn from(id: Id) -> Self {
ToDirect::ByIds(vec![id])
}
}
impl From<&[Id]> for ToDirect {
fn from(ids: &[Id]) -> Self {
ids.to_vec().into()
}
}
impl<const N: usize> From<&[Id; N]> for ToDirect {
fn from(ids: &[Id; N]) -> Self {
ids.as_slice().into()
}
}
impl From<Vec<Id>> for ToDirect {
fn from(ids: Vec<Id>) -> Self {
ToDirect::ByIds(ids)
}
}
pub enum ToChannel {
ByName(String),
ById(Id),
}
impl Display for ToChannel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ToChannel::ByName(name) => name.fmt(f),
ToChannel::ById(id) => id.fmt(f),
}
}
}
impl From<String> for ToChannel {
fn from(name: String) -> Self {
ToChannel::ByName(name)
}
}
impl From<&str> for ToChannel {
fn from(name: &str) -> Self {
name.to_string().into()
}
}
impl From<Id> for ToChannel {
fn from(id: Id) -> Self {
ToChannel::ById(id)
}
}
+1
View File
@@ -1091,6 +1091,7 @@ dependencies = [
name = "nym-coconut-dkg"
version = "0.1.0"
dependencies = [
"anyhow",
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
+1
View File
@@ -29,6 +29,7 @@ cw4 = { workspace = true }
thiserror = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
easy-addr = { path = "../../common/cosmwasm-smart-contracts/easy_addr" }
cw-multi-test = { workspace = true }
cw4-group = { path = "../multisig/cw4-group" }
@@ -361,6 +361,29 @@
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"get_epoch_state_at_height"
],
"properties": {
"get_epoch_state_at_height": {
"type": "object",
"required": [
"height"
],
"properties": {
"height": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
@@ -460,6 +483,80 @@
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"get_epoch_dealers_addresses"
],
"properties": {
"get_epoch_dealers_addresses": {
"type": "object",
"required": [
"epoch_id"
],
"properties": {
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"get_epoch_dealers"
],
"properties": {
"get_epoch_dealers": {
"type": "object",
"required": [
"epoch_id"
],
"properties": {
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
@@ -1858,6 +1955,375 @@
}
}
},
"get_epoch_dealers": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PagedDealerResponse",
"type": "object",
"required": [
"dealers",
"per_page"
],
"properties": {
"dealers": {
"type": "array",
"items": {
"$ref": "#/definitions/DealerDetails"
}
},
"per_page": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"start_next_after": {
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
"anyOf": [
{
"$ref": "#/definitions/Addr"
},
{
"type": "null"
}
]
}
},
"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"
},
"DealerDetails": {
"type": "object",
"required": [
"address",
"announce_address",
"assigned_index",
"bte_public_key_with_proof",
"ed25519_identity"
],
"properties": {
"address": {
"$ref": "#/definitions/Addr"
},
"announce_address": {
"type": "string"
},
"assigned_index": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"bte_public_key_with_proof": {
"type": "string"
},
"ed25519_identity": {
"type": "string"
}
},
"additionalProperties": false
}
}
},
"get_epoch_dealers_addresses": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PagedDealerAddressesResponse",
"type": "object",
"required": [
"dealers"
],
"properties": {
"dealers": {
"type": "array",
"items": {
"$ref": "#/definitions/Addr"
}
},
"start_next_after": {
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
"anyOf": [
{
"$ref": "#/definitions/Addr"
},
{
"type": "null"
}
]
}
},
"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"
}
}
},
"get_epoch_state_at_height": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Nullable_Epoch",
"anyOf": [
{
"$ref": "#/definitions/Epoch"
},
{
"type": "null"
}
],
"definitions": {
"Epoch": {
"type": "object",
"required": [
"epoch_id",
"state",
"state_progress",
"time_configuration"
],
"properties": {
"deadline": {
"anyOf": [
{
"$ref": "#/definitions/Timestamp"
},
{
"type": "null"
}
]
},
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"state": {
"$ref": "#/definitions/EpochState"
},
"state_progress": {
"$ref": "#/definitions/StateProgress"
},
"time_configuration": {
"$ref": "#/definitions/TimeConfiguration"
}
},
"additionalProperties": false
},
"EpochState": {
"oneOf": [
{
"type": "string",
"enum": [
"waiting_initialisation",
"in_progress"
]
},
{
"type": "object",
"required": [
"public_key_submission"
],
"properties": {
"public_key_submission": {
"type": "object",
"required": [
"resharing"
],
"properties": {
"resharing": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"dealing_exchange"
],
"properties": {
"dealing_exchange": {
"type": "object",
"required": [
"resharing"
],
"properties": {
"resharing": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"verification_key_submission"
],
"properties": {
"verification_key_submission": {
"type": "object",
"required": [
"resharing"
],
"properties": {
"resharing": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"verification_key_validation"
],
"properties": {
"verification_key_validation": {
"type": "object",
"required": [
"resharing"
],
"properties": {
"resharing": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"verification_key_finalization"
],
"properties": {
"verification_key_finalization": {
"type": "object",
"required": [
"resharing"
],
"properties": {
"resharing": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
]
},
"StateProgress": {
"type": "object",
"required": [
"registered_dealers",
"registered_resharing_dealers",
"submitted_dealings",
"submitted_key_shares",
"verified_keys"
],
"properties": {
"registered_dealers": {
"description": "Counts the number of dealers that have registered in this epoch.",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"registered_resharing_dealers": {
"description": "Counts the number of resharing dealers that have registered in this epoch. This field is only populated during a resharing exchange. It is always <= registered_dealers.",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"submitted_dealings": {
"description": "Counts the number of fully received dealings (i.e. full chunks) from all the allowed dealers.",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"submitted_key_shares": {
"description": "Counts the number of submitted verification key shared from the dealers.",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"verified_keys": {
"description": "Counts the number of verified key shares.",
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
},
"TimeConfiguration": {
"type": "object",
"required": [
"dealing_exchange_time_secs",
"in_progress_time_secs",
"public_key_submission_time_secs",
"verification_key_finalization_time_secs",
"verification_key_submission_time_secs",
"verification_key_validation_time_secs"
],
"properties": {
"dealing_exchange_time_secs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"in_progress_time_secs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"public_key_submission_time_secs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"verification_key_finalization_time_secs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"verification_key_submission_time_secs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"verification_key_validation_time_secs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"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"
}
}
},
"get_epoch_threshold": {
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "uint64",
@@ -28,6 +28,29 @@
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"get_epoch_state_at_height"
],
"properties": {
"get_epoch_state_at_height": {
"type": "object",
"required": [
"height"
],
"properties": {
"height": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
@@ -127,6 +150,80 @@
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"get_epoch_dealers_addresses"
],
"properties": {
"get_epoch_dealers_addresses": {
"type": "object",
"required": [
"epoch_id"
],
"properties": {
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"get_epoch_dealers"
],
"properties": {
"get_epoch_dealers": {
"type": "object",
"required": [
"epoch_id"
],
"properties": {
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"limit": {
"type": [
"integer",
"null"
],
"format": "uint32",
"minimum": 0.0
},
"start_after": {
"type": [
"string",
"null"
]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
@@ -0,0 +1,70 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PagedDealerResponse",
"type": "object",
"required": [
"dealers",
"per_page"
],
"properties": {
"dealers": {
"type": "array",
"items": {
"$ref": "#/definitions/DealerDetails"
}
},
"per_page": {
"type": "integer",
"format": "uint",
"minimum": 0.0
},
"start_next_after": {
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
"anyOf": [
{
"$ref": "#/definitions/Addr"
},
{
"type": "null"
}
]
}
},
"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"
},
"DealerDetails": {
"type": "object",
"required": [
"address",
"announce_address",
"assigned_index",
"bte_public_key_with_proof",
"ed25519_identity"
],
"properties": {
"address": {
"$ref": "#/definitions/Addr"
},
"announce_address": {
"type": "string"
},
"assigned_index": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"bte_public_key_with_proof": {
"type": "string"
},
"ed25519_identity": {
"type": "string"
}
},
"additionalProperties": false
}
}
}
@@ -0,0 +1,34 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "PagedDealerAddressesResponse",
"type": "object",
"required": [
"dealers"
],
"properties": {
"dealers": {
"type": "array",
"items": {
"$ref": "#/definitions/Addr"
}
},
"start_next_after": {
"description": "Field indicating paging information for the following queries if the caller wishes to get further entries.",
"anyOf": [
{
"$ref": "#/definitions/Addr"
},
{
"type": "null"
}
]
}
},
"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"
}
}
}
@@ -0,0 +1,265 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Nullable_Epoch",
"anyOf": [
{
"$ref": "#/definitions/Epoch"
},
{
"type": "null"
}
],
"definitions": {
"Epoch": {
"type": "object",
"required": [
"epoch_id",
"state",
"state_progress",
"time_configuration"
],
"properties": {
"deadline": {
"anyOf": [
{
"$ref": "#/definitions/Timestamp"
},
{
"type": "null"
}
]
},
"epoch_id": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"state": {
"$ref": "#/definitions/EpochState"
},
"state_progress": {
"$ref": "#/definitions/StateProgress"
},
"time_configuration": {
"$ref": "#/definitions/TimeConfiguration"
}
},
"additionalProperties": false
},
"EpochState": {
"oneOf": [
{
"type": "string",
"enum": [
"waiting_initialisation",
"in_progress"
]
},
{
"type": "object",
"required": [
"public_key_submission"
],
"properties": {
"public_key_submission": {
"type": "object",
"required": [
"resharing"
],
"properties": {
"resharing": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"dealing_exchange"
],
"properties": {
"dealing_exchange": {
"type": "object",
"required": [
"resharing"
],
"properties": {
"resharing": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"verification_key_submission"
],
"properties": {
"verification_key_submission": {
"type": "object",
"required": [
"resharing"
],
"properties": {
"resharing": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"verification_key_validation"
],
"properties": {
"verification_key_validation": {
"type": "object",
"required": [
"resharing"
],
"properties": {
"resharing": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
},
{
"type": "object",
"required": [
"verification_key_finalization"
],
"properties": {
"verification_key_finalization": {
"type": "object",
"required": [
"resharing"
],
"properties": {
"resharing": {
"type": "boolean"
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}
]
},
"StateProgress": {
"type": "object",
"required": [
"registered_dealers",
"registered_resharing_dealers",
"submitted_dealings",
"submitted_key_shares",
"verified_keys"
],
"properties": {
"registered_dealers": {
"description": "Counts the number of dealers that have registered in this epoch.",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"registered_resharing_dealers": {
"description": "Counts the number of resharing dealers that have registered in this epoch. This field is only populated during a resharing exchange. It is always <= registered_dealers.",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"submitted_dealings": {
"description": "Counts the number of fully received dealings (i.e. full chunks) from all the allowed dealers.",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"submitted_key_shares": {
"description": "Counts the number of submitted verification key shared from the dealers.",
"type": "integer",
"format": "uint32",
"minimum": 0.0
},
"verified_keys": {
"description": "Counts the number of verified key shares.",
"type": "integer",
"format": "uint32",
"minimum": 0.0
}
},
"additionalProperties": false
},
"TimeConfiguration": {
"type": "object",
"required": [
"dealing_exchange_time_secs",
"in_progress_time_secs",
"public_key_submission_time_secs",
"verification_key_finalization_time_secs",
"verification_key_submission_time_secs",
"verification_key_validation_time_secs"
],
"properties": {
"dealing_exchange_time_secs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"in_progress_time_secs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"public_key_submission_time_secs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"verification_key_finalization_time_secs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"verification_key_submission_time_secs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
},
"verification_key_validation_time_secs": {
"type": "integer",
"format": "uint64",
"minimum": 0.0
}
},
"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"
}
}
}
+34 -12
View File
@@ -3,6 +3,7 @@
use crate::dealers::queries::{
query_current_dealers_paged, query_dealer_details, query_dealers_indices_paged,
query_epoch_dealers_addresses_paged, query_epoch_dealers_paged,
query_registered_dealer_details,
};
use crate::dealers::transactions::try_add_dealer;
@@ -13,13 +14,14 @@ use crate::dealings::queries::{
use crate::dealings::transactions::{try_commit_dealings_chunk, try_submit_dealings_metadata};
use crate::epoch_state::queries::{
query_can_advance_state, query_current_epoch, query_current_epoch_threshold,
query_epoch_threshold,
query_epoch_at_height, query_epoch_threshold,
};
use crate::epoch_state::storage::{CURRENT_EPOCH, EPOCH_THRESHOLDS, THRESHOLD};
use crate::epoch_state::storage::save_epoch;
use crate::epoch_state::transactions::{
try_advance_epoch_state, try_initiate_dkg, try_trigger_reset, try_trigger_resharing,
};
use crate::error::ContractError;
use crate::queued_migrations::introduce_historical_epochs;
use crate::state::queries::query_state;
use crate::state::storage::{DKG_ADMIN, MULTISIG, STATE};
use crate::verification_key_shares::queries::{query_vk_share, query_vk_shares_paged};
@@ -67,8 +69,9 @@ pub fn instantiate(
};
STATE.save(deps.storage, &state)?;
CURRENT_EPOCH.save(
save_epoch(
deps.storage,
env.block.height,
&Epoch::new(
EpochState::WaitingInitialisation,
0,
@@ -100,6 +103,7 @@ pub fn execute(
resharing,
} => try_add_dealer(
deps,
env,
info,
bte_key_with_proof,
identity_key,
@@ -118,7 +122,7 @@ pub fn execute(
try_commit_verification_key_share(deps, env, info, share, resharing)
}
ExecuteMsg::VerifyVerificationKeyShare { owner, resharing } => {
try_verify_verification_key_share(deps, info, owner, resharing)
try_verify_verification_key_share(deps, env, info, owner, resharing)
}
ExecuteMsg::AdvanceEpochState {} => try_advance_epoch_state(deps, env),
ExecuteMsg::TriggerReset {} => try_trigger_reset(deps, env, info),
@@ -131,6 +135,9 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
let response = match msg {
QueryMsg::GetState {} => to_json_binary(&query_state(deps.storage)?)?,
QueryMsg::GetCurrentEpochState {} => to_json_binary(&query_current_epoch(deps.storage)?)?,
QueryMsg::GetEpochStateAtHeight { height } => {
to_json_binary(&query_epoch_at_height(deps.storage, height)?)?
}
QueryMsg::CanAdvanceState {} => {
to_json_binary(&query_can_advance_state(deps.storage, env)?)?
}
@@ -151,6 +158,26 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
QueryMsg::GetDealerDetails { dealer_address } => {
to_json_binary(&query_dealer_details(deps, dealer_address)?)?
}
QueryMsg::GetEpochDealersAddresses {
epoch_id,
limit,
start_after,
} => to_json_binary(&query_epoch_dealers_addresses_paged(
deps,
epoch_id,
start_after,
limit,
)?)?,
QueryMsg::GetEpochDealers {
epoch_id,
limit,
start_after,
} => to_json_binary(&query_epoch_dealers_paged(
deps,
epoch_id,
start_after,
limit,
)?)?,
QueryMsg::GetCurrentDealers { limit, start_after } => {
to_json_binary(&query_current_dealers_paged(deps, start_after, limit)?)?
}
@@ -221,16 +248,11 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
}
#[entry_point]
pub fn migrate(deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
pub fn migrate(deps: DepsMut<'_>, env: Env, _msg: MigrateMsg) -> Result<Response, ContractError> {
set_build_information!(deps.storage)?;
cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?;
// MAINNET MIGRATION ASSERTION
let epoch = CURRENT_EPOCH.load(deps.storage)?;
assert_eq!(0, epoch.epoch_id);
let threshold = THRESHOLD.load(deps.storage)?;
EPOCH_THRESHOLDS.save(deps.storage, 0, &threshold)?;
introduce_historical_epochs(deps, env)?;
Ok(Response::new())
}
@@ -334,7 +356,7 @@ mod tests {
let api = MockApi::default();
const MEMBER_SIZE: usize = 100;
let members: [Addr; MEMBER_SIZE] =
std::array::from_fn(|idx| api.addr_make(&format!("member{}", idx)));
std::array::from_fn(|idx| api.addr_make(&format!("member{idx}")));
let mut app = AppBuilder::new().build(|router, _, storage| {
router
+353 -99
View File
@@ -5,12 +5,12 @@ use crate::dealers::storage::{
self, get_dealer_details, get_dealer_index, get_registration_details, DEALERS_INDICES,
EPOCH_DEALERS_MAP,
};
use crate::epoch_state::storage::CURRENT_EPOCH;
use crate::epoch_state::storage::load_current_epoch;
use cosmwasm_std::{Deps, Order, StdResult};
use cw_storage_plus::Bound;
use nym_coconut_dkg_common::dealer::{
DealerDetailsResponse, DealerType, PagedDealerIndexResponse, PagedDealerResponse,
RegisteredDealerDetails,
DealerDetailsResponse, DealerType, PagedDealerAddressesResponse, PagedDealerIndexResponse,
PagedDealerResponse, RegisteredDealerDetails,
};
use nym_coconut_dkg_common::types::{DealerDetails, EpochId};
@@ -23,7 +23,7 @@ pub fn query_registered_dealer_details(
let epoch_id = match epoch_id {
Some(epoch_id) => epoch_id,
None => CURRENT_EPOCH.load(deps.storage)?.epoch_id,
None => load_current_epoch(deps.storage)?.epoch_id,
};
Ok(RegisteredDealerDetails {
@@ -36,7 +36,7 @@ pub fn query_dealer_details(
dealer_address: String,
) -> StdResult<DealerDetailsResponse> {
let addr = deps.api.addr_validate(&dealer_address)?;
let current_epoch_id = CURRENT_EPOCH.load(deps.storage)?.epoch_id;
let current_epoch_id = load_current_epoch(deps.storage)?.epoch_id;
// if the address has registration data for the current epoch, it means it's an active dealer
if let Ok(dealer_details) = get_dealer_details(deps.storage, &addr, current_epoch_id) {
@@ -82,8 +82,37 @@ pub fn query_dealers_indices_paged(
Ok(PagedDealerIndexResponse::new(dealers, start_next_after))
}
pub fn query_current_dealers_paged(
pub fn query_epoch_dealers_addresses_paged(
deps: Deps<'_>,
epoch_id: EpochId,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<PagedDealerAddressesResponse> {
let limit = limit
.unwrap_or(storage::DEALERS_ADDRESSES_PAGE_DEFAULT_LIMIT)
.min(storage::DEALERS_ADDRESSES_PAGE_MAX_LIMIT) as usize;
let addr = start_after
.map(|addr| deps.api.addr_validate(&addr))
.transpose()?;
let start = addr.as_ref().map(Bound::exclusive);
let dealers = EPOCH_DEALERS_MAP
.prefix(epoch_id)
.keys(deps.storage, start, None, Order::Ascending)
.take(limit)
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = dealers.last().cloned();
Ok(PagedDealerAddressesResponse {
dealers,
start_next_after,
})
}
pub fn query_epoch_dealers_paged(
deps: Deps<'_>,
epoch_id: EpochId,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<PagedDealerResponse> {
@@ -96,10 +125,8 @@ pub fn query_current_dealers_paged(
let start = addr.as_ref().map(Bound::exclusive);
let current_epoch_id = CURRENT_EPOCH.load(deps.storage)?.epoch_id;
let dealers = EPOCH_DEALERS_MAP
.prefix(current_epoch_id)
.prefix(epoch_id)
.range(deps.storage, start, None, Order::Ascending)
.take(limit)
.map(|res| {
@@ -107,7 +134,7 @@ pub fn query_current_dealers_paged(
// SAFETY: if we have DealerRegistrationDetails saved, it means we MUST also have its node index
// otherwise some serious invariants have been broken in the contract, and we're in trouble
#[allow(clippy::expect_used)]
let assigned_index = get_dealer_index(deps.storage, &address, current_epoch_id)
let assigned_index = get_dealer_index(deps.storage, &address, epoch_id)
.expect("could not retrieve dealer index for a registered dealer");
DealerDetails {
@@ -125,6 +152,15 @@ pub fn query_current_dealers_paged(
Ok(PagedDealerResponse::new(dealers, limit, start_next_after))
}
pub fn query_current_dealers_paged(
deps: Deps<'_>,
start_after: Option<String>,
limit: Option<u32>,
) -> StdResult<PagedDealerResponse> {
let current_epoch_id = load_current_epoch(deps.storage)?.epoch_id;
query_epoch_dealers_paged(deps, current_epoch_id, start_after, limit)
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
@@ -158,112 +194,330 @@ pub(crate) mod tests {
}
}
#[test]
fn dealers_empty_on_init() {
let deps = init_contract();
#[cfg(test)]
mod current_epoch_dealers {
use super::*;
let page1 = query_current_dealers_paged(deps.as_ref(), None, None).unwrap();
assert_eq!(0, page1.dealers.len() as u32);
#[test]
fn dealers_empty_on_init() {
let deps = init_contract();
let page1 = query_current_dealers_paged(deps.as_ref(), None, None).unwrap();
assert_eq!(0, page1.dealers.len() as u32);
}
#[test]
fn dealers_paged_retrieval_obeys_limits() {
let mut deps = init_contract();
let limit = 2;
fill_dealers(&mut deps, 0, 1000);
let page1 =
query_current_dealers_paged(deps.as_ref(), None, Option::from(limit)).unwrap();
assert_eq!(limit, page1.dealers.len() as u32);
remove_dealers(&mut deps, 0, 1000);
}
#[test]
fn dealers_paged_retrieval_has_default_limit() {
let mut deps = init_contract();
fill_dealers(&mut deps, 0, 1000);
// query without explicitly setting a limit
let page1 = query_current_dealers_paged(deps.as_ref(), None, None).unwrap();
assert_eq!(DEALERS_PAGE_DEFAULT_LIMIT, page1.dealers.len() as u32);
remove_dealers(&mut deps, 0, 1000);
}
#[test]
fn dealers_paged_retrieval_has_max_limit() {
let mut deps = init_contract();
// query with a crazily high limit in an attempt to use too many resources
let crazy_limit = 1000 * DEALERS_PAGE_MAX_LIMIT;
fill_dealers(&mut deps, 0, 1000);
let page1 = query_current_dealers_paged(deps.as_ref(), None, Option::from(crazy_limit))
.unwrap();
// we default to a decent sized upper bound instead
let expected_limit = DEALERS_PAGE_MAX_LIMIT;
assert_eq!(expected_limit, page1.dealers.len() as u32);
remove_dealers(&mut deps, 0, 1000);
}
#[test]
fn dealers_pagination_works() {
let mut deps = init_contract();
let per_page = 2;
fill_dealers(&mut deps, 0, 1);
let page1 =
query_current_dealers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
// page should have 1 result on it
assert_eq!(1, page1.dealers.len());
remove_dealers(&mut deps, 0, 1);
fill_dealers(&mut deps, 0, 2);
// page1 should have 2 results on it
let page1 =
query_current_dealers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
assert_eq!(2, page1.dealers.len());
remove_dealers(&mut deps, 0, 2);
fill_dealers(&mut deps, 0, 3);
// page1 still has 2 results
let page1 =
query_current_dealers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
assert_eq!(2, page1.dealers.len());
// retrieving the next page should start after the last key on this page
let start_after = page1.start_next_after.unwrap();
let page2 = query_current_dealers_paged(
deps.as_ref(),
Option::from(start_after.to_string()),
Option::from(per_page),
)
.unwrap();
assert_eq!(1, page2.dealers.len());
remove_dealers(&mut deps, 0, 3);
fill_dealers(&mut deps, 0, 4);
let page1 =
query_current_dealers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
let start_after = page1.start_next_after.unwrap();
let page2 = query_current_dealers_paged(
deps.as_ref(),
Option::from(start_after.to_string()),
Option::from(per_page),
)
.unwrap();
// now we have 2 pages, with 2 results on the second page
assert_eq!(2, page2.dealers.len());
remove_dealers(&mut deps, 0, 4);
}
}
#[cfg(test)]
mod epoch_dealers {
use super::*;
#[test]
fn dealers_empty_on_init() {
let deps = init_contract();
// check few epochs
for epoch_id in 0..10 {
let page1 = query_epoch_dealers_paged(deps.as_ref(), epoch_id, None, None).unwrap();
assert_eq!(0, page1.dealers.len() as u32);
}
}
#[test]
fn theres_no_ovewriting_between_epochs() {
let mut deps = init_contract();
fill_dealers(&mut deps, 1, 1000);
let page1 = query_epoch_dealers_paged(deps.as_ref(), 1, None, None).unwrap();
assert!(!page1.dealers.is_empty());
// nothing for other epochs
let another_epoch = query_epoch_dealers_paged(deps.as_ref(), 2, None, None).unwrap();
assert!(another_epoch.dealers.is_empty());
let another_epoch = query_epoch_dealers_paged(deps.as_ref(), 42, None, None).unwrap();
assert!(another_epoch.dealers.is_empty());
}
#[test]
fn dealers_paged_retrieval_obeys_limits() {
let mut deps = init_contract();
let limit = 2;
fill_dealers(&mut deps, 0, 1000);
let page1 =
query_epoch_dealers_paged(deps.as_ref(), 0, None, Option::from(limit)).unwrap();
assert_eq!(limit, page1.dealers.len() as u32);
}
#[test]
fn dealers_paged_retrieval_has_default_limit() {
let mut deps = init_contract();
fill_dealers(&mut deps, 0, 1000);
// query without explicitly setting a limit
let page1 = query_epoch_dealers_paged(deps.as_ref(), 0, None, None).unwrap();
assert_eq!(DEALERS_PAGE_DEFAULT_LIMIT, page1.dealers.len() as u32);
}
#[test]
fn dealers_paged_retrieval_has_max_limit() {
let mut deps = init_contract();
// query with a crazily high limit in an attempt to use too many resources
let crazy_limit = 1000 * DEALERS_PAGE_MAX_LIMIT;
fill_dealers(&mut deps, 0, 1000);
let page1 =
query_epoch_dealers_paged(deps.as_ref(), 0, None, Option::from(crazy_limit))
.unwrap();
// we default to a decent sized upper bound instead
let expected_limit = DEALERS_PAGE_MAX_LIMIT;
assert_eq!(expected_limit, page1.dealers.len() as u32);
}
#[test]
fn dealers_pagination_works() {
let mut deps = init_contract();
let per_page = 2;
fill_dealers(&mut deps, 0, 1);
let page1 =
query_epoch_dealers_paged(deps.as_ref(), 0, None, Option::from(per_page)).unwrap();
// page should have 1 result on it
assert_eq!(1, page1.dealers.len());
remove_dealers(&mut deps, 0, 1);
fill_dealers(&mut deps, 0, 2);
// page1 should have 2 results on it
let page1 =
query_epoch_dealers_paged(deps.as_ref(), 0, None, Option::from(per_page)).unwrap();
assert_eq!(2, page1.dealers.len());
remove_dealers(&mut deps, 0, 2);
fill_dealers(&mut deps, 0, 3);
// page1 still has 2 results
let page1 =
query_epoch_dealers_paged(deps.as_ref(), 0, None, Option::from(per_page)).unwrap();
assert_eq!(2, page1.dealers.len());
// retrieving the next page should start after the last key on this page
let start_after = page1.start_next_after.unwrap();
let page2 = query_epoch_dealers_paged(
deps.as_ref(),
0,
Option::from(start_after.to_string()),
Option::from(per_page),
)
.unwrap();
assert_eq!(1, page2.dealers.len());
remove_dealers(&mut deps, 0, 3);
fill_dealers(&mut deps, 0, 4);
let page1 =
query_epoch_dealers_paged(deps.as_ref(), 0, None, Option::from(per_page)).unwrap();
let start_after = page1.start_next_after.unwrap();
let page2 = query_epoch_dealers_paged(
deps.as_ref(),
0,
Option::from(start_after.to_string()),
Option::from(per_page),
)
.unwrap();
// now we have 2 pages, with 2 results on the second page
assert_eq!(2, page2.dealers.len());
}
}
#[test]
fn dealers_paged_retrieval_obeys_limits() {
let mut deps = init_contract();
let limit = 2;
fill_dealers(&mut deps, 0, 1000);
let page1 = query_current_dealers_paged(deps.as_ref(), None, Option::from(limit)).unwrap();
assert_eq!(limit, page1.dealers.len() as u32);
remove_dealers(&mut deps, 0, 1000);
}
#[test]
fn dealers_paged_retrieval_has_default_limit() {
fn epoch_dealers_addresses() {
let mut deps = init_contract();
fill_dealers(&mut deps, 0, 1000);
let mut fixtures = Vec::new();
for i in 0..100 {
let mut dealer_details = dealer_details_fixture(&deps.api, i);
dealer_details.address = deps.api.addr_make(&format!("dummy-dealer-{i}"));
fixtures.push(dealer_details);
}
// query without explicitly setting a limit
let page1 = query_current_dealers_paged(deps.as_ref(), None, None).unwrap();
// initially empty for all epochs
for epoch_id in 0..10 {
let page1 =
query_epoch_dealers_addresses_paged(deps.as_ref(), epoch_id, None, None).unwrap();
assert_eq!(0, page1.dealers.len() as u32);
}
assert_eq!(DEALERS_PAGE_DEFAULT_LIMIT, page1.dealers.len() as u32);
// epoch0: dealers 0,1,2,3
// epoch1: dealers 4,5,6
// epoch2: dealers: 1,4,6 (some overlap)
// epoch3: dealer 7
// epoch4: dealers 0..100 (to check limits)
insert_dealer(deps.as_mut(), 0, &fixtures[0]);
insert_dealer(deps.as_mut(), 0, &fixtures[1]);
insert_dealer(deps.as_mut(), 0, &fixtures[2]);
insert_dealer(deps.as_mut(), 0, &fixtures[3]);
remove_dealers(&mut deps, 0, 1000);
}
insert_dealer(deps.as_mut(), 1, &fixtures[4]);
insert_dealer(deps.as_mut(), 1, &fixtures[5]);
insert_dealer(deps.as_mut(), 1, &fixtures[6]);
#[test]
fn dealers_paged_retrieval_has_max_limit() {
let mut deps = init_contract();
insert_dealer(deps.as_mut(), 2, &fixtures[1]);
insert_dealer(deps.as_mut(), 2, &fixtures[4]);
insert_dealer(deps.as_mut(), 2, &fixtures[6]);
// query with a crazily high limit in an attempt to use too many resources
let crazy_limit = 1000 * DEALERS_PAGE_MAX_LIMIT;
insert_dealer(deps.as_mut(), 3, &fixtures[7]);
fill_dealers(&mut deps, 0, 1000);
for fixture in &fixtures {
insert_dealer(deps.as_mut(), 4, fixture);
}
let page1 =
query_current_dealers_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap();
let res = query_epoch_dealers_addresses_paged(deps.as_ref(), 0, None, None).unwrap();
assert_eq!(4, res.dealers.len() as u32);
for fixture in &fixtures[0..=3] {
assert!(res.dealers.contains(&fixture.address))
}
// we default to a decent sized upper bound instead
let expected_limit = DEALERS_PAGE_MAX_LIMIT;
assert_eq!(expected_limit, page1.dealers.len() as u32);
let res = query_epoch_dealers_addresses_paged(deps.as_ref(), 1, None, None).unwrap();
assert_eq!(3, res.dealers.len() as u32);
for fixture in &fixtures[4..=6] {
assert!(res.dealers.contains(&fixture.address))
}
remove_dealers(&mut deps, 0, 1000);
}
let res = query_epoch_dealers_addresses_paged(deps.as_ref(), 2, None, None).unwrap();
assert_eq!(3, res.dealers.len() as u32);
for fixture in &[
fixtures[1].clone(),
fixtures[4].clone(),
fixtures[6].clone(),
] {
assert!(res.dealers.contains(&fixture.address))
}
#[test]
fn dealers_pagination_works() {
let mut deps = init_contract();
let res = query_epoch_dealers_addresses_paged(deps.as_ref(), 3, None, None).unwrap();
assert_eq!(vec![fixtures[7].address.clone()], res.dealers);
let per_page = 2;
let res = query_epoch_dealers_addresses_paged(deps.as_ref(), 4, None, None).unwrap();
assert_eq!(
storage::DEALERS_ADDRESSES_PAGE_DEFAULT_LIMIT,
res.dealers.len() as u32
);
fill_dealers(&mut deps, 0, 1);
let page1 =
query_current_dealers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
// page should have 1 result on it
assert_eq!(1, page1.dealers.len());
remove_dealers(&mut deps, 0, 1);
fill_dealers(&mut deps, 0, 2);
// page1 should have 2 results on it
let page1 =
query_current_dealers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
assert_eq!(2, page1.dealers.len());
remove_dealers(&mut deps, 0, 2);
fill_dealers(&mut deps, 0, 3);
// page1 still has 2 results
let page1 =
query_current_dealers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
assert_eq!(2, page1.dealers.len());
// retrieving the next page should start after the last key on this page
let start_after = page1.start_next_after.unwrap();
let page2 = query_current_dealers_paged(
deps.as_ref(),
Option::from(start_after.to_string()),
Option::from(per_page),
)
.unwrap();
assert_eq!(1, page2.dealers.len());
remove_dealers(&mut deps, 0, 3);
fill_dealers(&mut deps, 0, 4);
let page1 =
query_current_dealers_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
let start_after = page1.start_next_after.unwrap();
let page2 = query_current_dealers_paged(
deps.as_ref(),
Option::from(start_after.to_string()),
Option::from(per_page),
)
.unwrap();
// now we have 2 pages, with 2 results on the second page
assert_eq!(2, page2.dealers.len());
remove_dealers(&mut deps, 0, 4);
let res =
query_epoch_dealers_addresses_paged(deps.as_ref(), 4, None, Some(1000000)).unwrap();
assert_eq!(
storage::DEALERS_ADDRESSES_PAGE_MAX_LIMIT,
res.dealers.len() as u32
);
}
}
@@ -13,6 +13,9 @@ pub(crate) const DEALER_INDICES_PAGE_DEFAULT_LIMIT: u32 = 40;
pub(crate) const DEALERS_PAGE_MAX_LIMIT: u32 = 25;
pub(crate) const DEALERS_PAGE_DEFAULT_LIMIT: u32 = 10;
pub(crate) const DEALERS_ADDRESSES_PAGE_MAX_LIMIT: u32 = 50;
pub(crate) const DEALERS_ADDRESSES_PAGE_DEFAULT_LIMIT: u32 = 25;
pub(crate) const NODE_INDEX_COUNTER: Item<NodeIndex> = Item::new("node_index_counter");
pub(crate) const DEALERS_INDICES: Map<Dealer, NodeIndex> = Map::new("dealer_index");
@@ -4,12 +4,12 @@
use crate::dealers::storage::{
get_or_assign_index, is_dealer, save_dealer_details_if_not_a_dealer,
};
use crate::epoch_state::storage::CURRENT_EPOCH;
use crate::epoch_state::storage::{load_current_epoch, save_epoch};
use crate::epoch_state::utils::check_epoch_state;
use crate::error::ContractError;
use crate::state::storage::STATE;
use crate::Dealer;
use cosmwasm_std::{Deps, DepsMut, MessageInfo, Response, StdResult};
use cosmwasm_std::{Deps, DepsMut, Env, MessageInfo, Response};
use nym_coconut_dkg_common::dealer::DealerRegistrationDetails;
use nym_coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState};
@@ -28,13 +28,14 @@ fn ensure_group_member(deps: Deps, dealer: Dealer) -> Result<(), ContractError>
// for a recurring dealer just let it refresh the keys without having to do all the storage operations
pub fn try_add_dealer(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
bte_key_with_proof: EncodedBTEPublicKeyWithProof,
identity_key: String,
announce_address: String,
resharing: bool,
) -> Result<Response, ContractError> {
let epoch = CURRENT_EPOCH.load(deps.storage)?;
let epoch = load_current_epoch(deps.storage)?;
check_epoch_state(deps.storage, EpochState::PublicKeySubmission { resharing })?;
// make sure this potential dealer actually belong to the group
@@ -68,16 +69,16 @@ pub fn try_add_dealer(
);
// increment the number of registered dealers
CURRENT_EPOCH.update(deps.storage, |epoch| -> StdResult<_> {
let mut updated_epoch = epoch;
{
let current_epoch = load_current_epoch(deps.storage)?;
let mut updated_epoch = current_epoch;
updated_epoch.state_progress.registered_dealers += 1;
if is_resharing_dealer {
updated_epoch.state_progress.registered_resharing_dealers += 1;
}
Ok(updated_epoch)
})?;
save_epoch(deps.storage, env.block.height, &updated_epoch)?;
}
Ok(Response::new().add_attribute("node_index", node_index.to_string()))
}
@@ -115,10 +116,11 @@ pub(crate) mod tests {
.plus_seconds(TimeConfiguration::default().public_key_submission_time_secs);
add_fixture_dealer(deps.as_mut());
try_advance_epoch_state(deps.as_mut(), env).unwrap();
try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
let ret = try_add_dealer(
deps.as_mut(),
env,
info,
bte_key_with_proof,
identity,
@@ -5,7 +5,7 @@ use crate::dealers::storage::ensure_dealer;
use crate::dealings::storage::{
metadata_exists, must_read_metadata, store_metadata, StoredDealing,
};
use crate::epoch_state::storage::CURRENT_EPOCH;
use crate::epoch_state::storage::{load_current_epoch, save_epoch};
use crate::epoch_state::utils::check_epoch_state;
use crate::error::ContractError;
use crate::state::storage::STATE;
@@ -42,7 +42,7 @@ pub fn try_submit_dealings_metadata(
chunks: Vec<DealingChunkInfo>,
resharing: bool,
) -> Result<Response, ContractError> {
let epoch = CURRENT_EPOCH.load(deps.storage)?;
let epoch = load_current_epoch(deps.storage)?;
let state = STATE.load(deps.storage)?;
ensure_permission(deps.storage, &info.sender, epoch.epoch_id, resharing)?;
@@ -137,7 +137,7 @@ pub fn try_commit_dealings_chunk(
// note: checking permissions is implicit as if the metadata exists,
// the sender must have been allowed to submit it
let mut epoch = CURRENT_EPOCH.load(deps.storage)?;
let mut epoch = load_current_epoch(deps.storage)?;
// read meta
let mut metadata = must_read_metadata(
@@ -197,7 +197,7 @@ pub fn try_commit_dealings_chunk(
// there won't be a lot of them
if metadata.is_complete() {
epoch.state_progress.submitted_dealings += 1;
CURRENT_EPOCH.save(deps.storage, &epoch)?;
save_epoch(deps.storage, env.block.height, &epoch)?;
}
Ok(Response::new())
@@ -309,12 +309,10 @@ pub(crate) mod tests {
);
// same index, but next epoch
CURRENT_EPOCH
.update::<_, ContractError>(deps.as_mut().storage, |mut epoch| {
epoch.epoch_id += 1;
Ok(epoch)
})
.unwrap();
let mut epoch = load_current_epoch(&deps.storage).unwrap();
epoch.epoch_id += 1;
save_epoch(deps.as_mut().storage, epoch.epoch_id, &epoch).unwrap();
re_register_dealer(deps.as_mut(), &info.sender);
try_submit_dealings_metadata(
@@ -1,17 +1,19 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::epoch_state::storage::{CURRENT_EPOCH, EPOCH_THRESHOLDS, THRESHOLD};
use crate::epoch_state::storage::{
load_current_epoch, EPOCH_THRESHOLDS, HISTORICAL_EPOCH, THRESHOLD,
};
use crate::epoch_state::utils::check_state_completion;
use crate::error::ContractError;
use cosmwasm_std::{Env, Storage};
use cosmwasm_std::{Env, StdResult, Storage};
use nym_coconut_dkg_common::types::{Epoch, EpochId, EpochState, StateAdvanceResponse};
pub(crate) fn query_can_advance_state(
storage: &dyn Storage,
env: Env,
) -> Result<StateAdvanceResponse, ContractError> {
let epoch = CURRENT_EPOCH.load(storage)?;
let epoch = load_current_epoch(storage)?;
if epoch.state == EpochState::WaitingInitialisation {
return Ok(StateAdvanceResponse::default());
@@ -34,9 +36,14 @@ pub(crate) fn query_can_advance_state(
}
pub(crate) fn query_current_epoch(storage: &dyn Storage) -> Result<Epoch, ContractError> {
CURRENT_EPOCH
.load(storage)
.map_err(|_| ContractError::EpochNotInitialised)
load_current_epoch(storage).map_err(|_| ContractError::EpochNotInitialised)
}
pub(crate) fn query_epoch_at_height(
storage: &dyn Storage,
height: u64,
) -> StdResult<Option<Epoch>> {
HISTORICAL_EPOCH.may_load_at_height(storage, height)
}
pub(crate) fn query_current_epoch_threshold(
@@ -1,11 +1,225 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cw_storage_plus::{Item, Map};
use cosmwasm_std::{StdResult, Storage};
use cw_storage_plus::{Item, Map, SnapshotItem, Strategy};
use nym_coconut_dkg_common::types::{Epoch, EpochId};
#[deprecated]
// leave old values in storage for backwards compatibility, but make sure everything in the contract
// uses the new reference
pub(crate) const CURRENT_EPOCH: Item<Epoch> = Item::new("current_epoch");
pub const HISTORICAL_EPOCH: SnapshotItem<Epoch> = SnapshotItem::new(
"historical_epoch",
"historical_epoch__checkpoints",
"historical_epoch__changelog",
Strategy::EveryBlock,
);
pub const THRESHOLD: Item<u64> = Item::new("threshold");
pub const EPOCH_THRESHOLDS: Map<EpochId, u64> = Map::new("epoch_thresholds");
#[allow(deprecated)]
pub fn save_epoch(storage: &mut dyn Storage, height: u64, epoch: &Epoch) -> StdResult<()> {
CURRENT_EPOCH.save(storage, epoch)?;
HISTORICAL_EPOCH.save(storage, epoch, height)
}
#[allow(deprecated)]
pub fn load_current_epoch(storage: &dyn Storage) -> StdResult<Epoch> {
#[cfg(debug_assertions)]
{
let current = CURRENT_EPOCH.load(storage);
let historical = HISTORICAL_EPOCH.load(storage);
debug_assert_eq!(current, historical);
}
HISTORICAL_EPOCH.load(storage)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::epoch_state::transactions::{try_advance_epoch_state, try_initiate_dkg};
use crate::support::tests::helpers::{init_contract, ADMIN_ADDRESS};
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};
use cosmwasm_std::{Addr, Env};
use nym_coconut_dkg_common::types::EpochState;
use std::ops::{Deref, DerefMut};
#[test]
fn full_dkg_correctly_updates_historical_epoch() -> anyhow::Result<()> {
struct EnvWrapper {
env: Env,
}
impl EnvWrapper {
fn next_block(&mut self) {
self.env.block.height += 1;
self.env.block.time = self.env.block.time.plus_seconds(5);
}
fn height(&self) -> u64 {
self.block.height
}
}
impl Deref for EnvWrapper {
type Target = Env;
fn deref(&self) -> &Self::Target {
&self.env
}
}
impl DerefMut for EnvWrapper {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.env
}
}
let mut empty_deps = mock_dependencies();
// before contract is initialised, there's nothing saved
assert!(HISTORICAL_EPOCH
.may_load(empty_deps.as_mut().storage)?
.is_none());
let mut deps = init_contract();
let mut env = EnvWrapper { env: mock_env() };
let init_height = env.height();
// after init it has initial state
assert_eq!(HISTORICAL_EPOCH.load(deps.as_mut().storage)?.epoch_id, 0);
assert_eq!(
HISTORICAL_EPOCH.load(deps.as_mut().storage)?.state,
EpochState::WaitingInitialisation
);
env.next_block();
let pub_key_submission_height = env.height();
try_initiate_dkg(
deps.as_mut(),
(*env).clone(),
message_info(&Addr::unchecked(ADMIN_ADDRESS), &[]),
)?;
assert_eq!(
HISTORICAL_EPOCH.load(deps.as_mut().storage)?.state,
EpochState::PublicKeySubmission { resharing: false }
);
env.block.time = env.block.time.plus_seconds(100000);
env.next_block();
let dealing_exchange_height = env.height();
try_advance_epoch_state(deps.as_mut(), (*env).clone())?;
assert_eq!(
HISTORICAL_EPOCH.load(deps.as_mut().storage)?.state,
EpochState::DealingExchange { resharing: false }
);
env.block.time = env.block.time.plus_seconds(100000);
env.next_block();
let verification_key_submission_height = env.height();
try_advance_epoch_state(deps.as_mut(), (*env).clone())?;
assert_eq!(
HISTORICAL_EPOCH.load(deps.as_mut().storage)?.state,
EpochState::VerificationKeySubmission { resharing: false }
);
env.block.time = env.block.time.plus_seconds(100000);
env.next_block();
let verification_key_validation_height = env.height();
try_advance_epoch_state(deps.as_mut(), (*env).clone())?;
assert_eq!(
HISTORICAL_EPOCH.load(deps.as_mut().storage)?.state,
EpochState::VerificationKeyValidation { resharing: false }
);
env.block.time = env.block.time.plus_seconds(100000);
env.next_block();
let verification_key_finalization_height = env.height();
try_advance_epoch_state(deps.as_mut(), (*env).clone())?;
assert_eq!(
HISTORICAL_EPOCH.load(deps.as_mut().storage)?.state,
EpochState::VerificationKeyFinalization { resharing: false }
);
env.block.time = env.block.time.plus_seconds(100000);
env.next_block();
let in_progress_height = env.height();
try_advance_epoch_state(deps.as_mut(), (*env).clone())?;
assert_eq!(
HISTORICAL_EPOCH.load(deps.as_mut().storage)?.state,
EpochState::InProgress {}
);
// check old data
assert!(HISTORICAL_EPOCH
.may_load_at_height(deps.as_mut().storage, init_height - 1)?
.is_none());
assert_eq!(
HISTORICAL_EPOCH
.may_load_at_height(deps.as_mut().storage, init_height + 1)?
.unwrap()
.state,
EpochState::WaitingInitialisation
);
assert_eq!(
HISTORICAL_EPOCH
.may_load_at_height(deps.as_mut().storage, pub_key_submission_height + 1)?
.unwrap()
.state,
EpochState::PublicKeySubmission { resharing: false }
);
assert_eq!(
HISTORICAL_EPOCH
.may_load_at_height(deps.as_mut().storage, dealing_exchange_height + 1)?
.unwrap()
.state,
EpochState::DealingExchange { resharing: false }
);
assert_eq!(
HISTORICAL_EPOCH
.may_load_at_height(
deps.as_mut().storage,
verification_key_submission_height + 1
)?
.unwrap()
.state,
EpochState::VerificationKeySubmission { resharing: false }
);
assert_eq!(
HISTORICAL_EPOCH
.may_load_at_height(
deps.as_mut().storage,
verification_key_validation_height + 1
)?
.unwrap()
.state,
EpochState::VerificationKeyValidation { resharing: false }
);
assert_eq!(
HISTORICAL_EPOCH
.may_load_at_height(
deps.as_mut().storage,
verification_key_finalization_height + 1
)?
.unwrap()
.state,
EpochState::VerificationKeyFinalization { resharing: false }
);
assert_eq!(
HISTORICAL_EPOCH
.may_load_at_height(deps.as_mut().storage, in_progress_height + 1)?
.unwrap()
.state,
EpochState::InProgress
);
Ok(())
}
}
@@ -1,7 +1,7 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::epoch_state::storage::{CURRENT_EPOCH, EPOCH_THRESHOLDS, THRESHOLD};
use crate::epoch_state::storage::{load_current_epoch, save_epoch, EPOCH_THRESHOLDS, THRESHOLD};
use crate::epoch_state::transactions::reset_dkg_state;
use crate::epoch_state::utils::check_state_completion;
use crate::error::ContractError;
@@ -39,7 +39,7 @@ fn ensure_can_advance_state(
pub fn try_advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Response, ContractError> {
// TODO: the only case where this can retrigger itself is when insufficient number of parties completed it, i.e. we don't have threshold
let current_epoch = CURRENT_EPOCH.load(deps.storage)?;
let current_epoch = load_current_epoch(deps.storage)?;
// checks whether the given phase has either completed or reached its deadline
ensure_can_advance_state(deps.as_ref(), &env, &current_epoch)?;
@@ -82,7 +82,7 @@ pub fn try_advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Response,
};
// update the epoch state
CURRENT_EPOCH.save(deps.storage, &next_epoch)?;
save_epoch(deps.storage, env.block.height, &next_epoch)?;
Ok(Response::new())
}
@@ -90,23 +90,33 @@ pub fn try_advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result<Response,
#[cfg(test)]
mod tests {
use super::*;
use crate::epoch_state::storage::load_current_epoch;
use crate::epoch_state::transactions::try_initiate_dkg;
use crate::epoch_state::utils::check_epoch_state;
use crate::error::ContractError::EarlyEpochStateAdvancement;
use crate::state::storage::STATE;
use crate::support::tests::helpers::{init_contract, ADMIN_ADDRESS};
use cosmwasm_std::testing::{message_info, mock_env};
use cosmwasm_std::{Addr, StdResult, Storage};
use cosmwasm_std::{Addr, Storage};
use nym_coconut_dkg_common::types::TimeConfiguration;
fn update_epoch<A>(storage: &mut dyn Storage, env: &Env, action: A)
where
A: Fn(Epoch) -> Epoch,
{
let current = load_current_epoch(storage).unwrap();
let updated = action(current);
save_epoch(storage, env.block.height, &updated).unwrap();
}
#[test]
fn short_circuit_advance_state() {
fn epoch_in_state(state: EpochState, env: &Env) -> Epoch {
Epoch::new(state, 0, Default::default(), env.block.time)
}
fn set_epoch(storage: &mut dyn Storage, epoch: Epoch) {
CURRENT_EPOCH.save(storage, &epoch).unwrap();
fn set_epoch(storage: &mut dyn Storage, env: &Env, epoch: Epoch) {
save_epoch(storage, env.block.height, &epoch).unwrap();
}
let mut deps = init_contract();
@@ -114,18 +124,18 @@ mod tests {
// it's never possible to short-circuit `WaitingInitialisation`
let epoch = epoch_in_state(EpochState::WaitingInitialisation, &env);
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
// neither PublicKeySubmission (in either resharing or non-resharing)
let epoch = epoch_in_state(EpochState::PublicKeySubmission { resharing: false }, &env);
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
let epoch = epoch_in_state(EpochState::PublicKeySubmission { resharing: true }, &env);
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
@@ -138,7 +148,7 @@ mod tests {
// no dealings
let mut epoch = epoch_in_state(EpochState::DealingExchange { resharing: false }, &env);
epoch.state_progress.registered_dealers = 5;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
@@ -146,7 +156,7 @@ mod tests {
let mut epoch = epoch_in_state(EpochState::DealingExchange { resharing: false }, &env);
epoch.state_progress.registered_dealers = 5;
epoch.state_progress.submitted_dealings = 5;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
@@ -154,7 +164,7 @@ mod tests {
let mut epoch = epoch_in_state(EpochState::DealingExchange { resharing: false }, &env);
epoch.state_progress.registered_dealers = 5;
epoch.state_progress.submitted_dealings = key_size * 5;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_ok());
check_epoch_state(
@@ -167,7 +177,7 @@ mod tests {
let mut epoch = epoch_in_state(EpochState::DealingExchange { resharing: true }, &env);
epoch.state_progress.registered_dealers = 5;
epoch.state_progress.registered_resharing_dealers = 4;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
@@ -176,7 +186,7 @@ mod tests {
epoch.state_progress.registered_dealers = 5;
epoch.state_progress.registered_resharing_dealers = 4;
epoch.state_progress.submitted_dealings = 5;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
@@ -185,7 +195,7 @@ mod tests {
epoch.state_progress.registered_dealers = 5;
epoch.state_progress.registered_resharing_dealers = 4;
epoch.state_progress.submitted_dealings = key_size * 4;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_ok());
check_epoch_state(
@@ -200,7 +210,7 @@ mod tests {
&env,
);
epoch.state_progress.registered_dealers = 5;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
@@ -209,7 +219,7 @@ mod tests {
&env,
);
epoch.state_progress.registered_dealers = 5;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
@@ -219,7 +229,7 @@ mod tests {
);
epoch.state_progress.registered_dealers = 5;
epoch.state_progress.submitted_key_shares = 4;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
@@ -229,7 +239,7 @@ mod tests {
);
epoch.state_progress.registered_dealers = 5;
epoch.state_progress.submitted_key_shares = 4;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
@@ -239,7 +249,7 @@ mod tests {
);
epoch.state_progress.registered_dealers = 5;
epoch.state_progress.submitted_key_shares = 5;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_ok());
check_epoch_state(
@@ -254,7 +264,7 @@ mod tests {
);
epoch.state_progress.registered_dealers = 5;
epoch.state_progress.submitted_key_shares = 5;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_ok());
check_epoch_state(
@@ -268,7 +278,7 @@ mod tests {
EpochState::VerificationKeyValidation { resharing: false },
&env,
);
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
@@ -276,7 +286,7 @@ mod tests {
EpochState::VerificationKeyValidation { resharing: true },
&env,
);
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
@@ -286,7 +296,7 @@ mod tests {
&env,
);
epoch.state_progress.submitted_key_shares = 5;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
@@ -295,7 +305,7 @@ mod tests {
&env,
);
epoch.state_progress.submitted_key_shares = 5;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
@@ -305,7 +315,7 @@ mod tests {
);
epoch.state_progress.submitted_key_shares = 5;
epoch.state_progress.verified_keys = 4;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
@@ -315,7 +325,7 @@ mod tests {
);
epoch.state_progress.submitted_key_shares = 5;
epoch.state_progress.verified_keys = 4;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
@@ -325,7 +335,7 @@ mod tests {
);
epoch.state_progress.submitted_key_shares = 5;
epoch.state_progress.verified_keys = 5;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_ok());
check_epoch_state(deps.as_ref().storage, EpochState::InProgress).unwrap();
@@ -336,14 +346,14 @@ mod tests {
);
epoch.state_progress.submitted_key_shares = 5;
epoch.state_progress.verified_keys = 5;
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_ok());
check_epoch_state(deps.as_ref().storage, EpochState::InProgress).unwrap();
// it's never possible to short-circuit `InProgress`
let epoch = epoch_in_state(EpochState::InProgress, &env);
set_epoch(deps.as_mut().storage, epoch);
set_epoch(deps.as_mut().storage, &env, epoch);
let res = try_advance_epoch_state(deps.as_mut(), env.clone());
assert!(res.is_err());
}
@@ -366,7 +376,7 @@ mod tests {
)
.unwrap();
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
let epoch = load_current_epoch(deps.as_mut().storage).unwrap();
assert_eq!(
epoch.state,
EpochState::PublicKeySubmission { resharing: false }
@@ -390,19 +400,16 @@ mod tests {
env.block.time = env.block.time.plus_seconds(1);
// add some dealers to prevent short-circuiting
CURRENT_EPOCH
.update(deps.as_mut().storage, |mut e| -> StdResult<_> {
e.state_progress.registered_dealers = 42;
Ok(e)
})
.unwrap();
update_epoch(deps.as_mut().storage, &env, |mut e| {
e.state_progress.registered_dealers = 42;
e
});
env.block.time = env
.block
.time
.plus_seconds(epoch.time_configuration.public_key_submission_time_secs);
try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
let epoch = load_current_epoch(deps.as_mut().storage).unwrap();
assert_eq!(
epoch.state,
EpochState::DealingExchange { resharing: false }
@@ -425,7 +432,7 @@ mod tests {
env.block.time = env.block.time.plus_seconds(3);
try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
let epoch = load_current_epoch(deps.as_mut().storage).unwrap();
assert_eq!(
epoch.state,
EpochState::VerificationKeySubmission { resharing: false }
@@ -452,7 +459,7 @@ mod tests {
env.block.time = env.block.time.plus_seconds(3);
try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
let epoch = load_current_epoch(deps.as_mut().storage).unwrap();
assert_eq!(
epoch.state,
EpochState::VerificationKeyValidation { resharing: false }
@@ -478,16 +485,13 @@ mod tests {
);
// add some key shares to prevent short-circuiting
CURRENT_EPOCH
.update(deps.as_mut().storage, |mut e| -> StdResult<_> {
e.state_progress.submitted_key_shares = 42;
Ok(e)
})
.unwrap();
update_epoch(deps.as_mut().storage, &env, |mut e| {
e.state_progress.submitted_key_shares = 42;
e
});
env.block.time = env.block.time.plus_seconds(3);
try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
let epoch = load_current_epoch(deps.as_mut().storage).unwrap();
assert_eq!(
epoch.state,
EpochState::VerificationKeyFinalization { resharing: false }
@@ -512,16 +516,14 @@ mod tests {
);
// add some finalized keys to prevent reset
CURRENT_EPOCH
.update(deps.as_mut().storage, |mut e| -> StdResult<_> {
e.state_progress.verified_keys = 42;
Ok(e)
})
.unwrap();
update_epoch(deps.as_mut().storage, &env, |mut e| {
e.state_progress.verified_keys = 42;
e
});
env.block.time = env.block.time.plus_seconds(1);
try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
let epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
let epoch = load_current_epoch(deps.as_mut().storage).unwrap();
assert_eq!(epoch.state, EpochState::InProgress);
assert_eq!(
epoch.deadline.unwrap(),
@@ -547,9 +549,9 @@ mod tests {
// Group hasn't changed, so we remain in the same epoch, with updated finish timestamp
env.block.time = env.block.time.plus_seconds(100);
let prev_epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
let prev_epoch = load_current_epoch(deps.as_mut().storage).unwrap();
try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
let curr_epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
let curr_epoch = load_current_epoch(deps.as_mut().storage).unwrap();
let mut expected_epoch = Epoch::new(
EpochState::InProgress,
prev_epoch.epoch_id,
@@ -570,11 +572,11 @@ mod tests {
// fewer than the threshold
epoch.state_progress.verified_keys = 41;
CURRENT_EPOCH.save(deps.as_mut().storage, &epoch).unwrap();
save_epoch(deps.as_mut().storage, env.block.height, &epoch).unwrap();
env.block.time = env.block.time.plus_seconds(5000000);
try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
let curr_epoch = CURRENT_EPOCH.load(deps.as_mut().storage).unwrap();
let curr_epoch = load_current_epoch(deps.as_mut().storage).unwrap();
let expected_epoch = Epoch::new(
EpochState::PublicKeySubmission { resharing: false },
epoch.epoch_id + 1,
@@ -598,12 +600,10 @@ mod tests {
assert!(THRESHOLD.may_load(deps.as_mut().storage).unwrap().is_none());
CURRENT_EPOCH
.update(deps.as_mut().storage, |mut e| -> StdResult<_> {
e.state_progress.registered_dealers = 100;
Ok(e)
})
.unwrap();
update_epoch(deps.as_mut().storage, &env, |mut e| {
e.state_progress.registered_dealers = 100;
e
});
env.block.time = env
.block
@@ -1,7 +1,7 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::epoch_state::storage::{CURRENT_EPOCH, THRESHOLD};
use crate::epoch_state::storage::{load_current_epoch, save_epoch, THRESHOLD};
use crate::error::ContractError;
use crate::state::storage::DKG_ADMIN;
use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Storage};
@@ -29,7 +29,7 @@ pub(crate) fn try_initiate_dkg(
// only the admin is allowed to kick start the process
DKG_ADMIN.assert_admin(deps.as_ref(), &info.sender)?;
let epoch = CURRENT_EPOCH.load(deps.storage)?;
let epoch = load_current_epoch(deps.storage)?;
if !matches!(epoch.state, EpochState::WaitingInitialisation) {
return Err(ContractError::AlreadyInitialised);
}
@@ -37,7 +37,7 @@ pub(crate) fn try_initiate_dkg(
// the first exchange won't involve resharing
let initial_state = EpochState::PublicKeySubmission { resharing: false };
let initial_epoch = Epoch::new(initial_state, 0, epoch.time_configuration, env.block.time);
CURRENT_EPOCH.save(deps.storage, &initial_epoch)?;
save_epoch(deps.storage, env.block.height, &initial_epoch)?;
Ok(Response::default())
}
@@ -49,7 +49,7 @@ pub(crate) fn try_trigger_reset(
) -> Result<Response, ContractError> {
// only the admin is allowed to trigger DKG reset
DKG_ADMIN.assert_admin(deps.as_ref(), &info.sender)?;
let current_epoch = CURRENT_EPOCH.load(deps.storage)?;
let current_epoch = load_current_epoch(deps.storage)?;
// only allow reset when the DKG exchange isn't in progress
if !current_epoch.state.is_in_progress() {
@@ -57,7 +57,7 @@ pub(crate) fn try_trigger_reset(
}
let next_epoch = current_epoch.next_reset(env.block.time);
CURRENT_EPOCH.save(deps.storage, &next_epoch)?;
save_epoch(deps.storage, env.block.height, &next_epoch)?;
reset_dkg_state(deps.storage)?;
@@ -71,7 +71,7 @@ pub(crate) fn try_trigger_resharing(
) -> Result<Response, ContractError> {
// only the admin is allowed to trigger DKG resharing
DKG_ADMIN.assert_admin(deps.as_ref(), &info.sender)?;
let current_epoch = CURRENT_EPOCH.load(deps.storage)?;
let current_epoch = load_current_epoch(deps.storage)?;
// only allow resharing when the DKG exchange isn't in progress
if !current_epoch.state.is_in_progress() {
@@ -79,7 +79,7 @@ pub(crate) fn try_trigger_resharing(
}
let next_epoch = current_epoch.next_resharing(env.block.time);
CURRENT_EPOCH.save(deps.storage, &next_epoch)?;
save_epoch(deps.storage, env.block.height, &next_epoch)?;
reset_dkg_state(deps.storage)?;
@@ -89,6 +89,7 @@ pub(crate) fn try_trigger_resharing(
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use crate::epoch_state::storage::load_current_epoch;
use crate::support::tests::helpers::{init_contract, ADMIN_ADDRESS};
use cosmwasm_std::testing::{message_info, mock_env};
use cosmwasm_std::Addr;
@@ -99,7 +100,7 @@ pub(crate) mod tests {
let mut deps = init_contract();
let env = mock_env();
let initial_epoch_info = CURRENT_EPOCH.load(&deps.storage).unwrap();
let initial_epoch_info = load_current_epoch(&deps.storage).unwrap();
assert!(initial_epoch_info.deadline.is_none());
let not_admin = deps.api.addr_make("not an admin");
@@ -125,7 +126,7 @@ pub(crate) mod tests {
assert_eq!(ContractError::AlreadyInitialised, res);
// sets the correct epoch data
let epoch = CURRENT_EPOCH.load(&deps.storage).unwrap();
let epoch = load_current_epoch(&deps.storage).unwrap();
assert_eq!(epoch.epoch_id, 0);
assert_eq!(
epoch.state,
@@ -1,7 +1,7 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::epoch_state::storage::CURRENT_EPOCH;
use crate::epoch_state::storage::load_current_epoch;
use crate::error::ContractError;
use crate::state::storage::STATE;
use cosmwasm_std::Storage;
@@ -52,7 +52,7 @@ pub(crate) fn check_epoch_state(
storage: &dyn Storage,
against: EpochState,
) -> Result<(), ContractError> {
let epoch_state = CURRENT_EPOCH.load(storage)?.state;
let epoch_state = load_current_epoch(storage)?.state;
if epoch_state != against {
Err(ContractError::IncorrectEpochState {
current_state: epoch_state.to_string(),
@@ -66,6 +66,7 @@ pub(crate) fn check_epoch_state(
#[cfg(test)]
pub(crate) mod test {
use super::*;
use crate::epoch_state::storage::save_epoch;
use crate::support::tests::helpers::init_contract;
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::Timestamp;
@@ -210,12 +211,12 @@ pub(crate) mod test {
let env = mock_env();
for fixed_state in EpochState::first().all_until(EpochState::InProgress) {
CURRENT_EPOCH
.save(
deps.as_mut().storage,
&Epoch::new(fixed_state, 0, TimeConfiguration::default(), env.block.time),
)
.unwrap();
save_epoch(
deps.as_mut().storage,
env.block.height,
&Epoch::new(fixed_state, 0, TimeConfiguration::default(), env.block.time),
)
.unwrap();
for against_state in EpochState::first().all_until(EpochState::InProgress) {
let ret = check_epoch_state(deps.as_mut().storage, against_state);
if fixed_state == against_state {
+3
View File
@@ -10,6 +10,9 @@ use thiserror::Error;
/// Custom errors for contract failure conditions.
#[derive(Error, Debug, PartialEq)]
pub enum ContractError {
#[error("could not perform contract migration: {comment}")]
FailedMigration { comment: String },
#[error(transparent)]
Std(#[from] StdError),
+1
View File
@@ -11,6 +11,7 @@ mod dealers;
mod dealings;
mod epoch_state;
pub mod error;
mod queued_migrations;
mod state;
mod support;
mod verification_key_shares;
@@ -0,0 +1,21 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::epoch_state::storage::HISTORICAL_EPOCH;
use crate::error::ContractError;
use cosmwasm_std::{DepsMut, Env};
pub fn introduce_historical_epochs(deps: DepsMut, env: Env) -> Result<(), ContractError> {
if HISTORICAL_EPOCH.may_load(deps.storage)?.is_some() {
return Err(ContractError::FailedMigration {
comment: "this migration has already been run before".to_string(),
});
}
#[allow(deprecated)]
let current = crate::epoch_state::storage::CURRENT_EPOCH.load(deps.storage)?;
// we won't have information on intermediate states prior to now, but that's not the end of the world
HISTORICAL_EPOCH.save(deps.storage, &current, env.block.height)?;
Ok(())
}
@@ -12,8 +12,8 @@ pub const TEST_MIX_DENOM: &str = "unym";
pub fn vk_share_fixture(owner: &str, index: u64) -> ContractVKShare {
ContractVKShare {
share: format!("share{}", index),
announce_address: format!("localhost:{}", index),
share: format!("share{index}"),
announce_address: format!("localhost:{index}"),
node_index: index,
owner: Addr::unchecked(owner),
epoch_id: index,
@@ -43,7 +43,7 @@ pub fn dealing_metadata_fixture() -> Vec<DealingChunkInfo> {
pub fn dealer_details_fixture(api: &MockApi, assigned_index: u64) -> DealerDetails {
DealerDetails {
address: api.addr_make(&format!("owner{}", assigned_index)),
address: api.addr_make(&format!("owner{assigned_index}")),
bte_public_key_with_proof: "".to_string(),
ed25519_identity: "".to_string(),
announce_address: "".to_string(),
@@ -3,7 +3,7 @@
use crate::contract::instantiate;
use crate::dealers::storage::{DEALERS_INDICES, EPOCH_DEALERS_MAP};
use crate::epoch_state::storage::CURRENT_EPOCH;
use crate::epoch_state::storage::load_current_epoch;
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env, MockApi, MockQuerier};
use cosmwasm_std::{
from_json, to_json_binary, Addr, ContractResult, DepsMut, Empty, MemoryStorage, OwnedDeps,
@@ -27,7 +27,7 @@ pub const MULTISIG_CONTRACT: &str = addr!("multisig contract address");
pub(crate) static GROUP_MEMBERS: Mutex<Vec<(Member, u64)>> = Mutex::new(Vec::new());
pub fn re_register_dealer(deps: DepsMut, dealer: &Addr) {
let epoch_id = CURRENT_EPOCH.load(deps.storage).unwrap().epoch_id;
let epoch_id = load_current_epoch(deps.storage).unwrap().epoch_id;
let previous = epoch_id - 1;
let details = EPOCH_DEALERS_MAP
.load(deps.storage, (previous, dealer))
@@ -38,7 +38,7 @@ pub fn re_register_dealer(deps: DepsMut, dealer: &Addr) {
}
pub fn add_current_dealer(deps: DepsMut<'_>, details: &DealerDetails) {
let epoch_id = CURRENT_EPOCH.load(deps.storage).unwrap().epoch_id;
let epoch_id = load_current_epoch(deps.storage).unwrap().epoch_id;
insert_dealer(deps, epoch_id, details)
}
@@ -99,7 +99,7 @@ pub(crate) mod tests {
let mut deps = init_contract();
let limit = 2;
for n in 0..1000 {
let owner = format!("owner{}", n);
let owner = format!("owner{n}");
let vk_share = vk_share_fixture(&owner, 0);
let sender = Addr::unchecked(owner);
vk_shares()
@@ -115,7 +115,7 @@ pub(crate) mod tests {
fn vk_shares_paged_retrieval_has_default_limit() {
let mut deps = init_contract();
for n in 0..1000 {
let owner = format!("owner{}", n);
let owner = format!("owner{n}");
let vk_share = vk_share_fixture(&owner, 0);
let sender = Addr::unchecked(owner);
vk_shares()
@@ -136,7 +136,7 @@ pub(crate) mod tests {
fn vk_shares_paged_retrieval_has_max_limit() {
let mut deps = init_contract();
for n in 0..1000 {
let owner = format!("owner{}", n);
let owner = format!("owner{n}");
let vk_share = vk_share_fixture(&owner, 0);
let sender = Addr::unchecked(owner);
vk_shares()
@@ -3,7 +3,7 @@
use crate::constants::BLOCK_TIME_FOR_VERIFICATION_SECS;
use crate::dealers::storage::get_dealer_details;
use crate::epoch_state::storage::CURRENT_EPOCH;
use crate::epoch_state::storage::{load_current_epoch, save_epoch};
use crate::epoch_state::utils::check_epoch_state;
use crate::error::ContractError;
use crate::state::storage::{MULTISIG, STATE};
@@ -25,7 +25,7 @@ pub fn try_commit_verification_key_share(
deps.storage,
EpochState::VerificationKeySubmission { resharing },
)?;
let mut epoch = CURRENT_EPOCH.load(deps.storage)?;
let mut epoch = load_current_epoch(deps.storage)?;
let epoch_id = epoch.epoch_id;
let details = get_dealer_details(deps.storage, &info.sender, epoch_id)?;
@@ -43,7 +43,7 @@ pub fn try_commit_verification_key_share(
node_index: details.assigned_index,
announce_address: details.announce_address,
owner: info.sender.clone(),
epoch_id: CURRENT_EPOCH.load(deps.storage)?.epoch_id,
epoch_id: load_current_epoch(deps.storage)?.epoch_id,
verified: false,
};
vk_shares().save(deps.storage, (&info.sender, epoch_id), &data)?;
@@ -60,13 +60,14 @@ pub fn try_commit_verification_key_share(
)?;
epoch.state_progress.submitted_key_shares += 1;
CURRENT_EPOCH.save(deps.storage, &epoch)?;
save_epoch(deps.storage, env.block.height, &epoch)?;
Ok(Response::new().add_message(msg))
}
pub fn try_verify_verification_key_share(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
owner: String,
resharing: bool,
@@ -77,7 +78,7 @@ pub fn try_verify_verification_key_share(
deps.storage,
EpochState::VerificationKeyFinalization { resharing },
)?;
let mut epoch = CURRENT_EPOCH.load(deps.storage)?;
let mut epoch = load_current_epoch(deps.storage)?;
let epoch_id = epoch.epoch_id;
MULTISIG.assert_admin(deps.as_ref(), &info.sender)?;
@@ -93,7 +94,7 @@ pub fn try_verify_verification_key_share(
})?;
epoch.state_progress.verified_keys += 1;
CURRENT_EPOCH.save(deps.storage, &epoch)?;
save_epoch(deps.storage, env.block.height, &epoch)?;
Ok(Response::default())
}
@@ -259,9 +260,14 @@ mod tests {
let owner = deps.api.addr_make("owner").to_string();
let multisig_info = message_info(&Addr::unchecked(MULTISIG_CONTRACT), &[]);
let ret =
try_verify_verification_key_share(deps.as_mut(), info.clone(), owner.clone(), false)
.unwrap_err();
let ret = try_verify_verification_key_share(
deps.as_mut(),
env.clone(),
info.clone(),
owner.clone(),
false,
)
.unwrap_err();
assert_eq!(
ret,
ContractError::IncorrectEpochState {
@@ -291,15 +297,26 @@ mod tests {
.block
.time
.plus_seconds(TimeConfiguration::default().verification_key_validation_time_secs);
try_advance_epoch_state(deps.as_mut(), env).unwrap();
try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
let ret = try_verify_verification_key_share(deps.as_mut(), info, owner.clone(), false)
.unwrap_err();
let ret = try_verify_verification_key_share(
deps.as_mut(),
env.clone(),
info,
owner.clone(),
false,
)
.unwrap_err();
assert_eq!(ret, ContractError::Admin(AdminError::NotAdmin {}));
let ret =
try_verify_verification_key_share(deps.as_mut(), multisig_info, owner.clone(), false)
.unwrap_err();
let ret = try_verify_verification_key_share(
deps.as_mut(),
env.clone(),
multisig_info,
owner.clone(),
false,
)
.unwrap_err();
assert_eq!(
ret,
ContractError::NoCommitForOwner {
@@ -356,9 +373,15 @@ mod tests {
.block
.time
.plus_seconds(TimeConfiguration::default().verification_key_validation_time_secs);
try_advance_epoch_state(deps.as_mut(), env).unwrap();
try_advance_epoch_state(deps.as_mut(), env.clone()).unwrap();
try_verify_verification_key_share(deps.as_mut(), multisig_info, owner.to_string(), false)
.unwrap();
try_verify_verification_key_share(
deps.as_mut(),
env,
multisig_info,
owner.to_string(),
false,
)
.unwrap();
}
}
+1
View File
@@ -104,6 +104,7 @@ allow = [
"Unicode-3.0",
"OpenSSL",
"Zlib",
"CDLA-Permissive-2.0",
]
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
@@ -7,20 +7,32 @@ net.ipv6.conf.all.forwarding = 1
net.ipv4.ip_forward = 1
IP forwarding configured successfully.
Creating Nym exit policy chain...
Creating chain NYM-EXIT...
Creating chain NYM-EXIT in ip6tables...
Linking NYM-EXIT to FORWARD chain...
Linking NYM-EXIT to IPv6 FORWARD chain...
Chain NYM-EXIT already exists. Flushing it...
Chain NYM-EXIT already exists in ip6tables. Flushing it...
NYM-EXIT all opt -- in * out nymwg 0.0.0.0/0 -> 0.0.0.0/0
NYM-EXIT all opt in * out nymwg ::/0 -> ::/0
Setting up NAT rules...
MASQUERADE all opt -- in * out ens3 0.0.0.0/0 -> 0.0.0.0/0
IPv4 NAT rule already exists.
MASQUERADE all opt in * out ens3 ::/0 -> ::/0
IPv6 NAT rule already exists.
ACCEPT all opt -- in nymwg out ens3 0.0.0.0/0 -> 0.0.0.0/0
ACCEPT all opt -- in ens3 out nymwg 0.0.0.0/0 -> 0.0.0.0/0 state RELATED,ESTABLISHED
ACCEPT all opt in nymwg out ens3 ::/0 -> ::/0
ACCEPT all opt in ens3 out nymwg ::/0 -> ::/0 state RELATED,ESTABLISHED
Configuring DNS and ICMP rules...
Added IPv6 ICMP rule (allow ping6).
Added IPv6 DNS rule (UDP).
Added IPv6 DNS rule (TCP).
ACCEPT icmp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 icmptype 8
ACCEPT icmp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 icmptype 0
ACCEPT icmpv6 opt in * out * ::/0 -> ::/0
ACCEPT udp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 udp dpt:53
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:53
ACCEPT udp opt in * out * ::/0 -> ::/0 udp dpt:53
ACCEPT tcp opt in * out * ::/0 -> ::/0 tcp dpt:53
Applying Spamhaus blocklist...
Downloading exit policy from https://nymtech.net/.wellknown/network-requester/exit-policy.txt
Processing 429 blocklist rules...
REJECT all opt -- in * out * 0.0.0.0/0 -> 205.189.71.0/24 reject-with icmp-port-unreachable
REJECT all opt -- in * out * 0.0.0.0/0 -> 205.189.72.0/23 reject-with icmp-port-unreachable
Blocklist applied successfully.
Applying allowed ports...
Adding rules for SILC (Port: 706)
@@ -103,6 +115,11 @@ Adding rules for POP3OverTLS (Port: 995)
Added: NYM-EXIT tcp port 995
Added: NYM-EXIT udp port 995
Added: NYM-EXIT udp port 995
Adding rules for DarkFiTor (Port: 25551)
Added: NYM-EXIT tcp port 25551
Added: NYM-EXIT tcp port 25551
Added: NYM-EXIT udp port 25551
Added: NYM-EXIT udp port 25551
Adding rules for MMCC (Port: 5050)
Added: NYM-EXIT tcp port 5050
Added: NYM-EXIT tcp port 5050
@@ -268,6 +285,11 @@ Adding rules for Mumble (Port: 64738)
Added: NYM-EXIT tcp port 64738
Added: NYM-EXIT udp port 64738
Added: NYM-EXIT udp port 64738
Adding rules for DarkFi (Port: 26661)
Added: NYM-EXIT tcp port 26661
Added: NYM-EXIT tcp port 26661
Added: NYM-EXIT udp port 26661
Added: NYM-EXIT udp port 26661
Adding rules for PPTP (Port: 1723)
Added: NYM-EXIT tcp port 1723
Added: NYM-EXIT tcp port 1723
@@ -333,6 +355,11 @@ Adding rules for Kpasswd (Port: 464)
Added: NYM-EXIT tcp port 464
Added: NYM-EXIT udp port 464
Added: NYM-EXIT udp port 464
Adding rules for MoneroRPC (Port: 18089)
Added: NYM-EXIT tcp port 18089
Added: NYM-EXIT tcp port 18089
Added: NYM-EXIT udp port 18089
Added: NYM-EXIT udp port 18089
Adding rules for RemoteHTTPS (Port: 981)
Added: NYM-EXIT tcp port 981
Added: NYM-EXIT tcp port 981
@@ -398,6 +425,11 @@ Adding rules for GroupWise (Port: 1677)
Added: NYM-EXIT tcp port 1677
Added: NYM-EXIT udp port 1677
Added: NYM-EXIT udp port 1677
Adding rules for Monero (Port: 18080-18081)
Added: NYM-EXIT tcp port range 18080:18081
Added: NYM-EXIT tcp port range 18080:18081
Added: NYM-EXIT udp port range 18080:18081
Added: NYM-EXIT udp port range 18080:18081
Adding rules for EnsimControlPanel (Port: 19638)
Added: NYM-EXIT tcp port 19638
Added: NYM-EXIT tcp port 19638
File diff suppressed because it is too large Load Diff
@@ -2,41 +2,61 @@
Running Nym Exit Policy Verification Tests...
Testing Port Range Rules...
Testing FTP tcp port range 20-21
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:20:21
✓ Rule exists: NYM-EXIT tcp port range 20:21
Testing HTTP tcp port range 80-81
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:80:81
✓ Rule exists: NYM-EXIT tcp port range 80:81
Testing CPanel tcp port range 2082-2083
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:2082:2083
✓ Rule exists: NYM-EXIT tcp port range 2082:2083
Testing XMPP tcp port range 5222-5223
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:5222:5223
✓ Rule exists: NYM-EXIT tcp port range 5222:5223
Testing Steam (sampling) tcp port range 27000-27050
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:27000:27050
✓ Rule exists: NYM-EXIT tcp port range 27000:27050
Testing FTP over TLS tcp port range 989-990
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:989:990
✓ Rule exists: NYM-EXIT tcp port range 989:990
Testing RTP/VoIP tcp port range 5000-5005
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:5000:5005
✓ Rule exists: NYM-EXIT tcp port range 5000:5005
Testing Simplify Media tcp port range 8087-8088
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:8087:8088
✓ Rule exists: NYM-EXIT tcp port range 8087:8088
Testing Zcash tcp port range 8232-8233
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:8232:8233
✓ Rule exists: NYM-EXIT tcp port range 8232:8233
Testing Bitcoin tcp port range 8332-8333
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:8332:8333
✓ Rule exists: NYM-EXIT tcp port range 8332:8333
Testing Monero tcp port range 18080-18081
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpts:18080:18081
✓ Rule exists: NYM-EXIT tcp port range 18080:18081
Test test_port_range_rules PASSED
Testing Critical Service Rules...
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:22
✓ Rule exists: NYM-EXIT tcp port 22
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:53
✓ Rule exists: NYM-EXIT tcp port 53
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:443
✓ Rule exists: NYM-EXIT tcp port 443
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:853
✓ Rule exists: NYM-EXIT tcp port 853
ACCEPT tcp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 tcp dpt:1194
✓ Rule exists: NYM-EXIT tcp port 1194
ACCEPT udp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 udp dpt:53
✓ Rule exists: NYM-EXIT udp port 53
ACCEPT udp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 udp dpt:123
✓ Rule exists: NYM-EXIT udp port 123
ACCEPT udp opt -- in * out * 0.0.0.0/0 -> 0.0.0.0/0 udp dpt:1194
✓ Rule exists: NYM-EXIT udp port 1194
Relevant existing rules for HTTP (port 80):
Test test_critical_services PASSED
This test takes some time, do not quit the process
Testing Default Reject Rule...
✓ Default REJECT rule exists
Test test_default_reject_rule PASSED
@@ -1 +1 @@
Friday, July 4th 2025, 14:45:02 UTC
Wednesday, July 30th 2025, 09:32:50 UTC
@@ -2,13 +2,14 @@
Usage: nym-node [OPTIONS] <COMMAND>
Commands:
build-info Show build information of this binary
bonding-information Show bonding information of this node depending on its currently selected mode
node-details Show details of this node
migrate Attempt to migrate an existing mixnode or gateway into a nym-node
run Start this nym-node
sign Use identity key of this node to sign provided message
help Print this message or the help of the given subcommand(s)
build-info Show build information of this binary
bonding-information Show bonding information of this node depending on its currently selected mode
node-details Show details of this node
migrate Attempt to migrate an existing mixnode or gateway into a nym-node
run Start this nym-node
sign Use identity key of this node to sign provided message
unsafe-reset-sphinx-keys UNSAFE: reset existing sphinx keys and attempt to generate fresh one for the current network state
help Print this message or the help of the given subcommand(s)
Options:
-c, --config-env-file <CONFIG_ENV_FILE>
@@ -1,21 +1,44 @@
| **ISP** | **Locations** | **Public IPv6** | **Crypto Payments** | **Comments** | **Last Updated** |
|:------------------------------------------------|:---------------------------------------------------------------------------------------------------------|:-------------------------------------|:---------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------|
| [AlexHost](https://alexhost.com) | Moldova, Bulgaria, Sweden, Netherlands | Yes, on by default | Yes | They allow TOR Bridges, Relays. Exit nodes are only allowed on dedicated servers (prices start from 26 EUR) | 07/2024 |
| [BitLaunch](https://bitlaunch.io) | Canada, USA, UK | No | Yes | Expensive. Digial Ocean through BitLanch has IPv6 | 05/2024 |
| [Cherry Servers](https://www.cherryservers.com) | Lithuania, Netherlands, USA, Singapore | No | Yes | Issued IP doesnt match the location offered by the provider. | 05/2024 |
| [Flokinet](https://flokinet.is) | Netherlands, Iceland, Romania,France | Yes, needs a ticket and custom setup | yes, including XMR | Very slow customer support | 05/2024 |
| [HostSailor](https://hostsailor.com) | USA | Yes, based on ticket | Yes | The IPv6 setup needs custom research and is not documented | 05/2024 |
| [Hostiko](https://hostiko.com.ua) | Ukraine, Germany | Yes, on by default | Yes | Ukrainian provider. They allow Exit nodes on Germany boxes but limit the bandwidth, you also have to restrict certain ports like 25 and 587. Make sure you open a ticket. | 07/2024 |
| [Hostinger](https://hostinger.com) | France, Lithuania, India, USA, Brazil | Yes, out of the box | Yes | Crypto payments must be done per each server monthly or annually. | 05/2024 |
| [Hostslick](https://hostslick.com) | Netherlands, Germany | Yes, on by default | Yes | Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node | 07/2024 |
| [Incognet](https://incognet.io) | Netherlands and USA | Yes, on by default | Yes | They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits | 07/2024 |
| [IsHosting](https://ishosting.com/en) | Brazil, Netherlands | Yes, based on ticket | Yes | Expensive | 05/2024 |
| [Linode](https://linode.com) | USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy | Yes out of the box | No, only through [BitLAunch](https://bitlaunch.io) | IPv6 sometimes need to be re-added in Networking tab, no reboot needed | 05/2024 |
| [LiteServer](https://liteserver.nl) | Netherlands | Yes, on by default | Yes | Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal | 07/2024 |
| [Mevspace](https://mevspace.com) | Poland | Yes, on by default | Yes | Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff | 07/2024 |
| [Misaka](https://www.misaka.io/) | South Africa | Yes, native support | No | Very Expensive | 05/2024 |
| [Njalla](https://nja.la) | Sweden | Yes | Yes | Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market. | 05/2024 |
| [RDP](https://rdp.sh) | Netherlands, USA, Poland | Yes, on by default | Yes | German provider. Exit nodes are allowed, policy is here https://rdp.sh/docs/faq/tor ports 25,465,587 must be closed. Make sure you open a ticket before running an exit node. | 07/2024 |
| [TerraHost](https://terrahost.no) | Norway | Yes, on by default | Yes | Very reliable Norwegian provider. Only allow exit nodes on Dedicated servers subject to certain caveats (you must open a ticket). Always check T&C https://terrahost.no/avtalebetingelser | 07/2024 |
| [iHostArt](https://ihostart.com) | Romania | Yes, on by default | Yes | Super permissive provider. They do allow Tor Exit/Relay/Bridge. Pro-free speech etc. Recently, IPv6 geolocation was set to North Korea, so be aware. | 07/2024 |
| [vSys Host](https://vsys.host) | Ukraine, Netherlands, USA | Yes, on by default | Yes | Pretty permissive provider registered in Ukraine. Should allow Relay/Exit nodes but nothing in T&C, so better double check. | 07/2024 |
| **ISP** | **Locations** | **Public IPv6** | **Crypto Payments** | **Comments** | **Last Updated** |
|:---------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------------------------|:---------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-------------------|
| [AlexHost](https://alexhost.com) | Moldova, Bulgaria, Sweden, Netherlands | Yes, on by default | Yes | They allow TOR Bridges, Relays. Exit nodes are only allowed on dedicated servers (prices start from 26 EUR) | 07/2024 |
| [AmeriNoc](https://www.amerinoc.com) | USA | Yes | nan | nan | 07/2025 |
| [BitLaunch](https://bitlaunch.io) | Canada, USA, UK | No | Yes | Expensive. Digial Ocean through BitLanch has IPv6 | 05/2024 |
| [Cherry Servers](https://www.cherryservers.com) | Lithuania, Netherlands, USA, Singapore | No | Yes | Issued IP doesnt match the location offered by the provider. | 05/2024 |
| [Colocall](https://www.colocall.net/) | Ukraine | Yes | nan | 07/2025 | nan |
| [DataPacket](https://www.datapacket.com/pricing) | NL, GR, SK, BE, RO, HU, DK, IE, DE, UA, PT, GB, ES, FR, IT, NO, CZ, BG, SE, AT, PL, HR, CH, USA, CO, AR, PE, MX, CL, TR, ZA, NG, IL, HK, AU, SG, JP | Yes | nan | nan | 07/2025 |
| [Dataclub](https://www.dataclub.eu/) | Latvia, Sweden, Netherlands | Yes | nan | nan | 07/2027 |
| [Flokinet](https://flokinet.is) | Netherlands, Iceland, Romania,France | Yes, needs a ticket and custom setup | yes, including XMR | Very slow customer support | 05/2024 |
| [FranTech](https://my.frantech.ca) | USA | Yes | nan | nan | 07/2025 |
| [Fsit](https://www.fsit.com/server/vps-vserver-kvm) | Swiss | Yes | Yes | nan | 07/2025 |
| [HostSailor](https://hostsailor.com) | USA | Yes, based on ticket | Yes | The IPv6 setup needs custom research and is not documented | 05/2024 |
| [Hostiko](https://hostiko.com.ua) | Ukraine, Germany | Yes, on by default | Yes | Ukrainian provider. They allow Exit nodes on Germany boxes but limit the bandwidth, you also have to restrict certain ports like 25 and 587. Make sure you open a ticket. | 07/2024 |
| [Hostinger](https://hostinger.com) | France, Lithuania, India, USA, Brazil | Yes, out of the box | Yes | Not fast enough, Crypto payments must be done per each server monthly or annually. | 07/2025 |
| [Hostroyale](https://hostroyale.com/hosting/dedicated-server/) | Various countries with different pricing | nan | Yes | nan | 07/2025 |
| [Hostslick](https://hostslick.com) | Netherlands, Germany | Yes, on by default | Yes | Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node | 07/2024 |
| [Incognet](https://incognet.io) | Netherlands and USA | Yes, on by default | Yes | They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits | 07/2024 |
| [Incognet](https://incognet.io/kansas-city-dedicated-servers) | USA, Netherlands | Yes | nan | nan | 07/2025 |
| [Ionos](https://www.ionos.com/servers/amd-servers) | US, DE, UK, ESP, FR | nan | No | nan | 07/2025 |
| [IsHosting](https://ishosting.com/en) | Brazil, Netherlands | Yes, based on ticket | Yes | Expensive | 05/2024 |
| [Leaseweb](https://www.leaseweb.com/en/configure/vc/product/entityKey/DEDSER02_NEW_ORDER_BUSINESS_R740XD-24SFF-6134) | US, NL, DE, UK, CA, SG, JP, AUS, HK | nan | No | KYC mandatory | 07/2025 |
| [Linode](https://linode.com) | USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy | Yes out of the box | No, only through [BitLAunch](https://bitlaunch.io) | IPv6 sometimes need to be re-added in Networking tab, no reboot needed | 05/2024 |
| [LiteServer](https://liteserver.nl) | Netherlands | Yes, on by default | Yes | Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal | 07/2024 |
| [Lowendbox](https://lowendbox.com/category/dedicated-servers) | | | | Just an aggregator with good offers | 07/2025 |
| [M247](https://m247.com/eu/services/host/dedicated-servers/) | UK, Austria, Br, Sw, Jp, Poland, Fr, USA, Netherlands | Yes | No | nan | 07/2025 |
| [Mebilcom](https://www.melbicom.net/dedicatedserver/) | NL, US, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL | nan | No | nan | 07/2025 |
| [Mevspace](https://mevspace.com) | Poland | Yes, on by default | Yes | Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff | 07/2024 |
| [Misaka](https://www.misaka.io/) | South Africa | Yes, native support | No | Very Expensive | 05/2024 |
| [NiceVPS](https://nicevps.net/) | Netherlands | Yes | nan | nan | 07/2025 |
| [Njalla](https://nja.la) | Sweden | Yes | Yes | Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market. | 05/2024 |
| [OVH](https://us.ovhcloud.com/bare-metal/rise/rise-3/) | USA, DE, FR, UK, PL, CA | | No | Not all locations always available | 07/2025 |
| [Oneprovider](https://oneprovider.com/en/dedicated-servers/ipv6) | PL, FR, NL, UA, US, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR | Yes | No | nan | 07/2025 |
| [PrivateLayer](https://privatelayer.com) | Swiss | Yes | Yes | Slow customer response | 07/2025 |
| [Privex](https://www.privex.io/tor-exit-policy/) | USA, Germany, Sweden | Yes | Yes | nan | 07/2025 |
| [Psychz](https://www.psychz.net) | US, UK, Brazil, Japan, Russia, South Africa and many more | Yes | nan | nan | 07/2025 |
| [RDP](https://rdp.sh) | Netherlands, USA, Poland | Yes, on by default | Yes | German provider. Exit nodes are allowed, policy is here https://rdp.sh/docs/faq/tor ports 25,465,587 must be closed. Make sure you open a ticket before running an exit node. | 07/2024 |
| [Servermania](https://www.servermania.com/dedicated-servers-hosting.htm) | USA, Canada | nan | No | nan | 07/2025 |
| [Svea](https://svea.net/vps) | Sweden | Yes | nan | nan | 07/2025 |
| [TerraHost](https://terrahost.no) | Norway | Yes, on by default | Yes | Very reliable Norwegian provider. Only allow exit nodes on Dedicated servers subject to certain caveats (you must open a ticket). Always check T&C https://terrahost.no/avtalebetingelser | 07/2024 |
| [Thundervm](https://thundervm.com/en/hosting/dedicated-server) | USA, UK, France, Italy, Switzerland, Netherlands | nan | Yes | | 07/2025 |
| [Zenlayer](https://www.zenlayer.com/bare-metal/) | [advertised over 50 locations](50+ https://www.zenlayer.com/global-network) | nan | nan | nan | 07/2025 |
| [iHostArt](https://ihostart.com) | Romania | Yes, on by default | Yes | Super permissive provider. They do allow Tor Exit/Relay/Bridge. Pro-free speech etc. Recently, IPv6 geolocation was set to North Korea, so be aware. | 07/2024 |
| [vSys Host](https://vsys.host) | Ukraine, Netherlands, USA | Yes, on by default | Yes | Pretty permissive provider registered in Ukraine. Should allow Relay/Exit nodes but nothing in T&C, so better double check. | 07/2024 |
+42 -25
View File
@@ -1,26 +1,43 @@
**ISP**,**Locations**,**Public IPv6**,**Crypto Payments**,**Comments**,**Last Updated**
[Flokinet](https://flokinet.is),"Netherlands, Iceland, Romania,France","Yes, needs a ticket and custom setup","yes, including XMR","Very slow customer support","05/2024"
[BitLaunch](https://bitlaunch.io),"Canada, USA, UK","No","Yes","Expensive. Digial Ocean through BitLanch has IPv6","05/2024"
[Hostinger](https://hostinger.com),"France, Lithuania, India, USA, Brazil","Yes, out of the box","Yes","Crypto payments must be done per each server monthly or annually.","05/2024"
[Linode](https://linode.com),"USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy","Yes out of the box","No, only through [BitLAunch](https://bitlaunch.io)","IPv6 sometimes need to be re-added in Networking tab, no reboot needed","05/2024"
[Cherry Servers](https://www.cherryservers.com),"Lithuania, Netherlands, USA, Singapore","No","Yes","Issued IP doesnt match the location offered by the provider.","05/2024"
[Njalla](https://nja.la),"Sweden","Yes","Yes","Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market.","05/2024"
[HostSailor](https://hostsailor.com),"USA","Yes, based on ticket","Yes","The IPv6 setup needs custom research and is not documented","05/2024"
[Misaka](https://www.misaka.io/),"South Africa","Yes, native support","No","Very Expensive","05/2024"
[IsHosting](https://ishosting.com/en),"Brazil, Netherlands","Yes, based on ticket","Yes","Expensive","05/2024"
[AlexHost](https://alexhost.com),"Moldova, Bulgaria, Sweden, Netherlands","Yes, on by default","Yes","They allow TOR Bridges, Relays. Exit nodes are only allowed on dedicated servers (prices start from 26 EUR)","07/2024"
[iHostArt](https://ihostart.com),"Romania","Yes, on by default","Yes","Super permissive provider. They do allow Tor Exit/Relay/Bridge. Pro-free speech etc. Recently, IPv6 geolocation was set to North Korea, so be aware.","07/2024"
[Incognet](https://incognet.io),"Netherlands and USA","Yes, on by default","Yes","They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits","07/2024"
[vSys Host](https://vsys.host),"Ukraine, Netherlands, USA","Yes, on by default","Yes","Pretty permissive provider registered in Ukraine. Should allow Relay/Exit nodes but nothing in T&C, so better double check.","07/2024"
[LiteServer](https://liteserver.nl),"Netherlands","Yes, on by default","Yes","Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal","07/2024"
[TerraHost](https://terrahost.no),"Norway","Yes, on by default","Yes","Very reliable Norwegian provider. Only allow exit nodes on Dedicated servers subject to certain caveats (you must open a ticket). Always check T&C https://terrahost.no/avtalebetingelser","07/2024"
[Mevspace](https://mevspace.com),"Poland","Yes, on by default","Yes","Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff","07/2024"
[Hostiko](https://hostiko.com.ua),"Ukraine, Germany","Yes, on by default","Yes","Ukrainian provider. They allow Exit nodes on Germany boxes but limit the bandwidth, you also have to restrict certain ports like 25 and 587. Make sure you open a ticket.","07/2024"
[Hostslick](https://hostslick.com),"Netherlands, Germany","Yes, on by default","Yes","Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node","07/2024"
[RDP](https://rdp.sh),"Netherlands, USA, Poland","Yes, on by default","Yes","German provider. Exit nodes are allowed, policy is here https://rdp.sh/docs/faq/tor ports 25,465,587 must be closed. Make sure you open a ticket before running an exit node.","07/2024"
[Flokinet](https://flokinet.is),"Netherlands, Iceland, Romania,France","Yes, needs a ticket and custom setup","yes, including XMR",Very slow customer support,05/2024
[BitLaunch](https://bitlaunch.io),"Canada, USA, UK",No,Yes,Expensive. Digial Ocean through BitLanch has IPv6,05/2024
[Hostinger](https://hostinger.com),"France, Lithuania, India, USA, Brazil","Yes, out of the box",Yes,"Not fast enough, Crypto payments must be done per each server monthly or annually.",07/2025
[Linode](https://linode.com),"USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy",Yes out of the box,"No, only through [BitLAunch](https://bitlaunch.io)","IPv6 sometimes need to be re-added in Networking tab, no reboot needed",05/2024
[Cherry Servers](https://www.cherryservers.com),"Lithuania, Netherlands, USA, Singapore",No,Yes,Issued IP doesnt match the location offered by the provider.,05/2024
[Njalla](https://nja.la),Sweden,Yes,Yes,"Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market.",05/2024
[HostSailor](https://hostsailor.com),USA,"Yes, based on ticket",Yes,The IPv6 setup needs custom research and is not documented,05/2024
[Misaka](https://www.misaka.io/),South Africa,"Yes, native support",No,Very Expensive,05/2024
[IsHosting](https://ishosting.com/en),"Brazil, Netherlands","Yes, based on ticket",Yes,Expensive,05/2024
[AlexHost](https://alexhost.com),"Moldova, Bulgaria, Sweden, Netherlands","Yes, on by default",Yes,"They allow TOR Bridges, Relays. Exit nodes are only allowed on dedicated servers (prices start from 26 EUR)",07/2024
[iHostArt](https://ihostart.com),Romania,"Yes, on by default",Yes,"Super permissive provider. They do allow Tor Exit/Relay/Bridge. Pro-free speech etc. Recently, IPv6 geolocation was set to North Korea, so be aware.",07/2024
[Incognet](https://incognet.io),Netherlands and USA,"Yes, on by default",Yes,They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits,07/2024
[vSys Host](https://vsys.host),"Ukraine, Netherlands, USA","Yes, on by default",Yes,"Pretty permissive provider registered in Ukraine. Should allow Relay/Exit nodes but nothing in T&C, so better double check.",07/2024
[LiteServer](https://liteserver.nl),Netherlands,"Yes, on by default",Yes,Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal,07/2024
[TerraHost](https://terrahost.no),Norway,"Yes, on by default",Yes,Very reliable Norwegian provider. Only allow exit nodes on Dedicated servers subject to certain caveats (you must open a ticket). Always check T&C https://terrahost.no/avtalebetingelser,07/2024
[Mevspace](https://mevspace.com),Poland,"Yes, on by default",Yes,"Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff",07/2024
[Hostiko](https://hostiko.com.ua),"Ukraine, Germany","Yes, on by default",Yes,"Ukrainian provider. They allow Exit nodes on Germany boxes but limit the bandwidth, you also have to restrict certain ports like 25 and 587. Make sure you open a ticket.",07/2024
[Hostslick](https://hostslick.com),"Netherlands, Germany","Yes, on by default",Yes,Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node,07/2024
[RDP](https://rdp.sh),"Netherlands, USA, Poland","Yes, on by default",Yes,"German provider. Exit nodes are allowed, policy is here https://rdp.sh/docs/faq/tor ports 25,465,587 must be closed. Make sure you open a ticket before running an exit node.",07/2024
[Lowendbox](https://lowendbox.com/category/dedicated-servers), , , ,Just an aggregator with good offers,07/2025
[Thundervm](https://thundervm.com/en/hosting/dedicated-server),"USA, UK, France, Italy, Switzerland, Netherlands",,Yes, ,07/2025
[OVH](https://us.ovhcloud.com/bare-metal/rise/rise-3/),"USA, DE, FR, UK, PL, CA", ,No,Not all locations always available,07/2025
[Mebilcom](https://www.melbicom.net/dedicatedserver/),"NL, US, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL",,No,,07/2025
[Servermania](https://www.servermania.com/dedicated-servers-hosting.htm),"USA, Canada",,No,,07/2025
[Oneprovider](https://oneprovider.com/en/dedicated-servers/ipv6),"PL, FR, NL, UA, US, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR",Yes,No,,07/2025
[Ionos](https://www.ionos.com/servers/amd-servers),"US, DE, UK, ESP, FR",,No,,07/2025
[Leaseweb](https://www.leaseweb.com/en/configure/vc/product/entityKey/DEDSER02_NEW_ORDER_BUSINESS_R740XD-24SFF-6134),"US, NL, DE, UK, CA, SG, JP, AUS, HK",,No,KYC mandatory,07/2025
[M247](https://m247.com/eu/services/host/dedicated-servers/),"UK, Austria, Br, Sw, Jp, Poland, Fr, USA, Netherlands",Yes,No,,07/2025
[Hostroyale](https://hostroyale.com/hosting/dedicated-server/),Various countries with different pricing,, Yes,,07/2025
[DataPacket](https://www.datapacket.com/pricing),"NL, GR, SK, BE, RO, HU, DK, IE, DE, UA, PT, GB, ES, FR, IT, NO, CZ, BG, SE, AT, PL, HR, CH, USA, CO, AR, PE, MX, CL, TR, ZA, NG, IL, HK, AU, SG, JP",Yes,,,07/2025
[Zenlayer](https://www.zenlayer.com/bare-metal/), [advertised over 50 locations](50+ https://www.zenlayer.com/global-network),,,,07/2025
[PrivateLayer](https://privatelayer.com),Swiss,Yes,Yes,Slow customer response,07/2025
[AmeriNoc](https://www.amerinoc.com),USA,Yes,,,07/2025
[Colocall](https://www.colocall.net/),Ukraine,Yes,,07/2025,
[Incognet](https://incognet.io/kansas-city-dedicated-servers),"USA, Netherlands",Yes,,,07/2025
[FranTech](https://my.frantech.ca),USA,Yes,,,07/2025
[Psychz](https://www.psychz.net),"US, UK, Brazil, Japan, Russia, South Africa and many more",Yes,,,07/2025
[Fsit](https://www.fsit.com/server/vps-vserver-kvm),Swiss,Yes,Yes,,07/2025
[NiceVPS](https://nicevps.net/),Netherlands,Yes,,,07/2025
[Dataclub](https://www.dataclub.eu/),"Latvia, Sweden, Netherlands",Yes,,,07/2027
[Privex](https://www.privex.io/tor-exit-policy/),"USA, Germany, Sweden",Yes,Yes,,07/2025
[Svea](https://svea.net/vps),Sweden,Yes,,,07/2025
1 **ISP** **Locations** **Public IPv6** **Crypto Payments** **Comments** **Last Updated**
2 [Flokinet](https://flokinet.is) Netherlands, Iceland, Romania,France Yes, needs a ticket and custom setup yes, including XMR Very slow customer support 05/2024
3 [BitLaunch](https://bitlaunch.io) Canada, USA, UK No Yes Expensive. Digial Ocean through BitLanch has IPv6 05/2024
4 [Hostinger](https://hostinger.com) France, Lithuania, India, USA, Brazil Yes, out of the box Yes Crypto payments must be done per each server monthly or annually. Not fast enough, Crypto payments must be done per each server monthly or annually. 05/2024 07/2025
5 [Linode](https://linode.com) USA, Canada, Japan, India, Indonesia, Sweden, Netherlands, Germany, Brazil, France, UK, Australia, Italy Yes out of the box No, only through [BitLAunch](https://bitlaunch.io) IPv6 sometimes need to be re-added in Networking tab, no reboot needed 05/2024
6 [Cherry Servers](https://www.cherryservers.com) Lithuania, Netherlands, USA, Singapore No Yes Issued IP doesn’t match the location offered by the provider. 05/2024
7 [Njalla](https://nja.la) Sweden Yes Yes Privacy vandguards! The biggest VPS 45 is 3 cores only, but it works better than many “larger” servers on the market. 05/2024
8 [HostSailor](https://hostsailor.com) USA Yes, based on ticket Yes The IPv6 setup needs custom research and is not documented 05/2024
9 [Misaka](https://www.misaka.io/) South Africa Yes, native support No Very Expensive 05/2024
10 [IsHosting](https://ishosting.com/en) Brazil, Netherlands Yes, based on ticket Yes Expensive 05/2024
11 [AlexHost](https://alexhost.com) Moldova, Bulgaria, Sweden, Netherlands Yes, on by default Yes They allow TOR Bridges, Relays. Exit nodes are only allowed on dedicated servers (prices start from 26 EUR) 07/2024
12 [iHostArt](https://ihostart.com) Romania Yes, on by default Yes Super permissive provider. They do allow Tor Exit/Relay/Bridge. Pro-free speech etc. Recently, IPv6 geolocation was set to North Korea, so be aware. 07/2024
13 [Incognet](https://incognet.io) Netherlands and USA Yes, on by default Yes They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits 07/2024
14 [vSys Host](https://vsys.host) Ukraine, Netherlands, USA Yes, on by default Yes Pretty permissive provider registered in Ukraine. Should allow Relay/Exit nodes but nothing in T&C, so better double check. 07/2024
15 [LiteServer](https://liteserver.nl) Netherlands Yes, on by default Yes Very reliable Dutch provider. They do allow Relay nodes but for Exit nodes you need to contact them. Always check T&C https://liteserver.nl/legal 07/2024
16 [TerraHost](https://terrahost.no) Norway Yes, on by default Yes Very reliable Norwegian provider. Only allow exit nodes on Dedicated servers subject to certain caveats (you must open a ticket). Always check T&C https://terrahost.no/avtalebetingelser 07/2024
17 [Mevspace](https://mevspace.com) Poland Yes, on by default Yes Flexible Polish providers with 3 DCs in Poland. They do allow Tor Exit nodes but you may need a dedicated server for this. Make sure you open a ticket to check. As of today's date, they have 48h for 1 EUR tariff 07/2024
18 [Hostiko](https://hostiko.com.ua) Ukraine, Germany Yes, on by default Yes Ukrainian provider. They allow Exit nodes on Germany boxes but limit the bandwidth, you also have to restrict certain ports like 25 and 587. Make sure you open a ticket. 07/2024
19 [Hostslick](https://hostslick.com) Netherlands, Germany Yes, on by default Yes Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node 07/2024
20 [RDP](https://rdp.sh) Netherlands, USA, Poland Yes, on by default Yes German provider. Exit nodes are allowed, policy is here https://rdp.sh/docs/faq/tor ports 25,465,587 must be closed. Make sure you open a ticket before running an exit node. 07/2024
21 [Lowendbox](https://lowendbox.com/category/dedicated-servers) Just an aggregator with good offers 07/2025
22 [Thundervm](https://thundervm.com/en/hosting/dedicated-server) USA, UK, France, Italy, Switzerland, Netherlands Yes 07/2025
23 [OVH](https://us.ovhcloud.com/bare-metal/rise/rise-3/) USA, DE, FR, UK, PL, CA No Not all locations always available 07/2025
24 [Mebilcom](https://www.melbicom.net/dedicatedserver/) NL, US, DE, UAE, NG, ESP, IN, IT, FR, LT, SG, BG, LV, PL No 07/2025
25 [Servermania](https://www.servermania.com/dedicated-servers-hosting.htm) USA, Canada No 07/2025
26 [Oneprovider](https://oneprovider.com/en/dedicated-servers/ipv6) PL, FR, NL, UA, US, BG, RO, DK, ESP, NO, CZ, RS, IE, IT, UK, HU, CH, SK, AT, BE, BA, HK, JP, SG, LU, AU, SWE, UAE, BR, CR, MX, GR, CL, MA, AR Yes No 07/2025
27 [Ionos](https://www.ionos.com/servers/amd-servers) US, DE, UK, ESP, FR No 07/2025
28 [Leaseweb](https://www.leaseweb.com/en/configure/vc/product/entityKey/DEDSER02_NEW_ORDER_BUSINESS_R740XD-24SFF-6134) US, NL, DE, UK, CA, SG, JP, AUS, HK No KYC mandatory 07/2025
29 [M247](https://m247.com/eu/services/host/dedicated-servers/) UK, Austria, Br, Sw, Jp, Poland, Fr, USA, Netherlands Yes No 07/2025
30 [Hostroyale](https://hostroyale.com/hosting/dedicated-server/) Various countries with different pricing Yes 07/2025
31 [DataPacket](https://www.datapacket.com/pricing) NL, GR, SK, BE, RO, HU, DK, IE, DE, UA, PT, GB, ES, FR, IT, NO, CZ, BG, SE, AT, PL, HR, CH, USA, CO, AR, PE, MX, CL, TR, ZA, NG, IL, HK, AU, SG, JP Yes 07/2025
32 [Zenlayer](https://www.zenlayer.com/bare-metal/) [advertised over 50 locations](50+ https://www.zenlayer.com/global-network) 07/2025
33 [PrivateLayer](https://privatelayer.com) Swiss Yes Yes Slow customer response 07/2025
34 [AmeriNoc](https://www.amerinoc.com) USA Yes 07/2025
35 [Colocall](https://www.colocall.net/) Ukraine Yes 07/2025
36 [Incognet](https://incognet.io/kansas-city-dedicated-servers) USA, Netherlands Yes 07/2025
37 [FranTech](https://my.frantech.ca) USA Yes 07/2025
38 [Psychz](https://www.psychz.net) US, UK, Brazil, Japan, Russia, South Africa and many more Yes 07/2025
39 [Fsit](https://www.fsit.com/server/vps-vserver-kvm) Swiss Yes Yes 07/2025
40 [NiceVPS](https://nicevps.net/) Netherlands Yes 07/2025
41 [Dataclub](https://www.dataclub.eu/) Latvia, Sweden, Netherlands Yes 07/2027
42 [Privex](https://www.privex.io/tor-exit-policy/) USA, Germany, Sweden Yes Yes 07/2025
43 [Svea](https://svea.net/vps) Sweden Yes 07/2025
@@ -306,6 +306,7 @@ export RUST_LOG="info"
export NODE_STATUS_AGENT_SERVER_ADDRESS="http://127.0.0.1"
export NODE_STATUS_AGENT_SERVER_PORT="8000"
export NODE_STATUS_AGENT_AUTH_KEY=<NS_AGENT_PRIVATE_KEY>
export NODE_STATUS_AGENT_PROBE_MNEMONIC=<NS_AGENT_MNEMONIC>
export NODE_STATUS_AGENT_PROBE_EXTRA_ARGS="netstack-download-timeout-sec=30,netstack-num-ping=2,netstack-send-timeout-sec=1,netstack-recv-timeout-sec=1"
workers=${1:-1}
@@ -47,6 +47,83 @@ This page displays a full list of all the changes during our release cycle from
<VarInfo />
## `v2025.13-emmental`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.13-emmental)
- [`nym-node`](nodes/nym-node.mdx) version `1.15.0`
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2025-07-22T09:24:35.790560275Z
Build Version: 1.15.0
Commit SHA: 578c9b0567656d86812aa21eb0b4c93b5a7235bd
Commit Date: 2025-07-22T11:09:35.000000000+02:00
Commit Branch: HEAD
rustc Version: 1.86.0
rustc Channel: stable
cargo Profile: release
```
### Operators Updates & Tools
- [**NIP-3: Nym Exit Policy Update**](https://forum.nym.com/t/nip-3-nym-exit-policy-update/1462/2) is out - **VOTE [HERE](https://governator.nym.com/proposal/prop-d0c0d398-43bd-4a6f-b008-1921b64ae4ed)!**
- `nym-node` can only run one `--mode` at a time. Multiple mode nodes will fail to start.
### Features
- [Initial performance contract](https://github.com/nymtech/nym/pull/5833): Introduces the foundational structure of the performance contract. Not yet integrated into any system.
- [Basic performance contract integration within Nym API](https://github.com/nymtech/nym/pull/5871): Integrates the performance contract into `nym-api`, enabling node score source selection from the contract (disabled by default).
- [Forbid running `mixnode` + `entry-gateway` on the same node](https://github.com/nymtech/nym/pull/5878): Prevents configuration of `mixnode` and `entry-gateway` node on the same host.
- [HTTP Discovery objects & network defaults](https://github.com/nymtech/nym/pull/5814): Adds config options for expanded API URLs via discovery environment objects.
- [Add build info endpoints](https://github.com/nymtech/nym/pull/5857)
- [Batch SQL writes for packet stats](https://github.com/nymtech/nym/pull/5874)
- [Update push-node-status-agent.yaml](https://github.com/nymtech/nym/pull/5882): Adds input for setting release tag and prefixes image tag with `golden-`.
- [Make Mix hops optional for Mixnet Client SURBs](https://github.com/nymtech/nym/pull/5861): Allows SURBs to be configured to only use gateways in the mixnet return path.
- [Check gateway supported versions](https://github.com/nymtech/nym/pull/5860)
- [Clear out screaming logs](https://github.com/nymtech/nym/pull/5856): Removes unnecessary alarming log messages.
- [Use display when printing paths](https://github.com/nymtech/nym/pull/5853): Uses Rusts `display()` method for better path output.
- [Remove old explorer references](https://github.com/nymtech/nym/pull/5846)
- [Listen for shutdown signals during nym-node startup](https://github.com/nymtech/nym/pull/5879): This is to avoid situation where the process can't be killed without 'kill -9' because the logic to listen to shutdown signals hasn't been hit yet
### Bugfixes
- [Don't allow mixnode running in exit mode](https://github.com/nymtech/nym/pull/5898)
- [Fix contract build process in Makefile](https://github.com/nymtech/nym/pull/5892)
- [Ignore 'Send' responses when claiming bandwidth](https://github.com/nymtech/nym/pull/5884)
- [Fix the broken link](https://github.com/nymtech/nym/pull/5873)
- [Scraper bugfix: ignore precommits from missing validators](https://github.com/nymtech/nym/pull/5867)
- [Fix removal of qa env](https://github.com/nymtech/nym/pull/5855)
- [Return true remaining](https://github.com/nymtech/nym/pull/5866)
### Refactors & Maintenance
- [chore: 1.88 clippy](https://github.com/nymtech/nym/pull/5877): Applies clippy fixes based on recent compiler version update.
- [Security patches for the `dkg` crate](https://github.com/nymtech/nym/pull/5828): Addresses overflows, unsafe RNGs, and integrity checks as per Cryspen audit.
## `v2025.12-dolcelatte`
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.12-dolcelatte)
@@ -24,25 +24,23 @@ Write a message to your provider telling them about your intention to run a `nym
</AccordionTemplate>
#### Join Operators Legal Forum
This [Matrix channel]((https://matrix.to/#/!YfoUFsJjsXbWmijbPG:nymtech.chat?via=nymtech.chat&via=matrix.org&via=matrix.su4ka.icu)) is the best place to ask questions and share your experience with others. You can share screen prints of abuse reports and ask for support.
This [Matrix channel]((https://matrix.to/#/!YfoUFsJjsXbWmijbPG:nymtech.chat?via=nymtech.chat&via=matrix.org&via=matrix.su4ka.icu)) is the best place to ask questions and share your experience with others. You can share screen prints of abuse reports and ask for support.
#### Join Operators Legal Clinic
Do you have any questions directed for lawyers? Come and chat with Nym COO Alexis Roussel, every Wednesday 14:30 UTC for 60min in our [Operator Legal Forum channel on Matrix](https://matrix.to/#/!YfoUFsJjsXbWmijbPG:nymtech.chat?via=nymtech.chat&via=matrix.org&via=matrix.su4ka.icu).
#### Use a friendly provider
Nym operators community shares their experience with different ISPs on [this page](isp-list). At the same time, consider to move away from these provides:
- Servinga / VPS2day (AS39378)
- Frantech / Ponynet / BuyVM (AS53667)
- OVH SAS / OVHcloud (AS16276)
- Online S.A.S. / Scaleway (AS12876)
- Hetzner Online GmbH (AS24940)
- IONOS SE (AS8560)
- Psychz Networks (AS40676)
- 1337 Services GmbH / RDP.sh (AS210558)
- Stark Industries Solutions Ltd. / PQ.Hosting / The.Hosting / UFO-AS (AS44477 / ASN 33993)
#### Backup your nodes
Your only way to restore your node is when you have an access to `.nym` directory locally. Follow [node](../nodes/maintenance#backup-a-node) and [proxy configuration](../nodes/maintenance#backup-proxy-configuration) backup guides to be able to [restore your node](../nodes/maintenance#restoring-a-node) on another machine later on, without losing your delegation.
Your only way to restore your node is when you have an access to `.nym` directory locally. Follow [node](../nodes/maintenance#backup-a-node) and [proxy configuration](../nodes/maintenance#backup-proxy-configuration) backup guides to be able to [restore your node](../nodes/maintenance#restoring-a-node) on another machine later on, without losing your delegation.
#### Use `nym-exit` prefix on your landing page URL
We would like to ask operators to use [reverse proxy](../nodes/nym-node/configuration/proxy-configuration) with a [landing page](landing-pages). When assigning a domain please use a common convention with `nym-exit` in the beginning of the the page URL as this will create a reputation and reference. The entire address should have this new format:
@@ -69,10 +67,10 @@ nym-exit.mysquad.org
**The `NYM-EXIT` part in the beginning is what's important.**
#### Chose the right TLD
When registering a domain, check [Top Level Domain (TLD)](https://www.techopedia.com/definition/1348/top-level-domain-tld) terms and conditions. For example `.icu` is a no go. Having a wrong TLD may lead to your domain being taken away from you when facing a DMCA report.
When registering a domain, check [Top Level Domain (TLD)](https://www.techopedia.com/definition/1348/top-level-domain-tld) terms and conditions. For example `.icu` is a no go. Having a wrong TLD may lead to your domain being taken away from you when facing a DMCA report.
#### Respond to abuse reports
Make sure to read notifications from your account provider and if you receive an abuse report, respond to it in time. Here is a template explaining the essential legal background of Nym Node Exit Gateway. Don't forget to adjust the variables.
Make sure to read notifications from your account provider and if you receive an abuse report, respond to it in time. Here is a template explaining the essential legal background of Nym Node Exit Gateway. Don't forget to adjust the variables.
<br/>
<AccordionTemplate name="Email template: responding to DMCA take-down notices">
@@ -81,4 +79,3 @@ Make sure to read notifications from your account provider and if you receive an
#### Help us to improve these pages
Add your findings by opening a [Pull Request](add-content).
@@ -454,7 +454,7 @@ The exit policy is same for all NRs, the content is shaped by an offchain govern
There is a caveat though. NR is only routing TCP streams and therefore any other type of routing is *not* filtered thorugh the exit policy. To ensure that Nym Nodes follow the same exit policy when routing IP packets through wireguard and don't act as open proxies, the operators have to set up these rules via IP tables rules.
**Follow these steps, using a [setup script]i(https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh) and [testing scripts](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh) written by Nym quality assurance team, to setup exit policy for wireguard:**
**Follow these steps, using a [setup script](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/wireguard-exit-policy-manager.sh) and [testing scripts](https://raw.githubusercontent.com/nymtech/nym/refs/heads/develop/scripts/wireguard-exit-policy/exit-policy-tests.sh) written by Nym quality assurance team, to setup exit policy for wireguard:**
<Steps>
@@ -20,10 +20,10 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
```sh
nym-node
Binary Name: nym-node
Build Timestamp: 2025-07-09T12:50:32.123212812Z
Build Version: 1.14.0
Commit SHA: 089c47cce70ef8ea5ecfe3d00b52e510d006e120
Commit Date: 2025-07-07T15:44:15.000000000+02:00
Build Timestamp: 2025-07-22T09:24:35.790560275Z
Build Version: 1.15.0
Commit SHA: 578c9b0567656d86812aa21eb0b4c93b5a7235bd
Commit Date: 2025-07-22T11:09:35.000000000+02:00
Commit Branch: HEAD
rustc Version: 1.86.0
rustc Channel: stable
@@ -9,7 +9,7 @@ Nym Node operators running Gateway functionality are already familiar with the m
## Preparation
We recommend to have install all [the prerequisites](../binaries/building-nym.md#prerequisites) needed to build `nym-node` from source including latest [Rust Toolchain](https://www.rust-lang.org/tools/install).
We recommend to have installed all [the prerequisites](../binaries/building-nym.md#prerequisites) needed to build `nym-node` from source including latest [Rust Toolchain](https://www.rust-lang.org/tools/install), **and** make sure to have [Go](https://go.dev/doc/install) installed. Go is necessary as the probe uses the `rust2go` FFI library to use `netstack` when making requests.
## Installation
@@ -34,27 +34,65 @@ cargo build --release -p nym-gateway-probe
To list all commands and options run the binary with `--help` command:
```sh
./target/release/nym-gateway-probe --help
./target/release/nym-gateway-probe -h
```
- Output:
```sh
Usage: nym-gateway-probe [OPTIONS]
Usage: nym-gateway-probe [OPTIONS] --mnemonic <MNEMONIC>
Options:
-c, --config-env-file <CONFIG_ENV_FILE> Path pointing to an env file describing the network
-g, --gateway <GATEWAY>
-n, --no-log
-h, --help Print help
-V, --version Print version
-c, --config-env-file <CONFIG_ENV_FILE>
Path pointing to an env file describing the network
-g, --entry-gateway <ENTRY_GATEWAY>
The specific gateway specified by ID
-n, --node <NODE>
Identity of the node to test
--min-gateway-mixnet-performance <MIN_GATEWAY_MIXNET_PERFORMANCE>
--min-gateway-vpn-performance <MIN_GATEWAY_VPN_PERFORMANCE>
--only-wireguard
-i, --ignore-egress-epoch-role
Disable logging during probe
--no-log
-a, --amnezia-args <AMNEZIA_ARGS>
Arguments to be appended to the wireguard config enabling amnezia-wg configuration
--netstack-download-timeout-sec <NETSTACK_DOWNLOAD_TIMEOUT_SEC>
[default: 180]
--netstack-v4-dns <NETSTACK_V4_DNS>
[default: 1.1.1.1]
--netstack-v6-dns <NETSTACK_V6_DNS>
[default: 2606:4700:4700::1111]
--netstack-num-ping <NETSTACK_NUM_PING>
[default: 5]
--netstack-send-timeout-sec <NETSTACK_SEND_TIMEOUT_SEC>
[default: 3]
--netstack-recv-timeout-sec <NETSTACK_RECV_TIMEOUT_SEC>
[default: 3]
--netstack-ping-hosts-v4 <NETSTACK_PING_HOSTS_V4>
[default: nymtech.net]
--netstack-ping-ips-v4 <NETSTACK_PING_IPS_V4>
[default: 1.1.1.1]
--netstack-ping-hosts-v6 <NETSTACK_PING_HOSTS_V6>
[default: ipv6.google.com]
--netstack-ping-ips-v6 <NETSTACK_PING_IPS_V6>
[default: 2001:4860:4860::8888 2606:4700:4700::1111 2620:fe::fe]
--mnemonic <MNEMONIC>
-h, --help
Print help
-V, --version
Print version
```
To run the client, simply add a flag `--gateway` with a targeted gateway identity key.
To run the client, simply add `-n` flag followed by the ID key of the node you wish to test, as well as the mnemonic of a funded Nyx account; this is required to test the ticketbook generation.
```sh
./target/release/nym-gateway-probe --gateway <GATEWAY_IDENTITY_KEY>
./target/release/nym-gateway-probe -n <GATEWAY_IDENTITY_KEY> --mnemonic <MNEMONIC>
```
For any `nym-node --mode exit-gateway` the aim is to have this outcome:
@@ -87,4 +125,4 @@ For any `nym-node --mode exit-gateway` the aim is to have this outcome:
**If your Gateway is blacklisted, the probe will not work.**
If you don't provide a `--gateway` flag it will pick a random one to test.
If you don't provide a `-n` flag it will pick a random node to test.
@@ -9,12 +9,12 @@ import { AccordionTemplate } from 'components/accordion-template.tsx';
> Nym has two main codebases:
> - the [Nym platform](https://github.com/nymtech/nym), written in Rust. This contains all of our code except for the validators.
> - the [Nym validators](https://github.com/nymtech/nyxd), written in Go & maintained as a no-modification fork of [wasmd](https://github.com/CosmWasm/wasmd)
> - the [Nym validators](https://github.com/nymtech/nyxd), written in Go & maintained as fork of [wasmd](https://github.com/CosmWasm/wasmd)
The validator is a Go application which implements it's functionalities using [Cosmos SDK](https://v1.cosmos.network/sdk). The underlying state-replication engine is powered by [CometBFT](https://cometbft.com/), where the consensus mechanism is based on the [Tendermint Consensus Algorithm](https://arxiv.org/abs/1807.04938). Finally, a [CosmWasm](https://cosmwasm.com) smart contract module controls crucial mixnet functionalities like decentralised directory service, node bonding, and delegated mixnet staking.
<Callout type="info" emoji="️">
We are currently running mainnet with a closed set of reputable validators. To ensure decentralisation of Nyx chain, we are working on a mechanism to onboard new validators to the network. To join the waitlist, please drop an email to `validators [at] nymtech.net` with details of your setup, experience and any other relevent information
At present, our mainnet operates with a select group of reputed validators. We are not accepting new validators at this time. Any updates or changes to this policy will be promptly announced.
</Callout>
## Building your validator
@@ -52,7 +52,7 @@ pacman -S git gcc jq
- First remove any existing old Go installation and extract the archive you just downloaded into /usr/local:
```sh
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.20.10.linux-amd64.tar.gz
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.23.11.linux-amd64.tar.gz
```
- Then add /usr/local/go/bin to the PATH environment variable
@@ -69,14 +69,14 @@ go version
- Should return something like:
```sh
go version go1.20.10 linux/amd64
go version go1.23.11 linux/amd64
```
</Steps>
### Download a precompiled validator binary
You can find pre-compiled binaries for Ubuntu `22.04` and `20.04` [here](https://github.com/nymtech/nyxd/releases).
You can find pre-compiled binaries for Ubuntu `22.04` and `24.04` [here](https://github.com/nymtech/nyxd/releases).
### Manually compiling your validator binary
@@ -109,8 +109,9 @@ Usage:
nyxd [command]
Available Commands:
comet CometBFT subcommands
completion Generate the autocompletion script for the specified shell
config Create or query an application CLI configuration file
config Utilities for managing application configuration
debug Tool for helping with debugging your application
export Export state to JSON
genesis Application's genesis-related subcommands
@@ -119,20 +120,20 @@ Available Commands:
keys Manage your application's keys
prune Prune app history states by keeping the recent heights and deleting old heights
query Querying subcommands
rollback rollback cosmos-sdk and tendermint state by one height
rosetta spin up a rosetta server
rollback rollback Cosmos SDK and CometBFT state by one height
snapshots Manage local snapshots
start Run the full node
status Query remote node for status
tendermint Tendermint subcommands
testnet subcommands for starting or configuring local testnets
tx Transactions subcommands
version Print the application binary version information
Flags:
-h, --help help for nyxd
--home string directory for config and data
--home string directory for config and data (default "/Users/neo/.nyxd")
--log_format string The logging format (json|plain) (default "plain")
--log_level string The logging level (trace|debug|info|warn|error|fatal|panic) (default "info")
--log_level string The logging level (trace|debug|info|warn|error|fatal|panic|disabled or '*:<level>,<key>:<level>') (default "info")
--log_no_color Disable colored logs
--trace print out full stack trace on errors
Use "nyxd [command] --help" for more information about a command.
@@ -180,8 +181,9 @@ Usage:
nyxd [command]
Available Commands:
comet CometBFT subcommands
completion Generate the autocompletion script for the specified shell
config Create or query an application CLI configuration file
config Utilities for managing application configuration
debug Tool for helping with debugging your application
export Export state to JSON
genesis Application's genesis-related subcommands
@@ -190,20 +192,20 @@ Available Commands:
keys Manage your application's keys
prune Prune app history states by keeping the recent heights and deleting old heights
query Querying subcommands
rollback rollback cosmos-sdk and tendermint state by one height
rosetta spin up a rosetta server
rollback rollback Cosmos SDK and CometBFT state by one height
snapshots Manage local snapshots
start Run the full node
status Query remote node for status
tendermint Tendermint subcommands
testnet subcommands for starting or configuring local testnets
tx Transactions subcommands
version Print the application binary version information
Flags:
-h, --help help for nyxd
--home string directory for config and data
--home string directory for config and data (default "/Users/neo/.nyxd")
--log_format string The logging format (json|plain) (default "plain")
--log_level string The logging level (trace|debug|info|warn|error|fatal|panic) (default "info")
--log_level string The logging level (trace|debug|info|warn|error|fatal|panic|disabled or '*:<level>,<key>:<level>') (default "info")
--log_no_color Disable colored logs
--trace print out full stack trace on errors
Use "nyxd [command] --help" for more information about a command.
@@ -246,7 +248,7 @@ You can use the following command to download them for the correct network:
wget -O $HOME/.nyxd/config/genesis.json https://nymtech.net/genesis/genesis.json
# Sandbox testnet
curl https://rpc.sandbox.nymtech.net/snapshots/genesis.json | jq '.result.genesis' > $HOME/.nyxd/config/genesis.json
curl https://rpc.sandbox.nymtech.net/genesis | jq '.result.genesis' > $HOME/.nyxd/config/genesis.json
```
### `config.toml` configuration
@@ -512,7 +514,7 @@ nyxd tx slashing unjail
--from="KEYRING_NAME"
--chain-id=nyx
--gas=auto
--gas-adjustment=1.4
--gas-adjustment=1.5
--gas-prices=0.025unyx
```
@@ -523,7 +525,7 @@ nyxd tx slashing unjail
--from="KEYRING_NAME"
--chain-id=sandbox
--gas=auto
--gas-adjustment=1.4
--gas-adjustment=1.5
--gas-prices=0.025unyx
```
+21 -1
View File
@@ -40,7 +40,7 @@ tokio = { workspace = true, features = [
tokio-stream = { workspace = true, features = ["fs"] }
tokio-tungstenite = { workspace = true }
tokio-util = { workspace = true, features = ["codec"] }
tracing = { workspace = true }
#tracing = { workspace = true }
url = { workspace = true, features = ["serde"] }
zeroize = { workspace = true }
@@ -72,6 +72,26 @@ nym-wireguard = { path = "../common/wireguard" }
nym-wireguard-types = { path = "../common/wireguard-types", default-features = false }
defguard_wireguard_rs = { workspace = true }
opentelemetry = { workspace = true, features = ["trace"] }
tracing = { workspace = true, features = [
"std",
"attributes",
"tracing-attributes",
] }
tracing-opentelemetry = { workspace = true }
opentelemetry_sdk = { version = "0.30.0", default-features = false, features = [
"trace",
"rt-tokio",
"experimental_metrics_custom_reader",
] }
opentelemetry-otlp = { workspace = true, features = [
"metrics",
"grpc-tonic",
"tls",
"tls-webpki-roots",
] }
opentelemetry-stdout = { workspace = true, features = ["trace", "metrics"] }
[build-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
@@ -24,6 +24,8 @@ use nym_gateway_requests::{
SimpleGatewayRequestsError,
};
use nym_gateway_storage::error::GatewayStorageError;
use nym_gateway_storage::traits::BandwidthGatewayStorage;
use nym_gateway_storage::traits::SharedKeyGatewayStorage;
use nym_node_metrics::events::MetricsEvent;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_statistics_common::{gateways::GatewaySessionEvent, types::SessionType};
@@ -190,7 +192,7 @@ impl<R, S> AuthenticatedHandler<R, S> {
let handler = AuthenticatedHandler {
bandwidth_storage_manager: BandwidthStorageManager::new(
fresh.shared_state.storage.clone(),
Box::new(fresh.shared_state.storage.clone()),
ClientBandwidth::new(bandwidth.into()),
client.id,
fresh.shared_state.cfg.bandwidth,
@@ -27,9 +27,14 @@ use nym_gateway_requests::{
INITIAL_PROTOCOL_VERSION,
};
use nym_gateway_storage::error::GatewayStorageError;
use nym_gateway_storage::traits::BandwidthGatewayStorage;
use nym_gateway_storage::traits::InboxGatewayStorage;
use nym_gateway_storage::traits::SharedKeyGatewayStorage;
use nym_node_metrics::events::MetricsEvent;
use nym_sphinx::DestinationAddressBytes;
use nym_task::TaskClient;
use opentelemetry::trace::TraceContextExt;
use opentelemetry_sdk::trace::{IdGenerator, RandomIdGenerator};
use rand::CryptoRng;
use std::net::SocketAddr;
use std::time::Duration;
@@ -38,7 +43,8 @@ use time::OffsetDateTime;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio::time::timeout;
use tokio_tungstenite::tungstenite::{protocol::Message, Error as WsError};
use tracing::*;
use tracing::{debug, error, info, info_span, instrument, warn};
use tracing_opentelemetry::OpenTelemetrySpanExt;
#[derive(Debug, Error)]
pub(crate) enum InitialAuthenticationError {
@@ -851,6 +857,92 @@ impl<R, S> FreshHandler<R, S> {
S: AsyncRead + AsyncWrite + Unpin + Send,
R: CryptoRng + RngCore + Send,
{
/*
Incoming Request
trace_id: "abc123..." (from client)
1. Create SpanContext
SpanContext::new(
trace_id: "abc123..." (preserved)
span_id: "new_random_id"
is_remote: true
)
2. Convert to Context
Context::current().with_remote_span_context(...)
3. Create & Configure Span
span = info_span!("authenticate_v1")
span.set_parent(context) // Before entering │
-
4. Enter Span
let _enter = span.enter()
// All child spans inherit trace_id "abc123..." │
*/
let span = if let ClientControlRequest::AuthenticateV2(ref auth_req) = request {
if let Some(ref trace_id) = auth_req.debug_trace_id {
warn!("RAW TRACE ID: {trace_id:?}");
let trace_id = opentelemetry::trace::TraceId::from_hex(&trace_id)
.expect("Invalid trace ID format");
warn!("🫂TraceID: {trace_id}🫂");
// We don't need to try and preserve the SpanID, just the TraceID (right?) so
// just making a new SpanID for the moment
let id_generator = RandomIdGenerator::default();
let span_id = id_generator.new_span_id();
let span_context = opentelemetry::trace::SpanContext::new(
trace_id,
span_id,
opentelemetry::trace::TraceFlags::SAMPLED,
true, // is_remote = true since this comes from another service
Default::default(),
);
let remote_context =
opentelemetry::Context::current().with_remote_span_context(span_context);
let _context_guard = remote_context.clone().attach();
let span = info_span!(
"authenticate_v2",
trace_id = %trace_id
);
span.set_parent(remote_context.clone());
Some(span)
} else {
warn!("AuthenticateV2 request but no trace_id provided");
None
}
} else {
warn!("Not an AuthenticateV2 request");
None
};
// Probably a nicer way to do this but for now just match
let _guard = match &span {
Some(s) => {
warn!("ENTERED SPAN");
Some(s.enter())
}
None => {
warn!("COULDN'T ENTER SPAN");
None
}
};
// we can handle stateless client requests without prior authentication, like `ClientControlRequest::SupportedProtocol`
let auth_result = match request {
ClientControlRequest::Authenticate {
@@ -858,6 +950,7 @@ impl<R, S> FreshHandler<R, S> {
address,
enc_address,
iv,
debug_trace_id: _,
} => {
self.handle_legacy_authenticate(protocol_version, address, enc_address, iv)
.await
+14 -9
View File
@@ -13,7 +13,6 @@ use nym_credential_verification::ecash::{
credential_sender::CredentialHandlerConfig, EcashManager,
};
use nym_crypto::asymmetric::ed25519;
use nym_gateway_storage::models::WireguardPeer;
use nym_ip_packet_router::IpPacketRouter;
use nym_mixnet_client::forwarder::MixForwardingSender;
use nym_network_defaults::NymNetworkDetails;
@@ -38,7 +37,11 @@ mod stale_data_cleaner;
use crate::node::stale_data_cleaner::StaleMessagesCleaner;
pub use client_handling::active_clients::ActiveClientsStore;
pub use nym_gateway_stats_storage::PersistentStatsStorage;
pub use nym_gateway_storage::{error::GatewayStorageError, GatewayStorage};
pub use nym_gateway_storage::{
error::GatewayStorageError,
traits::{BandwidthGatewayStorage, InboxGatewayStorage},
GatewayStorage,
};
use nym_node_metrics::NymNodeMetrics;
pub use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent};
@@ -93,7 +96,7 @@ pub struct GatewayTasksBuilder {
// populated and cached as necessary
ecash_manager: Option<Arc<EcashManager>>,
wireguard_peers: Option<Vec<WireguardPeer>>,
wireguard_peers: Option<Vec<defguard_wireguard_rs::host::Peer>>,
wireguard_networks: Option<Vec<IpAddr>>,
}
@@ -357,12 +360,12 @@ impl GatewayTasksBuilder {
async fn build_wireguard_peers_and_networks(
&self,
) -> Result<(Vec<WireguardPeer>, Vec<IpAddr>), GatewayError> {
) -> Result<(Vec<defguard_wireguard_rs::host::Peer>, Vec<IpAddr>), GatewayError> {
let mut used_private_network_ips = vec![];
let mut all_peers = vec![];
for wireguard_peer in self.storage.get_all_wireguard_peers().await?.into_iter() {
let mut peer = defguard_wireguard_rs::host::Peer::try_from(wireguard_peer.clone())?;
let Some(peer) = peer.allowed_ips.pop() else {
let Some(allowed_ip) = peer.allowed_ips.pop() else {
let peer_identity = &peer.public_key;
warn!("Peer {peer_identity} has empty allowed ips. It will be removed",);
self.storage
@@ -370,8 +373,8 @@ impl GatewayTasksBuilder {
.await?;
continue;
};
used_private_network_ips.push(peer.ip);
all_peers.push(wireguard_peer);
used_private_network_ips.push(allowed_ip.ip);
all_peers.push(peer);
}
Ok((all_peers, used_private_network_ips))
@@ -379,7 +382,9 @@ impl GatewayTasksBuilder {
// only used under linux
#[allow(dead_code)]
async fn get_wireguard_peers(&mut self) -> Result<Vec<WireguardPeer>, GatewayError> {
async fn get_wireguard_peers(
&mut self,
) -> Result<Vec<defguard_wireguard_rs::host::Peer>, GatewayError> {
if let Some(cached) = self.wireguard_peers.take() {
return Ok(cached);
}
@@ -432,8 +437,8 @@ impl GatewayTasksBuilder {
opts.config.clone(),
wireguard_data.inner.clone(),
used_private_network_ips,
ecash_manager,
)
.with_ecash_verifier(ecash_manager)
.with_custom_gateway_transceiver(transceiver)
.with_shutdown(self.shutdown.fork("authenticator_sp"))
.with_wait_for_gateway(true)
+1 -1
View File
@@ -4,7 +4,7 @@
[package]
name = "nym-api"
license = "GPL-3.0"
version = "1.1.61"
version = "1.1.62"
authors.workspace = true
edition = "2021"
rust-version.workspace = true

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