Merge pull request #5937 from nymtech/release/2025.14-feta
This commit is contained in:
@@ -38,15 +38,14 @@ jobs:
|
||||
rm -rf ci-builds || true
|
||||
mkdir -p $OUTPUT_DIR
|
||||
echo $OUTPUT_DIR
|
||||
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libudev-dev
|
||||
|
||||
- name: Sets env vars for tokio if set in manual dispatch inputs
|
||||
run: |
|
||||
echo 'RUSTFLAGS="--cfg tokio_unstable"' >> $GITHUB_ENV
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true
|
||||
|
||||
run: |
|
||||
echo "RUSTFLAGS=--cfg tokio_unstable" >> $GITHUB_ENV
|
||||
echo "CARGO_FEATURES=--features tokio-console" >> $GITHUB_ENV
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
@@ -103,7 +102,6 @@ jobs:
|
||||
if [ ${{ github.event_name == 'workflow_dispatch' && inputs.enable_deb == true }} = true ]; then
|
||||
cp target/debian/*.deb $OUTPUT_DIR
|
||||
fi
|
||||
|
||||
- name: Deploy branch to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Run SonarQube Scan
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
# pull_request:
|
||||
# types: [opened, synchronize, reopened]
|
||||
jobs:
|
||||
sonarqube:
|
||||
name: SonarQube
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@v5
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
+1112
-1090
File diff suppressed because it is too large
Load Diff
+11
-3
@@ -39,7 +39,8 @@ members = [
|
||||
"common/cosmwasm-smart-contracts/ecash-contract",
|
||||
"common/cosmwasm-smart-contracts/group-contract",
|
||||
"common/cosmwasm-smart-contracts/mixnet-contract",
|
||||
"common/cosmwasm-smart-contracts/multisig-contract", "common/cosmwasm-smart-contracts/nym-performance-contract",
|
||||
"common/cosmwasm-smart-contracts/multisig-contract",
|
||||
"common/cosmwasm-smart-contracts/nym-performance-contract",
|
||||
"common/cosmwasm-smart-contracts/nym-pool-contract",
|
||||
"common/cosmwasm-smart-contracts/vesting-contract",
|
||||
"common/credential-storage",
|
||||
@@ -49,6 +50,8 @@ members = [
|
||||
"common/credentials-interface",
|
||||
"common/crypto",
|
||||
"common/dkg",
|
||||
"common/ecash-signer-check",
|
||||
"common/ecash-signer-check-types",
|
||||
"common/ecash-time",
|
||||
"common/execute",
|
||||
"common/exit-policy",
|
||||
@@ -99,7 +102,7 @@ members = [
|
||||
"common/wasm/storage",
|
||||
"common/wasm/utils",
|
||||
"common/wireguard",
|
||||
"common/wireguard-types",
|
||||
"common/wireguard-types", "common/zulip-client",
|
||||
"documentation/autodoc",
|
||||
"gateway",
|
||||
"nym-api",
|
||||
@@ -218,7 +221,7 @@ clap_complete_fig = "4.5"
|
||||
colored = "2.2"
|
||||
comfy-table = "7.1.4"
|
||||
console = "0.15.11"
|
||||
console-subscriber = "0.1.1"
|
||||
console-subscriber = "0.4.1"
|
||||
console_error_panic_hook = "0.1"
|
||||
const-str = "0.5.6"
|
||||
const_format = "0.2.34"
|
||||
@@ -234,6 +237,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"
|
||||
@@ -434,6 +438,9 @@ opt-level = 'z'
|
||||
# lto = true
|
||||
opt-level = 'z'
|
||||
|
||||
[workspace.lints.rust]
|
||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] }
|
||||
|
||||
[workspace.lints.clippy]
|
||||
unwrap_used = "deny"
|
||||
expect_used = "deny"
|
||||
@@ -443,3 +450,4 @@ exit = "deny"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.59"
|
||||
version = "1.1.60"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -111,7 +111,7 @@ impl SocketClient {
|
||||
let dkg_query_client = if self.config.base.client.disabled_credentials_mode {
|
||||
None
|
||||
} else {
|
||||
Some(default_query_dkg_client_from_config(&self.config.base))
|
||||
Some(default_query_dkg_client_from_config(&self.config.base)?)
|
||||
};
|
||||
|
||||
let storage = self.initialise_storage().await?;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.59"
|
||||
version = "1.1.60"
|
||||
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,
|
||||
|
||||
@@ -13,6 +13,7 @@ async-trait = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
bs58 = { workspace = true }
|
||||
clap = { workspace = true, optional = true }
|
||||
cfg-if = { workspace = true }
|
||||
comfy-table = { workspace = true, optional = true }
|
||||
futures = { workspace = true }
|
||||
humantime = { workspace = true }
|
||||
@@ -123,3 +124,6 @@ fs-surb-storage = ["nym-client-core-surb-storage/fs-surb-storage"]
|
||||
fs-gateways-storage = ["nym-client-core-gateways-storage/fs-gateways-storage"]
|
||||
wasm = ["nym-gateway-client/wasm"]
|
||||
metrics-server = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -58,6 +58,7 @@ where
|
||||
Some(data) => data,
|
||||
None => {
|
||||
// SAFETY: one of those arguments must have been set
|
||||
#[allow(clippy::unwrap_used)]
|
||||
fs::read(common_args.signatures_path.unwrap())?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -64,6 +64,7 @@ where
|
||||
Some(data) => data,
|
||||
None => {
|
||||
// SAFETY: one of those arguments must have been set
|
||||
#[allow(clippy::unwrap_used)]
|
||||
fs::read(common_args.credential_path.unwrap())?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,6 +58,7 @@ where
|
||||
Some(data) => data,
|
||||
None => {
|
||||
// SAFETY: one of those arguments must have been set
|
||||
#[allow(clippy::unwrap_used)]
|
||||
fs::read(common_args.signatures_path.unwrap())?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -58,6 +58,7 @@ where
|
||||
Some(data) => data,
|
||||
None => {
|
||||
// SAFETY: one of those arguments must have been set
|
||||
#[allow(clippy::unwrap_used)]
|
||||
fs::read(common_args.key_path.unwrap())?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -135,9 +135,11 @@ pub enum ClientInputStatus {
|
||||
}
|
||||
|
||||
impl ClientInputStatus {
|
||||
#[allow(clippy::panic)]
|
||||
pub fn register_producer(&mut self) -> ClientInput {
|
||||
match std::mem::replace(self, ClientInputStatus::Connected) {
|
||||
ClientInputStatus::AwaitingProducer { client_input } => client_input,
|
||||
// critical failure implying misuse of software
|
||||
ClientInputStatus::Connected => panic!("producer was already registered before"),
|
||||
}
|
||||
}
|
||||
@@ -149,9 +151,11 @@ pub enum ClientOutputStatus {
|
||||
}
|
||||
|
||||
impl ClientOutputStatus {
|
||||
#[allow(clippy::panic)]
|
||||
pub fn register_consumer(&mut self) -> ClientOutput {
|
||||
match std::mem::replace(self, ClientOutputStatus::Connected) {
|
||||
ClientOutputStatus::AwaitingConsumer { client_output } => client_output,
|
||||
// critical failure implying misuse of software
|
||||
ClientOutputStatus::Connected => panic!("consumer was already registered before"),
|
||||
}
|
||||
}
|
||||
@@ -707,11 +711,14 @@ where
|
||||
})?;
|
||||
|
||||
let store_clone = mem_store.clone();
|
||||
spawn_future(async move {
|
||||
persistent_storage
|
||||
.flush_on_shutdown(store_clone, shutdown)
|
||||
.await
|
||||
});
|
||||
spawn_future!(
|
||||
async move {
|
||||
persistent_storage
|
||||
.flush_on_shutdown(store_clone, shutdown)
|
||||
.await
|
||||
},
|
||||
"PersistentReplyStorage::flush_on_shutdown"
|
||||
);
|
||||
|
||||
Ok(mem_store)
|
||||
}
|
||||
@@ -732,7 +739,7 @@ where
|
||||
let mut rng = OsRng;
|
||||
let keys = if let Some(derivation_material) = derivation_material {
|
||||
ClientKeys::from_master_key(&mut rng, &derivation_material)
|
||||
.map_err(|_| ClientCoreError::HkdfDerivationError {})?
|
||||
.map_err(|_| ClientCoreError::HkdfDerivationError)?
|
||||
} else {
|
||||
ClientKeys::generate_new(&mut rng)
|
||||
};
|
||||
|
||||
@@ -114,41 +114,32 @@ pub async fn setup_fs_gateways_storage<P: AsRef<Path>>(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_bandwidth_controller<St: CredentialStorage>(
|
||||
config: &Config,
|
||||
storage: St,
|
||||
) -> BandwidthController<QueryHttpRpcNyxdClient, St> {
|
||||
let nyxd_url = config
|
||||
.get_validator_endpoints()
|
||||
.pop()
|
||||
.expect("No nyxd validator endpoint provided");
|
||||
|
||||
create_bandwidth_controller_with_urls(nyxd_url, storage)
|
||||
}
|
||||
|
||||
pub fn create_bandwidth_controller_with_urls<St: CredentialStorage>(
|
||||
nyxd_url: Url,
|
||||
storage: St,
|
||||
) -> BandwidthController<QueryHttpRpcNyxdClient, St> {
|
||||
let client = default_query_dkg_client(nyxd_url);
|
||||
) -> Result<BandwidthController<QueryHttpRpcNyxdClient, St>, ClientCoreError> {
|
||||
let client = default_query_dkg_client(nyxd_url)?;
|
||||
|
||||
BandwidthController::new(storage, client)
|
||||
Ok(BandwidthController::new(storage, client))
|
||||
}
|
||||
|
||||
pub fn default_query_dkg_client_from_config(config: &Config) -> QueryHttpRpcNyxdClient {
|
||||
pub fn default_query_dkg_client_from_config(
|
||||
config: &Config,
|
||||
) -> Result<QueryHttpRpcNyxdClient, ClientCoreError> {
|
||||
let nyxd_url = config
|
||||
.get_validator_endpoints()
|
||||
.pop()
|
||||
.expect("No nyxd validator endpoint provided");
|
||||
.ok_or(ClientCoreError::RpcClientMissingUrl)?;
|
||||
|
||||
default_query_dkg_client(nyxd_url)
|
||||
}
|
||||
|
||||
pub fn default_query_dkg_client(nyxd_url: Url) -> QueryHttpRpcNyxdClient {
|
||||
pub fn default_query_dkg_client(nyxd_url: Url) -> Result<QueryHttpRpcNyxdClient, ClientCoreError> {
|
||||
let details = nym_network_defaults::NymNetworkDetails::new_from_env();
|
||||
let client_config = nyxd::Config::try_from_nym_network_details(&details)
|
||||
.expect("failed to construct validator client config");
|
||||
.map_err(|source| ClientCoreError::InvalidNetworkDetails { source })?;
|
||||
// overwrite env configuration with config URLs
|
||||
|
||||
QueryHttpRpcNyxdClient::connect(client_config, nyxd_url.as_str())
|
||||
.expect("Could not construct query client")
|
||||
.map_err(|source| ClientCoreError::RpcClientCreationFailure { source })
|
||||
}
|
||||
|
||||
@@ -235,6 +235,7 @@ impl LoopCoverTrafficStream<OsRng> {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
|
||||
#[allow(clippy::panic)]
|
||||
pub fn start(mut self) {
|
||||
if self.cover_traffic.disable_loop_cover_traffic_stream {
|
||||
// we should have never got here in the first place - the task should have never been created to begin with
|
||||
@@ -251,27 +252,30 @@ impl LoopCoverTrafficStream<OsRng> {
|
||||
|
||||
let mut shutdown = self.task_client.fork("select");
|
||||
|
||||
spawn_future(async move {
|
||||
debug!("Started LoopCoverTrafficStream with graceful shutdown support");
|
||||
spawn_future!(
|
||||
async move {
|
||||
debug!("Started LoopCoverTrafficStream with graceful shutdown support");
|
||||
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.recv() => {
|
||||
tracing::trace!("LoopCoverTrafficStream: Received shutdown");
|
||||
}
|
||||
next = self.next() => {
|
||||
if next.is_some() {
|
||||
self.on_new_message().await;
|
||||
} else {
|
||||
tracing::trace!("LoopCoverTrafficStream: Stopping since channel closed");
|
||||
break;
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.recv() => {
|
||||
tracing::trace!("LoopCoverTrafficStream: Received shutdown");
|
||||
}
|
||||
next = self.next() => {
|
||||
if next.is_some() {
|
||||
self.on_new_message().await;
|
||||
} else {
|
||||
tracing::trace!("LoopCoverTrafficStream: Stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
shutdown.recv_timeout().await;
|
||||
tracing::debug!("LoopCoverTrafficStream: Exiting");
|
||||
})
|
||||
shutdown.recv_timeout().await;
|
||||
tracing::debug!("LoopCoverTrafficStream: Exiting");
|
||||
},
|
||||
"LoopCoverTrafficStream"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +98,8 @@ impl MixTrafficController {
|
||||
debug_assert!(!mix_packets.is_empty());
|
||||
|
||||
let result = if mix_packets.len() == 1 {
|
||||
// SAFETY: we just checked we have one packet
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let mix_packet = mix_packets.pop().unwrap();
|
||||
self.gateway_transceiver.send_mix_packet(mix_packet).await
|
||||
} else {
|
||||
@@ -117,51 +119,54 @@ impl MixTrafficController {
|
||||
}
|
||||
|
||||
pub fn start(mut self) {
|
||||
spawn_future(async move {
|
||||
debug!("Started MixTrafficController with graceful shutdown support");
|
||||
spawn_future!(
|
||||
async move {
|
||||
debug!("Started MixTrafficController with graceful shutdown support");
|
||||
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
mix_packets = self.mix_rx.recv() => match mix_packets {
|
||||
Some(mix_packets) => {
|
||||
if let Err(err) = self.on_messages(mix_packets).await {
|
||||
error!("Failed to send sphinx packet(s) to the gateway: {err}");
|
||||
if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT {
|
||||
// Disconnect from the gateway. If we should try to re-connect
|
||||
// is handled at a higher layer.
|
||||
error!("Failed to send sphinx packet to the gateway {MAX_FAILURE_COUNT} times in a row - assuming the gateway is dead");
|
||||
// Do we need to handle the embedded mixnet client case
|
||||
// separately?
|
||||
self.task_client.send_we_stopped(Box::new(ClientCoreError::GatewayFailedToForwardMessages));
|
||||
break;
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
mix_packets = self.mix_rx.recv() => match mix_packets {
|
||||
Some(mix_packets) => {
|
||||
if let Err(err) = self.on_messages(mix_packets).await {
|
||||
error!("Failed to send sphinx packet(s) to the gateway: {err}");
|
||||
if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT {
|
||||
// Disconnect from the gateway. If we should try to re-connect
|
||||
// is handled at a higher layer.
|
||||
error!("Failed to send sphinx packet to the gateway {MAX_FAILURE_COUNT} times in a row - assuming the gateway is dead");
|
||||
// Do we need to handle the embedded mixnet client case
|
||||
// separately?
|
||||
self.task_client.send_we_stopped(Box::new(ClientCoreError::GatewayFailedToForwardMessages));
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
tracing::trace!("MixTrafficController: Stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
},
|
||||
None => {
|
||||
tracing::trace!("MixTrafficController: Stopping since channel closed");
|
||||
client_request = self.client_rx.recv() => match client_request {
|
||||
Some(client_request) => {
|
||||
match self.gateway_transceiver.send_client_request(client_request).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => error!("Failed to send client request: {e}"),
|
||||
};
|
||||
},
|
||||
None => {
|
||||
tracing::trace!("MixTrafficController, client request channel closed");
|
||||
}
|
||||
},
|
||||
_ = self.task_client.recv() => {
|
||||
tracing::trace!("MixTrafficController: Received shutdown");
|
||||
break;
|
||||
}
|
||||
},
|
||||
client_request = self.client_rx.recv() => match client_request {
|
||||
Some(client_request) => {
|
||||
match self.gateway_transceiver.send_client_request(client_request).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => error!("Failed to send client request: {e}"),
|
||||
};
|
||||
},
|
||||
None => {
|
||||
tracing::trace!("MixTrafficController, client request channel closed");
|
||||
}
|
||||
},
|
||||
_ = self.task_client.recv() => {
|
||||
tracing::trace!("MixTrafficController: Received shutdown");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.task_client.recv_timeout().await;
|
||||
self.task_client.recv_timeout().await;
|
||||
|
||||
tracing::debug!("MixTrafficController: Exiting");
|
||||
});
|
||||
tracing::debug!("MixTrafficController: Exiting");
|
||||
},
|
||||
"MixTrafficController"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +269,8 @@ pub struct MockGateway {
|
||||
}
|
||||
|
||||
impl Default for MockGateway {
|
||||
// test code
|
||||
#[allow(clippy::unwrap_used)]
|
||||
fn default() -> Self {
|
||||
MockGateway {
|
||||
dummy_identity: "3ebjp1Fb9hdcS1AR6AZihgeJiMHkB5jjJUsvqNnfQwU7"
|
||||
|
||||
+3
-1
@@ -194,10 +194,11 @@ impl ActionController {
|
||||
trace!("{frag_id} is updating its delay");
|
||||
// TODO: is it possible to solve this without either locking or temporarily removing the value?
|
||||
if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.remove(&frag_id) {
|
||||
// this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets)
|
||||
// SAFETY: this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets)
|
||||
// or `ReplyController` (for 'reply' packets) which held the other potential
|
||||
// reference to this Arc. HOWEVER, before the Action was pushed onto the queue, the reference
|
||||
// was dropped hence this unwrap is safe.
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let mut inner_data = Arc::try_unwrap(pending_ack_data).unwrap();
|
||||
inner_data.update_retransmitted(delay);
|
||||
|
||||
@@ -209,6 +210,7 @@ impl ActionController {
|
||||
}
|
||||
|
||||
// note: when the entry expires it's automatically removed from pending_acks_timers
|
||||
#[allow(clippy::panic)]
|
||||
fn handle_expired_ack_timer(&mut self, expired_ack: Expired<FragmentIdentifier>) {
|
||||
let frag_id = expired_ack.into_inner();
|
||||
|
||||
|
||||
+4
-1
@@ -120,6 +120,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::panic)]
|
||||
async fn on_input_message(&mut self, msg: InputMessage) {
|
||||
match msg {
|
||||
InputMessage::Regular {
|
||||
@@ -213,7 +214,9 @@ where
|
||||
self.handle_premade_packets(msgs, lane).await
|
||||
}
|
||||
// MessageWrappers can't be nested
|
||||
InputMessage::MessageWrapper { .. } => unimplemented!(),
|
||||
InputMessage::MessageWrapper { .. } => {
|
||||
panic!("attempted to use nested MessageWrapper")
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
+35
-20
@@ -298,29 +298,44 @@ where
|
||||
let mut sent_notification_listener = self.sent_notification_listener;
|
||||
let mut action_controller = self.action_controller;
|
||||
|
||||
spawn_future(async move {
|
||||
acknowledgement_listener.run().await;
|
||||
debug!("The acknowledgement listener has finished execution!");
|
||||
});
|
||||
spawn_future!(
|
||||
async move {
|
||||
acknowledgement_listener.run().await;
|
||||
debug!("The acknowledgement listener has finished execution!");
|
||||
},
|
||||
"AcknowledgementController::AcknowledgementListener"
|
||||
);
|
||||
|
||||
spawn_future(async move {
|
||||
input_message_listener.run().await;
|
||||
debug!("The input listener has finished execution!");
|
||||
});
|
||||
spawn_future!(
|
||||
async move {
|
||||
input_message_listener.run().await;
|
||||
debug!("The input listener has finished execution!");
|
||||
},
|
||||
"AcknowledgementController::InputMessageListener"
|
||||
);
|
||||
|
||||
spawn_future(async move {
|
||||
retransmission_request_listener.run(packet_type).await;
|
||||
debug!("The retransmission request listener has finished execution!");
|
||||
});
|
||||
spawn_future!(
|
||||
async move {
|
||||
retransmission_request_listener.run(packet_type).await;
|
||||
debug!("The retransmission request listener has finished execution!");
|
||||
},
|
||||
"AcknowledgementController::RetransmissionRequestListener"
|
||||
);
|
||||
|
||||
spawn_future(async move {
|
||||
sent_notification_listener.run().await;
|
||||
debug!("The sent notification listener has finished execution!");
|
||||
});
|
||||
spawn_future!(
|
||||
async move {
|
||||
sent_notification_listener.run().await;
|
||||
debug!("The sent notification listener has finished execution!");
|
||||
},
|
||||
"AcknowledgementController::SentNotificationListener"
|
||||
);
|
||||
|
||||
spawn_future(async move {
|
||||
action_controller.run().await;
|
||||
debug!("The controller has finished execution!");
|
||||
});
|
||||
spawn_future!(
|
||||
async move {
|
||||
action_controller.run().await;
|
||||
debug!("The controller has finished execution!");
|
||||
},
|
||||
"AcknowledgementController::ActionController"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ pub enum PreparationError {
|
||||
#[error(transparent)]
|
||||
NymTopologyError(#[from] NymTopologyError),
|
||||
|
||||
#[error("message wasn't split into any fragments!")]
|
||||
EmptyFragments,
|
||||
|
||||
#[error("message too long for a single SURB, splitting into {fragments} fragments.")]
|
||||
MessageTooLongForSingleSurb { fragments: usize },
|
||||
|
||||
@@ -320,6 +323,16 @@ where
|
||||
});
|
||||
}
|
||||
|
||||
if fragment.is_empty() {
|
||||
error!("CRITICAL FAILURE: our split message didn't result in any sendable fragments");
|
||||
return Err(SurbWrappedPreparationError {
|
||||
source: PreparationError::EmptyFragments,
|
||||
returned_surbs: Some(vec![reply_surb]),
|
||||
});
|
||||
}
|
||||
|
||||
// SAFETY: we just checked we have one fragment
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let chunk = fragment.pop().unwrap();
|
||||
let chunk_clone = chunk.clone();
|
||||
let prepared_fragment = self
|
||||
@@ -657,6 +670,7 @@ where
|
||||
.zip(reply_surbs.into_iter())
|
||||
.map(|(fragment, reply_surb)| {
|
||||
// unwrap here is fine as we know we have a valid topology
|
||||
#[allow(clippy::unwrap_used)]
|
||||
self.message_preparer
|
||||
.prepare_reply_chunk_for_sending(
|
||||
fragment,
|
||||
|
||||
@@ -224,14 +224,20 @@ impl RealMessagesController<OsRng> {
|
||||
let ack_control = self.ack_control;
|
||||
let mut reply_control = self.reply_control;
|
||||
|
||||
spawn_future(async move {
|
||||
out_queue_control.run().await;
|
||||
debug!("The out queue controller has finished execution!");
|
||||
});
|
||||
spawn_future(async move {
|
||||
reply_control.run().await;
|
||||
debug!("The reply controller has finished execution!");
|
||||
});
|
||||
spawn_future!(
|
||||
async move {
|
||||
out_queue_control.run().await;
|
||||
debug!("The out queue controller has finished execution!");
|
||||
},
|
||||
"RealMessagesController::OutQueueControl)"
|
||||
);
|
||||
spawn_future!(
|
||||
async move {
|
||||
reply_control.run().await;
|
||||
debug!("The reply controller has finished execution!");
|
||||
},
|
||||
"RealMessagesController::ReplyController"
|
||||
);
|
||||
|
||||
ack_control.start(packet_type);
|
||||
}
|
||||
|
||||
@@ -249,6 +249,8 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
// SAFETY: our topology must be valid at this point
|
||||
#[allow(clippy::expect_used)]
|
||||
(
|
||||
generate_loop_cover_packet(
|
||||
&mut self.rng,
|
||||
@@ -439,6 +441,8 @@ where
|
||||
tracing::trace!("handling real_messages: size: {}", real_messages.len());
|
||||
|
||||
self.transmission_buffer.store(&conn_id, real_messages);
|
||||
// SAFETY: we just stored the message
|
||||
#[allow(clippy::expect_used)]
|
||||
let real_next = self.pop_next_message().expect("Just stored one");
|
||||
|
||||
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
|
||||
@@ -487,6 +491,8 @@ where
|
||||
|
||||
// First store what we got for the given connection id
|
||||
self.transmission_buffer.store(&conn_id, real_messages);
|
||||
// SAFETY: we just stored the message
|
||||
#[allow(clippy::expect_used)]
|
||||
let real_next = self.pop_next_message().expect("we just added one");
|
||||
|
||||
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
|
||||
|
||||
@@ -198,6 +198,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::panic)]
|
||||
async fn disconnect_sender(&mut self) {
|
||||
let mut guard = self.inner.lock().await;
|
||||
if guard.message_sender.is_none() {
|
||||
@@ -208,6 +209,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
|
||||
guard.message_sender = None;
|
||||
}
|
||||
|
||||
#[allow(clippy::panic)]
|
||||
async fn connect_sender(&mut self, sender: ReconstructedMessagesSender) {
|
||||
let mut guard = self.inner.lock().await;
|
||||
if guard.message_sender.is_some() {
|
||||
@@ -599,14 +601,20 @@ impl<R: MessageReceiver + Clone + Send + 'static> ReceivedMessagesBufferControll
|
||||
let mut fragmented_message_receiver = self.fragmented_message_receiver;
|
||||
let mut request_receiver = self.request_receiver;
|
||||
|
||||
spawn_future(async move {
|
||||
match fragmented_message_receiver.run().await {
|
||||
Ok(_) => {}
|
||||
Err(e) => error!("{e}"),
|
||||
}
|
||||
});
|
||||
spawn_future(async move {
|
||||
request_receiver.run().await;
|
||||
});
|
||||
spawn_future!(
|
||||
async move {
|
||||
match fragmented_message_receiver.run().await {
|
||||
Ok(_) => {}
|
||||
Err(e) => error!("{e}"),
|
||||
}
|
||||
},
|
||||
"ReceivedMessagesBufferController::FragmentedMessageReceiver"
|
||||
);
|
||||
spawn_future!(
|
||||
async move {
|
||||
request_receiver.run().await;
|
||||
},
|
||||
"ReceivedMessagesBufferController::RequestReceiver"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,8 +155,9 @@ where
|
||||
data: Vec<Arc<PendingAcknowledgement>>,
|
||||
) {
|
||||
trace!("re-inserting pending retransmissions for {recipient}");
|
||||
// the underlying entry MUST exist as we've just got data from there
|
||||
// SAFETY: the underlying entry MUST exist as we've just got data from there
|
||||
// and we hold a mut reference
|
||||
#[allow(clippy::expect_used)]
|
||||
let map_entry = &mut self
|
||||
.surb_senders
|
||||
.get_mut(recipient)
|
||||
@@ -429,6 +430,7 @@ where
|
||||
.pop_at_most_n_next_messages_at_random(amount)
|
||||
}
|
||||
|
||||
#[allow(clippy::panic)]
|
||||
async fn try_clear_pending_queue(&mut self, target: AnonymousSenderTag) {
|
||||
trace!("trying to clear pending queue");
|
||||
let available_surbs = self.surbs_storage.available_surbs(&target);
|
||||
|
||||
@@ -165,9 +165,12 @@ impl StatisticsControl {
|
||||
}
|
||||
|
||||
pub(crate) fn start(mut self) {
|
||||
spawn_future(async move {
|
||||
self.run().await;
|
||||
})
|
||||
spawn_future!(
|
||||
async move {
|
||||
self.run().await;
|
||||
},
|
||||
"StatisticsControl"
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn create_and_start(
|
||||
|
||||
@@ -145,36 +145,39 @@ impl TopologyRefresher {
|
||||
}
|
||||
|
||||
pub fn start(mut self) {
|
||||
spawn_future(async move {
|
||||
debug!("Started TopologyRefresher with graceful shutdown support");
|
||||
spawn_future!(
|
||||
async move {
|
||||
debug!("Started TopologyRefresher with graceful shutdown support");
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mut interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
|
||||
self.refresh_rate,
|
||||
));
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mut interval = tokio_stream::wrappers::IntervalStream::new(
|
||||
tokio::time::interval(self.refresh_rate),
|
||||
);
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let mut interval =
|
||||
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let mut interval =
|
||||
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32);
|
||||
|
||||
// We already have an initial topology, so no need to refresh it immediately.
|
||||
// My understanding is that js setInterval does not fire immediately, so it's not
|
||||
// needed there.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
interval.next().await;
|
||||
// We already have an initial topology, so no need to refresh it immediately.
|
||||
// My understanding is that js setInterval does not fire immediately, so it's not
|
||||
// needed there.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
interval.next().await;
|
||||
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = interval.next() => {
|
||||
self.try_refresh().await;
|
||||
},
|
||||
_ = self.task_client.recv() => {
|
||||
tracing::trace!("TopologyRefresher: Received shutdown");
|
||||
},
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = interval.next() => {
|
||||
self.try_refresh().await;
|
||||
},
|
||||
_ = self.task_client.recv() => {
|
||||
tracing::trace!("TopologyRefresher: Received shutdown");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
self.task_client.recv_timeout().await;
|
||||
tracing::debug!("TopologyRefresher: Exiting");
|
||||
})
|
||||
self.task_client.recv_timeout().await;
|
||||
tracing::debug!("TopologyRefresher: Exiting");
|
||||
},
|
||||
"TopologyRefresher"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ use nym_gateway_client::error::GatewayClientError;
|
||||
use nym_topology::node::RoutingNodeError;
|
||||
use nym_topology::{NodeId, NymTopologyError};
|
||||
use nym_validator_client::nym_api::error::NymAPIError;
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use nym_validator_client::ValidatorClientError;
|
||||
use rand::distributions::WeightedError;
|
||||
use std::error::Error;
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -230,7 +232,19 @@ pub enum ClientCoreError {
|
||||
UnexpectedKeyUpgrade { gateway_id: String },
|
||||
|
||||
#[error("failed to derive keys from master key")]
|
||||
HkdfDerivationError {},
|
||||
HkdfDerivationError,
|
||||
|
||||
#[error("missing url for constructing RPC client")]
|
||||
RpcClientMissingUrl,
|
||||
|
||||
#[error("provided nym network details were malformed: {source}")]
|
||||
InvalidNetworkDetails { source: NyxdError },
|
||||
|
||||
#[error("failed to construct RPC client: {source}")]
|
||||
RpcClientCreationFailure { source: NyxdError },
|
||||
|
||||
#[error("failed to select valid gateway due to incomputable latency")]
|
||||
GatewaySelectionFailure { source: WeightedError },
|
||||
}
|
||||
|
||||
impl From<tungstenite::Error> for ClientCoreError {
|
||||
|
||||
@@ -245,7 +245,7 @@ pub async fn choose_gateway_by_latency<R: Rng, G: ConnectableGateway + Clone>(
|
||||
let gateways_with_latency = gateways_with_latency.lock().await;
|
||||
let chosen = gateways_with_latency
|
||||
.choose_weighted(rng, |item| 1. / item.latency.as_secs_f32())
|
||||
.expect("invalid selection weight!");
|
||||
.map_err(|source| ClientCoreError::GatewaySelectionFailure { source })?;
|
||||
|
||||
info!(
|
||||
"chose gateway {} with average latency of {:?}",
|
||||
|
||||
@@ -18,18 +18,54 @@ pub use nym_topology::{
|
||||
};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub(crate) fn spawn_future<F>(future: F)
|
||||
pub fn spawn_future<F>(future: F)
|
||||
where
|
||||
F: Future<Output = ()> + 'static,
|
||||
{
|
||||
wasm_bindgen_futures::spawn_local(future);
|
||||
}
|
||||
|
||||
// TODO: expose similar API to the rest of the codebase,
|
||||
// perhaps with some simple trait for a task to define its name
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) fn spawn_future<F>(future: F)
|
||||
#[track_caller]
|
||||
pub fn spawn_future<F>(future: F)
|
||||
where
|
||||
F: Future + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
tokio::spawn(future);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[track_caller]
|
||||
pub fn spawn_named_future<F>(future: F, name: &str)
|
||||
where
|
||||
F: Future + Send + 'static,
|
||||
F::Output: Send + 'static,
|
||||
{
|
||||
cfg_if::cfg_if! {if #[cfg(tokio_unstable)] {
|
||||
#[allow(clippy::expect_used)]
|
||||
tokio::task::Builder::new().name(name).spawn(future).expect("failed to spawn future");
|
||||
} else {
|
||||
let _ = name;
|
||||
tracing::debug!(r#"the underlying binary hasn't been built with `RUSTFLAGS="--cfg tokio_unstable"` - the future naming won't do anything"#);
|
||||
spawn_future(future);
|
||||
}}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! spawn_future {
|
||||
($future:expr) => {{
|
||||
$crate::spawn_future($future)
|
||||
}};
|
||||
($future:expr, $name:expr) => {{
|
||||
cfg_if::cfg_if! {if #[cfg(not(target_arch = "wasm32"))] {
|
||||
$crate::spawn_named_future($future, $name)
|
||||
} else {
|
||||
let _ = $name;
|
||||
$crate::spawn_future($future)
|
||||
}}
|
||||
}};
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -6,16 +6,18 @@ use crate::nym_api::routes::{ecash, CORE_STATUS_COUNT, SINCE_ARG};
|
||||
use async_trait::async_trait;
|
||||
use nym_api_requests::ecash::models::{
|
||||
AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse,
|
||||
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationResponse,
|
||||
IssuedTicketbooksChallengeCommitmentRequest, IssuedTicketbooksChallengeCommitmentResponse,
|
||||
IssuedTicketbooksDataRequest, IssuedTicketbooksDataResponse, IssuedTicketbooksForCountResponse,
|
||||
IssuedTicketbooksForResponse, VerifyEcashTicketBody,
|
||||
BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashSignerStatusResponse,
|
||||
EcashTicketVerificationResponse, IssuedTicketbooksChallengeCommitmentRequest,
|
||||
IssuedTicketbooksChallengeCommitmentResponse, IssuedTicketbooksDataRequest,
|
||||
IssuedTicketbooksDataResponse, IssuedTicketbooksForCountResponse, IssuedTicketbooksForResponse,
|
||||
VerifyEcashTicketBody,
|
||||
};
|
||||
use nym_api_requests::ecash::VerificationKeyResponse;
|
||||
use nym_api_requests::models::{
|
||||
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainStatusResponse,
|
||||
KeyRotationInfoResponse, LegacyDescribedMixNode, NodePerformanceResponse, NodeRefreshBody,
|
||||
NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
|
||||
AnnotationResponse, ApiHealthResponse, BinaryBuildInformationOwned, ChainBlocksStatusResponse,
|
||||
ChainStatusResponse, KeyRotationInfoResponse, LegacyDescribedMixNode, NodePerformanceResponse,
|
||||
NodeRefreshBody, NymNodeDescription, PerformanceHistoryResponse, RewardedSetResponse,
|
||||
SignerInformationResponse,
|
||||
};
|
||||
use nym_api_requests::nym_nodes::{
|
||||
NodesByAddressesRequestBody, NodesByAddressesResponse, PaginatedCachedNodesResponseV1,
|
||||
@@ -1331,6 +1333,22 @@ pub trait NymApiClientExt: ApiClient {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_chain_blocks_status(&self) -> Result<ChainBlocksStatusResponse, NymAPIError> {
|
||||
self.get_json("/v1/network/chain-blocks-status", NO_PARAMS)
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_signer_status(&self) -> Result<EcashSignerStatusResponse, NymAPIError> {
|
||||
self.get_json("/v1/ecash/signer-status", NO_PARAMS).await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_signer_information(&self) -> Result<SignerInformationResponse, NymAPIError> {
|
||||
self.get_json("/v1/api-status/signer-information", NO_PARAMS)
|
||||
.await
|
||||
}
|
||||
|
||||
#[instrument(level = "debug", skip(self))]
|
||||
async fn get_key_rotation_info(&self) -> Result<KeyRotationInfoResponse, NymAPIError> {
|
||||
self.get_json(
|
||||
|
||||
@@ -8,11 +8,11 @@ use crate::nyxd::CosmWasmClient;
|
||||
use async_trait::async_trait;
|
||||
use cosmrs::AccountId;
|
||||
use cosmwasm_std::Addr;
|
||||
use nym_coconut_dkg_common::dealer::RegisteredDealerDetails;
|
||||
use nym_coconut_dkg_common::types::{ChunkIndex, NodeIndex, StateAdvanceResponse};
|
||||
use serde::Deserialize;
|
||||
use tracing::trace;
|
||||
|
||||
use nym_coconut_dkg_common::dealer::RegisteredDealerDetails;
|
||||
pub use nym_coconut_dkg_common::{
|
||||
dealer::{DealerDetailsResponse, PagedDealerIndexResponse, PagedDealerResponse},
|
||||
dealing::{
|
||||
@@ -21,7 +21,9 @@ pub use nym_coconut_dkg_common::{
|
||||
},
|
||||
msg::QueryMsg as DkgQueryMsg,
|
||||
types::{DealerDetails, DealingIndex, Epoch, EpochId, EpochState, State},
|
||||
verification_key::{ContractVKShare, PagedVKSharesResponse, VkShareResponse},
|
||||
verification_key::{
|
||||
ContractVKShare, PagedVKSharesResponse, VerificationKeyShare, VkShareResponse,
|
||||
},
|
||||
};
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
@@ -41,6 +43,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 +94,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 +243,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 +306,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 +328,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()
|
||||
}
|
||||
|
||||
@@ -139,12 +139,22 @@ impl NyxdClient<HttpClient> {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn connect_with_network_details<U>(
|
||||
endpoint: U,
|
||||
network_details: NymNetworkDetails,
|
||||
) -> Result<QueryHttpRpcNyxdClient, NyxdError>
|
||||
where
|
||||
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
|
||||
{
|
||||
let config = Config::try_from_nym_network_details(&network_details)?;
|
||||
Self::connect(config, endpoint)
|
||||
}
|
||||
|
||||
pub fn connect_to_default_env<U>(endpoint: U) -> Result<QueryHttpRpcNyxdClient, NyxdError>
|
||||
where
|
||||
U: TryInto<HttpClientUrl, Error = TendermintRpcError>,
|
||||
{
|
||||
let config = Config::try_from_nym_network_details(&NymNetworkDetails::new_from_env())?;
|
||||
Self::connect(config, endpoint)
|
||||
Self::connect_with_network_details(endpoint, NymNetworkDetails::new_from_env())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "nym-ecash-signer-check-types"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
semver = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
url = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
time = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
utoipa = { workspace = true }
|
||||
|
||||
nym-coconut-dkg-common = { path = "../cosmwasm-smart-contracts/coconut-dkg" }
|
||||
nym-crypto = { path = "../crypto", features = ["asymmetric"] }
|
||||
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_coconut_dkg_common::dealer::DealerDetails;
|
||||
use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum MalformedDealer {
|
||||
#[error("dealer at {dealer_url} has provided invalid ed25519 pubkey: {source}")]
|
||||
InvalidDealerPubkey {
|
||||
dealer_url: String,
|
||||
source: Ed25519RecoveryError,
|
||||
},
|
||||
|
||||
#[error("dealer at {dealer_url} has provided invalid announce url: {source}")]
|
||||
InvalidDealerAddress {
|
||||
dealer_url: String,
|
||||
source: url::ParseError,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct RawDealerInformation {
|
||||
pub announce_address: String,
|
||||
pub owner_address: String,
|
||||
pub node_index: u64,
|
||||
pub public_key: String,
|
||||
pub verification_key_share: Option<VerificationKeyShare>,
|
||||
pub share_verified: bool,
|
||||
}
|
||||
|
||||
impl RawDealerInformation {
|
||||
pub fn new(
|
||||
dealer_details: &DealerDetails,
|
||||
contract_share: Option<&ContractVKShare>,
|
||||
) -> RawDealerInformation {
|
||||
RawDealerInformation {
|
||||
announce_address: dealer_details.announce_address.clone(),
|
||||
owner_address: dealer_details.address.to_string(),
|
||||
node_index: dealer_details.assigned_index,
|
||||
public_key: dealer_details.ed25519_identity.clone(),
|
||||
verification_key_share: contract_share.map(|s| s.share.clone()),
|
||||
share_verified: contract_share.map(|s| s.verified).unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(&self) -> Result<DealerInformation, MalformedDealer> {
|
||||
Ok(DealerInformation {
|
||||
announce_address: self.announce_address.parse().map_err(|source| {
|
||||
MalformedDealer::InvalidDealerAddress {
|
||||
dealer_url: self.announce_address.clone(),
|
||||
source,
|
||||
}
|
||||
})?,
|
||||
owner_address: self.owner_address.clone(),
|
||||
node_index: self.node_index,
|
||||
public_key: self.public_key.parse().map_err(|source| {
|
||||
MalformedDealer::InvalidDealerPubkey {
|
||||
dealer_url: self.announce_address.clone(),
|
||||
source,
|
||||
}
|
||||
})?,
|
||||
verification_key_share: self.verification_key_share.clone(),
|
||||
share_verified: self.share_verified,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DealerInformation {
|
||||
pub announce_address: Url,
|
||||
pub owner_address: String,
|
||||
pub node_index: u64,
|
||||
pub public_key: ed25519::PublicKey,
|
||||
// no need to parse it into the full type as it doesn't get us anything
|
||||
pub verification_key_share: Option<VerificationKeyShare>,
|
||||
pub share_verified: bool,
|
||||
}
|
||||
|
||||
impl From<DealerInformation> for RawDealerInformation {
|
||||
fn from(d: DealerInformation) -> Self {
|
||||
RawDealerInformation {
|
||||
announce_address: d.announce_address.to_string(),
|
||||
owner_address: d.owner_address,
|
||||
node_index: d.node_index,
|
||||
public_key: d.public_key.to_base58_string(),
|
||||
verification_key_share: d.verification_key_share,
|
||||
share_verified: d.share_verified,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{debug, warn};
|
||||
|
||||
pub trait Verifiable {
|
||||
fn verify_signature(&self, pub_key: &ed25519::PublicKey) -> bool;
|
||||
}
|
||||
|
||||
pub trait TimestampedResponse {
|
||||
fn timestamp(&self) -> OffsetDateTime;
|
||||
}
|
||||
|
||||
pub trait LegacyChainResponse {
|
||||
fn chain_synced(&self, now: OffsetDateTime, stall_threshold: Duration) -> bool;
|
||||
}
|
||||
|
||||
pub trait ChainResponse: Verifiable + TimestampedResponse {
|
||||
fn chain_synced(&self) -> bool;
|
||||
|
||||
fn chain_available(
|
||||
&self,
|
||||
pub_key: &ed25519::PublicKey,
|
||||
now: OffsetDateTime,
|
||||
stale_response_threshold: Duration,
|
||||
) -> bool {
|
||||
if !self.verify_signature(pub_key) {
|
||||
warn!("failed signature verification on chain status response");
|
||||
return false;
|
||||
}
|
||||
|
||||
// we rely on information provided from the api itself AS LONG AS it's not too outdated
|
||||
if self.timestamp() + stale_response_threshold < now {
|
||||
return false;
|
||||
}
|
||||
self.chain_synced()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LegacySignerResponse {
|
||||
fn signer_identity(&self) -> &str;
|
||||
|
||||
fn signer_verification_key(&self) -> &Option<String>;
|
||||
|
||||
fn unprovable_signing_available(
|
||||
&self,
|
||||
pub_key: &ed25519::PublicKey,
|
||||
expected_verification_key: Option<VerificationKeyShare>,
|
||||
share_verified: bool,
|
||||
) -> bool {
|
||||
if self.signer_identity() != pub_key.to_base58_string() {
|
||||
warn!("mismatched identity key on the legacy response");
|
||||
return false;
|
||||
}
|
||||
|
||||
// the contract share hasn't been verified yet, so we're probably in the middle of DKG
|
||||
// thus if there's a bit of desync in the state, it's fine
|
||||
if !share_verified {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.signer_verification_key() != &expected_verification_key {
|
||||
warn!("mismatched [ecash] verification key on the legacy response");
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SignerResponse: Verifiable + TimestampedResponse {
|
||||
fn has_signing_keys(&self) -> bool;
|
||||
|
||||
fn signer_disabled(&self) -> bool;
|
||||
|
||||
fn is_ecash_signer(&self) -> bool;
|
||||
|
||||
fn dkg_ecash_epoch_id(&self) -> EpochId;
|
||||
|
||||
fn provable_signing_available(
|
||||
&self,
|
||||
pub_key: &ed25519::PublicKey,
|
||||
dkg_epoch_id: EpochId,
|
||||
now: OffsetDateTime,
|
||||
stale_response_threshold: Duration,
|
||||
) -> bool {
|
||||
if !self.verify_signature(pub_key) {
|
||||
warn!("failed signature verification on chain status response");
|
||||
return false;
|
||||
}
|
||||
|
||||
// we rely on information provided from the api itself AS LONG AS it's not too outdated
|
||||
if self.timestamp() + stale_response_threshold < now {
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.has_signing_keys() {
|
||||
debug!("missing signing keys");
|
||||
return false;
|
||||
}
|
||||
|
||||
if self.signer_disabled() {
|
||||
debug!("signer functionalities explicitly disabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
if !self.is_ecash_signer() {
|
||||
debug!("signer doesn't recognise it's a signer for this epoch");
|
||||
return false;
|
||||
}
|
||||
|
||||
if dkg_epoch_id != self.dkg_ecash_epoch_id() {
|
||||
debug!(
|
||||
"mismatched dkg epoch id. current: {dkg_epoch_id}, signer's: {}",
|
||||
self.dkg_ecash_epoch_id()
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod dealer_information;
|
||||
pub mod helper_traits;
|
||||
pub mod status;
|
||||
@@ -0,0 +1,303 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::dealer_information::RawDealerInformation;
|
||||
use crate::helper_traits::{
|
||||
ChainResponse, LegacyChainResponse, LegacySignerResponse, SignerResponse,
|
||||
};
|
||||
use nym_coconut_dkg_common::types::EpochId;
|
||||
use nym_coconut_dkg_common::verification_key::VerificationKeyShare;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub(crate) const CHAIN_STALL_THRESHOLD: Duration = Duration::from_secs(5 * 60);
|
||||
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
|
||||
|
||||
// the reason for generics is not to remove duplication of code,
|
||||
// but because without them, we'd be having problems with circular dependencies,
|
||||
// i.e. nym-api-requests depending on ecash-signer-check-types and
|
||||
// ecash-signer-check-types needing nym-api-requests
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub enum Status<L, T> {
|
||||
/// The API, even though it reports correct version, did not response to the status query
|
||||
Unreachable,
|
||||
|
||||
/// The API is running an outdated version that does not expose the required endpoint
|
||||
Outdated,
|
||||
|
||||
/// Response to the legacy (unsigned) status query
|
||||
ReachableLegacy { response: Box<L> },
|
||||
|
||||
/// Response to the current (signed) status query
|
||||
Reachable { response: Box<T> },
|
||||
}
|
||||
|
||||
impl<L, T> Status<L, T>
|
||||
where
|
||||
L: LegacyChainResponse,
|
||||
T: ChainResponse,
|
||||
{
|
||||
pub fn chain_available(&self, pub_key: ed25519::PublicKey) -> bool {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
|
||||
match self {
|
||||
Status::Unreachable | Status::Outdated => false,
|
||||
Status::ReachableLegacy { response } => {
|
||||
response.chain_synced(now, CHAIN_STALL_THRESHOLD)
|
||||
}
|
||||
Status::Reachable { response } => {
|
||||
response.chain_available(&pub_key, now, STALE_RESPONSE_THRESHOLD)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn chain_provably_stalled(&self, pub_key: ed25519::PublicKey) -> bool {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
|
||||
match self {
|
||||
Status::Unreachable | Status::Outdated | Status::ReachableLegacy { .. } => false,
|
||||
Status::Reachable { response } => {
|
||||
!response.chain_available(&pub_key, now, STALE_RESPONSE_THRESHOLD)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn chain_unprovably_stalled(&self) -> bool {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
|
||||
match self {
|
||||
Status::Unreachable | Status::Outdated | Status::Reachable { .. } => false,
|
||||
Status::ReachableLegacy { response } => {
|
||||
!response.chain_synced(now, CHAIN_STALL_THRESHOLD)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<L, T> Status<L, T>
|
||||
where
|
||||
L: LegacySignerResponse,
|
||||
T: SignerResponse,
|
||||
{
|
||||
pub fn signing_available(
|
||||
&self,
|
||||
pub_key: ed25519::PublicKey,
|
||||
dkg_epoch_id: u64,
|
||||
expected_verification_key: Option<VerificationKeyShare>,
|
||||
share_verified: bool,
|
||||
) -> bool {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
|
||||
match self {
|
||||
Status::Unreachable | Status::Outdated => false,
|
||||
Status::ReachableLegacy { response } => response.unprovable_signing_available(
|
||||
&pub_key,
|
||||
expected_verification_key,
|
||||
share_verified,
|
||||
),
|
||||
Status::Reachable { response } => response.provable_signing_available(
|
||||
&pub_key,
|
||||
dkg_epoch_id,
|
||||
now,
|
||||
STALE_RESPONSE_THRESHOLD,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn signing_provably_unavailable(
|
||||
&self,
|
||||
pub_key: ed25519::PublicKey,
|
||||
dkg_epoch_id: EpochId,
|
||||
) -> bool {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
|
||||
match self {
|
||||
Status::Unreachable | Status::Outdated | Status::ReachableLegacy { .. } => false,
|
||||
Status::Reachable { response } => !response.provable_signing_available(
|
||||
&pub_key,
|
||||
dkg_epoch_id,
|
||||
now,
|
||||
STALE_RESPONSE_THRESHOLD,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn signing_unprovably_unavailable(
|
||||
&self,
|
||||
pub_key: ed25519::PublicKey,
|
||||
expected_verification_key: Option<VerificationKeyShare>,
|
||||
share_verified: bool,
|
||||
) -> bool {
|
||||
match self {
|
||||
Status::Unreachable | Status::Outdated | Status::Reachable { .. } => false,
|
||||
Status::ReachableLegacy { response } => !response.unprovable_signing_available(
|
||||
&pub_key,
|
||||
expected_verification_key,
|
||||
share_verified,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct SignerResult<LS, TS, LC, TC> {
|
||||
pub dkg_epoch_id: u64,
|
||||
pub information: RawDealerInformation,
|
||||
pub status: SignerStatus<LS, TS, LC, TC>,
|
||||
}
|
||||
|
||||
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC> {
|
||||
pub fn signer_unreachable(&self) -> bool {
|
||||
matches!(self.status, SignerStatus::Unreachable)
|
||||
}
|
||||
|
||||
pub fn malformed_details(&self) -> bool {
|
||||
self.information.parse().is_err()
|
||||
}
|
||||
}
|
||||
|
||||
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC>
|
||||
where
|
||||
LC: LegacyChainResponse,
|
||||
TC: ChainResponse,
|
||||
{
|
||||
pub fn unknown_chain_status(&self) -> bool {
|
||||
let Ok(_) = self.information.parse() else {
|
||||
return true;
|
||||
};
|
||||
if let SignerStatus::Tested { .. } = &self.status {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn chain_available(&self) -> bool {
|
||||
let Ok(parsed_info) = self.information.parse() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let SignerStatus::Tested { result } = &self.status else {
|
||||
return false;
|
||||
};
|
||||
result
|
||||
.local_chain_status
|
||||
.chain_available(parsed_info.public_key)
|
||||
}
|
||||
|
||||
pub fn chain_provably_stalled(&self) -> bool {
|
||||
let Ok(parsed_info) = self.information.parse() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let SignerStatus::Tested { result } = &self.status else {
|
||||
return false;
|
||||
};
|
||||
|
||||
result
|
||||
.local_chain_status
|
||||
.chain_provably_stalled(parsed_info.public_key)
|
||||
}
|
||||
|
||||
pub fn chain_unprovably_stalled(&self) -> bool {
|
||||
let SignerStatus::Tested { result } = &self.status else {
|
||||
return false;
|
||||
};
|
||||
|
||||
result.local_chain_status.chain_unprovably_stalled()
|
||||
}
|
||||
}
|
||||
|
||||
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC>
|
||||
where
|
||||
LS: LegacySignerResponse,
|
||||
TS: SignerResponse,
|
||||
{
|
||||
pub fn unknown_signing_status(&self) -> bool {
|
||||
let Ok(_) = self.information.parse() else {
|
||||
return true;
|
||||
};
|
||||
if let SignerStatus::Tested { .. } = &self.status {
|
||||
return false;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn signing_available(&self) -> bool {
|
||||
let Ok(parsed_info) = self.information.parse() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let SignerStatus::Tested { result } = &self.status else {
|
||||
return false;
|
||||
};
|
||||
result.signing_status.signing_available(
|
||||
parsed_info.public_key,
|
||||
self.dkg_epoch_id,
|
||||
parsed_info.verification_key_share,
|
||||
parsed_info.share_verified,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn signing_provably_unavailable(&self) -> bool {
|
||||
let Ok(parsed_info) = self.information.parse() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let SignerStatus::Tested { result } = &self.status else {
|
||||
return false;
|
||||
};
|
||||
|
||||
result
|
||||
.signing_status
|
||||
.signing_provably_unavailable(parsed_info.public_key, self.dkg_epoch_id)
|
||||
}
|
||||
|
||||
pub fn signing_unprovably_unavailable(&self) -> bool {
|
||||
let Ok(parsed_info) = self.information.parse() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let SignerStatus::Tested { result } = &self.status else {
|
||||
return false;
|
||||
};
|
||||
|
||||
result.signing_status.signing_unprovably_unavailable(
|
||||
parsed_info.public_key,
|
||||
parsed_info.verification_key_share,
|
||||
parsed_info.share_verified,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub enum SignerStatus<LS, TS, LC, TC> {
|
||||
Unreachable,
|
||||
ProvidedInvalidDetails,
|
||||
Tested {
|
||||
result: SignerTestResult<LS, TS, LC, TC>,
|
||||
},
|
||||
}
|
||||
|
||||
impl<LS, TS, LC, TC> SignerStatus<LS, TS, LC, TC> {
|
||||
pub fn with_details(
|
||||
self,
|
||||
information: impl Into<RawDealerInformation>,
|
||||
dkg_epoch_id: u64,
|
||||
) -> SignerResult<LS, TS, LC, TC> {
|
||||
SignerResult {
|
||||
dkg_epoch_id,
|
||||
status: self,
|
||||
information: information.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||
pub struct SignerTestResult<LS, TS, LC, TC> {
|
||||
pub reported_version: String,
|
||||
pub signing_status: Status<LS, TS>,
|
||||
pub local_chain_status: Status<LC, TC>,
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "nym-ecash-signer-check"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
futures = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
semver = { workspace = true }
|
||||
tokio = { workspace = true, features = ["time"] }
|
||||
tracing = { workspace = true }
|
||||
url = { workspace = true }
|
||||
|
||||
|
||||
nym-validator-client = { path = "../client-libs/validator-client" }
|
||||
nym-network-defaults = { path = "../network-defaults" }
|
||||
nym-ecash-signer-check-types = { path = "../ecash-signer-check-types" }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,225 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{LocalChainStatus, SigningStatus, TypedSignerResult};
|
||||
use nym_ecash_signer_check_types::dealer_information::RawDealerInformation;
|
||||
use nym_ecash_signer_check_types::status::{SignerStatus, SignerTestResult};
|
||||
use nym_validator_client::client::NymApiClientExt;
|
||||
use nym_validator_client::models::BinaryBuildInformationOwned;
|
||||
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
|
||||
ContractVKShare, DealerDetails,
|
||||
};
|
||||
use nym_validator_client::NymApiClient;
|
||||
use std::time::Duration;
|
||||
use tracing::{error, warn};
|
||||
use url::Url;
|
||||
|
||||
pub(crate) mod chain_status {
|
||||
|
||||
// Dorina
|
||||
pub(crate) const MINIMUM_VERSION_LEGACY: semver::Version = semver::Version::new(1, 1, 51);
|
||||
|
||||
// Gruyere
|
||||
pub(crate) const MINIMUM_VERSION: semver::Version = semver::Version::new(1, 1, 64);
|
||||
}
|
||||
|
||||
pub(crate) mod signing_status {
|
||||
// Magura (possibly earlier)
|
||||
pub(crate) const MINIMUM_LEGACY_VERSION: semver::Version = semver::Version::new(1, 1, 46);
|
||||
|
||||
// Gruyere
|
||||
pub(crate) const MINIMUM_VERSION: semver::Version = semver::Version::new(1, 1, 64);
|
||||
}
|
||||
|
||||
struct ClientUnderTest {
|
||||
api_client: NymApiClient,
|
||||
build_info: Option<BinaryBuildInformationOwned>,
|
||||
}
|
||||
|
||||
impl ClientUnderTest {
|
||||
pub(crate) fn new(api_url: &Url) -> Self {
|
||||
ClientUnderTest {
|
||||
api_client: NymApiClient::new(api_url.clone()),
|
||||
build_info: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn try_retrieve_build_information(&mut self) -> bool {
|
||||
match tokio::time::timeout(
|
||||
Duration::from_secs(5),
|
||||
self.api_client.nym_api.build_information(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(build_information)) => {
|
||||
self.build_info = Some(build_information);
|
||||
true
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
warn!("{}: failed to retrieve build information: {err}. the signer is most likely down", self.api_client.api_url());
|
||||
false
|
||||
}
|
||||
Err(_timeout) => {
|
||||
warn!(
|
||||
"{}: timed out while attempting to retrieve build information",
|
||||
self.api_client.api_url()
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn version(&self) -> Option<semver::Version> {
|
||||
self.build_info.as_ref().and_then(|build_info| {
|
||||
build_info
|
||||
.build_version
|
||||
.parse()
|
||||
.inspect_err(|err| {
|
||||
error!(
|
||||
"ecash signer '{}' reports invalid version {}: {err}",
|
||||
self.api_client.api_url(),
|
||||
build_info.build_version
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn supports_legacy_signing_status_query(&self) -> bool {
|
||||
let Some(version) = self.version() else {
|
||||
return false;
|
||||
};
|
||||
version >= signing_status::MINIMUM_LEGACY_VERSION
|
||||
}
|
||||
|
||||
pub(crate) fn supports_signing_status_query(&self) -> bool {
|
||||
let Some(version) = self.version() else {
|
||||
return false;
|
||||
};
|
||||
version >= signing_status::MINIMUM_VERSION
|
||||
}
|
||||
|
||||
pub(crate) fn supports_chain_status_query(&self) -> bool {
|
||||
let Some(version) = self.version() else {
|
||||
return false;
|
||||
};
|
||||
version >= chain_status::MINIMUM_VERSION
|
||||
}
|
||||
|
||||
pub(crate) fn supports_legacy_chain_status_query(&self) -> bool {
|
||||
let Some(version) = self.version() else {
|
||||
return false;
|
||||
};
|
||||
version >= chain_status::MINIMUM_VERSION_LEGACY
|
||||
}
|
||||
|
||||
pub(crate) async fn check_local_chain(&self) -> LocalChainStatus {
|
||||
// check if it at least supports legacy query
|
||||
if !self.supports_legacy_chain_status_query() {
|
||||
return LocalChainStatus::Outdated;
|
||||
}
|
||||
|
||||
// check if it supports the current query
|
||||
if self.supports_chain_status_query() {
|
||||
return match self.api_client.nym_api.get_chain_blocks_status().await {
|
||||
Ok(status) => LocalChainStatus::Reachable {
|
||||
response: Box::new(status),
|
||||
},
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"{}: failed to retrieve local chain status: {err}",
|
||||
self.api_client.api_url()
|
||||
);
|
||||
LocalChainStatus::Unreachable
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// fallback to the legacy query
|
||||
match self.api_client.nym_api.get_chain_status().await {
|
||||
Ok(status) => LocalChainStatus::ReachableLegacy {
|
||||
response: Box::new(status),
|
||||
},
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"{}: failed to retrieve [legacy] local chain status: {err}",
|
||||
self.api_client.api_url()
|
||||
);
|
||||
LocalChainStatus::Unreachable
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn check_signing_status(&self) -> SigningStatus {
|
||||
// check if it at least supports legacy query
|
||||
if !self.supports_legacy_signing_status_query() {
|
||||
return SigningStatus::Outdated;
|
||||
}
|
||||
|
||||
// check if it supports the current query
|
||||
if self.supports_signing_status_query() {
|
||||
return match self.api_client.nym_api.get_signer_status().await {
|
||||
Ok(response) => SigningStatus::Reachable {
|
||||
response: Box::new(response),
|
||||
},
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"{}: failed to retrieve signer chain status: {err}",
|
||||
self.api_client.api_url()
|
||||
);
|
||||
SigningStatus::Unreachable
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// fallback to the legacy query
|
||||
match self.api_client.nym_api.get_signer_information().await {
|
||||
Ok(status) => SigningStatus::ReachableLegacy {
|
||||
response: Box::new(status),
|
||||
},
|
||||
Err(err) => {
|
||||
warn!(
|
||||
"{}: failed to retrieve [legacy] signer chain status: {err}",
|
||||
self.api_client.api_url()
|
||||
);
|
||||
// NOTE: this might equally mean the signing is disabled
|
||||
SigningStatus::Unreachable
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn check_client(
|
||||
dealer_details: DealerDetails,
|
||||
dkg_epoch: u64,
|
||||
contract_share: Option<&ContractVKShare>,
|
||||
) -> TypedSignerResult {
|
||||
let dealer_information = RawDealerInformation::new(&dealer_details, contract_share);
|
||||
|
||||
// 7. attempt to construct client instances out of them
|
||||
let Ok(parsed_information) = dealer_information.parse() else {
|
||||
return SignerStatus::ProvidedInvalidDetails.with_details(dealer_information, dkg_epoch);
|
||||
};
|
||||
|
||||
let mut client = ClientUnderTest::new(&parsed_information.announce_address);
|
||||
|
||||
// 8. check basic connection status - can you retrieve build information?
|
||||
if !client.try_retrieve_build_information().await {
|
||||
return SignerStatus::Unreachable.with_details(dealer_information, dkg_epoch);
|
||||
}
|
||||
|
||||
// 9. check perceived chain status
|
||||
let local_chain_status = client.check_local_chain().await;
|
||||
|
||||
// 10. check signer status
|
||||
let signing_status = client.check_signing_status().await;
|
||||
|
||||
SignerStatus::Tested {
|
||||
result: SignerTestResult {
|
||||
reported_version: client.version().map(|v| v.to_string()).unwrap_or_default(),
|
||||
signing_status,
|
||||
local_chain_status,
|
||||
},
|
||||
}
|
||||
.with_details(dealer_information, dkg_epoch)
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::SignerCheckError;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
|
||||
ContractVKShare, DealerDetails, VerificationKeyShare,
|
||||
};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RawDealerInformation {
|
||||
pub announce_address: String,
|
||||
pub owner_address: String,
|
||||
pub node_index: u64,
|
||||
pub public_key: String,
|
||||
pub verification_key_share: Option<VerificationKeyShare>,
|
||||
pub share_verified: bool,
|
||||
}
|
||||
|
||||
impl RawDealerInformation {
|
||||
pub fn new(
|
||||
dealer_details: &DealerDetails,
|
||||
contract_share: Option<&ContractVKShare>,
|
||||
) -> RawDealerInformation {
|
||||
RawDealerInformation {
|
||||
announce_address: dealer_details.announce_address.clone(),
|
||||
owner_address: dealer_details.address.to_string(),
|
||||
node_index: dealer_details.assigned_index,
|
||||
public_key: dealer_details.ed25519_identity.clone(),
|
||||
verification_key_share: contract_share.map(|s| s.share.clone()),
|
||||
share_verified: contract_share.map(|s| s.verified).unwrap_or(false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(&self) -> Result<DealerInformation, SignerCheckError> {
|
||||
Ok(DealerInformation {
|
||||
announce_address: self.announce_address.parse().map_err(|source| {
|
||||
SignerCheckError::InvalidDealerAddress {
|
||||
dealer_url: self.announce_address.clone(),
|
||||
source,
|
||||
}
|
||||
})?,
|
||||
owner_address: self.owner_address.clone(),
|
||||
node_index: self.node_index,
|
||||
public_key: self.announce_address.parse().map_err(|source| {
|
||||
SignerCheckError::InvalidDealerPubkey {
|
||||
dealer_url: self.announce_address.clone(),
|
||||
source,
|
||||
}
|
||||
})?,
|
||||
verification_key_share: self.verification_key_share.clone(),
|
||||
share_verified: self.share_verified,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DealerInformation {
|
||||
pub announce_address: Url,
|
||||
pub owner_address: String,
|
||||
pub node_index: u64,
|
||||
pub public_key: ed25519::PublicKey,
|
||||
// no need to parse it into the full type as it doesn't get us anything
|
||||
pub verification_key_share: Option<VerificationKeyShare>,
|
||||
pub share_verified: bool,
|
||||
}
|
||||
|
||||
impl From<DealerInformation> for RawDealerInformation {
|
||||
fn from(d: DealerInformation) -> Self {
|
||||
RawDealerInformation {
|
||||
announce_address: d.announce_address.to_string(),
|
||||
owner_address: d.owner_address,
|
||||
node_index: d.node_index,
|
||||
public_key: d.public_key.to_base58_string(),
|
||||
verification_key_share: d.verification_key_share,
|
||||
share_verified: d.share_verified,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum SignerCheckError {
|
||||
#[error("failed to connect to nyxd chain due to invalid connection details: {source}")]
|
||||
InvalidNyxdConnectionDetails { source: NyxdError },
|
||||
|
||||
#[error("failed to query the DKG contract: {source}")]
|
||||
DKGContractQueryFailure { source: NyxdError },
|
||||
}
|
||||
|
||||
impl SignerCheckError {
|
||||
pub fn invalid_nyxd_connection_details(source: NyxdError) -> Self {
|
||||
Self::InvalidNyxdConnectionDetails { source }
|
||||
}
|
||||
|
||||
pub fn dkg_contract_query_failure(source: NyxdError) -> Self {
|
||||
Self::DKGContractQueryFailure { source }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client_check::check_client;
|
||||
use futures::stream::{FuturesUnordered, StreamExt};
|
||||
use nym_network_defaults::NymNetworkDetails;
|
||||
use nym_validator_client::nyxd::contract_traits::{DkgQueryClient, PagedDkgQueryClient};
|
||||
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||
use std::collections::HashMap;
|
||||
use url::Url;
|
||||
|
||||
pub use error::SignerCheckError;
|
||||
use nym_ecash_signer_check_types::status::{SignerResult, Status};
|
||||
use nym_validator_client::ecash::models::EcashSignerStatusResponse;
|
||||
use nym_validator_client::models::{
|
||||
ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse,
|
||||
};
|
||||
|
||||
mod client_check;
|
||||
pub mod error;
|
||||
|
||||
pub type TypedSignerResult = SignerResult<
|
||||
SignerInformationResponse,
|
||||
EcashSignerStatusResponse,
|
||||
ChainStatusResponse,
|
||||
ChainBlocksStatusResponse,
|
||||
>;
|
||||
pub type LocalChainStatus = Status<ChainStatusResponse, ChainBlocksStatusResponse>;
|
||||
pub type SigningStatus = Status<SignerInformationResponse, EcashSignerStatusResponse>;
|
||||
|
||||
pub struct SignersTestResult {
|
||||
pub threshold: Option<u64>,
|
||||
pub results: Vec<TypedSignerResult>,
|
||||
}
|
||||
|
||||
pub async fn check_signers(
|
||||
rpc_endpoint: Url,
|
||||
// details such as denoms, prefixes, etc.
|
||||
network_details: NymNetworkDetails,
|
||||
) -> Result<SignersTestResult, SignerCheckError> {
|
||||
// 1. create nyx client instance
|
||||
let client = QueryHttpRpcNyxdClient::connect_with_network_details(
|
||||
rpc_endpoint.as_str(),
|
||||
network_details,
|
||||
)
|
||||
.map_err(SignerCheckError::invalid_nyxd_connection_details)?;
|
||||
|
||||
check_signers_with_client(&client).await
|
||||
}
|
||||
|
||||
pub async fn check_signers_with_client<C>(client: &C) -> Result<SignersTestResult, SignerCheckError>
|
||||
where
|
||||
C: DkgQueryClient + Sync,
|
||||
{
|
||||
// 2. retrieve current dkg epoch
|
||||
let dkg_epoch = client
|
||||
.get_current_epoch()
|
||||
.await
|
||||
.map_err(SignerCheckError::dkg_contract_query_failure)?;
|
||||
|
||||
// 3. retrieve the dkg threshold as reference point
|
||||
let threshold = client
|
||||
.get_epoch_threshold(dkg_epoch.epoch_id)
|
||||
.await
|
||||
.map_err(SignerCheckError::dkg_contract_query_failure)?;
|
||||
|
||||
// 4. retrieve information on current DKG dealers (i.e. eligible signers)
|
||||
let dealers = client
|
||||
.get_all_current_dealers()
|
||||
.await
|
||||
.map_err(SignerCheckError::dkg_contract_query_failure)?;
|
||||
|
||||
// 5. retrieve their published keys (if available)
|
||||
let shares: HashMap<_, _> = client
|
||||
.get_all_verification_key_shares(dkg_epoch.epoch_id)
|
||||
.await
|
||||
.map_err(SignerCheckError::dkg_contract_query_failure)?
|
||||
.into_iter()
|
||||
.map(|share| (share.node_index, share))
|
||||
.collect();
|
||||
|
||||
// 6. for each dealer attempt to perform the checks
|
||||
let results = dealers
|
||||
.into_iter()
|
||||
.map(|d| {
|
||||
let share = shares.get(&d.assigned_index);
|
||||
check_client(d, dkg_epoch.epoch_id, share)
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>()
|
||||
.collect::<Vec<_>>()
|
||||
.await;
|
||||
|
||||
Ok(SignersTestResult { threshold, results })
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::chain_status::LocalChainStatus;
|
||||
use crate::dealer_information::RawDealerInformation;
|
||||
use crate::signing_status::SigningStatus;
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) const STALE_RESPONSE_THRESHOLD: Duration = Duration::from_secs(5 * 60);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SignerResult {
|
||||
pub dkg_epoch_id: u64,
|
||||
pub information: RawDealerInformation,
|
||||
pub status: SignerStatus,
|
||||
}
|
||||
|
||||
impl SignerResult {
|
||||
pub fn chain_available(&self) -> bool {
|
||||
let Ok(parsed_info) = self.information.parse() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let SignerStatus::Tested { result } = &self.status else {
|
||||
return false;
|
||||
};
|
||||
result.local_chain_status.available(parsed_info.public_key)
|
||||
}
|
||||
|
||||
pub fn signer_available(&self) -> bool {
|
||||
let Ok(parsed_info) = self.information.parse() else {
|
||||
return false;
|
||||
};
|
||||
let SignerStatus::Tested { result } = &self.status else {
|
||||
return false;
|
||||
};
|
||||
|
||||
result.signing_status.available(
|
||||
parsed_info.public_key,
|
||||
self.dkg_epoch_id,
|
||||
parsed_info.verification_key_share,
|
||||
parsed_info.share_verified,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SignerStatus {
|
||||
Unreachable,
|
||||
ProvidedInvalidDetails,
|
||||
Tested { result: SignerTestResult },
|
||||
}
|
||||
|
||||
impl SignerStatus {
|
||||
pub fn with_details(
|
||||
self,
|
||||
information: impl Into<RawDealerInformation>,
|
||||
dkg_epoch_id: u64,
|
||||
) -> SignerResult {
|
||||
SignerResult {
|
||||
dkg_epoch_id,
|
||||
status: self,
|
||||
information: information.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SignerTestResult {
|
||||
pub reported_version: String,
|
||||
pub signing_status: SigningStatus,
|
||||
pub local_chain_status: LocalChainStatus,
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
//! The resolver itself is the set combination of the google, cloudflare, and quad9 endpoints
|
||||
//! supporting DoH and DoT.
|
||||
//!
|
||||
//! This resolver implements a fallback mechanism where, should the DNS-over-TLS resolution fail, a
|
||||
//! This resolver supports a fallback mechanism where, should the DNS-over-TLS resolution fail, a
|
||||
//! followup resolution will be done using the hosts configured default (e.g. `/etc/resolve.conf` on
|
||||
//! linux).
|
||||
//! linux). This is disabled by default and can be enabled using [`enable_system_fallback`].
|
||||
//!
|
||||
//! Requires the `dns-over-https-rustls`, `webpki-roots` feature for the
|
||||
//! `hickory-resolver` crate
|
||||
@@ -93,14 +93,14 @@ pub struct HickoryDnsResolver {
|
||||
// Tokio Runtime in initialization, so we must delay the actual
|
||||
// construction of the resolver.
|
||||
state: Arc<OnceCell<TokioResolver>>,
|
||||
fallback: Arc<OnceCell<TokioResolver>>,
|
||||
fallback: Option<Arc<OnceCell<TokioResolver>>>,
|
||||
dont_use_shared: bool,
|
||||
}
|
||||
|
||||
impl Resolve for HickoryDnsResolver {
|
||||
fn resolve(&self, name: Name) -> Resolving {
|
||||
let resolver = self.state.clone();
|
||||
let fallback = self.fallback.clone();
|
||||
let maybe_fallback = self.fallback.clone();
|
||||
let independent = self.dont_use_shared;
|
||||
Box::pin(async move {
|
||||
let resolver = resolver.get_or_try_init(|| {
|
||||
@@ -117,23 +117,30 @@ impl Resolve for HickoryDnsResolver {
|
||||
let lookup = match resolver.lookup_ip(name.as_str()).await {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
// on failure use the fall back system configured DNS resolver
|
||||
if !e.is_no_records_found() {
|
||||
warn!("primary DNS failed w/ error {e}: using system fallback");
|
||||
}
|
||||
let resolver = fallback.get_or_try_init(|| {
|
||||
// using a closure here is slightly gross, but this makes sure that if the
|
||||
// lazy-init returns an error it can be handled by the client
|
||||
if independent {
|
||||
new_resolver_system()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER
|
||||
.fallback
|
||||
.get_or_try_init(new_resolver_system)?
|
||||
.clone())
|
||||
if let Some(ref fallback) = maybe_fallback {
|
||||
// on failure use the fall back system configured DNS resolver
|
||||
if !e.is_no_records_found() {
|
||||
warn!("primary DNS failed w/ error {e}: using system fallback");
|
||||
}
|
||||
})?;
|
||||
resolver.lookup_ip(name.as_str()).await?
|
||||
let resolver = fallback.get_or_try_init(|| {
|
||||
// using a closure here is slightly gross, but this makes sure that if the
|
||||
// lazy-init returns an error it can be handled by the client
|
||||
if independent {
|
||||
new_resolver_system()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER
|
||||
.fallback
|
||||
.as_ref()
|
||||
.ok_or(e)? // if the shared resolver has no fallback return the original error
|
||||
.get_or_try_init(new_resolver_system)?
|
||||
.clone())
|
||||
}
|
||||
})?;
|
||||
|
||||
resolver.lookup_ip(name.as_str()).await?
|
||||
} else {
|
||||
return Err(e.into());
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -162,14 +169,17 @@ impl HickoryDnsResolver {
|
||||
let lookup = match resolver.lookup_ip(name).await {
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
// on failure use the fall back system configured DNS resolver
|
||||
if !e.is_no_records_found() {
|
||||
warn!("primary DNS failed w/ error {e}: using system fallback");
|
||||
if let Some(ref fallback) = self.fallback {
|
||||
// on failure use the fall back system configured DNS resolver
|
||||
if !e.is_no_records_found() {
|
||||
warn!("primary DNS failed w/ error {e}: using system fallback");
|
||||
}
|
||||
|
||||
let resolver = fallback.get_or_try_init(|| self.new_resolver_system())?;
|
||||
resolver.lookup_ip(name).await?
|
||||
} else {
|
||||
return Err(e.into());
|
||||
}
|
||||
let resolver = self
|
||||
.fallback
|
||||
.get_or_try_init(|| self.new_resolver_system())?;
|
||||
resolver.lookup_ip(name).await?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -193,15 +203,34 @@ impl HickoryDnsResolver {
|
||||
}
|
||||
|
||||
fn new_resolver_system(&self) -> Result<TokioResolver, HickoryDnsError> {
|
||||
if self.dont_use_shared {
|
||||
if self.dont_use_shared || SHARED_RESOLVER.fallback.is_none() {
|
||||
new_resolver_system()
|
||||
} else {
|
||||
Ok(SHARED_RESOLVER
|
||||
.fallback
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_or_try_init(new_resolver_system)?
|
||||
.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Enable fallback to the system default resolver if the primary (DoX) resolver fails
|
||||
pub fn enable_system_fallback(&mut self) -> Result<(), HickoryDnsError> {
|
||||
self.fallback = Some(Default::default());
|
||||
let _ = self
|
||||
.fallback
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_or_try_init(new_resolver_system)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Disable fallback resolution. If the primary resolver fails the error is
|
||||
/// returned immediately
|
||||
pub fn disable_system_fallback(&mut self) {
|
||||
self.fallback = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new resolver with a custom DoT based configuration. The options are overridden to look
|
||||
|
||||
@@ -16,7 +16,7 @@ pub struct UserAgent {
|
||||
pub version: String,
|
||||
/// client platform
|
||||
pub platform: String,
|
||||
/// source commit version for the calling calling crate / subsystem
|
||||
/// source commit version for the calling crate / subsystem
|
||||
pub git_commit: String,
|
||||
}
|
||||
|
||||
|
||||
@@ -146,6 +146,12 @@ pub struct OutputParams {
|
||||
pub output: Option<Output>,
|
||||
}
|
||||
|
||||
impl OutputParams {
|
||||
pub fn get_output(&self) -> Output {
|
||||
self.output.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl Output {
|
||||
pub fn to_response<T: Serialize>(self, data: T) -> FormattedResponse<T> {
|
||||
match self {
|
||||
|
||||
@@ -233,7 +233,7 @@ where
|
||||
let dkg_query_client = if self.config.base.client.disabled_credentials_mode {
|
||||
None
|
||||
} else {
|
||||
Some(default_query_dkg_client_from_config(&self.config.base))
|
||||
Some(default_query_dkg_client_from_config(&self.config.base)?)
|
||||
};
|
||||
|
||||
let mut base_builder =
|
||||
|
||||
@@ -265,6 +265,7 @@ impl ShutdownManager {
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[track_caller]
|
||||
pub fn with_shutdown<F>(mut self, shutdown: F) -> Self
|
||||
where
|
||||
F: Future<Output = ()>,
|
||||
@@ -281,6 +282,7 @@ impl ShutdownManager {
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[track_caller]
|
||||
pub fn with_shutdown_signal(self, signal_kind: SignalKind) -> std::io::Result<Self> {
|
||||
let mut sig = signal(signal_kind)?;
|
||||
Ok(self.with_shutdown(async move {
|
||||
@@ -289,6 +291,7 @@ impl ShutdownManager {
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[track_caller]
|
||||
pub fn with_interrupt_signal(self) -> Self {
|
||||
self.with_shutdown(async move {
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
@@ -296,11 +299,13 @@ impl ShutdownManager {
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[track_caller]
|
||||
pub fn with_terminate_signal(self) -> std::io::Result<Self> {
|
||||
self.with_shutdown_signal(SignalKind::terminate())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[track_caller]
|
||||
pub fn with_quit_signal(self) -> std::io::Result<Self> {
|
||||
self.with_shutdown_signal(SignalKind::quit())
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
@@ -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
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "zulip-client"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
|
||||
itertools = { workspace = true }
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
zeroize = { workspace = true }
|
||||
|
||||
nym-bin-common = { path = "../bin-common" }
|
||||
nym-http-api-client = { path = "../http-api-client" }
|
||||
reqwest = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
serde_json = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,151 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! An incomplete Zulip API Client
|
||||
//!
|
||||
//! Currently, it serves a single purpose: to send a message to a server,
|
||||
//! however, it could very easily be extended with additional functionalities.
|
||||
//!
|
||||
//! ## Sending Direct Message
|
||||
//!
|
||||
//! ```rust
|
||||
//! # use zulip_client::{Client, ZulipClientError};
|
||||
//! # use zulip_client::message::DirectMessage;
|
||||
//! # async fn try_send() -> Result<(), ZulipClientError> {
|
||||
//! let api_key = "your-api-key";
|
||||
//! let email = "associated-email-address";
|
||||
//! let server = "https://server-address.com";
|
||||
//! let client = Client::builder(email, api_key, server)?.build()?;
|
||||
//! // send to userid 12
|
||||
//! client.send_message((12u32, "hello world")).await?;
|
||||
//! // more concrete typing
|
||||
//! client.send_message(DirectMessage::new(12, "hello world2")).await?;
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use crate::error::ZulipClientError;
|
||||
use crate::message::{SendMessageResponse, SendableMessage};
|
||||
use nym_bin_common::bin_info;
|
||||
use nym_http_api_client::UserAgent;
|
||||
use reqwest::{header, Method, RequestBuilder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
use tracing::trace;
|
||||
use url::Url;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct ClientConfig {
|
||||
pub user_email: String,
|
||||
pub api_key: String,
|
||||
// TODO: introduce validation
|
||||
pub user_agent: Option<String>,
|
||||
pub server_url: Url,
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
server_url: Url,
|
||||
|
||||
api_key: Zeroizing<String>,
|
||||
user_email: String,
|
||||
|
||||
inner_client: reqwest::Client,
|
||||
}
|
||||
|
||||
fn default_user_agent() -> String {
|
||||
UserAgent::from(bin_info!()).to_string()
|
||||
}
|
||||
|
||||
impl Client {
|
||||
const MESSAGES_ENDPOINT: &'static str = "/api/v1/messages";
|
||||
|
||||
pub fn builder(
|
||||
user_email: impl Into<String>,
|
||||
api_key: impl Into<String>,
|
||||
server_url: impl Into<String>,
|
||||
) -> Result<ClientBuilder, ZulipClientError> {
|
||||
ClientBuilder::new(user_email, api_key, server_url)
|
||||
}
|
||||
|
||||
pub fn new(config: ClientConfig) -> Result<Self, ZulipClientError> {
|
||||
let builder = ClientBuilder::new(config.user_email, config.api_key, config.server_url)?;
|
||||
match config.user_agent {
|
||||
Some(user_agent) => builder.user_agent(user_agent).build(),
|
||||
None => builder.build(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_message(
|
||||
&self,
|
||||
msg: impl Into<SendableMessage>,
|
||||
) -> Result<SendMessageResponse, ZulipClientError> {
|
||||
let url = format!("{}{}", self.server_url, Self::MESSAGES_ENDPOINT);
|
||||
|
||||
self.build_request(Method::POST, Self::MESSAGES_ENDPOINT)
|
||||
.form(&msg.into())
|
||||
.send()
|
||||
.await
|
||||
.map_err(|source| ZulipClientError::RequestSendingFailure { source, url })?
|
||||
.json()
|
||||
.await
|
||||
.map_err(|source| ZulipClientError::RequestDecodeFailure { source })
|
||||
}
|
||||
|
||||
fn build_request(&self, method: Method, endpoint: &'static str) -> RequestBuilder {
|
||||
let url = format!("{}{endpoint}", self.server_url);
|
||||
trace!("posting to {url}");
|
||||
|
||||
self.inner_client
|
||||
.request(method, url)
|
||||
.basic_auth(&self.user_email, Some(self.api_key.to_string()))
|
||||
.header(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClientBuilder {
|
||||
api_key: Zeroizing<String>,
|
||||
user_email: String,
|
||||
server_url: Url,
|
||||
user_agent: Option<String>,
|
||||
}
|
||||
|
||||
impl ClientBuilder {
|
||||
pub fn new(
|
||||
user_email: impl Into<String>,
|
||||
api_key: impl Into<String>,
|
||||
server_url: impl Into<String>,
|
||||
) -> Result<Self, ZulipClientError> {
|
||||
let server_url = server_url.into();
|
||||
let parsed_url =
|
||||
Url::from_str(&server_url).map_err(|source| ZulipClientError::MalformedServerUrl {
|
||||
raw: server_url,
|
||||
source,
|
||||
})?;
|
||||
Ok(ClientBuilder {
|
||||
api_key: Zeroizing::new(api_key.into()),
|
||||
user_email: user_email.into(),
|
||||
server_url: parsed_url,
|
||||
user_agent: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn user_agent(mut self, user_agent: impl Into<String>) -> Self {
|
||||
self.user_agent = Some(user_agent.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<Client, ZulipClientError> {
|
||||
let user_agent = self.user_agent.unwrap_or_else(default_user_agent);
|
||||
Ok(Client {
|
||||
api_key: self.api_key,
|
||||
server_url: self.server_url,
|
||||
user_email: self.user_email,
|
||||
inner_client: reqwest::ClientBuilder::new()
|
||||
.user_agent(user_agent)
|
||||
.build()
|
||||
.map_err(|source| ZulipClientError::ClientBuildFailure { source })?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ZulipClientError {
|
||||
#[error("failed to send request to {url}: {source}")]
|
||||
RequestSendingFailure { url: String, source: reqwest::Error },
|
||||
|
||||
#[error("failed to decode received response: {source}")]
|
||||
RequestDecodeFailure { source: reqwest::Error },
|
||||
|
||||
#[error("failed to build internal client: {source}")]
|
||||
ClientBuildFailure { source: reqwest::Error },
|
||||
|
||||
#[error("provided url ({raw}) is malformed: {source}")]
|
||||
MalformedServerUrl {
|
||||
raw: String,
|
||||
source: url::ParseError,
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
pub mod client;
|
||||
pub mod error;
|
||||
pub mod message;
|
||||
|
||||
pub type Id = u32;
|
||||
|
||||
pub use client::{Client, ClientBuilder};
|
||||
pub use error::ZulipClientError;
|
||||
@@ -0,0 +1,215 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::message::to::{ToChannel, ToDirect};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod to;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "result")]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum SendMessageResponse {
|
||||
Success {
|
||||
id: i64,
|
||||
automatic_new_visibility_policy: Option<i64>,
|
||||
msg: String,
|
||||
},
|
||||
Error {
|
||||
code: String,
|
||||
msg: String,
|
||||
stream: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum SendableMessageContent {
|
||||
// old name: 'private'
|
||||
Direct {
|
||||
// internally this is a list
|
||||
to: String,
|
||||
content: String,
|
||||
},
|
||||
// alternative name: 'channel'
|
||||
Stream {
|
||||
to: String,
|
||||
topic: Option<String>,
|
||||
content: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct SendableMessage {
|
||||
#[serde(flatten)]
|
||||
content: SendableMessageContent,
|
||||
|
||||
/// For clients supporting local echo, the event queue ID for the client.
|
||||
/// If passed, `local_id` is required. If the message is successfully sent,
|
||||
/// the server will include `local_id` in the message event that the client with this `queue_id`
|
||||
/// will receive notifying it of the new message via `GET /events`.
|
||||
/// This lets the client know unambiguously that it should replace the locally echoed message,
|
||||
/// rather than adding this new message
|
||||
/// (which would be correct if the user had sent the new message from another device).
|
||||
/// example: "fb67bf8a-c031-47cc-84cf-ed80accacda8"
|
||||
queue_id: Option<String>,
|
||||
|
||||
/// For clients supporting local echo, a unique string-format identifier chosen freely by the client;
|
||||
/// the server will pass it back to the client without inspecting it, as described in the `queue_id` description.
|
||||
/// example: "100.01"
|
||||
local_id: Option<String>,
|
||||
|
||||
/// Whether the message should be initially marked read by its sender.
|
||||
/// If unspecified, the server uses a heuristic based on the client name.
|
||||
read_by_sender: bool,
|
||||
}
|
||||
|
||||
impl SendableMessage {
|
||||
pub fn new(content: impl Into<SendableMessageContent>) -> Self {
|
||||
SendableMessage {
|
||||
content: content.into(),
|
||||
queue_id: None,
|
||||
local_id: None,
|
||||
read_by_sender: false,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_queue(mut self, queue_id: impl Into<String>, local_id: impl Into<String>) -> Self {
|
||||
self.queue_id = Some(queue_id.into());
|
||||
self.local_id = Some(local_id.into());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn read_by_sender(mut self, read_by_sender: bool) -> Self {
|
||||
self.read_by_sender = read_by_sender;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub type PrivateMessage = DirectMessage;
|
||||
|
||||
pub struct DirectMessage {
|
||||
to: String,
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl DirectMessage {
|
||||
pub fn new(to: impl Into<ToDirect>, content: impl Into<String>) -> Self {
|
||||
DirectMessage {
|
||||
to: to.into().to_string(),
|
||||
content: content.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub type ChannelMessage = StreamMessage;
|
||||
pub struct StreamMessage {
|
||||
to: String,
|
||||
topic: Option<String>,
|
||||
content: String,
|
||||
}
|
||||
|
||||
impl StreamMessage {
|
||||
pub fn new(
|
||||
to: impl Into<ToChannel>,
|
||||
content: impl Into<String>,
|
||||
topic: Option<String>,
|
||||
) -> Self {
|
||||
StreamMessage {
|
||||
to: to.into().to_string(),
|
||||
topic,
|
||||
content: content.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn no_topic(to: impl Into<ToChannel>, content: impl Into<String>) -> Self {
|
||||
Self::new(to, content, None)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn with_topic(mut self, topic: impl Into<String>) -> Self {
|
||||
self.topic = Some(topic.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SendableMessageContent> for SendableMessage {
|
||||
fn from(content: SendableMessageContent) -> Self {
|
||||
SendableMessage::new(content)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DirectMessage> for SendableMessage {
|
||||
fn from(msg: DirectMessage) -> Self {
|
||||
SendableMessageContent::from(msg).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DirectMessage> for SendableMessageContent {
|
||||
fn from(msg: DirectMessage) -> Self {
|
||||
SendableMessageContent::Direct {
|
||||
to: msg.to,
|
||||
content: msg.content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<(T, S)> for DirectMessage
|
||||
where
|
||||
T: Into<ToDirect>,
|
||||
S: Into<String>,
|
||||
{
|
||||
fn from((to, content): (T, S)) -> Self {
|
||||
DirectMessage::new(to, content)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<(T, S)> for SendableMessage
|
||||
where
|
||||
T: Into<ToDirect>,
|
||||
S: Into<String>,
|
||||
{
|
||||
fn from((to, content): (T, S)) -> Self {
|
||||
DirectMessage::new(to, content).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StreamMessage> for SendableMessage {
|
||||
fn from(msg: StreamMessage) -> Self {
|
||||
SendableMessageContent::from(msg).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<StreamMessage> for SendableMessageContent {
|
||||
fn from(msg: StreamMessage) -> Self {
|
||||
SendableMessageContent::Stream {
|
||||
to: msg.to,
|
||||
topic: msg.topic,
|
||||
content: msg.content,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<(T, S, Option<S>)> for StreamMessage
|
||||
where
|
||||
T: Into<ToChannel>,
|
||||
S: Into<String>,
|
||||
{
|
||||
fn from((to, content, topic): (T, S, Option<S>)) -> Self {
|
||||
StreamMessage::new(to, content, topic.map(Into::into))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S> From<(T, S, Option<S>)> for SendableMessage
|
||||
where
|
||||
T: Into<ToChannel>,
|
||||
S: Into<String>,
|
||||
{
|
||||
fn from(inner: (T, S, Option<S>)) -> Self {
|
||||
StreamMessage::from(inner).into()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::Id;
|
||||
use itertools::Itertools;
|
||||
use std::fmt::Display;
|
||||
|
||||
// from the docs:
|
||||
// For channel messages, either the name or integer ID of the channel.
|
||||
// For direct messages, either a list containing integer user IDs
|
||||
// or a list containing string Zulip API email addresses.
|
||||
pub enum ToDirect {
|
||||
ByIds(Vec<Id>),
|
||||
ByNames(Vec<String>),
|
||||
}
|
||||
|
||||
impl Display for ToDirect {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ToDirect::ByIds(ids) => write!(f, "[{}]", ids.iter().join(",")),
|
||||
ToDirect::ByNames(names) => {
|
||||
write!(f, "[{}]", names.join(","))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<String>> for ToDirect {
|
||||
fn from(names: Vec<String>) -> Self {
|
||||
ToDirect::ByNames(names)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[String]> for ToDirect {
|
||||
fn from(names: &[String]) -> Self {
|
||||
names.to_vec().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[&str]> for ToDirect {
|
||||
fn from(names: &[&str]) -> Self {
|
||||
names
|
||||
.iter()
|
||||
.map(|s| s.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> From<&[&str; N]> for ToDirect {
|
||||
fn from(names: &[&str; N]) -> Self {
|
||||
names.as_slice().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<&str>> for ToDirect {
|
||||
fn from(names: Vec<&str>) -> Self {
|
||||
names.as_slice().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ToDirect {
|
||||
fn from(name: String) -> Self {
|
||||
ToDirect::ByNames(vec![name])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ToDirect {
|
||||
fn from(name: &str) -> Self {
|
||||
name.to_string().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for ToDirect {
|
||||
fn from(id: Id) -> Self {
|
||||
ToDirect::ByIds(vec![id])
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&[Id]> for ToDirect {
|
||||
fn from(ids: &[Id]) -> Self {
|
||||
ids.to_vec().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> From<&[Id; N]> for ToDirect {
|
||||
fn from(ids: &[Id; N]) -> Self {
|
||||
ids.as_slice().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<Id>> for ToDirect {
|
||||
fn from(ids: Vec<Id>) -> Self {
|
||||
ToDirect::ByIds(ids)
|
||||
}
|
||||
}
|
||||
|
||||
pub enum ToChannel {
|
||||
ByName(String),
|
||||
ById(Id),
|
||||
}
|
||||
|
||||
impl Display for ToChannel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
ToChannel::ByName(name) => name.fmt(f),
|
||||
ToChannel::ById(id) => id.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for ToChannel {
|
||||
fn from(name: String) -> Self {
|
||||
ToChannel::ByName(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&str> for ToChannel {
|
||||
fn from(name: &str) -> Self {
|
||||
name.to_string().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Id> for ToChannel {
|
||||
fn from(id: Id) -> Self {
|
||||
ToChannel::ById(id)
|
||||
}
|
||||
}
|
||||
Generated
+1
@@ -1091,6 +1091,7 @@ dependencies = [
|
||||
name = "nym-coconut-dkg"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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, ¤t_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 {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user