Compare commits

..

22 Commits

Author SHA1 Message Date
durch 70fb7d75d6 Split daily stats queries 2025-07-25 12:07:42 +02:00
durch f11b2caeb1 No error swallowing 2025-07-25 11:43:05 +02:00
durch 1a07dbb09c Bump cutoff 2025-07-25 10:18:35 +02:00
durch 9587247536 Time and id header middleware 2025-07-24 14:35:57 +02: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
240 changed files with 12988 additions and 4679 deletions
+6 -12
View File
@@ -49,8 +49,6 @@ jobs:
run: |
curl -L0 https://www.ssl.com/download/codesigntool-for-linux-and-macos/ -o codesigntool.zip
unzip codesigntool.zip
chmod +x CodeSignTool.sh
- name: Get EV certificate credential id
working-directory: nym-wallet/src-tauri
if: ${{ inputs.sign }}
@@ -58,7 +56,6 @@ jobs:
shell: bash
run: |
echo "SSL_COM_CREDENTIAL_ID=$(./CodeSignTool.sh get_credential_ids -username=${{ secrets.SSL_COM_USERNAME }} -password=${{ secrets.SSL_COM_PASSWORD }} | sed -n '1!p' | sed 's/- //')" >> "$GITHUB_OUTPUT"
- name: Add custom sign command to tauri.conf.json
working-directory: nym-wallet/src-tauri
if: ${{ inputs.sign }}
@@ -82,7 +79,6 @@ jobs:
]
}
}' tauri.conf.json
- name: Install project dependencies
shell: bash
run: cd .. && yarn --network-timeout 100000
@@ -97,14 +93,12 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
SSL_COM_USERNAME: ${{ inputs.sign && secrets.SSL_COM_USERNAME || '' }}
SSL_COM_PASSWORD: ${{ inputs.sign && secrets.SSL_COM_PASSWORD || '' }}
SSL_COM_CREDENTIAL_ID: ${{ inputs.sign && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID || '' }}
SSL_COM_TOTP_SECRET: ${{ inputs.sign && secrets.SSL_COM_TOTP_SECRET || '' }}
CODE_SIGN_TOOL_PATH: ${{ inputs.sign && 'C:\\actions-runner\\_work\\nym\\nym\\nym-wallet\\src-tauri\\' || '' }}
SSL_COM_USERNAME: ${{ inputs.sign && secrets.SSL_COM_USERNAME }}
SSL_COM_PASSWORD: ${{ inputs.sign && secrets.SSL_COM_PASSWORD }}
SSL_COM_CREDENTIAL_ID: ${{ inputs.sign && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
SSL_COM_TOTP_SECRET: ${{ inputs.sign && secrets.SSL_COM_TOTP_SECRET }}
run: |
echo "Starting build process..."
echo "Signing enabled: ${{ inputs.sign }}"
yarn build
- name: Check bundle directory
@@ -153,7 +147,7 @@ jobs:
nym-wallet/${{ env.BUNDLE_PATH }}/msi/*.msi.zip*
nym-wallet/${{ env.BUNDLE_PATH }}/*/nym-wallet*.msi
nym-wallet/src-tauri/target/release/bundle/msi/*.msi
- name: Find MSI path for deployment
id: find-msi
shell: bash
@@ -173,4 +167,4 @@ jobs:
needs: publish-tauri
with:
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
secrets: inherit
secrets: inherit
@@ -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
+1020 -956
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -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"
+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,
@@ -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
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)
+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;
}
}
+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
@@ -1 +1 @@
Friday, July 4th 2025, 14:45:02 UTC
Tuesday, July 22nd 2025, 11:37:27 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>
@@ -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)
@@ -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.
@@ -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,6 +27,9 @@ 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;
+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
+58
View File
@@ -4,3 +4,61 @@ The Node Status API serves information about individual `nym-nodes` in the Mixne
We recommend that developers building applications such as explorers or analytics interfaces about the Mixnet run their own instance of the API, in order to promote a robust network of downstream services, and spread the load of API calls amongst as many endpoints as possible.
You can find build and operation instructions in the [docs](https://nym.com/docs/apis/ns-api).
## Database Support
The Node Status API supports both SQLite and PostgreSQL databases through Cargo feature flags:
- **SQLite** (default): Lightweight, file-based database suitable for development and small deployments
- **PostgreSQL**: Full-featured database recommended for production deployments
### Building with Different Database Backends
```bash
# Build with SQLite (default)
cargo build --features sqlite --no-default-features
# Build with PostgreSQL
cargo build --features pg --no-default-features
```
### Running Tests
```bash
# Test with SQLite
cargo test --features sqlite --no-default-features
# Test with PostgreSQL
make test-db # This sets up a test PostgreSQL instance
```
### Development Commands
The project includes a Makefile with helpful commands for both database backends:
```bash
# Check code compilation
make check-sqlite # Check with SQLite
make check-pg # Check with PostgreSQL
# Run clippy linter
make clippy-sqlite # Lint with SQLite
make clippy-pg # Lint with PostgreSQL
make clippy # Run both
# PostgreSQL development
make dev-db # Start a PostgreSQL instance for development
make prepare-pg # Prepare SQLx offline cache for PostgreSQL
```
### Implementation Details
The database abstraction is implemented using a query wrapper that automatically converts SQLite-style `?` placeholders to PostgreSQL-style `$1, $2, ...` placeholders at runtime. This allows writing queries once using SQLite syntax while maintaining compatibility with both databases.
Key differences handled:
- Placeholder syntax (`?` vs `$1, $2, ...`)
- Type conversions (SQLite uses i64, PostgreSQL uses i32 for many fields)
- SQL dialect differences (e.g., `INSERT OR IGNORE` vs `ON CONFLICT DO NOTHING`)
- RETURNING clause behavior
For more details on PostgreSQL setup, see [README_PG.md](nym-node-status-api/README_PG.md).
@@ -3,7 +3,7 @@
[package]
name = "nym-node-status-agent"
version = "1.0.0"
version = "1.0.4"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
@@ -14,13 +14,25 @@ rust-version.workspace = true
readme.workspace = true
[dependencies]
anyhow = { workspace = true}
anyhow = { workspace = true }
clap = { workspace = true, features = ["derive", "env"] }
nym-bin-common = { path = "../../common/bin-common", features = ["models"]}
futures = { workspace = true }
# nym-bin-common = { path = "../../common/bin-common", features = ["models"] }
nym-bin-common = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar", features = [
"models",
] }
nym-node-status-client = { path = "../nym-node-status-client" }
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
nym-crypto = { git = "https://github.com/nymtech/nym.git", branch = "release/2025.11-cheddar", features = [
"asymmetric",
"rand",
] }
rand = { workspace = true }
tokio = { workspace = true, features = ["macros", "rt-multi-thread", "process", "fs"] }
tokio = { workspace = true, features = [
"macros",
"rt-multi-thread",
"process",
"fs",
] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, features = ["env-filter"] }
@@ -16,7 +16,7 @@ WORKDIR /usr/src/nym-vpn-client/nym-vpn-core
RUN cargo build --release --package nym-gateway-probe
COPY ./ /usr/src/nym
WORKDIR /usr/src/nym/nym-node-status-agent
WORKDIR /usr/src/nym/nym-node-status-api/nym-node-status-agent
RUN cargo build --release
#-------------------------------------------------------------------
@@ -0,0 +1,71 @@
#!/bin/bash
# Build and push Node Status Agent container to harbor.nymte.ch
set -e
# Configuration
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
WORKING_DIRECTORY="${SCRIPT_DIR}"
CONTAINER_NAME="node-status-agent"
REGISTRY="harbor.nymte.ch"
NAMESPACE="nym"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Function to display usage
usage() {
echo "Usage: $0 <gateway-probe-git-ref>"
echo " gateway-probe-git-ref - Git reference (branch/tag/commit) for gateway probe"
echo ""
echo "Example: $0 main"
echo "Example: $0 release/2025.11-cheddar"
echo "Example: $0 v1.2.3"
exit 1
}
# Parse arguments
if [ $# -ne 1 ]; then
usage
fi
GATEWAY_PROBE_GIT_REF="$1"
# Get version from Cargo.toml
VERSION=$(grep "^version = " "${WORKING_DIRECTORY}/Cargo.toml" | sed -E 's/version = "(.*)"/\1/')
if [ -z "$VERSION" ]; then
echo -e "${RED}Error: Could not extract version from Cargo.toml${NC}"
exit 1
fi
# Clean up git ref for use in tag (replace / with -)
GIT_REF_SLUG="${GATEWAY_PROBE_GIT_REF//\//-}"
echo -e "${YELLOW}Building Node Status Agent${NC}"
echo -e "${YELLOW}Version: ${VERSION}${NC}"
echo -e "${YELLOW}Gateway Probe Git Ref: ${GATEWAY_PROBE_GIT_REF} (slug: ${GIT_REF_SLUG})${NC}"
# Login to Harbor
echo -e "${GREEN}Logging into Harbor...${NC}"
docker login "${REGISTRY}"
# Build the container
echo -e "${GREEN}Building container with gateway probe from ${GATEWAY_PROBE_GIT_REF}...${NC}"
# Build from repository root (two levels up from script location)
docker build \
--build-arg GIT_REF="${GATEWAY_PROBE_GIT_REF}" \
-f "${WORKING_DIRECTORY}/Dockerfile" \
"${SCRIPT_DIR}/../.." \
-t "${REGISTRY}/${NAMESPACE}/${CONTAINER_NAME}:${VERSION}-${GIT_REF_SLUG}" \
-t "${REGISTRY}/${NAMESPACE}/${CONTAINER_NAME}:latest-${GIT_REF_SLUG}"
# Push to Harbor
echo -e "${GREEN}Pushing container to Harbor...${NC}"
docker push "${REGISTRY}/${NAMESPACE}/${CONTAINER_NAME}:${VERSION}-${GIT_REF_SLUG}"
docker push "${REGISTRY}/${NAMESPACE}/${CONTAINER_NAME}:latest-${GIT_REF_SLUG}"
echo -e "${GREEN}Successfully built and pushed ${CONTAINER_NAME}:${VERSION}-${GIT_REF_SLUG}${NC}"
@@ -21,6 +21,7 @@ export NODE_STATUS_AGENT_SERVER_ADDRESS="http://127.0.0.1"
export NODE_STATUS_AGENT_SERVER_PORT="8000"
export NODE_STATUS_AGENT_PROBE_PATH="$crate_root/nym-gateway-probe"
export NODE_STATUS_AGENT_AUTH_KEY="BjyC9SsHAZUzPRkQR4sPTvVrp4GgaquTh5YfSJksvvWT"
export NODE_STATUS_AGENT_PROBE_MNEMONIC="$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}
@@ -1,17 +1,45 @@
use crate::probe::GwProbe;
use clap::{Parser, Subcommand};
use nym_bin_common::bin_info;
use std::sync::OnceLock;
use nym_crypto::asymmetric::ed25519::PrivateKey;
use std::{env, sync::OnceLock};
pub(crate) mod generate_keypair;
pub(crate) mod run_probe;
#[derive(Debug)]
pub(crate) struct ServerConfig {
pub(crate) address: String,
pub(crate) port: u16,
pub(crate) auth_key: PrivateKey,
}
// Helper for passing LONG_VERSION to clap
fn pretty_build_info_static() -> &'static str {
static PRETTY_BUILD_INFORMATION: OnceLock<String> = OnceLock::new();
PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print())
}
fn parse_server_config(s: &str) -> Result<ServerConfig, String> {
let parts: Vec<&str> = s.split('|').collect();
if parts.len() != 2 {
return Err("Server config must be in format 'address|port'".to_string());
}
let address = parts[0].to_string();
let port = parts[1]
.parse::<u16>()
.map_err(|_| "Invalid port number".to_string())?;
let auth_key =
PrivateKey::from_base58_string(env::var("NODE_STATUS_AGENT_AUTH_KEY").unwrap()).unwrap();
Ok(ServerConfig {
address,
port,
auth_key,
})
}
#[derive(Parser, Debug)]
#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)]
pub(crate) struct Args {
@@ -22,20 +50,19 @@ pub(crate) struct Args {
#[derive(Subcommand, Debug)]
pub(crate) enum Command {
RunProbe {
#[arg(short, long, env = "NODE_STATUS_AGENT_SERVER_ADDRESS")]
server_address: String,
#[arg(short = 'p', long, env = "NODE_STATUS_AGENT_SERVER_PORT")]
server_port: u16,
/// base58-encoded private key
#[arg(long, env = "NODE_STATUS_AGENT_AUTH_KEY")]
ns_api_auth_key: String,
/// Server configurations in format "address:port:auth_key"
/// Can be specified multiple times for multiple servers
#[arg(short, long, required = true)]
server: Vec<String>,
/// path of binary to run
#[arg(long, env = "NODE_STATUS_AGENT_PROBE_PATH")]
probe_path: String,
/// mnemonic for acquiring zk-nyms
#[arg(long, env = "NYM_NODE_MNEMONICS")]
mnemonic: String,
#[arg(
long,
env = "NODE_STATUS_AGENT_PROBE_EXTRA_ARGS",
@@ -54,22 +81,29 @@ impl Args {
pub(crate) async fn execute(&self) -> anyhow::Result<()> {
match &self.command {
Command::RunProbe {
server_address,
server_port,
ns_api_auth_key,
server,
probe_path,
mnemonic,
probe_extra_args,
} => run_probe::run_probe(
server_address,
server_port.to_owned(),
ns_api_auth_key,
probe_path,
probe_extra_args,
)
.await
.inspect_err(|err| {
tracing::error!("{err}");
})?,
} => {
// Parse server configs
let mut servers = Vec::new();
for s in server {
match parse_server_config(s) {
Ok(config) => servers.push(config),
Err(e) => {
tracing::error!("Invalid server config '{}': {}", s, e);
anyhow::bail!("Invalid server config '{}': {}", s, e);
}
}
}
run_probe::run_probe(&servers, probe_path, mnemonic, probe_extra_args)
.await
.inspect_err(|err| {
tracing::error!("{err}");
})?
}
Command::GenerateKeypair { path } => {
let path = path
.to_owned()
@@ -1,33 +1,143 @@
use crate::cli::GwProbe;
use anyhow::Context;
use nym_crypto::asymmetric::ed25519::PrivateKey;
use crate::cli::{GwProbe, ServerConfig};
pub(crate) async fn run_probe(
server_ip: &str,
server_port: u16,
ns_api_auth_key: &str,
servers: &[ServerConfig],
probe_path: &str,
mnemonic: &str,
probe_extra_args: &Vec<String>,
) -> anyhow::Result<()> {
let auth_key = PrivateKey::from_base58_string(ns_api_auth_key)
.context("Couldn't parse auth key, exiting")?;
let ns_api_client = nym_node_status_client::NsApiClient::new(server_ip, server_port, auth_key);
if servers.is_empty() {
anyhow::bail!("No servers configured");
}
let probe = GwProbe::new(probe_path.to_string());
let version = probe.version().await;
tracing::info!("Probe version:\n{}", version);
if let Some(testrun) = ns_api_client.request_testrun().await? {
let log = probe.run_and_get_log(&Some(testrun.gateway_identity_key), probe_extra_args);
// Always use first server as primary
let primary_server = &servers[0];
tracing::info!(
"Requesting testrun from primary server: {}:{}",
primary_server.address,
primary_server.port
);
ns_api_client
.submit_results(testrun.testrun_id, log, testrun.assigned_at_utc)
.await?;
} else {
tracing::info!("No testruns available, exiting")
let auth_key = nym_crypto::asymmetric::ed25519::PrivateKey::from_bytes(
&primary_server.auth_key.to_bytes(),
)
.expect("Failed to clone auth key");
let ns_api_client = nym_node_status_client::NsApiClient::new(
&primary_server.address,
primary_server.port,
auth_key,
);
match ns_api_client.request_testrun().await {
Ok(Some(testrun)) => {
tracing::info!(
"Received testrun {} for gateway {} from primary",
testrun.testrun_id,
testrun.gateway_identity_key
);
// Run the probe
let log = probe.run_and_get_log(
&Some(testrun.gateway_identity_key.clone()),
mnemonic,
probe_extra_args,
);
// Submit to ALL servers in parallel
let handles = servers
.iter()
.enumerate()
.map(|(idx, server)| {
let testrun = testrun.clone();
let log = log.clone();
async move {
let auth_key = nym_crypto::asymmetric::ed25519::PrivateKey::from_bytes(
&server.auth_key.to_bytes(),
)
.expect("Failed to clone auth key");
let client = nym_node_status_client::NsApiClient::new(
&server.address,
server.port,
auth_key,
);
let result = if idx == 0 {
// Primary server: submit regular results without context
client
.submit_results(
testrun.testrun_id as i64,
log,
testrun.assigned_at_utc,
)
.await
} else {
// Other servers: submit results with context
client
.submit_results_with_context(
testrun.testrun_id,
log,
testrun.assigned_at_utc,
testrun.gateway_identity_key,
)
.await
};
(idx, server.address.clone(), server.port, result)
}
})
.collect::<Vec<_>>();
let results = futures::future::join_all(handles).await;
for result in results {
match result.3 {
Ok(()) => {
let method = if result.0 == 0 {
"regular"
} else {
"with context"
};
tracing::info!(
"✅ Successfully submitted {} to server[{}] {}:{}",
method,
result.0,
result.1,
result.2
);
}
Err(e) => {
let method = if result.0 == 0 {
"regular"
} else {
"with context"
};
tracing::warn!(
"❌ Failed to submit {} to server[{}] {}:{} - {}",
method,
result.0,
result.1,
result.2,
e
);
}
}
}
Ok(())
}
Ok(None) => {
tracing::info!("No testruns available from primary server");
Ok(())
}
Err(e) => {
tracing::error!("Failed to contact primary server: {}", e);
Err(e)
}
}
Ok(())
}
@@ -73,6 +73,7 @@ impl GwProbe {
pub(crate) fn run_and_get_log(
&self,
gateway_key: &Option<String>,
mnemonic: &str,
probe_extra_args: &Vec<String>,
) -> String {
let mut command = std::process::Command::new(&self.path);
@@ -81,6 +82,7 @@ impl GwProbe {
if let Some(gateway_id) = gateway_key {
command.arg("--gateway").arg(gateway_id);
}
command.arg("--mnemonic").arg(mnemonic);
tracing::info!("Extra args for the probe:");
for arg in probe_extra_args {
@@ -0,0 +1,32 @@
# Example environment variables for nym-node-status-api
# Database configuration
# For SQLite:
# DATABASE_URL=sqlite://nym-node-status-api.sqlite
# For PostgreSQL:
# DATABASE_URL=postgres://testuser:testpass@localhost:5433/nym_node_status_api_test
# Network configuration
NETWORK_NAME=sandbox
NYM_API=https://sandbox-nym-api1.nymtech.net/api
NYXD=https://rpc.sandbox.nymtech.net
# API configuration
NYM_NODE_STATUS_API_HTTP_PORT=8000
NYM_API_CLIENT_TIMEOUT=15
SQLX_BUSY_TIMEOUT_S=5
# Monitoring intervals
NODE_STATUS_API_MONITOR_REFRESH_INTERVAL=300
NODE_STATUS_API_TESTRUN_REFRESH_INTERVAL=300
NODE_STATUS_API_GEODATA_TTL=86400
# Agent keys (comma-separated list)
NODE_STATUS_API_AGENT_KEY_LIST=
# External service tokens
IPINFO_API_TOKEN=your_token_here
# MixNodes (Optional)
DATA_PROVIDER_DELEGATION_LIST=
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO mixnode_daily_stats (\n mix_id, date_utc,\n total_stake, packets_received,\n packets_sent, packets_dropped\n ) VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT(mix_id, date_utc) DO UPDATE SET\n total_stake = excluded.total_stake,\n packets_received = mixnode_daily_stats.packets_received + excluded.packets_received,\n packets_sent = mixnode_daily_stats.packets_sent + excluded.packets_sent,\n packets_dropped = mixnode_daily_stats.packets_dropped + excluded.packets_dropped\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "01ee4a30bc3104712e5bc371a45d614a89d88adf02358800433e06100c13c548"
}
@@ -1,32 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT mix_id as node_id, host, http_api_port\n FROM mixnodes\n WHERE bonded = true\n ",
"describe": {
"columns": [
{
"name": "node_id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "host",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "http_api_port",
"ordinal": 2,
"type_info": "Integer"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false
]
},
"hash": "021c6c65d1ed806d8430bef7883906b42a7e4b280c8efb32db15d7c6a51d7a27"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO mixnode_description (\n mix_id, moniker, website, security_contact, details, last_updated_utc\n ) VALUES (?, ?, ?, ?, ?, ?)\n ON CONFLICT (mix_id) DO UPDATE SET\n moniker = excluded.moniker,\n website = excluded.website,\n security_contact = excluded.security_contact,\n details = excluded.details,\n last_updated_utc = excluded.last_updated_utc\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 6
},
"nullable": []
},
"hash": "06065394c157927e4002ddd5c7c1af626ae15728d615f539470cd7c189312385"
}
@@ -1,20 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT\n gateway_identity_key\n FROM\n gateways\n WHERE\n id = ?",
"describe": {
"columns": [
{
"name": "gateway_identity_key",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "06b17d1e5f61201a1b7542896ba55c69cd5c1a7e7d87073c94600c783a0a3984"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE\n testruns\n SET\n status = ?\n WHERE\n status = ?\n AND\n last_assigned_utc < ?\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "0cf0e4d4f30e90caecffd6255ef85dab12730e538be194438f19ed7f198bd50e"
}
@@ -1,44 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n date_utc as \"date_utc!\",\n SUM(total_stake) as \"total_stake!: i64\",\n SUM(packets_received) as \"total_packets_received!: i64\",\n SUM(packets_sent) as \"total_packets_sent!: i64\",\n SUM(packets_dropped) as \"total_packets_dropped!: i64\"\n FROM (\n SELECT\n date_utc,\n n.total_stake,\n n.packets_received,\n n.packets_sent,\n n.packets_dropped\n FROM nym_node_daily_mixing_stats n\n UNION ALL\n SELECT\n m.date_utc,\n m.total_stake,\n m.packets_received,\n m.packets_sent,\n m.packets_dropped\n FROM mixnode_daily_stats m\n LEFT JOIN nym_node_daily_mixing_stats ON m.mix_id = nym_node_daily_mixing_stats.node_id\n WHERE nym_node_daily_mixing_stats.node_id IS NULL\n )\n GROUP BY date_utc\n ORDER BY date_utc ASC\n ",
"describe": {
"columns": [
{
"name": "date_utc!",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "total_stake!: i64",
"ordinal": 1,
"type_info": "Integer"
},
{
"name": "total_packets_received!: i64",
"ordinal": 2,
"type_info": "Integer"
},
{
"name": "total_packets_sent!: i64",
"ordinal": 3,
"type_info": "Integer"
},
{
"name": "total_packets_dropped!: i64",
"ordinal": 4,
"type_info": "Integer"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
true,
true,
true
]
},
"hash": "124d45b9604439584650f401607c46bdbd162c7c689f74fe9ddfdfd48f5ddc07"
}
@@ -1,32 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT\n key as \"key!\",\n value_json as \"value_json!\",\n last_updated_utc as \"last_updated_utc!\"\n FROM summary",
"describe": {
"columns": [
{
"name": "key!",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "value_json!",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "last_updated_utc!",
"ordinal": 2,
"type_info": "Integer"
}
],
"parameters": {
"Right": 0
},
"nullable": [
true,
true,
false
]
},
"hash": "1327b5118f9144dddbcf8edb11f7dc549cf503409fd6dfedcdc02dbcd61d5454"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO mixnode_packet_stats_raw (\n mix_id, timestamp_utc, packets_received, packets_sent, packets_dropped\n ) VALUES (?, ?, ?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "21e44766729777756f6eb04bf3b81df3e591008a1e3fd664ed83ca86ac51bd8c"
}
@@ -1,26 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id,\n gateway_identity_key\n FROM gateways\n WHERE id = ?\n LIMIT 1",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "gateway_identity_key",
"ordinal": 1,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false
]
},
"hash": "2236299f9f691376db54cbd58ec5ceb89b9925cba46efcf4ed79ef0759a01129"
}
@@ -1,26 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT\n node_id,\n bond_info as \"bond_info: serde_json::Value\"\n FROM\n nym_nodes\n WHERE\n bond_info IS NOT NULL\n AND\n self_described IS NOT NULL\n ",
"describe": {
"columns": [
{
"name": "node_id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "bond_info: serde_json::Value",
"ordinal": 1,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
true
]
},
"hash": "227539374e7473f6f9642289c5b5d1bcd636315ab23537cb5f6d2f82a2bcb7bf"
}
@@ -1,20 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT gateway_identity_key\n FROM gateways\n WHERE bonded = true\n ",
"describe": {
"columns": [
{
"name": "gateway_identity_key",
"ordinal": 0,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false
]
},
"hash": "25300e435780101fa207c8e26ef2f49ba5db84d63e89440bb494e8327fe73686"
}
@@ -1,86 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT\n node_id,\n ed25519_identity_pubkey,\n total_stake,\n ip_addresses as \"ip_addresses!: serde_json::Value\",\n mix_port,\n x25519_sphinx_pubkey,\n node_role as \"node_role: serde_json::Value\",\n supported_roles as \"supported_roles: serde_json::Value\",\n entry as \"entry: serde_json::Value\",\n performance,\n self_described as \"self_described: serde_json::Value\",\n bond_info as \"bond_info: serde_json::Value\"\n FROM\n nym_nodes\n WHERE\n self_described IS NOT NULL\n AND\n bond_info IS NOT NULL\n ",
"describe": {
"columns": [
{
"name": "node_id",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "ed25519_identity_pubkey",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "total_stake",
"ordinal": 2,
"type_info": "Integer"
},
{
"name": "ip_addresses!: serde_json::Value",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "mix_port",
"ordinal": 4,
"type_info": "Integer"
},
{
"name": "x25519_sphinx_pubkey",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "node_role: serde_json::Value",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "supported_roles: serde_json::Value",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "entry: serde_json::Value",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "performance",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "self_described: serde_json::Value",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "bond_info: serde_json::Value",
"ordinal": 11,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false,
false,
false,
false,
false,
true,
false,
true,
true
]
},
"hash": "283f49a65c7d70bf271702ff6a5c7ad6e68c81932d295ff18ed198c54706a57c"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "INSERT OR IGNORE INTO gateway_session_stats\n (gateway_identity_key, node_id, day,\n unique_active_clients, session_started, users_hashes,\n vpn_sessions, mixnet_sessions, unknown_sessions)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 9
},
"nullable": []
},
"hash": "3243cf5646255a9430d1e6710970505d0dbcc62703f40e090e80ff48c77723c4"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "INSERT INTO mixnodes\n (mix_id, identity_key, bonded, total_stake,\n host, http_api_port, full_details,\n self_described, last_updated_utc, is_dp_delegatee)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(mix_id) DO UPDATE SET\n bonded=excluded.bonded,\n total_stake=excluded.total_stake, host=excluded.host,\n http_api_port=excluded.http_api_port,\n full_details=excluded.full_details,self_described=excluded.self_described,\n last_updated_utc=excluded.last_updated_utc,\n is_dp_delegatee = excluded.is_dp_delegatee;",
"describe": {
"columns": [],
"parameters": {
"Right": 10
},
"nullable": []
},
"hash": "3cd5cb4bfca4243925da4ddbccd811e842090e98982e1032670df77961870b32"
}
@@ -1,38 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT\n id as \"id!\",\n gateway_identity_key as \"gateway_identity_key!\",\n self_described as \"self_described?\",\n explorer_pretty_bond as \"explorer_pretty_bond?\"\n FROM gateways\n WHERE gateway_identity_key = ?\n AND bonded = true\n ORDER BY gateway_identity_key\n LIMIT 1",
"describe": {
"columns": [
{
"name": "id!",
"ordinal": 0,
"type_info": "Integer"
},
{
"name": "gateway_identity_key!",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "self_described?",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "explorer_pretty_bond?",
"ordinal": 3,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
true,
false,
false,
true
]
},
"hash": "3e7e987780937873cdb393b157d7708c9f01047b0689eb0d4f7a973b328c609d"
}
@@ -1,92 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT\n gw.gateway_identity_key as \"gateway_identity_key!\",\n gw.bonded as \"bonded: bool\",\n gw.performance as \"performance!\",\n gw.self_described as \"self_described?\",\n gw.explorer_pretty_bond as \"explorer_pretty_bond?\",\n gw.last_probe_result as \"last_probe_result?\",\n gw.last_probe_log as \"last_probe_log?\",\n gw.last_testrun_utc as \"last_testrun_utc?\",\n gw.last_updated_utc as \"last_updated_utc!\",\n COALESCE(gd.moniker, \"NA\") as \"moniker!\",\n COALESCE(gd.website, \"NA\") as \"website!\",\n COALESCE(gd.security_contact, \"NA\") as \"security_contact!\",\n COALESCE(gd.details, \"NA\") as \"details!\"\n FROM gateways gw\n LEFT JOIN gateway_description gd\n ON gw.gateway_identity_key = gd.gateway_identity_key\n ORDER BY gw.gateway_identity_key",
"describe": {
"columns": [
{
"name": "gateway_identity_key!",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "bonded: bool",
"ordinal": 1,
"type_info": "Integer"
},
{
"name": "performance!",
"ordinal": 2,
"type_info": "Integer"
},
{
"name": "self_described?",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "explorer_pretty_bond?",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "last_probe_result?",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "last_probe_log?",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "last_testrun_utc?",
"ordinal": 7,
"type_info": "Integer"
},
{
"name": "last_updated_utc!",
"ordinal": 8,
"type_info": "Integer"
},
{
"name": "moniker!",
"ordinal": 9,
"type_info": "Text"
},
{
"name": "website!",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "security_contact!",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "details!",
"ordinal": 12,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
false,
false,
false,
true,
true,
true,
true,
false,
false,
false,
false,
false
]
},
"hash": "3eb1d8491bda3c1d6e071b6eb364b9a979f4bdb11ea81b2d0f022555bab51ecb"
}
@@ -1,20 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n total_stake\n FROM mixnodes\n WHERE mix_id = ?\n ",
"describe": {
"columns": [
{
"name": "total_stake",
"ordinal": 0,
"type_info": "Integer"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false
]
},
"hash": "3fc2baabf194b147b20be2a49401cc0c100a1d7a7c347393adde2410fa6f4dfe"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE testruns SET status = ? WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "418944f2eccb838cb3882f34469203c8569f03fdd39ce09d7b74177896e52a8c"
}
@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE gateways SET last_probe_log = ? WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "4afcc6673890f795c2793f1e2f8570ee787fc7daf00fcb916f18d1cb7d6c8f08"
}

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