Compare commits

..

59 Commits

Author SHA1 Message Date
durch f6d49d2e1f Disable delay and retransmission for network-monitor 2025-06-25 15:01:37 +02:00
durch 1665b1f2a6 Fix off by 100x 2025-06-25 12:48:39 +02:00
durch 94cc0db2d2 Dedup routes before submission, stagger out batches 2025-06-25 12:00:17 +02:00
durch 688660a7d5 Remove OR ignore 2025-06-25 11:44:44 +02:00
durch e1a0235556 Filter only on tested nodes 2025-06-25 10:59:39 +02:00
durch 29227f452d fix(nym-api): prevent duplicate simulations for the same epoch
- Add create_or_get_simulated_reward_epoch to check for existing simulations
- Skip simulation if already exists for the epoch and calculation method
- Add tests to verify duplicate prevention works correctly
- Update all test calls to use the new method

This fixes the issue where multiple simulations were created for the same
epoch when epoch operations were retried or the service restarted.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-25 10:50:15 +02:00
durch c5c64327da Fix simulated epoch generation 2025-06-25 08:31:14 +02:00
durch 1364316f95 FMT, fix typo 2025-06-25 00:43:53 +02:00
durch 55f0471481 fix(nym-api): refactor simulation API to remove redundant data structures
- Remove old_method/new_method nested objects from NodeMethodComparison
- Use direct fields for production_performance and simulated_performance
- Update build_node_comparisons_from_single_dataset to create cleaner structure
- Fix calculate_summary_statistics to work with new data model
- Update all tests to match new structure

The API now returns a single dataset with production performance values
included directly, rather than separate old/new method objects.

- Deduplicate routes, to control for deterministic routing
2025-06-25 00:40:52 +02:00
durch a127a303f1 refactor(nym-api): simplify simulation to only run new method
Since old method data is already available in production database,
simulation now only calculates new method performance. The API combines
production data with simulated data at query time.

Key changes:
- Remove old_method_simulation and run_both_methods config
- Add production_performance field to NodePerformanceData model
- Update API endpoints to fetch production data from node annotations cache
- Refactor compare_methods to use single performance dataset
- Fix route analysis comparison to handle missing old method data
- Update all tests to use new single-dataset approach

This simplifies the simulation code and ensures consistency with
production calculations.
2025-06-24 23:43:24 +02:00
durch 730c88b0d2 feat(nym-api): add reliability distribution categories to simulation comparison
- Added ReliabilityDistribution struct with 6 categories: excellent (>95%), very_good (90-95%), good (75-90%), moderate (50-75%), poor (25-50%), very_poor (<25%)
- Updated ComparisonSummaryStats to include distribution breakdowns for both old and new methods
- Provides better insight into performance distribution beyond averages
- Added tests for distribution calculation
2025-06-24 21:53:29 +02:00
durch 9cb18cd163 Bump nnm 2025-06-24 21:35:06 +02:00
durch 6993ef0dc8 Bump nym-api 2025-06-24 21:31:50 +02:00
durch 0e53562ce2 Fmt 2025-06-24 21:31:15 +02:00
Drazen Urch c1acef9bc8 Check gateway supported versions (#5860)
* Check gateway supported versions

* Fix boxed errors

* Fmt

* feat(client-core): integrate gateway protocol validation into SDK

Enable protocol version checking for all SDK-based clients by using
gateways_for_init_with_protocol_validation in MixnetClientBuilder.
This ensures clients only connect to gateways with compatible protocol
versions, preventing potential communication issues.

- Replace gateways_for_init with gateways_for_init_with_protocol_validation
- Add import for the new validation function
- Protocol validation now active for network monitor and all SDK users

* refactor(client-core): allow gateways with newer protocol versions

Change protocol validation to be more permissive - instead of rejecting
gateways with newer protocol versions, now logs a warning and continues.
This enables graceful degradation when gateways upgrade, relying on their
backward compatibility while signaling users to update their clients.

Changes:
- Accept gateways with protocol version > client version
- Log warning about version mismatch suggesting client update
- Update log messages from "validation" to "check" for clarity
- Add trace logging showing both gateway and client versions

This prevents connectivity issues when gateways upgrade before clients,
improving overall network resilience.

* feat(nym-network-monitor): add diagnostic logging for ConnectionRefused debugging

- Add detailed logging for client rotation lifecycle with [CLIENT_ROTATION_*] tags
- Add request tracking with unique IDs using the random message as identifier
- Add HTTP connection acceptance logging with [HTTP_REQUEST] tags
- Improve locust script with connection error handling and backoff strategy
- Add TCP backlog configuration support (default 1024)
- Add socket configuration with SO_REUSEADDR and configurable backlog
- Add HTTP timeout and tracing layers for better observability

These changes help diagnose when and why ConnectionRefused errors occur during load testing,
particularly around client rotation periods.

* fix(nym-api): calculate route average reliability as node mean instead of route-based

- Changed route average reliability calculation to use mean of all node reliabilities
- Added median calculation alongside mean for better statistical representation
- Fixed null average reliability for old method which doesn't analyze routes
- Rounded all float values to 2 decimal places in API responses for cleaner display
- Store median values in analysis_parameters JSON field
2025-06-24 21:28:07 +02:00
durch d67a968e76 fix(nym-api): correct reliability value conversions in performance simulation
The old method simulation was incorrectly treating reliability values (0-1 fractions)
as percentages, causing all performance scores to be truncated to 0 when cast to u64.

Changes:
- Old method: multiply reliability by 100 before passing to Performance::from_percentage_value()
- New method: remove division by 100 since Performance::naive_try_from_f64() expects fractions
- Route analysis: multiply average reliability by 100 for percentage display

This ensures consistent handling where internal calculations use fractions (0-1)
while storage and API responses use percentages (0-100).
2025-06-24 19:05:07 +02:00
durch 8ad641f8f8 fix(nym-api): allow simulation mode to run when another API is advancing epoch
In simulation mode, the nym-api should be able to run performance
simulations regardless of which instance is handling epoch advancement,
since simulations don't perform any blockchain transactions.

This fix:
- Allows simulation mode to proceed when another API is advancing the epoch
- Skips epoch transition attempts in simulation mode
- Skips actual reward distribution blockchain operations in simulation mode
2025-06-24 17:44:25 +02:00
durch b45bb9e7e9 Add socket-level websocket connection health checking
Implements proper socket-level health checking for gateway websocket connections:

- Add is_connection_alive() method to GatewayTransceiver trait
- Implement socket-level health check using TcpStream::peek() on Unix systems
- Add is_gateway_connection_alive() method to MixnetClient for easy access
- Update network monitor to use connection health checks before sending
- Fix axum router state configuration in network monitor

The health check performs actual OS-level socket validation rather than just
checking file descriptor existence, providing reliable connection status.
2025-06-06 12:15:58 +02:00
durch cab072f2d0 Fix client drop loop hanging in network monitor
Replace infinite busy-wait loop with timeout-based client cleanup to prevent potential system hangs during client lifecycle management.
2025-06-06 11:02:06 +02:00
durch c389f43dd0 Put client factory back 2025-06-06 10:51:32 +02:00
durch 71c24d8c81 Respond with 504 to timeouts 2025-06-06 10:49:03 +02:00
durch e336e02df2 Dont use hickory dns for gateway client 2025-06-06 10:42:43 +02:00
durch 74db9be819 Fix config cleanup 2025-06-06 10:22:55 +02:00
durch 77c4acf602 Add RESET_CONFIG option to entrypoint 2025-06-06 09:38:28 +02:00
durch f4d0ac855c Add periodic route data cleanup to epoch operations
Implements automatic cleanup of route monitoring results to prevent
unbounded storage growth while maintaining data for performance analysis.

- Add purge_old_routes() method to StorageManager following existing patterns
- Integrate route cleanup into purge_old_statuses() wrapper function
- Route data now purged every epoch with 48-hour retention, to facilitate comparisons with legacy data
- Update logging to reflect cleanup of both node statuses and routes
2025-06-06 09:12:21 +02:00
durch eb1c7d649e Client per request 2025-06-05 12:06:15 +02:00
durch 75f34ef51b Add timeout to locust 2025-06-05 11:43:04 +02:00
durch 4f7fa557d5 Optimize database queries by eliminating N+1 patterns in simulation system
This commit addresses critical N+1 query performance issues identified in
the reward simulation system. The optimizations significantly reduce database
round trips and improve performance when processing large datasets.

**Key Optimizations:**

1. **Batch Identity Key Lookups**
   - Added `get_mixnode_identity_keys_batch()` and `get_gateway_identity_keys_batch()`
   - Updated simulation performance conversion to use batch operations
   - Reduced from N individual queries to 2 batch queries

2. **Batch Node Classification**
   - Added `classify_nodes_batch()` method for mixnode/gateway determination
   - Updated reliability calculation methods to use batch classification
   - Reduced from N individual lookups to 2 batch queries

3. **Batch Epoch Metadata Enhancement**
   - Added `count_simulated_node_performance_for_epochs_batch()`
   - Added `get_available_calculation_methods_for_epochs_batch()`
   - Updated API handlers to use batch operations for metadata enhancement
   - Reduced from 2N queries to 2 batch queries for epoch data

4. **Bulk Insert Optimizations**
   - Converted individual INSERT operations to use `sqlx::QueryBuilder::push_values()`
   - Optimized simulation data insertion methods
   - Eliminated transaction overhead from individual inserts

**Performance Impact:**
- Before: N+2N database queries for N nodes/epochs
- After: 2+2 batch queries regardless of dataset size
- Significant performance improvement for large simulation datasets

All changes maintain backward compatibility while providing substantial
performance benefits for the reward simulation system.
2025-06-05 10:45:08 +02:00
durch a96fb098c2 Locust sleep if no clients are available 2025-06-05 09:54:39 +02:00
durch ad5c6ab829 Enhance simulation system with performance comparison framework
Refactors the simulation system to focus on performance methodology comparison
rather than reward amounts, enabling robust analysis of old vs new calculation
methods. Key improvements:

- Replace simulated_rewards table with performance_comparisons for better metrics
- Add performance_rankings table for ranking analysis across methodologies
- Enhance database schema with additional performance tracking fields
- Update simulation coordinator to use performance-focused data structures
- Add comprehensive performance ranking calculations
- Improve API models and handlers for performance comparison workflows
- Update SQLx query cache with new database schema changes

This provides a foundation for data-driven performance methodology evaluation
while maintaining separation from actual reward calculations.
2025-06-04 16:59:42 +02:00
durch b3d07e8832 Tests 2025-06-04 11:28:47 +02:00
durch e761255174 Add complete simulation API layer for reward method comparison
This completes Phase 3 of the simulation system implementation:

- Add comprehensive REST API endpoints for simulation data access
- Implement /v1/simulation/* routes with full CRUD operations
- Support JSON/CSV export for external analysis
- Add statistical comparison between old vs new methods
- Provide node performance history tracking
- Include proper error handling and response formatting
- Simplify simulation coordinator to remove unused complex return types
- Clean up dead code while maintaining all functionality
- Pass clippy with no warnings

The simulation API provides complete access to:
- Simulation epoch listing and details
- Method comparison analytics (old 24h vs new 1h)
- Node performance analysis across epochs
- Route reliability statistics
- Export capabilities for further analysis

All simulation data is persisted and accessible via REST endpoints.
2025-06-03 15:38:58 +02:00
durch e4a20f9cf5 Implement core simulation logic for dual reward calculations
Add complete simulation engine that compares old (24h cache-based) vs new (1h route-based)
reward calculation methodologies with full integration into epoch operations.

Core Simulation Engine:
- Add SimulationCoordinator with configurable time windows and comparison settings
- Implement dual calculation methods with proper Performance type conversions
- Add comprehensive error handling with DatabaseError variant in RewardingError
- Store simulation results in database with proper relationship constraints

Old Method Implementation (24h Cache-Based):
- Wrap existing reliability calculation using get_all_avg_mix_reliability_in_last_24hr()
- Convert reliability percentages to Performance types using from_percentage_value()
- Maintain exact same logic as production for accurate baseline comparison
- Generate simulation data structures with proper metadata

New Method Implementation (1h Route-Based):
- Leverage calculate_corrected_node_reliabilities_for_interval() for route analysis
- Support configurable time windows (default 1 hour vs 24 hours)
- Provide detailed route statistics including success rates and failure analysis
- Convert route reliability data to Performance types with naive_try_from_f64()

Epoch Operations Integration:
- Extend EpochAdvancer struct with optional SimulationConfig field
- Update constructor and start method to accept simulation configuration
- Add simulation trigger in perform_epoch_operations() before real rewarding
- Ensure simulation failures don't break epoch advancement process

CLI Integration:
- Update run.rs to handle both --enable-rewarding and --simulate-rewarding modes
- Create SimulationConfig from rewarding.debug configuration settings
- Implement mutual exclusivity between real rewarding and simulation mode
- Skip permission checks for simulation-only mode (no blockchain transactions)

The simulation system runs in parallel with epoch operations, storing comparative
data for analysis without affecting production reward distribution.
2025-06-03 14:52:13 +02:00
durch 1eefe8a579 Add simulated rewarding system foundation
Implement foundation for simulated reward calculations to compare old (24h cache-based)
vs new (1h route-based) methodologies without blockchain transactions.

Database Changes:
- Add migration with 4 new tables for simulation system
- simulated_reward_epochs: tracks each simulation run
- simulated_node_performance: stores performance calculations
- simulated_rewards: stores reward calculation results
- simulated_route_analysis: metadata for route analysis
- Add comprehensive indexes for efficient querying

Configuration Changes:
- Add simulation_mode flag to rewarding configuration
- Add CLI flag --simulate-rewarding with proper dependencies/conflicts
- Add validation for simulation-specific settings
- Add time window configuration for new method (default: 1 hour)

Storage Layer:
- Add model structs with SQLx FromRow derives for all simulation tables
- Add comprehensive CRUD methods for simulation data management
- Add proper type annotations to fix SQLx compile issues
- Maintain separation between simulation and real rewarding logic

The simulation mode is mutually exclusive with real rewarding and does not
require mnemonic since no blockchain transactions are performed.
2025-06-03 12:39:44 +02:00
durch e9dc848950 Bump nym-api 2025-06-02 12:49:13 +02:00
durch 81162fba7e Allow nym-api init fail 2025-06-02 12:45:21 +02:00
durch be36da68b1 Clap value_delimiter 2025-06-02 11:58:12 +02:00
durch 21a56e307f Bump NNM 2025-06-02 09:31:09 +02:00
durch bd966383be Towards untangling nym-api client 2025-06-02 09:30:36 +02:00
durch 7626785ce4 Bump NM version 2025-05-27 15:40:59 +02:00
durch 6f79d39d48 Filter out non mixnodes 2025-05-27 15:40:33 +02:00
durch 014b5f767a Log APIs used 2025-05-27 15:23:18 +02:00
durch e0966565e6 Mnemonic to run, bump 2025-05-27 13:38:13 +02:00
durch c6aec663b7 Bump nym-api version 2025-05-27 13:18:31 +02:00
durch 7d041ddd44 Explicit mnemonic to entrypoint 2025-05-27 13:02:38 +02:00
durch 5d8bdc6570 Bunch of new query files 2025-05-27 10:28:00 +02:00
durch 06c412b3ba Remove debug logging 2025-05-27 10:25:27 +02:00
durch 356cf00106 Put the monitoring back properly 2025-05-27 10:25:27 +02:00
durch 58493a69aa Fix submission URLs 2025-05-27 10:25:27 +02:00
durch e881da834b More NM logging 2025-05-27 10:25:27 +02:00
durch eee9d8ab0c DEBUG: disable epoch operations, less noisy logging 2025-05-27 10:25:27 +02:00
durch 09026307f4 Debug logging for nym-api 2025-05-27 10:25:27 +02:00
durch 507ddf246c Stagger out route sending 2025-05-27 10:25:26 +02:00
durch 8d8ce29113 Update NM readme, fmt 2025-05-27 10:25:26 +02:00
durch 3be9e06bef sqlx prepare, bunch of nits 2025-05-27 10:25:26 +02:00
durch 770078a9ed Delete test script 2025-05-27 10:25:26 +02:00
durch fcffebfe45 Raw route handling and reliability corrections 2025-05-27 10:25:19 +02:00
durch 9c7d79683b Force routing through all nodes 2025-05-27 10:21:02 +02:00
durch c7f34d04c0 Support submitting to multiple APIs 2025-05-27 10:21:02 +02:00
1594 changed files with 130961 additions and 67226 deletions
+3 -3
View File
@@ -415,9 +415,9 @@
} }
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "5.29.0", "version": "5.28.5",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz",
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", "integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fastify/busboy": "^2.0.0" "@fastify/busboy": "^2.0.0"
@@ -38,14 +38,15 @@ jobs:
rm -rf ci-builds || true rm -rf ci-builds || true
mkdir -p $OUTPUT_DIR mkdir -p $OUTPUT_DIR
echo $OUTPUT_DIR echo $OUTPUT_DIR
- name: Install Dependencies (Linux) - name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libudev-dev run: sudo apt-get update && sudo apt-get -y install libudev-dev
- name: Sets env vars for tokio if set in manual dispatch inputs - name: Sets env vars for tokio if set in manual dispatch inputs
if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true
run: | run: |
echo "RUSTFLAGS=--cfg tokio_unstable" >> $GITHUB_ENV echo 'RUSTFLAGS="--cfg tokio_unstable"' >> $GITHUB_ENV
echo "CARGO_FEATURES=--features tokio-console" >> $GITHUB_ENV if: github.event_name == 'workflow_dispatch' && inputs.add_tokio_unstable == true
- name: Install Rust stable - name: Install Rust stable
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
@@ -102,6 +103,7 @@ jobs:
if [ ${{ github.event_name == 'workflow_dispatch' && inputs.enable_deb == true }} = true ]; then if [ ${{ github.event_name == 'workflow_dispatch' && inputs.enable_deb == true }} = true ]; then
cp target/debian/*.deb $OUTPUT_DIR cp target/debian/*.deb $OUTPUT_DIR
fi fi
- name: Deploy branch to CI www - name: Deploy branch to CI www
continue-on-error: true continue-on-error: true
uses: easingthemes/ssh-deploy@main uses: easingthemes/ssh-deploy@main
+8 -8
View File
@@ -5,6 +5,7 @@ on:
paths: paths:
- 'clients/**' - 'clients/**'
- 'common/**' - 'common/**'
- 'explorer-api/**'
- 'gateway/**' - 'gateway/**'
- 'integrations/**' - 'integrations/**'
- 'nym-api/**' - 'nym-api/**'
@@ -12,7 +13,6 @@ on:
- 'nym-network-monitor/**' - 'nym-network-monitor/**'
- 'nym-node/**' - 'nym-node/**'
- 'nym-node-status-api/**' - 'nym-node-status-api/**'
- 'nym-statistics-api/**'
- 'nym-outfox/**' - 'nym-outfox/**'
- 'nym-validator-rewarder/**' - 'nym-validator-rewarder/**'
- 'nyx-chain-watcher/**' - 'nyx-chain-watcher/**'
@@ -38,7 +38,7 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
os: [ arc-linux-latest, custom-windows-11, custom-macos-15 ] os: [ arc-ubuntu-22.04, custom-windows-11, custom-runner-mac-m1 ]
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@@ -46,9 +46,9 @@ jobs:
RUSTUP_PERMIT_COPY_RENAME: 1 RUSTUP_PERMIT_COPY_RENAME: 1
steps: steps:
- name: Install Dependencies (Linux) - name: Install Dependencies (Linux)
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler cmake run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools protobuf-compiler
continue-on-error: true continue-on-error: true
if: contains(matrix.os, 'linux') if: contains(matrix.os, 'ubuntu')
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -63,7 +63,7 @@ jobs:
# To avoid running out of disk space, skip generating debug symbols # To avoid running out of disk space, skip generating debug symbols
- name: Set debug to false (unix) - name: Set debug to false (unix)
if: contains(matrix.os, 'linux') || contains(matrix.os, 'mac') if: contains(matrix.os, 'ubuntu') || contains(matrix.os, 'mac')
run: | run: |
sed -i.bak 's/\[profile.dev\]/\[profile.dev\]\ndebug = false/' Cargo.toml sed -i.bak 's/\[profile.dev\]/\[profile.dev\]\ndebug = false/' Cargo.toml
git diff git diff
@@ -93,14 +93,14 @@ jobs:
command: build command: build
- name: Build all examples - name: Build all examples
if: contains(matrix.os, 'linux') if: contains(matrix.os, 'ubuntu')
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: build command: build
args: --workspace --examples args: --workspace --examples
- name: Run all tests - name: Run all tests
if: contains(matrix.os, 'linux') if: contains(matrix.os, 'ubuntu')
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
env: env:
NYM_API: https://sandbox-nym-api1.nymtech.net/api NYM_API: https://sandbox-nym-api1.nymtech.net/api
@@ -109,7 +109,7 @@ jobs:
args: --workspace args: --workspace
- name: Run expensive tests - name: Run expensive tests
if: (github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master') && contains(matrix.os, 'linux') if: (github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master') && contains(matrix.os, 'ubuntu')
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: test command: test
@@ -44,10 +44,8 @@ jobs:
echo "Tag is empty" echo "Tag is empty"
exit 1 exit 1
fi fi
# first, list all tags for logging purposes
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
# check if there's a matching tag exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' )
if [[ $exists = "true" ]]; then if [[ $exists = "true" ]]; then
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo" echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
exit 1 exit 1
@@ -55,5 +53,5 @@ jobs:
echo "Version '$TAG' doesn't exist on the remote" echo "Version '$TAG' doesn't exist on the remote"
else else
echo "Unknown output '$exists'" echo "Unknown output '$exists'"
exit 2 exit 1
fi fi
@@ -1,59 +0,0 @@
name: ci-check-nym-stats-api-version
on:
pull_request:
paths:
- "nym-statistics-api/**"
env:
WORKING_DIRECTORY: "nym-statistics-api"
jobs:
check-if-tag-exists:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Checkout repo
uses: actions/checkout@v4
- 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
- name: Check if git tag exists
run: |
TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
if [[ -z "$TAG" ]]; then
echo "Tag is empty"
exit 1
fi
git ls-remote --tags origin | awk '{print $2}'
if git ls-remote --tags origin | awk '{print $2}' | grep -q "refs/tags/$TAG$" ; then
echo "Tag '$TAG' ALREADY EXISTS on the remote"
exit 1
else
echo "Tag '$TAG' does not exist on the remote"
fi
- name: Check if harbor tag exists
run: |
TAG=${{ steps.get_version.outputs.result }}
registry=https://harbor.nymte.ch
repo_name=nym/nym-statistics-api
if [[ -z $TAG ]]; then
echo "Tag is empty"
exit 1
fi
# first, list all tags for logging purposes
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
# check if there's a matching tag
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' )
if [[ $exists = "true" ]]; then
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
exit 1
elif [[ $exists = "false" ]]; then
echo "Version '$TAG' doesn't exist on the remote"
else
echo "Unknown output '$exists'"
exit 2
fi
@@ -31,26 +31,31 @@ jobs:
- name: Install Rust stable - name: Install Rust stable
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: stable
target: wasm32-unknown-unknown target: wasm32-unknown-unknown
override: true override: true
- name: Install wasm-opt
uses: ./.github/actions/install-wasm-opt
with:
version: '114'
- name: Install cosmwasm-check - name: Install cosmwasm-check
run: cargo install cosmwasm-check run: cargo install cosmwasm-check
- name: Build release contracts - name: Build release contracts
run: make publish-contracts run: make contracts
- name: Prepare build output - name: Prepare build output
shell: bash shell: bash
env: env:
OUTPUT_DIR: ci-contract-builds/${{ github.ref_name }} OUTPUT_DIR: ci-contract-builds/${{ github.ref_name }}
run: | run: |
find contracts/artifacts -maxdepth 1 -type f -name '*.wasm' -exec cp {} $OUTPUT_DIR \; cp contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm $OUTPUT_DIR
# Also include the optimizer-generated checksums if present cp contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm $OUTPUT_DIR
if [ -f contracts/artifacts/checksums.txt ]; then cp contracts/target/wasm32-unknown-unknown/release/nym_coconut_dkg.wasm $OUTPUT_DIR
cp contracts/artifacts/checksums.txt $OUTPUT_DIR cp contracts/target/wasm32-unknown-unknown/release/cw3_flex_multisig.wasm $OUTPUT_DIR
fi cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_ecash.wasm $OUTPUT_DIR
- name: Deploy branch to CI www - name: Deploy branch to CI www
continue-on-error: true continue-on-error: true
+1 -3
View File
@@ -20,7 +20,6 @@ jobs:
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
RUSTUP_PERMIT_COPY_RENAME: 1
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -28,8 +27,7 @@ jobs:
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
profile: minimal profile: minimal
# pinned due to issues building contracts toolchain: stable
toolchain: 1.86.0
target: wasm32-unknown-unknown target: wasm32-unknown-unknown
override: true override: true
components: rustfmt, clippy components: rustfmt, clippy
-19
View File
@@ -1,19 +0,0 @@
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 }}
+6 -8
View File
@@ -19,11 +19,7 @@ jobs:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }} if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: runs-on: arc-ubuntu-22.04
include:
- os: arc-ubuntu-22.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
outputs: outputs:
release_id: ${{ steps.create-release.outputs.id }} release_id: ${{ steps.create-release.outputs.id }}
@@ -56,7 +52,7 @@ jobs:
- name: Install Rust stable - name: Install Rust stable
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
toolchain: 1.86.0 toolchain: stable
override: true override: true
- name: Build all binaries - name: Build all binaries
@@ -70,6 +66,7 @@ jobs:
with: with:
name: my-artifact name: my-artifact
path: | path: |
target/release/explorer-api
target/release/nym-client target/release/nym-client
target/release/nym-socks5-client target/release/nym-socks5-client
target/release/nym-api target/release/nym-api
@@ -78,13 +75,14 @@ jobs:
target/release/nymvisor target/release/nymvisor
target/release/nym-node target/release/nym-node
retention-days: 30 retention-days: 30
- id: create-release - id: create-release
name: Upload to release based on tag name name: Upload to release based on tag name
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 uses: softprops/action-gh-release@v2
if: github.event_name == 'release' if: github.event_name == 'release'
with: with:
files: | files: |
target/release/explorer-api
target/release/nym-client target/release/nym-client
target/release/nym-socks5-client target/release/nym-socks5-client
target/release/nym-api target/release/nym-api
+15 -38
View File
@@ -5,15 +5,8 @@ on:
inputs: inputs:
gateway_probe_git_ref: gateway_probe_git_ref:
type: string type: string
default: nym-vpn-core-v1.4.0
required: true
description: Which gateway probe git ref to build the image with description: Which gateway probe git ref to build the image with
release_image:
description: 'Tag image as a release'
required: true
default: false
type: boolean
env: env:
WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-agent" WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-agent"
CONTAINER_NAME: "node-status-agent" CONTAINER_NAME: "node-status-agent"
@@ -38,10 +31,10 @@ jobs:
git config --global user.name "Lawrence Stalder" git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml - name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
id: get_version id: get_version
run: | with:
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml) cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
echo "result=$VERSION" >> $GITHUB_OUTPUT
- name: cleanup-gateway-probe-ref - name: cleanup-gateway-probe-ref
id: cleanup_gateway_probe_ref id: cleanup_gateway_probe_ref
@@ -50,35 +43,19 @@ jobs:
GIT_REF_SLUG="${GATEWAY_PROBE_GIT_REF//\//-}" GIT_REF_SLUG="${GATEWAY_PROBE_GIT_REF//\//-}"
echo "git_ref=${GIT_REF_SLUG}" >> $GITHUB_OUTPUT echo "git_ref=${GIT_REF_SLUG}" >> $GITHUB_OUTPUT
- name: Set GIT_TAG variable - name: Remove existing tag if exists
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV run: |
if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} >/dev/null 2>&1; then
git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}
git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}
fi
- name: Initialize RELEASE_TAG - name: Create tag
run: echo "RELEASE_TAG=" >> $GITHUB_ENV run: |
git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} -m "Version ${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}"
- name: Set RELEASE_TAG for release git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}
if: github.event.inputs.release_image == 'true'
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
- name: Set IMAGE_NAME_AND_TAGS variable
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
- name: New env vars
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
# - name: Remove existing tag if exists
# run: |
# if git rev-parse $${{ env.GIT_TAG }} >/dev/null 2>&1; then
# git push --delete origin $${{ env.GIT_TAG }}
# git tag -d $${{ env.GIT_TAG }}
# fi
# - name: Create tag
# run: |
# git tag -a $${{ env.GIT_TAG }} -m "Version ${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}"
# git push origin $${{ env.GIT_TAG }}
- name: BuildAndPushImageOnHarbor - name: BuildAndPushImageOnHarbor
run: | run: |
docker build --build-arg GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }} -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }} docker build --build-arg GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }} -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
+20 -39
View File
@@ -1,13 +1,7 @@
name: Build and upload Node Status API container to harbor.nymte.ch name: Build and upload Node Status API container to harbor.nymte.ch
on: on:
workflow_dispatch: workflow_dispatch:
inputs:
release_image:
description: 'Tag image as a release'
required: true
default: false
type: boolean
env: env:
WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-api" WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-api"
CONTAINER_NAME: "node-status-api" CONTAINER_NAME: "node-status-api"
@@ -32,43 +26,30 @@ jobs:
git config --global user.name "Lawrence Stalder" git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml - name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
id: get_version id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
- name: Check if tag exists
run: | run: |
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml) if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then
echo "result=$VERSION" >> $GITHUB_OUTPUT echo "Tag ${{ steps.get_version.outputs.result }} already exists"
fi
- name: Set GIT_TAG variable - name: Remove existing tag if exists
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV run: |
if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then
git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
fi
- name: Initialise RELEASE_TAG - name: Create tag
run: echo "RELEASE_TAG=" >> $GITHUB_ENV run: |
git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}"
- name: Set RELEASE_TAG for release git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
if: github.event.inputs.release_image == 'true'
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
- name: Set IMAGE_NAME_AND_TAGS variable
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
- name: New env vars
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
# - name: Remove existing tag if exists, then create
# run: |
# if git rev-parse "$GIT_TAG" >/dev/null 2>&1; then
# echo "Tag '$GIT_TAG' already exists, deleting"
# git push --delete origin "$GIT_TAG"
# git tag -d "$GIT_TAG"
# echo "Tag '$GIT_TAG' deleted"
# else
# echo "Tag '$GIT_TAG' does not exist, creating it"
# git tag -a $GIT_TAG -m "Version ${{ steps.get_version.outputs.result }}"
# git push origin $GIT_TAG
# echo "Tag '$GIT_TAG' created"
# fi
- name: BuildAndPushImageOnHarbor - name: BuildAndPushImageOnHarbor
run: | run: |
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile-sqlite . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
@@ -1,42 +0,0 @@
name: Build and upload Nym Statistics API container to harbor.nymte.ch
on:
workflow_dispatch:
env:
WORKING_DIRECTORY: "nym-statistics-api"
CONTAINER_NAME: "nym-statistics-api"
jobs:
build-container:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
- name: Checkout repo
uses: actions/checkout@v4
- name: Configure git identity
run: |
git config --global user.email "lawrence@nymtech.net"
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
- name: Create tag
run: |
git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}"
git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
- name: BuildAndPushImageOnHarbor
run: |
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
+2 -2
View File
@@ -35,13 +35,12 @@ validator-api/keypair
contracts/mixnet/code_id contracts/mixnet/code_id
contracts/mixnet/Justfile contracts/mixnet/Justfile
contracts/mixnet/Makefile contracts/mixnet/Makefile
artifacts
contracts/artifacts
validator-config validator-config
*.patch *.patch
validator-api-config.toml validator-api-config.toml
dist dist
storybook-static storybook-static
envs/qwerty.env
.parcel-cache .parcel-cache
**/.DS_Store **/.DS_Store
cpu-cycles/libcpucycles/build cpu-cycles/libcpucycles/build
@@ -63,3 +62,4 @@ nym-api/redocly/formatted-openapi.json
**/settings.sql **/settings.sql
**/enter_db.sh **/enter_db.sh
CLAUDE.md
-228
View File
@@ -4,234 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased] ## [Unreleased]
## [2025.15-gruyere] (2025-08-20)
- Migrate strum to 0.27.2 ([#5960])
- WG exit policy scripts update ([#5921])
- Make DNS Resolver fallback optional ([#5920])
- nym-node debug command to reset providers db ([#5914])
- basic zulip client for sending messages ([#5913])
- chore: allow compatibility with 'CDLA-Permissive-2.0' ([#5910])
- feat: ecash liveness check ([#5890])
- Remove old free credential handle ([#5864])
[#5960]: https://github.com/nymtech/nym/pull/5960
[#5921]: https://github.com/nymtech/nym/pull/5921
[#5920]: https://github.com/nymtech/nym/pull/5920
[#5914]: https://github.com/nymtech/nym/pull/5914
[#5913]: https://github.com/nymtech/nym/pull/5913
[#5910]: https://github.com/nymtech/nym/pull/5910
[#5890]: https://github.com/nymtech/nym/pull/5890
[#5864]: https://github.com/nymtech/nym/pull/5864
## [2025.14-feta] (2025-08-05)
- chore: nym node tokio console ([#5909])
- Feature/dkg snapshot epoch ([#5900])
- Feature/dkg epoch dealers query ([#5899])
- sqlx-pool-guard: allocate more memory on windows ([#5896])
- Support mnemonic in the NS agent ([#5883])
- Allow PG database backend ([#5880])
[#5909]: https://github.com/nymtech/nym/pull/5909
[#5900]: https://github.com/nymtech/nym/pull/5900
[#5899]: https://github.com/nymtech/nym/pull/5899
[#5896]: https://github.com/nymtech/nym/pull/5896
[#5883]: https://github.com/nymtech/nym/pull/5883
[#5880]: https://github.com/nymtech/nym/pull/5880
## [2025.13-emmental] (2025-07-22)
- fix: don't allow mixnode running in exit mode ([#5898])
- fix contract build process in Makefile ([#5892])
- bugfix: ignore 'Send' responses when claiming bandwidth ([#5884])
- Update push-node-status-agent.yaml ([#5882])
- listen for shutdown signals during nym-node startup ([#5879])
- feat: forbid running mixnode + entry on the same node ([#5878])
- chore: 1.88 clippy ([#5877])
- Batch SQL writes for packet stats ([#5874])
- fix the broken link ([#5873])
- Set busy_timeout in sqlx ([#5872])
- feat: basic performance contract integration [within Nym API] ([#5871])
- scraper bugfix: ignore precommits from missing validators ([#5867])
- Return true remaining ([#5866])
- Make Mix hops optional for Mixnet Client SURBs ([#5861])
- Check gateway supported versions ([#5860])
- Add build info endpoints ([#5857])
- Clear out screaming logs ([#5856])
- fix removal of qa env ([#5855])
- Use display when printing paths ([#5853])
- feat: initial performance contract ([#5833])
- Security patches for the `dkg` crate ([#5828])
- HTTP Discovery objects & network defaults ([#5814])
[#5898]: https://github.com/nymtech/nym/pull/5898
[#5892]: https://github.com/nymtech/nym/pull/5892
[#5884]: https://github.com/nymtech/nym/pull/5884
[#5882]: https://github.com/nymtech/nym/pull/5882
[#5879]: https://github.com/nymtech/nym/pull/5879
[#5878]: https://github.com/nymtech/nym/pull/5878
[#5877]: https://github.com/nymtech/nym/pull/5877
[#5874]: https://github.com/nymtech/nym/pull/5874
[#5873]: https://github.com/nymtech/nym/pull/5873
[#5872]: https://github.com/nymtech/nym/pull/5872
[#5871]: https://github.com/nymtech/nym/pull/5871
[#5867]: https://github.com/nymtech/nym/pull/5867
[#5866]: https://github.com/nymtech/nym/pull/5866
[#5861]: https://github.com/nymtech/nym/pull/5861
[#5860]: https://github.com/nymtech/nym/pull/5860
[#5857]: https://github.com/nymtech/nym/pull/5857
[#5856]: https://github.com/nymtech/nym/pull/5856
[#5855]: https://github.com/nymtech/nym/pull/5855
[#5853]: https://github.com/nymtech/nym/pull/5853
[#5833]: https://github.com/nymtech/nym/pull/5833
[#5828]: https://github.com/nymtech/nym/pull/5828
[#5814]: https://github.com/nymtech/nym/pull/5814
## [2025.12-dolcelatte] (2025-07-07)
- bugfix: key-rotation + reply SURBs ([#5876])
- Bugfix/backwards compat ([#5865])
- bugfix: allow gateways to permit authentication from v4 clients ([#5862])
- fixed client route for obtaining v2 list of gateways ([#5859])
- Updated browser extension piece removal ([#5849])
- Remove/old env references ([#5848])
- Remove qa env ([#5847])
- remove not used old mock-api ([#5845])
- remove bity dir ([#5844])
- build(deps-dev): bump webpack-dev-server from 4.13.2 to 5.2.1 in /wasm/mix-fetch/internal-dev ([#5843])
- Amended the buy section ([#5841])
- Removing test-net faucet ([#5840])
- Feature/node status dvpn directory ([#5829])
- build(deps-dev): bump webpack-dev-server from 4.15.2 to 5.2.1 in /nym-credential-proxy/vpn-api-lib-wasm/internal-dev ([#5826])
- bugfix: fix swapped total and circulating supplies ([#5822])
- build(deps): bump tar-fs from 3.0.8 to 3.0.9 in /sdk/typescript/tests/integration-tests/mix-fetch ([#5821])
- Url scheme warning log ([#5819])
- chore: adjust heuristic for wireguard peer activity ([#5818])
- Use the same client bandwidth for top up ([#5813])
- Replace chrono with time in NS API ([#5811])
- build(deps-dev): bump http-proxy-middleware from 2.0.4 to 2.0.9 in /clients/native/examples/js-examples/websocket ([#5810])
- build(deps): bump tokio from 1.44.2 to 1.45.1 ([#5798])
- Close sqlite pool before moving or reopening databases ([#5796])
- HTTP Client Retries, Fallbacks, and Redirects ([#5789])
- feat: key rotation ([#5777])
- build(deps): bump next from 14.2.15 to 14.2.26 in /documentation/docs ([#5772])
- build(deps): bump undici from 5.28.5 to 5.29.0 in /.github/actions/nym-hash-releases/src ([#5771])
- build(deps): bump cargo_metadata from 0.18.1 to 0.19.2 ([#5765])
- build(deps): bump tempfile from 3.19.1 to 3.20.0 ([#5764])
- [Feature] Noise XKpsk3 integration (2025 version) ([#5692])
- feature: nympool contract ([#5464])
- chore: fixed typo in API endpoint parameter ([#5449])
[#5876]: https://github.com/nymtech/nym/pull/5876
[#5865]: https://github.com/nymtech/nym/pull/5865
[#5862]: https://github.com/nymtech/nym/pull/5862
[#5859]: https://github.com/nymtech/nym/pull/5859
[#5849]: https://github.com/nymtech/nym/pull/5849
[#5848]: https://github.com/nymtech/nym/pull/5848
[#5847]: https://github.com/nymtech/nym/pull/5847
[#5845]: https://github.com/nymtech/nym/pull/5845
[#5844]: https://github.com/nymtech/nym/pull/5844
[#5843]: https://github.com/nymtech/nym/pull/5843
[#5841]: https://github.com/nymtech/nym/pull/5841
[#5840]: https://github.com/nymtech/nym/pull/5840
[#5829]: https://github.com/nymtech/nym/pull/5829
[#5826]: https://github.com/nymtech/nym/pull/5826
[#5822]: https://github.com/nymtech/nym/pull/5822
[#5821]: https://github.com/nymtech/nym/pull/5821
[#5819]: https://github.com/nymtech/nym/pull/5819
[#5818]: https://github.com/nymtech/nym/pull/5818
[#5813]: https://github.com/nymtech/nym/pull/5813
[#5811]: https://github.com/nymtech/nym/pull/5811
[#5810]: https://github.com/nymtech/nym/pull/5810
[#5798]: https://github.com/nymtech/nym/pull/5798
[#5796]: https://github.com/nymtech/nym/pull/5796
[#5789]: https://github.com/nymtech/nym/pull/5789
[#5777]: https://github.com/nymtech/nym/pull/5777
[#5772]: https://github.com/nymtech/nym/pull/5772
[#5771]: https://github.com/nymtech/nym/pull/5771
[#5765]: https://github.com/nymtech/nym/pull/5765
[#5764]: https://github.com/nymtech/nym/pull/5764
[#5692]: https://github.com/nymtech/nym/pull/5692
[#5464]: https://github.com/nymtech/nym/pull/5464
[#5449]: https://github.com/nymtech/nym/pull/5449
## [2025.11-cheddar] (2025-06-10)
- No autoremoval of peers ([#5831])
- Set cached storage counters to 0 ([#5812])
- hack: temporarily use next.config.js instead of next.config.ts ([#5805])
- chore: resolve 1.87 clippy warnings ([#5802])
- Nym Statistics API ([#5800])
- QoL: RequestPath trait for http-api-client ([#5788])
- Fix contains ticketbook function that always returned true ([#5787])
- swap a decode into a fromrow to please future postgres feature ([#5785])
- Make address cache configurable ([#5784])
- Track wireguard credential retries ([#5783])
[#5831]: https://github.com/nymtech/nym/pull/5831
[#5812]: https://github.com/nymtech/nym/pull/5812
[#5805]: https://github.com/nymtech/nym/pull/5805
[#5802]: https://github.com/nymtech/nym/pull/5802
[#5800]: https://github.com/nymtech/nym/pull/5800
[#5788]: https://github.com/nymtech/nym/pull/5788
[#5787]: https://github.com/nymtech/nym/pull/5787
[#5785]: https://github.com/nymtech/nym/pull/5785
[#5784]: https://github.com/nymtech/nym/pull/5784
[#5783]: https://github.com/nymtech/nym/pull/5783
## [2025.10-brie] (2025-05-27)
- Backport PR 5779 ([#5801])
- Expanded Accept Encoding for `reqwest` ([#5779])
- Teach HttpClientError how to report its status code and timeout ([#5770])
- Skip refreshing the topology on startup as we already have an initial set ([#5768])
- Fetch the topology from the nym-api concurrently ([#5767])
- feat: use bincode by default in NymApiClient + remove feature-lock ([#5761])
- Instrument create_request ([#5760])
- Add node_bonded field to delegations ([#5759])
- build(deps): bump mikefarah/yq from 4.45.1 to 4.45.4 ([#5758])
- Raw route submissions ([#5756])
- feat: expires header for `/active` nym-api responses ([#5755])
- Decrease default average packet delay to 15 ms ([#5754])
- build(deps): bump the patch-updates group across 1 directory with 12 updates ([#5753])
- Remove pretty_env_logger and switch remaining crates to use tracing ([#5749])
- Update pretty_env_logger to latest to not depend on unmaintained crate atty ([#5748])
- Upgrade prometheus crate to fix security warning ([#5747])
- Downgrade deranged crate to 0.4.0 ([#5746])
- feat: nym-api bincode + yaml support ([#5745])
- fix parallel feature in ecash crate with send + sync ([#5744])
- Remove old test directory - Update validator docker ([#5743])
- [Feature] `RememberMe` is the new don't `ForgetMe` ([#5742])
- build(deps): bump ammonia from 4.0.0 to 4.1.0 ([#5739])
- build(deps): bump base-x from 3.0.9 to 3.0.11 in /testnet-faucet ([#5737])
- build(deps): bump http-proxy-middleware from 2.0.8 to 2.0.9 ([#5730])
[#5801]: https://github.com/nymtech/nym/pull/5801
[#5779]: https://github.com/nymtech/nym/pull/5779
[#5770]: https://github.com/nymtech/nym/pull/5770
[#5768]: https://github.com/nymtech/nym/pull/5768
[#5767]: https://github.com/nymtech/nym/pull/5767
[#5761]: https://github.com/nymtech/nym/pull/5761
[#5760]: https://github.com/nymtech/nym/pull/5760
[#5759]: https://github.com/nymtech/nym/pull/5759
[#5758]: https://github.com/nymtech/nym/pull/5758
[#5756]: https://github.com/nymtech/nym/pull/5756
[#5755]: https://github.com/nymtech/nym/pull/5755
[#5754]: https://github.com/nymtech/nym/pull/5754
[#5753]: https://github.com/nymtech/nym/pull/5753
[#5749]: https://github.com/nymtech/nym/pull/5749
[#5748]: https://github.com/nymtech/nym/pull/5748
[#5747]: https://github.com/nymtech/nym/pull/5747
[#5746]: https://github.com/nymtech/nym/pull/5746
[#5745]: https://github.com/nymtech/nym/pull/5745
[#5744]: https://github.com/nymtech/nym/pull/5744
[#5743]: https://github.com/nymtech/nym/pull/5743
[#5742]: https://github.com/nymtech/nym/pull/5742
[#5739]: https://github.com/nymtech/nym/pull/5739
[#5737]: https://github.com/nymtech/nym/pull/5737
[#5730]: https://github.com/nymtech/nym/pull/5730
## [2025.9-appenzeller] (2025-05-13) ## [2025.9-appenzeller] (2025-05-13)
- build(deps): bump clap from 4.5.36 to 4.5.37 in the patch-updates group ([#5722]) - build(deps): bump clap from 4.5.36 to 4.5.37 in the patch-updates group ([#5722])
-686
View File
@@ -1,686 +0,0 @@
# 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
+466 -697
View File
File diff suppressed because it is too large Load Diff
+15 -30
View File
@@ -33,15 +33,11 @@ members = [
"common/commands", "common/commands",
"common/config", "common/config",
"common/cosmwasm-smart-contracts/coconut-dkg", "common/cosmwasm-smart-contracts/coconut-dkg",
"common/cosmwasm-smart-contracts/contracts-common", "common/cosmwasm-smart-contracts/contracts-common", "common/cosmwasm-smart-contracts/easy_addr",
"common/cosmwasm-smart-contracts/contracts-common-testing",
"common/cosmwasm-smart-contracts/easy_addr",
"common/cosmwasm-smart-contracts/ecash-contract", "common/cosmwasm-smart-contracts/ecash-contract",
"common/cosmwasm-smart-contracts/group-contract", "common/cosmwasm-smart-contracts/group-contract",
"common/cosmwasm-smart-contracts/mixnet-contract", "common/cosmwasm-smart-contracts/mixnet-contract",
"common/cosmwasm-smart-contracts/multisig-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/cosmwasm-smart-contracts/vesting-contract",
"common/credential-storage", "common/credential-storage",
"common/credential-utils", "common/credential-utils",
@@ -50,8 +46,6 @@ members = [
"common/credentials-interface", "common/credentials-interface",
"common/crypto", "common/crypto",
"common/dkg", "common/dkg",
"common/ecash-signer-check",
"common/ecash-signer-check-types",
"common/ecash-time", "common/ecash-time",
"common/execute", "common/execute",
"common/exit-policy", "common/exit-policy",
@@ -70,8 +64,6 @@ members = [
"common/nym-id", "common/nym-id",
"common/nym-metrics", "common/nym-metrics",
"common/nym_offline_compact_ecash", "common/nym_offline_compact_ecash",
"common/nymnoise",
"common/nymnoise/keys",
"common/nymsphinx", "common/nymsphinx",
"common/nymsphinx/acknowledgements", "common/nymsphinx/acknowledgements",
"common/nymsphinx/addressing", "common/nymsphinx/addressing",
@@ -92,7 +84,7 @@ members = [
"common/socks5/requests", "common/socks5/requests",
"common/statistics", "common/statistics",
"common/store-cipher", "common/store-cipher",
"common/task", "common/test-utils", "common/task",
"common/ticketbooks-merkle", "common/ticketbooks-merkle",
"common/topology", "common/topology",
"common/tun", "common/tun",
@@ -103,9 +95,9 @@ members = [
"common/wasm/utils", "common/wasm/utils",
"common/wireguard", "common/wireguard",
"common/wireguard-types", "common/wireguard-types",
"common/zulip-client",
"documentation/autodoc", "documentation/autodoc",
"gateway", "gateway",
"integrations/bity",
"nym-api", "nym-api",
"nym-api/nym-api-requests", "nym-api/nym-api-requests",
"nym-browser-extension/storage", "nym-browser-extension/storage",
@@ -120,7 +112,6 @@ members = [
"nym-node/nym-node-metrics", "nym-node/nym-node-metrics",
"nym-node/nym-node-requests", "nym-node/nym-node-requests",
"nym-outfox", "nym-outfox",
"nym-statistics-api",
"nym-validator-rewarder", "nym-validator-rewarder",
"nyx-chain-watcher", "nyx-chain-watcher",
"sdk/ffi/cpp", "sdk/ffi/cpp",
@@ -131,7 +122,6 @@ members = [
"service-providers/common", "service-providers/common",
"service-providers/ip-packet-router", "service-providers/ip-packet-router",
"service-providers/network-requester", "service-providers/network-requester",
"sqlx-pool-guard",
"tools/echo-server", "tools/echo-server",
"tools/internal/contract-state-importer/importer-cli", "tools/internal/contract-state-importer/importer-cli",
"tools/internal/contract-state-importer/importer-contract", "tools/internal/contract-state-importer/importer-contract",
@@ -141,7 +131,7 @@ members = [
"tools/internal/testnet-manager", "tools/internal/testnet-manager",
"tools/internal/testnet-manager", "tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract", "tools/internal/testnet-manager/dkg-bypass-contract",
"tools/internal/validator-status-check", "tools/internal/testnet-manager/dkg-bypass-contract", "tools/internal/validator-status-check",
"tools/nym-cli", "tools/nym-cli",
"tools/nym-id-cli", "tools/nym-id-cli",
"tools/nym-nr-query", "tools/nym-nr-query",
@@ -162,7 +152,6 @@ default-members = [
"nym-node", "nym-node",
"nym-node-status-api/nym-node-status-agent", "nym-node-status-api/nym-node-status-agent",
"nym-node-status-api/nym-node-status-api", "nym-node-status-api/nym-node-status-api",
"nym-statistics-api",
"nym-validator-rewarder", "nym-validator-rewarder",
"nyx-chain-watcher", "nyx-chain-watcher",
"service-providers/authenticator", "service-providers/authenticator",
@@ -209,7 +198,7 @@ bloomfilter = "3.0.1"
bs58 = "0.5.1" bs58 = "0.5.1"
bytecodec = "0.4.15" bytecodec = "0.4.15"
bytes = "1.10.1" bytes = "1.10.1"
cargo_metadata = "0.19.2" cargo_metadata = "0.18.1"
celes = "2.6.0" celes = "2.6.0"
cfg-if = "1.0.0" cfg-if = "1.0.0"
chacha20 = "0.9.0" chacha20 = "0.9.0"
@@ -222,7 +211,7 @@ clap_complete_fig = "4.5"
colored = "2.2" colored = "2.2"
comfy-table = "7.1.4" comfy-table = "7.1.4"
console = "0.15.11" console = "0.15.11"
console-subscriber = "0.4.1" console-subscriber = "0.1.1"
console_error_panic_hook = "0.1" console_error_panic_hook = "0.1"
const-str = "0.5.6" const-str = "0.5.6"
const_format = "0.2.34" const_format = "0.2.34"
@@ -238,7 +227,6 @@ digest = "0.10.7"
dirs = "5.0" dirs = "5.0"
doc-comment = "0.3" doc-comment = "0.3"
dotenvy = "0.15.6" dotenvy = "0.15.6"
dyn-clone = "1.0.19"
ecdsa = "0.16" ecdsa = "0.16"
ed25519-dalek = "2.1" ed25519-dalek = "2.1"
encoding_rs = "0.8.35" encoding_rs = "0.8.35"
@@ -293,7 +281,6 @@ petgraph = "0.6.5"
pin-project = "1.1" pin-project = "1.1"
pin-project-lite = "0.2.16" pin-project-lite = "0.2.16"
publicsuffix = "2.3.0" publicsuffix = "2.3.0"
proc_pidinfo = "0.1.3"
quote = "1" quote = "1"
rand = "0.8.5" rand = "0.8.5"
rand_chacha = "0.3" rand_chacha = "0.3"
@@ -318,20 +305,19 @@ serde_with = "3.9.0"
serde_yaml = "0.9.25" serde_yaml = "0.9.25"
sha2 = "0.10.9" sha2 = "0.10.9"
si-scale = "0.2.3" si-scale = "0.2.3"
snow = "0.9.6"
sphinx-packet = "=0.6.0" sphinx-packet = "=0.6.0"
sqlx = "0.8.6" sqlx = "0.7.4"
strum = "0.27.2" strum = "0.26"
strum_macros = "0.27.2" strum_macros = "0.26"
subtle-encoding = "0.5" subtle-encoding = "0.5"
syn = "1" syn = "1"
sysinfo = "0.33.0" sysinfo = "0.33.0"
tap = "1.0.1" tap = "1.0.1"
tar = "0.4.44" tar = "0.4.44"
tempfile = "3.20" tempfile = "3.19"
thiserror = "2.0" thiserror = "2.0"
time = "0.3.41" time = "0.3.41"
tokio = "1.45" tokio = "1.44"
tokio-postgres = "0.7" tokio-postgres = "0.7"
tokio-stream = "0.1.17" tokio-stream = "0.1.17"
tokio-test = "0.4.4" tokio-test = "0.4.4"
@@ -358,6 +344,7 @@ utoipauto = "0.2"
uuid = "*" uuid = "*"
vergen = { version = "=8.3.1", default-features = false } vergen = { version = "=8.3.1", default-features = false }
walkdir = "2" walkdir = "2"
wasm-bindgen-test = "0.3.49"
x25519-dalek = "2.0.0" x25519-dalek = "2.0.0"
zeroize = "1.7.0" zeroize = "1.7.0"
@@ -375,6 +362,9 @@ subtle = "2.5.0"
# cosmwasm-related # cosmwasm-related
cosmwasm-schema = "=2.2.2" cosmwasm-schema = "=2.2.2"
cosmwasm-std = "=2.2.2" cosmwasm-std = "=2.2.2"
# use 1.0.1 as that's the version used by cosmwasm-std 2.2.1
# (and ideally we don't want to pull the same dependency twice)
serde-json-wasm = "=1.0.1"
# same version as used by cosmwasm # same version as used by cosmwasm
cw-utils = "=2.0.0" cw-utils = "=2.0.0"
cw-storage-plus = "=2.0.0" cw-storage-plus = "=2.0.0"
@@ -382,7 +372,6 @@ cw2 = { version = "=2.0.0" }
cw3 = { version = "=2.0.0" } cw3 = { version = "=2.0.0" }
cw4 = { version = "=2.0.0" } cw4 = { version = "=2.0.0" }
cw-controllers = { version = "=2.0.0" } cw-controllers = { version = "=2.0.0" }
cw-multi-test = "=2.3.2"
# cosmrs-related # cosmrs-related
bip32 = { version = "0.5.3", default-features = false } bip32 = { version = "0.5.3", default-features = false }
@@ -403,7 +392,6 @@ serde-wasm-bindgen = "0.6.5"
tsify = "0.4.5" tsify = "0.4.5"
wasm-bindgen = "0.2.99" wasm-bindgen = "0.2.99"
wasm-bindgen-futures = "0.4.49" wasm-bindgen-futures = "0.4.49"
wasm-bindgen-test = "0.3.49"
wasmtimer = "0.4.1" wasmtimer = "0.4.1"
web-sys = "0.3.76" web-sys = "0.3.76"
@@ -439,9 +427,6 @@ opt-level = 'z'
# lto = true # lto = true
opt-level = 'z' opt-level = 'z'
[workspace.lints.rust]
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(tokio_unstable)'] }
[workspace.lints.clippy] [workspace.lints.clippy]
unwrap_used = "deny" unwrap_used = "deny"
expect_used = "deny" expect_used = "deny"
+10 -58
View File
@@ -12,11 +12,7 @@ help:
@echo " clippy: run clippy for all workspaces" @echo " clippy: run clippy for all workspaces"
@echo " test: run clippy, unit tests, and formatting." @echo " test: run clippy, unit tests, and formatting."
@echo " test-all: like test, but also includes the expensive tests" @echo " test-all: like test, but also includes the expensive tests"
@echo " deb: build debian packages" @echo " deb: build debian packages
@echo ""
@echo "Contract building targets:"
@echo " contracts: build contracts for development (includes wasm-opt)"
@echo " publish-contracts: build contracts using Docker optimizer (deterministic)"
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Meta targets # Meta targets
@@ -134,69 +130,25 @@ cargo-test: sdk-wasm-test
clippy: sdk-wasm-lint clippy: sdk-wasm-lint
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
# Build CosmWasm contracts (deterministic docker build) # Build contracts ready for deploy
# ----------------------------------------------------------------------------- # -----------------------------------------------------------------------------
CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg
CONTRACTS_WASM=$(addsuffix .wasm, $(CONTRACTS))
CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release
WASM_CONTRACT_DIR := contracts/target/wasm32-unknown-unknown/release contracts: build-release-contracts wasm-opt-contracts cosmwasm-check-contracts
# Find every direct contract folder that contains a Cargo.toml
CONTRACT_DIRS := $(shell find contracts -type f -name Cargo.toml \( ! -path "contracts/Cargo.toml" \) | grep -v integration-tests | xargs -n1 dirname | sort -u)
CONTRACTS_OUT_DIR = contracts/artifacts
# Build all contracts via the official CosmWasm optimizer image (one invocation per contract)
# See : https://github.com/CosmWasm/optimizer?tab=readme-ov-file#contracts-excluded-from-workspace
# The optimizer ships separate multi-arch images. ARM builds are *not* bit-for-bit identical to the
# canonical x86_64 build (see README notice in CosmWasm/optimizer). For reproducible artefacts we
# therefore always run the amd64 variant by default.
# Override with :
# $ COSMWASM_OPTIMIZER_IMAGE=cosmwasm/optimizer-arm64:0.17.0 make contracts-publish
#
COSMWASM_OPTIMIZER_IMAGE ?= cosmwasm/optimizer:0.17.0
COSMWASM_OPTIMIZER_PLATFORM ?= linux/amd64
# Ensure clean build environment and run the optimizer
optimize-contracts:
@rm -rf artifacts 2>/dev/null || true
@echo "=== Ensuring clean build environment"
docker volume rm nym_contracts_cache 2>/dev/null || true
docker volume rm registry_cache 2>/dev/null || true
@for DIR in $(CONTRACT_DIRS); do \
echo "=== Optimizing $${DIR}"; \
docker run --rm --platform $(COSMWASM_OPTIMIZER_PLATFORM) \
-v $(CURDIR):/code \
--mount type=volume,source=nym_contracts_cache,target=/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
-e CARGO_BUILD_INCREMENTAL=false \
-e RUSTFLAGS="-C target-cpu=generic -C debuginfo=0" \
-e SOURCE_DATE_EPOCH=1 \
$(COSMWASM_OPTIMIZER_IMAGE) $${DIR}; \
done
@mkdir -p $(CONTRACTS_OUT_DIR)
@cp artifacts/*.wasm $(CONTRACTS_OUT_DIR)/ 2>/dev/null || true
@cd $(CONTRACTS_OUT_DIR) && sha256sum *.wasm > checksums.txt
# Cleanup temporary artefacts directory
@rm -rf artifacts 2>/dev/null || true
wasm-opt-contracts: wasm-opt-contracts:
@for WASM in $(WASM_CONTRACT_DIR)/*.wasm; do \ for contract in $(CONTRACTS_WASM); do \
echo "Running wasm-opt on $$WASM"; \ wasm-opt --signext-lowering -Os $(CONTRACTS_OUT_DIR)/$$contract -o $(CONTRACTS_OUT_DIR)/$$contract; \
wasm-opt --signext-lowering -Os $$WASM -o $$WASM ; \
done done
cosmwasm-check-contracts: cosmwasm-check-contracts:
@for WASM in $(WASM_CONTRACT_DIR)/*.wasm; do \ for contract in $(CONTRACTS_WASM); do \
echo "Checking $$WASM"; \ cosmwasm-check $(CONTRACTS_OUT_DIR)/$$contract; \
cosmwasm-check $$WASM ; \
done done
# Default development build
contracts: build-release-contracts wasm-opt-contracts cosmwasm-check-contracts
# Publishing build used by CI deterministic Docker optimiser
publish-contracts: optimize-contracts cosmwasm-check-contracts
# Consider adding 's' to make plural consistent (beware: used in github workflow) # Consider adding 's' to make plural consistent (beware: used in github workflow)
contract-schema: contract-schema:
$(MAKE) -C contracts schema $(MAKE) -C contracts schema
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "nym-client" name = "nym-client"
version = "1.1.61" version = "1.1.55"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"] authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client" description = "Implementation of the Nym Client"
edition = "2021" edition = "2021"
@@ -2048,11 +2048,10 @@
} }
}, },
"node_modules/http-proxy-middleware": { "node_modules/http-proxy-middleware": {
"version": "2.0.9", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz",
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "integrity": "sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@types/http-proxy": "^1.17.8", "@types/http-proxy": "^1.17.8",
"http-proxy": "^1.18.1", "http-proxy": "^1.18.1",
@@ -6096,9 +6095,9 @@
} }
}, },
"http-proxy-middleware": { "http-proxy-middleware": {
"version": "2.0.9", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz",
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", "integrity": "sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/http-proxy": "^1.17.8", "@types/http-proxy": "^1.17.8",
+1 -1
View File
@@ -111,7 +111,7 @@ impl SocketClient {
let dkg_query_client = if self.config.base.client.disabled_credentials_mode { let dkg_query_client = if self.config.base.client.disabled_credentials_mode {
None None
} else { } 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?; let storage = self.initialise_storage().await?;
+1 -1
View File
@@ -318,7 +318,7 @@ impl Handler {
async fn handle_text_message(&mut self, msg: String) -> Option<WsMessage> { async fn handle_text_message(&mut self, msg: String) -> Option<WsMessage> {
debug!("Handling text message request"); debug!("Handling text message request");
trace!("Content: {msg:?}"); trace!("Content: {:?}", msg);
self.received_response_type = ReceivedResponseType::Text; self.received_response_type = ReceivedResponseType::Text;
let client_request = ClientRequest::try_from_text(msg); let client_request = ClientRequest::try_from_text(msg);
+2 -2
View File
@@ -68,9 +68,9 @@ impl Listener {
new_conn = tcp_listener.accept() => { new_conn = tcp_listener.accept() => {
match new_conn { match new_conn {
Ok((mut socket, remote_addr)) => { Ok((mut socket, remote_addr)) => {
debug!("Received connection from {remote_addr:?}"); debug!("Received connection from {:?}", remote_addr);
if self.state.is_connected() { if self.state.is_connected() {
warn!("Tried to open a duplicate websocket connection. The request came from {remote_addr}"); warn!("Tried to open a duplicate websocket connection. The request came from {}", remote_addr);
// if we've already got a connection, don't allow another one // if we've already got a connection, don't allow another one
// while we only ever want to accept a single connection, we don't want // while we only ever want to accept a single connection, we don't want
// to leave clients hanging (and also allow for reconnection if it somehow // to leave clients hanging (and also allow for reconnection if it somehow
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "nym-socks5-client" name = "nym-socks5-client"
version = "1.1.61" version = "1.1.55"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"] 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" description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021" edition = "2021"
+1 -1
View File
@@ -137,7 +137,7 @@ impl AsyncFileWatcher {
log::error!("the file watcher receiver has been dropped!"); log::error!("the file watcher receiver has been dropped!");
} }
} else { } else {
log::debug!("will not propagate information about {event:?}"); log::debug!("will not propagate information about {:?}", event);
} }
} }
Err(err) => { Err(err) => {
@@ -108,7 +108,7 @@ impl GatewayClient {
#[cfg(feature = "verify")] #[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key // use gateways key as a ref to an x25519_dalek key
let dh = gateway_key.inner().diffie_hellman(&self.pub_key); let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead // TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
@@ -117,7 +117,7 @@ impl GatewayClient {
#[cfg(feature = "verify")] #[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key // use gateways key as a ref to an x25519_dalek key
let dh = gateway_key.inner().diffie_hellman(&self.pub_key); let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead // TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
@@ -117,7 +117,7 @@ impl GatewayClient {
#[cfg(feature = "verify")] #[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key // use gateways key as a ref to an x25519_dalek key
let dh = gateway_key.inner().diffie_hellman(&self.pub_key); let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead // TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
@@ -169,7 +169,7 @@ impl GatewayClient {
#[cfg(feature = "verify")] #[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key // use gateways key as a ref to an x25519_dalek key
let dh = gateway_key.inner().diffie_hellman(&self.pub_key); let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead // TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
@@ -28,6 +28,8 @@ pub type HmacSha256 = Hmac<Sha256>;
pub type Nonce = u64; pub type Nonce = u64;
pub type Taken = Option<SystemTime>; 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)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct IpPair { pub struct IpPair {
pub ipv4: Ipv4Addr, pub ipv4: Ipv4Addr,
@@ -167,7 +169,7 @@ impl GatewayClient {
#[cfg(feature = "verify")] #[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key // use gateways key as a ref to an x25519_dalek key
let dh = gateway_key.inner().diffie_hellman(&self.pub_key); let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead // TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)] #[allow(clippy::expect_used)]
+1 -1
View File
@@ -11,7 +11,7 @@ impl std::fmt::Display for BandwidthStatusMessage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
BandwidthStatusMessage::RemainingBandwidth(b) => { BandwidthStatusMessage::RemainingBandwidth(b) => {
write!(f, "remaining bandwidth: {b}") write!(f, "remaining bandwidth: {}", b)
} }
BandwidthStatusMessage::NoBandwidth => write!(f, "no bandwidth left"), BandwidthStatusMessage::NoBandwidth => write!(f, "no bandwidth left"),
} }
+2 -2
View File
@@ -207,7 +207,7 @@ where
<St as Storage>::StorageError: Send + Sync + 'static, <St as Storage>::StorageError: Send + Sync + 'static,
{ {
if let Some(stored) = storage if let Some(stored) = storage
.get_expiration_date_signatures(expiration_date, epoch_id) .get_expiration_date_signatures(expiration_date)
.await .await
.map_err(BandwidthControllerError::credential_storage_error)? .map_err(BandwidthControllerError::credential_storage_error)?
{ {
@@ -220,7 +220,7 @@ where
ecash_apis, ecash_apis,
|api| async move { |api| async move {
api.api_client api.api_client
.global_expiration_date_signatures(Some(expiration_date), Some(epoch_id)) .global_expiration_date_signatures(Some(expiration_date))
.await .await
}, },
format!("aggregated coin index signatures for date {expiration_date}"), format!("aggregated coin index signatures for date {expiration_date}"),
+6 -9
View File
@@ -13,10 +13,10 @@ async-trait = { workspace = true }
base64 = { workspace = true } base64 = { workspace = true }
bs58 = { workspace = true } bs58 = { workspace = true }
clap = { workspace = true, optional = true } clap = { workspace = true, optional = true }
cfg-if = { workspace = true }
comfy-table = { workspace = true, optional = true } comfy-table = { workspace = true, optional = true }
futures = { workspace = true } futures = { workspace = true }
humantime = { workspace = true } humantime-serde = { workspace = true }
log = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
rand_chacha = { workspace = true } rand_chacha = { workspace = true }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
@@ -25,23 +25,26 @@ sha2 = { workspace = true }
si-scale = { workspace = true } si-scale = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
url = { workspace = true, features = ["serde"] } url = { workspace = true, features = ["serde"] }
tokio = { workspace = true, features = ["macros"] }
time = { workspace = true } time = { workspace = true }
tokio = { workspace = true, features = ["sync", "macros"] }
tracing = { workspace = true } tracing = { workspace = true }
zeroize = { workspace = true } zeroize = { workspace = true }
# internal # internal
nym-id = { path = "../nym-id" } nym-id = { path = "../nym-id" }
nym-bandwidth-controller = { path = "../bandwidth-controller" } nym-bandwidth-controller = { path = "../bandwidth-controller" }
nym-config = { path = "../config" }
nym-crypto = { path = "../crypto" } nym-crypto = { path = "../crypto" }
nym-gateway-client = { path = "../client-libs/gateway-client" } nym-gateway-client = { path = "../client-libs/gateway-client" }
nym-gateway-requests = { path = "../gateway-requests" } nym-gateway-requests = { path = "../gateway-requests" }
nym-http-api-client = { path = "../http-api-client" } nym-http-api-client = { path = "../http-api-client" }
nym-metrics = { path = "../nym-metrics" }
nym-nonexhaustive-delayqueue = { path = "../nonexhaustive-delayqueue" } nym-nonexhaustive-delayqueue = { path = "../nonexhaustive-delayqueue" }
nym-sphinx = { path = "../nymsphinx" } nym-sphinx = { path = "../nymsphinx" }
nym-statistics-common = { path = "../statistics" } nym-statistics-common = { path = "../statistics" }
nym-pemstore = { path = "../pemstore" } nym-pemstore = { path = "../pemstore" }
nym-topology = { path = "../topology", features = ["persistence"] } nym-topology = { path = "../topology", features = ["persistence"] }
nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false }
nym-validator-client = { path = "../client-libs/validator-client", default-features = false } nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
nym-task = { path = "../task" } nym-task = { path = "../task" }
nym-credentials-interface = { path = "../credentials-interface" } nym-credentials-interface = { path = "../credentials-interface" }
@@ -54,9 +57,6 @@ nym-client-core-surb-storage = { path = "./surb-storage" }
nym-client-core-gateways-storage = { path = "./gateways-storage" } nym-client-core-gateways-storage = { path = "./gateways-storage" }
nym-ecash-time = { path = "../ecash-time" } nym-ecash-time = { path = "../ecash-time" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false }
### For serving prometheus metrics ### For serving prometheus metrics
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.hyper] [target."cfg(not(target_arch = \"wasm32\"))".dependencies.hyper]
workspace = true workspace = true
@@ -124,6 +124,3 @@ fs-surb-storage = ["nym-client-core-surb-storage/fs-surb-storage"]
fs-gateways-storage = ["nym-client-core-gateways-storage/fs-gateways-storage"] fs-gateways-storage = ["nym-client-core-gateways-storage/fs-gateways-storage"]
wasm = ["nym-gateway-client/wasm"] wasm = ["nym-gateway-client/wasm"]
metrics-server = [] metrics-server = []
[lints]
workspace = true
+12 -8
View File
@@ -57,7 +57,9 @@ const DEFAULT_MAXIMUM_ALLOWED_SURB_REQUEST_SIZE: u32 = 500;
const DEFAULT_MAXIMUM_REPLY_SURB_REREQUEST_WAITING_PERIOD: Duration = Duration::from_secs(10); const DEFAULT_MAXIMUM_REPLY_SURB_REREQUEST_WAITING_PERIOD: Duration = Duration::from_secs(10);
const DEFAULT_MAXIMUM_REPLY_SURB_DROP_WAITING_PERIOD: Duration = Duration::from_secs(5 * 60); const DEFAULT_MAXIMUM_REPLY_SURB_DROP_WAITING_PERIOD: Duration = Duration::from_secs(5 * 60);
const DEFAULT_MAXIMUM_REPLY_SURB_REREQUESTS: usize = 5;
// 12 hours
const DEFAULT_MAXIMUM_REPLY_SURB_AGE: Duration = Duration::from_secs(12 * 60 * 60);
// 24 hours // 24 hours
const DEFAULT_MAXIMUM_REPLY_KEY_AGE: Duration = Duration::from_secs(24 * 60 * 60); const DEFAULT_MAXIMUM_REPLY_KEY_AGE: Duration = Duration::from_secs(24 * 60 * 60);
@@ -416,9 +418,6 @@ pub struct Traffic {
/// will be routed as usual, to the entry gateway, through three mix nodes, egressing /// will be routed as usual, to the entry gateway, through three mix nodes, egressing
/// through the exit gateway. If mix hops are disabled, traffic will be routed directly /// through the exit gateway. If mix hops are disabled, traffic will be routed directly
/// from the entry gateway to the exit gateway, bypassing the mix nodes. /// from the entry gateway to the exit gateway, bypassing the mix nodes.
///
/// This overrides the `use_legacy_sphinx_format` setting as reduced mix hops
/// requires use of the updated SURB packet format.
pub disable_mix_hops: bool, pub disable_mix_hops: bool,
} }
@@ -626,9 +625,10 @@ pub struct ReplySurbs {
#[serde(with = "humantime_serde")] #[serde(with = "humantime_serde")]
pub maximum_reply_surb_drop_waiting_period: Duration, pub maximum_reply_surb_drop_waiting_period: Duration,
/// Defines maximum number of times the client is going to re-request reply surbs /// Defines maximum amount of time given reply surb is going to be valid for.
/// for clearing pending messages before giving up after making no progress. /// This is going to be superseded by key rotation once implemented.
pub maximum_reply_surbs_rerequests: usize, #[serde(with = "humantime_serde")]
pub maximum_reply_surb_age: Duration,
/// Defines maximum amount of time given reply key is going to be valid for. /// Defines maximum amount of time given reply key is going to be valid for.
/// This is going to be superseded by key rotation once implemented. /// This is going to be superseded by key rotation once implemented.
@@ -638,6 +638,9 @@ pub struct ReplySurbs {
/// Specifies the number of mixnet hops the packet should go through. If not specified, then /// Specifies the number of mixnet hops the packet should go through. If not specified, then
/// the default value is used. /// the default value is used.
pub surb_mix_hops: Option<u8>, pub surb_mix_hops: Option<u8>,
/// Specifies if we should reset all the sender tags on startup
pub fresh_sender_tags: bool,
} }
impl Default for ReplySurbs { impl Default for ReplySurbs {
@@ -652,9 +655,10 @@ impl Default for ReplySurbs {
maximum_reply_surb_rerequest_waiting_period: maximum_reply_surb_rerequest_waiting_period:
DEFAULT_MAXIMUM_REPLY_SURB_REREQUEST_WAITING_PERIOD, DEFAULT_MAXIMUM_REPLY_SURB_REREQUEST_WAITING_PERIOD,
maximum_reply_surb_drop_waiting_period: DEFAULT_MAXIMUM_REPLY_SURB_DROP_WAITING_PERIOD, maximum_reply_surb_drop_waiting_period: DEFAULT_MAXIMUM_REPLY_SURB_DROP_WAITING_PERIOD,
maximum_reply_surbs_rerequests: DEFAULT_MAXIMUM_REPLY_SURB_REREQUESTS, maximum_reply_surb_age: DEFAULT_MAXIMUM_REPLY_SURB_AGE,
maximum_reply_key_age: DEFAULT_MAXIMUM_REPLY_KEY_AGE, maximum_reply_key_age: DEFAULT_MAXIMUM_REPLY_KEY_AGE,
surb_mix_hops: None, surb_mix_hops: None,
fresh_sender_tags: false,
} }
} }
} }
@@ -189,13 +189,14 @@ impl From<ConfigV6> for Config {
.debug .debug
.reply_surbs .reply_surbs
.maximum_reply_surb_drop_waiting_period, .maximum_reply_surb_drop_waiting_period,
maximum_reply_surb_age: value.debug.reply_surbs.maximum_reply_surb_age,
maximum_reply_key_age: value.debug.reply_surbs.maximum_reply_key_age, maximum_reply_key_age: value.debug.reply_surbs.maximum_reply_key_age,
surb_mix_hops: value.debug.reply_surbs.surb_mix_hops, surb_mix_hops: value.debug.reply_surbs.surb_mix_hops,
minimum_reply_surb_threshold_buffer: value minimum_reply_surb_threshold_buffer: value
.debug .debug
.reply_surbs .reply_surbs
.minimum_reply_surb_threshold_buffer, .minimum_reply_surb_threshold_buffer,
..Default::default() fresh_sender_tags: value.debug.reply_surbs.fresh_sender_tags,
}, },
stats_reporting: StatsReporting { stats_reporting: StatsReporting {
enabled: value.debug.stats_reporting.enabled, enabled: value.debug.stats_reporting.enabled,
@@ -9,11 +9,11 @@ license.workspace = true
[dependencies] [dependencies]
async-trait.workspace = true async-trait.workspace = true
cosmrs.workspace = true cosmrs.workspace = true
log.workspace = true
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
thiserror.workspace = true thiserror.workspace = true
time.workspace = true time.workspace = true
tokio = { workspace = true, features = ["sync"] } tokio = { workspace = true, features = ["sync"] }
tracing.workspace = true
url.workspace = true url.workspace = true
zeroize = { workspace = true, features = ["zeroize_derive"] } zeroize = { workspace = true, features = ["zeroize_derive"] }
@@ -2,7 +2,8 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use crate::BadGateway; use crate::BadGateway;
use std::{io, path::PathBuf}; use std::io;
use std::path::PathBuf;
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -18,6 +19,7 @@ pub enum StorageError {
#[error("failed to perform sqlx migration: {source}")] #[error("failed to perform sqlx migration: {source}")]
MigrationError { MigrationError {
#[source]
#[from] #[from]
source: sqlx::migrate::MigrateError, source: sqlx::migrate::MigrateError,
}, },
@@ -30,6 +32,7 @@ pub enum StorageError {
#[error("failed to run the SQL query: {source}")] #[error("failed to run the SQL query: {source}")]
QueryError { QueryError {
#[source]
#[from] #[from]
source: sqlx::error::Error, source: sqlx::error::Error,
}, },
@@ -7,12 +7,12 @@ use crate::{
RawActiveGateway, RawCustomGatewayDetails, RawRegisteredGateway, RawRemoteGatewayDetails, RawActiveGateway, RawCustomGatewayDetails, RawRegisteredGateway, RawRemoteGatewayDetails,
}, },
}; };
use log::{debug, error};
use sqlx::{ use sqlx::{
sqlite::{SqliteAutoVacuum, SqliteSynchronous}, sqlite::{SqliteAutoVacuum, SqliteSynchronous},
ConnectOptions, ConnectOptions,
}; };
use std::path::Path; use std::path::Path;
use tracing::{debug, error};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StorageManager { pub struct StorageManager {
@@ -87,7 +87,7 @@ impl StorageManager {
sqlx::query!("SELECT EXISTS (SELECT 1 FROM registered_gateway WHERE gateway_id_bs58 = ?) AS 'exists'", gateway_id) sqlx::query!("SELECT EXISTS (SELECT 1 FROM registered_gateway WHERE gateway_id_bs58 = ?) AS 'exists'", gateway_id)
.fetch_one(&self.connection_pool) .fetch_one(&self.connection_pool)
.await .await
.map(|result| result.exists == 1) .map(|result| result.exists == Some(1))
} }
pub(crate) async fn maybe_get_registered_gateway( pub(crate) async fn maybe_get_registered_gateway(
@@ -12,12 +12,12 @@ use crate::{
error::ClientCoreError, error::ClientCoreError,
init::types::{GatewaySelectionSpecification, GatewaySetup}, init::types::{GatewaySelectionSpecification, GatewaySetup},
}; };
use log::info;
use nym_client_core_gateways_storage::GatewayDetails; use nym_client_core_gateways_storage::GatewayDetails;
use nym_crypto::asymmetric::ed25519; use nym_crypto::asymmetric::ed25519;
use nym_topology::NymTopology; use nym_topology::NymTopology;
use nym_validator_client::UserAgent; use nym_validator_client::UserAgent;
use std::path::PathBuf; use std::path::PathBuf;
use tracing::info;
#[cfg_attr(feature = "cli", derive(clap::Args))] #[cfg_attr(feature = "cli", derive(clap::Args))]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -81,14 +81,14 @@ where
// Attempt to use a user-provided gateway, if possible // Attempt to use a user-provided gateway, if possible
let user_chosen_gateway_id = common_args.gateway_id; let user_chosen_gateway_id = common_args.gateway_id;
tracing::debug!("User chosen gateway id: {user_chosen_gateway_id:?}"); log::debug!("User chosen gateway id: {user_chosen_gateway_id:?}");
let selection_spec = GatewaySelectionSpecification::new( let selection_spec = GatewaySelectionSpecification::new(
user_chosen_gateway_id.map(|id| id.to_base58_string()), user_chosen_gateway_id.map(|id| id.to_base58_string()),
Some(common_args.latency_based_selection), Some(common_args.latency_based_selection),
common_args.force_tls_gateway, common_args.force_tls_gateway,
); );
tracing::debug!("Gateway selection specification: {selection_spec:?}"); log::debug!("Gateway selection specification: {selection_spec:?}");
let registered_gateways = get_all_registered_identities(&details_store).await?; let registered_gateways = get_all_registered_identities(&details_store).await?;
@@ -58,7 +58,6 @@ where
Some(data) => data, Some(data) => data,
None => { None => {
// SAFETY: one of those arguments must have been set // SAFETY: one of those arguments must have been set
#[allow(clippy::unwrap_used)]
fs::read(common_args.signatures_path.unwrap())? fs::read(common_args.signatures_path.unwrap())?
} }
}; };
@@ -64,7 +64,6 @@ where
Some(data) => data, Some(data) => data,
None => { None => {
// SAFETY: one of those arguments must have been set // SAFETY: one of those arguments must have been set
#[allow(clippy::unwrap_used)]
fs::read(common_args.credential_path.unwrap())? fs::read(common_args.credential_path.unwrap())?
} }
}; };
@@ -58,7 +58,6 @@ where
Some(data) => data, Some(data) => data,
None => { None => {
// SAFETY: one of those arguments must have been set // SAFETY: one of those arguments must have been set
#[allow(clippy::unwrap_used)]
fs::read(common_args.signatures_path.unwrap())? fs::read(common_args.signatures_path.unwrap())?
} }
}; };
@@ -58,7 +58,6 @@ where
Some(data) => data, Some(data) => data,
None => { None => {
// SAFETY: one of those arguments must have been set // SAFETY: one of those arguments must have been set
#[allow(clippy::unwrap_used)]
fs::read(common_args.key_path.unwrap())? fs::read(common_args.key_path.unwrap())?
} }
}; };
@@ -12,6 +12,7 @@ use crate::{
}, },
init::types::{GatewaySelectionSpecification, GatewaySetup, InitResults}, init::types::{GatewaySelectionSpecification, GatewaySetup, InitResults},
}; };
use log::info;
use nym_client_core_gateways_storage::GatewayDetails; use nym_client_core_gateways_storage::GatewayDetails;
use nym_crypto::asymmetric::ed25519; use nym_crypto::asymmetric::ed25519;
use nym_sphinx::addressing::Recipient; use nym_sphinx::addressing::Recipient;
@@ -19,7 +20,6 @@ use nym_topology::NymTopology;
use nym_validator_client::UserAgent; use nym_validator_client::UserAgent;
use rand::rngs::OsRng; use rand::rngs::OsRng;
use std::path::PathBuf; use std::path::PathBuf;
use tracing::info;
// we can suppress this warning (as suggested by linter itself) since we're only using it in our own code // we can suppress this warning (as suggested by linter itself) since we're only using it in our own code
#[allow(async_fn_in_trait)] #[allow(async_fn_in_trait)]
@@ -130,23 +130,23 @@ where
// Attempt to use a user-provided gateway, if possible // Attempt to use a user-provided gateway, if possible
let user_chosen_gateway_id = common_args.gateway; let user_chosen_gateway_id = common_args.gateway;
tracing::debug!("User chosen gateway id: {user_chosen_gateway_id:?}"); log::debug!("User chosen gateway id: {user_chosen_gateway_id:?}");
let selection_spec = GatewaySelectionSpecification::new( let selection_spec = GatewaySelectionSpecification::new(
user_chosen_gateway_id.map(|id| id.to_base58_string()), user_chosen_gateway_id.map(|id| id.to_base58_string()),
Some(common_args.latency_based_selection), Some(common_args.latency_based_selection),
common_args.force_tls_gateway, common_args.force_tls_gateway,
); );
tracing::debug!("Gateway selection specification: {selection_spec:?}"); log::debug!("Gateway selection specification: {selection_spec:?}");
// Load and potentially override config // Load and potentially override config
tracing::debug!("Init arguments: {init_args:#?}"); log::debug!("Init arguments: {init_args:#?}");
let config = C::construct_config(&init_args); let config = C::construct_config(&init_args);
tracing::debug!("Constructed config: {config:#?}"); log::debug!("Constructed config: {config:#?}");
let paths = config.common_paths(); let paths = config.common_paths();
let core = config.core_config(); let core = config.core_config();
tracing::info!( log::info!(
"Using nym-api: {}", "Using nym-api: {}",
core.client core.client
.nym_api_urls .nym_api_urls
@@ -18,7 +18,6 @@ use crate::client::received_buffer::{
ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController, ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController,
}; };
use crate::client::replies::reply_controller; use crate::client::replies::reply_controller;
use crate::client::replies::reply_controller::key_rotation_helpers::KeyRotationConfig;
use crate::client::replies::reply_controller::{ReplyControllerReceiver, ReplyControllerSender}; use crate::client::replies::reply_controller::{ReplyControllerReceiver, ReplyControllerSender};
use crate::client::replies::reply_storage::{ use crate::client::replies::reply_storage::{
CombinedReplyStorage, PersistentReplyStorage, ReplyStorageBackend, SentReplyKeys, CombinedReplyStorage, PersistentReplyStorage, ReplyStorageBackend, SentReplyKeys,
@@ -35,6 +34,7 @@ use crate::init::{
}; };
use crate::{config, spawn_future}; use crate::{config, spawn_future};
use futures::channel::mpsc; use futures::channel::mpsc;
use log::*;
use nym_bandwidth_controller::BandwidthController; use nym_bandwidth_controller::BandwidthController;
use nym_client_core_config_types::{ForgetMe, RememberMe}; use nym_client_core_config_types::{ForgetMe, RememberMe};
use nym_client_core_gateways_storage::{GatewayDetails, GatewaysDetailsStore}; use nym_client_core_gateways_storage::{GatewayDetails, GatewaysDetailsStore};
@@ -56,18 +56,13 @@ use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender,
use nym_task::{TaskClient, TaskHandle}; use nym_task::{TaskClient, TaskHandle};
use nym_topology::provider_trait::TopologyProvider; use nym_topology::provider_trait::TopologyProvider;
use nym_topology::HardcodedTopologyProvider; use nym_topology::HardcodedTopologyProvider;
use nym_validator_client::nym_api::NymApiClientExt; use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, UserAgent};
use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, NymApiClient, UserAgent};
use rand::prelude::SliceRandom;
use rand::rngs::OsRng; use rand::rngs::OsRng;
use rand::thread_rng;
use std::fmt::Debug; use std::fmt::Debug;
use std::os::raw::c_int as RawFd; use std::os::raw::c_int as RawFd;
use std::path::Path; use std::path::Path;
use std::sync::Arc; use std::sync::Arc;
use time::OffsetDateTime;
use tokio::sync::mpsc::Sender; use tokio::sync::mpsc::Sender;
use tracing::*;
use url::Url; use url::Url;
#[cfg(all( #[cfg(all(
@@ -135,11 +130,9 @@ pub enum ClientInputStatus {
} }
impl ClientInputStatus { impl ClientInputStatus {
#[allow(clippy::panic)]
pub fn register_producer(&mut self) -> ClientInput { pub fn register_producer(&mut self) -> ClientInput {
match std::mem::replace(self, ClientInputStatus::Connected) { match std::mem::replace(self, ClientInputStatus::Connected) {
ClientInputStatus::AwaitingProducer { client_input } => client_input, ClientInputStatus::AwaitingProducer { client_input } => client_input,
// critical failure implying misuse of software
ClientInputStatus::Connected => panic!("producer was already registered before"), ClientInputStatus::Connected => panic!("producer was already registered before"),
} }
} }
@@ -151,11 +144,9 @@ pub enum ClientOutputStatus {
} }
impl ClientOutputStatus { impl ClientOutputStatus {
#[allow(clippy::panic)]
pub fn register_consumer(&mut self) -> ClientOutput { pub fn register_consumer(&mut self) -> ClientOutput {
match std::mem::replace(self, ClientOutputStatus::Connected) { match std::mem::replace(self, ClientOutputStatus::Connected) {
ClientOutputStatus::AwaitingConsumer { client_output } => client_output, ClientOutputStatus::AwaitingConsumer { client_output } => client_output,
// critical failure implying misuse of software
ClientOutputStatus::Connected => panic!("consumer was already registered before"), ClientOutputStatus::Connected => panic!("consumer was already registered before"),
} }
} }
@@ -347,7 +338,6 @@ where
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn start_real_traffic_controller( fn start_real_traffic_controller(
controller_config: real_messages_control::Config, controller_config: real_messages_control::Config,
key_rotation_config: KeyRotationConfig,
topology_accessor: TopologyAccessor, topology_accessor: TopologyAccessor,
ack_receiver: AcknowledgementReceiver, ack_receiver: AcknowledgementReceiver,
input_receiver: InputMessageReceiver, input_receiver: InputMessageReceiver,
@@ -365,7 +355,6 @@ where
RealMessagesController::new( RealMessagesController::new(
controller_config, controller_config,
key_rotation_config,
ack_receiver, ack_receiver,
input_receiver, input_receiver,
mix_sender, mix_sender,
@@ -464,10 +453,10 @@ where
}; };
let gateway_failure = |err| { let gateway_failure = |err| {
tracing::error!("Could not authenticate and start up the gateway connection - {err}"); log::error!("Could not authenticate and start up the gateway connection - {err}");
ClientCoreError::GatewayClientError { ClientCoreError::GatewayClientError {
gateway_id: details.gateway_id.to_base58_string(), gateway_id: details.gateway_id.to_base58_string(),
source: Box::new(err), source: err,
} }
}; };
@@ -566,14 +555,14 @@ where
custom_provider: Option<Box<dyn TopologyProvider + Send + Sync>>, custom_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
config_topology: config::Topology, config_topology: config::Topology,
nym_api_urls: Vec<Url>, nym_api_urls: Vec<Url>,
nym_api_client: NymApiClient, user_agent: Option<UserAgent>,
) -> Box<dyn TopologyProvider + Send + Sync> { ) -> Box<dyn TopologyProvider + Send + Sync> {
// if no custom provider was ... provided ..., create one using nym-api // if no custom provider was ... provided ..., create one using nym-api
custom_provider.unwrap_or_else(|| { custom_provider.unwrap_or_else(|| {
Box::new(NymApiTopologyProvider::new( Box::new(NymApiTopologyProvider::new(
config_topology, config_topology,
nym_api_urls, nym_api_urls,
nym_api_client, user_agent,
)) ))
}) })
} }
@@ -609,7 +598,7 @@ where
topology_refresher.try_refresh().await; topology_refresher.try_refresh().await;
if let Err(err) = topology_refresher.ensure_topology_is_routable().await { if let Err(err) = topology_refresher.ensure_topology_is_routable().await {
tracing::error!( log::error!(
"The current network topology seem to be insufficient to route any packets through \ "The current network topology seem to be insufficient to route any packets through \
- check if enough nodes and a gateway are online - source: {err}" - check if enough nodes and a gateway are online - source: {err}"
); );
@@ -685,40 +674,27 @@ where
// TODO: rename it as it implies the data is persistent whilst one can use InMemBackend // TODO: rename it as it implies the data is persistent whilst one can use InMemBackend
async fn setup_persistent_reply_storage( async fn setup_persistent_reply_storage(
backend: S::ReplyStore, backend: S::ReplyStore,
key_rotation_config: KeyRotationConfig,
shutdown: TaskClient, shutdown: TaskClient,
) -> Result<CombinedReplyStorage, ClientCoreError> ) -> Result<CombinedReplyStorage, ClientCoreError>
where where
<S::ReplyStore as ReplyStorageBackend>::StorageError: Sync + Send, <S::ReplyStore as ReplyStorageBackend>::StorageError: Sync + Send,
S::ReplyStore: Send + Sync, S::ReplyStore: Send + Sync,
{ {
tracing::trace!("Setup persistent reply storage"); log::trace!("Setup persistent reply storage");
let now = OffsetDateTime::now_utc();
let expected_current_key_rotation_start =
key_rotation_config.expected_current_key_rotation_start(now);
// time of the start of one epoch BEFORE the CURRENT rotation has begun
// this indicates the starting time of when packets with the current keys might have been constructed
// (i.e. any surbs OLDER than that MUST BE invalid)
let prior_epoch_start =
expected_current_key_rotation_start - key_rotation_config.epoch_duration;
let persistent_storage = PersistentReplyStorage::new(backend); let persistent_storage = PersistentReplyStorage::new(backend);
let mem_store = persistent_storage let mem_store = persistent_storage
.load_state_from_backend(prior_epoch_start) .load_state_from_backend()
.await .await
.map_err(|err| ClientCoreError::SurbStorageError { .map_err(|err| ClientCoreError::SurbStorageError {
source: Box::new(err), source: Box::new(err),
})?; })?;
let store_clone = mem_store.clone(); let store_clone = mem_store.clone();
spawn_future!( spawn_future(async move {
async move { persistent_storage
persistent_storage .flush_on_shutdown(store_clone, shutdown)
.flush_on_shutdown(store_clone, shutdown) .await
.await });
},
"PersistentReplyStorage::flush_on_shutdown"
);
Ok(mem_store) Ok(mem_store)
} }
@@ -739,7 +715,7 @@ where
let mut rng = OsRng; let mut rng = OsRng;
let keys = if let Some(derivation_material) = derivation_material { let keys = if let Some(derivation_material) = derivation_material {
ClientKeys::from_master_key(&mut rng, &derivation_material) ClientKeys::from_master_key(&mut rng, &derivation_material)
.map_err(|_| ClientCoreError::HkdfDerivationError)? .map_err(|_| ClientCoreError::HkdfDerivationError {})?
} else { } else {
ClientKeys::generate_new(&mut rng) ClientKeys::generate_new(&mut rng)
}; };
@@ -749,23 +725,6 @@ where
setup_gateway(setup_method, key_store, details_store).await setup_gateway(setup_method, key_store, details_store).await
} }
fn construct_nym_api_client(config: &Config, user_agent: Option<UserAgent>) -> NymApiClient {
let mut nym_api_urls = config.get_nym_api_endpoints();
nym_api_urls.shuffle(&mut thread_rng());
if let Some(user_agent) = user_agent {
NymApiClient::new_with_user_agent(nym_api_urls[0].clone(), user_agent)
} else {
NymApiClient::new(nym_api_urls[0].clone())
}
}
async fn determine_key_rotation_state(
client: &NymApiClient,
) -> Result<KeyRotationConfig, ClientCoreError> {
Ok(client.nym_api.get_key_rotation_info().await?.into())
}
pub async fn start_base(mut self) -> Result<BaseClient, ClientCoreError> pub async fn start_base(mut self) -> Result<BaseClient, ClientCoreError>
where where
S::ReplyStore: Send + Sync, S::ReplyStore: Send + Sync,
@@ -830,14 +789,11 @@ where
.dkg_query_client .dkg_query_client
.map(|client| BandwidthController::new(credential_store, client)); .map(|client| BandwidthController::new(credential_store, client));
let nym_api_client = Self::construct_nym_api_client(&self.config, self.user_agent.clone());
let key_rotation_config = Self::determine_key_rotation_state(&nym_api_client).await?;
let topology_provider = Self::setup_topology_provider( let topology_provider = Self::setup_topology_provider(
self.custom_topology_provider.take(), self.custom_topology_provider.take(),
self.config.debug.topology, self.config.debug.topology,
self.config.get_nym_api_endpoints(), self.config.get_nym_api_endpoints(),
nym_api_client, self.user_agent.clone(),
); );
let stats_reporter = Self::start_statistics_control( let stats_reporter = Self::start_statistics_control(
@@ -882,7 +838,6 @@ where
let reply_storage = Self::setup_persistent_reply_storage( let reply_storage = Self::setup_persistent_reply_storage(
reply_storage_backend, reply_storage_backend,
key_rotation_config,
shutdown.fork("persistent_reply_storage"), shutdown.fork("persistent_reply_storage"),
) )
.await?; .await?;
@@ -923,7 +878,6 @@ where
Self::start_real_traffic_controller( Self::start_real_traffic_controller(
controller_config, controller_config,
key_rotation_config,
shared_topology_accessor.clone(), shared_topology_accessor.clone(),
ack_receiver, ack_receiver,
input_receiver, input_receiver,
@@ -1,30 +1,32 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net> // Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use crate::{ use crate::client::replies::reply_storage::{
client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend}, fs_backend, CombinedReplyStorage, ReplyStorageBackend,
config,
config::Config,
error::ClientCoreError,
}; };
use crate::config;
use crate::config::Config;
use crate::error::ClientCoreError;
use log::{error, info, trace};
use nym_bandwidth_controller::BandwidthController; use nym_bandwidth_controller::BandwidthController;
use nym_client_core_gateways_storage::OnDiskGatewaysDetails; use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
use nym_credential_storage::storage::Storage as CredentialStorage; use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient}; use nym_validator_client::nyxd;
use std::{io, path::Path}; use nym_validator_client::QueryHttpRpcNyxdClient;
use std::path::Path;
use std::{fs, io};
use time::OffsetDateTime; use time::OffsetDateTime;
use tracing::{error, info, trace};
use url::Url; use url::Url;
async fn setup_fresh_backend<P: AsRef<Path>>( async fn setup_fresh_backend<P: AsRef<Path>>(
db_path: P, db_path: P,
surb_config: &config::ReplySurbs, surb_config: &config::ReplySurbs,
) -> Result<fs_backend::Backend, ClientCoreError> { ) -> Result<fs_backend::Backend, ClientCoreError> {
info!("Creating fresh surb database"); info!("creating fresh surb database");
let mut storage_backend = match fs_backend::Backend::init(db_path).await { let mut storage_backend = match fs_backend::Backend::init(db_path).await {
Ok(backend) => backend, Ok(backend) => backend,
Err(err) => { Err(err) => {
error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}"); error!("failed to setup persistent storage backend for our reply needs: {err}");
return Err(ClientCoreError::SurbStorageError { return Err(ClientCoreError::SurbStorageError {
source: Box::new(err), source: Box::new(err),
}); });
@@ -38,15 +40,14 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
surb_config.minimum_reply_surb_storage_threshold, surb_config.minimum_reply_surb_storage_threshold,
surb_config.maximum_reply_surb_storage_threshold, surb_config.maximum_reply_surb_storage_threshold,
); );
match storage_backend.init_fresh(&mem_store).await { storage_backend
Ok(()) => Ok(storage_backend), .init_fresh(&mem_store)
Err(err) => { .await
storage_backend.shutdown().await; .map_err(|err| ClientCoreError::SurbStorageError {
Err(ClientCoreError::SurbStorageError { source: Box::new(err),
source: Box::new(err), })?;
})
} Ok(storage_backend)
}
} }
// fn setup_inactive_backend(surb_config: &config::ReplySurbs) -> fs_backend::Backend { // fn setup_inactive_backend(surb_config: &config::ReplySurbs) -> fs_backend::Backend {
@@ -57,11 +58,12 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
// ) // )
// } // }
async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> { fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
let db_path = db_path.as_ref(); let db_path = db_path.as_ref();
debug_assert!(db_path.exists()); debug_assert!(db_path.exists());
let now = OffsetDateTime::now_utc().unix_timestamp(); let now = OffsetDateTime::now_utc().unix_timestamp();
let suffix = format!("_{now}.corrupted"); let suffix = format!("_{now}.corrupted");
let new_extension = let new_extension =
@@ -70,15 +72,11 @@ async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()
} else { } else {
suffix suffix
}; };
let renamed = db_path.with_extension(new_extension);
tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| { let mut renamed = db_path.to_owned();
error!( renamed.set_extension(new_extension);
"Failed to rename corrupt database file: {} to {}",
db_path.display(), fs::rename(db_path, renamed)
renamed.display()
);
})
} }
pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>( pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
@@ -89,12 +87,13 @@ pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
// the existing one // the existing one
let db_path = db_path.as_ref(); let db_path = db_path.as_ref();
if db_path.exists() { if db_path.exists() {
info!("Loading existing surb database"); info!("loading existing surb database");
match fs_backend::Backend::try_load(db_path).await { match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await {
Ok(backend) => Ok(backend), Ok(backend) => Ok(backend),
Err(err) => { Err(err) => {
error!("setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future"); error!("failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
archive_corrupted_database(db_path).await?;
archive_corrupted_database(db_path)?;
setup_fresh_backend(db_path, surb_config).await setup_fresh_backend(db_path, surb_config).await
} }
} }
@@ -114,32 +113,41 @@ pub async fn setup_fs_gateways_storage<P: AsRef<Path>>(
}) })
} }
pub fn create_bandwidth_controller_with_urls<St: CredentialStorage>( pub fn create_bandwidth_controller<St: CredentialStorage>(
nyxd_url: Url,
storage: St,
) -> Result<BandwidthController<QueryHttpRpcNyxdClient, St>, ClientCoreError> {
let client = default_query_dkg_client(nyxd_url)?;
Ok(BandwidthController::new(storage, client))
}
pub fn default_query_dkg_client_from_config(
config: &Config, config: &Config,
) -> Result<QueryHttpRpcNyxdClient, ClientCoreError> { storage: St,
) -> BandwidthController<QueryHttpRpcNyxdClient, St> {
let nyxd_url = config let nyxd_url = config
.get_validator_endpoints() .get_validator_endpoints()
.pop() .pop()
.ok_or(ClientCoreError::RpcClientMissingUrl)?; .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);
BandwidthController::new(storage, client)
}
pub fn default_query_dkg_client_from_config(config: &Config) -> QueryHttpRpcNyxdClient {
let nyxd_url = config
.get_validator_endpoints()
.pop()
.expect("No nyxd validator endpoint provided");
default_query_dkg_client(nyxd_url) default_query_dkg_client(nyxd_url)
} }
pub fn default_query_dkg_client(nyxd_url: Url) -> Result<QueryHttpRpcNyxdClient, ClientCoreError> { pub fn default_query_dkg_client(nyxd_url: Url) -> QueryHttpRpcNyxdClient {
let details = nym_network_defaults::NymNetworkDetails::new_from_env(); let details = nym_network_defaults::NymNetworkDetails::new_from_env();
let client_config = nyxd::Config::try_from_nym_network_details(&details) let client_config = nyxd::Config::try_from_nym_network_details(&details)
.map_err(|source| ClientCoreError::InvalidNetworkDetails { source })?; .expect("failed to construct validator client config");
// overwrite env configuration with config URLs // overwrite env configuration with config URLs
QueryHttpRpcNyxdClient::connect(client_config, nyxd_url.as_str()) QueryHttpRpcNyxdClient::connect(client_config, nyxd_url.as_str())
.map_err(|source| ClientCoreError::RpcClientCreationFailure { source }) .expect("Could not construct query client")
} }
@@ -6,6 +6,7 @@ use crate::client::topology_control::TopologyAccessor;
use crate::{config, spawn_future}; use crate::{config, spawn_future};
use futures::task::{Context, Poll}; use futures::task::{Context, Poll};
use futures::{Future, Stream, StreamExt}; use futures::{Future, Stream, StreamExt};
use log::*;
use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::acknowledgements::AckKey;
use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::cover::generate_loop_cover_packet; use nym_sphinx::cover::generate_loop_cover_packet;
@@ -18,7 +19,6 @@ use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tokio::sync::mpsc::error::TrySendError; use tokio::sync::mpsc::error::TrySendError;
use tracing::*;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use tokio::time::{sleep, Sleep}; use tokio::time::{sleep, Sleep};
@@ -210,10 +210,10 @@ impl LoopCoverTrafficStream<OsRng> {
TrySendError::Full(_) => { TrySendError::Full(_) => {
// This isn't a problem, if the channel is full means we're already sending the // This isn't a problem, if the channel is full means we're already sending the
// max amount of messages downstream can handle. // max amount of messages downstream can handle.
tracing::debug!("Failed to send cover message - channel full"); log::debug!("Failed to send cover message - channel full");
} }
TrySendError::Closed(_) => { TrySendError::Closed(_) => {
tracing::warn!("Failed to send cover message - channel closed"); log::warn!("Failed to send cover message - channel closed");
} }
} }
} else { } else {
@@ -235,7 +235,6 @@ impl LoopCoverTrafficStream<OsRng> {
tokio::task::yield_now().await; tokio::task::yield_now().await;
} }
#[allow(clippy::panic)]
pub fn start(mut self) { pub fn start(mut self) {
if self.cover_traffic.disable_loop_cover_traffic_stream { 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 // we should have never got here in the first place - the task should have never been created to begin with
@@ -252,30 +251,27 @@ impl LoopCoverTrafficStream<OsRng> {
let mut shutdown = self.task_client.fork("select"); let mut shutdown = self.task_client.fork("select");
spawn_future!( spawn_future(async move {
async move { debug!("Started LoopCoverTrafficStream with graceful shutdown support");
debug!("Started LoopCoverTrafficStream with graceful shutdown support");
while !shutdown.is_shutdown() { while !shutdown.is_shutdown() {
tokio::select! { tokio::select! {
biased; biased;
_ = shutdown.recv() => { _ = shutdown.recv() => {
tracing::trace!("LoopCoverTrafficStream: Received shutdown"); log::trace!("LoopCoverTrafficStream: Received shutdown");
} }
next = self.next() => { next = self.next() => {
if next.is_some() { if next.is_some() {
self.on_new_message().await; self.on_new_message().await;
} else { } else {
tracing::trace!("LoopCoverTrafficStream: Stopping since channel closed"); log::trace!("LoopCoverTrafficStream: Stopping since channel closed");
break; break;
}
} }
} }
} }
shutdown.recv_timeout().await; }
tracing::debug!("LoopCoverTrafficStream: Exiting"); shutdown.recv_timeout().await;
}, log::debug!("LoopCoverTrafficStream: Exiting");
"LoopCoverTrafficStream" })
)
} }
} }
@@ -135,9 +135,7 @@ impl InputMessage {
recipient_tag, recipient_tag,
data, data,
lane, lane,
// \/ set it to SOME sane default so that if we run out of surbs and constantly max_retransmissions: None,
// fail to request more, we wouldn't be stuck in limbo
max_retransmissions: Some(10),
}; };
if let Some(packet_type) = packet_type { if let Some(packet_type) = packet_type {
InputMessage::new_wrapper(message, packet_type) InputMessage::new_wrapper(message, packet_type)
@@ -4,10 +4,10 @@
use crate::client::mix_traffic::transceiver::GatewayTransceiver; use crate::client::mix_traffic::transceiver::GatewayTransceiver;
use crate::error::ClientCoreError; use crate::error::ClientCoreError;
use crate::spawn_future; use crate::spawn_future;
use log::*;
use nym_gateway_requests::ClientRequest; use nym_gateway_requests::ClientRequest;
use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::forwarding::packet::MixPacket;
use nym_task::TaskClient; use nym_task::TaskClient;
use tracing::*;
use transceiver::ErasedGatewayError; use transceiver::ErasedGatewayError;
pub type BatchMixMessageSender = tokio::sync::mpsc::Sender<Vec<MixPacket>>; pub type BatchMixMessageSender = tokio::sync::mpsc::Sender<Vec<MixPacket>>;
@@ -96,93 +96,72 @@ impl MixTrafficController {
mut mix_packets: Vec<MixPacket>, mut mix_packets: Vec<MixPacket>,
) -> Result<(), ErasedGatewayError> { ) -> Result<(), ErasedGatewayError> {
debug_assert!(!mix_packets.is_empty()); debug_assert!(!mix_packets.is_empty());
let send_future = if mix_packets.len() == 1 {
// SAFETY: we just checked we have one packet let result = if mix_packets.len() == 1 {
#[allow(clippy::unwrap_used)]
let mix_packet = mix_packets.pop().unwrap(); let mix_packet = mix_packets.pop().unwrap();
self.gateway_transceiver.send_mix_packet(mix_packet) self.gateway_transceiver.send_mix_packet(mix_packet).await
} else { } else {
self.gateway_transceiver.batch_send_mix_packets(mix_packets) self.gateway_transceiver
.batch_send_mix_packets(mix_packets)
.await
}; };
tokio::select! { if result.is_err() {
biased; self.consecutive_gateway_failure_count += 1;
_ = self.task_client.recv() => { } else {
trace!("received shutdown while handling messages"); trace!("We *might* have managed to forward sphinx packet(s) to the gateway!");
Ok(()) self.consecutive_gateway_failure_count = 0;
}
result = send_future => {
if result.is_err() {
self.consecutive_gateway_failure_count += 1;
} else {
trace!("We *might* have managed to forward sphinx packet(s) to the gateway!");
self.consecutive_gateway_failure_count = 0;
}
result
}
} }
}
async fn on_client_request(&mut self, client_request: ClientRequest) { result
tokio::select! {
biased;
_ = self.task_client.recv() => {
trace!("received shutdown while handling client request");
}
result = self.gateway_transceiver.send_client_request(client_request) => {
if let Err(err) = result {
error!("Failed to send client request: {err}")
}
}
}
} }
pub fn start(mut self) { pub fn start(mut self) {
spawn_future!( spawn_future(async move {
async move { debug!("Started MixTrafficController with graceful shutdown support");
debug!("Started MixTrafficController with graceful shutdown support");
while !self.task_client.is_shutdown() { while !self.task_client.is_shutdown() {
tokio::select! { tokio::select! {
biased; mix_packets = self.mix_rx.recv() => match mix_packets {
_ = self.task_client.recv() => { Some(mix_packets) => {
tracing::trace!("MixTrafficController: Received shutdown"); 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 => {
log::trace!("MixTrafficController: Stopping since channel closed");
break; break;
} }
mix_packets = self.mix_rx.recv() => match mix_packets { },
Some(mix_packets) => { client_request = self.client_rx.recv() => match client_request {
if let Err(err) = self.on_messages(mix_packets).await { Some(client_request) => {
error!("Failed to send sphinx packet(s) to the gateway: {err}"); match self.gateway_transceiver.send_client_request(client_request).await {
if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT { Ok(_) => (),
// Disconnect from the gateway. If we should try to re-connect Err(e) => error!("Failed to send client request: {}", e),
// 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;
}
},
client_request = self.client_rx.recv() => match client_request {
Some(client_request) => {
self.on_client_request(client_request).await;
},
None => {
tracing::trace!("MixTrafficController, client request channel closed");
}
}, },
None => {
log::trace!("MixTrafficController, client request channel closed");
}
},
_ = self.task_client.recv() => {
log::trace!("MixTrafficController: Received shutdown");
break;
} }
} }
self.task_client.recv_timeout().await; }
tracing::debug!("MixTrafficController: Exiting"); self.task_client.recv_timeout().await;
},
"MixTrafficController" log::debug!("MixTrafficController: Exiting");
); });
} }
} }
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait; use async_trait::async_trait;
use log::{debug, error};
use nym_credential_storage::storage::Storage as CredentialStorage; use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_crypto::asymmetric::ed25519; use nym_crypto::asymmetric::ed25519;
use nym_gateway_client::error::GatewayClientError; use nym_gateway_client::error::GatewayClientError;
@@ -13,7 +14,6 @@ use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use std::fmt::Debug; use std::fmt::Debug;
use std::os::raw::c_int as RawFd; use std::os::raw::c_int as RawFd;
use thiserror::Error; use thiserror::Error;
use tracing::{debug, error};
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use futures::channel::oneshot; use futures::channel::oneshot;
@@ -27,7 +27,7 @@ fn erase_err<E: std::error::Error + Send + Sync + 'static>(err: E) -> ErasedGate
ErasedGatewayError(Box::new(err)) ErasedGatewayError(Box::new(err))
} }
/// This combines the functionalities of being able to send and receive mix packets. /// This combines combines the functionalities of being able to send and receive mix packets.
#[async_trait] #[async_trait]
pub trait GatewayTransceiver: GatewaySender + GatewayReceiver { pub trait GatewayTransceiver: GatewaySender + GatewayReceiver {
fn gateway_identity(&self) -> ed25519::PublicKey; fn gateway_identity(&self) -> ed25519::PublicKey;
@@ -36,6 +36,9 @@ pub trait GatewayTransceiver: GatewaySender + GatewayReceiver {
&mut self, &mut self,
message: ClientRequest, message: ClientRequest,
) -> Result<(), GatewayClientError>; ) -> Result<(), GatewayClientError>;
/// Check if the websocket connection to the gateway is alive
fn is_connection_alive(&self) -> bool;
} }
/// This trait defines the functionality of sending `MixPacket` into the mixnet, /// This trait defines the functionality of sending `MixPacket` into the mixnet,
@@ -87,9 +90,14 @@ impl<G: GatewayTransceiver + ?Sized + Send> GatewayTransceiver for Box<G> {
message: ClientRequest, message: ClientRequest,
) -> Result<(), GatewayClientError> { ) -> Result<(), GatewayClientError> {
let _ = (**self).send_client_request(message.clone()).await?; let _ = (**self).send_client_request(message.clone()).await?;
tracing::debug!("Sent client request: {:?}", message); log::debug!("Sent client request: {:?}", message);
Ok(()) Ok(())
} }
#[inline]
fn is_connection_alive(&self) -> bool {
(**self).is_connection_alive()
}
} }
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -147,6 +155,10 @@ where
) -> Result<(), GatewayClientError> { ) -> Result<(), GatewayClientError> {
self.gateway_client.send_client_request(message).await self.gateway_client.send_client_request(message).await
} }
fn is_connection_alive(&self) -> bool {
self.gateway_client.is_connection_alive()
}
} }
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
@@ -234,6 +246,11 @@ mod nonwasm_sealed {
) -> Result<(), GatewayClientError> { ) -> Result<(), GatewayClientError> {
Ok(()) Ok(())
} }
fn is_connection_alive(&self) -> bool {
// LocalGateway is always "connected" since it's in-process
true
}
} }
#[async_trait] #[async_trait]
@@ -269,8 +286,6 @@ pub struct MockGateway {
} }
impl Default for MockGateway { impl Default for MockGateway {
// test code
#[allow(clippy::unwrap_used)]
fn default() -> Self { fn default() -> Self {
MockGateway { MockGateway {
dummy_identity: "3ebjp1Fb9hdcS1AR6AZihgeJiMHkB5jjJUsvqNnfQwU7" dummy_identity: "3ebjp1Fb9hdcS1AR6AZihgeJiMHkB5jjJUsvqNnfQwU7"
@@ -318,4 +333,9 @@ impl GatewayTransceiver for MockGateway {
) -> Result<(), GatewayClientError> { ) -> Result<(), GatewayClientError> {
Ok(()) Ok(())
} }
fn is_connection_alive(&self) -> bool {
// MockGateway is always "connected" for testing purposes
true
}
} }
@@ -5,6 +5,7 @@ use super::action_controller::{AckActionSender, Action};
use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender}; use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender};
use futures::StreamExt; use futures::StreamExt;
use log::*;
use nym_gateway_client::AcknowledgementReceiver; use nym_gateway_client::AcknowledgementReceiver;
use nym_sphinx::{ use nym_sphinx::{
acknowledgements::{identifier::recover_identifier, AckKey}, acknowledgements::{identifier::recover_identifier, AckKey},
@@ -12,7 +13,6 @@ use nym_sphinx::{
}; };
use nym_task::TaskClient; use nym_task::TaskClient;
use std::sync::Arc; use std::sync::Arc;
use tracing::*;
/// Module responsible for listening for any data resembling acknowledgements from the network /// Module responsible for listening for any data resembling acknowledgements from the network
/// and firing actions to remove them from the 'Pending' state. /// and firing actions to remove them from the 'Pending' state.
@@ -65,7 +65,7 @@ impl AcknowledgementListener {
return; return;
} }
trace!("Received {frag_id} from the mix network"); trace!("Received {} from the mix network", frag_id);
self.stats_tx self.stats_tx
.report(PacketStatisticsEvent::RealAckReceived(ack_content.len()).into()); .report(PacketStatisticsEvent::RealAckReceived(ack_content.len()).into());
if let Err(err) = self if let Err(err) = self
@@ -93,16 +93,16 @@ impl AcknowledgementListener {
acks = self.ack_receiver.next() => match acks { acks = self.ack_receiver.next() => match acks {
Some(acks) => self.handle_ack_receiver_item(acks).await, Some(acks) => self.handle_ack_receiver_item(acks).await,
None => { None => {
tracing::trace!("AcknowledgementListener: Stopping since channel closed"); log::trace!("AcknowledgementListener: Stopping since channel closed");
break; break;
} }
}, },
_ = self.task_client.recv() => { _ = self.task_client.recv() => {
tracing::trace!("AcknowledgementListener: Received shutdown"); log::trace!("AcknowledgementListener: Received shutdown");
} }
} }
} }
self.task_client.recv_timeout().await; self.task_client.recv_timeout().await;
tracing::debug!("AcknowledgementListener: Exiting"); log::debug!("AcknowledgementListener: Exiting");
} }
} }
@@ -5,6 +5,7 @@ use super::PendingAcknowledgement;
use crate::client::real_messages_control::acknowledgement_control::RetransmissionRequestSender; use crate::client::real_messages_control::acknowledgement_control::RetransmissionRequestSender;
use futures::channel::mpsc; use futures::channel::mpsc;
use futures::StreamExt; use futures::StreamExt;
use log::*;
use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue, QueueKey}; use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue, QueueKey};
use nym_sphinx::chunking::fragment::FragmentIdentifier; use nym_sphinx::chunking::fragment::FragmentIdentifier;
use nym_sphinx::Delay as SphinxDelay; use nym_sphinx::Delay as SphinxDelay;
@@ -12,7 +13,6 @@ use nym_task::TaskClient;
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tracing::*;
pub(crate) type AckActionSender = mpsc::UnboundedSender<Action>; pub(crate) type AckActionSender = mpsc::UnboundedSender<Action>;
pub(crate) type AckActionReceiver = mpsc::UnboundedReceiver<Action>; pub(crate) type AckActionReceiver = mpsc::UnboundedReceiver<Action>;
@@ -126,7 +126,7 @@ impl ActionController {
fn handle_insert(&mut self, pending_acks: Vec<PendingAcknowledgement>) { fn handle_insert(&mut self, pending_acks: Vec<PendingAcknowledgement>) {
for pending_ack in pending_acks { for pending_ack in pending_acks {
let frag_id = pending_ack.message_chunk.fragment_identifier(); let frag_id = pending_ack.message_chunk.fragment_identifier();
trace!("{frag_id} is inserted"); trace!("{} is inserted", frag_id);
if self if self
.pending_acks_data .pending_acks_data
@@ -161,16 +161,22 @@ impl ActionController {
let new_queue_key = self.pending_acks_timers.insert(frag_id, timeout); let new_queue_key = self.pending_acks_timers.insert(frag_id, timeout);
*queue_key = Some(new_queue_key) *queue_key = Some(new_queue_key)
} else { } else {
debug!("Tried to START TIMER on pending ack that is already gone! - {frag_id}"); debug!(
"Tried to START TIMER on pending ack that is already gone! - {}",
frag_id
);
} }
} }
fn handle_remove(&mut self, frag_id: FragmentIdentifier) { fn handle_remove(&mut self, frag_id: FragmentIdentifier) {
trace!("{frag_id} is getting removed"); trace!("{} is getting removed", frag_id);
match self.pending_acks_data.remove(&frag_id) { match self.pending_acks_data.remove(&frag_id) {
None => { None => {
debug!("Tried to REMOVE pending ack that is already gone! - {frag_id}"); debug!(
"Tried to REMOVE pending ack that is already gone! - {}",
frag_id
);
} }
Some((_, queue_key)) => { Some((_, queue_key)) => {
if let Some(queue_key) = queue_key { if let Some(queue_key) = queue_key {
@@ -182,7 +188,10 @@ impl ActionController {
} else { } else {
// I'm not 100% sure if having a `None` key is even possible here // I'm not 100% sure if having a `None` key is even possible here
// (REMOVE would have to be called before START TIMER), // (REMOVE would have to be called before START TIMER),
debug!("Tried to REMOVE pending ack without TIMER active - {frag_id}"); debug!(
"Tried to REMOVE pending ack without TIMER active - {}",
frag_id
);
} }
} }
} }
@@ -191,26 +200,27 @@ impl ActionController {
// initiated basically as a first step of retransmission. At first data has its delay updated // initiated basically as a first step of retransmission. At first data has its delay updated
// (as new sphinx packet was created with new expected delivery time) // (as new sphinx packet was created with new expected delivery time)
fn handle_update_pending_ack(&mut self, frag_id: FragmentIdentifier, delay: SphinxDelay) { fn handle_update_pending_ack(&mut self, frag_id: FragmentIdentifier, delay: SphinxDelay) {
trace!("{frag_id} is updating its delay"); trace!("{} is updating its delay", frag_id);
// TODO: is it possible to solve this without either locking or temporarily removing the value? // 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) { if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.remove(&frag_id) {
// SAFETY: this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets) // this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets)
// or `ReplyController` (for 'reply' packets) which held the other potential // 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 // reference to this Arc. HOWEVER, before the Action was pushed onto the queue, the reference
// was dropped hence this unwrap is safe. // was dropped hence this unwrap is safe.
#[allow(clippy::unwrap_used)]
let mut inner_data = Arc::try_unwrap(pending_ack_data).unwrap(); let mut inner_data = Arc::try_unwrap(pending_ack_data).unwrap();
inner_data.update_retransmitted(delay); inner_data.update_retransmitted(delay);
self.pending_acks_data self.pending_acks_data
.insert(frag_id, (Arc::new(inner_data), queue_key)); .insert(frag_id, (Arc::new(inner_data), queue_key));
} else { } else {
debug!("Tried to UPDATE TIMER on pending ack that is already gone! - {frag_id}"); debug!(
"Tried to UPDATE TIMER on pending ack that is already gone! - {}",
frag_id
);
} }
} }
// note: when the entry expires it's automatically removed from pending_acks_timers // note: when the entry expires it's automatically removed from pending_acks_timers
#[allow(clippy::panic)]
fn handle_expired_ack_timer(&mut self, expired_ack: Expired<FragmentIdentifier>) { fn handle_expired_ack_timer(&mut self, expired_ack: Expired<FragmentIdentifier>) {
let frag_id = expired_ack.into_inner(); let frag_id = expired_ack.into_inner();
@@ -231,7 +241,7 @@ impl ActionController {
.unbounded_send(Arc::downgrade(pending_ack_data)) .unbounded_send(Arc::downgrade(pending_ack_data))
{ {
if !self.task_client.is_shutdown_poll() { if !self.task_client.is_shutdown_poll() {
tracing::error!("Failed to send pending ack for retransmission: {err}"); log::error!("Failed to send pending ack for retransmission: {err}");
} }
} }
} else { } else {
@@ -259,7 +269,7 @@ impl ActionController {
action = self.incoming_actions.next() => match action { action = self.incoming_actions.next() => match action {
Some(action) => self.process_action(action), Some(action) => self.process_action(action),
None => { None => {
tracing::trace!( log::trace!(
"ActionController: Stopping since incoming actions channel closed" "ActionController: Stopping since incoming actions channel closed"
); );
break; break;
@@ -268,17 +278,17 @@ impl ActionController {
expired_ack = self.pending_acks_timers.next() => match expired_ack { expired_ack = self.pending_acks_timers.next() => match expired_ack {
Some(expired_ack) => self.handle_expired_ack_timer(expired_ack), Some(expired_ack) => self.handle_expired_ack_timer(expired_ack),
None => { None => {
tracing::trace!("ActionController: Stopping since ack channel closed"); log::trace!("ActionController: Stopping since ack channel closed");
break; break;
} }
}, },
_ = self.task_client.recv() => { _ = self.task_client.recv() => {
tracing::trace!("ActionController: Received shutdown"); log::trace!("ActionController: Received shutdown");
break; break;
} }
} }
} }
self.task_client.recv_timeout().await; self.task_client.recv_timeout().await;
tracing::debug!("ActionController: Exiting"); log::debug!("ActionController: Exiting");
} }
} }
@@ -5,6 +5,7 @@ use crate::client::inbound_messages::{InputMessage, InputMessageReceiver};
use crate::client::real_messages_control::message_handler::MessageHandler; use crate::client::real_messages_control::message_handler::MessageHandler;
use crate::client::real_messages_control::real_traffic_stream::RealMessage; use crate::client::real_messages_control::real_traffic_stream::RealMessage;
use crate::client::replies::reply_controller::ReplyControllerSender; use crate::client::replies::reply_controller::ReplyControllerSender;
use log::*;
use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::forwarding::packet::MixPacket;
@@ -12,7 +13,6 @@ use nym_sphinx::params::PacketType;
use nym_task::connections::TransmissionLane; use nym_task::connections::TransmissionLane;
use nym_task::TaskClient; use nym_task::TaskClient;
use rand::{CryptoRng, Rng}; use rand::{CryptoRng, Rng};
use tracing::*;
/// Module responsible for dealing with the received messages: splitting them, creating acknowledgements, /// Module responsible for dealing with the received messages: splitting them, creating acknowledgements,
/// putting everything into sphinx packets, etc. /// putting everything into sphinx packets, etc.
@@ -120,7 +120,6 @@ where
} }
} }
#[allow(clippy::panic)]
async fn on_input_message(&mut self, msg: InputMessage) { async fn on_input_message(&mut self, msg: InputMessage) {
match msg { match msg {
InputMessage::Regular { InputMessage::Regular {
@@ -214,9 +213,7 @@ where
self.handle_premade_packets(msgs, lane).await self.handle_premade_packets(msgs, lane).await
} }
// MessageWrappers can't be nested // MessageWrappers can't be nested
InputMessage::MessageWrapper { .. } => { InputMessage::MessageWrapper { .. } => unimplemented!(),
panic!("attempted to use nested MessageWrapper")
}
}, },
}; };
} }
@@ -226,24 +223,21 @@ where
while !self.task_client.is_shutdown() { while !self.task_client.is_shutdown() {
tokio::select! { tokio::select! {
biased;
_ = self.task_client.recv() => {
tracing::trace!("InputMessageListener: Received shutdown");
break;
}
input_msg = self.input_receiver.recv() => match input_msg { input_msg = self.input_receiver.recv() => match input_msg {
Some(input_msg) => { Some(input_msg) => {
self.on_input_message(input_msg).await; self.on_input_message(input_msg).await;
}, },
None => { None => {
tracing::trace!("InputMessageListener: Stopping since channel closed"); log::trace!("InputMessageListener: Stopping since channel closed");
break; break;
} }
}, },
_ = self.task_client.recv() => {
log::trace!("InputMessageListener: Received shutdown");
}
} }
} }
self.task_client.recv_timeout().await; self.task_client.recv_timeout().await;
tracing::debug!("InputMessageListener: Exiting"); log::debug!("InputMessageListener: Exiting");
} }
} }
@@ -13,6 +13,7 @@ use crate::client::replies::reply_controller::ReplyControllerSender;
use crate::spawn_future; use crate::spawn_future;
use action_controller::AckActionReceiver; use action_controller::AckActionReceiver;
use futures::channel::mpsc; use futures::channel::mpsc;
use log::*;
use nym_gateway_client::AcknowledgementReceiver; use nym_gateway_client::AcknowledgementReceiver;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::params::{PacketSize, PacketType}; use nym_sphinx::params::{PacketSize, PacketType};
@@ -29,7 +30,6 @@ use std::{
sync::{Arc, Weak}, sync::{Arc, Weak},
time::Duration, time::Duration,
}; };
use tracing::*;
pub(crate) use action_controller::{AckActionSender, Action}; pub(crate) use action_controller::{AckActionSender, Action};
@@ -298,44 +298,29 @@ where
let mut sent_notification_listener = self.sent_notification_listener; let mut sent_notification_listener = self.sent_notification_listener;
let mut action_controller = self.action_controller; let mut action_controller = self.action_controller;
spawn_future!( spawn_future(async move {
async move { acknowledgement_listener.run().await;
acknowledgement_listener.run().await; debug!("The acknowledgement listener has finished execution!");
debug!("The acknowledgement listener has finished execution!"); });
},
"AcknowledgementController::AcknowledgementListener"
);
spawn_future!( spawn_future(async move {
async move { input_message_listener.run().await;
input_message_listener.run().await; debug!("The input listener has finished execution!");
debug!("The input listener has finished execution!"); });
},
"AcknowledgementController::InputMessageListener"
);
spawn_future!( spawn_future(async move {
async move { retransmission_request_listener.run(packet_type).await;
retransmission_request_listener.run(packet_type).await; debug!("The retransmission request listener has finished execution!");
debug!("The retransmission request listener has finished execution!"); });
},
"AcknowledgementController::RetransmissionRequestListener"
);
spawn_future!( spawn_future(async move {
async move { sent_notification_listener.run().await;
sent_notification_listener.run().await; debug!("The sent notification listener has finished execution!");
debug!("The sent notification listener has finished execution!"); });
},
"AcknowledgementController::SentNotificationListener"
);
spawn_future!( spawn_future(async move {
async move { action_controller.run().await;
action_controller.run().await; debug!("The controller has finished execution!");
debug!("The controller has finished execution!"); });
},
"AcknowledgementController::ActionController"
);
} }
} }
@@ -10,13 +10,13 @@ use crate::client::real_messages_control::message_handler::{MessageHandler, Prep
use crate::client::real_messages_control::real_traffic_stream::RealMessage; use crate::client::real_messages_control::real_traffic_stream::RealMessage;
use crate::client::replies::reply_controller::ReplyControllerSender; use crate::client::replies::reply_controller::ReplyControllerSender;
use futures::StreamExt; use futures::StreamExt;
use log::*;
use nym_sphinx::chunking::fragment::Fragment; use nym_sphinx::chunking::fragment::Fragment;
use nym_sphinx::preparer::PreparedFragment; use nym_sphinx::preparer::PreparedFragment;
use nym_sphinx::{addressing::clients::Recipient, params::PacketType}; use nym_sphinx::{addressing::clients::Recipient, params::PacketType};
use nym_task::{connections::TransmissionLane, TaskClient}; use nym_task::{connections::TransmissionLane, TaskClient};
use rand::{CryptoRng, Rng}; use rand::{CryptoRng, Rng};
use std::sync::{Arc, Weak}; use std::sync::{Arc, Weak};
use tracing::*;
// responsible for packet retransmission upon fired timer // responsible for packet retransmission upon fired timer
pub(super) struct RetransmissionRequestListener<R> { pub(super) struct RetransmissionRequestListener<R> {
@@ -179,22 +179,19 @@ where
while !self.task_client.is_shutdown() { while !self.task_client.is_shutdown() {
tokio::select! { tokio::select! {
biased;
_ = self.task_client.recv() => {
tracing::trace!("RetransmissionRequestListener: Received shutdown");
break;
}
timed_out_ack = self.request_receiver.next() => match timed_out_ack { timed_out_ack = self.request_receiver.next() => match timed_out_ack {
Some(timed_out_ack) => self.on_retransmission_request(timed_out_ack, packet_type).await, Some(timed_out_ack) => self.on_retransmission_request(timed_out_ack, packet_type).await,
None => { None => {
tracing::trace!("RetransmissionRequestListener: Stopping since channel closed"); log::trace!("RetransmissionRequestListener: Stopping since channel closed");
break; break;
} }
}, },
_ = self.task_client.recv() => {
log::trace!("RetransmissionRequestListener: Received shutdown");
}
} }
} }
self.task_client.recv_timeout().await; self.task_client.recv_timeout().await;
tracing::debug!("RetransmissionRequestListener: Exiting"); log::debug!("RetransmissionRequestListener: Exiting");
} }
} }
@@ -4,9 +4,9 @@
use super::action_controller::{AckActionSender, Action}; use super::action_controller::{AckActionSender, Action};
use super::SentPacketNotificationReceiver; use super::SentPacketNotificationReceiver;
use futures::StreamExt; use futures::StreamExt;
use log::*;
use nym_sphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID}; use nym_sphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID};
use nym_task::TaskClient; use nym_task::TaskClient;
use tracing::*;
/// Module responsible for starting up retransmission timers. /// Module responsible for starting up retransmission timers.
/// It is required because when we send our packet to the `real traffic stream` controlled /// It is required because when we send our packet to the `real traffic stream` controlled
@@ -56,17 +56,17 @@ impl SentNotificationListener {
self.on_sent_message(frag_id).await; self.on_sent_message(frag_id).await;
} }
None => { None => {
tracing::trace!("SentNotificationListener: Stopping since channel closed"); log::trace!("SentNotificationListener: Stopping since channel closed");
break; break;
} }
}, },
_ = self.task_client.recv() => { _ = self.task_client.recv() => {
tracing::trace!("SentNotificationListener: Received shutdown"); log::trace!("SentNotificationListener: Received shutdown");
break; break;
} }
} }
} }
assert!(self.task_client.is_shutdown_poll()); assert!(self.task_client.is_shutdown_poll());
tracing::debug!("SentNotificationListener: Exiting"); log::debug!("SentNotificationListener: Exiting");
} }
} }
@@ -9,11 +9,10 @@ use crate::client::real_messages_control::{AckActionSender, Action};
use crate::client::replies::reply_controller::MaxRetransmissions; use crate::client::replies::reply_controller::MaxRetransmissions;
use crate::client::replies::reply_storage::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags}; use crate::client::replies::reply_storage::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags};
use crate::client::topology_control::{TopologyAccessor, TopologyReadPermit}; use crate::client::topology_control::{TopologyAccessor, TopologyReadPermit};
use nym_client_core_surb_storage::RetrievedReplySurb;
use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::acknowledgements::AckKey;
use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage}; use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage};
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation; use nym_sphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey};
use nym_sphinx::chunking::fragment::{Fragment, FragmentIdentifier}; use nym_sphinx::chunking::fragment::{Fragment, FragmentIdentifier};
use nym_sphinx::message::NymMessage; use nym_sphinx::message::NymMessage;
use nym_sphinx::params::{PacketSize, PacketType}; use nym_sphinx::params::{PacketSize, PacketType};
@@ -35,9 +34,6 @@ pub enum PreparationError {
#[error(transparent)] #[error(transparent)]
NymTopologyError(#[from] NymTopologyError), NymTopologyError(#[from] NymTopologyError),
#[error("message wasn't split into any fragments!")]
EmptyFragments,
#[error("message too long for a single SURB, splitting into {fragments} fragments.")] #[error("message too long for a single SURB, splitting into {fragments} fragments.")]
MessageTooLongForSingleSurb { fragments: usize }, MessageTooLongForSingleSurb { fragments: usize },
@@ -48,7 +44,7 @@ pub enum PreparationError {
} }
impl PreparationError { impl PreparationError {
fn return_surbs(self, returned_surbs: Vec<RetrievedReplySurb>) -> SurbWrappedPreparationError { fn return_surbs(self, returned_surbs: Vec<ReplySurb>) -> SurbWrappedPreparationError {
SurbWrappedPreparationError { SurbWrappedPreparationError {
source: self, source: self,
returned_surbs: Some(returned_surbs), returned_surbs: Some(returned_surbs),
@@ -62,7 +58,7 @@ pub struct SurbWrappedPreparationError {
#[source] #[source]
source: PreparationError, source: PreparationError,
returned_surbs: Option<Vec<RetrievedReplySurb>>, returned_surbs: Option<Vec<ReplySurb>>,
} }
impl<T> From<T> for SurbWrappedPreparationError impl<T> From<T> for SurbWrappedPreparationError
@@ -84,7 +80,7 @@ impl SurbWrappedPreparationError {
target: &AnonymousSenderTag, target: &AnonymousSenderTag,
) -> PreparationError { ) -> PreparationError {
if let Some(reply_surbs) = self.returned_surbs { if let Some(reply_surbs) = self.returned_surbs {
surb_storage.re_insert_reply_surbs(target, reply_surbs) surb_storage.insert_surbs(target, reply_surbs)
} }
self.source self.source
} }
@@ -106,9 +102,6 @@ pub(crate) struct Config {
/// will be routed as usual, to the entry gateway, through three mix nodes, egressing /// will be routed as usual, to the entry gateway, through three mix nodes, egressing
/// through the exit gateway. If mix hops are disabled, traffic will be routed directly /// through the exit gateway. If mix hops are disabled, traffic will be routed directly
/// from the entry gateway to the exit gateway, bypassing the mix nodes. /// from the entry gateway to the exit gateway, bypassing the mix nodes.
///
/// This overrides the `use_legacy_sphinx_format` setting as reduced mix hops
/// requires use of the updated SURB packet format.
disable_mix_hops: bool, disable_mix_hops: bool,
/// Average delay a data packet is going to get delay at a single mixnode. /// Average delay a data packet is going to get delay at a single mixnode.
@@ -163,12 +156,8 @@ impl Config {
} }
/// Configure whether messages senders using this config should use mix hops or not when sending messages. /// Configure whether messages senders using this config should use mix hops or not when sending messages.
///
/// This overrides the `use_legacy_sphinx_format` setting as disabled mix hops
/// requires use of the updated SURB packet format.
pub fn disable_mix_hops(mut self, disable_mix_hops: bool) -> Self { pub fn disable_mix_hops(mut self, disable_mix_hops: bool) -> Self {
self.disable_mix_hops = disable_mix_hops; self.disable_mix_hops = disable_mix_hops;
self.use_legacy_sphinx_format = false;
self self
} }
} }
@@ -232,10 +221,6 @@ where
} }
} }
pub(crate) fn topology_access_handle(&self) -> &TopologyAccessor {
&self.topology_access
}
fn get_or_create_sender_tag(&mut self, recipient: &Recipient) -> AnonymousSenderTag { fn get_or_create_sender_tag(&mut self, recipient: &Recipient) -> AnonymousSenderTag {
if let Some(existing) = self.tag_storage.try_get_existing(recipient) { if let Some(existing) = self.tag_storage.try_get_existing(recipient) {
trace!("we already had sender tag for {recipient}"); trace!("we already had sender tag for {recipient}");
@@ -283,10 +268,10 @@ where
} }
} }
async fn generate_reply_surbs( async fn generate_reply_surbs_with_keys(
&mut self, &mut self,
amount: usize, amount: usize,
) -> Result<Vec<ReplySurbWithKeyRotation>, PreparationError> { ) -> Result<(Vec<ReplySurb>, Vec<SurbEncryptionKey>), PreparationError> {
let topology_permit = self.topology_access.get_read_permit().await; let topology_permit = self.topology_access.get_read_permit().await;
let topology = self.get_topology(&topology_permit)?; let topology = self.get_topology(&topology_permit)?;
@@ -296,14 +281,19 @@ where
topology, topology,
)?; )?;
Ok(reply_surbs) let reply_keys = reply_surbs
.iter()
.map(|s| *s.encryption_key())
.collect::<Vec<_>>();
Ok((reply_surbs, reply_keys))
} }
pub(crate) async fn try_send_single_surb_message( pub(crate) async fn try_send_single_surb_message(
&mut self, &mut self,
target: AnonymousSenderTag, target: AnonymousSenderTag,
message: ReplyMessage, message: ReplyMessage,
reply_surb: RetrievedReplySurb, reply_surb: ReplySurb,
is_extra_surb_request: bool, is_extra_surb_request: bool,
) -> Result<(), SurbWrappedPreparationError> { ) -> Result<(), SurbWrappedPreparationError> {
let msg = NymMessage::new_reply(message); let msg = NymMessage::new_reply(message);
@@ -323,16 +313,6 @@ 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 = fragment.pop().unwrap();
let chunk_clone = chunk.clone(); let chunk_clone = chunk.clone();
let prepared_fragment = self let prepared_fragment = self
@@ -344,10 +324,7 @@ where
Some(chunk.fragment_identifier()), Some(chunk.fragment_identifier()),
); );
let delay = prepared_fragment.total_delay; let delay = prepared_fragment.total_delay;
let max_retransmissions = None;
// we have to set a maximum number of retransmissions in case we fail to retrieve
// surbs for a long period of time; we don't want to be stuck constantly resending the data
let max_retransmissions = Some(10);
let pending_ack = PendingAcknowledgement::new_anonymous( let pending_ack = PendingAcknowledgement::new_anonymous(
chunk, chunk,
delay, delay,
@@ -370,7 +347,7 @@ where
pub(crate) async fn try_request_additional_reply_surbs( pub(crate) async fn try_request_additional_reply_surbs(
&mut self, &mut self,
from: AnonymousSenderTag, from: AnonymousSenderTag,
reply_surb: RetrievedReplySurb, reply_surb: ReplySurb,
amount: u32, amount: u32,
) -> Result<(), SurbWrappedPreparationError> { ) -> Result<(), SurbWrappedPreparationError> {
debug!("requesting {amount} reply SURBs from {from}"); debug!("requesting {amount} reply SURBs from {from}");
@@ -410,9 +387,11 @@ where
&mut self, &mut self,
target: AnonymousSenderTag, target: AnonymousSenderTag,
fragments: Vec<FragmentWithMaxRetransmissions>, fragments: Vec<FragmentWithMaxRetransmissions>,
reply_surbs: impl IntoIterator<Item = RetrievedReplySurb>, reply_surbs: Vec<ReplySurb>,
lane: TransmissionLane, lane: TransmissionLane,
) -> Result<(), SurbWrappedPreparationError> { ) -> Result<(), SurbWrappedPreparationError> {
// TODO: technically this is performing an unnecessary cloning, but in the grand scheme of things
// is it really that bad?
self.try_send_reply_chunks( self.try_send_reply_chunks(
target, target,
fragments.into_iter().map(|f| (lane, f)).collect(), fragments.into_iter().map(|f| (lane, f)).collect(),
@@ -425,7 +404,7 @@ where
&mut self, &mut self,
target: AnonymousSenderTag, target: AnonymousSenderTag,
fragments: Vec<(TransmissionLane, FragmentWithMaxRetransmissions)>, fragments: Vec<(TransmissionLane, FragmentWithMaxRetransmissions)>,
reply_surbs: impl IntoIterator<Item = RetrievedReplySurb>, reply_surbs: Vec<ReplySurb>,
) -> Result<(), SurbWrappedPreparationError> { ) -> Result<(), SurbWrappedPreparationError> {
let prepared_fragments = self let prepared_fragments = self
.prepare_reply_chunks_for_sending( .prepare_reply_chunks_for_sending(
@@ -548,7 +527,6 @@ where
pending_acks.push(pending_ack); pending_acks.push(pending_ack);
} }
drop(topology_permit);
self.insert_pending_acks(pending_acks); self.insert_pending_acks(pending_acks);
self.forward_messages(real_messages, lane).await; self.forward_messages(real_messages, lane).await;
@@ -563,12 +541,8 @@ where
) -> Result<(), PreparationError> { ) -> Result<(), PreparationError> {
debug!("Sending additional reply SURBs with packet type {packet_type}"); debug!("Sending additional reply SURBs with packet type {packet_type}");
let sender_tag = self.get_or_create_sender_tag(&recipient); let sender_tag = self.get_or_create_sender_tag(&recipient);
let reply_surbs = self.generate_reply_surbs(amount as usize).await?; let (reply_surbs, reply_keys) =
self.generate_reply_surbs_with_keys(amount as usize).await?;
let reply_keys = reply_surbs
.iter()
.map(|s| *s.encryption_key())
.collect::<Vec<_>>();
let message = NymMessage::new_repliable(RepliableMessage::new_additional_surbs( let message = NymMessage::new_repliable(RepliableMessage::new_additional_surbs(
self.config.use_legacy_sphinx_format, self.config.use_legacy_sphinx_format,
@@ -588,7 +562,7 @@ where
) )
.await?; .await?;
tracing::trace!("storing {} reply keys", reply_keys.len()); log::trace!("storing {} reply keys", reply_keys.len());
self.reply_key_storage.insert_multiple(reply_keys); self.reply_key_storage.insert_multiple(reply_keys);
Ok(()) Ok(())
@@ -605,12 +579,9 @@ where
) -> Result<(), SurbWrappedPreparationError> { ) -> Result<(), SurbWrappedPreparationError> {
debug!("Sending message with reply SURBs with packet type {packet_type}"); debug!("Sending message with reply SURBs with packet type {packet_type}");
let sender_tag = self.get_or_create_sender_tag(&recipient); let sender_tag = self.get_or_create_sender_tag(&recipient);
let reply_surbs = self.generate_reply_surbs(num_reply_surbs as usize).await?; let (reply_surbs, reply_keys) = self
.generate_reply_surbs_with_keys(num_reply_surbs as usize)
let reply_keys = reply_surbs .await?;
.iter()
.map(|s| *s.encryption_key())
.collect::<Vec<_>>();
let message = NymMessage::new_repliable(RepliableMessage::new_data( let message = NymMessage::new_repliable(RepliableMessage::new_data(
self.config.use_legacy_sphinx_format, self.config.use_legacy_sphinx_format,
@@ -628,7 +599,7 @@ where
) )
.await?; .await?;
tracing::trace!("storing {} reply keys", reply_keys.len()); log::trace!("storing {} reply keys", reply_keys.len());
self.reply_key_storage.insert_multiple(reply_keys); self.reply_key_storage.insert_multiple(reply_keys);
Ok(()) Ok(())
@@ -658,12 +629,20 @@ where
pub(crate) async fn prepare_reply_chunks_for_sending( pub(crate) async fn prepare_reply_chunks_for_sending(
&mut self, &mut self,
fragments: Vec<Fragment>, fragments: Vec<Fragment>,
reply_surbs: impl IntoIterator<Item = RetrievedReplySurb>, reply_surbs: Vec<ReplySurb>,
) -> Result<Vec<PreparedFragment>, SurbWrappedPreparationError> { ) -> Result<Vec<PreparedFragment>, SurbWrappedPreparationError> {
debug_assert_eq!(
fragments.len(),
reply_surbs.len(),
"attempted to send {} fragments with {} reply surbs",
fragments.len(),
reply_surbs.len()
);
let topology_permit = self.topology_access.get_read_permit().await; let topology_permit = self.topology_access.get_read_permit().await;
let topology = match self.get_topology(&topology_permit) { let topology = match self.get_topology(&topology_permit) {
Ok(topology) => topology, Ok(topology) => topology,
Err(err) => return Err(err.return_surbs(reply_surbs.into_iter().collect())), Err(err) => return Err(err.return_surbs(reply_surbs)),
}; };
Ok(fragments Ok(fragments
@@ -671,13 +650,12 @@ where
.zip(reply_surbs.into_iter()) .zip(reply_surbs.into_iter())
.map(|(fragment, reply_surb)| { .map(|(fragment, reply_surb)| {
// unwrap here is fine as we know we have a valid topology // unwrap here is fine as we know we have a valid topology
#[allow(clippy::unwrap_used)]
self.message_preparer self.message_preparer
.prepare_reply_chunk_for_sending( .prepare_reply_chunk_for_sending(
fragment, fragment,
topology, topology,
&self.config.ack_key, &self.config.ack_key,
reply_surb.into(), reply_surb,
PacketType::Mix, PacketType::Mix,
) )
.unwrap() .unwrap()
@@ -687,7 +665,7 @@ where
pub(crate) async fn try_prepare_single_reply_chunk_for_sending( pub(crate) async fn try_prepare_single_reply_chunk_for_sending(
&mut self, &mut self,
reply_surb: RetrievedReplySurb, reply_surb: ReplySurb,
chunk: Fragment, chunk: Fragment,
) -> Result<PreparedFragment, SurbWrappedPreparationError> { ) -> Result<PreparedFragment, SurbWrappedPreparationError> {
let topology_permit = self.topology_access.get_read_permit().await; let topology_permit = self.topology_access.get_read_permit().await;
@@ -700,7 +678,7 @@ where
chunk, chunk,
topology, topology,
&self.config.ack_key, &self.config.ack_key,
reply_surb.into(), reply_surb,
PacketType::Mix, PacketType::Mix,
)?; )?;
@@ -731,21 +709,17 @@ where
// tells real message sender (with the poisson timer) to send this to the mix network // tells real message sender (with the poisson timer) to send this to the mix network
pub(crate) async fn forward_messages( pub(crate) async fn forward_messages(
&mut self, &self,
messages: Vec<RealMessage>, messages: Vec<RealMessage>,
transmission_lane: TransmissionLane, transmission_lane: TransmissionLane,
) { ) {
tokio::select! { if let Err(err) = self
biased; .real_message_sender
_ = self.task_client.recv() => { .send((messages, transmission_lane))
trace!("received shutdown while attempting to forward mixnet messages"); .await
} {
sending_res = self.real_message_sender.send((messages, transmission_lane)) => { if !self.task_client.is_shutdown_poll() {
if sending_res.is_err() { error!("Failed to forward messages to the real message sender: {err}");
error!(
"failed to forward mixnet messages due to closed channel (outside of shutdown!)"
);
}
} }
} }
} }
@@ -24,6 +24,7 @@ use crate::{
spawn_future, spawn_future,
}; };
use futures::channel::mpsc; use futures::channel::mpsc;
use log::*;
use nym_gateway_client::AcknowledgementReceiver; use nym_gateway_client::AcknowledgementReceiver;
use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::acknowledgements::AckKey;
use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::addressing::clients::Recipient;
@@ -33,9 +34,7 @@ use nym_task::connections::{ConnectionCommandReceiver, LaneQueueLengths};
use nym_task::TaskClient; use nym_task::TaskClient;
use rand::{rngs::OsRng, CryptoRng, Rng}; use rand::{rngs::OsRng, CryptoRng, Rng};
use std::sync::Arc; use std::sync::Arc;
use tracing::*;
use crate::client::replies::reply_controller::key_rotation_helpers::KeyRotationConfig;
pub(crate) use acknowledgement_control::{AckActionSender, Action}; pub(crate) use acknowledgement_control::{AckActionSender, Action};
pub(crate) mod acknowledgement_control; pub(crate) mod acknowledgement_control;
@@ -86,6 +85,12 @@ impl<'a> From<&'a Config> for real_traffic_stream::Config {
} }
} }
impl<'a> From<&'a Config> for reply_controller::Config {
fn from(cfg: &'a Config) -> Self {
reply_controller::Config::new(cfg.reply_surbs)
}
}
impl<'a> From<&'a Config> for message_handler::Config { impl<'a> From<&'a Config> for message_handler::Config {
fn from(cfg: &'a Config) -> Self { fn from(cfg: &'a Config) -> Self {
message_handler::Config::new( message_handler::Config::new(
@@ -134,7 +139,6 @@ impl RealMessagesController<OsRng> {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(crate) fn new( pub(crate) fn new(
config: Config, config: Config,
key_rotation_config: KeyRotationConfig,
ack_receiver: AcknowledgementReceiver, ack_receiver: AcknowledgementReceiver,
input_receiver: InputMessageReceiver, input_receiver: InputMessageReceiver,
mix_sender: BatchMixMessageSender, mix_sender: BatchMixMessageSender,
@@ -165,8 +169,7 @@ impl RealMessagesController<OsRng> {
// create all configs for the components // create all configs for the components
let ack_control_config = (&config).into(); let ack_control_config = (&config).into();
let out_queue_config = (&config).into(); let out_queue_config = (&config).into();
let reply_controller_config = let reply_controller_config = (&config).into();
reply_controller::Config::new(config.reply_surbs, key_rotation_config);
let message_handler_config = (&config).into(); let message_handler_config = (&config).into();
// create the actual components // create the actual components
@@ -224,20 +227,14 @@ impl RealMessagesController<OsRng> {
let ack_control = self.ack_control; let ack_control = self.ack_control;
let mut reply_control = self.reply_control; let mut reply_control = self.reply_control;
spawn_future!( spawn_future(async move {
async move { out_queue_control.run().await;
out_queue_control.run().await; debug!("The out queue controller has finished execution!");
debug!("The out queue controller has finished execution!"); });
}, spawn_future(async move {
"RealMessagesController::OutQueueControl)" reply_control.run().await;
); debug!("The reply controller has finished execution!");
spawn_future!( });
async move {
reply_control.run().await;
debug!("The reply controller has finished execution!");
},
"RealMessagesController::ReplyController"
);
ack_control.start(packet_type); ack_control.start(packet_type);
} }
@@ -9,6 +9,7 @@ use crate::client::transmission_buffer::TransmissionBuffer;
use crate::config; use crate::config;
use futures::task::{Context, Poll}; use futures::task::{Context, Poll};
use futures::{Future, Stream, StreamExt}; use futures::{Future, Stream, StreamExt};
use log::*;
use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::acknowledgements::AckKey;
use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::chunking::fragment::FragmentIdentifier; use nym_sphinx::chunking::fragment::FragmentIdentifier;
@@ -26,7 +27,6 @@ use rand::{CryptoRng, Rng};
use std::pin::Pin; use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use std::time::Duration; use std::time::Duration;
use tracing::*;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use tokio::time::{sleep, Sleep}; use tokio::time::{sleep, Sleep};
@@ -202,7 +202,7 @@ where
// well technically the message was not sent just yet, but now it's up to internal // well technically the message was not sent just yet, but now it's up to internal
// queues and client load rather than the required delay. So realistically we can treat // queues and client load rather than the required delay. So realistically we can treat
// whatever is about to happen as negligible additional delay. // whatever is about to happen as negligible additional delay.
trace!("{frag_id} is about to get sent to the mixnet"); trace!("{} is about to get sent to the mixnet", frag_id);
if let Err(err) = self.sent_notifier.unbounded_send(frag_id) { if let Err(err) = self.sent_notifier.unbounded_send(frag_id) {
error!("Failed to notify about sent message: {err}"); error!("Failed to notify about sent message: {err}");
} }
@@ -249,8 +249,6 @@ where
} }
}; };
// SAFETY: our topology must be valid at this point
#[allow(clippy::expect_used)]
( (
generate_loop_cover_packet( generate_loop_cover_packet(
&mut self.rng, &mut self.rng,
@@ -280,33 +278,17 @@ where
} }
}; };
let sending_res = tokio::select! { if let Err(err) = self.mix_tx.send(vec![next_message]).await {
biased; if !self.task_client.is_shutdown_poll() {
_ = self.task_client.recv() => { log::error!("Failed to send: {err}");
trace!("received shutdown signal while attempting to send mix message");
return
}
sending_res = self.mix_tx.send(vec![next_message]) => {
sending_res
}
};
match sending_res {
Err(_) => {
if !self.task_client.is_shutdown_poll() {
tracing::error!(
"failed to send mixnet packet due to closed channel (outside of shutdown!)"
);
}
}
Ok(_) => {
let event = if fragment_id.is_some() {
PacketStatisticsEvent::RealPacketSent(packet_size)
} else {
PacketStatisticsEvent::CoverPacketSent(packet_size)
};
self.stats_tx.report(event.into());
} }
} else {
let event = if fragment_id.is_some() {
PacketStatisticsEvent::RealPacketSent(packet_size)
} else {
PacketStatisticsEvent::CoverPacketSent(packet_size)
};
self.stats_tx.report(event.into());
} }
// notify ack controller about sending our message only after we actually managed to push it // notify ack controller about sending our message only after we actually managed to push it
@@ -331,7 +313,7 @@ where
} }
fn on_close_connection(&mut self, connection_id: ConnectionId) { fn on_close_connection(&mut self, connection_id: ConnectionId) {
tracing::debug!("Removing lane for connection: {connection_id}"); log::debug!("Removing lane for connection: {connection_id}");
self.transmission_buffer self.transmission_buffer
.remove(&TransmissionLane::ConnectionId(connection_id)); .remove(&TransmissionLane::ConnectionId(connection_id));
} }
@@ -343,7 +325,7 @@ where
fn adjust_current_average_message_sending_delay(&mut self) { fn adjust_current_average_message_sending_delay(&mut self) {
let used_slots = self.mix_tx.max_capacity() - self.mix_tx.capacity(); let used_slots = self.mix_tx.max_capacity() - self.mix_tx.capacity();
tracing::trace!( log::trace!(
"used_slots: {used_slots}, current_multiplier: {}", "used_slots: {used_slots}, current_multiplier: {}",
self.sending_delay_controller.current_multiplier() self.sending_delay_controller.current_multiplier()
); );
@@ -352,7 +334,7 @@ where
.sending_delay_controller .sending_delay_controller
.is_backpressure_currently_detected(used_slots) .is_backpressure_currently_detected(used_slots)
{ {
tracing::trace!("Backpressure detected"); log::trace!("Backpressure detected");
self.sending_delay_controller.record_backpressure_detected(); self.sending_delay_controller.record_backpressure_detected();
} }
@@ -454,11 +436,9 @@ where
Poll::Ready(None) => Poll::Ready(None), Poll::Ready(None) => Poll::Ready(None),
Poll::Ready(Some((real_messages, conn_id))) => { Poll::Ready(Some((real_messages, conn_id))) => {
tracing::trace!("handling real_messages: size: {}", real_messages.len()); log::trace!("handling real_messages: size: {}", real_messages.len());
self.transmission_buffer.store(&conn_id, real_messages); 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"); let real_next = self.pop_next_message().expect("Just stored one");
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next)))) Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
@@ -503,12 +483,10 @@ where
Poll::Ready(None) => Poll::Ready(None), Poll::Ready(None) => Poll::Ready(None),
Poll::Ready(Some((real_messages, conn_id))) => { Poll::Ready(Some((real_messages, conn_id))) => {
tracing::trace!("handling real_messages: size: {}", real_messages.len()); log::trace!("handling real_messages: size: {}", real_messages.len());
// First store what we got for the given connection id // First store what we got for the given connection id
self.transmission_buffer.store(&conn_id, real_messages); 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"); let real_next = self.pop_next_message().expect("we just added one");
Poll::Ready(Some(StreamMessage::Real(Box::new(real_next)))) Poll::Ready(Some(StreamMessage::Real(Box::new(real_next))))
@@ -560,11 +538,11 @@ where
}; };
if packets > 1000 { if packets > 1000 {
tracing::warn!("{status_str}"); log::warn!("{status_str}");
} else if packets > 0 { } else if packets > 0 {
tracing::info!("{status_str}"); log::info!("{status_str}");
} else { } else {
tracing::debug!("{status_str}"); log::debug!("{status_str}");
} }
// Send status message to whoever is listening (possibly UI) // Send status message to whoever is listening (possibly UI)
@@ -588,7 +566,7 @@ where
tokio::select! { tokio::select! {
biased; biased;
_ = shutdown.recv() => { _ = shutdown.recv() => {
tracing::trace!("OutQueueControl: Received shutdown"); log::trace!("OutQueueControl: Received shutdown");
break; break;
} }
_ = status_timer.tick() => { _ = status_timer.tick() => {
@@ -597,7 +575,7 @@ where
next_message = self.next() => if let Some(next_message) = next_message { next_message = self.next() => if let Some(next_message) = next_message {
self.on_message(next_message).await; self.on_message(next_message).await;
} else { } else {
tracing::trace!("OutQueueControl: Stopping since channel closed"); log::trace!("OutQueueControl: Stopping since channel closed");
break; break;
} }
} }
@@ -611,18 +589,18 @@ where
tokio::select! { tokio::select! {
biased; biased;
_ = shutdown.recv() => { _ = shutdown.recv() => {
tracing::trace!("OutQueueControl: Received shutdown"); log::trace!("OutQueueControl: Received shutdown");
} }
next_message = self.next() => if let Some(next_message) = next_message { next_message = self.next() => if let Some(next_message) = next_message {
self.on_message(next_message).await; self.on_message(next_message).await;
} else { } else {
tracing::trace!("OutQueueControl: Stopping since channel closed"); log::trace!("OutQueueControl: Stopping since channel closed");
break; break;
} }
} }
} }
} }
tracing::debug!("OutQueueControl: Exiting"); log::debug!("OutQueueControl: Exiting");
} }
} }
@@ -98,12 +98,12 @@ impl SendingDelayController {
self.current_multiplier = self.current_multiplier =
(self.current_multiplier + 1).clamp(self.lower_bound, self.upper_bound); (self.current_multiplier + 1).clamp(self.lower_bound, self.upper_bound);
self.time_when_changed = get_time_now(); self.time_when_changed = get_time_now();
tracing::debug!( log::debug!(
"Increasing sending delay multiplier to: {}", "Increasing sending delay multiplier to: {}",
self.current_multiplier self.current_multiplier
); );
} else { } else {
tracing::warn!("Trying to increase delay multipler higher than allowed"); log::warn!("Trying to increase delay multipler higher than allowed");
} }
} }
@@ -112,7 +112,7 @@ impl SendingDelayController {
self.current_multiplier = self.current_multiplier =
(self.current_multiplier - 1).clamp(self.lower_bound, self.upper_bound); (self.current_multiplier - 1).clamp(self.lower_bound, self.upper_bound);
self.time_when_changed = get_time_now(); self.time_when_changed = get_time_now();
tracing::debug!( log::debug!(
"Decreasing sending delay multiplier to: {}", "Decreasing sending delay multiplier to: {}",
self.current_multiplier self.current_multiplier
); );
@@ -164,11 +164,11 @@ impl SendingDelayController {
self.current_multiplier() self.current_multiplier()
); );
if self.current_multiplier() > 0 { if self.current_multiplier() > 0 {
tracing::debug!("{status_str}"); log::debug!("{}", status_str);
} else if self.current_multiplier() > 1 { } else if self.current_multiplier() > 1 {
tracing::info!("{status_str}"); log::info!("{}", status_str);
} else if self.current_multiplier() > 2 { } else if self.current_multiplier() > 2 {
tracing::warn!("{status_str}"); log::warn!("{}", status_str);
} }
self.time_when_logged_about_elevated_multiplier = now; self.time_when_logged_about_elevated_multiplier = now;
} }
@@ -8,6 +8,7 @@ use crate::spawn_future;
use futures::channel::mpsc; use futures::channel::mpsc;
use futures::lock::Mutex; use futures::lock::Mutex;
use futures::StreamExt; use futures::StreamExt;
use log::*;
use nym_crypto::asymmetric::x25519; use nym_crypto::asymmetric::x25519;
use nym_crypto::Digest; use nym_crypto::Digest;
use nym_gateway_client::MixnetMessageReceiver; use nym_gateway_client::MixnetMessageReceiver;
@@ -23,7 +24,6 @@ use nym_task::TaskClient;
use std::collections::HashSet; use std::collections::HashSet;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use tracing::*;
// The interval at which we check for stale buffers // The interval at which we check for stale buffers
const STALE_BUFFER_CHECK_INTERVAL: Duration = Duration::from_secs(10); const STALE_BUFFER_CHECK_INTERVAL: Duration = Duration::from_secs(10);
@@ -198,7 +198,6 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
} }
} }
#[allow(clippy::panic)]
async fn disconnect_sender(&mut self) { async fn disconnect_sender(&mut self) {
let mut guard = self.inner.lock().await; let mut guard = self.inner.lock().await;
if guard.message_sender.is_none() { if guard.message_sender.is_none() {
@@ -209,7 +208,6 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
guard.message_sender = None; guard.message_sender = None;
} }
#[allow(clippy::panic)]
async fn connect_sender(&mut self, sender: ReconstructedMessagesSender) { async fn connect_sender(&mut self, sender: ReconstructedMessagesSender) {
let mut guard = self.inner.lock().await; let mut guard = self.inner.lock().await;
if guard.message_sender.is_some() { if guard.message_sender.is_some() {
@@ -223,7 +221,10 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
let stored_messages = std::mem::take(&mut guard.messages); let stored_messages = std::mem::take(&mut guard.messages);
if !stored_messages.is_empty() { if !stored_messages.is_empty() {
if let Err(err) = sender.unbounded_send(stored_messages) { if let Err(err) = sender.unbounded_send(stored_messages) {
error!("The sender channel we just received is already invalidated - {err:?}"); error!(
"The sender channel we just received is already invalidated - {:?}",
err
);
// put the values back to the buffer // put the values back to the buffer
// the returned error has two fields: err: SendError and val: T, // the returned error has two fields: err: SendError and val: T,
// where val is the value that was failed to get sent; // where val is the value that was failed to get sent;
@@ -309,15 +310,13 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
} }
}; };
if !reply_surbs.is_empty() { if let Err(err) = self.reply_controller_sender.send_additional_surbs(
if let Err(err) = self.reply_controller_sender.send_additional_surbs( msg.sender_tag,
msg.sender_tag, reply_surbs,
reply_surbs, from_surb_request,
from_surb_request, ) {
) { if !self.task_client.is_shutdown_poll() {
if !self.task_client.is_shutdown_poll() { error!("{err}");
error!("{err}");
}
} }
} }
} }
@@ -501,20 +500,20 @@ impl<R: MessageReceiver> RequestReceiver<R> {
tokio::select! { tokio::select! {
biased; biased;
_ = self.task_client.recv() => { _ = self.task_client.recv() => {
tracing::trace!("RequestReceiver: Received shutdown"); log::trace!("RequestReceiver: Received shutdown");
} }
request = self.query_receiver.next() => { request = self.query_receiver.next() => {
if let Some(message) = request { if let Some(message) = request {
self.handle_message(message).await self.handle_message(message).await
} else { } else {
tracing::trace!("RequestReceiver: Stopping since channel closed"); log::trace!("RequestReceiver: Stopping since channel closed");
break; break;
} }
}, },
} }
} }
self.task_client.recv().await; self.task_client.recv().await;
tracing::debug!("RequestReceiver: Exiting"); log::debug!("RequestReceiver: Exiting");
} }
} }
@@ -545,17 +544,17 @@ impl<R: MessageReceiver> FragmentedMessageReceiver<R> {
if let Some(new_messages) = new_messages { if let Some(new_messages) = new_messages {
self.received_buffer.handle_new_received(new_messages).await?; self.received_buffer.handle_new_received(new_messages).await?;
} else { } else {
tracing::trace!("FragmentedMessageReceiver: Stopping since channel closed"); log::trace!("FragmentedMessageReceiver: Stopping since channel closed");
break; break;
} }
}, },
_ = self.task_client.recv_with_delay() => { _ = self.task_client.recv_with_delay() => {
tracing::trace!("FragmentedMessageReceiver: Received shutdown"); log::trace!("FragmentedMessageReceiver: Received shutdown");
} }
} }
} }
self.task_client.recv_timeout().await; self.task_client.recv_timeout().await;
tracing::debug!("FragmentedMessageReceiver: Exiting"); log::debug!("FragmentedMessageReceiver: Exiting");
Ok(()) Ok(())
} }
} }
@@ -601,20 +600,14 @@ impl<R: MessageReceiver + Clone + Send + 'static> ReceivedMessagesBufferControll
let mut fragmented_message_receiver = self.fragmented_message_receiver; let mut fragmented_message_receiver = self.fragmented_message_receiver;
let mut request_receiver = self.request_receiver; let mut request_receiver = self.request_receiver;
spawn_future!( spawn_future(async move {
async move { match fragmented_message_receiver.run().await {
match fragmented_message_receiver.run().await { Ok(_) => {}
Ok(_) => {} Err(e) => error!("{e}"),
Err(e) => error!("{e}"), }
} });
}, spawn_future(async move {
"ReceivedMessagesBufferController::FragmentedMessageReceiver" request_receiver.run().await;
); });
spawn_future!(
async move {
request_receiver.run().await;
},
"ReceivedMessagesBufferController::RequestReceiver"
);
} }
} }
@@ -1,169 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_topology::NymTopologyMetadata;
use nym_validator_client::models::{
EpochId, KeyRotationId, KeyRotationInfoResponse, KeyRotationState,
};
use std::time::Duration;
use time::OffsetDateTime;
#[derive(Clone, Copy)]
pub(crate) enum SurbRefreshState {
WaitingForNextRotation { last_known: KeyRotationId },
ScheduledForNextInvocation,
}
#[derive(Clone, Copy)]
pub(crate) struct ReferenceEpoch {
pub(crate) absolute_epoch_id: EpochId,
pub(crate) start_time: OffsetDateTime,
}
#[derive(Clone, Copy)]
pub(crate) struct KeyRotationConfig {
pub(crate) epoch_duration: Duration,
pub(crate) rotation_state: KeyRotationState,
pub(crate) reference_epoch: ReferenceEpoch,
}
impl From<KeyRotationInfoResponse> for KeyRotationConfig {
fn from(value: KeyRotationInfoResponse) -> Self {
KeyRotationConfig {
epoch_duration: value.details.epoch_duration,
rotation_state: value.details.key_rotation_state,
reference_epoch: ReferenceEpoch {
absolute_epoch_id: value.details.current_absolute_epoch_id,
start_time: value.details.current_epoch_start,
},
}
}
}
impl KeyRotationConfig {
pub(crate) fn rotation_lifetime(&self) -> Duration {
(self.rotation_state.validity_epochs + 1) * self.epoch_duration
}
pub(crate) fn key_rotation_id(&self, current_absolute_epoch_id: EpochId) -> KeyRotationId {
self.rotation_state
.key_rotation_id(current_absolute_epoch_id)
}
// this is called with the assumption that now is always > reference epoch start
pub(crate) fn expected_current_epoch_id(&self, now: OffsetDateTime) -> EpochId {
let diff_secs = (now - self.reference_epoch.start_time).as_seconds_f64();
let epochs = (diff_secs / self.epoch_duration.as_secs_f64()).floor() as u32;
self.reference_epoch.absolute_epoch_id + epochs
}
fn initial_rotation_epoch_start(&self) -> OffsetDateTime {
let epochs_diff = self
.reference_epoch
.absolute_epoch_id
.saturating_sub(self.rotation_state.initial_epoch_id);
self.reference_epoch.start_time - epochs_diff * self.epoch_duration
}
pub(crate) fn key_rotation_start(&self, key_rotation_id: KeyRotationId) -> OffsetDateTime {
let rotation_duration = self.rotation_state.validity_epochs * self.epoch_duration;
let initial_start = self.initial_rotation_epoch_start();
// note: key rotation starts from 0
initial_start + rotation_duration * key_rotation_id
}
pub(crate) fn expected_current_key_rotation_id(&self, now: OffsetDateTime) -> KeyRotationId {
let expected_current_epoch = self.expected_current_epoch_id(now);
self.key_rotation_id(expected_current_epoch)
}
pub(crate) fn expected_current_key_rotation_start(
&self,
now: OffsetDateTime,
) -> OffsetDateTime {
let expected_current_key_rotation_id = self.expected_current_key_rotation_id(now);
self.key_rotation_start(expected_current_key_rotation_id)
}
pub(crate) fn epoch_stuck(&self, topology_metadata: NymTopologyMetadata) -> bool {
// add leeway of 2mins each direction since transition is not instantaneous
let lower_bound = topology_metadata.refreshed_at - Duration::from_secs(2);
let upper_bound = topology_metadata.refreshed_at + Duration::from_secs(2);
let expected_epoch_lower = self.expected_current_epoch_id(lower_bound);
let expected_epoch_upper = self.expected_current_epoch_id(upper_bound);
topology_metadata.absolute_epoch_id != expected_epoch_lower
&& topology_metadata.absolute_epoch_id != expected_epoch_upper
}
}
#[cfg(test)]
mod tests {
use super::*;
use time::macros::datetime;
fn mock_config() -> KeyRotationConfig {
KeyRotationConfig {
epoch_duration: Duration::from_secs(60 * 60),
rotation_state: KeyRotationState {
validity_epochs: 10,
initial_epoch_id: 80,
},
reference_epoch: ReferenceEpoch {
absolute_epoch_id: 100,
start_time: datetime!(2025-06-30 12:00:00+00:00),
},
}
}
#[test]
fn expected_current_key_rotation_start() {
// rot0: 80-89
// rot1: 90-99
// rot2: 100-109
// rot3: 110-119
// ... etc
let cfg = mock_config();
assert_eq!(
cfg.initial_rotation_epoch_start(),
datetime!(2025-06-29 16:00:00+00:00)
);
let fake_now = datetime!(2025-06-30 12:00:00+00:00);
assert_eq!(cfg.expected_current_epoch_id(fake_now), 100);
assert_eq!(cfg.expected_current_key_rotation_id(fake_now), 2);
assert_eq!(
cfg.expected_current_key_rotation_start(fake_now),
datetime!(2025-06-30 12:00:00+00:00)
);
let fake_now = datetime!(2025-06-30 12:30:00+00:00);
assert_eq!(cfg.expected_current_epoch_id(fake_now), 100);
assert_eq!(cfg.expected_current_key_rotation_id(fake_now), 2);
assert_eq!(
cfg.expected_current_key_rotation_start(fake_now),
datetime!(2025-06-30 12:00:00+00:00)
);
let fake_now = datetime!(2025-06-30 13:01:00+00:00);
assert_eq!(cfg.expected_current_epoch_id(fake_now), 101);
assert_eq!(cfg.expected_current_key_rotation_id(fake_now), 2);
assert_eq!(
cfg.expected_current_key_rotation_start(fake_now),
datetime!(2025-06-30 12:00:00+00:00)
);
let fake_now = datetime!(2025-06-30 22:02:00+00:00);
assert_eq!(cfg.expected_current_epoch_id(fake_now), 110);
assert_eq!(cfg.expected_current_key_rotation_id(fake_now), 3);
assert_eq!(
cfg.expected_current_key_rotation_start(fake_now),
datetime!(2025-06-30 22:00:00+00:00)
);
}
}
File diff suppressed because it is too large Load Diff
@@ -1,901 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::real_messages_control::acknowledgement_control::PendingAcknowledgement;
use crate::client::real_messages_control::message_handler::{
FragmentWithMaxRetransmissions, MessageHandler, PreparationError,
};
use crate::client::replies::reply_controller::key_rotation_helpers::SurbRefreshState;
use crate::client::replies::reply_controller::Config;
use crate::client::topology_control::TopologyAccessor;
use crate::client::transmission_buffer::TransmissionBuffer;
use futures::channel::oneshot;
use nym_client_core_surb_storage::{ReceivedReplySurb, ReceivedReplySurbsMap};
use nym_crypto::aes::cipher::crypto_common::rand_core::CryptoRng;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
use nym_sphinx::chunking::fragment::FragmentIdentifier;
use nym_task::connections::{ConnectionId, TransmissionLane};
use nym_topology::NymTopologyMetadata;
use rand::Rng;
use std::cmp::{max, min};
use std::collections::btree_map::Entry;
use std::collections::{BTreeMap, HashMap};
use std::mem;
use std::sync::{Arc, Weak};
use time::OffsetDateTime;
use tracing::{debug, error, info, trace, warn};
struct SenderData {
current_clear_rerequest_counter: usize,
pending_replies: TransmissionBuffer<FragmentWithMaxRetransmissions>,
pending_retransmissions: BTreeMap<FragmentIdentifier, Weak<PendingAcknowledgement>>,
last_request_failure: OffsetDateTime,
}
impl Default for SenderData {
fn default() -> Self {
SenderData {
current_clear_rerequest_counter: 0,
pending_replies: Default::default(),
pending_retransmissions: Default::default(),
last_request_failure: OffsetDateTime::UNIX_EPOCH,
}
}
}
impl SenderData {
fn total_pending(&self) -> usize {
let pending_replies = self.pending_replies.total_size();
let pending_retransmissions = self.pending_retransmissions.len();
let total_pending = pending_retransmissions + pending_replies;
debug!("total queue size: {total_pending} = pending data {pending_replies} + pending retransmission {pending_retransmissions}");
total_pending
}
pub(crate) fn increment_current_clear_rerequest_counter(&mut self) {
self.current_clear_rerequest_counter += 1;
}
pub(crate) fn reset_current_clear_rerequest_counter(&mut self) {
self.current_clear_rerequest_counter = 0;
}
pub(crate) fn reset_last_request_failure(&mut self, now: OffsetDateTime) -> OffsetDateTime {
mem::replace(&mut self.last_request_failure, now)
}
}
/// Reply controller responsible for controlling receiver-related part
/// of replies, such as requesting additional reply SURBs
pub struct ReceiverReplyController<R> {
config: Config,
surb_refresh_state: SurbRefreshState,
topology_access: TopologyAccessor,
surb_senders: HashMap<AnonymousSenderTag, SenderData>,
unavailable: HashMap<AnonymousSenderTag, OffsetDateTime>,
surbs_storage: ReceivedReplySurbsMap,
// TODO: incorporate that field at some point
// and use binomial distribution to determine the expected required number
// of surbs required to send the message through
// expected_reliability: f32,
message_handler: MessageHandler<R>,
}
impl<R> ReceiverReplyController<R>
where
R: CryptoRng + Rng,
{
pub(crate) fn new(
config: Config,
storage: ReceivedReplySurbsMap,
message_handler: MessageHandler<R>,
) -> Self {
let topology_access = message_handler.topology_access_handle().clone();
ReceiverReplyController {
config,
surb_refresh_state: SurbRefreshState::WaitingForNextRotation {
last_known: config
.key_rotation
.expected_current_key_rotation_id(OffsetDateTime::now_utc()),
},
topology_access,
surb_senders: Default::default(),
unavailable: Default::default(),
surbs_storage: storage,
message_handler,
}
}
fn get_or_create_surb_sender(&mut self, tag: &AnonymousSenderTag) -> &mut SenderData {
self.surb_senders.entry(*tag).or_default()
}
async fn current_topology_metadata(&self) -> Option<NymTopologyMetadata> {
self.topology_access.current_metadata().await
}
fn insert_pending_replies<I: IntoIterator<Item = FragmentWithMaxRetransmissions>>(
&mut self,
recipient: &AnonymousSenderTag,
fragments: I,
lane: TransmissionLane,
) {
trace!("buffering pending replies for {recipient}");
self.surb_senders
.entry(*recipient)
.or_default()
.pending_replies
.store(&lane, fragments)
}
fn re_insert_pending_replies(
&mut self,
recipient: &AnonymousSenderTag,
fragments: Vec<(TransmissionLane, FragmentWithMaxRetransmissions)>,
) {
trace!("re-inserting pending replies for {recipient}");
// the buffer should ALWAYS exist at this point, if it doesn't, it's a bug...
self.surb_senders
.entry(*recipient)
.or_default()
.pending_replies
.store_multiple(fragments)
}
fn re_insert_pending_retransmission(
&mut self,
recipient: &AnonymousSenderTag,
data: Vec<Arc<PendingAcknowledgement>>,
) {
trace!("re-inserting pending retransmissions for {recipient}");
// 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)
.expect("our pending retransmission entry is somehow gone!")
.pending_retransmissions;
for pending in data {
// if it's 0, we don't need to do anything - we just got that ack!
if Arc::strong_count(&pending) > 1 {
let id = pending.inner_fragment_identifier();
let downgraded = Arc::downgrade(&pending);
map_entry.insert(id, downgraded);
}
}
}
fn should_request_more_surbs(&self, target: &AnonymousSenderTag) -> bool {
trace!("checking if we should request more surbs from {target}");
let total_queue = self
.surb_senders
.get(target)
.map(|pending| pending.total_pending())
.unwrap_or_default();
// only consider 'fresh' surbs
let available_surbs = self.surbs_storage.available_fresh_surbs(target);
let pending_surbs = self.surbs_storage.pending_reception(target) as usize;
let min_surbs_threshold = self.surbs_storage.min_surb_threshold();
let max_surbs_threshold = self.surbs_storage.max_surb_threshold();
let min_surbs_threshold_buffer =
self.config.reply_surbs.minimum_reply_surb_threshold_buffer;
// After clearing the queue, we want to have at least `min_surbs_threshold` surbs available
// and reserved for requesting additional surbs, and in addition to that we also want to
// have `min_surbs_threshold_buffer` surbs available proactively.
let target_surbs_after_clearing_queue = min_surbs_threshold + min_surbs_threshold_buffer;
// Check if we have enough surbs to handle the total queue and maintain minimum thresholds
let total_required_surbs = total_queue + target_surbs_after_clearing_queue;
let total_available_surbs = pending_surbs + available_surbs;
debug!("available surbs: {available_surbs} pending surbs: {pending_surbs} threshold range: {min_surbs_threshold}..+{min_surbs_threshold_buffer}..{max_surbs_threshold}");
// We should request more surbs if:
// 1. We haven't hit the maximum surb threshold, and
// 2. We don't have enough surbs to handle the queue plus minimum thresholds
let is_below_max_threshold = total_available_surbs < max_surbs_threshold;
let is_below_required_surbs = total_available_surbs < total_required_surbs;
is_below_max_threshold && is_below_required_surbs
}
pub(crate) async fn handle_send_reply(
&mut self,
recipient_tag: AnonymousSenderTag,
data: Vec<u8>,
lane: TransmissionLane,
max_retransmissions: Option<u32>,
) {
if !self.surbs_storage.contains_surbs_for(&recipient_tag) {
if self
.unavailable
.insert(recipient_tag, OffsetDateTime::now_utc())
.is_none()
{
// don't report it every single time
warn!("received reply request for {recipient_tag} but we don't have any surbs stored for that recipient!");
} else {
trace!("received reply request for {recipient_tag} but we don't have any surbs stored for that recipient!");
}
return;
}
trace!("handling reply to {recipient_tag}");
let mut fragments = self.message_handler.split_reply_message(data);
let total_size = fragments.len();
trace!("This reply requires {total_size} SURBs");
// for the purposes of sending reply, do allow using possibly stale entries
let available_surbs = self.surbs_storage.available_surbs(&recipient_tag);
let min_surbs_threshold = self.surbs_storage.min_surb_threshold();
let max_to_send = if available_surbs > min_surbs_threshold {
min(fragments.len(), available_surbs - min_surbs_threshold)
} else {
0
};
if max_to_send > 0 {
let (surbs, surbs_left) = self
.surbs_storage
.get_reply_surbs(&recipient_tag, max_to_send);
debug!(
"retrieved {} reply surbs. {surbs_left} surbs remaining in storage",
surbs.as_ref().map(|s| s.len()).unwrap_or_default()
);
if let Some(reply_surbs) = surbs {
let to_send = fragments
.drain(..reply_surbs.len())
.map(|f| FragmentWithMaxRetransmissions {
fragment: f,
max_retransmissions,
})
.collect::<Vec<_>>();
if let Err(err) = self
.message_handler
.try_send_reply_chunks_on_lane(
recipient_tag,
to_send.clone(),
reply_surbs,
lane,
)
.await
{
let err = err.return_unused_surbs(&self.surbs_storage, &recipient_tag);
warn!("failed to send reply to {recipient_tag}: {err}");
info!(
"buffering {no_fragments} fragments for {recipient_tag}",
no_fragments = to_send.len()
);
self.insert_pending_replies(&recipient_tag, to_send, lane);
}
}
}
// if there's leftover data we didn't send because we didn't have enough (or any) surbs - buffer it
if !fragments.is_empty() {
// Ideally we should have enough surbs above the minimum threshold to handle sending
// new replies without having to first request more surbs. That's why I'd like to log
// these cases as they might indicate a problem with the surb management.
debug!(
"buffering {no_fragments} fragments for {recipient_tag}",
no_fragments = fragments.len()
);
let fragments: Vec<_> = fragments
.into_iter()
.map(|fragment| FragmentWithMaxRetransmissions {
fragment,
max_retransmissions,
})
.collect();
self.insert_pending_replies(&recipient_tag, fragments, lane);
}
if self.should_request_more_surbs(&recipient_tag) {
self.request_reply_surbs_for_queue_clearing(recipient_tag)
.await;
}
}
async fn request_additional_reply_surbs(
&mut self,
target: AnonymousSenderTag,
amount: u32,
) -> Result<(), PreparationError> {
debug!("requesting {amount} additional reply surbs for {target}");
let (reply_surb, _) = self
.surbs_storage
.get_reply_surb_ignoring_threshold(&target);
let reply_surb = reply_surb.ok_or(PreparationError::NotEnoughSurbs {
available: 0,
required: 1,
})?;
if let Err(err) = self
.message_handler
.try_request_additional_reply_surbs(target, reply_surb, amount)
.await
{
let err = err.return_unused_surbs(&self.surbs_storage, &target);
warn!("failed to request additional surbs from {target}: {err}",);
return Err(err);
} else {
self.surbs_storage
.increment_pending_reception(&target, amount);
}
Ok(())
}
async fn try_clear_pending_retransmission(&mut self, target: AnonymousSenderTag) {
trace!("trying to clear pending retransmission queue");
let available_surbs = self.surbs_storage.available_surbs(&target);
let min_surbs_threshold = self.surbs_storage.min_surb_threshold();
let max_to_clear = if available_surbs > min_surbs_threshold {
available_surbs - min_surbs_threshold
} else {
trace!("we don't have enough surbs for retransmission queue clearing...");
return;
};
trace!("we can clear up to {max_to_clear} entries");
let Some(pending) = self.surb_senders.get_mut(&target) else {
trace!("no pending entry for {target}!");
return;
};
let mut to_take = Vec::new();
while to_take.len() < max_to_clear {
if let Some((_, data)) = pending.pending_retransmissions.pop_first() {
// no need to do anything if we failed to upgrade the reference,
// it means we got the ack while the data was waiting in the queue
if let Some(upgraded) = data.upgrade() {
to_take.push(upgraded)
}
} else {
// our map is empty!
break;
}
}
if to_take.is_empty() {
// no need to do anything
return;
}
let (surbs_for_reply, _) = self.surbs_storage.get_reply_surbs(&target, to_take.len());
let Some(surbs_for_reply) = surbs_for_reply else {
error!("somehow different task has stolen our reply surbs! - this should have been impossible");
self.re_insert_pending_retransmission(&target, to_take);
return;
};
let to_send_vec = to_take.iter().map(|ack| ack.fragment_data()).collect();
let prepared_fragments = match self
.message_handler
.prepare_reply_chunks_for_sending(to_send_vec, surbs_for_reply)
.await
{
Ok(prepared) => prepared,
Err(err) => {
let err = err.return_unused_surbs(&self.surbs_storage, &target);
self.re_insert_pending_retransmission(&target, to_take);
warn!("failed to clear pending retransmission queue for {target}: {err}",);
return;
}
};
// we can't fail at this point, so drop all references to acks so that timer updates wouldn't blow up
drop(to_take);
self.message_handler
.send_retransmission_reply_chunks(prepared_fragments, TransmissionLane::Retransmission)
.await;
}
fn pop_at_most_pending_replies(
&mut self,
from: &AnonymousSenderTag,
amount: usize,
) -> Option<Vec<(TransmissionLane, FragmentWithMaxRetransmissions)>> {
// if possible, pop all pending replies, if not, pop only entries for which we'd have a reply surb
let pending = self.surb_senders.get_mut(from)?;
let total = pending.pending_replies.total_size();
trace!("pending queue has {total} elements");
if total == 0 {
return None;
}
pending
.pending_replies
.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);
let min_surbs_threshold = self.surbs_storage.min_surb_threshold();
let max_to_clear = if available_surbs > min_surbs_threshold {
available_surbs - min_surbs_threshold
} else {
trace!("we don't have enough surbs for queue clearing...");
return;
};
trace!("we can clear up to {max_to_clear} entries");
// we're guaranteed to not get more entries than we have reply surbs for
if let Some(to_send) = self.pop_at_most_pending_replies(&target, max_to_clear) {
let to_send_clone = to_send.clone();
if to_send_clone.is_empty() {
panic!(
"please let the devs know if you ever see this message (reply_controller.rs)"
);
}
let (surbs_for_reply, _) = self
.surbs_storage
.get_reply_surbs(&target, to_send_clone.len());
let Some(surbs_for_reply) = surbs_for_reply else {
error!("somehow different task has stolen our reply surbs! - this should have been impossible");
self.re_insert_pending_replies(&target, to_send);
return;
};
if let Err(err) = self
.message_handler
.try_send_reply_chunks(target, to_send_clone, surbs_for_reply)
.await
{
let err = err.return_unused_surbs(&self.surbs_storage, &target);
self.re_insert_pending_replies(&target, to_send);
warn!("failed to clear pending queue for {target}: {err}");
}
} else {
trace!("the pending queue is empty");
}
}
fn reset_rerequest_counter(&mut self, from: &AnonymousSenderTag) {
if let Some(pending) = self.surb_senders.get_mut(from) {
pending.reset_current_clear_rerequest_counter()
}
}
pub(crate) async fn handle_received_surbs(
&mut self,
from: AnonymousSenderTag,
reply_surbs: Vec<ReplySurbWithKeyRotation>,
from_surb_request: bool,
) {
trace!("handling received surbs");
// clear the requesting flag since we should have been asking for surbs
if from_surb_request {
self.surbs_storage
.decrement_pending_reception(&from, reply_surbs.len() as u32);
}
// store received surbs
self.surbs_storage.insert_fresh_surbs(&from, reply_surbs);
// reset, if applicable, request counter
self.reset_rerequest_counter(&from);
// use as many as we can for clearing pending retransmission queue
self.try_clear_pending_retransmission(from).await;
// use as many as we can for clearing pending 'normal' queue
self.try_clear_pending_queue(from).await;
// if we have to, request more
if self.should_request_more_surbs(&from) {
self.request_reply_surbs_for_queue_clearing(from).await;
}
}
fn buffer_pending_ack(
&mut self,
recipient: AnonymousSenderTag,
ack_ref: Arc<PendingAcknowledgement>,
weak_ack_ref: Weak<PendingAcknowledgement>,
) {
let frag_id = ack_ref.inner_fragment_identifier();
let pending = self.surb_senders.entry(recipient).or_default();
if let Entry::Vacant(e) = pending.pending_retransmissions.entry(frag_id) {
e.insert(weak_ack_ref);
} else {
warn!(
"we're already trying to retransmit {frag_id}. We must be really behind in surbs!"
);
}
}
pub(crate) async fn handle_reply_retransmission(
&mut self,
recipient_tag: AnonymousSenderTag,
timed_out_ack: Weak<PendingAcknowledgement>,
extra_surbs_request: bool,
) {
// seems we got the ack in the end
let ack_ref = match timed_out_ack.upgrade() {
Some(ack) => ack,
None => {
debug!("we received the ack for one of the reply packets as we were putting it in the retransmission queue");
return;
}
};
// if this is retransmission for obtaining additional reply surbs,
// we can dip below the storage threshold
let (maybe_reply_surb, _) = if extra_surbs_request {
self.surbs_storage
.get_reply_surb_ignoring_threshold(&recipient_tag)
} else {
self.surbs_storage.get_reply_surb(&recipient_tag)
};
if let Some(reply_surb) = maybe_reply_surb {
match self
.message_handler
.try_prepare_single_reply_chunk_for_sending(reply_surb, ack_ref.fragment_data())
.await
{
Ok(prepared) => {
// drop the ack ref so that controller would not panic on `UpdateTimer` if that task
// got to handle the action before this function terminated (which is very much
// possible if `forward_messages` takes a while)
drop(ack_ref);
self.message_handler
.update_ack_delay(prepared.fragment_identifier, prepared.total_delay);
self.message_handler
.forward_messages(vec![prepared.into()], TransmissionLane::Retransmission)
.await;
}
Err(err) => {
let err = err.return_unused_surbs(&self.surbs_storage, &recipient_tag);
warn!("failed to prepare message for retransmission - {err}");
// we buffer that packet and to try another day
self.buffer_pending_ack(recipient_tag, ack_ref, timed_out_ack);
if self.should_request_more_surbs(&recipient_tag) {
self.request_reply_surbs_for_queue_clearing(recipient_tag)
.await;
}
}
};
} else {
self.buffer_pending_ack(recipient_tag, ack_ref, timed_out_ack);
if self.should_request_more_surbs(&recipient_tag) {
self.request_reply_surbs_for_queue_clearing(recipient_tag)
.await;
}
}
}
// to be honest this doesn't make a lot of sense in the context of `connection_id`,
// it should really be asked per tag
pub(crate) fn handle_lane_queue_length(
&self,
connection_id: ConnectionId,
response_channel: oneshot::Sender<usize>,
) {
// TODO: if we ever have duplicate ids for different senders, it means our rng is super weak
// thus I don't think we have to worry about it?
let lane = TransmissionLane::ConnectionId(connection_id);
for buf in self.surb_senders.values().map(|p| &p.pending_replies) {
if let Some(length) = buf.lane_length(&lane) {
if response_channel.send(length).is_err() {
error!("the requester for lane queue length has dropped the response channel!")
}
return;
}
}
// make sure that if we didn't find that lane, we reply with 0
if response_channel.send(0).is_err() {
error!("the requester for lane queue length has dropped the response channel!")
}
}
// TODO: modify this method to more accurately determine the amount of surbs it needs to request
// it should take into consideration the average latency, sending rate and queue size.
// it should request as many surbs as it takes to saturate its sending rate before next batch arrives
async fn request_reply_surbs_for_queue_clearing(&mut self, target: AnonymousSenderTag) {
trace!("requesting surbs for queue clearing");
let total_queue = self
.surb_senders
.get(&target)
.map(|pending| pending.total_pending() as u32)
.unwrap_or_default();
let min_surbs_buffer = self.config.reply_surbs.minimum_reply_surb_threshold_buffer as u32;
// To proactively request additional surbs, we aim to have a buffer of extra surbs in our
// storage.
let total_queue_with_buffer = total_queue + min_surbs_buffer;
let request_size = min(
self.config.reply_surbs.maximum_reply_surb_request_size,
max(
total_queue_with_buffer,
self.config.reply_surbs.minimum_reply_surb_request_size,
),
);
if let Err(err) = self
.request_additional_reply_surbs(target, request_size)
.await
{
let now = OffsetDateTime::now_utc();
let sender_info = self.get_or_create_surb_sender(&target);
let last_failure = sender_info.reset_last_request_failure(now);
// only log at higher level if it's the first time this error has occurred in a while
if now - last_failure > time::Duration::seconds(30) {
warn!("failed to request more surbs to clear pending queue of size {total_queue} (attempted to request: {request_size}): {err}")
} else {
debug!("failed to request more surbs to clear pending queue of size {total_queue} (attempted to request: {request_size}): {err}")
}
}
}
pub(crate) async fn inspect_stale_pending_data(&mut self) {
let mut to_request = Vec::new();
let mut to_remove = Vec::new();
let now = OffsetDateTime::now_utc();
for (pending_reply_target, vals) in self.surb_senders.iter_mut() {
// for now recreate old behaviour
let retransmission_buf = &vals.pending_replies;
if retransmission_buf.is_empty() {
continue;
}
let Some(last_received_time) = self
.surbs_storage
.surbs_last_received_at(pending_reply_target)
else {
error!("we have {} pending replies for {pending_reply_target}, but we somehow never received any reply surbs from them!", retransmission_buf.total_size());
to_remove.push(*pending_reply_target);
continue;
};
let diff = now - last_received_time;
let max_rerequest_wait = self
.config
.reply_surbs
.maximum_reply_surb_rerequest_waiting_period;
let max_drop_wait = self
.config
.reply_surbs
.maximum_reply_surb_drop_waiting_period;
let max_rerequests = self.config.reply_surbs.maximum_reply_surbs_rerequests;
// if we have already requested extra surbs because of the stale entry,
// don't do it again (otherwise we'll get stuck in a constant cycle of requesting more surbs
// if client is offline)
if vals.current_clear_rerequest_counter > max_rerequests {
to_remove.push(*pending_reply_target);
debug!("we have reached the maximum threshold of attempting to request surbs from {pending_reply_target}. dropping the sender");
continue;
}
if diff > max_rerequest_wait {
if diff > max_drop_wait {
to_remove.push(*pending_reply_target)
} else {
debug!("We haven't received any surbs in {} from {pending_reply_target}. Going to explicitly ask for more", humantime::format_duration(diff.unsigned_abs()));
vals.increment_current_clear_rerequest_counter();
to_request.push(*pending_reply_target);
}
}
}
for pending_reply_target in to_request {
self.request_reply_surbs_for_queue_clearing(pending_reply_target)
.await;
self.surbs_storage
.reset_pending_reception(&pending_reply_target)
}
for to_remove in to_remove {
// TODO: in the 'old' version we just removed pending messages,
// not retransmissions, but I think those should follow the same logic.
// if something breaks because of that. I guess here is your explanation, future reader
self.surb_senders.remove(&to_remove);
}
}
pub(crate) async fn check_surb_refresh(&mut self) {
let Some(current_rotation_id) = self.topology_access.current_key_rotation_id().await else {
warn!("failed to retrieve current key rotation id from the network topology");
return;
};
if let SurbRefreshState::WaitingForNextRotation { last_known } = self.surb_refresh_state {
if last_known == current_rotation_id {
trace!("no changes in key rotation id");
} else {
// key rotation actually changed and given the polling rate (1/8th epoch) we should have plenty
// of time to perform the upgrade.
// but wait for one more call before doing this so that the clients could also resync
// their topologies and discover new rotation
self.surb_refresh_state = SurbRefreshState::ScheduledForNextInvocation;
}
return;
}
// here we are in `SurbRefreshState::ScheduledForNextInvocation` state
let mut marked_as_stale = HashMap::new();
// 1. mark all existing surbs we have as possibly stale
for mut map_entry in self.surbs_storage.as_raw_iter_mut() {
let (sender, received) = map_entry.pair_mut();
let num_downgraded = received.downgrade_freshness();
trace!("{sender}: {num_downgraded} downgraded");
if num_downgraded != 0 {
marked_as_stale.insert(*sender, num_downgraded);
}
}
// 2. attempt to re-request the equivalent number of fresh surbs
// TODO PROBLEM: if our request gets lost, we might be in trouble...
// we need some sort of retry mechanism
for (sender, num_to_request) in marked_as_stale {
if self
.request_additional_reply_surbs(sender, num_to_request as u32)
.await
.is_err()
{
warn!("surb refresh request failed")
}
}
self.surb_refresh_state = SurbRefreshState::WaitingForNextRotation {
last_known: current_rotation_id,
};
}
pub(crate) async fn inspect_and_clear_stale_data(&mut self, now: OffsetDateTime) {
// technically we don't know if epoch is stuck, but we're flying in blind here,
// so we have to assume the worst and not purge anything depending on proper epoch progression
let is_epoch_stuck = self
.current_topology_metadata()
.await
.map(|m| self.config.key_rotation.epoch_stuck(m))
.unwrap_or(false);
// expected time of when the CURRENT key rotation has begun
let expected_current_key_rotation_start = self
.config
.key_rotation
.expected_current_key_rotation_start(now);
// expected ID of the CURRENT key rotation
let expected_current_key_rotation = self
.config
.key_rotation
.expected_current_key_rotation_id(now);
// time of the start of one epoch BEFORE the CURRENT rotation has begun
// this indicates the starting time of when packets with the current keys might have been constructed
let prior_epoch_start =
expected_current_key_rotation_start - self.config.key_rotation.epoch_duration;
// time of the start of one epoch AFTER the current rotation has begun
// this indicates the end of transition period and any packets constructed with keys different
// from the current one are definitely invalid
let following_epoch_start =
expected_current_key_rotation_start + self.config.key_rotation.epoch_duration;
// define a closure for validating individual surbs
// (we have to run it twice for different piles)
let basic_surb_retention_logic = |received_surb: &ReceivedReplySurb| {
if is_epoch_stuck {
let diff = now - received_surb.received_at();
return diff < self.config.key_rotation.rotation_lifetime();
}
if received_surb.received_at() < prior_epoch_start {
// it's definitely from previous rotation
return false;
}
let surb_rotation = received_surb.key_rotation();
if surb_rotation.is_unknown() {
// can't do anything, so just retain it
return true;
}
// TODO: will this backfire during transition period where we need surbs to refresh surbs
// and we failed to send a request?
if surb_rotation.is_even() && expected_current_key_rotation % 2 == 1 {
return false;
}
if surb_rotation.is_odd() && expected_current_key_rotation % 2 == 0 {
return false;
}
true
};
// 1. purge full old clients data (this applies to RECEIVER)
self.surbs_storage.retain(|_, received| {
if is_epoch_stuck {
// if epoch is stuck, we can't do much (because we don't know for certain if rotation has advanced)
// apart from the basic check of surbs being received more than maximum lifetime of a rotation
// because at that point we know they must be invalid
let diff = now - received.surbs_last_received_at();
return diff < self.config.key_rotation.rotation_lifetime();
}
// if surbs were received more than 1h before the start of the current rotation,
// they're DEFINITELY invalid.
// if it was up until 1h AFTER the start of the current rotation they MIGHT be valid -
// we don't know for sure, unless the client explicitly attached rotation information
// (which only applies to more recent versions of clients so we can't 100% rely on that)
if received.surbs_last_received_at() < prior_epoch_start {
return false;
}
// 1.1. check individual surbs (same basic logic applies)
received.retain_fresh_surbs(&basic_surb_retention_logic);
// 1.2. check the possibly stale entries
// 1.2.1. check if we're beyond the key rotation transition period,
// if so those surbs are definitely unusable
if now > following_epoch_start {
received.drop_possibly_stale_surbs();
}
// 1.2.2. otherwise continue with the same logic as the fresh ones
received.retain_possibly_stale_surbs(&basic_surb_retention_logic);
// no surbs left, we're not expecting any AND we haven't received anything in a while
// (i.e. sender probably abandoned us)
let max_drop_wait = self
.config
.reply_surbs
.maximum_reply_surb_drop_waiting_period;
let last_received = received.surbs_last_received_at();
let possibly_abandoned = last_received + max_drop_wait < now;
if received.is_empty() && received.pending_reception() == 0 && possibly_abandoned {
return false;
}
true
});
// 1.3 inspect old unavailable receivers to clear any stale data
self.unavailable
.retain(|_, last_reported| now - *last_reported < time::Duration::seconds(30));
}
}
@@ -3,12 +3,12 @@
use crate::client::real_messages_control::acknowledgement_control::PendingAcknowledgement; use crate::client::real_messages_control::acknowledgement_control::PendingAcknowledgement;
use futures::channel::{mpsc, oneshot}; use futures::channel::{mpsc, oneshot};
use log::error;
use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::addressing::clients::Recipient;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation; use nym_sphinx::anonymous_replies::ReplySurb;
use nym_task::connections::{ConnectionId, TransmissionLane}; use nym_task::connections::{ConnectionId, TransmissionLane};
use std::sync::Weak; use std::sync::Weak;
use tracing::error;
pub(crate) fn new_control_channels() -> (ReplyControllerSender, ReplyControllerReceiver) { pub(crate) fn new_control_channels() -> (ReplyControllerSender, ReplyControllerReceiver) {
let (tx, rx) = mpsc::unbounded(); let (tx, rx) = mpsc::unbounded();
@@ -81,7 +81,7 @@ impl ReplyControllerSender {
pub(crate) fn send_additional_surbs( pub(crate) fn send_additional_surbs(
&self, &self,
sender_tag: AnonymousSenderTag, sender_tag: AnonymousSenderTag,
reply_surbs: Vec<ReplySurbWithKeyRotation>, reply_surbs: Vec<ReplySurb>,
from_surb_request: bool, from_surb_request: bool,
) -> Result<(), ReplyControllerSenderError> { ) -> Result<(), ReplyControllerSenderError> {
self.0 self.0
@@ -167,7 +167,7 @@ pub enum ReplyControllerMessage {
AdditionalSurbs { AdditionalSurbs {
sender_tag: AnonymousSenderTag, sender_tag: AnonymousSenderTag,
reply_surbs: Vec<ReplySurbWithKeyRotation>, reply_surbs: Vec<ReplySurb>,
from_surb_request: bool, from_surb_request: bool,
}, },
@@ -1,101 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::real_messages_control::message_handler::MessageHandler;
use crate::client::replies::reply_controller::Config;
use nym_client_core_surb_storage::{CombinedReplyStorage, SentReplyKeys, UsedSenderTags};
use nym_crypto::aes::cipher::crypto_common::rand_core::CryptoRng;
use nym_sphinx::addressing::Recipient;
use rand::Rng;
use std::cmp::min;
use std::time::Duration;
use time::OffsetDateTime;
use tracing::{debug, trace, warn};
/// Reply controller responsible for controlling sender-related part
/// of replies, such as checking if any reply keys are stale
pub struct SenderReplyController<R> {
config: Config,
tags_storage: UsedSenderTags,
sent_reply_keys: SentReplyKeys,
message_handler: MessageHandler<R>,
}
impl<R> SenderReplyController<R>
where
R: CryptoRng + Rng,
{
pub(crate) fn new(
config: Config,
storage: &CombinedReplyStorage,
message_handler: MessageHandler<R>,
) -> Self {
SenderReplyController {
config,
tags_storage: storage.tags_storage(),
sent_reply_keys: storage.key_storage(),
message_handler,
}
}
pub(crate) async fn handle_surb_request(&mut self, recipient: Recipient, mut amount: u32) {
// 1. check whether we sent any surbs in the past to this recipient, otherwise
// they have no business in asking for more
if !self.tags_storage.exists(&recipient) {
warn!("{recipient} asked us for reply SURBs even though we never sent them any anonymous messages before!");
return;
}
// 2. check whether the requested amount is within sane range
if amount
> self
.config
.reply_surbs
.maximum_allowed_reply_surb_request_size
{
warn!("The requested reply surb amount is larger than our maximum allowed ({amount} > {}). Lowering it to a more sane value...", self.config.reply_surbs.maximum_allowed_reply_surb_request_size);
amount = self
.config
.reply_surbs
.maximum_allowed_reply_surb_request_size;
}
// 3. construct and send the surbs away
// (send them in smaller batches to make the experience a bit smoother
let mut remaining = amount;
while remaining > 0 {
let to_send = min(remaining, 100);
if let Err(err) = self
.message_handler
.try_send_additional_reply_surbs(
recipient,
to_send,
nym_sphinx::params::PacketType::Mix,
)
.await
{
warn!("failed to send additional surbs to {recipient} - {err}");
} else {
trace!("sent {to_send} reply SURBs to {recipient}");
}
remaining -= to_send;
}
}
pub(crate) fn inspect_and_clear_stale_data(&self, now: OffsetDateTime) {
// check reply keys (this applies to SENDER)
self.sent_reply_keys.retain(|_, reply_key| {
let diff = now - reply_key.sent_at;
if diff > self.config.reply_surbs.maximum_reply_key_age {
let std_diff = Duration::try_from(diff).unwrap_or_default();
let diff_formatted = humantime::format_duration(std_diff);
debug!("it's been {diff_formatted} since we created this reply key. it's probably never going to get used, so we're going to purge it...");
false
} else {
true
}
});
}
}
@@ -93,14 +93,14 @@ impl StatisticsControl {
None, None,
); );
if let Err(err) = self.report_tx.send(report_message).await { if let Err(err) = self.report_tx.send(report_message).await {
tracing::error!("Failed to report client stats: {err:?}"); log::error!("Failed to report client stats: {:?}", err);
} else { } else {
self.stats.reset(); self.stats.reset();
} }
} }
async fn run(&mut self) { async fn run(&mut self) {
tracing::debug!("Started StatisticsControl with graceful shutdown support"); log::debug!("Started StatisticsControl with graceful shutdown support");
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
let mut stats_report_interval = tokio_stream::wrappers::IntervalStream::new( let mut stats_report_interval = tokio_stream::wrappers::IntervalStream::new(
@@ -133,13 +133,13 @@ impl StatisticsControl {
tokio::select! { tokio::select! {
biased; biased;
_ = self.task_client.recv() => { _ = self.task_client.recv() => {
tracing::trace!("StatisticsControl: Received shutdown"); log::trace!("StatisticsControl: Received shutdown");
break; break;
}, },
stats_event = self.stats_rx.recv() => match stats_event { stats_event = self.stats_rx.recv() => match stats_event {
Some(stats_event) => self.stats.handle_event(stats_event), Some(stats_event) => self.stats.handle_event(stats_event),
None => { None => {
tracing::trace!("StatisticsControl: shutting down due to closed stats channel"); log::trace!("StatisticsControl: shutting down due to closed stats channel");
break; break;
} }
}, },
@@ -161,16 +161,13 @@ impl StatisticsControl {
} }
} }
} }
tracing::debug!("StatisticsControl: Exiting"); log::debug!("StatisticsControl: Exiting");
} }
pub(crate) fn start(mut self) { pub(crate) fn start(mut self) {
spawn_future!( spawn_future(async move {
async move { self.run().await;
self.run().await; })
},
"StatisticsControl"
)
} }
pub(crate) fn create_and_start( pub(crate) fn create_and_start(
@@ -2,8 +2,7 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::addressing::clients::Recipient;
use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError, NymTopologyMetadata}; use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError};
use nym_validator_client::models::KeyRotationId;
use std::ops::Deref; use std::ops::Deref;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc; use std::sync::Arc;
@@ -126,7 +125,7 @@ impl TopologyAccessor {
.map(|p| p.topology.clone()) .map(|p| p.topology.clone())
} }
pub async fn current_route_provider(&self) -> Option<RwLockReadGuard<'_, NymRouteProvider>> { pub async fn current_route_provider(&self) -> Option<RwLockReadGuard<NymRouteProvider>> {
let provider = self.inner.topology.read().await; let provider = self.inner.topology.read().await;
if provider.topology.is_empty() { if provider.topology.is_empty() {
None None
@@ -135,21 +134,6 @@ impl TopologyAccessor {
} }
} }
pub async fn current_mixnet_epoch_id(&self) -> Option<u32> {
let route_provider = self.current_route_provider().await?;
Some(route_provider.absolute_epoch_id())
}
pub async fn current_key_rotation_id(&self) -> Option<KeyRotationId> {
let route_provider = self.current_route_provider().await?;
Some(route_provider.current_key_rotation())
}
pub async fn current_metadata(&self) -> Option<NymTopologyMetadata> {
let route_provider = self.current_route_provider().await?;
Some(route_provider.metadata())
}
pub async fn manually_change_topology(&self, new_topology: NymTopology) { pub async fn manually_change_topology(&self, new_topology: NymTopology) {
self.inner.controlled_manually.store(true, Ordering::SeqCst); self.inner.controlled_manually.store(true, Ordering::SeqCst);
self.inner.update(Some(new_topology)).await; self.inner.update(Some(new_topology)).await;
@@ -4,11 +4,11 @@
use crate::spawn_future; use crate::spawn_future;
pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit}; pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit};
use futures::StreamExt; use futures::StreamExt;
use log::*;
use nym_sphinx::addressing::nodes::NodeIdentity; use nym_sphinx::addressing::nodes::NodeIdentity;
use nym_task::TaskClient; use nym_task::TaskClient;
use nym_topology::NymTopologyError; use nym_topology::NymTopologyError;
use std::time::Duration; use std::time::Duration;
use tracing::*;
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
use tokio::time::sleep; use tokio::time::sleep;
@@ -20,7 +20,7 @@ mod accessor;
pub mod nym_api_provider; pub mod nym_api_provider;
pub use nym_api_provider::{Config as NymApiTopologyProviderConfig, NymApiTopologyProvider}; pub use nym_api_provider::{Config as NymApiTopologyProviderConfig, NymApiTopologyProvider};
pub use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider}; pub use nym_topology::provider_trait::TopologyProvider;
// TODO: move it to config later // TODO: move it to config later
const MAX_FAILURE_COUNT: usize = 10; const MAX_FAILURE_COUNT: usize = 10;
@@ -145,39 +145,36 @@ impl TopologyRefresher {
} }
pub fn start(mut self) { pub fn start(mut self) {
spawn_future!( spawn_future(async move {
async move { debug!("Started TopologyRefresher with graceful shutdown support");
debug!("Started TopologyRefresher with graceful shutdown support");
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
let mut interval = tokio_stream::wrappers::IntervalStream::new( let mut interval = tokio_stream::wrappers::IntervalStream::new(tokio::time::interval(
tokio::time::interval(self.refresh_rate), self.refresh_rate,
); ));
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
let mut interval = let mut interval =
gloo_timers::future::IntervalStream::new(self.refresh_rate.as_millis() as u32); 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. // 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 // My understanding is that js setInterval does not fire immediately, so it's not
// needed there. // needed there.
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
interval.next().await; interval.next().await;
while !self.task_client.is_shutdown() { while !self.task_client.is_shutdown() {
tokio::select! { tokio::select! {
_ = interval.next() => { _ = interval.next() => {
self.try_refresh().await; self.try_refresh().await;
}, },
_ = self.task_client.recv() => { _ = self.task_client.recv() => {
tracing::trace!("TopologyRefresher: Received shutdown"); log::trace!("TopologyRefresher: Received shutdown");
}, },
}
} }
self.task_client.recv_timeout().await; }
tracing::debug!("TopologyRefresher: Exiting"); self.task_client.recv_timeout().await;
}, log::debug!("TopologyRefresher: Exiting");
"TopologyRefresher" })
)
} }
} }
@@ -2,12 +2,13 @@
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use async_trait::async_trait; use async_trait::async_trait;
use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider}; use log::{debug, error, warn};
use nym_topology::provider_trait::TopologyProvider;
use nym_topology::NymTopology; use nym_topology::NymTopology;
use nym_validator_client::UserAgent;
use rand::prelude::SliceRandom; use rand::prelude::SliceRandom;
use rand::thread_rng; use rand::thread_rng;
use std::cmp::min; use std::cmp::min;
use tracing::{debug, error, warn};
use url::Url; use url::Url;
#[derive(Debug)] #[derive(Debug)]
@@ -48,10 +49,18 @@ impl NymApiTopologyProvider {
pub fn new( pub fn new(
config: impl Into<Config>, config: impl Into<Config>,
mut nym_api_urls: Vec<Url>, mut nym_api_urls: Vec<Url>,
mut validator_client: nym_validator_client::client::NymApiClient, user_agent: Option<UserAgent>,
) -> Self { ) -> Self {
nym_api_urls.shuffle(&mut thread_rng()); nym_api_urls.shuffle(&mut thread_rng());
validator_client.change_nym_api(nym_api_urls[0].clone());
let validator_client = if let Some(user_agent) = user_agent {
nym_validator_client::client::NymApiClient::new_with_user_agent(
nym_api_urls[0].clone(),
user_agent,
)
} else {
nym_validator_client::client::NymApiClient::new(nym_api_urls[0].clone())
};
NymApiTopologyProvider { NymApiTopologyProvider {
config: config.into(), config: config.into(),
@@ -80,76 +89,55 @@ impl NymApiTopologyProvider {
let rewarded_set_fut = self.validator_client.get_current_rewarded_set(); let rewarded_set_fut = self.validator_client.get_current_rewarded_set();
let topology = if self.config.use_extended_topology { let topology = if self.config.use_extended_topology {
let all_nodes_fut = self.validator_client.get_all_basic_nodes_with_metadata(); let all_nodes_fut = self.validator_client.get_all_basic_nodes();
// Join rewarded_set_fut and all_nodes_fut concurrently // Join rewarded_set_fut and all_nodes_fut concurrently
let (rewarded_set, all_nodes_res) = futures::try_join!(rewarded_set_fut, all_nodes_fut) let (rewarded_set, all_nodes) = futures::try_join!(rewarded_set_fut, all_nodes_fut)
.inspect_err(|err| error!("failed to get network nodes: {err}")) .inspect_err(|err| error!("failed to get network nodes: {err}"))
.ok()?; .ok()?;
let metadata = all_nodes_res.metadata;
let all_nodes = all_nodes_res.nodes;
debug!( debug!(
"there are {} nodes on the network (before filtering)", "there are {} nodes on the network (before filtering)",
all_nodes.len() all_nodes.len()
); );
let nodes_filtered = all_nodes let mut topology = NymTopology::new_empty(rewarded_set);
.into_iter() topology.add_additional_nodes(all_nodes.iter().filter(|n| {
.filter(|n| n.performance.round_to_integer() >= self.config.min_node_performance()) n.performance.round_to_integer() >= self.config.min_node_performance()
.collect::<Vec<_>>(); }));
NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new()) topology
.with_skimmed_nodes(&nodes_filtered)
} else { } else {
// if we're not using extended topology, we're only getting active set mixnodes and gateways // if we're not using extended topology, we're only getting active set mixnodes and gateways
let mixnodes_fut = self let mixnodes_fut = self
.validator_client .validator_client
.get_all_basic_active_mixing_assigned_nodes_with_metadata(); .get_all_basic_active_mixing_assigned_nodes();
// TODO: we really should be getting ACTIVE gateways only // TODO: we really should be getting ACTIVE gateways only
let gateways_fut = self let gateways_fut = self.validator_client.get_all_basic_entry_assigned_nodes();
.validator_client
.get_all_basic_entry_assigned_nodes_with_metadata();
let (rewarded_set, mixnodes_res, gateways_res) = let (rewarded_set, mixnodes, gateways) =
futures::try_join!(rewarded_set_fut, mixnodes_fut, gateways_fut) futures::try_join!(rewarded_set_fut, mixnodes_fut, gateways_fut)
.inspect_err(|err| { .inspect_err(|err| {
error!("failed to get network nodes: {err}"); error!("failed to get network nodes: {err}");
}) })
.ok()?; .ok()?;
let metadata = mixnodes_res.metadata;
let mixnodes = mixnodes_res.nodes;
if !gateways_res.metadata.consistency_check(&metadata) {
warn!("inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", gateways_res.metadata);
return None;
}
let gateways = gateways_res.nodes;
debug!( debug!(
"there are {} mixnodes and {} gateways in total (before performance filtering)", "there are {} mixnodes and {} gateways in total (before performance filtering)",
mixnodes.len(), mixnodes.len(),
gateways.len() gateways.len()
); );
let mut nodes = Vec::new(); let mut topology = NymTopology::new_empty(rewarded_set);
for mix in mixnodes { topology.add_additional_nodes(mixnodes.iter().filter(|m| {
if mix.performance.round_to_integer() >= self.config.min_mixnode_performance { m.performance.round_to_integer() >= self.config.min_mixnode_performance
nodes.push(mix) }));
} topology.add_additional_nodes(gateways.iter().filter(|m| {
} m.performance.round_to_integer() >= self.config.min_gateway_performance
for gateway in gateways { }));
if gateway.performance.round_to_integer() >= self.config.min_gateway_performance {
nodes.push(gateway)
}
}
NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new()) topology
.with_skimmed_nodes(&nodes)
}; };
if !topology.is_minimally_routable() { if !topology.is_minimally_routable() {
@@ -36,18 +36,11 @@ impl SizedData for Fragment {
} }
} }
#[derive(Default)]
pub(crate) struct TransmissionBuffer<T> { pub(crate) struct TransmissionBuffer<T> {
buffer: HashMap<TransmissionLane, LaneBufferEntry<T>>, buffer: HashMap<TransmissionLane, LaneBufferEntry<T>>,
} }
impl<T> Default for TransmissionBuffer<T> {
fn default() -> Self {
TransmissionBuffer {
buffer: HashMap::new(),
}
}
}
impl<T> TransmissionBuffer<T> { impl<T> TransmissionBuffer<T> {
pub(crate) fn new() -> Self { pub(crate) fn new() -> Self {
TransmissionBuffer { TransmissionBuffer {
@@ -218,7 +211,7 @@ impl<T> TransmissionBuffer<T> {
}; };
let msg = self.pop_front_from_lane(&lane)?; let msg = self.pop_front_from_lane(&lane)?;
tracing::trace!("picking to send from lane: {lane:?}"); log::trace!("picking to send from lane: {:?}", lane);
Some((lane, msg)) Some((lane, msg))
} }
+10 -35
View File
@@ -6,10 +6,7 @@ use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
use nym_gateway_client::error::GatewayClientError; use nym_gateway_client::error::GatewayClientError;
use nym_topology::node::RoutingNodeError; use nym_topology::node::RoutingNodeError;
use nym_topology::{NodeId, NymTopologyError}; 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 nym_validator_client::ValidatorClientError;
use rand::distributions::WeightedError;
use std::error::Error; use std::error::Error;
use std::path::PathBuf; use std::path::PathBuf;
@@ -21,7 +18,7 @@ pub enum ClientCoreError {
#[error("gateway client error ({gateway_id}): {source}")] #[error("gateway client error ({gateway_id}): {source}")]
GatewayClientError { GatewayClientError {
gateway_id: String, gateway_id: String,
source: Box<GatewayClientError>, source: GatewayClientError,
}, },
#[error("custom gateway client error: {source}")] #[error("custom gateway client error: {source}")]
@@ -55,15 +52,7 @@ pub enum ClientCoreError {
#[error("list of nym apis is empty")] #[error("list of nym apis is empty")]
ListOfNymApisIsEmpty, ListOfNymApisIsEmpty,
#[error("failed to resolve a query to nym API: {source}")] #[error("the current network topology seem to be insufficient to route any packets through")]
NymApiQueryFailure {
#[from]
source: NymAPIError,
},
#[error(
"the current network topology seem to be insufficient to route any packets through:\n\t{0}"
)]
InsufficientNetworkTopology(#[from] NymTopologyError), InsufficientNetworkTopology(#[from] NymTopologyError),
#[error("experienced a failure with our reply surb persistent storage: {source}")] #[error("experienced a failure with our reply surb persistent storage: {source}")]
@@ -99,7 +88,10 @@ pub enum ClientCoreError {
}, },
#[error("failed to establish connection to gateway: {source}")] #[error("failed to establish connection to gateway: {source}")]
GatewayConnectionFailure { source: Box<tungstenite::Error> }, GatewayConnectionFailure {
#[from]
source: tungstenite::Error,
},
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
#[error("failed to establish gateway connection (wasm)")] #[error("failed to establish gateway connection (wasm)")]
@@ -174,6 +166,9 @@ pub enum ClientCoreError {
#[error("there are no gateways supporting the wss protocol available")] #[error("there are no gateways supporting the wss protocol available")]
NoWssGateways, NoWssGateways,
#[error("there are no gateways with compatible protocol versions available")]
NoGatewaysWithCompatibleProtocol,
#[error("the specified gateway '{gateway}' does not support the wss protocol")] #[error("the specified gateway '{gateway}' does not support the wss protocol")]
UnsupportedWssProtocol { gateway: String }, UnsupportedWssProtocol { gateway: String },
@@ -232,27 +227,7 @@ pub enum ClientCoreError {
UnexpectedKeyUpgrade { gateway_id: String }, UnexpectedKeyUpgrade { gateway_id: String },
#[error("failed to derive keys from master key")] #[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 {
fn from(err: tungstenite::Error) -> ClientCoreError {
ClientCoreError::GatewayConnectionFailure {
source: Box::new(err),
}
}
} }
/// Set of messages that the client can send to listeners via the task manager /// Set of messages that the client can send to listeners via the task manager
+198 -17
View File
@@ -4,8 +4,10 @@
use crate::error::ClientCoreError; use crate::error::ClientCoreError;
use crate::init::types::RegistrationResult; use crate::init::types::RegistrationResult;
use futures::{SinkExt, StreamExt}; use futures::{SinkExt, StreamExt};
use log::{debug, info, trace, warn};
use nym_crypto::asymmetric::ed25519; use nym_crypto::asymmetric::ed25519;
use nym_gateway_client::GatewayClient; use nym_gateway_client::GatewayClient;
use nym_gateway_requests::{ClientControlRequest, ServerResponse, CURRENT_PROTOCOL_VERSION};
use nym_topology::node::RoutingNode; use nym_topology::node::RoutingNode;
use nym_validator_client::client::IdentityKeyRef; use nym_validator_client::client::IdentityKeyRef;
use nym_validator_client::UserAgent; use nym_validator_client::UserAgent;
@@ -13,7 +15,6 @@ use rand::{seq::SliceRandom, Rng};
#[cfg(unix)] #[cfg(unix)]
use std::os::fd::RawFd; use std::os::fd::RawFd;
use std::{sync::Arc, time::Duration}; use std::{sync::Arc, time::Duration};
use tracing::{debug, info, trace, warn};
use tungstenite::Message; use tungstenite::Message;
use url::Url; use url::Url;
@@ -105,15 +106,12 @@ pub async fn gateways_for_init<R: Rng>(
nym_validator_client::client::NymApiClient::new(nym_api.clone()) nym_validator_client::client::NymApiClient::new(nym_api.clone())
}; };
tracing::debug!("Fetching list of gateways from: {nym_api}"); log::debug!("Fetching list of gateways from: {nym_api}");
let gateways = client let gateways = client.get_all_basic_entry_assigned_nodes().await?;
.get_all_basic_entry_assigned_nodes_with_metadata()
.await?
.nodes;
info!("nym api reports {} gateways", gateways.len()); info!("nym api reports {} gateways", gateways.len());
tracing::trace!("Gateways: {gateways:#?}"); log::trace!("Gateways: {:#?}", gateways);
// filter out gateways below minimum performance and ones that could operate as a mixnode // filter out gateways below minimum performance and ones that could operate as a mixnode
// (we don't want instability) // (we don't want instability)
@@ -123,10 +121,10 @@ pub async fn gateways_for_init<R: Rng>(
.filter(|g| g.performance.round_to_integer() >= minimum_performance) .filter(|g| g.performance.round_to_integer() >= minimum_performance)
.filter_map(|gateway| gateway.try_into().ok()) .filter_map(|gateway| gateway.try_into().ok())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
tracing::debug!("After checking validity: {}", valid_gateways.len()); log::debug!("After checking validity: {}", valid_gateways.len());
tracing::trace!("Valid gateways: {valid_gateways:#?}"); log::trace!("Valid gateways: {:#?}", valid_gateways);
tracing::info!( log::info!(
"and {} after validity and performance filtering", "and {} after validity and performance filtering",
valid_gateways.len() valid_gateways.len()
); );
@@ -134,6 +132,63 @@ pub async fn gateways_for_init<R: Rng>(
Ok(valid_gateways) Ok(valid_gateways)
} }
pub async fn gateways_for_init_with_protocol_validation<R: Rng>(
rng: &mut R,
nym_apis: &[Url],
user_agent: Option<UserAgent>,
minimum_performance: u8,
ignore_epoch_roles: bool,
) -> Result<Vec<RoutingNode>, ClientCoreError> {
// First get the initial list of gateways
let gateways = gateways_for_init(
rng,
nym_apis,
user_agent,
minimum_performance,
ignore_epoch_roles,
)
.await?;
info!(
"Checking protocol compatibility for {} gateways...",
gateways.len()
);
// Filter out gateways with invalid protocols concurrently
let validated_gateways = Arc::new(tokio::sync::Mutex::new(Vec::new()));
futures::stream::iter(&gateways)
.for_each_concurrent(CONCURRENT_GATEWAYS_MEASURED, |gateway| async {
let id = gateway.identity();
trace!("validating protocol compatibility with {id}...");
match validate_gateway_protocol(gateway).await {
Ok(()) => {
debug!("{id}: protocol check successful");
validated_gateways.lock().await.push(gateway.clone());
}
Err(err) => {
warn!("failed to check protocol for {id}: {err}");
}
}
})
.await;
let validated_gateways = validated_gateways.lock().await;
info!(
"Protocol check complete: {}/{} gateways responded successfully",
validated_gateways.len(),
gateways.len()
);
if validated_gateways.is_empty() {
return Err(ClientCoreError::NoGatewaysWithCompatibleProtocol);
}
Ok(validated_gateways.clone())
}
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
async fn connect(endpoint: &str) -> Result<WsConn, ClientCoreError> { async fn connect(endpoint: &str) -> Result<WsConn, ClientCoreError> {
match tokio::time::timeout(CONN_TIMEOUT, connect_async(endpoint)).await { match tokio::time::timeout(CONN_TIMEOUT, connect_async(endpoint)).await {
@@ -148,7 +203,7 @@ async fn connect(endpoint: &str) -> Result<WsConn, ClientCoreError> {
JSWebsocket::new(endpoint).map_err(|_| ClientCoreError::GatewayJsConnectionFailure) JSWebsocket::new(endpoint).map_err(|_| ClientCoreError::GatewayJsConnectionFailure)
} }
async fn measure_latency<G>(gateway: &G) -> Result<GatewayWithLatency<'_, G>, ClientCoreError> async fn measure_latency<G>(gateway: &G) -> Result<GatewayWithLatency<G>, ClientCoreError>
where where
G: ConnectableGateway, G: ConnectableGateway,
{ {
@@ -213,6 +268,132 @@ where
Ok(GatewayWithLatency::new(gateway, avg)) Ok(GatewayWithLatency::new(gateway, avg))
} }
async fn validate_gateway_protocol<G>(gateway: &G) -> Result<(), ClientCoreError>
where
G: ConnectableGateway,
{
let Some(addr) = gateway.clients_address(false) else {
return Err(ClientCoreError::UnsupportedEntry {
id: gateway.node_id(),
identity: gateway.identity().to_string(),
});
};
trace!(
"validating protocol compatibility with {} ({addr})...",
gateway.identity(),
);
let mut stream = connect(&addr).await?;
// Send protocol version request
let protocol_request = ClientControlRequest::SupportedProtocol {};
// Send the request as JSON text message
stream.send(Message::from(protocol_request)).await?;
// Wait for response with timeout
let protocol_timeout = Duration::from_millis(2000);
let response_future = stream.next();
match tokio::time::timeout(protocol_timeout, response_future).await {
Err(_) => {
warn!("Gateway {} protocol check timed out", gateway.identity());
Err(ClientCoreError::GatewayConnectionTimeout)
}
Ok(Some(Ok(Message::Text(response_text)))) => {
// Try to deserialize the response
let response = ServerResponse::try_from(response_text).map_err(|_| {
ClientCoreError::GatewayClientError {
gateway_id: gateway.identity().to_base58_string(),
source: *Box::new(
nym_gateway_client::error::GatewayClientError::MalformedResponse,
),
}
})?;
match response {
ServerResponse::SupportedProtocol { version } => {
debug!(
"Gateway {} supports protocol version {}, ours: {}",
gateway.identity(),
version,
CURRENT_PROTOCOL_VERSION
);
// Check protocol compatibility
if version > CURRENT_PROTOCOL_VERSION {
warn!(
"Gateway {} uses newer protocol version {} (client supports {}). \
Gateway should gracefully degrade, but consider updating your client.",
gateway.identity(),
version,
CURRENT_PROTOCOL_VERSION
);
}
trace!(
"Gateway {} protocol validation successful (gateway: v{}, client: v{})",
gateway.identity(),
version,
CURRENT_PROTOCOL_VERSION
);
Ok(())
}
ServerResponse::Error { message } => {
warn!(
"Gateway {} returned error during protocol check: {}",
gateway.identity(),
message
);
Err(ClientCoreError::GatewayClientError {
gateway_id: gateway.identity().to_base58_string(),
source: *Box::new(
nym_gateway_client::error::GatewayClientError::GatewayError(message),
),
})
}
_ => {
warn!(
"Gateway {} returned unexpected response during protocol check",
gateway.identity()
);
Err(ClientCoreError::GatewayClientError {
gateway_id: gateway.identity().to_base58_string(),
source: *Box::new(
nym_gateway_client::error::GatewayClientError::UnexpectedResponse {
name: response.name().to_string(),
},
),
})
}
}
}
Ok(Some(Ok(_))) => {
warn!(
"Gateway {} sent non-text response during protocol check",
gateway.identity()
);
Err(ClientCoreError::GatewayConnectionAbruptlyClosed)
}
Ok(Some(Err(e))) => {
warn!(
"WebSocket error during protocol check with {}: {}",
gateway.identity(),
e
);
Err(e.into())
}
Ok(None) => {
warn!(
"Gateway {} closed connection during protocol check",
gateway.identity()
);
Err(ClientCoreError::GatewayConnectionAbruptlyClosed)
}
}
}
pub async fn choose_gateway_by_latency<R: Rng, G: ConnectableGateway + Clone>( pub async fn choose_gateway_by_latency<R: Rng, G: ConnectableGateway + Clone>(
rng: &mut R, rng: &mut R,
gateways: &[G], gateways: &[G],
@@ -245,7 +426,7 @@ pub async fn choose_gateway_by_latency<R: Rng, G: ConnectableGateway + Clone>(
let gateways_with_latency = gateways_with_latency.lock().await; let gateways_with_latency = gateways_with_latency.lock().await;
let chosen = gateways_with_latency let chosen = gateways_with_latency
.choose_weighted(rng, |item| 1. / item.latency.as_secs_f32()) .choose_weighted(rng, |item| 1. / item.latency.as_secs_f32())
.map_err(|source| ClientCoreError::GatewaySelectionFailure { source })?; .expect("invalid selection weight!");
info!( info!(
"chose gateway {} with average latency of {:?}", "chose gateway {} with average latency of {:?}",
@@ -289,7 +470,7 @@ pub(super) fn get_specified_gateway(
gateways: &[RoutingNode], gateways: &[RoutingNode],
must_use_tls: bool, must_use_tls: bool,
) -> Result<RoutingNode, ClientCoreError> { ) -> Result<RoutingNode, ClientCoreError> {
tracing::debug!("Requesting specified gateway: {gateway_identity}"); log::debug!("Requesting specified gateway: {}", gateway_identity);
let user_gateway = ed25519::PublicKey::from_base58_string(gateway_identity) let user_gateway = ed25519::PublicKey::from_base58_string(gateway_identity)
.map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?; .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?;
@@ -329,20 +510,20 @@ pub(super) async fn register_with_gateway(
); );
gateway_client.establish_connection().await.map_err(|err| { gateway_client.establish_connection().await.map_err(|err| {
tracing::warn!("Failed to establish connection with gateway!"); log::warn!("Failed to establish connection with gateway!");
ClientCoreError::GatewayClientError { ClientCoreError::GatewayClientError {
gateway_id: gateway_id.to_base58_string(), gateway_id: gateway_id.to_base58_string(),
source: Box::new(err), source: err,
} }
})?; })?;
let auth_response = gateway_client let auth_response = gateway_client
.perform_initial_authentication() .perform_initial_authentication()
.await .await
.map_err(|err| { .map_err(|err| {
tracing::warn!("Failed to register with the gateway {gateway_id}: {err}"); log::warn!("Failed to register with the gateway {gateway_id}: {err}");
ClientCoreError::GatewayClientError { ClientCoreError::GatewayClientError {
gateway_id: gateway_id.to_base58_string(), gateway_id: gateway_id.to_base58_string(),
source: Box::new(err), source: err,
} }
})?; })?;
+6 -6
View File
@@ -63,7 +63,7 @@ where
K::StorageError: Send + Sync + 'static, K::StorageError: Send + Sync + 'static,
D::StorageError: Send + Sync + 'static, D::StorageError: Send + Sync + 'static,
{ {
tracing::trace!("Setting up new gateway"); log::trace!("Setting up new gateway");
// if we're setting up new gateway, we must have had generated long-term client keys before // if we're setting up new gateway, we must have had generated long-term client keys before
let client_keys = load_client_keys(key_store).await?; let client_keys = load_client_keys(key_store).await?;
@@ -202,10 +202,10 @@ where
K::StorageError: Send + Sync + 'static, K::StorageError: Send + Sync + 'static,
D::StorageError: Send + Sync + 'static, D::StorageError: Send + Sync + 'static,
{ {
tracing::debug!("Setting up gateway"); log::debug!("Setting up gateway");
match setup { match setup {
GatewaySetup::MustLoad { gateway_id } => { GatewaySetup::MustLoad { gateway_id } => {
tracing::debug!("GatewaySetup::MustLoad with id: {gateway_id:?}"); log::debug!("GatewaySetup::MustLoad with id: {gateway_id:?}");
use_loaded_gateway_details(key_store, details_store, gateway_id).await use_loaded_gateway_details(key_store, details_store, gateway_id).await
} }
GatewaySetup::New { GatewaySetup::New {
@@ -214,7 +214,7 @@ where
#[cfg(unix)] #[cfg(unix)]
connection_fd_callback, connection_fd_callback,
} => { } => {
tracing::debug!("GatewaySetup::New with spec: {specification:?}"); log::debug!("GatewaySetup::New with spec: {specification:?}");
setup_new_gateway( setup_new_gateway(
key_store, key_store,
details_store, details_store,
@@ -230,9 +230,9 @@ where
gateway_details, gateway_details,
client_keys: managed_keys, client_keys: managed_keys,
} => { } => {
tracing::debug!("GatewaySetup::ReuseConnection"); log::debug!("GatewaySetup::ReuseConnection");
Ok(reuse_gateway_connection( Ok(reuse_gateway_connection(
*authenticated_ephemeral_client, authenticated_ephemeral_client,
*gateway_details, *gateway_details,
managed_keys, managed_keys,
)) ))
+2 -2
View File
@@ -218,7 +218,7 @@ pub enum GatewaySetup {
ReuseConnection { ReuseConnection {
/// The authenticated ephemeral client that was created during `init` /// The authenticated ephemeral client that was created during `init`
authenticated_ephemeral_client: Box<InitGatewayClient>, authenticated_ephemeral_client: InitGatewayClient,
// Details of this pre-initialised client (i.e. gateway and keys) // Details of this pre-initialised client (i.e. gateway and keys)
gateway_details: Box<GatewayRegistration>, gateway_details: Box<GatewayRegistration>,
@@ -261,7 +261,7 @@ impl GatewaySetup {
pub fn try_reuse_connection(init_res: InitialisationResult) -> Result<Self, ClientCoreError> { pub fn try_reuse_connection(init_res: InitialisationResult) -> Result<Self, ClientCoreError> {
if let Some(authenticated_ephemeral_client) = init_res.authenticated_ephemeral_client { if let Some(authenticated_ephemeral_client) = init_res.authenticated_ephemeral_client {
Ok(GatewaySetup::ReuseConnection { Ok(GatewaySetup::ReuseConnection {
authenticated_ephemeral_client: Box::new(authenticated_ephemeral_client), authenticated_ephemeral_client,
gateway_details: Box::new(init_res.gateway_registration), gateway_details: Box::new(init_res.gateway_registration),
client_keys: init_res.client_keys, client_keys: init_res.client_keys,
}) })
+2 -38
View File
@@ -18,54 +18,18 @@ pub use nym_topology::{
}; };
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub fn spawn_future<F>(future: F) pub(crate) fn spawn_future<F>(future: F)
where where
F: Future<Output = ()> + 'static, F: Future<Output = ()> + 'static,
{ {
wasm_bindgen_futures::spawn_local(future); 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"))] #[cfg(not(target_arch = "wasm32"))]
#[track_caller] pub(crate) fn spawn_future<F>(future: F)
pub fn spawn_future<F>(future: F)
where where
F: Future + Send + 'static, F: Future + Send + 'static,
F::Output: Send + 'static, F::Output: Send + 'static,
{ {
tokio::spawn(future); 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)
}}
}};
}
+3 -14
View File
@@ -9,7 +9,7 @@ license.workspace = true
[dependencies] [dependencies]
async-trait.workspace = true async-trait.workspace = true
dashmap.workspace = true dashmap.workspace = true
tracing.workspace = true log.workspace = true
thiserror.workspace = true thiserror.workspace = true
time.workspace = true time.workspace = true
@@ -17,26 +17,15 @@ nym-crypto = { path = "../../crypto", optional = true, default-features = false
nym-sphinx = { path = "../../nymsphinx" } nym-sphinx = { path = "../../nymsphinx" }
nym-task = { path = "../../task" } nym-task = { path = "../../task" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
workspace = true
features = ["fs"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx] [target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
workspace = true workspace = true
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
optional = true optional = true
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
path = "../../../sqlx-pool-guard"
[build-dependencies] [build-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = [ sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
"runtime-tokio-rustls",
"sqlite",
"macros",
"migrate",
] }
[features] [features]
fs-surb-storage = ["sqlx", "nym-crypto", "nym-crypto/hashing"] fs-surb-storage = ["sqlx", "nym-crypto", "nym-crypto/hashing"]
@@ -1,8 +0,0 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
-- default value of 0 implies 'unknown' variant
ALTER TABLE reply_surb
ADD COLUMN encoded_key_rotation TINYINT NOT NULL DEFAULT 0;
@@ -1,81 +0,0 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
-- change `previous_flush_timestamp` unix timestamp to `previous_flush` timestamp
CREATE TABLE status_new
(
flush_in_progress INTEGER NOT NULL,
previous_flush TIMESTAMP WITHOUT TIME ZONE NOT NULL,
client_in_use INTEGER NOT NULL
);
INSERT INTO status_new (flush_in_progress, previous_flush, client_in_use)
SELECT flush_in_progress,
datetime(previous_flush_timestamp, 'unixepoch') AS previous_flush,
client_in_use
FROM status;
DROP TABLE status;
ALTER TABLE status_new
RENAME TO status;
-- change `sent_at_timestamp` unix timestamp to `sent_at` timestamp
CREATE TABLE reply_key_new
(
key_digest BLOB NOT NULL UNIQUE,
reply_key BLOB NOT NULL UNIQUE,
sent_at TIMESTAMP WITHOUT TIME ZONE NOT NULL
);
INSERT INTO reply_key_new (key_digest, reply_key, sent_at)
SELECT key_digest,
reply_key,
datetime(sent_at_timestamp, 'unixepoch') AS sent_at
FROM reply_key;
DROP TABLE reply_key;
ALTER TABLE reply_key_new
RENAME TO reply_key;
-- change `last_sent_timestamp` unix timestamp to `sent_at` last_sent
CREATE TABLE reply_surb_sender_new
(
id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
last_sent TIMESTAMP WITHOUT TIME ZONE NOT NULL,
tag BLOB NOT NULL UNIQUE
);
INSERT INTO reply_surb_sender_new (id, last_sent, tag)
SELECT id,
datetime(last_sent_timestamp, 'unixepoch') AS last_sent,
tag
FROM reply_surb_sender;
-- recreate `reply_surb` table due to foreign key constraint
CREATE TABLE reply_surb_new
(
reply_surb_sender_id INTEGER NOT NULL,
reply_surb BLOB NOT NULL,
encoded_key_rotation TINYINT NOT NULL,
FOREIGN KEY (reply_surb_sender_id) REFERENCES reply_surb_sender_new (id)
);
INSERT INTO reply_surb_new
SELECT *
FROM reply_surb;
DROP TABLE reply_surb;
ALTER TABLE reply_surb_new
RENAME TO reply_surb;
DROP TABLE reply_surb_sender;
ALTER TABLE reply_surb_sender_new
RENAME TO reply_surb_sender;
@@ -1,12 +0,0 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
-- don't persist sender_tag in the DB. instead generate fresh one on each restart
-- this will:
-- A) further help against correlation attacks
-- B) realistically after client restarts, we might be in new key rotation anyway meaning receiver would have to start
-- "from scratch" with surbs
DROP TABLE sender_tag;
@@ -4,7 +4,6 @@
use crate::backend::Empty; use crate::backend::Empty;
use crate::{CombinedReplyStorage, ReplyStorageBackend}; use crate::{CombinedReplyStorage, ReplyStorageBackend};
use async_trait::async_trait; use async_trait::async_trait;
use time::OffsetDateTime;
// well, right now we don't have the browser storage : ( // well, right now we don't have the browser storage : (
// so we keep everything in memory // so we keep everything in memory
@@ -39,10 +38,7 @@ impl ReplyStorageBackend for Backend {
self.empty.init_fresh(fresh).await self.empty.init_fresh(fresh).await
} }
async fn load_surb_storage( async fn load_surb_storage(&self) -> Result<CombinedReplyStorage, Self::StorageError> {
&self, self.empty.load_surb_storage().await
surb_freshness_cutoff: OffsetDateTime,
) -> Result<CombinedReplyStorage, Self::StorageError> {
self.empty.load_surb_storage(surb_freshness_cutoff).await
} }
} }
@@ -1,7 +1,8 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net> // Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use std::{io, path::PathBuf}; use std::io;
use std::path::PathBuf;
use thiserror::Error; use thiserror::Error;
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -29,6 +30,7 @@ pub enum StorageError {
#[error("failed to perform sqlx migration: {source}")] #[error("failed to perform sqlx migration: {source}")]
MigrationError { MigrationError {
#[source]
#[from] #[from]
source: sqlx::migrate::MigrateError, source: sqlx::migrate::MigrateError,
}, },
@@ -41,6 +43,7 @@ pub enum StorageError {
#[error("failed to run the SQL query: {source}")] #[error("failed to run the SQL query: {source}")]
QueryError { QueryError {
#[source]
#[from] #[from]
source: sqlx::error::Error, source: sqlx::error::Error,
}, },
@@ -3,21 +3,21 @@
use crate::backend::fs_backend::{ use crate::backend::fs_backend::{
error::StorageError, error::StorageError,
models::{ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSurbSender}, models::{
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag,
StoredSurbSender,
},
}; };
use log::{error, info};
use sqlx::{ use sqlx::{
sqlite::{SqliteAutoVacuum, SqliteSynchronous}, sqlite::{SqliteAutoVacuum, SqliteSynchronous},
ConnectOptions, ConnectOptions,
}; };
use std::path::Path; use std::path::Path;
use time::OffsetDateTime;
use tracing::{error, info};
use sqlx_pool_guard::SqlitePoolGuard;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StorageManager { pub struct StorageManager {
connection_pool: SqlitePoolGuard, pub connection_pool: sqlx::SqlitePool,
} }
// all SQL goes here // all SQL goes here
@@ -49,14 +49,11 @@ impl StorageManager {
} }
}; };
let connection_pool = SqlitePoolGuard::new(connection_pool);
if let Err(err) = sqlx::migrate!("./fs_surbs_migrations") if let Err(err) = sqlx::migrate!("./fs_surbs_migrations")
.run(&*connection_pool) .run(&connection_pool)
.await .await
{ {
error!("Failed to initialize SQLx database: {err}"); error!("Failed to initialize SQLx database: {err}");
connection_pool.close().await;
return Err(err.into()); return Err(err.into());
} }
@@ -64,60 +61,53 @@ impl StorageManager {
Ok(StorageManager { connection_pool }) Ok(StorageManager { connection_pool })
} }
/// Close connection pool waiting for all connections to be closed.
pub async fn close_pool(&self) {
self.connection_pool.close().await;
}
#[allow(dead_code)] #[allow(dead_code)]
pub async fn status_table_exists(&self) -> Result<bool, sqlx::Error> { pub async fn status_table_exists(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'") sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'")
.fetch_optional(&*self.connection_pool) .fetch_optional(&self.connection_pool)
.await .await
.map(|r| r.is_some()) .map(|r| r.is_some())
} }
pub async fn create_status_table(&self) -> Result<(), sqlx::Error> { pub async fn create_status_table(&self) -> Result<(), sqlx::Error> {
sqlx::query!( sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)")
"INSERT INTO status(flush_in_progress, previous_flush, client_in_use) VALUES (0, 0, 1)" .execute(&self.connection_pool)
) .await?;
.execute(&*self.connection_pool)
.await?;
Ok(()) Ok(())
} }
pub async fn get_flush_status(&self) -> Result<bool, sqlx::Error> { pub async fn get_flush_status(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT flush_in_progress FROM status;") sqlx::query!("SELECT flush_in_progress FROM status;")
.fetch_one(&*self.connection_pool) .fetch_one(&self.connection_pool)
.await .await
.map(|r| r.flush_in_progress > 0) .map(|r| r.flush_in_progress > 0)
} }
pub async fn set_previous_flush(&self, timestamp: OffsetDateTime) -> Result<(), sqlx::Error> { pub async fn set_previous_flush_timestamp(&self, timestamp: i64) -> Result<(), sqlx::Error> {
sqlx::query!("UPDATE status SET previous_flush = ?", timestamp) sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp)
.execute(&*self.connection_pool) .execute(&self.connection_pool)
.await?; .await?;
Ok(()) Ok(())
} }
pub async fn get_previous_flush_time(&self) -> Result<OffsetDateTime, sqlx::Error> { pub async fn get_previous_flush_timestamp(&self) -> Result<i64, sqlx::Error> {
sqlx::query!(r#"SELECT previous_flush AS "previous_flush: OffsetDateTime" FROM status"#) sqlx::query!("SELECT previous_flush_timestamp FROM status;")
.fetch_one(&*self.connection_pool) .fetch_one(&self.connection_pool)
.await .await
.map(|r| r.previous_flush) .map(|r| r.previous_flush_timestamp)
} }
pub async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> { pub async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> {
let in_progress_int = i64::from(in_progress); let in_progress_int = i64::from(in_progress);
sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int) sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int)
.execute(&*self.connection_pool) .execute(&self.connection_pool)
.await?; .await?;
Ok(()) Ok(())
} }
pub async fn get_client_in_use_status(&self) -> Result<bool, sqlx::Error> { pub async fn get_client_in_use_status(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT client_in_use FROM status;") sqlx::query!("SELECT client_in_use FROM status;")
.fetch_one(&*self.connection_pool) .fetch_one(&self.connection_pool)
.await .await
.map(|r| r.client_in_use > 0) .map(|r| r.client_in_use > 0)
} }
@@ -125,21 +115,47 @@ impl StorageManager {
pub async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> { pub async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> {
let in_use_int = i64::from(in_use); let in_use_int = i64::from(in_use);
sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int) sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int)
.execute(&*self.connection_pool) .execute(&self.connection_pool)
.await?; .await?;
Ok(()) Ok(())
} }
pub async fn delete_all_tags(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM sender_tag;")
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn get_tags(&self) -> Result<Vec<StoredSenderTag>, sqlx::Error> {
sqlx::query_as!(StoredSenderTag, "SELECT * FROM sender_tag;",)
.fetch_all(&self.connection_pool)
.await
}
pub async fn insert_tag(&self, stored_tag: StoredSenderTag) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO sender_tag(recipient, tag) VALUES (?, ?);
"#,
stored_tag.recipient,
stored_tag.tag
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> { pub async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM reply_key;") sqlx::query!("DELETE FROM reply_key;")
.execute(&*self.connection_pool) .execute(&self.connection_pool)
.await?; .await?;
Ok(()) Ok(())
} }
pub async fn get_reply_keys(&self) -> Result<Vec<StoredReplyKey>, sqlx::Error> { pub async fn get_reply_keys(&self) -> Result<Vec<StoredReplyKey>, sqlx::Error> {
sqlx::query_as("SELECT * FROM reply_key;") sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",)
.fetch_all(&*self.connection_pool) .fetch_all(&self.connection_pool)
.await .await
} }
@@ -149,20 +165,20 @@ impl StorageManager {
) -> Result<(), sqlx::Error> { ) -> Result<(), sqlx::Error> {
sqlx::query!( sqlx::query!(
r#" r#"
INSERT INTO reply_key(key_digest, reply_key, sent_at) VALUES (?, ?, ?); INSERT INTO reply_key(key_digest, reply_key, sent_at_timestamp) VALUES (?, ?, ?);
"#, "#,
stored_reply_key.key_digest, stored_reply_key.key_digest,
stored_reply_key.reply_key, stored_reply_key.reply_key,
stored_reply_key.sent_at stored_reply_key.sent_at_timestamp
) )
.execute(&*self.connection_pool) .execute(&self.connection_pool)
.await?; .await?;
Ok(()) Ok(())
} }
pub async fn get_surb_senders(&self) -> Result<Vec<StoredSurbSender>, sqlx::Error> { pub async fn get_surb_senders(&self) -> Result<Vec<StoredSurbSender>, sqlx::Error> {
sqlx::query_as("SELECT * FROM reply_surb_sender;") sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",)
.fetch_all(&*self.connection_pool) .fetch_all(&self.connection_pool)
.await .await
} }
@@ -172,12 +188,12 @@ impl StorageManager {
) -> Result<i64, sqlx::Error> { ) -> Result<i64, sqlx::Error> {
let id = sqlx::query!( let id = sqlx::query!(
r#" r#"
INSERT INTO reply_surb_sender(tag, last_sent) VALUES (?, ?); INSERT INTO reply_surb_sender(tag, last_sent_timestamp) VALUES (?, ?);
"#, "#,
stored_surb_sender.tag, stored_surb_sender.tag,
stored_surb_sender.last_sent stored_surb_sender.last_sent_timestamp
) )
.execute(&*self.connection_pool) .execute(&self.connection_pool)
.await? .await?
.last_insert_rowid(); .last_insert_rowid();
Ok(id) Ok(id)
@@ -189,23 +205,20 @@ impl StorageManager {
) -> Result<Vec<StoredReplySurb>, sqlx::Error> { ) -> Result<Vec<StoredReplySurb>, sqlx::Error> {
sqlx::query_as!( sqlx::query_as!(
StoredReplySurb, StoredReplySurb,
r#" "SELECT * FROM reply_surb WHERE reply_surb_sender_id = ?",
SELECT reply_surb_sender_id, reply_surb, encoded_key_rotation as "encoded_key_rotation: u8" FROM reply_surb
WHERE reply_surb_sender_id = ?
"#,
sender_id sender_id
) )
.fetch_all(&*self.connection_pool) .fetch_all(&self.connection_pool)
.await .await
} }
pub async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> { pub async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM reply_surb;") sqlx::query!("DELETE FROM reply_surb;")
.execute(&*self.connection_pool) .execute(&self.connection_pool)
.await?; .await?;
sqlx::query!("DELETE FROM reply_surb_sender;") sqlx::query!("DELETE FROM reply_surb_sender;")
.execute(&*self.connection_pool) .execute(&self.connection_pool)
.await?; .await?;
Ok(()) Ok(())
@@ -217,13 +230,12 @@ impl StorageManager {
) -> Result<(), sqlx::Error> { ) -> Result<(), sqlx::Error> {
sqlx::query!( sqlx::query!(
r#" r#"
INSERT INTO reply_surb(reply_surb_sender_id, reply_surb, encoded_key_rotation) VALUES (?, ?, ?); INSERT INTO reply_surb(reply_surb_sender_id, reply_surb) VALUES (?, ?);
"#, "#,
stored_reply_surb.reply_surb_sender_id, stored_reply_surb.reply_surb_sender_id,
stored_reply_surb.reply_surb, stored_reply_surb.reply_surb
stored_reply_surb.encoded_key_rotation
) )
.execute(&*self.connection_pool) .execute(&self.connection_pool)
.await?; .await?;
Ok(()) Ok(())
} }
@@ -237,7 +249,7 @@ impl StorageManager {
SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata; SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata;
"#, "#,
) )
.fetch_one(&*self.connection_pool) .fetch_one(&self.connection_pool)
.await .await
} }
@@ -251,7 +263,7 @@ impl StorageManager {
"#, "#,
metadata.min_reply_surb_threshold, metadata.min_reply_surb_threshold,
metadata.max_reply_surb_threshold, metadata.max_reply_surb_threshold,
).execute(&*self.connection_pool).await?; ).execute(&self.connection_pool).await?;
Ok(()) Ok(())
} }
} }
@@ -1,19 +1,20 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net> // Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use crate::backend::fs_backend::manager::StorageManager;
use crate::backend::fs_backend::models::{
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender,
};
use crate::surb_storage::ReceivedReplySurbs;
use crate::{ use crate::{
backend::fs_backend::{ CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags,
manager::StorageManager,
models::{ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSurbSender},
},
surb_storage::ReceivedReplySurbs,
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys,
}; };
use async_trait::async_trait; use async_trait::async_trait;
use log::{debug, error, info, warn};
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use time::OffsetDateTime; use time::OffsetDateTime;
use tracing::{error, info, warn};
pub use self::error::StorageError; pub use self::error::StorageError;
@@ -40,20 +41,21 @@ impl Backend {
} }
let manager = StorageManager::init(database_path, true).await?; let manager = StorageManager::init(database_path, true).await?;
match manager.create_status_table().await { manager.create_status_table().await?;
Ok(()) => Ok(Backend {
temporary_old_path: None, let backend = Backend {
database_path: owned_path, temporary_old_path: None,
manager, database_path: owned_path,
}), manager,
Err(err) => { };
manager.close_pool().await;
Err(err.into()) Ok(backend)
}
}
} }
pub async fn try_load<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> { pub async fn try_load<P: AsRef<Path>>(
database_path: P,
fresh_sender_tags: bool,
) -> Result<Self, StorageError> {
let owned_path: PathBuf = database_path.as_ref().into(); let owned_path: PathBuf = database_path.as_ref().into();
if owned_path.file_name().is_none() { if owned_path.file_name().is_none() {
return Err(StorageError::DatabasePathWithoutFilename { return Err(StorageError::DatabasePathWithoutFilename {
@@ -62,33 +64,15 @@ impl Backend {
} }
let manager = StorageManager::init(database_path, false).await?; let manager = StorageManager::init(database_path, false).await?;
match Self::try_load_inner(&manager).await {
Ok(()) => Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
}),
Err(e) => {
manager.close_pool().await;
Err(e)
}
}
}
/// Gracefully close sqlite connection pool and drop backend.
pub async fn shutdown(self) {
self.manager.close_pool().await
}
async fn try_load_inner(manager: &StorageManager) -> Result<(), StorageError> {
// the database flush wasn't fully finished and thus the data is in inconsistent state // the database flush wasn't fully finished and thus the data is in inconsistent state
// (we don't really know what's properly saved or what's not) // (we don't really know what's properly saved or what's not)
if manager.get_flush_status().await? { if manager.get_flush_status().await? {
return Err(StorageError::IncompleteDataFlush); return Err(StorageError::IncompleteDataFlush);
} }
let last_flush = manager.get_previous_flush_time().await?; let last_flush_timestamp = manager.get_previous_flush_timestamp().await?;
if last_flush == OffsetDateTime::UNIX_EPOCH { if last_flush_timestamp == 0 {
// either this client has been running since 1970 or the flush failed // either this client has been running since 1970 or the flush failed
return Err(StorageError::IncompleteDataFlush); return Err(StorageError::IncompleteDataFlush);
} }
@@ -108,6 +92,15 @@ impl Backend {
return Err(err.into()); return Err(err.into());
} }
let last_flush = match OffsetDateTime::from_unix_timestamp(last_flush_timestamp) {
Ok(last_flush) => last_flush,
Err(err) => {
return Err(StorageError::CorruptedData {
details: format!("failed to parse stored timestamp - {err}"),
});
}
};
// in theory clients can use our reply surbs whenever they want, even a year in the future // in theory clients can use our reply surbs whenever they want, even a year in the future
// (assuming no key rotation has happened) // (assuming no key rotation has happened)
// but the way it's currently coded, everyone will purge old data // but the way it's currently coded, everyone will purge old data
@@ -125,11 +118,28 @@ impl Backend {
manager.delete_all_reply_keys().await?; manager.delete_all_reply_keys().await?;
} }
Ok(()) if days > 2 {
info!("it's been over {days} days and {hours} hours since we last used our data store. our used sender tags are already outdated - we're going to purge them now.");
manager.delete_all_tags().await?;
} else if fresh_sender_tags {
debug!("starting with fresh sender tags");
manager.delete_all_tags().await?;
}
Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
// manager: StorageManagerState::Storage(manager),
manager,
})
}
async fn close_pool(&mut self) {
self.manager.connection_pool.close().await;
} }
async fn rotate(&mut self) -> Result<(), StorageError> { async fn rotate(&mut self) -> Result<(), StorageError> {
self.manager.close_pool().await; self.close_pool().await;
let new_extension = if let Some(existing_extension) = let new_extension = if let Some(existing_extension) =
self.database_path.extension().and_then(|ext| ext.to_str()) self.database_path.extension().and_then(|ext| ext.to_str())
@@ -142,8 +152,7 @@ impl Backend {
let mut temp_old = self.database_path.clone(); let mut temp_old = self.database_path.clone();
temp_old.set_extension(new_extension); temp_old.set_extension(new_extension);
tokio::fs::rename(&self.database_path, &temp_old) fs::rename(&self.database_path, &temp_old)
.await
.map_err(|err| StorageError::DatabaseRenameError { source: err })?; .map_err(|err| StorageError::DatabaseRenameError { source: err })?;
self.manager = StorageManager::init(&self.database_path, true).await?; self.manager = StorageManager::init(&self.database_path, true).await?;
self.manager.create_status_table().await?; self.manager.create_status_table().await?;
@@ -152,10 +161,9 @@ impl Backend {
Ok(()) Ok(())
} }
async fn remove_old(&mut self) -> Result<(), StorageError> { fn remove_old(&mut self) -> Result<(), StorageError> {
if let Some(old_path) = self.temporary_old_path.take() { if let Some(old_path) = self.temporary_old_path.take() {
tokio::fs::remove_file(old_path) fs::remove_file(old_path)
.await
.map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err }) .map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err })
} else { } else {
warn!("the old database file doesn't seem to exist!"); warn!("the old database file doesn't seem to exist!");
@@ -169,7 +177,7 @@ impl Backend {
async fn end_storage_flush(&self) -> Result<(), StorageError> { async fn end_storage_flush(&self) -> Result<(), StorageError> {
self.manager self.manager
.set_previous_flush(OffsetDateTime::now_utc()) .set_previous_flush_timestamp(OffsetDateTime::now_utc().unix_timestamp())
.await?; .await?;
Ok(self.manager.set_flush_status(false).await?) Ok(self.manager.set_flush_status(false).await?)
} }
@@ -182,6 +190,29 @@ impl Backend {
Ok(self.manager.set_client_in_use_status(false).await?) Ok(self.manager.set_client_in_use_status(false).await?)
} }
async fn get_stored_tags(&self) -> Result<UsedSenderTags, StorageError> {
let stored = self.manager.get_tags().await?;
// stop at the first instance of corruption. if even a single entry is malformed,
// something weird has happened and we can't trust the rest of the data
let raw = stored
.into_iter()
.map(TryInto::try_into)
.collect::<Result<_, _>>()?;
Ok(UsedSenderTags::from_raw(raw))
}
async fn dump_sender_tags(&self, tags: &UsedSenderTags) -> Result<(), StorageError> {
for map_ref in tags.as_raw_iter() {
let (recipient, tag) = map_ref.pair();
self.manager
.insert_tag(StoredSenderTag::new(*recipient, *tag))
.await?;
}
Ok(())
}
async fn get_stored_reply_keys(&self) -> Result<SentReplyKeys, StorageError> { async fn get_stored_reply_keys(&self) -> Result<SentReplyKeys, StorageError> {
let stored = self.manager.get_reply_keys().await?; let stored = self.manager.get_reply_keys().await?;
@@ -205,17 +236,14 @@ impl Backend {
Ok(()) Ok(())
} }
async fn get_stored_reply_surbs( async fn get_stored_reply_surbs(&self) -> Result<ReceivedReplySurbsMap, StorageError> {
&self,
surb_freshness_cutoff: OffsetDateTime,
) -> Result<ReceivedReplySurbsMap, StorageError> {
let surb_senders = self.manager.get_surb_senders().await?; let surb_senders = self.manager.get_surb_senders().await?;
let metadata = self.get_reply_surb_storage_metadata().await?; let metadata = self.get_reply_surb_storage_metadata().await?;
let mut received_surbs = Vec::with_capacity(surb_senders.len()); let mut received_surbs = Vec::with_capacity(surb_senders.len());
for sender in surb_senders { for sender in surb_senders {
let sender_id = sender.id; let sender_id = sender.id;
let (sender_tag, surbs_last_received_at): (AnonymousSenderTag, OffsetDateTime) = let (sender_tag, surbs_last_received_at_timestamp): (AnonymousSenderTag, i64) =
sender.try_into()?; sender.try_into()?;
let stored_surbs = self let stored_surbs = self
.manager .manager
@@ -227,17 +255,15 @@ impl Backend {
received_surbs.push(( received_surbs.push((
sender_tag, sender_tag,
ReceivedReplySurbs::new_retrieved(stored_surbs, surbs_last_received_at), ReceivedReplySurbs::new_retrieved(stored_surbs, surbs_last_received_at_timestamp),
)) ))
} }
let received_surbs = ReceivedReplySurbsMap::from_raw( Ok(ReceivedReplySurbsMap::from_raw(
metadata.min_reply_surb_threshold as usize, metadata.min_reply_surb_threshold as usize,
metadata.max_reply_surb_threshold as usize, metadata.max_reply_surb_threshold as usize,
received_surbs, received_surbs,
); ))
received_surbs.drop_stale_loaded_surbs(surb_freshness_cutoff);
Ok(received_surbs)
} }
async fn dump_reply_surbs( async fn dump_reply_surbs(
@@ -259,14 +285,6 @@ impl Backend {
.insert_reply_surb(StoredReplySurb::new(sender_id, reply_surb)) .insert_reply_surb(StoredReplySurb::new(sender_id, reply_surb))
.await? .await?
} }
// TODO: should we also retain the stale ones?
if received_surbs.possibly_stale_left() != 0 {
warn!(
"dropping {} possibly stale surbs for {tag}",
received_surbs.possibly_stale_left()
);
}
} }
Ok(()) Ok(())
} }
@@ -310,13 +328,14 @@ impl ReplyStorageBackend for Backend {
self.rotate().await?; self.rotate().await?;
self.start_storage_flush().await?; self.start_storage_flush().await?;
self.dump_sender_tags(storage.tags_storage_ref()).await?;
self.dump_sender_reply_keys(storage.key_storage_ref()) self.dump_sender_reply_keys(storage.key_storage_ref())
.await?; .await?;
let surbs_ref = storage.surbs_storage_ref(); let surbs_ref = storage.surbs_storage_ref();
self.dump_reply_surb_storage_metadata(surbs_ref).await?; self.dump_reply_surb_storage_metadata(surbs_ref).await?;
self.dump_reply_surbs(surbs_ref).await?; self.dump_reply_surbs(surbs_ref).await?;
self.remove_old().await?; self.remove_old()?;
self.end_storage_flush().await self.end_storage_flush().await
} }
@@ -326,14 +345,12 @@ impl ReplyStorageBackend for Backend {
.await .await
} }
async fn load_surb_storage( async fn load_surb_storage(&self) -> Result<CombinedReplyStorage, Self::StorageError> {
&self,
surb_freshness_cutoff: OffsetDateTime,
) -> Result<CombinedReplyStorage, Self::StorageError> {
let reply_keys = self.get_stored_reply_keys().await?; let reply_keys = self.get_stored_reply_keys().await?;
let reply_surbs = self.get_stored_reply_surbs(surb_freshness_cutoff).await?; let tags = self.get_stored_tags().await?;
let reply_surbs = self.get_stored_reply_surbs().await?;
Ok(CombinedReplyStorage::load(reply_keys, reply_surbs)) Ok(CombinedReplyStorage::load(reply_keys, reply_surbs, tags))
} }
async fn stop_storage_session(self) -> Result<(), Self::StorageError> { async fn stop_storage_session(self) -> Result<(), Self::StorageError> {
@@ -3,18 +3,13 @@
use crate::backend::fs_backend::error::StorageError; use crate::backend::fs_backend::error::StorageError;
use crate::key_storage::UsedReplyKey; use crate::key_storage::UsedReplyKey;
use crate::ReceivedReplySurb;
use nym_crypto::generic_array::typenum::Unsigned; use nym_crypto::generic_array::typenum::Unsigned;
use nym_crypto::Digest; use nym_crypto::Digest;
use nym_sphinx::addressing::clients::{Recipient, RecipientBytes}; use nym_sphinx::addressing::clients::{Recipient, RecipientBytes};
use nym_sphinx::anonymous_replies::encryption_key::EncryptionKeyDigest; use nym_sphinx::anonymous_replies::encryption_key::EncryptionKeyDigest;
use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, SENDER_TAG_SIZE}; use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, SENDER_TAG_SIZE};
use nym_sphinx::anonymous_replies::{ use nym_sphinx::anonymous_replies::{ReplySurb, SurbEncryptionKey, SurbEncryptionKeySize};
ReplySurb, ReplySurbWithKeyRotation, SurbEncryptionKey, SurbEncryptionKeySize, use nym_sphinx::params::ReplySurbKeyDigestAlgorithm;
};
use nym_sphinx::params::{ReplySurbKeyDigestAlgorithm, SphinxKeyRotation};
use sqlx::FromRow;
use time::OffsetDateTime;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct StoredSenderTag { pub struct StoredSenderTag {
@@ -61,11 +56,11 @@ impl TryFrom<StoredSenderTag> for (RecipientBytes, AnonymousSenderTag) {
} }
} }
#[derive(Debug, Clone, FromRow)] #[derive(Debug, Clone)]
pub struct StoredReplyKey { pub struct StoredReplyKey {
pub key_digest: Vec<u8>, pub key_digest: Vec<u8>,
pub reply_key: Vec<u8>, pub reply_key: Vec<u8>,
pub sent_at: OffsetDateTime, pub sent_at_timestamp: i64,
} }
impl StoredReplyKey { impl StoredReplyKey {
@@ -73,7 +68,7 @@ impl StoredReplyKey {
StoredReplyKey { StoredReplyKey {
key_digest: key_digest.to_vec(), key_digest: key_digest.to_vec(),
reply_key: (*reply_key).to_bytes(), reply_key: (*reply_key).to_bytes(),
sent_at: reply_key.sent_at, sent_at_timestamp: reply_key.sent_at_timestamp,
} }
} }
} }
@@ -103,30 +98,32 @@ impl TryFrom<StoredReplyKey> for (EncryptionKeyDigest, UsedReplyKey) {
}); });
}; };
Ok((digest, UsedReplyKey::new(reply_key, value.sent_at))) Ok((
digest,
UsedReplyKey::new(reply_key, value.sent_at_timestamp),
))
} }
} }
#[derive(FromRow)]
pub struct StoredSurbSender { pub struct StoredSurbSender {
pub id: i64, pub id: i64,
pub tag: Vec<u8>, pub tag: Vec<u8>,
pub last_sent: OffsetDateTime, pub last_sent_timestamp: i64,
} }
impl StoredSurbSender { impl StoredSurbSender {
pub fn new(tag: AnonymousSenderTag, last_sent: OffsetDateTime) -> Self { pub fn new(tag: AnonymousSenderTag, last_sent_timestamp: i64) -> Self {
StoredSurbSender { StoredSurbSender {
// for the purposes of STORING data, // for the purposes of STORING data,
// we ignore that field anyway // we ignore that field anyway
id: 0, id: 0,
tag: tag.to_bytes().to_vec(), tag: tag.to_bytes().to_vec(),
last_sent, last_sent_timestamp,
} }
} }
} }
impl TryFrom<StoredSurbSender> for (AnonymousSenderTag, OffsetDateTime) { impl TryFrom<StoredSurbSender> for (AnonymousSenderTag, i64) {
type Error = StorageError; type Error = StorageError;
fn try_from(value: StoredSurbSender) -> Result<Self, Self::Error> { fn try_from(value: StoredSurbSender) -> Result<Self, Self::Error> {
@@ -141,7 +138,7 @@ impl TryFrom<StoredSurbSender> for (AnonymousSenderTag, OffsetDateTime) {
Ok(( Ok((
AnonymousSenderTag::from_bytes(sender_tag_bytes), AnonymousSenderTag::from_bytes(sender_tag_bytes),
value.last_sent, value.last_sent_timestamp,
)) ))
} }
} }
@@ -149,40 +146,24 @@ impl TryFrom<StoredSurbSender> for (AnonymousSenderTag, OffsetDateTime) {
pub struct StoredReplySurb { pub struct StoredReplySurb {
pub reply_surb_sender_id: i64, pub reply_surb_sender_id: i64,
pub reply_surb: Vec<u8>, pub reply_surb: Vec<u8>,
// encodes only whether it's 'even', 'odd' or 'unknown' (default)
// and not the whole id because that's redundant
pub encoded_key_rotation: u8,
} }
impl StoredReplySurb { impl StoredReplySurb {
pub fn new(reply_surb_sender_id: i64, reply_surb: &ReceivedReplySurb) -> Self { pub fn new(reply_surb_sender_id: i64, reply_surb: &ReplySurb) -> Self {
StoredReplySurb { StoredReplySurb {
reply_surb_sender_id, reply_surb_sender_id,
reply_surb: reply_surb.surb.inner_reply_surb().to_bytes(), reply_surb: reply_surb.to_bytes(),
encoded_key_rotation: reply_surb.key_rotation() as u8,
} }
} }
} }
impl TryFrom<StoredReplySurb> for ReplySurbWithKeyRotation { impl TryFrom<StoredReplySurb> for ReplySurb {
type Error = StorageError; type Error = StorageError;
fn try_from(value: StoredReplySurb) -> Result<Self, Self::Error> { fn try_from(value: StoredReplySurb) -> Result<Self, Self::Error> {
let key_rotation = ReplySurb::from_bytes(&value.reply_surb).map_err(|err| StorageError::CorruptedData {
SphinxKeyRotation::try_from(value.encoded_key_rotation).map_err(|err| { details: format!("failed to recover the reply surb: {err}"),
StorageError::CorruptedData { })
details: format!("stored key rotation was malformed: {err}"),
}
})?;
let reply_surb = ReplySurb::from_bytes(&value.reply_surb).map_err(|err| {
StorageError::CorruptedData {
details: format!("failed to recover the reply surb: {err}"),
}
})?;
Ok(reply_surb.with_key_rotation(key_rotation))
} }
} }
@@ -5,7 +5,6 @@ use crate::CombinedReplyStorage;
use async_trait::async_trait; use async_trait::async_trait;
use std::error::Error; use std::error::Error;
use thiserror::Error; use thiserror::Error;
use time::OffsetDateTime;
// TODO: this should now live inside our wasm/client-core // TODO: this should now live inside our wasm/client-core
pub mod browser_backend; pub mod browser_backend;
@@ -54,10 +53,7 @@ impl ReplyStorageBackend for Empty {
Ok(()) Ok(())
} }
async fn load_surb_storage( async fn load_surb_storage(&self) -> Result<CombinedReplyStorage, Self::StorageError> {
&self,
_: OffsetDateTime,
) -> Result<CombinedReplyStorage, Self::StorageError> {
Ok(CombinedReplyStorage::new( Ok(CombinedReplyStorage::new(
self.min_surb_threshold, self.min_surb_threshold,
self.max_surb_threshold, self.max_surb_threshold,
@@ -84,10 +80,7 @@ pub trait ReplyStorageBackend: Sized {
/// (such as surb thresholds) /// (such as surb thresholds)
async fn init_fresh(&mut self, fresh: &CombinedReplyStorage) -> Result<(), Self::StorageError>; async fn init_fresh(&mut self, fresh: &CombinedReplyStorage) -> Result<(), Self::StorageError>;
async fn load_surb_storage( async fn load_surb_storage(&self) -> Result<CombinedReplyStorage, Self::StorageError>;
&self,
surb_freshness_cutoff: OffsetDateTime,
) -> Result<CombinedReplyStorage, Self::StorageError>;
async fn stop_storage_session(self) -> Result<(), Self::StorageError> { async fn stop_storage_session(self) -> Result<(), Self::StorageError> {
Ok(()) Ok(())
@@ -25,11 +25,12 @@ impl CombinedReplyStorage {
pub fn load( pub fn load(
sent_reply_keys: SentReplyKeys, sent_reply_keys: SentReplyKeys,
received_reply_surbs: ReceivedReplySurbsMap, received_reply_surbs: ReceivedReplySurbsMap,
used_tags: UsedSenderTags,
) -> Self { ) -> Self {
CombinedReplyStorage { CombinedReplyStorage {
sent_reply_keys, sent_reply_keys,
received_reply_surbs, received_reply_surbs,
used_tags: UsedSenderTags::new(), used_tags,
} }
} }
@@ -47,12 +47,8 @@ impl SentReplyKeys {
self.inner.data.iter() self.inner.data.iter()
} }
pub fn retain(&self, f: impl FnMut(&EncryptionKeyDigest, &mut UsedReplyKey) -> bool) {
self.inner.data.retain(f);
}
pub fn insert_multiple(&self, keys: Vec<SurbEncryptionKey>) { pub fn insert_multiple(&self, keys: Vec<SurbEncryptionKey>) {
let now = OffsetDateTime::now_utc(); let now = OffsetDateTime::now_utc().unix_timestamp();
for key in keys { for key in keys {
self.insert(UsedReplyKey::new(key, now)) self.insert(UsedReplyKey::new(key, now))
} }
@@ -75,12 +71,15 @@ impl SentReplyKeys {
pub struct UsedReplyKey { pub struct UsedReplyKey {
key: SurbEncryptionKey, key: SurbEncryptionKey,
// the purpose of this field is to perform invalidation at relatively very long intervals // the purpose of this field is to perform invalidation at relatively very long intervals
pub sent_at: OffsetDateTime, pub sent_at_timestamp: i64,
} }
impl UsedReplyKey { impl UsedReplyKey {
pub(crate) fn new(key: SurbEncryptionKey, sent_at: OffsetDateTime) -> Self { pub(crate) fn new(key: SurbEncryptionKey, sent_at_timestamp: i64) -> Self {
UsedReplyKey { key, sent_at } UsedReplyKey {
key,
sent_at_timestamp,
}
} }
} }
+6 -8
View File
@@ -4,9 +4,8 @@
pub use backend::*; pub use backend::*;
pub use combined::CombinedReplyStorage; pub use combined::CombinedReplyStorage;
pub use key_storage::SentReplyKeys; pub use key_storage::SentReplyKeys;
pub use surb_storage::{ReceivedReplySurb, ReceivedReplySurbsMap, RetrievedReplySurb}; pub use surb_storage::ReceivedReplySurbsMap;
pub use tag_storage::UsedSenderTags; pub use tag_storage::UsedSenderTags;
use time::OffsetDateTime;
mod backend; mod backend;
mod combined; mod combined;
@@ -30,19 +29,17 @@ where
PersistentReplyStorage { backend } PersistentReplyStorage { backend }
} }
pub async fn load_state_from_backend( pub async fn load_state_from_backend(&self) -> Result<CombinedReplyStorage, T::StorageError> {
&self, self.backend.load_surb_storage().await
surb_freshness_cutoff: OffsetDateTime,
) -> Result<CombinedReplyStorage, T::StorageError> {
self.backend.load_surb_storage(surb_freshness_cutoff).await
} }
// this will have to get enabled after merging develop
pub async fn flush_on_shutdown( pub async fn flush_on_shutdown(
mut self, mut self,
mem_state: CombinedReplyStorage, mem_state: CombinedReplyStorage,
mut shutdown: nym_task::TaskClient, mut shutdown: nym_task::TaskClient,
) { ) {
use tracing::{debug, error, info}; use log::{debug, error, info};
debug!("Started PersistentReplyStorage"); debug!("Started PersistentReplyStorage");
if let Err(err) = self.backend.start_storage_session().await { if let Err(err) = self.backend.start_storage_session().await {
@@ -53,6 +50,7 @@ where
shutdown.recv().await; shutdown.recv().await;
info!("PersistentReplyStorage is flushing all reply-related data to underlying storage"); info!("PersistentReplyStorage is flushing all reply-related data to underlying storage");
info!("you MUST NOT forcefully shutdown now or you risk data corruption!");
if let Err(err) = self.backend.flush_surb_storage(&mem_state).await { if let Err(err) = self.backend.flush_surb_storage(&mem_state).await {
error!("failed to flush our reply-related data to the persistent storage: {err}") error!("failed to flush our reply-related data to the persistent storage: {err}")
} else { } else {
@@ -1,45 +1,15 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net> // Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use dashmap::iter::{Iter, IterMut}; use dashmap::iter::Iter;
use dashmap::DashMap; use dashmap::DashMap;
use log::trace;
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag; use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation; use nym_sphinx::anonymous_replies::ReplySurb;
use nym_sphinx::params::SphinxKeyRotation;
use std::cmp::min;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use time::OffsetDateTime; use time::OffsetDateTime;
use tracing::{error, info, trace};
#[derive(Debug)]
pub struct RetrievedReplySurb {
pub(crate) reply_surb: ReceivedReplySurb,
pub(crate) stale_pile: bool,
}
impl RetrievedReplySurb {
pub(crate) fn new_fresh(reply_surb: ReceivedReplySurb) -> Self {
RetrievedReplySurb {
reply_surb,
stale_pile: false,
}
}
pub(crate) fn new_stale(reply_surb: ReceivedReplySurb) -> Self {
RetrievedReplySurb {
reply_surb,
stale_pile: true,
}
}
}
impl From<RetrievedReplySurb> for ReplySurbWithKeyRotation {
fn from(retrieved: RetrievedReplySurb) -> Self {
retrieved.reply_surb.into()
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct ReceivedReplySurbsMap { pub struct ReceivedReplySurbsMap {
@@ -87,40 +57,17 @@ impl ReceivedReplySurbsMap {
self.inner.data.iter() self.inner.data.iter()
} }
pub fn as_raw_iter_mut(&self) -> IterMut<'_, AnonymousSenderTag, ReceivedReplySurbs> { pub fn remove(&self, target: &AnonymousSenderTag) {
self.inner.data.iter_mut() self.inner.data.remove(target);
} }
fn total_surbs(&self) -> usize { pub fn reset_surbs_last_received_at(&self, target: &AnonymousSenderTag) {
self.inner if let Some(mut entry) = self.inner.data.get_mut(target) {
.data entry.surbs_last_received_at_timestamp = OffsetDateTime::now_utc().unix_timestamp();
.iter()
.map(|entry| entry.value().data.len())
.sum()
}
pub fn drop_stale_loaded_surbs(&self, cutoff: OffsetDateTime) {
let before = self.total_surbs();
self.inner.data.retain(|_, v| {
if v.surbs_last_received_at() < cutoff {
return false;
}
v.data.retain(|s| s.received_at > cutoff);
!v.data.is_empty()
});
let after = self.total_surbs();
let diff = before - after;
if diff != 0 {
info!("removed {diff} stale reply SURBs")
} }
} }
pub fn retain(&self, f: impl FnMut(&AnonymousSenderTag, &mut ReceivedReplySurbs) -> bool) { pub fn surbs_last_received_at(&self, target: &AnonymousSenderTag) -> Option<i64> {
self.inner.data.retain(f);
}
pub fn surbs_last_received_at(&self, target: &AnonymousSenderTag) -> Option<OffsetDateTime> {
self.inner self.inner
.data .data
.get(target) .get(target)
@@ -179,25 +126,15 @@ impl ReceivedReplySurbsMap {
.unwrap_or_default() .unwrap_or_default()
} }
pub fn available_fresh_surbs(&self, target: &AnonymousSenderTag) -> usize {
self.inner
.data
.get(target)
.map(|entry| entry.fresh_left())
.unwrap_or_default()
}
pub fn contains_surbs_for(&self, target: &AnonymousSenderTag) -> bool { pub fn contains_surbs_for(&self, target: &AnonymousSenderTag) -> bool {
self.inner.data.contains_key(target) self.inner.data.contains_key(target)
} }
/// Attempt to retrieve the specified number of reply SURBs for the target sender
/// and return the number of SURBs remaining in the storage after the call.
pub fn get_reply_surbs( pub fn get_reply_surbs(
&self, &self,
target: &AnonymousSenderTag, target: &AnonymousSenderTag,
amount: usize, amount: usize,
) -> (Option<Vec<RetrievedReplySurb>>, usize) { ) -> (Option<Vec<ReplySurb>>, usize) {
if let Some(mut entry) = self.inner.data.get_mut(target) { if let Some(mut entry) = self.inner.data.get_mut(target) {
let surbs_left = entry.items_left(); let surbs_left = entry.items_left();
if surbs_left < self.min_surb_threshold() + amount { if surbs_left < self.min_surb_threshold() + amount {
@@ -213,72 +150,34 @@ impl ReceivedReplySurbsMap {
pub fn get_reply_surb_ignoring_threshold( pub fn get_reply_surb_ignoring_threshold(
&self, &self,
target: &AnonymousSenderTag, target: &AnonymousSenderTag,
) -> (Option<RetrievedReplySurb>, usize) { ) -> Option<(Option<ReplySurb>, usize)> {
let Some(mut entry) = self.inner.data.get_mut(target) else { self.inner
return (None, 0); .data
}; .get_mut(target)
.map(|mut s| s.get_reply_surb())
entry.get_reply_surb()
} }
pub fn get_reply_surb( pub fn get_reply_surb(
&self, &self,
target: &AnonymousSenderTag, target: &AnonymousSenderTag,
) -> (Option<RetrievedReplySurb>, usize) { ) -> Option<(Option<ReplySurb>, usize)> {
let Some(mut entry) = self.inner.data.get_mut(target) else { self.inner.data.get_mut(target).map(|mut entry| {
return (None, 0); let surbs_left = entry.items_left();
}; if surbs_left < self.min_surb_threshold() {
(None, surbs_left)
let surbs_left = entry.items_left();
if surbs_left < self.min_surb_threshold() {
(None, surbs_left)
} else {
entry.get_reply_surb()
}
}
pub fn re_insert_reply_surbs(
&self,
target: &AnonymousSenderTag,
surbs: Vec<RetrievedReplySurb>,
) {
error!("re-inserting {} unused surbs", surbs.len());
let mut entry = self.inner.data.entry(*target).or_insert_with(|| {
// this branch should realistically NEVER happen, but software be software, so let's not crash
error!("attempting to return surbs to no longer existing entry {target}");
ReceivedReplySurbs::new(VecDeque::new())
});
let entry = entry.value_mut();
for returned_surb in surbs.into_iter().rev() {
if returned_surb.stale_pile {
entry.possibly_stale.push_front(returned_surb.reply_surb)
} else { } else {
entry.data.push_front(returned_surb.reply_surb) entry.get_reply_surb()
} }
} })
} }
pub fn insert_fresh_surbs<I: IntoIterator<Item = ReplySurbWithKeyRotation>>( pub fn insert_surbs<I: IntoIterator<Item = ReplySurb>>(
&self, &self,
target: &AnonymousSenderTag, target: &AnonymousSenderTag,
surbs: I, surbs: I,
) { ) {
if let Some(mut existing_data) = self.inner.data.get_mut(target) { if let Some(mut existing_data) = self.inner.data.get_mut(target) {
existing_data.insert_fresh_reply_surbs(surbs); existing_data.insert_reply_surbs(surbs)
if existing_data.possibly_stale.is_empty() {
return;
}
// if we're above the minimum threshold, remove stale surbs
let threshold = self.min_surb_threshold();
let diff = existing_data.data.len().saturating_sub(threshold);
trace!("will attempt to remove up to {diff} stale surbs");
if diff > 0 {
existing_data.remove_stale_surbs(diff);
}
} else { } else {
let new_entry = ReceivedReplySurbs::new(surbs.into_iter().collect()); let new_entry = ReceivedReplySurbs::new(surbs.into_iter().collect());
self.inner.data.insert(*target, new_entry); self.inner.data.insert(*target, new_entry);
@@ -286,102 +185,44 @@ impl ReceivedReplySurbsMap {
} }
} }
#[derive(Debug)]
pub struct ReceivedReplySurb {
pub(crate) surb: ReplySurbWithKeyRotation,
pub(crate) received_at: OffsetDateTime,
}
impl From<ReceivedReplySurb> for ReplySurbWithKeyRotation {
fn from(surb: ReceivedReplySurb) -> Self {
surb.surb
}
}
impl ReceivedReplySurb {
pub fn received_at(&self) -> OffsetDateTime {
self.received_at
}
pub fn key_rotation(&self) -> SphinxKeyRotation {
self.surb.key_rotation()
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct ReceivedReplySurbs { pub struct ReceivedReplySurbs {
data: VecDeque<ReceivedReplySurb>, // in the future we'd probably want to put extra data here to indicate when the SURBs got received
possibly_stale: VecDeque<ReceivedReplySurb>, // so we could invalidate entries from the previous key rotations
data: VecDeque<ReplySurb>,
pending_reception: u32, pending_reception: u32,
surbs_last_received_at: OffsetDateTime, surbs_last_received_at_timestamp: i64,
} }
impl ReceivedReplySurbs { impl ReceivedReplySurbs {
fn new(initial_surbs: VecDeque<ReplySurbWithKeyRotation>) -> Self { fn new(initial_surbs: VecDeque<ReplySurb>) -> Self {
let mut this = ReceivedReplySurbs { ReceivedReplySurbs {
data: Default::default(), data: initial_surbs,
possibly_stale: Default::default(),
pending_reception: 0, pending_reception: 0,
surbs_last_received_at: OffsetDateTime::now_utc(), surbs_last_received_at_timestamp: OffsetDateTime::now_utc().unix_timestamp(),
}; }
this.insert_fresh_reply_surbs(initial_surbs);
this
} }
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
pub fn new_retrieved( pub fn new_retrieved(
surbs: Vec<ReplySurbWithKeyRotation>, surbs: Vec<ReplySurb>,
surbs_last_received_at: OffsetDateTime, surbs_last_received_at_timestamp: i64,
) -> ReceivedReplySurbs { ) -> ReceivedReplySurbs {
let mut this = ReceivedReplySurbs { ReceivedReplySurbs {
data: Default::default(), data: surbs.into(),
possibly_stale: Default::default(),
pending_reception: 0, pending_reception: 0,
surbs_last_received_at, surbs_last_received_at_timestamp,
}; }
this.insert_fresh_reply_surbs(surbs);
this.surbs_last_received_at = surbs_last_received_at;
this
}
pub fn downgrade_freshness(&mut self) -> usize {
debug_assert!(self.possibly_stale.is_empty());
std::mem::swap(&mut self.data, &mut self.possibly_stale);
self.possibly_stale.len()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty() && self.possibly_stale.is_empty()
} }
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))] #[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
pub fn surbs_ref(&self) -> &VecDeque<ReceivedReplySurb> { pub fn surbs_ref(&self) -> &VecDeque<ReplySurb> {
&self.data &self.data
} }
pub fn retain_fresh_surbs(&mut self, f: impl FnMut(&ReceivedReplySurb) -> bool) { pub fn surbs_last_received_at(&self) -> i64 {
self.data.retain(f); self.surbs_last_received_at_timestamp
}
pub fn retain_possibly_stale_surbs(&mut self, f: impl FnMut(&ReceivedReplySurb) -> bool) {
self.possibly_stale.retain(f);
}
pub fn fresh_left(&self) -> usize {
self.data.len()
}
pub fn possibly_stale_left(&self) -> usize {
self.possibly_stale.len()
}
pub fn drop_possibly_stale_surbs(&mut self) {
self.possibly_stale = VecDeque::new();
}
pub fn surbs_last_received_at(&self) -> OffsetDateTime {
self.surbs_last_received_at
} }
pub fn pending_reception(&self) -> u32 { pub fn pending_reception(&self) -> u32 {
@@ -402,78 +243,33 @@ impl ReceivedReplySurbs {
self.pending_reception = 0; self.pending_reception = 0;
} }
/// Attempt to retrieve the specified number of reply SURBs (if at least that many are present) pub fn get_reply_surbs(&mut self, amount: usize) -> (Option<Vec<ReplySurb>>, usize) {
/// and return the number of SURBs remaining in the storage after the call.
pub fn get_reply_surbs(&mut self, amount: usize) -> (Option<Vec<RetrievedReplySurb>>, usize) {
if self.items_left() < amount { if self.items_left() < amount {
(None, self.items_left()) (None, self.items_left())
} else { } else {
let available_fresh = self.fresh_left(); let surbs = self.data.drain(..amount).collect();
(Some(surbs), self.items_left())
// prefer the 'fresh' data if available. otherwise fallback to the possibly stale entries
let mut reply_surbs = Vec::with_capacity(amount);
let fresh_to_retrieve = min(available_fresh, amount);
for surb in self.data.drain(..fresh_to_retrieve) {
reply_surbs.push(RetrievedReplySurb::new_fresh(surb))
}
if available_fresh < amount {
let stale_to_retrieve = amount - fresh_to_retrieve;
for surb in self.possibly_stale.drain(..stale_to_retrieve) {
reply_surbs.push(RetrievedReplySurb::new_stale(surb))
}
}
(Some(reply_surbs), self.items_left())
} }
} }
pub fn get_reply_surb(&mut self) -> (Option<RetrievedReplySurb>, usize) { pub fn get_reply_surb(&mut self) -> (Option<ReplySurb>, usize) {
(self.pop_surb(), self.items_left()) (self.pop_surb(), self.items_left())
} }
fn pop_surb(&mut self) -> Option<RetrievedReplySurb> { fn pop_surb(&mut self) -> Option<ReplySurb> {
// prefer the 'fresh' data if available. otherwise fallback to the possibly stale entries self.data.pop_front()
if let Some(fresh) = self.data.pop_front() {
return Some(RetrievedReplySurb::new_fresh(fresh));
}
if let Some(stale) = self.possibly_stale.pop_front() {
return Some(RetrievedReplySurb::new_stale(stale));
}
None
} }
fn items_left(&self) -> usize { fn items_left(&self) -> usize {
self.data.len() + self.possibly_stale.len() self.data.len()
}
pub fn remove_stale_surbs(&mut self, amount: usize) {
// remove up to amount number of possibly stale surbs
let amount = min(amount, self.possibly_stale.len());
self.possibly_stale.drain(..amount);
} }
// realistically we're always going to be getting multiple surbs at once // realistically we're always going to be getting multiple surbs at once
pub(crate) fn insert_fresh_reply_surbs<I: IntoIterator<Item = ReplySurbWithKeyRotation>>( pub fn insert_reply_surbs<I: IntoIterator<Item = ReplySurb>>(&mut self, surbs: I) {
&mut self, let mut v = surbs.into_iter().collect::<VecDeque<_>>();
surbs: I,
) {
let received_at = OffsetDateTime::now_utc();
let mut v = surbs
.into_iter()
.map(|surb| ReceivedReplySurb { surb, received_at })
.collect::<VecDeque<_>>();
if v.is_empty() {
return;
}
trace!("storing {} surbs in the storage", v.len()); trace!("storing {} surbs in the storage", v.len());
self.data.append(&mut v); self.data.append(&mut v);
self.surbs_last_received_at = received_at; self.surbs_last_received_at_timestamp = OffsetDateTime::now_utc().unix_timestamp();
trace!("we now have {} surbs!", self.data.len()); trace!("we now have {} surbs!", self.data.len());
} }
} }
@@ -27,7 +27,6 @@ nym-credential-storage = { path = "../../credential-storage" }
nym-credentials-interface = { path = "../../credentials-interface" } nym-credentials-interface = { path = "../../credentials-interface" }
nym-crypto = { path = "../../crypto" } nym-crypto = { path = "../../crypto" }
nym-gateway-requests = { path = "../../gateway-requests" } nym-gateway-requests = { path = "../../gateway-requests" }
nym-http-api-client = { path = "../../http-api-client" }
nym-network-defaults = { path = "../../network-defaults" } nym-network-defaults = { path = "../../network-defaults" }
nym-sphinx = { path = "../../nymsphinx" } nym-sphinx = { path = "../../nymsphinx" }
nym-statistics-common = { path = "../../statistics" } nym-statistics-common = { path = "../../statistics" }
@@ -21,8 +21,8 @@ use nym_crypto::asymmetric::ed25519;
use nym_gateway_requests::registration::handshake::client_handshake; use nym_gateway_requests::registration::handshake::client_handshake;
use nym_gateway_requests::{ use nym_gateway_requests::{
BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt, BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersionExt,
GatewayRequestsError, SensitiveServerResponse, ServerResponse, SharedGatewayKey, SensitiveServerResponse, ServerResponse, SharedGatewayKey, SharedSymmetricKey,
SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
}; };
use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::forwarding::packet::MixPacket;
use nym_statistics_common::clients::connection::ConnectionStatsEvent; use nym_statistics_common::clients::connection::ConnectionStatsEvent;
@@ -165,6 +165,24 @@ impl<C, St> GatewayClient<C, St> {
self.bandwidth.remaining() self.bandwidth.remaining()
} }
pub fn is_connection_established(&self) -> bool {
self.connection.is_established()
}
/// Check if the websocket connection is actually alive at the socket level
pub fn is_connection_alive(&self) -> bool {
// First check if we have an established connection
if !self.connection.is_established() {
return false;
}
// Get the file descriptor and check if the socket is alive
match self.ws_fd() {
Some(fd) => socket_is_alive(fd),
None => false,
}
}
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
async fn _close_connection(&mut self) -> Result<(), GatewayClientError> { async fn _close_connection(&mut self) -> Result<(), GatewayClientError> {
match std::mem::replace(&mut self.connection, SocketState::NotConnected) { match std::mem::replace(&mut self.connection, SocketState::NotConnected) {
@@ -201,7 +219,7 @@ impl<C, St> GatewayClient<C, St> {
#[cfg(not(target_arch = "wasm32"))] #[cfg(not(target_arch = "wasm32"))]
pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> { pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> {
debug!( debug!(
"Attempting to establish connection to gateway at: {}", "Attemting to establish connection to gateway at: {}",
self.gateway_address self.gateway_address
); );
let (ws_stream, _) = connect_async( let (ws_stream, _) = connect_async(
@@ -272,7 +290,7 @@ impl<C, St> GatewayClient<C, St> {
) -> Result<(), GatewayClientError> { ) -> Result<(), GatewayClientError> {
if let Some(shared_key) = self.shared_key() { if let Some(shared_key) = self.shared_key() {
let encrypted = message.encrypt(&*shared_key)?; let encrypted = message.encrypt(&*shared_key)?;
Box::pin(self.send_websocket_message_without_response(encrypted)).await?; Box::pin(self.send_websocket_message(encrypted)).await?;
Ok(()) Ok(())
} else { } else {
Err(GatewayClientError::ConnectionInInvalidState) Err(GatewayClientError::ConnectionInInvalidState)
@@ -330,80 +348,9 @@ impl<C, St> GatewayClient<C, St> {
} }
} }
/// Attempt to send a websocket message to the gateway without waiting for any response
async fn send_websocket_message_without_response(
&mut self,
msg: impl Into<Message>,
) -> Result<(), GatewayClientError> {
match self.connection {
SocketState::Available(ref mut conn) => Ok(conn.send(msg.into()).await?),
SocketState::PartiallyDelegated(ref mut partially_delegated) => {
if let Err(err) = partially_delegated.send_without_response(msg.into()).await {
error!("failed to send message without response - {err}...");
// we must ensure we do not leave the task still active
if let Err(err) = self.recover_socket_connection().await {
error!("... and the delegated stream has also errored out - {err}")
}
Err(err)
} else {
Ok(())
}
}
SocketState::NotConnected => Err(GatewayClientError::ConnectionNotEstablished),
_ => Err(GatewayClientError::ConnectionInInvalidState),
}
}
// A very nasty hack due to lack of id tags on messages - send a non-sphinx packet websocket
// message and wait until first non 'Send' response within timeout
pub async fn send_websocket_message_with_non_send_response(
&mut self,
msg: impl Into<Message>,
) -> Result<ServerResponse, GatewayClientError> {
let should_restart_mixnet_listener = if self.connection.is_partially_delegated() {
self.recover_socket_connection().await?;
true
} else {
false
};
let conn = match self.connection {
SocketState::Available(ref mut conn) => conn,
SocketState::NotConnected => return Err(GatewayClientError::ConnectionNotEstablished),
_ => return Err(GatewayClientError::ConnectionInInvalidState),
};
conn.send(msg.into()).await?;
let timeout = sleep(self.cfg.connection.response_timeout_duration);
tokio::pin!(timeout);
let response = loop {
tokio::select! {
_ = &mut timeout => {
break Err(GatewayClientError::Timeout);
}
// note: the below will also listen for shutdown signals
msg = self.read_control_response() => {
match msg {
Ok(res) => if !res.is_send() {
break Ok(res);
},
Err(err) => break Err(err),
}
}
}
};
if should_restart_mixnet_listener {
self.start_listening_for_mixnet_messages()?;
}
response
}
/// Attempt to send a websocket message to the gateway and wait until we receive a response.
// If we want to send a message (with response), we need to have a full control over the socket, // If we want to send a message (with response), we need to have a full control over the socket,
// as we need to be able to write the request and read the subsequent response // as we need to be able to write the request and read the subsequent response
pub async fn send_websocket_message_with_response( pub async fn send_websocket_message(
&mut self, &mut self,
msg: impl Into<Message>, msg: impl Into<Message>,
) -> Result<ServerResponse, GatewayClientError> { ) -> Result<ServerResponse, GatewayClientError> {
@@ -458,6 +405,29 @@ impl<C, St> GatewayClient<C, St> {
} }
} }
async fn send_websocket_message_without_response(
&mut self,
msg: Message,
) -> Result<(), GatewayClientError> {
match self.connection {
SocketState::Available(ref mut conn) => Ok(conn.send(msg).await?),
SocketState::PartiallyDelegated(ref mut partially_delegated) => {
if let Err(err) = partially_delegated.send_without_response(msg).await {
error!("failed to send message without response - {err}...");
// we must ensure we do not leave the task still active
if let Err(err) = self.recover_socket_connection().await {
error!("... and the delegated stream has also errored out - {err}")
}
Err(err)
} else {
Ok(())
}
}
SocketState::NotConnected => Err(GatewayClientError::ConnectionNotEstablished),
_ => Err(GatewayClientError::ConnectionInInvalidState),
}
}
fn check_gateway_protocol( fn check_gateway_protocol(
&self, &self,
gateway_protocol: Option<u8>, gateway_protocol: Option<u8>,
@@ -583,10 +553,7 @@ impl<C, St> GatewayClient<C, St> {
.encrypt(legacy_key)?; .encrypt(legacy_key)?;
info!("sending upgrade request and awaiting the acknowledgement back"); info!("sending upgrade request and awaiting the acknowledgement back");
let (ciphertext, nonce) = match self let (ciphertext, nonce) = match self.send_websocket_message(upgrade_request).await? {
.send_websocket_message_with_response(upgrade_request)
.await?
{
ServerResponse::EncryptedResponse { ciphertext, nonce } => (ciphertext, nonce), ServerResponse::EncryptedResponse { ciphertext, nonce } => (ciphertext, nonce),
ServerResponse::Error { message } => { ServerResponse::Error { message } => {
return Err(GatewayClientError::GatewayError(message)) return Err(GatewayClientError::GatewayError(message))
@@ -618,7 +585,7 @@ impl<C, St> GatewayClient<C, St> {
&mut self, &mut self,
msg: ClientControlRequest, msg: ClientControlRequest,
) -> Result<(), GatewayClientError> { ) -> Result<(), GatewayClientError> {
match self.send_websocket_message_with_response(msg).await? { match self.send_websocket_message(msg).await? {
ServerResponse::Authenticate { ServerResponse::Authenticate {
protocol_version, protocol_version,
status, status,
@@ -713,7 +680,6 @@ impl<C, St> GatewayClient<C, St> {
let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv(); let supports_aes_gcm_siv = gw_protocol.supports_aes256_gcm_siv();
let supports_auth_v2 = gw_protocol.supports_authenticate_v2(); let supports_auth_v2 = gw_protocol.supports_authenticate_v2();
let supports_key_rotation_info = gw_protocol.supports_key_rotation_packet();
if !supports_aes_gcm_siv { if !supports_aes_gcm_siv {
warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV"); warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV");
@@ -721,9 +687,6 @@ impl<C, St> GatewayClient<C, St> {
if !supports_auth_v2 { if !supports_auth_v2 {
warn!("this gateway is on an old version that doesn't support authentication v2") warn!("this gateway is on an old version that doesn't support authentication v2")
} }
if !supports_key_rotation_info {
warn!("this gateway is on an old version that doesn't support key rotation packets")
}
if self.authenticated { if self.authenticated {
debug!("Already authenticated"); debug!("Already authenticated");
@@ -768,16 +731,13 @@ impl<C, St> GatewayClient<C, St> {
} }
} }
/// Attempt to retrieve the currently supported gateway protocol version of the remote.
pub async fn get_gateway_protocol(&mut self) -> Result<u8, GatewayClientError> { pub async fn get_gateway_protocol(&mut self) -> Result<u8, GatewayClientError> {
if !self.connection.is_established() { if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished); return Err(GatewayClientError::ConnectionNotEstablished);
} }
match self match self
.send_websocket_message_with_non_send_response( .send_websocket_message(ClientControlRequest::SupportedProtocol {})
ClientControlRequest::SupportedProtocol {},
)
.await? .await?
{ {
ServerResponse::SupportedProtocol { version } => Ok(version), ServerResponse::SupportedProtocol { version } => Ok(version),
@@ -794,10 +754,7 @@ impl<C, St> GatewayClient<C, St> {
credential, credential,
self.shared_key.as_ref().unwrap(), self.shared_key.as_ref().unwrap(),
)?; )?;
let bandwidth_remaining = match self let bandwidth_remaining = match self.send_websocket_message(msg).await? {
.send_websocket_message_with_non_send_response(msg)
.await?
{
ServerResponse::Bandwidth { available_total } => Ok(available_total), ServerResponse::Bandwidth { available_total } => Ok(available_total),
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
ServerResponse::TypedError { error } => { ServerResponse::TypedError { error } => {
@@ -815,10 +772,7 @@ impl<C, St> GatewayClient<C, St> {
async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> { async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> {
let msg = ClientControlRequest::ClaimFreeTestnetBandwidth; let msg = ClientControlRequest::ClaimFreeTestnetBandwidth;
let bandwidth_remaining = match self let bandwidth_remaining = match self.send_websocket_message(msg).await? {
.send_websocket_message_with_non_send_response(msg)
.await?
{
ServerResponse::Bandwidth { available_total } => Ok(available_total), ServerResponse::Bandwidth { available_total } => Ok(available_total),
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }), other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
@@ -913,22 +867,6 @@ impl<C, St> GatewayClient<C, St> {
} }
} }
fn mix_packet_to_ws_message(&self, packet: MixPacket) -> Result<Message, GatewayRequestsError> {
// note: into_ws_message encrypts the requests and adds a MAC on it. Perhaps it should
// be more explicit in the naming?
let req = if self.negotiated_protocol.supports_key_rotation_packet() {
BinaryRequest::ForwardSphinxV2 { packet }
} else {
BinaryRequest::ForwardSphinx { packet }
};
req.into_ws_message(
self.shared_key
.as_ref()
.expect("no shared key present even though we're authenticated!"),
)
}
pub async fn batch_send_mix_packets( pub async fn batch_send_mix_packets(
&mut self, &mut self,
packets: Vec<MixPacket>, packets: Vec<MixPacket>,
@@ -957,7 +895,13 @@ impl<C, St> GatewayClient<C, St> {
let messages: Result<Vec<_>, _> = packets let messages: Result<Vec<_>, _> = packets
.into_iter() .into_iter()
.map(|mix_packet| self.mix_packet_to_ws_message(mix_packet)) .map(|mix_packet| {
BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message(
self.shared_key
.as_ref()
.expect("no shared key present even though we're authenticated!"),
)
})
.collect(); .collect();
if let Err(err) = self if let Err(err) = self
@@ -1023,8 +967,13 @@ impl<C, St> GatewayClient<C, St> {
if !self.connection.is_established() { if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished); return Err(GatewayClientError::ConnectionNotEstablished);
} }
// note: into_ws_message encrypts the requests and adds a MAC on it. Perhaps it should
let msg = self.mix_packet_to_ws_message(mix_packet)?; // be more explicit in the naming?
let msg = BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message(
self.shared_key
.as_ref()
.expect("no shared key present even though we're authenticated!"),
)?;
self.send_with_reconnection_on_failure(msg).await self.send_with_reconnection_on_failure(msg).await
} }
@@ -1197,3 +1146,49 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
} }
} }
} }
/// Check if a socket file descriptor is alive and responsive
///
/// This function performs socket-level checks to determine if the connection is actually alive.
/// It's cross-platform compatible and works on both Unix and non-Unix systems.
fn socket_is_alive(fd: RawFd) -> bool {
#[cfg(unix)]
{
use std::io::ErrorKind;
use std::net::TcpStream;
use std::os::unix::io::FromRawFd;
unsafe {
// Create a TcpStream from the raw fd to perform socket operations
let stream = TcpStream::from_raw_fd(fd);
// Try to peek at the socket to see if it's still connected
// We peek with a zero-length buffer to avoid consuming data
let mut buf = [0u8; 0];
let result = match stream.peek(&mut buf) {
Ok(_) => true, // Socket is alive and readable
Err(e) => match e.kind() {
ErrorKind::WouldBlock => true, // Socket is alive but no data available
ErrorKind::ConnectionReset
| ErrorKind::ConnectionAborted
| ErrorKind::BrokenPipe
| ErrorKind::NotConnected => false, // Socket is clearly dead
_ => true, // Other errors might be temporary, assume alive
},
};
// Prevent the TcpStream from closing the fd when it's dropped
// since we don't own the fd
std::mem::forget(stream);
result
}
}
#[cfg(not(unix))]
{
// On non-Unix systems, we can't easily check socket state
// Fall back to assuming the connection is alive if we have an fd
fd != 0
}
}
@@ -1,6 +1,5 @@
use crate::error::GatewayClientError; use crate::error::GatewayClientError;
use nym_http_api_client::HickoryDnsResolver;
#[cfg(unix)] #[cfg(unix)]
use std::{ use std::{
os::fd::{AsRawFd, RawFd}, os::fd::{AsRawFd, RawFd},
@@ -20,7 +19,6 @@ pub(crate) async fn connect_async(
) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response), GatewayClientError> { ) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response), GatewayClientError> {
use tokio::net::TcpSocket; use tokio::net::TcpSocket;
let resolver = HickoryDnsResolver::default();
let uri = let uri =
Url::parse(endpoint).map_err(|_| GatewayClientError::InvalidUrl(endpoint.to_owned()))?; Url::parse(endpoint).map_err(|_| GatewayClientError::InvalidUrl(endpoint.to_owned()))?;
let port: u16 = uri.port_or_known_default().unwrap_or(443); let port: u16 = uri.port_or_known_default().unwrap_or(443);
@@ -29,18 +27,18 @@ pub(crate) async fn connect_async(
.host() .host()
.ok_or(GatewayClientError::InvalidUrl(endpoint.to_owned()))?; .ok_or(GatewayClientError::InvalidUrl(endpoint.to_owned()))?;
// Get address for tcp connection, if a domain is provided use our preferred resolver rather than // Get address for tcp connection, using system DNS resolver
// the default std resolve
let sock_addrs: Vec<SocketAddr> = match host { let sock_addrs: Vec<SocketAddr> = match host {
Host::Ipv4(addr) => vec![SocketAddr::new(addr.into(), port)], Host::Ipv4(addr) => vec![SocketAddr::new(addr.into(), port)],
Host::Ipv6(addr) => vec![SocketAddr::new(addr.into(), port)], Host::Ipv6(addr) => vec![SocketAddr::new(addr.into(), port)],
Host::Domain(domain) => { Host::Domain(domain) => {
// Do a DNS lookup for the domain using our custom DNS resolver // Do a DNS lookup for the domain using system DNS resolver
resolver tokio::net::lookup_host((domain, port))
.resolve_str(domain) .await
.await? .map_err(|err| GatewayClientError::NetworkConnectionFailed {
.into_iter() address: endpoint.to_owned(),
.map(|a| SocketAddr::new(a, port)) source: err.into(),
})?
.collect() .collect()
} }
}; };
@@ -56,7 +54,7 @@ pub(crate) async fn connect_async(
} }
.map_err(|err| GatewayClientError::NetworkConnectionFailed { .map_err(|err| GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(), address: endpoint.to_owned(),
source: Box::new(tungstenite::Error::from(err)), source: err.into(),
})?; })?;
#[cfg(unix)] #[cfg(unix)]
@@ -72,7 +70,7 @@ pub(crate) async fn connect_async(
Err(err) => { Err(err) => {
stream = Err(GatewayClientError::NetworkConnectionFailed { stream = Err(GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(), address: endpoint.to_owned(),
source: Box::new(tungstenite::Error::from(err)), source: err.into(),
}); });
continue; continue;
} }
@@ -83,6 +81,6 @@ pub(crate) async fn connect_async(
.await .await
.map_err(|error| GatewayClientError::NetworkConnectionFailed { .map_err(|error| GatewayClientError::NetworkConnectionFailed {
address: endpoint.to_owned(), address: endpoint.to_owned(),
source: Box::new(error), source: error,
}) })
} }
+3 -16
View File
@@ -25,7 +25,7 @@ pub enum GatewayClientError {
RequestError(#[from] GatewayRequestsError), RequestError(#[from] GatewayRequestsError),
#[error("There was a network error: {0}")] #[error("There was a network error: {0}")]
NetworkError(Box<WsError>), NetworkError(#[from] WsError),
#[error("failed to upgrade our shared key - the gateway sent malformed response")] #[error("failed to upgrade our shared key - the gateway sent malformed response")]
FatalKeyUpgradeFailure, FatalKeyUpgradeFailure,
@@ -41,10 +41,7 @@ pub enum GatewayClientError {
NetworkErrorWasm(#[from] JsError), NetworkErrorWasm(#[from] JsError),
#[error("connection failed: {address}: {source}")] #[error("connection failed: {address}: {source}")]
NetworkConnectionFailed { NetworkConnectionFailed { address: String, source: WsError },
address: String,
source: Box<WsError>,
},
#[error("no socket address for endpoint: {address}")] #[error("no socket address for endpoint: {address}")]
NoEndpointForConnection { address: String }, NoEndpointForConnection { address: String },
@@ -52,10 +49,6 @@ pub enum GatewayClientError {
#[error("Invalid URL: {0}")] #[error("Invalid URL: {0}")]
InvalidUrl(String), InvalidUrl(String),
#[cfg(not(target_arch = "wasm32"))]
#[error("resolution failed: {0}")]
ResolutionFailed(#[from] nym_http_api_client::HickoryDnsError),
#[error("No shared key was provided or obtained")] #[error("No shared key was provided or obtained")]
NoSharedKeyAvailable, NoSharedKeyAvailable,
@@ -130,16 +123,10 @@ pub enum GatewayClientError {
ShutdownInProgress, ShutdownInProgress,
} }
impl From<WsError> for GatewayClientError {
fn from(error: WsError) -> Self {
GatewayClientError::NetworkError(Box::new(error))
}
}
impl GatewayClientError { impl GatewayClientError {
pub fn is_closed_connection(&self) -> bool { pub fn is_closed_connection(&self) -> bool {
match self { match self {
GatewayClientError::NetworkError(ws_err) => match ws_err.as_ref() { GatewayClientError::NetworkError(ws_err) => match ws_err {
WsError::AlreadyClosed | WsError::ConnectionClosed => true, WsError::AlreadyClosed | WsError::ConnectionClosed => true,
WsError::Io(io_err) => matches!( WsError::Io(io_err) => matches!(
io_err.kind(), io_err.kind(),
+2 -2
View File
@@ -28,7 +28,7 @@ pub(crate) fn cleanup_socket_message(
msg: Option<Result<Message, WsError>>, msg: Option<Result<Message, WsError>>,
) -> Result<Message, GatewayClientError> { ) -> Result<Message, GatewayClientError> {
match msg { match msg {
Some(msg) => msg.map_err(GatewayClientError::from), Some(msg) => msg.map_err(GatewayClientError::NetworkError),
None => Err(GatewayClientError::ConnectionAbruptlyClosed), None => Err(GatewayClientError::ConnectionAbruptlyClosed),
} }
} }
@@ -39,7 +39,7 @@ pub(crate) fn cleanup_socket_messages(
match msgs { match msgs {
Some(msgs) => msgs Some(msgs) => msgs
.into_iter() .into_iter()
.map(|msg| msg.map_err(GatewayClientError::from)) .map(|msg| msg.map_err(GatewayClientError::NetworkError))
.collect(), .collect(),
None => Err(GatewayClientError::ConnectionAbruptlyClosed), None => Err(GatewayClientError::ConnectionAbruptlyClosed),
} }
@@ -337,7 +337,7 @@ impl PartiallyDelegatedHandle {
// check if the split stream didn't error out // check if the split stream didn't error out
let receive_res = stream_receiver let receive_res = stream_receiver
.try_recv() .try_recv()
.map_err(|_| GatewayClientError::ConnectionAbruptlyClosed)?; .expect("stream sender was somehow dropped without sending anything!");
if let Some(res) = receive_res { if let Some(res) = receive_res {
let _res = res?; let _res = res?;
+1 -6
View File
@@ -16,14 +16,9 @@ tokio-util = { workspace = true, features = ["codec"], optional = true }
tokio-stream = { workspace = true } tokio-stream = { workspace = true }
# internal # internal
nym-noise = { path = "../../nymnoise" }
nym-sphinx = { path = "../../nymsphinx" } nym-sphinx = { path = "../../nymsphinx" }
nym-task = { path = "../../task", optional = true } nym-task = { path = "../../task", optional = true }
[features] [features]
default = ["client"] default = ["client"]
client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"] client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
[dev-dependencies]
nym-crypto = { path = "../../crypto" }
rand = { workspace = true }
+25 -57
View File
@@ -3,11 +3,11 @@
use dashmap::DashMap; use dashmap::DashMap;
use futures::StreamExt; use futures::StreamExt;
use nym_noise::config::NoiseConfig; use nym_sphinx::addressing::nodes::NymNodeRoutingAddress;
use nym_noise::upgrade_noise_initiator;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_sphinx::framing::codec::NymCodec; use nym_sphinx::framing::codec::NymCodec;
use nym_sphinx::framing::packet::FramedNymPacket; use nym_sphinx::framing::packet::FramedNymPacket;
use nym_sphinx::params::PacketType;
use nym_sphinx::NymPacket;
use std::io; use std::io;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::ops::Deref; use std::ops::Deref;
@@ -28,7 +28,6 @@ pub struct Config {
pub maximum_reconnection_backoff: Duration, pub maximum_reconnection_backoff: Duration,
pub initial_connection_timeout: Duration, pub initial_connection_timeout: Duration,
pub maximum_connection_buffer_size: usize, pub maximum_connection_buffer_size: usize,
pub use_legacy_packet_encoding: bool,
} }
impl Config { impl Config {
@@ -37,14 +36,12 @@ impl Config {
maximum_reconnection_backoff: Duration, maximum_reconnection_backoff: Duration,
initial_connection_timeout: Duration, initial_connection_timeout: Duration,
maximum_connection_buffer_size: usize, maximum_connection_buffer_size: usize,
use_legacy_packet_encoding: bool,
) -> Self { ) -> Self {
Config { Config {
initial_reconnection_backoff, initial_reconnection_backoff,
maximum_reconnection_backoff, maximum_reconnection_backoff,
initial_connection_timeout, initial_connection_timeout,
maximum_connection_buffer_size, maximum_connection_buffer_size,
use_legacy_packet_encoding,
} }
} }
} }
@@ -52,19 +49,23 @@ impl Config {
pub trait SendWithoutResponse { pub trait SendWithoutResponse {
// Without response in this context means we will not listen for anything we might get back (not // Without response in this context means we will not listen for anything we might get back (not
// that we should get anything), including any possible io errors // that we should get anything), including any possible io errors
fn send_without_response(&self, packet: MixPacket) -> io::Result<()>; fn send_without_response(
&self,
address: NymNodeRoutingAddress,
packet: NymPacket,
packet_type: PacketType,
) -> io::Result<()>;
} }
pub struct Client { pub struct Client {
active_connections: ActiveConnections, active_connections: ActiveConnections,
noise_config: NoiseConfig,
connections_count: Arc<AtomicUsize>, connections_count: Arc<AtomicUsize>,
config: Config, config: Config,
} }
#[derive(Default, Clone)] #[derive(Default, Clone)]
pub struct ActiveConnections { pub struct ActiveConnections {
inner: Arc<DashMap<SocketAddr, ConnectionSender>>, inner: Arc<DashMap<NymNodeRoutingAddress, ConnectionSender>>,
} }
impl ActiveConnections { impl ActiveConnections {
@@ -81,7 +82,7 @@ impl ActiveConnections {
} }
impl Deref for ActiveConnections { impl Deref for ActiveConnections {
type Target = DashMap<SocketAddr, ConnectionSender>; type Target = DashMap<NymNodeRoutingAddress, ConnectionSender>;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.inner &self.inner
} }
@@ -103,7 +104,6 @@ impl ConnectionSender {
struct ManagedConnection { struct ManagedConnection {
address: SocketAddr, address: SocketAddr,
noise_config: NoiseConfig,
message_receiver: ReceiverStream<FramedNymPacket>, message_receiver: ReceiverStream<FramedNymPacket>,
connection_timeout: Duration, connection_timeout: Duration,
current_reconnection: Arc<AtomicU32>, current_reconnection: Arc<AtomicU32>,
@@ -112,14 +112,12 @@ struct ManagedConnection {
impl ManagedConnection { impl ManagedConnection {
fn new( fn new(
address: SocketAddr, address: SocketAddr,
noise_config: NoiseConfig,
message_receiver: mpsc::Receiver<FramedNymPacket>, message_receiver: mpsc::Receiver<FramedNymPacket>,
connection_timeout: Duration, connection_timeout: Duration,
current_reconnection: Arc<AtomicU32>, current_reconnection: Arc<AtomicU32>,
) -> Self { ) -> Self {
ManagedConnection { ManagedConnection {
address, address,
noise_config,
message_receiver: ReceiverStream::new(message_receiver), message_receiver: ReceiverStream::new(message_receiver),
connection_timeout, connection_timeout,
current_reconnection, current_reconnection,
@@ -134,21 +132,9 @@ impl ManagedConnection {
Ok(stream_res) => match stream_res { Ok(stream_res) => match stream_res {
Ok(stream) => { Ok(stream) => {
debug!("Managed to establish connection to {}", self.address); debug!("Managed to establish connection to {}", self.address);
// if we managed to connect, reset the reconnection count (whatever it might have been)
let noise_stream =
match upgrade_noise_initiator(stream, &self.noise_config).await {
Ok(noise_stream) => noise_stream,
Err(err) => {
error!("Failed to perform Noise handshake with {address} - {err}");
// we failed to finish the noise handshake - increase reconnection attempt
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
return;
}
};
// if we managed to connect AND do the noise handshake, reset the reconnection count (whatever it might have been)
self.current_reconnection.store(0, Ordering::Release); self.current_reconnection.store(0, Ordering::Release);
debug!("Noise initiator handshake completed for {:?}", address); Framed::new(stream, NymCodec)
Framed::new(noise_stream, NymCodec)
} }
Err(err) => { Err(err) => {
debug!("failed to establish connection to {address} (err: {err})",); debug!("failed to establish connection to {address} (err: {err})",);
@@ -181,14 +167,9 @@ impl ManagedConnection {
} }
impl Client { impl Client {
pub fn new( pub fn new(config: Config, connections_count: Arc<AtomicUsize>) -> Client {
config: Config,
noise_config: NoiseConfig,
connections_count: Arc<AtomicUsize>,
) -> Client {
Client { Client {
active_connections: Default::default(), active_connections: Default::default(),
noise_config,
connections_count, connections_count,
config, config,
} }
@@ -215,7 +196,7 @@ impl Client {
} }
} }
fn make_connection(&self, address: SocketAddr, pending_packet: FramedNymPacket) { fn make_connection(&self, address: NymNodeRoutingAddress, pending_packet: FramedNymPacket) {
let (sender, receiver) = mpsc::channel(self.config.maximum_connection_buffer_size); let (sender, receiver) = mpsc::channel(self.config.maximum_connection_buffer_size);
// this CAN'T fail because we just created the channel which has a non-zero capacity // this CAN'T fail because we just created the channel which has a non-zero capacity
@@ -243,7 +224,6 @@ impl Client {
let initial_connection_timeout = self.config.initial_connection_timeout; let initial_connection_timeout = self.config.initial_connection_timeout;
let connections_count = self.connections_count.clone(); let connections_count = self.connections_count.clone();
let noise_config = self.noise_config.clone();
tokio::spawn(async move { tokio::spawn(async move {
// before executing the manager, wait for what was specified, if anything // before executing the manager, wait for what was specified, if anything
if let Some(backoff) = backoff { if let Some(backoff) = backoff {
@@ -253,8 +233,7 @@ impl Client {
connections_count.fetch_add(1, Ordering::SeqCst); connections_count.fetch_add(1, Ordering::SeqCst);
ManagedConnection::new( ManagedConnection::new(
address, address.into(),
noise_config,
receiver, receiver,
initial_connection_timeout, initial_connection_timeout,
current_reconnection_attempt, current_reconnection_attempt,
@@ -267,19 +246,18 @@ impl Client {
} }
impl SendWithoutResponse for Client { impl SendWithoutResponse for Client {
fn send_without_response(&self, packet: MixPacket) -> io::Result<()> { fn send_without_response(
let address = packet.next_hop_address(); &self,
trace!("Sending packet to {address}"); address: NymNodeRoutingAddress,
packet: NymPacket,
// TODO: optimisation for the future: rather than constantly using legacy encoding, packet_type: PacketType,
// once we're addressing by node_id (and thus have full node info here), ) -> io::Result<()> {
// we could simply infer supported encoding based on their version trace!("Sending packet to {address:?}");
let framed_packet = let framed_packet = FramedNymPacket::new(packet, packet_type);
FramedNymPacket::from_mix_packet(packet, self.config.use_legacy_packet_encoding);
let Some(sender) = self.active_connections.get_mut(&address) else { let Some(sender) = self.active_connections.get_mut(&address) else {
// there was never a connection to begin with // there was never a connection to begin with
debug!("establishing initial connection to {address}"); debug!("establishing initial connection to {}", address);
// it's not a 'big' error, but we did not manage to send the packet, but queue the packet // it's not a 'big' error, but we did not manage to send the packet, but queue the packet
// for sending for as soon as the connection is created // for sending for as soon as the connection is created
self.make_connection(address, framed_packet); self.make_connection(address, framed_packet);
@@ -324,25 +302,15 @@ impl SendWithoutResponse for Client {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use nym_crypto::asymmetric::x25519;
use nym_noise::config::NoiseNetworkView;
use rand::rngs::OsRng;
fn dummy_client() -> Client { fn dummy_client() -> Client {
let mut rng = OsRng; //for test only, so we don't care if rng source isn't crypto grade
Client::new( Client::new(
Config { Config {
initial_reconnection_backoff: Duration::from_millis(10_000), initial_reconnection_backoff: Duration::from_millis(10_000),
maximum_reconnection_backoff: Duration::from_millis(300_000), maximum_reconnection_backoff: Duration::from_millis(300_000),
initial_connection_timeout: Duration::from_millis(1_500), initial_connection_timeout: Duration::from_millis(1_500),
maximum_connection_buffer_size: 128, maximum_connection_buffer_size: 128,
use_legacy_packet_encoding: false,
}, },
NoiseConfig::new(
Arc::new(x25519::KeyPair::new(&mut rng)),
NoiseNetworkView::new_empty(),
Duration::from_millis(1_500),
),
Default::default(), Default::default(),
) )
} }
@@ -19,7 +19,6 @@ nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-c
nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" } nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" }
nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" } nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" }
nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" } nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" }
nym-performance-contract-common = { path = "../../cosmwasm-smart-contracts/nym-performance-contract" }
nym-serde-helpers = { path = "../../serde-helpers", features = ["hex", "base64"] } nym-serde-helpers = { path = "../../serde-helpers", features = ["hex", "base64"] }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
@@ -16,8 +16,8 @@ async fn main() {
let prefix = "n"; let prefix = "n";
let denom: Denom = "unym".parse().unwrap(); let denom: Denom = "unym".parse().unwrap();
let signer_mnemonic: bip39::Mnemonic = "<MNEMONIC WITH FUNDS HERE>".parse().unwrap(); let signer_mnemonic: bip39::Mnemonic = "<MNEMONIC WITH FUNDS HERE>".parse().unwrap();
let validator = "https://rpc.sandbox.nymtech.net"; let validator = "https://qwerty-validator.qa.nymte.ch";
let to_address: AccountId = "n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa".parse().unwrap(); let to_address: AccountId = "n19kdst4srf76xgwe55jg32mpcpcyf6aqgp6qrdk".parse().unwrap();
let signer = DirectSecp256k1HdWallet::from_mnemonic(prefix, signer_mnemonic); let signer = DirectSecp256k1HdWallet::from_mnemonic(prefix, signer_mnemonic);
let signer_address = signer.try_derive_accounts().unwrap()[0].address().clone(); let signer_address = signer.try_derive_accounts().unwrap()[0].address().clone();

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