Compare commits
53 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 70fb7d75d6 | |||
| f11b2caeb1 | |||
| 1a07dbb09c | |||
| 9587247536 | |||
| b975d08342 | |||
| 8e44f9f07f | |||
| 8461d085a5 | |||
| af9f6e5ca0 | |||
| a9ae2017f5 | |||
| 09ebe7f9e9 | |||
| b72915c224 | |||
| add3e864e3 | |||
| 578c9b0567 | |||
| 8f6f696f36 | |||
| e9165763b6 | |||
| 6c1149708b | |||
| aaf6931d78 | |||
| 97804f2fe5 | |||
| 802d9b69ca | |||
| 7313857bc8 | |||
| 779174ada5 | |||
| 8771c1dfa6 | |||
| 329ad83fc0 | |||
| aea5872ad0 | |||
| 9e9abd74d7 | |||
| 3832508af7 | |||
| 69a4e33b17 | |||
| 83385421ff | |||
| ec53b570dc | |||
| ebcc658f98 | |||
| 6a155721c6 | |||
| 1bb8b3a3ec | |||
| 8d1a16eb02 | |||
| 8d10cf70e9 | |||
| e32df10b4d | |||
| d1660c01e6 | |||
| 14378b1db9 | |||
| 35bbf5fd84 | |||
| c374a4935a | |||
| 513f4f652d | |||
| 82b9425ca6 | |||
| 615e98b166 | |||
| b11f6c6c70 | |||
| 2f5e8e0bcd | |||
| 812a8782b4 | |||
| 089c47cce7 | |||
| 833114372a | |||
| d6bb0979d0 | |||
| fa1d47e941 | |||
| 44ec6d6bc8 | |||
| 6d47046a38 | |||
| 5cfd09cd99 | |||
| 40b4670d80 |
@@ -31,33 +31,26 @@ jobs:
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
|
||||
- name: Install wasm-opt
|
||||
uses: ./.github/actions/install-wasm-opt
|
||||
with:
|
||||
version: '114'
|
||||
|
||||
- name: Install cosmwasm-check
|
||||
run: cargo install cosmwasm-check
|
||||
|
||||
- name: Build release contracts
|
||||
run: make contracts
|
||||
run: make publish-contracts
|
||||
|
||||
- name: Prepare build output
|
||||
shell: bash
|
||||
env:
|
||||
OUTPUT_DIR: ci-contract-builds/${{ github.ref_name }}
|
||||
run: |
|
||||
cp contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/nym_coconut_dkg.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/cw3_flex_multisig.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/nym_ecash.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/nym_pool_contract.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/nym_performance_contract.wasm $OUTPUT_DIR
|
||||
find contracts/artifacts -maxdepth 1 -type f -name '*.wasm' -exec cp {} $OUTPUT_DIR \;
|
||||
# Also include the optimizer-generated checksums if present
|
||||
if [ -f contracts/artifacts/checksums.txt ]; then
|
||||
cp contracts/artifacts/checksums.txt $OUTPUT_DIR
|
||||
fi
|
||||
|
||||
- name: Deploy branch to CI www
|
||||
continue-on-error: true
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
name: Run SonarQube Scan
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
# pull_request:
|
||||
# types: [opened, synchronize, reopened]
|
||||
jobs:
|
||||
sonarqube:
|
||||
name: SonarQube
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis
|
||||
- name: SonarQube Scan
|
||||
uses: SonarSource/sonarqube-scan-action@v5
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
@@ -5,8 +5,15 @@ on:
|
||||
inputs:
|
||||
gateway_probe_git_ref:
|
||||
type: string
|
||||
default: nym-vpn-core-v1.4.0
|
||||
required: true
|
||||
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:
|
||||
WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-agent"
|
||||
CONTAINER_NAME: "node-status-agent"
|
||||
@@ -31,10 +38,9 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.45.4
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
run: |
|
||||
yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
- name: cleanup-gateway-probe-ref
|
||||
id: cleanup_gateway_probe_ref
|
||||
@@ -43,19 +49,32 @@ jobs:
|
||||
GIT_REF_SLUG="${GATEWAY_PROBE_GIT_REF//\//-}"
|
||||
echo "git_ref=${GIT_REF_SLUG}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Remove existing tag if exists
|
||||
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: Set GIT_TAG variable
|
||||
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Create tag
|
||||
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 }}"
|
||||
git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}
|
||||
- name: Set RELEASE_TAG variable
|
||||
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
|
||||
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.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}
|
||||
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 push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
name: Build and upload Node Status API container to harbor.nymte.ch
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
inputs:
|
||||
release_image:
|
||||
description: 'Tag image as a release'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-api"
|
||||
CONTAINER_NAME: "node-status-api"
|
||||
@@ -26,30 +32,39 @@ jobs:
|
||||
git config --global user.name "Lawrence Stalder"
|
||||
|
||||
- name: Get version from cargo.toml
|
||||
uses: mikefarah/yq@v4.45.4
|
||||
id: get_version
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
- name: Check if tag exists
|
||||
run: |
|
||||
if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then
|
||||
echo "Tag ${{ steps.get_version.outputs.result }} already exists"
|
||||
fi
|
||||
yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
- name: Remove existing tag if exists
|
||||
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: Set GIT_TAG variable
|
||||
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
|
||||
|
||||
- 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: Set RELEASE_TAG variable
|
||||
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
|
||||
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 build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
|
||||
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ validator-api/keypair
|
||||
contracts/mixnet/code_id
|
||||
contracts/mixnet/Justfile
|
||||
contracts/mixnet/Makefile
|
||||
artifacts
|
||||
contracts/artifacts
|
||||
validator-config
|
||||
*.patch
|
||||
validator-api-config.toml
|
||||
|
||||
+116
@@ -4,6 +4,122 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2025.13-emmental] (2025-07-22)
|
||||
|
||||
- fix: don't allow mixnode running in exit mode ([#5898])
|
||||
- fix contract build process in Makefile ([#5892])
|
||||
- bugfix: ignore 'Send' responses when claiming bandwidth ([#5884])
|
||||
- Update push-node-status-agent.yaml ([#5882])
|
||||
- listen for shutdown signals during nym-node startup ([#5879])
|
||||
- feat: forbid running mixnode + entry on the same node ([#5878])
|
||||
- chore: 1.88 clippy ([#5877])
|
||||
- Batch SQL writes for packet stats ([#5874])
|
||||
- fix the broken link ([#5873])
|
||||
- Set busy_timeout in sqlx ([#5872])
|
||||
- feat: basic performance contract integration [within Nym API] ([#5871])
|
||||
- scraper bugfix: ignore precommits from missing validators ([#5867])
|
||||
- Return true remaining ([#5866])
|
||||
- Make Mix hops optional for Mixnet Client SURBs ([#5861])
|
||||
- Check gateway supported versions ([#5860])
|
||||
- Add build info endpoints ([#5857])
|
||||
- Clear out screaming logs ([#5856])
|
||||
- fix removal of qa env ([#5855])
|
||||
- Use display when printing paths ([#5853])
|
||||
- feat: initial performance contract ([#5833])
|
||||
- Security patches for the `dkg` crate ([#5828])
|
||||
- HTTP Discovery objects & network defaults ([#5814])
|
||||
|
||||
[#5898]: https://github.com/nymtech/nym/pull/5898
|
||||
[#5892]: https://github.com/nymtech/nym/pull/5892
|
||||
[#5884]: https://github.com/nymtech/nym/pull/5884
|
||||
[#5882]: https://github.com/nymtech/nym/pull/5882
|
||||
[#5879]: https://github.com/nymtech/nym/pull/5879
|
||||
[#5878]: https://github.com/nymtech/nym/pull/5878
|
||||
[#5877]: https://github.com/nymtech/nym/pull/5877
|
||||
[#5874]: https://github.com/nymtech/nym/pull/5874
|
||||
[#5873]: https://github.com/nymtech/nym/pull/5873
|
||||
[#5872]: https://github.com/nymtech/nym/pull/5872
|
||||
[#5871]: https://github.com/nymtech/nym/pull/5871
|
||||
[#5867]: https://github.com/nymtech/nym/pull/5867
|
||||
[#5866]: https://github.com/nymtech/nym/pull/5866
|
||||
[#5861]: https://github.com/nymtech/nym/pull/5861
|
||||
[#5860]: https://github.com/nymtech/nym/pull/5860
|
||||
[#5857]: https://github.com/nymtech/nym/pull/5857
|
||||
[#5856]: https://github.com/nymtech/nym/pull/5856
|
||||
[#5855]: https://github.com/nymtech/nym/pull/5855
|
||||
[#5853]: https://github.com/nymtech/nym/pull/5853
|
||||
[#5833]: https://github.com/nymtech/nym/pull/5833
|
||||
[#5828]: https://github.com/nymtech/nym/pull/5828
|
||||
[#5814]: https://github.com/nymtech/nym/pull/5814
|
||||
|
||||
## [2025.12-dolcelatte] (2025-07-07)
|
||||
|
||||
- bugfix: key-rotation + reply SURBs ([#5876])
|
||||
- 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])
|
||||
|
||||
@@ -0,0 +1,686 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
Nym is a privacy platform that uses mixnet technology to protect against metadata surveillance. The platform consists of several key components:
|
||||
- Mixnet nodes (mixnodes) for packet mixing
|
||||
- Gateways (entry/exit points for the network)
|
||||
- Clients for interacting with the network
|
||||
- Network monitoring tools
|
||||
- Validators for network consensus
|
||||
- Various service providers and integrations
|
||||
|
||||
## Build Commands
|
||||
|
||||
### Rust Components
|
||||
|
||||
```bash
|
||||
# Default build (debug)
|
||||
cargo build
|
||||
|
||||
# Release build
|
||||
cargo build --release
|
||||
|
||||
# Build a specific package
|
||||
cargo build -p <package-name>
|
||||
|
||||
# Build main components
|
||||
make build
|
||||
|
||||
# Build release versions of main binaries and contracts
|
||||
make build-release
|
||||
|
||||
# Build specific binaries
|
||||
make build-nym-cli
|
||||
cargo build -p nym-node --release
|
||||
cargo build -p nym-api --release
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run clippy, unit tests, and formatting
|
||||
make test
|
||||
|
||||
# Run all tests including slow tests
|
||||
make test-all
|
||||
|
||||
# Run clippy on all workspaces
|
||||
make clippy
|
||||
|
||||
# Run unit tests for a specific package
|
||||
cargo test -p <package-name>
|
||||
|
||||
# Run only expensive/ignored tests
|
||||
cargo test --workspace -- --ignored
|
||||
|
||||
# Run API tests
|
||||
dotenv -f envs/sandbox.env -- cargo test --test public-api-tests
|
||||
|
||||
# Run tests with specific log level
|
||||
RUST_LOG=debug cargo test -p <package-name>
|
||||
|
||||
# Run specific test scripts
|
||||
./nym-node/tests/test_apis.sh
|
||||
./scripts/wireguard-exit-policy/exit-policy-tests.sh
|
||||
```
|
||||
|
||||
### Linting and Formatting
|
||||
|
||||
```bash
|
||||
# Run rustfmt on all code
|
||||
make fmt
|
||||
|
||||
# Check formatting without modifying
|
||||
cargo fmt --all -- --check
|
||||
|
||||
# Run clippy with all targets
|
||||
cargo clippy --workspace --all-targets -- -D warnings
|
||||
|
||||
# TypeScript linting
|
||||
yarn lint
|
||||
yarn lint:fix
|
||||
yarn types:lint:fix
|
||||
|
||||
# Check dependencies for security/licensing issues
|
||||
cargo deny check
|
||||
```
|
||||
|
||||
### WASM Components
|
||||
|
||||
```bash
|
||||
# Build all WASM components
|
||||
make sdk-wasm-build
|
||||
|
||||
# Build TypeScript SDK
|
||||
yarn build:sdk
|
||||
npx lerna run --scope @nymproject/sdk build --stream
|
||||
|
||||
# Build and test WASM components
|
||||
make sdk-wasm
|
||||
|
||||
# Build specific WASM packages
|
||||
cd wasm/client && make
|
||||
cd wasm/mix-fetch && make
|
||||
cd wasm/node-tester && make
|
||||
```
|
||||
|
||||
### Contract Development
|
||||
|
||||
```bash
|
||||
# Build all contracts
|
||||
make contracts
|
||||
|
||||
# Build contracts in release mode
|
||||
make build-release-contracts
|
||||
|
||||
# Generate contract schemas
|
||||
make contract-schema
|
||||
|
||||
# Run wasm-opt on contracts
|
||||
make wasm-opt-contracts
|
||||
|
||||
# Check contracts with cosmwasm-check
|
||||
make cosmwasm-check-contracts
|
||||
```
|
||||
|
||||
### Running Components
|
||||
|
||||
```bash
|
||||
# Run nym-node as a mixnode
|
||||
cargo run -p nym-node -- run --mode mixnode
|
||||
|
||||
# Run nym-node as a gateway
|
||||
cargo run -p nym-node -- run --mode gateway
|
||||
|
||||
# Run the network monitor
|
||||
cargo run -p nym-network-monitor
|
||||
|
||||
# Run the API server
|
||||
cargo run -p nym-api
|
||||
|
||||
# Run with specific environment
|
||||
dotenv -f envs/sandbox.env -- cargo run -p nym-api
|
||||
|
||||
# Start a local network
|
||||
./scripts/localnet_start.sh
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
The Nym platform consists of various components organized as a monorepo:
|
||||
|
||||
1. **Core Mixnet Infrastructure**:
|
||||
- `nym-node`: Core binary supporting mixnode and gateway modes
|
||||
- `common/nymsphinx`: Implementation of the Sphinx packet format
|
||||
- `common/topology`: Network topology management
|
||||
- `common/types`: Shared data types across components
|
||||
|
||||
2. **Network Monitoring**:
|
||||
- `nym-network-monitor`: Monitors the network's reliability and performance
|
||||
- `nym-api`: API server for network stats and monitoring data
|
||||
- Metrics tracking for nodes, routes, and overall network health
|
||||
|
||||
3. **Client Implementations**:
|
||||
- `clients/native`: Native Rust client implementation
|
||||
- `clients/socks5`: SOCKS5 proxy client for standard applications
|
||||
- `wasm`: WebAssembly client implementations (for browsers)
|
||||
- `nym-connect`: Desktop and mobile clients
|
||||
|
||||
4. **Blockchain & Smart Contracts**:
|
||||
- `common/cosmwasm-smart-contracts`: Smart contract implementations
|
||||
- `contracts`: CosmWasm contracts for the Nym network
|
||||
- `common/ledger`: Blockchain integration
|
||||
|
||||
5. **Utilities & Tools**:
|
||||
- `tools`: Various CLI tools and utilities
|
||||
- `sdk`: SDKs for different languages and platforms
|
||||
- `documentation`: Documentation generation and management
|
||||
|
||||
## Packet System
|
||||
|
||||
Nym uses a modified Sphinx packet format for its mixnet:
|
||||
|
||||
1. **Message Chunking**:
|
||||
- Messages are divided into "sets" and "fragments"
|
||||
- Each fragment fits in a single Sphinx packet
|
||||
- The `common/nymsphinx/chunking` module handles message fragmentation
|
||||
|
||||
2. **Routing**:
|
||||
- Packets traverse through 3 layers of mixnodes
|
||||
- Routing information is encrypted in layers (onion routing)
|
||||
- The final gateway receives and processes the messages
|
||||
|
||||
3. **Monitoring**:
|
||||
- Monitoring system tracks packet delivery through the network
|
||||
- Routes are analyzed for reliability statistics
|
||||
- Node performance metrics are collected
|
||||
|
||||
## Network Protocol
|
||||
|
||||
Nym implements the Loopix mixnet design with several key privacy features:
|
||||
|
||||
1. **Continuous-time Mixing**:
|
||||
- Each mixnode delays messages independently with an exponential distribution
|
||||
- This creates random reordering of packets, destroying timing correlations
|
||||
- Offers better anonymity properties than batch mixing approaches
|
||||
|
||||
2. **Cover Traffic**:
|
||||
- Clients and nodes generate dummy "loop" packets that circulate through the network
|
||||
- These packets are indistinguishable from real traffic
|
||||
- Creates a baseline level of traffic that hides actual communication patterns
|
||||
- Provides unobservability (hiding when and how much real traffic is being sent)
|
||||
|
||||
3. **Stratified Network Architecture**:
|
||||
- Traffic flows through Entry Gateway → 3 Mixnode Layers → Exit Gateway
|
||||
- Path selection is independent per-message (unlike Tor)
|
||||
- Each node connects only to adjacent layers
|
||||
|
||||
4. **Anonymous Replies**:
|
||||
- Single-Use Reply Blocks (SURBs) allow receiving messages without revealing identity
|
||||
- Enables bidirectional communication while maintaining privacy
|
||||
|
||||
## Network Monitoring Architecture
|
||||
|
||||
The network monitoring system is a core component that measures mixnet reliability:
|
||||
|
||||
1. The `nym-network-monitor` sends test packets through the network
|
||||
2. These packets follow predefined routes through multiple mixnodes
|
||||
3. Metrics are collected about:
|
||||
- Successful and failed packet deliveries
|
||||
- Node reliability (percentage of successful packet handling)
|
||||
- Route reliability (which specific route combinations work best)
|
||||
4. Results are stored in the database and used by `nym-api` to:
|
||||
- Present node performance statistics
|
||||
- Determine network rewards
|
||||
- Provide route selection guidance to clients
|
||||
|
||||
In the current branch, metrics collection is being enhanced with a fanout approach to submit to multiple API endpoints.
|
||||
|
||||
## Development Environment
|
||||
|
||||
### Required Dependencies
|
||||
|
||||
- Rust toolchain (stable, 1.80+)
|
||||
- Node.js (v20+) and yarn for TypeScript components
|
||||
- SQLite for local database development
|
||||
- PostgreSQL for API database (optional, for full API functionality)
|
||||
- CosmWasm tools for contract development
|
||||
- For building contracts: `wasm-opt` tool from `binaryen`
|
||||
- Python 3.8+ for some scripts
|
||||
- Docker (optional, for containerized development)
|
||||
- protoc (Protocol Buffers compiler) for some components
|
||||
|
||||
### Environment Configurations
|
||||
|
||||
The `envs/` directory contains pre-configured environments:
|
||||
|
||||
#### Available Environments
|
||||
|
||||
- **`local.env`**: Local development environment
|
||||
- Points to local services (localhost)
|
||||
- Uses test mnemonics and keys
|
||||
- Ideal for testing without external dependencies
|
||||
|
||||
- **`sandbox.env`**: Sandbox test network
|
||||
- Public test network with real nodes
|
||||
- Test tokens available from faucet
|
||||
- Contract addresses for sandbox deployment
|
||||
- API: https://sandbox-nym-api1.nymtech.net
|
||||
|
||||
- **`mainnet.env`**: Production mainnet
|
||||
- Real network with real tokens
|
||||
- Production contract addresses
|
||||
- API: https://validator.nymtech.net
|
||||
- Use with caution!
|
||||
|
||||
- **`canary.env`**: Canary deployment
|
||||
- Pre-release testing environment
|
||||
- Tests new features before mainnet
|
||||
|
||||
- **`mainnet-local-api.env`**: Hybrid environment
|
||||
- Uses mainnet contracts but local API
|
||||
- Useful for API development against mainnet data
|
||||
|
||||
#### Key Environment Variables
|
||||
|
||||
```bash
|
||||
# Network configuration
|
||||
NETWORK_NAME=sandbox # Network identifier
|
||||
BECH32_PREFIX=n # Address prefix (n for sandbox, n for mainnet)
|
||||
NYM_API=https://sandbox-nym-api1.nymtech.net/api
|
||||
NYXD=https://rpc.sandbox.nymtech.net
|
||||
NYM_API_NETWORK=sandbox
|
||||
|
||||
# Contract addresses (network-specific)
|
||||
MIXNET_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav
|
||||
VESTING_CONTRACT_ADDRESS=n1unyuj8qnmygvzuex3dwmg9yzt9alhvyeat0uu0jedg2wj33efl5qackslz
|
||||
# ... other contract addresses
|
||||
|
||||
# Mnemonic for testing (NEVER use in production)
|
||||
MNEMONIC="clutch captain shoe salt awake harvest setup primary inmate ugly among become"
|
||||
|
||||
# API Keys and tokens
|
||||
IPINFO_API_TOKEN=your_token_here
|
||||
AUTHENTICATOR_PASSWORD=password_here
|
||||
|
||||
# Logging
|
||||
RUST_LOG=info # Options: error, warn, info, debug, trace
|
||||
RUST_BACKTRACE=1 # Enable backtraces
|
||||
|
||||
# Database
|
||||
DATABASE_URL=postgresql://user:pass@localhost/nym_api
|
||||
```
|
||||
|
||||
#### Using Environment Files
|
||||
|
||||
```bash
|
||||
# Load environment and run command
|
||||
dotenv -f envs/sandbox.env -- cargo run -p nym-api
|
||||
|
||||
# Export to shell
|
||||
source envs/sandbox.env
|
||||
|
||||
# Use with make targets
|
||||
dotenv -f envs/sandbox.env -- make run-api-tests
|
||||
```
|
||||
|
||||
## Initial Setup
|
||||
|
||||
### First Time Setup
|
||||
|
||||
1. **Install Prerequisites**
|
||||
```bash
|
||||
# Install Rust
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
# Install Node.js and yarn
|
||||
# Via nvm (recommended):
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
|
||||
nvm install 20
|
||||
npm install -g yarn
|
||||
|
||||
# Install build tools
|
||||
# Ubuntu/Debian:
|
||||
sudo apt-get install build-essential pkg-config libssl-dev protobuf-compiler libpq-dev
|
||||
|
||||
# macOS:
|
||||
brew install protobuf postgresql
|
||||
|
||||
# Install wasm-opt for contract builds
|
||||
npm install -g wasm-opt
|
||||
|
||||
# Add wasm target for Rust
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
|
||||
2. **Clone and Setup Repository**
|
||||
```bash
|
||||
git clone https://github.com/nymtech/nym.git
|
||||
cd nym/nym
|
||||
|
||||
# Install JavaScript dependencies
|
||||
yarn install
|
||||
|
||||
# Build the project
|
||||
make build
|
||||
```
|
||||
|
||||
3. **Database Setup (Optional, for API development)**
|
||||
```bash
|
||||
# Install PostgreSQL
|
||||
# Create database
|
||||
createdb nym_api
|
||||
|
||||
# Run migrations (from nym-api directory)
|
||||
cd nym-api
|
||||
sqlx migrate run
|
||||
```
|
||||
|
||||
### Quick Start
|
||||
|
||||
```bash
|
||||
# Run a mixnode locally
|
||||
dotenv -f envs/sandbox.env -- cargo run -p nym-node -- run --mode mixnode --id my-mixnode
|
||||
|
||||
# Run a gateway locally
|
||||
dotenv -f envs/sandbox.env -- cargo run -p nym-node -- run --mode gateway --id my-gateway
|
||||
|
||||
# Run the API server
|
||||
dotenv -f envs/sandbox.env -- cargo run -p nym-api
|
||||
|
||||
# Run a client
|
||||
cargo run -p nym-client -- init --id my-client
|
||||
cargo run -p nym-client -- run --id my-client
|
||||
```
|
||||
|
||||
## CI/CD Pipeline
|
||||
|
||||
The project uses GitHub Actions for CI/CD with several key workflows:
|
||||
|
||||
1. **Build and Test**:
|
||||
- `ci-build.yml`: Main build workflow for Rust components
|
||||
- Tests are run on multiple platforms (Linux, Windows, macOS)
|
||||
- Includes formatting check (rustfmt) and linting (clippy)
|
||||
|
||||
2. **Release Process**:
|
||||
- Binary artifacts are published on release tags
|
||||
- Multiple platform builds are created
|
||||
|
||||
3. **Documentation**:
|
||||
- Documentation is automatically built and deployed
|
||||
|
||||
## Database Structure
|
||||
|
||||
The system uses SQLite databases with tables like:
|
||||
- `mixnode_status`: Status information about mixnodes
|
||||
- `gateway_status`: Status information about gateways
|
||||
- `routes`: Route performance information (success/failure of specific paths)
|
||||
- `monitor_run`: Information about monitoring test runs
|
||||
|
||||
## Development Workflows
|
||||
|
||||
### Running a Node
|
||||
|
||||
To run the mixnode or gateway:
|
||||
|
||||
```bash
|
||||
# Run nym-node as a mixnode with specified identity
|
||||
cargo run -p nym-node -- run --mode mixnode --id my-mixnode
|
||||
|
||||
# Run nym-node as a gateway
|
||||
cargo run -p nym-node -- run --mode gateway --id my-gateway
|
||||
```
|
||||
|
||||
### Configuration
|
||||
|
||||
Nodes can be configured with files in various locations:
|
||||
- Command-line arguments
|
||||
- Environment variables
|
||||
- `.env` files specified with `--config-env-file`
|
||||
|
||||
### Monitoring
|
||||
|
||||
To monitor the health of your node:
|
||||
- View logs for real-time information
|
||||
- Use the node's HTTP API for status information
|
||||
- Check the explorer for public node statistics
|
||||
|
||||
## Common Libraries
|
||||
|
||||
- `common/types`: Shared data types across all components
|
||||
- `common/crypto`: Cryptographic primitives and wrappers
|
||||
- `common/client-core`: Core client functionality
|
||||
- `common/gateway-client`: Client-gateway communication
|
||||
- `common/task`: Task management and concurrency utilities
|
||||
- `common/nymsphinx`: Sphinx packet implementation for mixnet
|
||||
- `common/topology`: Network topology management
|
||||
- `common/credentials`: Credential system for privacy-preserving authentication
|
||||
- `common/bandwidth-controller`: Bandwidth management and accounting
|
||||
|
||||
## Code Conventions
|
||||
|
||||
- Error handling: Use anyhow/thiserror for structured error handling
|
||||
- Logging: Use the tracing framework for logging and diagnostics
|
||||
- State management: Generally use Tokio/futures for async code
|
||||
- Configuration: Use the config crate and env vars with defaults
|
||||
- Database: Use sqlx for type-safe database queries
|
||||
- Follow clippy recommendations and rustfmt formatting
|
||||
- Use semantic commit messages: feat, fix, docs, refactor, test, chore
|
||||
|
||||
## When Making Changes
|
||||
|
||||
- Run `make test` before submitting PRs
|
||||
- Follow Rust naming conventions
|
||||
- Use `clippy` to check for common issues
|
||||
- Update SQLx query caches when modifying DB queries: `cargo sqlx prepare`
|
||||
- Consider backward compatibility for protocol changes
|
||||
- Use lefthook pre-commit hooks for TypeScript formatting
|
||||
- Run `cargo deny check` to verify dependency compliance
|
||||
- Test against both sandbox and local environments when possible
|
||||
- Update relevant documentation and CHANGELOG.md
|
||||
|
||||
## Development Tools
|
||||
|
||||
### Useful Cargo Commands
|
||||
|
||||
```bash
|
||||
# Check for outdated dependencies
|
||||
cargo outdated
|
||||
|
||||
# Analyze binary size
|
||||
cargo bloat --release -p nym-node
|
||||
|
||||
# Generate dependency graph
|
||||
cargo tree -p nym-api
|
||||
|
||||
# Run with instrumentation
|
||||
cargo run --features profiling -p nym-node
|
||||
|
||||
# Check for security advisories
|
||||
cargo audit
|
||||
```
|
||||
|
||||
### Database Tools
|
||||
|
||||
```bash
|
||||
# SQLx CLI for migrations
|
||||
cargo install sqlx-cli
|
||||
|
||||
# Create new migration
|
||||
cd nym-api && sqlx migrate add <migration_name>
|
||||
|
||||
# Prepare query metadata for offline compilation
|
||||
cargo sqlx prepare --workspace
|
||||
|
||||
# View database schema
|
||||
./nym-api/enter_db.sh
|
||||
```
|
||||
|
||||
### Development Scripts
|
||||
|
||||
- `scripts/build_topology.py`: Generate network topology files
|
||||
- `scripts/node_api_check.py`: Verify node API endpoints
|
||||
- `scripts/network_tunnel_manager.sh`: Manage network tunnels
|
||||
- `scripts/localnet_start.sh`: Start a local test network
|
||||
- Various deployment scripts in `deployment/` for different environments
|
||||
|
||||
## Debugging
|
||||
|
||||
- Enable more verbose logging with the RUST_LOG environment variable:
|
||||
```
|
||||
RUST_LOG=debug,nym_node=trace cargo run -p nym-node -- run --mode mixnode
|
||||
```
|
||||
- Use the HTTP API endpoints for status information
|
||||
- Check monitoring data in the database for network performance metrics
|
||||
- For complex issues, use tracing tools to follow packet flow
|
||||
- Enable backtraces: `RUST_BACKTRACE=full`
|
||||
- For WASM debugging: Use browser developer tools with source maps
|
||||
|
||||
## Deployment and Advanced Configurations
|
||||
|
||||
### Deployment Structure
|
||||
|
||||
The `deployment/` directory contains Ansible playbooks and configurations for various deployment scenarios:
|
||||
|
||||
- **`aws/`**: AWS-specific deployment configurations
|
||||
- **`mixnode/`**: Mixnode deployment playbooks
|
||||
- **`gateway/`**: Gateway deployment playbooks
|
||||
- **`validator/`**: Validator node deployment
|
||||
- **`sandbox-v2/`**: Complete sandbox environment setup
|
||||
- **`big-dipper-2/`**: Block explorer deployment
|
||||
|
||||
### Sandbox V2 Deployment
|
||||
|
||||
The sandbox-v2 deployment (`deployment/sandbox-v2/`) provides a complete test environment:
|
||||
|
||||
```bash
|
||||
# Key playbooks:
|
||||
- deploy.yaml # Main deployment orchestrator
|
||||
- deploy-mixnodes.yaml # Deploy mixnodes
|
||||
- deploy-gateways.yaml # Deploy gateways
|
||||
- deploy-validators.yaml # Deploy validator nodes
|
||||
- deploy-nym-api.yaml # Deploy API services
|
||||
```
|
||||
|
||||
### Custom Environment Setup
|
||||
|
||||
To create a custom environment:
|
||||
|
||||
1. Copy an existing env file: `cp envs/sandbox.env envs/custom.env`
|
||||
2. Modify the network endpoints and contract addresses
|
||||
3. Update the `NETWORK_NAME` to your identifier
|
||||
4. Set appropriate mnemonics and keys (use fresh ones for production!)
|
||||
|
||||
### Contract Addresses
|
||||
|
||||
Contract addresses are network-specific and defined in environment files:
|
||||
- Mixnet contract: Manages mixnode/gateway registry
|
||||
- Vesting contract: Handles token vesting schedules
|
||||
- Coconut contracts: Privacy-preserving credentials
|
||||
- Name service: Human-readable address mapping
|
||||
- Ecash contract: Electronic cash functionality
|
||||
|
||||
### Local Network Setup
|
||||
|
||||
For a completely local network:
|
||||
```bash
|
||||
# Start local chain
|
||||
./scripts/localnet_start.sh
|
||||
|
||||
# Deploy contracts
|
||||
cd contracts
|
||||
make deploy-local
|
||||
|
||||
# Start nodes with local config
|
||||
dotenv -f envs/local.env -- cargo run -p nym-node -- run --mode mixnode
|
||||
```
|
||||
|
||||
## Common Issues and Troubleshooting
|
||||
|
||||
### Database Issues
|
||||
|
||||
- When modifying database queries, you must update SQLx query caches:
|
||||
```bash
|
||||
cargo sqlx prepare
|
||||
```
|
||||
- If you see SQLx errors about missing query files, this is likely the cause
|
||||
- For "database is locked" errors with SQLite, ensure only one process accesses the DB
|
||||
- For PostgreSQL connection issues, verify DATABASE_URL and that the server is running
|
||||
|
||||
### API Connection Issues
|
||||
|
||||
- Check the environment variables pointing to the APIs (NYM_API, NYXD)
|
||||
- Verify network connectivity and API health endpoints
|
||||
- For authentication issues, check node keys and credentials
|
||||
- Common endpoints to verify:
|
||||
- API health: `$NYM_API/health`
|
||||
- Chain status: `$NYXD/status`
|
||||
- Contract info: `$NYXD/cosmwasm/wasm/v1/contract/$CONTRACT_ADDRESS`
|
||||
|
||||
### Build Problems
|
||||
|
||||
- Clean dependencies with `cargo clean` for a fresh build
|
||||
- Check for compatible Rust version (1.80+ recommended)
|
||||
- For smart contract builds, ensure wasm-opt is installed: `npm install -g wasm-opt`
|
||||
- For cross-compilation issues, check target-specific dependencies
|
||||
- WASM build issues: Ensure wasm32-unknown-unknown target is installed:
|
||||
```bash
|
||||
rustup target add wasm32-unknown-unknown
|
||||
```
|
||||
- For "cannot find -lpq" errors, install PostgreSQL development files:
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt-get install libpq-dev
|
||||
# macOS
|
||||
brew install postgresql
|
||||
```
|
||||
|
||||
### Environment Issues
|
||||
|
||||
- Contract address mismatches: Ensure you're using the correct environment file
|
||||
- "Account sequence mismatch": The account nonce is out of sync, wait and retry
|
||||
- Token decimal issues: Sandbox uses different decimal places than mainnet
|
||||
- API version mismatches: Ensure your local API version matches the network
|
||||
- "Insufficient funds": Get test tokens from faucet (sandbox) or check balance
|
||||
- Gateway/mixnode bonding issues: Verify minimum stake requirements
|
||||
|
||||
## Working with Routes and Monitoring
|
||||
|
||||
1. Route monitoring metrics are stored in a `routes` table with:
|
||||
- Layer node IDs (layer1, layer2, layer3, gw)
|
||||
- Success flag (boolean)
|
||||
- Timestamp
|
||||
|
||||
2. To analyze routes:
|
||||
- Check `NetworkAccount` and `AccountingRoute` in `nym-network-monitor/src/accounting.rs`
|
||||
- View monitoring logic in `common/nymsphinx/chunking/monitoring.rs`
|
||||
- Observe how routes are submitted to the database in the `submit_accounting_routes_to_db` function
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Profiling and Benchmarking
|
||||
|
||||
```bash
|
||||
# Run benchmarks
|
||||
cargo bench -p nym-node
|
||||
|
||||
# Profile with perf (Linux)
|
||||
cargo build --release --features profiling
|
||||
perf record --call-graph=dwarf ./target/release/nym-node run --mode mixnode
|
||||
perf report
|
||||
|
||||
# Generate flamegraph
|
||||
cargo install flamegraph
|
||||
cargo flamegraph --bin nym-node -- run --mode mixnode
|
||||
```
|
||||
|
||||
### Common Performance Considerations
|
||||
|
||||
- Use bounded channels for backpressure
|
||||
- Batch database operations where possible
|
||||
- Monitor memory usage with `RUST_LOG=nym_node::metrics=debug`
|
||||
- Use connection pooling for database connections
|
||||
- Consider using `jemalloc` for better memory allocation performance
|
||||
Generated
+1030
-982
File diff suppressed because it is too large
Load Diff
@@ -234,6 +234,7 @@ digest = "0.10.7"
|
||||
dirs = "5.0"
|
||||
doc-comment = "0.3"
|
||||
dotenvy = "0.15.6"
|
||||
dyn-clone = "1.0.19"
|
||||
ecdsa = "0.16"
|
||||
ed25519-dalek = "2.1"
|
||||
encoding_rs = "0.8.35"
|
||||
|
||||
@@ -12,7 +12,11 @@ help:
|
||||
@echo " clippy: run clippy for all workspaces"
|
||||
@echo " test: run clippy, unit tests, and formatting."
|
||||
@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
|
||||
@@ -130,25 +134,69 @@ cargo-test: sdk-wasm-test
|
||||
clippy: sdk-wasm-lint
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Build contracts ready for deploy
|
||||
# Build CosmWasm contracts (deterministic docker build)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg nym_pool_contract nym_performance_contract
|
||||
CONTRACTS_WASM=$(addsuffix .wasm, $(CONTRACTS))
|
||||
CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release
|
||||
|
||||
contracts: build-release-contracts wasm-opt-contracts cosmwasm-check-contracts
|
||||
WASM_CONTRACT_DIR := contracts/target/wasm32-unknown-unknown/release
|
||||
# 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:
|
||||
for contract in $(CONTRACTS_WASM); do \
|
||||
wasm-opt --signext-lowering -Os $(CONTRACTS_OUT_DIR)/$$contract -o $(CONTRACTS_OUT_DIR)/$$contract; \
|
||||
@for WASM in $(WASM_CONTRACT_DIR)/*.wasm; do \
|
||||
echo "Running wasm-opt on $$WASM"; \
|
||||
wasm-opt --signext-lowering -Os $$WASM -o $$WASM ; \
|
||||
done
|
||||
|
||||
cosmwasm-check-contracts:
|
||||
for contract in $(CONTRACTS_WASM); do \
|
||||
cosmwasm-check $(CONTRACTS_OUT_DIR)/$$contract; \
|
||||
@for WASM in $(WASM_CONTRACT_DIR)/*.wasm; do \
|
||||
echo "Checking $$WASM"; \
|
||||
cosmwasm-check $$WASM ; \
|
||||
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)
|
||||
contract-schema:
|
||||
$(MAKE) -C contracts schema
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.57"
|
||||
version = "1.1.59"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.57"
|
||||
version = "1.1.59"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
edition = "2021"
|
||||
|
||||
@@ -28,8 +28,6 @@ pub type HmacSha256 = Hmac<Sha256>;
|
||||
pub type Nonce = u64;
|
||||
pub type Taken = Option<SystemTime>;
|
||||
|
||||
pub const BANDWIDTH_CAP_PER_DAY: u64 = 250 * 1024 * 1024 * 1024; // 250 GB
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct IpPair {
|
||||
pub ipv4: Ipv4Addr,
|
||||
|
||||
@@ -15,8 +15,7 @@ bs58 = { workspace = true }
|
||||
clap = { workspace = true, optional = true }
|
||||
comfy-table = { workspace = true, optional = true }
|
||||
futures = { workspace = true }
|
||||
humantime-serde = { workspace = true }
|
||||
log = { workspace = true }
|
||||
humantime = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
@@ -25,20 +24,18 @@ sha2 = { workspace = true }
|
||||
si-scale = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
tokio = { workspace = true, features = ["macros"] }
|
||||
time = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync", "macros"] }
|
||||
tracing = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
|
||||
# internal
|
||||
nym-id = { path = "../nym-id" }
|
||||
nym-bandwidth-controller = { path = "../bandwidth-controller" }
|
||||
nym-config = { path = "../config" }
|
||||
nym-crypto = { path = "../crypto" }
|
||||
nym-gateway-client = { path = "../client-libs/gateway-client" }
|
||||
nym-gateway-requests = { path = "../gateway-requests" }
|
||||
nym-http-api-client = { path = "../http-api-client" }
|
||||
nym-metrics = { path = "../nym-metrics" }
|
||||
nym-nonexhaustive-delayqueue = { path = "../nonexhaustive-delayqueue" }
|
||||
nym-sphinx = { path = "../nymsphinx" }
|
||||
nym-statistics-common = { path = "../statistics" }
|
||||
|
||||
@@ -57,9 +57,7 @@ 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_DROP_WAITING_PERIOD: Duration = Duration::from_secs(5 * 60);
|
||||
|
||||
// 12 hours
|
||||
const DEFAULT_MAXIMUM_REPLY_SURB_AGE: Duration = Duration::from_secs(12 * 60 * 60);
|
||||
const DEFAULT_MAXIMUM_REPLY_SURB_REREQUESTS: usize = 5;
|
||||
|
||||
// 24 hours
|
||||
const DEFAULT_MAXIMUM_REPLY_KEY_AGE: Duration = Duration::from_secs(24 * 60 * 60);
|
||||
@@ -628,10 +626,9 @@ pub struct ReplySurbs {
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub maximum_reply_surb_drop_waiting_period: Duration,
|
||||
|
||||
/// Defines maximum amount of time given reply surb is going to be valid for.
|
||||
/// This is going to be superseded by key rotation once implemented.
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub maximum_reply_surb_age: Duration,
|
||||
/// Defines maximum number of times the client is going to re-request reply surbs
|
||||
/// for clearing pending messages before giving up after making no progress.
|
||||
pub maximum_reply_surbs_rerequests: usize,
|
||||
|
||||
/// 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.
|
||||
@@ -641,9 +638,6 @@ pub struct ReplySurbs {
|
||||
/// Specifies the number of mixnet hops the packet should go through. If not specified, then
|
||||
/// the default value is used.
|
||||
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 {
|
||||
@@ -658,10 +652,9 @@ impl Default for ReplySurbs {
|
||||
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_age: DEFAULT_MAXIMUM_REPLY_SURB_AGE,
|
||||
maximum_reply_surbs_rerequests: DEFAULT_MAXIMUM_REPLY_SURB_REREQUESTS,
|
||||
maximum_reply_key_age: DEFAULT_MAXIMUM_REPLY_KEY_AGE,
|
||||
surb_mix_hops: None,
|
||||
fresh_sender_tags: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,14 +189,13 @@ impl From<ConfigV6> for Config {
|
||||
.debug
|
||||
.reply_surbs
|
||||
.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,
|
||||
surb_mix_hops: value.debug.reply_surbs.surb_mix_hops,
|
||||
minimum_reply_surb_threshold_buffer: value
|
||||
.debug
|
||||
.reply_surbs
|
||||
.minimum_reply_surb_threshold_buffer,
|
||||
fresh_sender_tags: value.debug.reply_surbs.fresh_sender_tags,
|
||||
..Default::default()
|
||||
},
|
||||
stats_reporting: StatsReporting {
|
||||
enabled: value.debug.stats_reporting.enabled,
|
||||
|
||||
@@ -9,11 +9,11 @@ license.workspace = true
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
cosmrs.workspace = true
|
||||
log.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
tokio = { workspace = true, features = ["sync"] }
|
||||
tracing.workspace = true
|
||||
url.workspace = true
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@ use crate::{
|
||||
RawActiveGateway, RawCustomGatewayDetails, RawRegisteredGateway, RawRemoteGatewayDetails,
|
||||
},
|
||||
};
|
||||
use log::{debug, error};
|
||||
use sqlx::{
|
||||
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
|
||||
ConnectOptions,
|
||||
};
|
||||
use std::path::Path;
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StorageManager {
|
||||
|
||||
@@ -12,12 +12,12 @@ use crate::{
|
||||
error::ClientCoreError,
|
||||
init::types::{GatewaySelectionSpecification, GatewaySetup},
|
||||
};
|
||||
use log::info;
|
||||
use nym_client_core_gateways_storage::GatewayDetails;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_topology::NymTopology;
|
||||
use nym_validator_client::UserAgent;
|
||||
use std::path::PathBuf;
|
||||
use tracing::info;
|
||||
|
||||
#[cfg_attr(feature = "cli", derive(clap::Args))]
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -81,14 +81,14 @@ where
|
||||
|
||||
// Attempt to use a user-provided gateway, if possible
|
||||
let user_chosen_gateway_id = common_args.gateway_id;
|
||||
log::debug!("User chosen gateway id: {user_chosen_gateway_id:?}");
|
||||
tracing::debug!("User chosen gateway id: {user_chosen_gateway_id:?}");
|
||||
|
||||
let selection_spec = GatewaySelectionSpecification::new(
|
||||
user_chosen_gateway_id.map(|id| id.to_base58_string()),
|
||||
Some(common_args.latency_based_selection),
|
||||
common_args.force_tls_gateway,
|
||||
);
|
||||
log::debug!("Gateway selection specification: {selection_spec:?}");
|
||||
tracing::debug!("Gateway selection specification: {selection_spec:?}");
|
||||
|
||||
let registered_gateways = get_all_registered_identities(&details_store).await?;
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ use crate::{
|
||||
},
|
||||
init::types::{GatewaySelectionSpecification, GatewaySetup, InitResults},
|
||||
};
|
||||
use log::info;
|
||||
use nym_client_core_gateways_storage::GatewayDetails;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_sphinx::addressing::Recipient;
|
||||
@@ -20,6 +19,7 @@ use nym_topology::NymTopology;
|
||||
use nym_validator_client::UserAgent;
|
||||
use rand::rngs::OsRng;
|
||||
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
|
||||
#[allow(async_fn_in_trait)]
|
||||
@@ -130,23 +130,23 @@ where
|
||||
|
||||
// Attempt to use a user-provided gateway, if possible
|
||||
let user_chosen_gateway_id = common_args.gateway;
|
||||
log::debug!("User chosen gateway id: {user_chosen_gateway_id:?}");
|
||||
tracing::debug!("User chosen gateway id: {user_chosen_gateway_id:?}");
|
||||
|
||||
let selection_spec = GatewaySelectionSpecification::new(
|
||||
user_chosen_gateway_id.map(|id| id.to_base58_string()),
|
||||
Some(common_args.latency_based_selection),
|
||||
common_args.force_tls_gateway,
|
||||
);
|
||||
log::debug!("Gateway selection specification: {selection_spec:?}");
|
||||
tracing::debug!("Gateway selection specification: {selection_spec:?}");
|
||||
|
||||
// Load and potentially override config
|
||||
log::debug!("Init arguments: {init_args:#?}");
|
||||
tracing::debug!("Init arguments: {init_args:#?}");
|
||||
let config = C::construct_config(&init_args);
|
||||
log::debug!("Constructed config: {config:#?}");
|
||||
tracing::debug!("Constructed config: {config:#?}");
|
||||
let paths = config.common_paths();
|
||||
let core = config.core_config();
|
||||
|
||||
log::info!(
|
||||
tracing::info!(
|
||||
"Using nym-api: {}",
|
||||
core.client
|
||||
.nym_api_urls
|
||||
|
||||
@@ -18,6 +18,7 @@ use crate::client::received_buffer::{
|
||||
ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController,
|
||||
};
|
||||
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_storage::{
|
||||
CombinedReplyStorage, PersistentReplyStorage, ReplyStorageBackend, SentReplyKeys,
|
||||
@@ -34,7 +35,6 @@ use crate::init::{
|
||||
};
|
||||
use crate::{config, spawn_future};
|
||||
use futures::channel::mpsc;
|
||||
use log::*;
|
||||
use nym_bandwidth_controller::BandwidthController;
|
||||
use nym_client_core_config_types::{ForgetMe, RememberMe};
|
||||
use nym_client_core_gateways_storage::{GatewayDetails, GatewaysDetailsStore};
|
||||
@@ -56,13 +56,18 @@ use nym_task::connections::{ConnectionCommandReceiver, ConnectionCommandSender,
|
||||
use nym_task::{TaskClient, TaskHandle};
|
||||
use nym_topology::provider_trait::TopologyProvider;
|
||||
use nym_topology::HardcodedTopologyProvider;
|
||||
use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, UserAgent};
|
||||
use nym_validator_client::nym_api::NymApiClientExt;
|
||||
use nym_validator_client::{nyxd::contract_traits::DkgQueryClient, NymApiClient, UserAgent};
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::thread_rng;
|
||||
use std::fmt::Debug;
|
||||
use std::os::raw::c_int as RawFd;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tracing::*;
|
||||
use url::Url;
|
||||
|
||||
#[cfg(all(
|
||||
@@ -338,6 +343,7 @@ where
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn start_real_traffic_controller(
|
||||
controller_config: real_messages_control::Config,
|
||||
key_rotation_config: KeyRotationConfig,
|
||||
topology_accessor: TopologyAccessor,
|
||||
ack_receiver: AcknowledgementReceiver,
|
||||
input_receiver: InputMessageReceiver,
|
||||
@@ -355,6 +361,7 @@ where
|
||||
|
||||
RealMessagesController::new(
|
||||
controller_config,
|
||||
key_rotation_config,
|
||||
ack_receiver,
|
||||
input_receiver,
|
||||
mix_sender,
|
||||
@@ -453,7 +460,7 @@ where
|
||||
};
|
||||
|
||||
let gateway_failure = |err| {
|
||||
log::error!("Could not authenticate and start up the gateway connection - {err}");
|
||||
tracing::error!("Could not authenticate and start up the gateway connection - {err}");
|
||||
ClientCoreError::GatewayClientError {
|
||||
gateway_id: details.gateway_id.to_base58_string(),
|
||||
source: Box::new(err),
|
||||
@@ -555,14 +562,14 @@ where
|
||||
custom_provider: Option<Box<dyn TopologyProvider + Send + Sync>>,
|
||||
config_topology: config::Topology,
|
||||
nym_api_urls: Vec<Url>,
|
||||
user_agent: Option<UserAgent>,
|
||||
nym_api_client: NymApiClient,
|
||||
) -> Box<dyn TopologyProvider + Send + Sync> {
|
||||
// if no custom provider was ... provided ..., create one using nym-api
|
||||
custom_provider.unwrap_or_else(|| {
|
||||
Box::new(NymApiTopologyProvider::new(
|
||||
config_topology,
|
||||
nym_api_urls,
|
||||
user_agent,
|
||||
nym_api_client,
|
||||
))
|
||||
})
|
||||
}
|
||||
@@ -598,7 +605,7 @@ where
|
||||
topology_refresher.try_refresh().await;
|
||||
|
||||
if let Err(err) = topology_refresher.ensure_topology_is_routable().await {
|
||||
log::error!(
|
||||
tracing::error!(
|
||||
"The current network topology seem to be insufficient to route any packets through \
|
||||
- check if enough nodes and a gateway are online - source: {err}"
|
||||
);
|
||||
@@ -674,16 +681,26 @@ where
|
||||
// TODO: rename it as it implies the data is persistent whilst one can use InMemBackend
|
||||
async fn setup_persistent_reply_storage(
|
||||
backend: S::ReplyStore,
|
||||
key_rotation_config: KeyRotationConfig,
|
||||
shutdown: TaskClient,
|
||||
) -> Result<CombinedReplyStorage, ClientCoreError>
|
||||
where
|
||||
<S::ReplyStore as ReplyStorageBackend>::StorageError: Sync + Send,
|
||||
S::ReplyStore: Send + Sync,
|
||||
{
|
||||
log::trace!("Setup persistent reply storage");
|
||||
tracing::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 mem_store = persistent_storage
|
||||
.load_state_from_backend()
|
||||
.load_state_from_backend(prior_epoch_start)
|
||||
.await
|
||||
.map_err(|err| ClientCoreError::SurbStorageError {
|
||||
source: Box::new(err),
|
||||
@@ -725,6 +742,23 @@ where
|
||||
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>
|
||||
where
|
||||
S::ReplyStore: Send + Sync,
|
||||
@@ -789,11 +823,14 @@ where
|
||||
.dkg_query_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(
|
||||
self.custom_topology_provider.take(),
|
||||
self.config.debug.topology,
|
||||
self.config.get_nym_api_endpoints(),
|
||||
self.user_agent.clone(),
|
||||
nym_api_client,
|
||||
);
|
||||
|
||||
let stats_reporter = Self::start_statistics_control(
|
||||
@@ -838,6 +875,7 @@ where
|
||||
|
||||
let reply_storage = Self::setup_persistent_reply_storage(
|
||||
reply_storage_backend,
|
||||
key_rotation_config,
|
||||
shutdown.fork("persistent_reply_storage"),
|
||||
)
|
||||
.await?;
|
||||
@@ -878,6 +916,7 @@ where
|
||||
|
||||
Self::start_real_traffic_controller(
|
||||
controller_config,
|
||||
key_rotation_config,
|
||||
shared_topology_accessor.clone(),
|
||||
ack_receiver,
|
||||
input_receiver,
|
||||
|
||||
@@ -7,13 +7,13 @@ use crate::{
|
||||
config::Config,
|
||||
error::ClientCoreError,
|
||||
};
|
||||
use log::{error, info, trace};
|
||||
use nym_bandwidth_controller::BandwidthController;
|
||||
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
|
||||
use nym_credential_storage::storage::Storage as CredentialStorage;
|
||||
use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient};
|
||||
use std::{io, path::Path};
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{error, info, trace};
|
||||
use url::Url;
|
||||
|
||||
async fn setup_fresh_backend<P: AsRef<Path>>(
|
||||
@@ -90,7 +90,7 @@ pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
|
||||
let db_path = db_path.as_ref();
|
||||
if db_path.exists() {
|
||||
info!("Loading existing surb database");
|
||||
match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await {
|
||||
match fs_backend::Backend::try_load(db_path).await {
|
||||
Ok(backend) => Ok(backend),
|
||||
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");
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::client::topology_control::TopologyAccessor;
|
||||
use crate::{config, spawn_future};
|
||||
use futures::task::{Context, Poll};
|
||||
use futures::{Future, Stream, StreamExt};
|
||||
use log::*;
|
||||
use nym_sphinx::acknowledgements::AckKey;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::cover::generate_loop_cover_packet;
|
||||
@@ -19,6 +18,7 @@ use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc::error::TrySendError;
|
||||
use tracing::*;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::time::{sleep, Sleep};
|
||||
@@ -210,10 +210,10 @@ impl LoopCoverTrafficStream<OsRng> {
|
||||
TrySendError::Full(_) => {
|
||||
// This isn't a problem, if the channel is full means we're already sending the
|
||||
// max amount of messages downstream can handle.
|
||||
log::debug!("Failed to send cover message - channel full");
|
||||
tracing::debug!("Failed to send cover message - channel full");
|
||||
}
|
||||
TrySendError::Closed(_) => {
|
||||
log::warn!("Failed to send cover message - channel closed");
|
||||
tracing::warn!("Failed to send cover message - channel closed");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -258,20 +258,20 @@ impl LoopCoverTrafficStream<OsRng> {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.recv() => {
|
||||
log::trace!("LoopCoverTrafficStream: Received shutdown");
|
||||
tracing::trace!("LoopCoverTrafficStream: Received shutdown");
|
||||
}
|
||||
next = self.next() => {
|
||||
if next.is_some() {
|
||||
self.on_new_message().await;
|
||||
} else {
|
||||
log::trace!("LoopCoverTrafficStream: Stopping since channel closed");
|
||||
tracing::trace!("LoopCoverTrafficStream: Stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
shutdown.recv_timeout().await;
|
||||
log::debug!("LoopCoverTrafficStream: Exiting");
|
||||
tracing::debug!("LoopCoverTrafficStream: Exiting");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,7 +135,9 @@ impl InputMessage {
|
||||
recipient_tag,
|
||||
data,
|
||||
lane,
|
||||
max_retransmissions: None,
|
||||
// \/ set it to SOME sane default so that if we run out of surbs and constantly
|
||||
// fail to request more, we wouldn't be stuck in limbo
|
||||
max_retransmissions: Some(10),
|
||||
};
|
||||
if let Some(packet_type) = packet_type {
|
||||
InputMessage::new_wrapper(message, packet_type)
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
use crate::client::mix_traffic::transceiver::GatewayTransceiver;
|
||||
use crate::error::ClientCoreError;
|
||||
use crate::spawn_future;
|
||||
use log::*;
|
||||
use nym_gateway_requests::ClientRequest;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_task::TaskClient;
|
||||
use tracing::*;
|
||||
use transceiver::ErasedGatewayError;
|
||||
|
||||
pub type BatchMixMessageSender = tokio::sync::mpsc::Sender<Vec<MixPacket>>;
|
||||
@@ -138,7 +138,7 @@ impl MixTrafficController {
|
||||
}
|
||||
},
|
||||
None => {
|
||||
log::trace!("MixTrafficController: Stopping since channel closed");
|
||||
tracing::trace!("MixTrafficController: Stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -150,18 +150,18 @@ impl MixTrafficController {
|
||||
};
|
||||
},
|
||||
None => {
|
||||
log::trace!("MixTrafficController, client request channel closed");
|
||||
tracing::trace!("MixTrafficController, client request channel closed");
|
||||
}
|
||||
},
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("MixTrafficController: Received shutdown");
|
||||
tracing::trace!("MixTrafficController: Received shutdown");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.task_client.recv_timeout().await;
|
||||
|
||||
log::debug!("MixTrafficController: Exiting");
|
||||
tracing::debug!("MixTrafficController: Exiting");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, error};
|
||||
use nym_credential_storage::storage::Storage as CredentialStorage;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_gateway_client::error::GatewayClientError;
|
||||
@@ -14,6 +13,7 @@ use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
|
||||
use std::fmt::Debug;
|
||||
use std::os::raw::c_int as RawFd;
|
||||
use thiserror::Error;
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
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))
|
||||
}
|
||||
|
||||
/// This combines combines the functionalities of being able to send and receive mix packets.
|
||||
/// This combines the functionalities of being able to send and receive mix packets.
|
||||
#[async_trait]
|
||||
pub trait GatewayTransceiver: GatewaySender + GatewayReceiver {
|
||||
fn gateway_identity(&self) -> ed25519::PublicKey;
|
||||
@@ -87,7 +87,7 @@ impl<G: GatewayTransceiver + ?Sized + Send> GatewayTransceiver for Box<G> {
|
||||
message: ClientRequest,
|
||||
) -> Result<(), GatewayClientError> {
|
||||
let _ = (**self).send_client_request(message.clone()).await?;
|
||||
log::debug!("Sent client request: {:?}", message);
|
||||
tracing::debug!("Sent client request: {:?}", message);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -5,7 +5,6 @@ use super::action_controller::{AckActionSender, Action};
|
||||
use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, ClientStatsSender};
|
||||
|
||||
use futures::StreamExt;
|
||||
use log::*;
|
||||
use nym_gateway_client::AcknowledgementReceiver;
|
||||
use nym_sphinx::{
|
||||
acknowledgements::{identifier::recover_identifier, AckKey},
|
||||
@@ -13,6 +12,7 @@ use nym_sphinx::{
|
||||
};
|
||||
use nym_task::TaskClient;
|
||||
use std::sync::Arc;
|
||||
use tracing::*;
|
||||
|
||||
/// Module responsible for listening for any data resembling acknowledgements from the network
|
||||
/// and firing actions to remove them from the 'Pending' state.
|
||||
@@ -93,16 +93,16 @@ impl AcknowledgementListener {
|
||||
acks = self.ack_receiver.next() => match acks {
|
||||
Some(acks) => self.handle_ack_receiver_item(acks).await,
|
||||
None => {
|
||||
log::trace!("AcknowledgementListener: Stopping since channel closed");
|
||||
tracing::trace!("AcknowledgementListener: Stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("AcknowledgementListener: Received shutdown");
|
||||
tracing::trace!("AcknowledgementListener: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
self.task_client.recv_timeout().await;
|
||||
log::debug!("AcknowledgementListener: Exiting");
|
||||
tracing::debug!("AcknowledgementListener: Exiting");
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -5,7 +5,6 @@ use super::PendingAcknowledgement;
|
||||
use crate::client::real_messages_control::acknowledgement_control::RetransmissionRequestSender;
|
||||
use futures::channel::mpsc;
|
||||
use futures::StreamExt;
|
||||
use log::*;
|
||||
use nym_nonexhaustive_delayqueue::{Expired, NonExhaustiveDelayQueue, QueueKey};
|
||||
use nym_sphinx::chunking::fragment::FragmentIdentifier;
|
||||
use nym_sphinx::Delay as SphinxDelay;
|
||||
@@ -13,6 +12,7 @@ use nym_task::TaskClient;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing::*;
|
||||
|
||||
pub(crate) type AckActionSender = mpsc::UnboundedSender<Action>;
|
||||
pub(crate) type AckActionReceiver = mpsc::UnboundedReceiver<Action>;
|
||||
@@ -229,7 +229,7 @@ impl ActionController {
|
||||
.unbounded_send(Arc::downgrade(pending_ack_data))
|
||||
{
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
log::error!("Failed to send pending ack for retransmission: {err}");
|
||||
tracing::error!("Failed to send pending ack for retransmission: {err}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -257,7 +257,7 @@ impl ActionController {
|
||||
action = self.incoming_actions.next() => match action {
|
||||
Some(action) => self.process_action(action),
|
||||
None => {
|
||||
log::trace!(
|
||||
tracing::trace!(
|
||||
"ActionController: Stopping since incoming actions channel closed"
|
||||
);
|
||||
break;
|
||||
@@ -266,17 +266,17 @@ impl ActionController {
|
||||
expired_ack = self.pending_acks_timers.next() => match expired_ack {
|
||||
Some(expired_ack) => self.handle_expired_ack_timer(expired_ack),
|
||||
None => {
|
||||
log::trace!("ActionController: Stopping since ack channel closed");
|
||||
tracing::trace!("ActionController: Stopping since ack channel closed");
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("ActionController: Received shutdown");
|
||||
tracing::trace!("ActionController: Received shutdown");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.task_client.recv_timeout().await;
|
||||
log::debug!("ActionController: Exiting");
|
||||
tracing::debug!("ActionController: Exiting");
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -5,7 +5,6 @@ use crate::client::inbound_messages::{InputMessage, InputMessageReceiver};
|
||||
use crate::client::real_messages_control::message_handler::MessageHandler;
|
||||
use crate::client::real_messages_control::real_traffic_stream::RealMessage;
|
||||
use crate::client::replies::reply_controller::ReplyControllerSender;
|
||||
use log::*;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
@@ -13,6 +12,7 @@ use nym_sphinx::params::PacketType;
|
||||
use nym_task::connections::TransmissionLane;
|
||||
use nym_task::TaskClient;
|
||||
use rand::{CryptoRng, Rng};
|
||||
use tracing::*;
|
||||
|
||||
/// Module responsible for dealing with the received messages: splitting them, creating acknowledgements,
|
||||
/// putting everything into sphinx packets, etc.
|
||||
@@ -228,16 +228,16 @@ where
|
||||
self.on_input_message(input_msg).await;
|
||||
},
|
||||
None => {
|
||||
log::trace!("InputMessageListener: Stopping since channel closed");
|
||||
tracing::trace!("InputMessageListener: Stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("InputMessageListener: Received shutdown");
|
||||
tracing::trace!("InputMessageListener: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
self.task_client.recv_timeout().await;
|
||||
log::debug!("InputMessageListener: Exiting");
|
||||
tracing::debug!("InputMessageListener: Exiting");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ use crate::client::replies::reply_controller::ReplyControllerSender;
|
||||
use crate::spawn_future;
|
||||
use action_controller::AckActionReceiver;
|
||||
use futures::channel::mpsc;
|
||||
use log::*;
|
||||
use nym_gateway_client::AcknowledgementReceiver;
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use nym_sphinx::params::{PacketSize, PacketType};
|
||||
@@ -30,6 +29,7 @@ use std::{
|
||||
sync::{Arc, Weak},
|
||||
time::Duration,
|
||||
};
|
||||
use tracing::*;
|
||||
|
||||
pub(crate) use action_controller::{AckActionSender, Action};
|
||||
|
||||
|
||||
+4
-4
@@ -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::replies::reply_controller::ReplyControllerSender;
|
||||
use futures::StreamExt;
|
||||
use log::*;
|
||||
use nym_sphinx::chunking::fragment::Fragment;
|
||||
use nym_sphinx::preparer::PreparedFragment;
|
||||
use nym_sphinx::{addressing::clients::Recipient, params::PacketType};
|
||||
use nym_task::{connections::TransmissionLane, TaskClient};
|
||||
use rand::{CryptoRng, Rng};
|
||||
use std::sync::{Arc, Weak};
|
||||
use tracing::*;
|
||||
|
||||
// responsible for packet retransmission upon fired timer
|
||||
pub(super) struct RetransmissionRequestListener<R> {
|
||||
@@ -182,16 +182,16 @@ where
|
||||
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,
|
||||
None => {
|
||||
log::trace!("RetransmissionRequestListener: Stopping since channel closed");
|
||||
tracing::trace!("RetransmissionRequestListener: Stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("RetransmissionRequestListener: Received shutdown");
|
||||
tracing::trace!("RetransmissionRequestListener: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
self.task_client.recv_timeout().await;
|
||||
log::debug!("RetransmissionRequestListener: Exiting");
|
||||
tracing::debug!("RetransmissionRequestListener: Exiting");
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -4,9 +4,9 @@
|
||||
use super::action_controller::{AckActionSender, Action};
|
||||
use super::SentPacketNotificationReceiver;
|
||||
use futures::StreamExt;
|
||||
use log::*;
|
||||
use nym_sphinx::chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID};
|
||||
use nym_task::TaskClient;
|
||||
use tracing::*;
|
||||
|
||||
/// Module responsible for starting up retransmission timers.
|
||||
/// 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;
|
||||
}
|
||||
None => {
|
||||
log::trace!("SentNotificationListener: Stopping since channel closed");
|
||||
tracing::trace!("SentNotificationListener: Stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("SentNotificationListener: Received shutdown");
|
||||
tracing::trace!("SentNotificationListener: Received shutdown");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(self.task_client.is_shutdown_poll());
|
||||
log::debug!("SentNotificationListener: Exiting");
|
||||
tracing::debug!("SentNotificationListener: Exiting");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::client::real_messages_control::{AckActionSender, Action};
|
||||
use crate::client::replies::reply_controller::MaxRetransmissions;
|
||||
use crate::client::replies::reply_storage::{ReceivedReplySurbsMap, SentReplyKeys, UsedSenderTags};
|
||||
use crate::client::topology_control::{TopologyAccessor, TopologyReadPermit};
|
||||
use nym_client_core_surb_storage::RetrievedReplySurb;
|
||||
use nym_sphinx::acknowledgements::AckKey;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::anonymous_replies::requests::{AnonymousSenderTag, RepliableMessage, ReplyMessage};
|
||||
@@ -44,10 +45,7 @@ pub enum PreparationError {
|
||||
}
|
||||
|
||||
impl PreparationError {
|
||||
fn return_surbs(
|
||||
self,
|
||||
returned_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
) -> SurbWrappedPreparationError {
|
||||
fn return_surbs(self, returned_surbs: Vec<RetrievedReplySurb>) -> SurbWrappedPreparationError {
|
||||
SurbWrappedPreparationError {
|
||||
source: self,
|
||||
returned_surbs: Some(returned_surbs),
|
||||
@@ -61,7 +59,7 @@ pub struct SurbWrappedPreparationError {
|
||||
#[source]
|
||||
source: PreparationError,
|
||||
|
||||
returned_surbs: Option<Vec<ReplySurbWithKeyRotation>>,
|
||||
returned_surbs: Option<Vec<RetrievedReplySurb>>,
|
||||
}
|
||||
|
||||
impl<T> From<T> for SurbWrappedPreparationError
|
||||
@@ -83,7 +81,7 @@ impl SurbWrappedPreparationError {
|
||||
target: &AnonymousSenderTag,
|
||||
) -> PreparationError {
|
||||
if let Some(reply_surbs) = self.returned_surbs {
|
||||
surb_storage.insert_surbs(target, reply_surbs)
|
||||
surb_storage.re_insert_reply_surbs(target, reply_surbs)
|
||||
}
|
||||
self.source
|
||||
}
|
||||
@@ -231,6 +229,10 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn topology_access_handle(&self) -> &TopologyAccessor {
|
||||
&self.topology_access
|
||||
}
|
||||
|
||||
fn get_or_create_sender_tag(&mut self, recipient: &Recipient) -> AnonymousSenderTag {
|
||||
if let Some(existing) = self.tag_storage.try_get_existing(recipient) {
|
||||
trace!("we already had sender tag for {recipient}");
|
||||
@@ -298,7 +300,7 @@ where
|
||||
&mut self,
|
||||
target: AnonymousSenderTag,
|
||||
message: ReplyMessage,
|
||||
reply_surb: ReplySurbWithKeyRotation,
|
||||
reply_surb: RetrievedReplySurb,
|
||||
is_extra_surb_request: bool,
|
||||
) -> Result<(), SurbWrappedPreparationError> {
|
||||
let msg = NymMessage::new_reply(message);
|
||||
@@ -329,7 +331,10 @@ where
|
||||
Some(chunk.fragment_identifier()),
|
||||
);
|
||||
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(
|
||||
chunk,
|
||||
delay,
|
||||
@@ -352,7 +357,7 @@ where
|
||||
pub(crate) async fn try_request_additional_reply_surbs(
|
||||
&mut self,
|
||||
from: AnonymousSenderTag,
|
||||
reply_surb: ReplySurbWithKeyRotation,
|
||||
reply_surb: RetrievedReplySurb,
|
||||
amount: u32,
|
||||
) -> Result<(), SurbWrappedPreparationError> {
|
||||
debug!("requesting {amount} reply SURBs from {from}");
|
||||
@@ -392,11 +397,9 @@ where
|
||||
&mut self,
|
||||
target: AnonymousSenderTag,
|
||||
fragments: Vec<FragmentWithMaxRetransmissions>,
|
||||
reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
reply_surbs: impl IntoIterator<Item = RetrievedReplySurb>,
|
||||
lane: TransmissionLane,
|
||||
) -> 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(
|
||||
target,
|
||||
fragments.into_iter().map(|f| (lane, f)).collect(),
|
||||
@@ -409,7 +412,7 @@ where
|
||||
&mut self,
|
||||
target: AnonymousSenderTag,
|
||||
fragments: Vec<(TransmissionLane, FragmentWithMaxRetransmissions)>,
|
||||
reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
reply_surbs: impl IntoIterator<Item = RetrievedReplySurb>,
|
||||
) -> Result<(), SurbWrappedPreparationError> {
|
||||
let prepared_fragments = self
|
||||
.prepare_reply_chunks_for_sending(
|
||||
@@ -571,7 +574,7 @@ where
|
||||
)
|
||||
.await?;
|
||||
|
||||
log::trace!("storing {} reply keys", reply_keys.len());
|
||||
tracing::trace!("storing {} reply keys", reply_keys.len());
|
||||
self.reply_key_storage.insert_multiple(reply_keys);
|
||||
|
||||
Ok(())
|
||||
@@ -611,7 +614,7 @@ where
|
||||
)
|
||||
.await?;
|
||||
|
||||
log::trace!("storing {} reply keys", reply_keys.len());
|
||||
tracing::trace!("storing {} reply keys", reply_keys.len());
|
||||
self.reply_key_storage.insert_multiple(reply_keys);
|
||||
|
||||
Ok(())
|
||||
@@ -641,20 +644,12 @@ where
|
||||
pub(crate) async fn prepare_reply_chunks_for_sending(
|
||||
&mut self,
|
||||
fragments: Vec<Fragment>,
|
||||
reply_surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
reply_surbs: impl IntoIterator<Item = RetrievedReplySurb>,
|
||||
) -> 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 = match self.get_topology(&topology_permit) {
|
||||
Ok(topology) => topology,
|
||||
Err(err) => return Err(err.return_surbs(reply_surbs)),
|
||||
Err(err) => return Err(err.return_surbs(reply_surbs.into_iter().collect())),
|
||||
};
|
||||
|
||||
Ok(fragments
|
||||
@@ -667,7 +662,7 @@ where
|
||||
fragment,
|
||||
topology,
|
||||
&self.config.ack_key,
|
||||
reply_surb,
|
||||
reply_surb.into(),
|
||||
PacketType::Mix,
|
||||
)
|
||||
.unwrap()
|
||||
@@ -677,7 +672,7 @@ where
|
||||
|
||||
pub(crate) async fn try_prepare_single_reply_chunk_for_sending(
|
||||
&mut self,
|
||||
reply_surb: ReplySurbWithKeyRotation,
|
||||
reply_surb: RetrievedReplySurb,
|
||||
chunk: Fragment,
|
||||
) -> Result<PreparedFragment, SurbWrappedPreparationError> {
|
||||
let topology_permit = self.topology_access.get_read_permit().await;
|
||||
@@ -690,7 +685,7 @@ where
|
||||
chunk,
|
||||
topology,
|
||||
&self.config.ack_key,
|
||||
reply_surb,
|
||||
reply_surb.into(),
|
||||
PacketType::Mix,
|
||||
)?;
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ use crate::{
|
||||
spawn_future,
|
||||
};
|
||||
use futures::channel::mpsc;
|
||||
use log::*;
|
||||
use nym_gateway_client::AcknowledgementReceiver;
|
||||
use nym_sphinx::acknowledgements::AckKey;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
@@ -34,7 +33,9 @@ use nym_task::connections::{ConnectionCommandReceiver, LaneQueueLengths};
|
||||
use nym_task::TaskClient;
|
||||
use rand::{rngs::OsRng, CryptoRng, Rng};
|
||||
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) mod acknowledgement_control;
|
||||
@@ -85,12 +86,6 @@ 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 {
|
||||
fn from(cfg: &'a Config) -> Self {
|
||||
message_handler::Config::new(
|
||||
@@ -139,6 +134,7 @@ impl RealMessagesController<OsRng> {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn new(
|
||||
config: Config,
|
||||
key_rotation_config: KeyRotationConfig,
|
||||
ack_receiver: AcknowledgementReceiver,
|
||||
input_receiver: InputMessageReceiver,
|
||||
mix_sender: BatchMixMessageSender,
|
||||
@@ -169,7 +165,8 @@ impl RealMessagesController<OsRng> {
|
||||
// create all configs for the components
|
||||
let ack_control_config = (&config).into();
|
||||
let out_queue_config = (&config).into();
|
||||
let reply_controller_config = (&config).into();
|
||||
let reply_controller_config =
|
||||
reply_controller::Config::new(config.reply_surbs, key_rotation_config);
|
||||
let message_handler_config = (&config).into();
|
||||
|
||||
// create the actual components
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::client::transmission_buffer::TransmissionBuffer;
|
||||
use crate::config;
|
||||
use futures::task::{Context, Poll};
|
||||
use futures::{Future, Stream, StreamExt};
|
||||
use log::*;
|
||||
use nym_sphinx::acknowledgements::AckKey;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::chunking::fragment::FragmentIdentifier;
|
||||
@@ -27,6 +26,7 @@ use rand::{CryptoRng, Rng};
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tracing::*;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::time::{sleep, Sleep};
|
||||
@@ -280,7 +280,7 @@ where
|
||||
|
||||
if let Err(err) = self.mix_tx.send(vec![next_message]).await {
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
log::error!("Failed to send: {err}");
|
||||
tracing::error!("Failed to send: {err}");
|
||||
}
|
||||
} else {
|
||||
let event = if fragment_id.is_some() {
|
||||
@@ -313,7 +313,7 @@ where
|
||||
}
|
||||
|
||||
fn on_close_connection(&mut self, connection_id: ConnectionId) {
|
||||
log::debug!("Removing lane for connection: {connection_id}");
|
||||
tracing::debug!("Removing lane for connection: {connection_id}");
|
||||
self.transmission_buffer
|
||||
.remove(&TransmissionLane::ConnectionId(connection_id));
|
||||
}
|
||||
@@ -325,7 +325,7 @@ where
|
||||
|
||||
fn adjust_current_average_message_sending_delay(&mut self) {
|
||||
let used_slots = self.mix_tx.max_capacity() - self.mix_tx.capacity();
|
||||
log::trace!(
|
||||
tracing::trace!(
|
||||
"used_slots: {used_slots}, current_multiplier: {}",
|
||||
self.sending_delay_controller.current_multiplier()
|
||||
);
|
||||
@@ -334,7 +334,7 @@ where
|
||||
.sending_delay_controller
|
||||
.is_backpressure_currently_detected(used_slots)
|
||||
{
|
||||
log::trace!("Backpressure detected");
|
||||
tracing::trace!("Backpressure detected");
|
||||
self.sending_delay_controller.record_backpressure_detected();
|
||||
}
|
||||
|
||||
@@ -436,7 +436,7 @@ where
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
|
||||
Poll::Ready(Some((real_messages, conn_id))) => {
|
||||
log::trace!("handling real_messages: size: {}", real_messages.len());
|
||||
tracing::trace!("handling real_messages: size: {}", real_messages.len());
|
||||
|
||||
self.transmission_buffer.store(&conn_id, real_messages);
|
||||
let real_next = self.pop_next_message().expect("Just stored one");
|
||||
@@ -483,7 +483,7 @@ where
|
||||
Poll::Ready(None) => Poll::Ready(None),
|
||||
|
||||
Poll::Ready(Some((real_messages, conn_id))) => {
|
||||
log::trace!("handling real_messages: size: {}", real_messages.len());
|
||||
tracing::trace!("handling real_messages: size: {}", real_messages.len());
|
||||
|
||||
// First store what we got for the given connection id
|
||||
self.transmission_buffer.store(&conn_id, real_messages);
|
||||
@@ -538,11 +538,11 @@ where
|
||||
};
|
||||
|
||||
if packets > 1000 {
|
||||
log::warn!("{status_str}");
|
||||
tracing::warn!("{status_str}");
|
||||
} else if packets > 0 {
|
||||
log::info!("{status_str}");
|
||||
tracing::info!("{status_str}");
|
||||
} else {
|
||||
log::debug!("{status_str}");
|
||||
tracing::debug!("{status_str}");
|
||||
}
|
||||
|
||||
// Send status message to whoever is listening (possibly UI)
|
||||
@@ -566,7 +566,7 @@ where
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.recv() => {
|
||||
log::trace!("OutQueueControl: Received shutdown");
|
||||
tracing::trace!("OutQueueControl: Received shutdown");
|
||||
break;
|
||||
}
|
||||
_ = status_timer.tick() => {
|
||||
@@ -575,7 +575,7 @@ where
|
||||
next_message = self.next() => if let Some(next_message) = next_message {
|
||||
self.on_message(next_message).await;
|
||||
} else {
|
||||
log::trace!("OutQueueControl: Stopping since channel closed");
|
||||
tracing::trace!("OutQueueControl: Stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -589,18 +589,18 @@ where
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.recv() => {
|
||||
log::trace!("OutQueueControl: Received shutdown");
|
||||
tracing::trace!("OutQueueControl: Received shutdown");
|
||||
}
|
||||
next_message = self.next() => if let Some(next_message) = next_message {
|
||||
self.on_message(next_message).await;
|
||||
} else {
|
||||
log::trace!("OutQueueControl: Stopping since channel closed");
|
||||
tracing::trace!("OutQueueControl: Stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
log::debug!("OutQueueControl: Exiting");
|
||||
tracing::debug!("OutQueueControl: Exiting");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -98,12 +98,12 @@ impl SendingDelayController {
|
||||
self.current_multiplier =
|
||||
(self.current_multiplier + 1).clamp(self.lower_bound, self.upper_bound);
|
||||
self.time_when_changed = get_time_now();
|
||||
log::debug!(
|
||||
tracing::debug!(
|
||||
"Increasing sending delay multiplier to: {}",
|
||||
self.current_multiplier
|
||||
);
|
||||
} else {
|
||||
log::warn!("Trying to increase delay multipler higher than allowed");
|
||||
tracing::warn!("Trying to increase delay multipler higher than allowed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ impl SendingDelayController {
|
||||
self.current_multiplier =
|
||||
(self.current_multiplier - 1).clamp(self.lower_bound, self.upper_bound);
|
||||
self.time_when_changed = get_time_now();
|
||||
log::debug!(
|
||||
tracing::debug!(
|
||||
"Decreasing sending delay multiplier to: {}",
|
||||
self.current_multiplier
|
||||
);
|
||||
@@ -164,11 +164,11 @@ impl SendingDelayController {
|
||||
self.current_multiplier()
|
||||
);
|
||||
if self.current_multiplier() > 0 {
|
||||
log::debug!("{status_str}");
|
||||
tracing::debug!("{status_str}");
|
||||
} else if self.current_multiplier() > 1 {
|
||||
log::info!("{status_str}");
|
||||
tracing::info!("{status_str}");
|
||||
} else if self.current_multiplier() > 2 {
|
||||
log::warn!("{status_str}");
|
||||
tracing::warn!("{status_str}");
|
||||
}
|
||||
self.time_when_logged_about_elevated_multiplier = now;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ use crate::spawn_future;
|
||||
use futures::channel::mpsc;
|
||||
use futures::lock::Mutex;
|
||||
use futures::StreamExt;
|
||||
use log::*;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_crypto::Digest;
|
||||
use nym_gateway_client::MixnetMessageReceiver;
|
||||
@@ -24,6 +23,7 @@ use nym_task::TaskClient;
|
||||
use std::collections::HashSet;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
use tracing::*;
|
||||
|
||||
// The interval at which we check for stale buffers
|
||||
const STALE_BUFFER_CHECK_INTERVAL: Duration = Duration::from_secs(10);
|
||||
@@ -307,13 +307,15 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = self.reply_controller_sender.send_additional_surbs(
|
||||
msg.sender_tag,
|
||||
reply_surbs,
|
||||
from_surb_request,
|
||||
) {
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("{err}");
|
||||
if !reply_surbs.is_empty() {
|
||||
if let Err(err) = self.reply_controller_sender.send_additional_surbs(
|
||||
msg.sender_tag,
|
||||
reply_surbs,
|
||||
from_surb_request,
|
||||
) {
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("{err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -497,20 +499,20 @@ impl<R: MessageReceiver> RequestReceiver<R> {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("RequestReceiver: Received shutdown");
|
||||
tracing::trace!("RequestReceiver: Received shutdown");
|
||||
}
|
||||
request = self.query_receiver.next() => {
|
||||
if let Some(message) = request {
|
||||
self.handle_message(message).await
|
||||
} else {
|
||||
log::trace!("RequestReceiver: Stopping since channel closed");
|
||||
tracing::trace!("RequestReceiver: Stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
self.task_client.recv().await;
|
||||
log::debug!("RequestReceiver: Exiting");
|
||||
tracing::debug!("RequestReceiver: Exiting");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -541,17 +543,17 @@ impl<R: MessageReceiver> FragmentedMessageReceiver<R> {
|
||||
if let Some(new_messages) = new_messages {
|
||||
self.received_buffer.handle_new_received(new_messages).await?;
|
||||
} else {
|
||||
log::trace!("FragmentedMessageReceiver: Stopping since channel closed");
|
||||
tracing::trace!("FragmentedMessageReceiver: Stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = self.task_client.recv_with_delay() => {
|
||||
log::trace!("FragmentedMessageReceiver: Received shutdown");
|
||||
tracing::trace!("FragmentedMessageReceiver: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
self.task_client.recv_timeout().await;
|
||||
log::debug!("FragmentedMessageReceiver: Exiting");
|
||||
tracing::debug!("FragmentedMessageReceiver: Exiting");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
// 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)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,46 @@
|
||||
// Copyright 2022 - 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::helpers::new_interval_stream;
|
||||
use crate::client::real_messages_control::message_handler::MessageHandler;
|
||||
use crate::client::replies::reply_controller::key_rotation_helpers::KeyRotationConfig;
|
||||
use crate::client::replies::reply_storage::CombinedReplyStorage;
|
||||
use futures::channel::oneshot;
|
||||
use crate::config;
|
||||
use futures::StreamExt;
|
||||
use log::{debug, error, info, trace, warn};
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
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_task::TaskClient;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::{CryptoRng, Rng};
|
||||
use std::cmp::{max, min};
|
||||
use std::collections::btree_map::Entry;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::client::helpers::new_interval_stream;
|
||||
use crate::client::transmission_buffer::TransmissionBuffer;
|
||||
use crate::config;
|
||||
use crate::client::replies::reply_controller::receiver_controller::ReceiverReplyController;
|
||||
use crate::client::replies::reply_controller::sender_controller::SenderReplyController;
|
||||
pub(crate) use requests::{ReplyControllerMessage, ReplyControllerReceiver, ReplyControllerSender};
|
||||
|
||||
pub mod key_rotation_helpers;
|
||||
mod receiver_controller;
|
||||
pub mod requests;
|
||||
mod sender_controller;
|
||||
|
||||
// this is still left as a separate config so I wouldn't need to replace it everywhere
|
||||
// plus its not unreasonable to think that we might need something outside config::ReplySurbs struct
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Config {
|
||||
reply_surbs: config::ReplySurbs,
|
||||
|
||||
/// Current configuration value of the key rotation as setup on this network.
|
||||
/// This includes things such as number of epochs per rotation, duration of epochs, etc.
|
||||
// NOTE: this is operating on the assumption of constant-length epochs
|
||||
key_rotation: KeyRotationConfig,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub(crate) fn new(reply_surbs_cfg: config::ReplySurbs) -> Self {
|
||||
pub(crate) fn new(
|
||||
reply_surbs_cfg: config::ReplySurbs,
|
||||
key_rotation: KeyRotationConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
reply_surbs: reply_surbs_cfg,
|
||||
key_rotation,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,645 +51,50 @@ impl Config {
|
||||
// - so I guess it will handle all 'RepliableMessage' and requests from 'ReplyMessage'
|
||||
// - replies to "give additional surbs" requests
|
||||
// - will reply to future heartbeats
|
||||
|
||||
pub type MaxRetransmissions = Option<u32>;
|
||||
|
||||
// TODO: this should be split into ingress and egress controllers
|
||||
// because currently its trying to perform two distinct jobs
|
||||
pub struct ReplyController<R> {
|
||||
config: Config,
|
||||
|
||||
// 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,
|
||||
sender_controller: SenderReplyController<R>,
|
||||
receiver_controller: ReceiverReplyController<R>,
|
||||
|
||||
request_receiver: ReplyControllerReceiver,
|
||||
pending_replies:
|
||||
HashMap<AnonymousSenderTag, TransmissionBuffer<FragmentWithMaxRetransmissions>>,
|
||||
|
||||
/// Retransmission packets that have already timed out and are waiting for additional reply SURBs
|
||||
/// so that they could be sent back to the network. Once we receive more SURBs, we should send them ASAP.
|
||||
// TODO: when purging stale entries, we must take extra care to also purge all pending ACK data!!
|
||||
pending_retransmissions:
|
||||
HashMap<AnonymousSenderTag, BTreeMap<FragmentIdentifier, Weak<PendingAcknowledgement>>>,
|
||||
|
||||
message_handler: MessageHandler<R>,
|
||||
full_reply_storage: CombinedReplyStorage,
|
||||
|
||||
// Listen for shutdown signals
|
||||
task_client: TaskClient,
|
||||
}
|
||||
|
||||
impl<R> ReplyController<R>
|
||||
where
|
||||
R: CryptoRng + Rng,
|
||||
{
|
||||
impl ReplyController<OsRng> {
|
||||
pub(crate) fn new(
|
||||
config: Config,
|
||||
message_handler: MessageHandler<R>,
|
||||
message_handler: MessageHandler<OsRng>,
|
||||
full_reply_storage: CombinedReplyStorage,
|
||||
request_receiver: ReplyControllerReceiver,
|
||||
task_client: TaskClient,
|
||||
) -> Self {
|
||||
ReplyController {
|
||||
config,
|
||||
sender_controller: SenderReplyController::new(
|
||||
config,
|
||||
&full_reply_storage,
|
||||
message_handler.clone(),
|
||||
),
|
||||
receiver_controller: ReceiverReplyController::new(
|
||||
config,
|
||||
full_reply_storage.surbs_storage(),
|
||||
message_handler,
|
||||
),
|
||||
request_receiver,
|
||||
pending_replies: HashMap::new(),
|
||||
pending_retransmissions: HashMap::new(),
|
||||
message_handler,
|
||||
full_reply_storage,
|
||||
task_client,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_pending_replies<I: IntoIterator<Item = FragmentWithMaxRetransmissions>>(
|
||||
&mut self,
|
||||
recipient: &AnonymousSenderTag,
|
||||
fragments: I,
|
||||
lane: TransmissionLane,
|
||||
) {
|
||||
trace!("buffering pending replies for {recipient}");
|
||||
self.pending_replies
|
||||
.entry(*recipient)
|
||||
.or_insert_with(TransmissionBuffer::new)
|
||||
.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.pending_replies
|
||||
.entry(*recipient)
|
||||
.or_insert_with(TransmissionBuffer::new)
|
||||
.store_multiple(fragments)
|
||||
}
|
||||
|
||||
fn re_insert_pending_retransmission(
|
||||
&mut self,
|
||||
recipient: &AnonymousSenderTag,
|
||||
data: Vec<Arc<PendingAcknowledgement>>,
|
||||
) {
|
||||
trace!("re-inserting pending retransmissions for {recipient}");
|
||||
// the underlying entry MUST exist as we've just got data from there
|
||||
let map_entry = self
|
||||
.pending_retransmissions
|
||||
.get_mut(recipient)
|
||||
.expect("our pending retransmission entry is somehow gone!");
|
||||
|
||||
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 pending_queue_size = self
|
||||
.pending_replies
|
||||
.get(target)
|
||||
.map(|pending_queue| pending_queue.total_size())
|
||||
.unwrap_or_default();
|
||||
|
||||
let retransmission_queue = self
|
||||
.pending_retransmissions
|
||||
.get(target)
|
||||
.map(|pending_queue| pending_queue.len())
|
||||
.unwrap_or_default();
|
||||
|
||||
let total_queue = pending_queue_size + retransmission_queue;
|
||||
|
||||
let available_surbs = self
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.available_surbs(target);
|
||||
let pending_surbs = self
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.pending_reception(target) as usize;
|
||||
let min_surbs_threshold = self
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.min_surb_threshold();
|
||||
let max_surbs_threshold = self
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.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!("total queue size: {total_queue} = pending data {pending_queue_size} + pending retransmission {retransmission_queue}, 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
|
||||
}
|
||||
|
||||
async fn handle_send_reply(
|
||||
&mut self,
|
||||
recipient_tag: AnonymousSenderTag,
|
||||
data: Vec<u8>,
|
||||
lane: TransmissionLane,
|
||||
max_retransmissions: Option<u32>,
|
||||
) {
|
||||
if !self
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.contains_surbs_for(&recipient_tag)
|
||||
{
|
||||
warn!("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");
|
||||
|
||||
let available_surbs = self
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.available_surbs(&recipient_tag);
|
||||
let min_surbs_threshold = self
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.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
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.get_reply_surbs(&recipient_tag, max_to_send);
|
||||
|
||||
if let Some(reply_surbs) = surbs {
|
||||
let to_send = fragments
|
||||
.drain(..max_to_send)
|
||||
.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.full_reply_storage.surbs_storage_ref(),
|
||||
&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
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.get_reply_surb_ignoring_threshold(&target)
|
||||
.and_then(|(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.full_reply_storage.surbs_storage_ref(), &target);
|
||||
warn!("failed to request additional surbs from {target:?} - {err}");
|
||||
return Err(err);
|
||||
} else {
|
||||
self.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.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
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.available_surbs(&target);
|
||||
let min_surbs_threshold = self
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.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.pending_retransmissions.get_mut(&target) else {
|
||||
trace!("there are no pending retransmissions for {target}!");
|
||||
return;
|
||||
};
|
||||
|
||||
let mut to_take = Vec::new();
|
||||
|
||||
while to_take.len() < max_to_clear {
|
||||
if let Some((_, data)) = pending.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
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.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.full_reply_storage.surbs_storage_ref(), &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 total = self.pending_replies.get(from)?.total_size();
|
||||
trace!("pending queue has {total} elements");
|
||||
if total == 0 {
|
||||
return None;
|
||||
}
|
||||
self.pending_replies
|
||||
.get_mut(from)?
|
||||
.pop_at_most_n_next_messages_at_random(amount)
|
||||
}
|
||||
|
||||
async fn try_clear_pending_queue(&mut self, target: AnonymousSenderTag) {
|
||||
trace!("trying to clear pending queue");
|
||||
let available_surbs = self
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.available_surbs(&target);
|
||||
let min_surbs_threshold = self
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.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
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.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.full_reply_storage.surbs_storage_ref(), &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");
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
self.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.reset_surbs_last_received_at(&from);
|
||||
if from_surb_request {
|
||||
self.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.decrement_pending_reception(&from, reply_surbs.len() as u32);
|
||||
}
|
||||
|
||||
// store received surbs
|
||||
self.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.insert_surbs(&from, reply_surbs);
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
.full_reply_storage
|
||||
.tags_storage_ref()
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
if let Some(existing) = self.pending_retransmissions.get_mut(&recipient) {
|
||||
if let Entry::Vacant(e) = existing.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!");
|
||||
}
|
||||
} else {
|
||||
let mut inner = BTreeMap::new();
|
||||
inner.insert(frag_id, weak_ack_ref);
|
||||
self.pending_retransmissions.insert(recipient, inner);
|
||||
}
|
||||
}
|
||||
|
||||
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.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.get_reply_surb_ignoring_threshold(&recipient_tag)
|
||||
} else {
|
||||
self.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.get_reply_surb(&recipient_tag)
|
||||
}
|
||||
.expect("attempted to retransmit a packet to an unknown recipient - we shouldn't have sent the original packet in the first place!");
|
||||
|
||||
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.full_reply_storage.surbs_storage_ref(),
|
||||
&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
|
||||
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.pending_replies.values() {
|
||||
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!")
|
||||
}
|
||||
}
|
||||
|
||||
impl<R> ReplyController<R>
|
||||
where
|
||||
R: CryptoRng + Rng,
|
||||
{
|
||||
async fn handle_request(&mut self, request: ReplyControllerMessage) {
|
||||
match request {
|
||||
ReplyControllerMessage::RetransmitReply {
|
||||
@@ -696,7 +102,8 @@ where
|
||||
timed_out_ack,
|
||||
extra_surb_request,
|
||||
} => {
|
||||
self.handle_reply_retransmission(recipient, timed_out_ack, extra_surb_request)
|
||||
self.receiver_controller
|
||||
.handle_reply_retransmission(recipient, timed_out_ack, extra_surb_request)
|
||||
.await
|
||||
}
|
||||
ReplyControllerMessage::SendReply {
|
||||
@@ -705,7 +112,8 @@ where
|
||||
lane,
|
||||
max_retransmissions,
|
||||
} => {
|
||||
self.handle_send_reply(recipient, message, lane, max_retransmissions)
|
||||
self.receiver_controller
|
||||
.handle_send_reply(recipient, message, lane, max_retransmissions)
|
||||
.await
|
||||
}
|
||||
ReplyControllerMessage::AdditionalSurbs {
|
||||
@@ -713,225 +121,67 @@ where
|
||||
reply_surbs,
|
||||
from_surb_request,
|
||||
} => {
|
||||
self.handle_received_surbs(sender_tag, reply_surbs, from_surb_request)
|
||||
self.receiver_controller
|
||||
.handle_received_surbs(sender_tag, reply_surbs, from_surb_request)
|
||||
.await
|
||||
}
|
||||
ReplyControllerMessage::LaneQueueLength {
|
||||
connection_id,
|
||||
response_channel,
|
||||
} => self.handle_lane_queue_length(connection_id, response_channel),
|
||||
} => self
|
||||
.receiver_controller
|
||||
.handle_lane_queue_length(connection_id, response_channel),
|
||||
ReplyControllerMessage::AdditionalSurbsRequest { recipient, amount } => {
|
||||
self.handle_surb_request(*recipient, amount).await
|
||||
self.sender_controller
|
||||
.handle_surb_request(*recipient, amount)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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 pending_queue_size = self
|
||||
.pending_replies
|
||||
.get(&target)
|
||||
.map(|pending_queue| pending_queue.total_size())
|
||||
.unwrap_or_default();
|
||||
|
||||
let retransmission_queue = self
|
||||
.pending_retransmissions
|
||||
.get(&target)
|
||||
.map(|pending_queue| pending_queue.len())
|
||||
.unwrap_or_default();
|
||||
|
||||
let min_surbs_buffer = self.config.reply_surbs.minimum_reply_surb_threshold_buffer as u32;
|
||||
|
||||
let total_queue = (pending_queue_size + retransmission_queue) 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
|
||||
{
|
||||
info!("{err}")
|
||||
}
|
||||
}
|
||||
|
||||
async fn inspect_stale_entries(&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.pending_replies {
|
||||
if vals.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(last_received) = self
|
||||
.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.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!", vals.total_size());
|
||||
to_remove.push(*pending_reply_target);
|
||||
continue;
|
||||
};
|
||||
|
||||
// this should never ever happen (famous last words, eh?), but in case it DOES happen eventually
|
||||
// purge that malformed data
|
||||
let Ok(last_received_time) = OffsetDateTime::from_unix_timestamp(last_received) else {
|
||||
error!("somehow our stored timestamp ({last_received}) for surbs from {pending_reply_target} is corrupted!. Going to remove all the associated entries");
|
||||
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;
|
||||
|
||||
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 {diff:?} from {pending_reply_target}. Going to explicitly ask for more");
|
||||
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.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.reset_pending_reception(&pending_reply_target)
|
||||
}
|
||||
for to_remove in to_remove {
|
||||
self.pending_replies.remove(&to_remove);
|
||||
}
|
||||
}
|
||||
|
||||
async fn invalidate_old_data(&self) {
|
||||
async fn remove_stale_storage(&mut self) {
|
||||
let now = OffsetDateTime::now_utc();
|
||||
|
||||
let mut to_remove_surbs = Vec::new();
|
||||
let mut to_remove_keys = Vec::new();
|
||||
for map_ref in self.full_reply_storage.surbs_storage_ref().as_raw_iter() {
|
||||
let (sender, received) = map_ref.pair();
|
||||
// TODO: handle the following edge case:
|
||||
// there's a malicious client sending us exactly one reply surb just before we should have invalidated
|
||||
// the data thus making us keep everything in memory
|
||||
// possible solution: keep timestamp PER reply surb (but that seems like an overkill)
|
||||
// but I doubt this is ever going to be a problem...
|
||||
// ...
|
||||
// However, if you're reading this message, it probably became a legit problem,
|
||||
// so I guess add timestamp per surb then? chop-chop.
|
||||
|
||||
let last_received = received.surbs_last_received_at();
|
||||
// this should never ever happen (famous last words, eh?), but in case it DOES happen eventually
|
||||
// purge that malformed data
|
||||
let Ok(last_received_time) = OffsetDateTime::from_unix_timestamp(last_received) else {
|
||||
error!("somehow our stored timestamp ({last_received}) for surbs from {sender} is corrupted!. Going to remove all the associated entries");
|
||||
to_remove_surbs.push(*sender);
|
||||
continue;
|
||||
};
|
||||
let diff = now - last_received_time;
|
||||
|
||||
if diff > self.config.reply_surbs.maximum_reply_surb_age {
|
||||
info!("it's been {diff:?} since we last received any reply surb from {sender}. Going to remove all stored entries...");
|
||||
|
||||
to_remove_surbs.push(*sender);
|
||||
}
|
||||
}
|
||||
|
||||
for map_ref in self.full_reply_storage.key_storage_ref().as_raw_iter() {
|
||||
let (digest, reply_key) = map_ref.pair();
|
||||
|
||||
// this should never ever happen (famous last words, eh?), but in case it DOES happen eventually
|
||||
// purge that malformed data
|
||||
let Ok(sent_at) = OffsetDateTime::from_unix_timestamp(reply_key.sent_at_timestamp)
|
||||
else {
|
||||
error!("somehow our stored timestamp ({}) for one of our reply key is corrupted!. Going to remove all the entry", reply_key.sent_at_timestamp);
|
||||
to_remove_keys.push(*digest);
|
||||
continue;
|
||||
};
|
||||
|
||||
let diff = now - sent_at;
|
||||
|
||||
if diff > self.config.reply_surbs.maximum_reply_key_age {
|
||||
debug!("it's been {diff:?} since we created this reply key. it's probably never going to get used, so we're going to purge it...");
|
||||
to_remove_keys.push(*digest);
|
||||
}
|
||||
}
|
||||
|
||||
for to_remove in to_remove_surbs {
|
||||
self.full_reply_storage
|
||||
.surbs_storage_ref()
|
||||
.remove(&to_remove);
|
||||
}
|
||||
|
||||
for to_remove in to_remove_keys {
|
||||
self.full_reply_storage.key_storage().remove(to_remove)
|
||||
}
|
||||
self.receiver_controller
|
||||
.inspect_and_clear_stale_data(now)
|
||||
.await;
|
||||
self.sender_controller.inspect_and_clear_stale_data(now)
|
||||
}
|
||||
|
||||
// #[cfg(not(target_arch = "wasm32"))]
|
||||
// async fn log_status(&self) {
|
||||
// todo!()
|
||||
// }
|
||||
|
||||
pub(crate) async fn run(&mut self) {
|
||||
debug!("Started ReplyController with graceful shutdown support");
|
||||
|
||||
let mut shutdown = self.task_client.fork("select");
|
||||
let mut shutdown = self.task_client.fork("reply-controller");
|
||||
|
||||
let polling_rate = Duration::from_secs(5);
|
||||
let mut stale_inspection = new_interval_stream(polling_rate);
|
||||
|
||||
// this is in the order of hours/days so we don't have to poll it that often
|
||||
let polling_rate =
|
||||
Duration::from_secs(self.config.reply_surbs.maximum_reply_surb_age.as_secs() / 10);
|
||||
let polling_rate = self.config.key_rotation.epoch_duration / 8;
|
||||
let mut invalidation_inspection = new_interval_stream(polling_rate);
|
||||
|
||||
while !shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = shutdown.recv() => {
|
||||
log::trace!("ReplyController: Received shutdown");
|
||||
tracing::trace!("ReplyController: Received shutdown");
|
||||
},
|
||||
req = self.request_receiver.next() => match req {
|
||||
Some(req) => self.handle_request(req).await,
|
||||
None => {
|
||||
log::trace!("ReplyController: Stopping since channel closed");
|
||||
tracing::trace!("ReplyController: Stopping since channel closed");
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = stale_inspection.next() => {
|
||||
self.inspect_stale_entries().await
|
||||
self.receiver_controller.inspect_stale_pending_data().await
|
||||
},
|
||||
_ = invalidation_inspection.next() => {
|
||||
self.invalidate_old_data().await
|
||||
self.receiver_controller.check_surb_refresh().await;
|
||||
self.remove_stale_storage().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(shutdown.is_shutdown_poll());
|
||||
log::debug!("ReplyController: Exiting");
|
||||
tracing::debug!("ReplyController: Exiting");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,899 @@
|
||||
// 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}");
|
||||
// the underlying entry MUST exist as we've just got data from there
|
||||
// and we hold a mut reference
|
||||
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)
|
||||
}
|
||||
|
||||
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 futures::channel::{mpsc, oneshot};
|
||||
use log::error;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
|
||||
use nym_task::connections::{ConnectionId, TransmissionLane};
|
||||
use std::sync::Weak;
|
||||
use tracing::error;
|
||||
|
||||
pub(crate) fn new_control_channels() -> (ReplyControllerSender, ReplyControllerReceiver) {
|
||||
let (tx, rx) = mpsc::unbounded();
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
// 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,
|
||||
);
|
||||
if let Err(err) = self.report_tx.send(report_message).await {
|
||||
log::error!("Failed to report client stats: {err:?}");
|
||||
tracing::error!("Failed to report client stats: {err:?}");
|
||||
} else {
|
||||
self.stats.reset();
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(&mut self) {
|
||||
log::debug!("Started StatisticsControl with graceful shutdown support");
|
||||
tracing::debug!("Started StatisticsControl with graceful shutdown support");
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let mut stats_report_interval = tokio_stream::wrappers::IntervalStream::new(
|
||||
@@ -133,13 +133,13 @@ impl StatisticsControl {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("StatisticsControl: Received shutdown");
|
||||
tracing::trace!("StatisticsControl: Received shutdown");
|
||||
break;
|
||||
},
|
||||
stats_event = self.stats_rx.recv() => match stats_event {
|
||||
Some(stats_event) => self.stats.handle_event(stats_event),
|
||||
None => {
|
||||
log::trace!("StatisticsControl: shutting down due to closed stats channel");
|
||||
tracing::trace!("StatisticsControl: shutting down due to closed stats channel");
|
||||
break;
|
||||
}
|
||||
},
|
||||
@@ -161,7 +161,7 @@ impl StatisticsControl {
|
||||
}
|
||||
}
|
||||
}
|
||||
log::debug!("StatisticsControl: Exiting");
|
||||
tracing::debug!("StatisticsControl: Exiting");
|
||||
}
|
||||
|
||||
pub(crate) fn start(mut self) {
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError};
|
||||
use nym_topology::{NymRouteProvider, NymTopology, NymTopologyError, NymTopologyMetadata};
|
||||
use nym_validator_client::models::KeyRotationId;
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
@@ -134,6 +135,21 @@ 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) {
|
||||
self.inner.controlled_manually.store(true, Ordering::SeqCst);
|
||||
self.inner.update(Some(new_topology)).await;
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
use crate::spawn_future;
|
||||
pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit};
|
||||
use futures::StreamExt;
|
||||
use log::*;
|
||||
use nym_sphinx::addressing::nodes::NodeIdentity;
|
||||
use nym_task::TaskClient;
|
||||
use nym_topology::NymTopologyError;
|
||||
use std::time::Duration;
|
||||
use tracing::*;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use tokio::time::sleep;
|
||||
@@ -20,7 +20,7 @@ mod accessor;
|
||||
pub mod nym_api_provider;
|
||||
|
||||
pub use nym_api_provider::{Config as NymApiTopologyProviderConfig, NymApiTopologyProvider};
|
||||
pub use nym_topology::provider_trait::TopologyProvider;
|
||||
pub use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider};
|
||||
|
||||
// TODO: move it to config later
|
||||
const MAX_FAILURE_COUNT: usize = 10;
|
||||
@@ -169,12 +169,12 @@ impl TopologyRefresher {
|
||||
self.try_refresh().await;
|
||||
},
|
||||
_ = self.task_client.recv() => {
|
||||
log::trace!("TopologyRefresher: Received shutdown");
|
||||
tracing::trace!("TopologyRefresher: Received shutdown");
|
||||
},
|
||||
}
|
||||
}
|
||||
self.task_client.recv_timeout().await;
|
||||
log::debug!("TopologyRefresher: Exiting");
|
||||
tracing::debug!("TopologyRefresher: Exiting");
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, error, warn};
|
||||
use nym_topology::provider_trait::TopologyProvider;
|
||||
use nym_topology::{NymTopology, NymTopologyMetadata};
|
||||
use nym_validator_client::UserAgent;
|
||||
use nym_topology::provider_trait::{ToTopologyMetadata, TopologyProvider};
|
||||
use nym_topology::NymTopology;
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::thread_rng;
|
||||
use std::cmp::min;
|
||||
use tracing::{debug, error, warn};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -49,18 +48,10 @@ impl NymApiTopologyProvider {
|
||||
pub fn new(
|
||||
config: impl Into<Config>,
|
||||
mut nym_api_urls: Vec<Url>,
|
||||
user_agent: Option<UserAgent>,
|
||||
mut validator_client: nym_validator_client::client::NymApiClient,
|
||||
) -> Self {
|
||||
nym_api_urls.shuffle(&mut thread_rng());
|
||||
|
||||
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())
|
||||
};
|
||||
validator_client.change_nym_api(nym_api_urls[0].clone());
|
||||
|
||||
NymApiTopologyProvider {
|
||||
config: config.into(),
|
||||
@@ -108,12 +99,8 @@ impl NymApiTopologyProvider {
|
||||
.filter(|n| n.performance.round_to_integer() >= self.config.min_node_performance())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
NymTopology::new(
|
||||
NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id),
|
||||
rewarded_set,
|
||||
Vec::new(),
|
||||
)
|
||||
.with_skimmed_nodes(&nodes_filtered)
|
||||
NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new())
|
||||
.with_skimmed_nodes(&nodes_filtered)
|
||||
} else {
|
||||
// if we're not using extended topology, we're only getting active set mixnodes and gateways
|
||||
|
||||
@@ -124,7 +111,7 @@ impl NymApiTopologyProvider {
|
||||
// TODO: we really should be getting ACTIVE gateways only
|
||||
let gateways_fut = self
|
||||
.validator_client
|
||||
.get_all_basic_entry_assigned_nodes_v2();
|
||||
.get_all_basic_entry_assigned_nodes_with_metadata();
|
||||
|
||||
let (rewarded_set, mixnodes_res, gateways_res) =
|
||||
futures::try_join!(rewarded_set_fut, mixnodes_fut, gateways_fut)
|
||||
@@ -136,7 +123,7 @@ impl NymApiTopologyProvider {
|
||||
let metadata = mixnodes_res.metadata;
|
||||
let mixnodes = mixnodes_res.nodes;
|
||||
|
||||
if gateways_res.metadata != metadata {
|
||||
if !gateways_res.metadata.consistency_check(&metadata) {
|
||||
warn!("inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", gateways_res.metadata);
|
||||
return None;
|
||||
}
|
||||
@@ -161,12 +148,8 @@ impl NymApiTopologyProvider {
|
||||
}
|
||||
}
|
||||
|
||||
NymTopology::new(
|
||||
NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id),
|
||||
rewarded_set,
|
||||
Vec::new(),
|
||||
)
|
||||
.with_skimmed_nodes(&nodes)
|
||||
NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new())
|
||||
.with_skimmed_nodes(&nodes)
|
||||
};
|
||||
|
||||
if !topology.is_minimally_routable() {
|
||||
|
||||
@@ -36,11 +36,18 @@ impl SizedData for Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct TransmissionBuffer<T> {
|
||||
buffer: HashMap<TransmissionLane, LaneBufferEntry<T>>,
|
||||
}
|
||||
|
||||
impl<T> Default for TransmissionBuffer<T> {
|
||||
fn default() -> Self {
|
||||
TransmissionBuffer {
|
||||
buffer: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> TransmissionBuffer<T> {
|
||||
pub(crate) fn new() -> Self {
|
||||
TransmissionBuffer {
|
||||
@@ -211,7 +218,7 @@ impl<T> TransmissionBuffer<T> {
|
||||
};
|
||||
|
||||
let msg = self.pop_front_from_lane(&lane)?;
|
||||
log::trace!("picking to send from lane: {lane:?}");
|
||||
tracing::trace!("picking to send from lane: {lane:?}");
|
||||
Some((lane, msg))
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
|
||||
use nym_gateway_client::error::GatewayClientError;
|
||||
use nym_topology::node::RoutingNodeError;
|
||||
use nym_topology::{NodeId, NymTopologyError};
|
||||
use nym_validator_client::nym_api::error::NymAPIError;
|
||||
use nym_validator_client::ValidatorClientError;
|
||||
use std::error::Error;
|
||||
use std::path::PathBuf;
|
||||
@@ -52,7 +53,15 @@ pub enum ClientCoreError {
|
||||
#[error("list of nym apis is empty")]
|
||||
ListOfNymApisIsEmpty,
|
||||
|
||||
#[error("the current network topology seem to be insufficient to route any packets through")]
|
||||
#[error("failed to resolve a query to nym API: {source}")]
|
||||
NymApiQueryFailure {
|
||||
#[from]
|
||||
source: NymAPIError,
|
||||
},
|
||||
|
||||
#[error(
|
||||
"the current network topology seem to be insufficient to route any packets through:\n\t{0}"
|
||||
)]
|
||||
InsufficientNetworkTopology(#[from] NymTopologyError),
|
||||
|
||||
#[error("experienced a failure with our reply surb persistent storage: {source}")]
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
use crate::error::ClientCoreError;
|
||||
use crate::init::types::RegistrationResult;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use log::{debug, info, trace, warn};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_gateway_client::GatewayClient;
|
||||
use nym_topology::node::RoutingNode;
|
||||
@@ -14,6 +13,7 @@ use rand::{seq::SliceRandom, Rng};
|
||||
#[cfg(unix)]
|
||||
use std::os::fd::RawFd;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tracing::{debug, info, trace, warn};
|
||||
use tungstenite::Message;
|
||||
use url::Url;
|
||||
|
||||
@@ -105,12 +105,15 @@ pub async fn gateways_for_init<R: Rng>(
|
||||
nym_validator_client::client::NymApiClient::new(nym_api.clone())
|
||||
};
|
||||
|
||||
log::debug!("Fetching list of gateways from: {nym_api}");
|
||||
tracing::debug!("Fetching list of gateways from: {nym_api}");
|
||||
|
||||
let gateways = client.get_all_basic_entry_assigned_nodes_v2().await?.nodes;
|
||||
let gateways = client
|
||||
.get_all_basic_entry_assigned_nodes_with_metadata()
|
||||
.await?
|
||||
.nodes;
|
||||
info!("nym api reports {} gateways", gateways.len());
|
||||
|
||||
log::trace!("Gateways: {gateways:#?}");
|
||||
tracing::trace!("Gateways: {gateways:#?}");
|
||||
|
||||
// filter out gateways below minimum performance and ones that could operate as a mixnode
|
||||
// (we don't want instability)
|
||||
@@ -120,10 +123,10 @@ pub async fn gateways_for_init<R: Rng>(
|
||||
.filter(|g| g.performance.round_to_integer() >= minimum_performance)
|
||||
.filter_map(|gateway| gateway.try_into().ok())
|
||||
.collect::<Vec<_>>();
|
||||
log::debug!("After checking validity: {}", valid_gateways.len());
|
||||
log::trace!("Valid gateways: {valid_gateways:#?}");
|
||||
tracing::debug!("After checking validity: {}", valid_gateways.len());
|
||||
tracing::trace!("Valid gateways: {valid_gateways:#?}");
|
||||
|
||||
log::info!(
|
||||
tracing::info!(
|
||||
"and {} after validity and performance filtering",
|
||||
valid_gateways.len()
|
||||
);
|
||||
@@ -286,7 +289,7 @@ pub(super) fn get_specified_gateway(
|
||||
gateways: &[RoutingNode],
|
||||
must_use_tls: bool,
|
||||
) -> Result<RoutingNode, ClientCoreError> {
|
||||
log::debug!("Requesting specified gateway: {gateway_identity}");
|
||||
tracing::debug!("Requesting specified gateway: {gateway_identity}");
|
||||
let user_gateway = ed25519::PublicKey::from_base58_string(gateway_identity)
|
||||
.map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?;
|
||||
|
||||
@@ -326,7 +329,7 @@ pub(super) async fn register_with_gateway(
|
||||
);
|
||||
|
||||
gateway_client.establish_connection().await.map_err(|err| {
|
||||
log::warn!("Failed to establish connection with gateway!");
|
||||
tracing::warn!("Failed to establish connection with gateway!");
|
||||
ClientCoreError::GatewayClientError {
|
||||
gateway_id: gateway_id.to_base58_string(),
|
||||
source: Box::new(err),
|
||||
@@ -336,7 +339,7 @@ pub(super) async fn register_with_gateway(
|
||||
.perform_initial_authentication()
|
||||
.await
|
||||
.map_err(|err| {
|
||||
log::warn!("Failed to register with the gateway {gateway_id}: {err}");
|
||||
tracing::warn!("Failed to register with the gateway {gateway_id}: {err}");
|
||||
ClientCoreError::GatewayClientError {
|
||||
gateway_id: gateway_id.to_base58_string(),
|
||||
source: Box::new(err),
|
||||
|
||||
@@ -63,7 +63,7 @@ where
|
||||
K::StorageError: Send + Sync + 'static,
|
||||
D::StorageError: Send + Sync + 'static,
|
||||
{
|
||||
log::trace!("Setting up new gateway");
|
||||
tracing::trace!("Setting up new gateway");
|
||||
|
||||
// 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?;
|
||||
@@ -202,10 +202,10 @@ where
|
||||
K::StorageError: Send + Sync + 'static,
|
||||
D::StorageError: Send + Sync + 'static,
|
||||
{
|
||||
log::debug!("Setting up gateway");
|
||||
tracing::debug!("Setting up gateway");
|
||||
match setup {
|
||||
GatewaySetup::MustLoad { gateway_id } => {
|
||||
log::debug!("GatewaySetup::MustLoad with id: {gateway_id:?}");
|
||||
tracing::debug!("GatewaySetup::MustLoad with id: {gateway_id:?}");
|
||||
use_loaded_gateway_details(key_store, details_store, gateway_id).await
|
||||
}
|
||||
GatewaySetup::New {
|
||||
@@ -214,7 +214,7 @@ where
|
||||
#[cfg(unix)]
|
||||
connection_fd_callback,
|
||||
} => {
|
||||
log::debug!("GatewaySetup::New with spec: {specification:?}");
|
||||
tracing::debug!("GatewaySetup::New with spec: {specification:?}");
|
||||
setup_new_gateway(
|
||||
key_store,
|
||||
details_store,
|
||||
@@ -230,7 +230,7 @@ where
|
||||
gateway_details,
|
||||
client_keys: managed_keys,
|
||||
} => {
|
||||
log::debug!("GatewaySetup::ReuseConnection");
|
||||
tracing::debug!("GatewaySetup::ReuseConnection");
|
||||
Ok(reuse_gateway_connection(
|
||||
*authenticated_ephemeral_client,
|
||||
*gateway_details,
|
||||
|
||||
@@ -9,7 +9,7 @@ license.workspace = true
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
dashmap.workspace = true
|
||||
log.workspace = true
|
||||
tracing.workspace = true
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
|
||||
@@ -23,7 +23,7 @@ features = ["fs"]
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
|
||||
workspace = true
|
||||
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
|
||||
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"]
|
||||
optional = true
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
/*
|
||||
* 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,6 +4,7 @@
|
||||
use crate::backend::Empty;
|
||||
use crate::{CombinedReplyStorage, ReplyStorageBackend};
|
||||
use async_trait::async_trait;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
// well, right now we don't have the browser storage : (
|
||||
// so we keep everything in memory
|
||||
@@ -38,7 +39,10 @@ impl ReplyStorageBackend for Backend {
|
||||
self.empty.init_fresh(fresh).await
|
||||
}
|
||||
|
||||
async fn load_surb_storage(&self) -> Result<CombinedReplyStorage, Self::StorageError> {
|
||||
self.empty.load_surb_storage().await
|
||||
async fn load_surb_storage(
|
||||
&self,
|
||||
surb_freshness_cutoff: OffsetDateTime,
|
||||
) -> Result<CombinedReplyStorage, Self::StorageError> {
|
||||
self.empty.load_surb_storage(surb_freshness_cutoff).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,15 @@
|
||||
|
||||
use crate::backend::fs_backend::{
|
||||
error::StorageError,
|
||||
models::{
|
||||
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag,
|
||||
StoredSurbSender,
|
||||
},
|
||||
models::{ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSurbSender},
|
||||
};
|
||||
use log::{error, info};
|
||||
use sqlx::{
|
||||
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
|
||||
ConnectOptions,
|
||||
};
|
||||
use std::path::Path;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{error, info};
|
||||
|
||||
use sqlx_pool_guard::SqlitePoolGuard;
|
||||
|
||||
@@ -39,7 +37,7 @@ impl StorageManager {
|
||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||
.synchronous(SqliteSynchronous::Normal)
|
||||
.auto_vacuum(SqliteAutoVacuum::Incremental)
|
||||
.filename(&database_path)
|
||||
.filename(database_path)
|
||||
.create_if_missing(fresh)
|
||||
.disable_statement_logging();
|
||||
|
||||
@@ -51,8 +49,7 @@ impl StorageManager {
|
||||
}
|
||||
};
|
||||
|
||||
let connection_pool =
|
||||
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
|
||||
let connection_pool = SqlitePoolGuard::new(connection_pool);
|
||||
|
||||
if let Err(err) = sqlx::migrate!("./fs_surbs_migrations")
|
||||
.run(&*connection_pool)
|
||||
@@ -81,9 +78,11 @@ impl StorageManager {
|
||||
}
|
||||
|
||||
pub async fn create_status_table(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)")
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"INSERT INTO status(flush_in_progress, previous_flush, client_in_use) VALUES (0, 0, 1)"
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -94,18 +93,18 @@ impl StorageManager {
|
||||
.map(|r| r.flush_in_progress > 0)
|
||||
}
|
||||
|
||||
pub async fn set_previous_flush_timestamp(&self, timestamp: i64) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp)
|
||||
pub async fn set_previous_flush(&self, timestamp: OffsetDateTime) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("UPDATE status SET previous_flush = ?", timestamp)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_previous_flush_timestamp(&self) -> Result<i64, sqlx::Error> {
|
||||
sqlx::query!("SELECT previous_flush_timestamp FROM status;")
|
||||
pub async fn get_previous_flush_time(&self) -> Result<OffsetDateTime, sqlx::Error> {
|
||||
sqlx::query!(r#"SELECT previous_flush AS "previous_flush: OffsetDateTime" FROM status"#)
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.previous_flush_timestamp)
|
||||
.map(|r| r.previous_flush)
|
||||
}
|
||||
|
||||
pub async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> {
|
||||
@@ -131,32 +130,6 @@ impl StorageManager {
|
||||
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> {
|
||||
sqlx::query!("DELETE FROM reply_key;")
|
||||
.execute(&*self.connection_pool)
|
||||
@@ -165,7 +138,7 @@ impl StorageManager {
|
||||
}
|
||||
|
||||
pub async fn get_reply_keys(&self) -> Result<Vec<StoredReplyKey>, sqlx::Error> {
|
||||
sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",)
|
||||
sqlx::query_as("SELECT * FROM reply_key;")
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
@@ -176,11 +149,11 @@ impl StorageManager {
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO reply_key(key_digest, reply_key, sent_at_timestamp) VALUES (?, ?, ?);
|
||||
INSERT INTO reply_key(key_digest, reply_key, sent_at) VALUES (?, ?, ?);
|
||||
"#,
|
||||
stored_reply_key.key_digest,
|
||||
stored_reply_key.reply_key,
|
||||
stored_reply_key.sent_at_timestamp
|
||||
stored_reply_key.sent_at
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
@@ -188,7 +161,7 @@ impl StorageManager {
|
||||
}
|
||||
|
||||
pub async fn get_surb_senders(&self) -> Result<Vec<StoredSurbSender>, sqlx::Error> {
|
||||
sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",)
|
||||
sqlx::query_as("SELECT * FROM reply_surb_sender;")
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
@@ -199,10 +172,10 @@ impl StorageManager {
|
||||
) -> Result<i64, sqlx::Error> {
|
||||
let id = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO reply_surb_sender(tag, last_sent_timestamp) VALUES (?, ?);
|
||||
INSERT INTO reply_surb_sender(tag, last_sent) VALUES (?, ?);
|
||||
"#,
|
||||
stored_surb_sender.tag,
|
||||
stored_surb_sender.last_sent_timestamp
|
||||
stored_surb_sender.last_sent
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?
|
||||
@@ -217,7 +190,7 @@ impl StorageManager {
|
||||
sqlx::query_as!(
|
||||
StoredReplySurb,
|
||||
r#"
|
||||
SELECT reply_surb_sender_id, reply_surb, encoded_key_rotation as "encoded_key_rotation: u8" FROM reply_surb
|
||||
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
|
||||
|
||||
@@ -4,20 +4,16 @@
|
||||
use crate::{
|
||||
backend::fs_backend::{
|
||||
manager::StorageManager,
|
||||
models::{
|
||||
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag,
|
||||
StoredSurbSender,
|
||||
},
|
||||
models::{ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSurbSender},
|
||||
},
|
||||
surb_storage::ReceivedReplySurbs,
|
||||
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys,
|
||||
UsedSenderTags,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, error, info, warn};
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use std::path::{Path, PathBuf};
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{error, info, warn};
|
||||
|
||||
pub use self::error::StorageError;
|
||||
|
||||
@@ -57,10 +53,7 @@ impl Backend {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn try_load<P: AsRef<Path>>(
|
||||
database_path: P,
|
||||
fresh_sender_tags: bool,
|
||||
) -> Result<Self, StorageError> {
|
||||
pub async fn try_load<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> {
|
||||
let owned_path: PathBuf = database_path.as_ref().into();
|
||||
if owned_path.file_name().is_none() {
|
||||
return Err(StorageError::DatabasePathWithoutFilename {
|
||||
@@ -69,7 +62,7 @@ impl Backend {
|
||||
}
|
||||
|
||||
let manager = StorageManager::init(database_path, false).await?;
|
||||
match Self::try_load_inner(&manager, fresh_sender_tags).await {
|
||||
match Self::try_load_inner(&manager).await {
|
||||
Ok(()) => Ok(Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
@@ -87,18 +80,15 @@ impl Backend {
|
||||
self.manager.close_pool().await
|
||||
}
|
||||
|
||||
async fn try_load_inner(
|
||||
manager: &StorageManager,
|
||||
fresh_sender_tags: bool,
|
||||
) -> Result<(), StorageError> {
|
||||
async fn try_load_inner(manager: &StorageManager) -> Result<(), StorageError> {
|
||||
// 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)
|
||||
if manager.get_flush_status().await? {
|
||||
return Err(StorageError::IncompleteDataFlush);
|
||||
}
|
||||
|
||||
let last_flush_timestamp = manager.get_previous_flush_timestamp().await?;
|
||||
if last_flush_timestamp == 0 {
|
||||
let last_flush = manager.get_previous_flush_time().await?;
|
||||
if last_flush == OffsetDateTime::UNIX_EPOCH {
|
||||
// either this client has been running since 1970 or the flush failed
|
||||
return Err(StorageError::IncompleteDataFlush);
|
||||
}
|
||||
@@ -118,15 +108,6 @@ impl Backend {
|
||||
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
|
||||
// (assuming no key rotation has happened)
|
||||
// but the way it's currently coded, everyone will purge old data
|
||||
@@ -144,14 +125,6 @@ impl Backend {
|
||||
manager.delete_all_reply_keys().await?;
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
@@ -196,7 +169,7 @@ impl Backend {
|
||||
|
||||
async fn end_storage_flush(&self) -> Result<(), StorageError> {
|
||||
self.manager
|
||||
.set_previous_flush_timestamp(OffsetDateTime::now_utc().unix_timestamp())
|
||||
.set_previous_flush(OffsetDateTime::now_utc())
|
||||
.await?;
|
||||
Ok(self.manager.set_flush_status(false).await?)
|
||||
}
|
||||
@@ -209,29 +182,6 @@ impl Backend {
|
||||
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> {
|
||||
let stored = self.manager.get_reply_keys().await?;
|
||||
|
||||
@@ -255,14 +205,17 @@ impl Backend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_stored_reply_surbs(&self) -> Result<ReceivedReplySurbsMap, StorageError> {
|
||||
async fn get_stored_reply_surbs(
|
||||
&self,
|
||||
surb_freshness_cutoff: OffsetDateTime,
|
||||
) -> Result<ReceivedReplySurbsMap, StorageError> {
|
||||
let surb_senders = self.manager.get_surb_senders().await?;
|
||||
|
||||
let metadata = self.get_reply_surb_storage_metadata().await?;
|
||||
let mut received_surbs = Vec::with_capacity(surb_senders.len());
|
||||
for sender in surb_senders {
|
||||
let sender_id = sender.id;
|
||||
let (sender_tag, surbs_last_received_at_timestamp): (AnonymousSenderTag, i64) =
|
||||
let (sender_tag, surbs_last_received_at): (AnonymousSenderTag, OffsetDateTime) =
|
||||
sender.try_into()?;
|
||||
let stored_surbs = self
|
||||
.manager
|
||||
@@ -274,15 +227,17 @@ impl Backend {
|
||||
|
||||
received_surbs.push((
|
||||
sender_tag,
|
||||
ReceivedReplySurbs::new_retrieved(stored_surbs, surbs_last_received_at_timestamp),
|
||||
ReceivedReplySurbs::new_retrieved(stored_surbs, surbs_last_received_at),
|
||||
))
|
||||
}
|
||||
|
||||
Ok(ReceivedReplySurbsMap::from_raw(
|
||||
let received_surbs = ReceivedReplySurbsMap::from_raw(
|
||||
metadata.min_reply_surb_threshold as usize,
|
||||
metadata.max_reply_surb_threshold as usize,
|
||||
received_surbs,
|
||||
))
|
||||
);
|
||||
received_surbs.drop_stale_loaded_surbs(surb_freshness_cutoff);
|
||||
Ok(received_surbs)
|
||||
}
|
||||
|
||||
async fn dump_reply_surbs(
|
||||
@@ -304,6 +259,14 @@ impl Backend {
|
||||
.insert_reply_surb(StoredReplySurb::new(sender_id, reply_surb))
|
||||
.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(())
|
||||
}
|
||||
@@ -347,7 +310,6 @@ impl ReplyStorageBackend for Backend {
|
||||
self.rotate().await?;
|
||||
self.start_storage_flush().await?;
|
||||
|
||||
self.dump_sender_tags(storage.tags_storage_ref()).await?;
|
||||
self.dump_sender_reply_keys(storage.key_storage_ref())
|
||||
.await?;
|
||||
let surbs_ref = storage.surbs_storage_ref();
|
||||
@@ -364,12 +326,14 @@ impl ReplyStorageBackend for Backend {
|
||||
.await
|
||||
}
|
||||
|
||||
async fn load_surb_storage(&self) -> Result<CombinedReplyStorage, Self::StorageError> {
|
||||
async fn load_surb_storage(
|
||||
&self,
|
||||
surb_freshness_cutoff: OffsetDateTime,
|
||||
) -> Result<CombinedReplyStorage, Self::StorageError> {
|
||||
let reply_keys = self.get_stored_reply_keys().await?;
|
||||
let tags = self.get_stored_tags().await?;
|
||||
let reply_surbs = self.get_stored_reply_surbs().await?;
|
||||
let reply_surbs = self.get_stored_reply_surbs(surb_freshness_cutoff).await?;
|
||||
|
||||
Ok(CombinedReplyStorage::load(reply_keys, reply_surbs, tags))
|
||||
Ok(CombinedReplyStorage::load(reply_keys, reply_surbs))
|
||||
}
|
||||
|
||||
async fn stop_storage_session(self) -> Result<(), Self::StorageError> {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use crate::backend::fs_backend::error::StorageError;
|
||||
use crate::key_storage::UsedReplyKey;
|
||||
use crate::ReceivedReplySurb;
|
||||
use nym_crypto::generic_array::typenum::Unsigned;
|
||||
use nym_crypto::Digest;
|
||||
use nym_sphinx::addressing::clients::{Recipient, RecipientBytes};
|
||||
@@ -12,6 +13,8 @@ use nym_sphinx::anonymous_replies::{
|
||||
ReplySurb, ReplySurbWithKeyRotation, SurbEncryptionKey, SurbEncryptionKeySize,
|
||||
};
|
||||
use nym_sphinx::params::{ReplySurbKeyDigestAlgorithm, SphinxKeyRotation};
|
||||
use sqlx::FromRow;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StoredSenderTag {
|
||||
@@ -58,11 +61,11 @@ impl TryFrom<StoredSenderTag> for (RecipientBytes, AnonymousSenderTag) {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct StoredReplyKey {
|
||||
pub key_digest: Vec<u8>,
|
||||
pub reply_key: Vec<u8>,
|
||||
pub sent_at_timestamp: i64,
|
||||
pub sent_at: OffsetDateTime,
|
||||
}
|
||||
|
||||
impl StoredReplyKey {
|
||||
@@ -70,7 +73,7 @@ impl StoredReplyKey {
|
||||
StoredReplyKey {
|
||||
key_digest: key_digest.to_vec(),
|
||||
reply_key: (*reply_key).to_bytes(),
|
||||
sent_at_timestamp: reply_key.sent_at_timestamp,
|
||||
sent_at: reply_key.sent_at,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -100,32 +103,30 @@ impl TryFrom<StoredReplyKey> for (EncryptionKeyDigest, UsedReplyKey) {
|
||||
});
|
||||
};
|
||||
|
||||
Ok((
|
||||
digest,
|
||||
UsedReplyKey::new(reply_key, value.sent_at_timestamp),
|
||||
))
|
||||
Ok((digest, UsedReplyKey::new(reply_key, value.sent_at)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(FromRow)]
|
||||
pub struct StoredSurbSender {
|
||||
pub id: i64,
|
||||
pub tag: Vec<u8>,
|
||||
pub last_sent_timestamp: i64,
|
||||
pub last_sent: OffsetDateTime,
|
||||
}
|
||||
|
||||
impl StoredSurbSender {
|
||||
pub fn new(tag: AnonymousSenderTag, last_sent_timestamp: i64) -> Self {
|
||||
pub fn new(tag: AnonymousSenderTag, last_sent: OffsetDateTime) -> Self {
|
||||
StoredSurbSender {
|
||||
// for the purposes of STORING data,
|
||||
// we ignore that field anyway
|
||||
id: 0,
|
||||
tag: tag.to_bytes().to_vec(),
|
||||
last_sent_timestamp,
|
||||
last_sent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<StoredSurbSender> for (AnonymousSenderTag, i64) {
|
||||
impl TryFrom<StoredSurbSender> for (AnonymousSenderTag, OffsetDateTime) {
|
||||
type Error = StorageError;
|
||||
|
||||
fn try_from(value: StoredSurbSender) -> Result<Self, Self::Error> {
|
||||
@@ -140,7 +141,7 @@ impl TryFrom<StoredSurbSender> for (AnonymousSenderTag, i64) {
|
||||
|
||||
Ok((
|
||||
AnonymousSenderTag::from_bytes(sender_tag_bytes),
|
||||
value.last_sent_timestamp,
|
||||
value.last_sent,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -155,10 +156,10 @@ pub struct StoredReplySurb {
|
||||
}
|
||||
|
||||
impl StoredReplySurb {
|
||||
pub fn new(reply_surb_sender_id: i64, reply_surb: &ReplySurbWithKeyRotation) -> Self {
|
||||
pub fn new(reply_surb_sender_id: i64, reply_surb: &ReceivedReplySurb) -> Self {
|
||||
StoredReplySurb {
|
||||
reply_surb_sender_id,
|
||||
reply_surb: reply_surb.inner_reply_surb().to_bytes(),
|
||||
reply_surb: reply_surb.surb.inner_reply_surb().to_bytes(),
|
||||
encoded_key_rotation: reply_surb.key_rotation() as u8,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::CombinedReplyStorage;
|
||||
use async_trait::async_trait;
|
||||
use std::error::Error;
|
||||
use thiserror::Error;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
// TODO: this should now live inside our wasm/client-core
|
||||
pub mod browser_backend;
|
||||
@@ -53,7 +54,10 @@ impl ReplyStorageBackend for Empty {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn load_surb_storage(&self) -> Result<CombinedReplyStorage, Self::StorageError> {
|
||||
async fn load_surb_storage(
|
||||
&self,
|
||||
_: OffsetDateTime,
|
||||
) -> Result<CombinedReplyStorage, Self::StorageError> {
|
||||
Ok(CombinedReplyStorage::new(
|
||||
self.min_surb_threshold,
|
||||
self.max_surb_threshold,
|
||||
@@ -80,7 +84,10 @@ pub trait ReplyStorageBackend: Sized {
|
||||
/// (such as surb thresholds)
|
||||
async fn init_fresh(&mut self, fresh: &CombinedReplyStorage) -> Result<(), Self::StorageError>;
|
||||
|
||||
async fn load_surb_storage(&self) -> Result<CombinedReplyStorage, Self::StorageError>;
|
||||
async fn load_surb_storage(
|
||||
&self,
|
||||
surb_freshness_cutoff: OffsetDateTime,
|
||||
) -> Result<CombinedReplyStorage, Self::StorageError>;
|
||||
|
||||
async fn stop_storage_session(self) -> Result<(), Self::StorageError> {
|
||||
Ok(())
|
||||
|
||||
@@ -25,12 +25,11 @@ impl CombinedReplyStorage {
|
||||
pub fn load(
|
||||
sent_reply_keys: SentReplyKeys,
|
||||
received_reply_surbs: ReceivedReplySurbsMap,
|
||||
used_tags: UsedSenderTags,
|
||||
) -> Self {
|
||||
CombinedReplyStorage {
|
||||
sent_reply_keys,
|
||||
received_reply_surbs,
|
||||
used_tags,
|
||||
used_tags: UsedSenderTags::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -47,8 +47,12 @@ impl SentReplyKeys {
|
||||
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>) {
|
||||
let now = OffsetDateTime::now_utc().unix_timestamp();
|
||||
let now = OffsetDateTime::now_utc();
|
||||
for key in keys {
|
||||
self.insert(UsedReplyKey::new(key, now))
|
||||
}
|
||||
@@ -71,15 +75,12 @@ impl SentReplyKeys {
|
||||
pub struct UsedReplyKey {
|
||||
key: SurbEncryptionKey,
|
||||
// the purpose of this field is to perform invalidation at relatively very long intervals
|
||||
pub sent_at_timestamp: i64,
|
||||
pub sent_at: OffsetDateTime,
|
||||
}
|
||||
|
||||
impl UsedReplyKey {
|
||||
pub(crate) fn new(key: SurbEncryptionKey, sent_at_timestamp: i64) -> Self {
|
||||
UsedReplyKey {
|
||||
key,
|
||||
sent_at_timestamp,
|
||||
}
|
||||
pub(crate) fn new(key: SurbEncryptionKey, sent_at: OffsetDateTime) -> Self {
|
||||
UsedReplyKey { key, sent_at }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
pub use backend::*;
|
||||
pub use combined::CombinedReplyStorage;
|
||||
pub use key_storage::SentReplyKeys;
|
||||
pub use surb_storage::ReceivedReplySurbsMap;
|
||||
pub use surb_storage::{ReceivedReplySurb, ReceivedReplySurbsMap, RetrievedReplySurb};
|
||||
pub use tag_storage::UsedSenderTags;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
mod backend;
|
||||
mod combined;
|
||||
@@ -29,8 +30,11 @@ where
|
||||
PersistentReplyStorage { backend }
|
||||
}
|
||||
|
||||
pub async fn load_state_from_backend(&self) -> Result<CombinedReplyStorage, T::StorageError> {
|
||||
self.backend.load_surb_storage().await
|
||||
pub async fn load_state_from_backend(
|
||||
&self,
|
||||
surb_freshness_cutoff: OffsetDateTime,
|
||||
) -> Result<CombinedReplyStorage, T::StorageError> {
|
||||
self.backend.load_surb_storage(surb_freshness_cutoff).await
|
||||
}
|
||||
|
||||
pub async fn flush_on_shutdown(
|
||||
@@ -38,7 +42,7 @@ where
|
||||
mem_state: CombinedReplyStorage,
|
||||
mut shutdown: nym_task::TaskClient,
|
||||
) {
|
||||
use log::{debug, error, info};
|
||||
use tracing::{debug, error, info};
|
||||
|
||||
debug!("Started PersistentReplyStorage");
|
||||
if let Err(err) = self.backend.start_storage_session().await {
|
||||
|
||||
@@ -1,15 +1,45 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use dashmap::iter::Iter;
|
||||
use dashmap::iter::{Iter, IterMut};
|
||||
use dashmap::DashMap;
|
||||
use log::trace;
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use nym_sphinx::anonymous_replies::ReplySurbWithKeyRotation;
|
||||
use nym_sphinx::params::SphinxKeyRotation;
|
||||
use std::cmp::min;
|
||||
use std::collections::VecDeque;
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
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)]
|
||||
pub struct ReceivedReplySurbsMap {
|
||||
@@ -57,17 +87,40 @@ impl ReceivedReplySurbsMap {
|
||||
self.inner.data.iter()
|
||||
}
|
||||
|
||||
pub fn remove(&self, target: &AnonymousSenderTag) {
|
||||
self.inner.data.remove(target);
|
||||
pub fn as_raw_iter_mut(&self) -> IterMut<'_, AnonymousSenderTag, ReceivedReplySurbs> {
|
||||
self.inner.data.iter_mut()
|
||||
}
|
||||
|
||||
pub fn reset_surbs_last_received_at(&self, target: &AnonymousSenderTag) {
|
||||
if let Some(mut entry) = self.inner.data.get_mut(target) {
|
||||
entry.surbs_last_received_at_timestamp = OffsetDateTime::now_utc().unix_timestamp();
|
||||
fn total_surbs(&self) -> usize {
|
||||
self.inner
|
||||
.data
|
||||
.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 surbs_last_received_at(&self, target: &AnonymousSenderTag) -> Option<i64> {
|
||||
pub fn retain(&self, f: impl FnMut(&AnonymousSenderTag, &mut ReceivedReplySurbs) -> bool) {
|
||||
self.inner.data.retain(f);
|
||||
}
|
||||
|
||||
pub fn surbs_last_received_at(&self, target: &AnonymousSenderTag) -> Option<OffsetDateTime> {
|
||||
self.inner
|
||||
.data
|
||||
.get(target)
|
||||
@@ -126,15 +179,25 @@ impl ReceivedReplySurbsMap {
|
||||
.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 {
|
||||
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(
|
||||
&self,
|
||||
target: &AnonymousSenderTag,
|
||||
amount: usize,
|
||||
) -> (Option<Vec<ReplySurbWithKeyRotation>>, usize) {
|
||||
) -> (Option<Vec<RetrievedReplySurb>>, usize) {
|
||||
if let Some(mut entry) = self.inner.data.get_mut(target) {
|
||||
let surbs_left = entry.items_left();
|
||||
if surbs_left < self.min_surb_threshold() + amount {
|
||||
@@ -150,34 +213,72 @@ impl ReceivedReplySurbsMap {
|
||||
pub fn get_reply_surb_ignoring_threshold(
|
||||
&self,
|
||||
target: &AnonymousSenderTag,
|
||||
) -> Option<(Option<ReplySurbWithKeyRotation>, usize)> {
|
||||
self.inner
|
||||
.data
|
||||
.get_mut(target)
|
||||
.map(|mut s| s.get_reply_surb())
|
||||
) -> (Option<RetrievedReplySurb>, usize) {
|
||||
let Some(mut entry) = self.inner.data.get_mut(target) else {
|
||||
return (None, 0);
|
||||
};
|
||||
|
||||
entry.get_reply_surb()
|
||||
}
|
||||
|
||||
pub fn get_reply_surb(
|
||||
&self,
|
||||
target: &AnonymousSenderTag,
|
||||
) -> Option<(Option<ReplySurbWithKeyRotation>, usize)> {
|
||||
self.inner.data.get_mut(target).map(|mut entry| {
|
||||
let surbs_left = entry.items_left();
|
||||
if surbs_left < self.min_surb_threshold() {
|
||||
(None, surbs_left)
|
||||
} else {
|
||||
entry.get_reply_surb()
|
||||
}
|
||||
})
|
||||
) -> (Option<RetrievedReplySurb>, usize) {
|
||||
let Some(mut entry) = self.inner.data.get_mut(target) else {
|
||||
return (None, 0);
|
||||
};
|
||||
|
||||
let surbs_left = entry.items_left();
|
||||
if surbs_left < self.min_surb_threshold() {
|
||||
(None, surbs_left)
|
||||
} else {
|
||||
entry.get_reply_surb()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_surbs<I: IntoIterator<Item = ReplySurbWithKeyRotation>>(
|
||||
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 {
|
||||
entry.data.push_front(returned_surb.reply_surb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_fresh_surbs<I: IntoIterator<Item = ReplySurbWithKeyRotation>>(
|
||||
&self,
|
||||
target: &AnonymousSenderTag,
|
||||
surbs: I,
|
||||
) {
|
||||
if let Some(mut existing_data) = self.inner.data.get_mut(target) {
|
||||
existing_data.insert_reply_surbs(surbs)
|
||||
existing_data.insert_fresh_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 {
|
||||
let new_entry = ReceivedReplySurbs::new(surbs.into_iter().collect());
|
||||
self.inner.data.insert(*target, new_entry);
|
||||
@@ -185,44 +286,102 @@ 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)]
|
||||
pub struct ReceivedReplySurbs {
|
||||
// in the future we'd probably want to put extra data here to indicate when the SURBs got received
|
||||
// so we could invalidate entries from the previous key rotations
|
||||
data: VecDeque<ReplySurbWithKeyRotation>,
|
||||
data: VecDeque<ReceivedReplySurb>,
|
||||
possibly_stale: VecDeque<ReceivedReplySurb>,
|
||||
|
||||
pending_reception: u32,
|
||||
surbs_last_received_at_timestamp: i64,
|
||||
surbs_last_received_at: OffsetDateTime,
|
||||
}
|
||||
|
||||
impl ReceivedReplySurbs {
|
||||
fn new(initial_surbs: VecDeque<ReplySurbWithKeyRotation>) -> Self {
|
||||
ReceivedReplySurbs {
|
||||
data: initial_surbs,
|
||||
let mut this = ReceivedReplySurbs {
|
||||
data: Default::default(),
|
||||
possibly_stale: Default::default(),
|
||||
pending_reception: 0,
|
||||
surbs_last_received_at_timestamp: OffsetDateTime::now_utc().unix_timestamp(),
|
||||
}
|
||||
surbs_last_received_at: OffsetDateTime::now_utc(),
|
||||
};
|
||||
this.insert_fresh_reply_surbs(initial_surbs);
|
||||
this
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-surb-storage"))]
|
||||
pub fn new_retrieved(
|
||||
surbs: Vec<ReplySurbWithKeyRotation>,
|
||||
surbs_last_received_at_timestamp: i64,
|
||||
surbs_last_received_at: OffsetDateTime,
|
||||
) -> ReceivedReplySurbs {
|
||||
ReceivedReplySurbs {
|
||||
data: surbs.into(),
|
||||
let mut this = ReceivedReplySurbs {
|
||||
data: Default::default(),
|
||||
possibly_stale: Default::default(),
|
||||
pending_reception: 0,
|
||||
surbs_last_received_at_timestamp,
|
||||
}
|
||||
surbs_last_received_at,
|
||||
};
|
||||
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"))]
|
||||
pub fn surbs_ref(&self) -> &VecDeque<ReplySurbWithKeyRotation> {
|
||||
pub fn surbs_ref(&self) -> &VecDeque<ReceivedReplySurb> {
|
||||
&self.data
|
||||
}
|
||||
|
||||
pub fn surbs_last_received_at(&self) -> i64 {
|
||||
self.surbs_last_received_at_timestamp
|
||||
pub fn retain_fresh_surbs(&mut self, f: impl FnMut(&ReceivedReplySurb) -> bool) {
|
||||
self.data.retain(f);
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -243,39 +402,78 @@ impl ReceivedReplySurbs {
|
||||
self.pending_reception = 0;
|
||||
}
|
||||
|
||||
pub fn get_reply_surbs(
|
||||
&mut self,
|
||||
amount: usize,
|
||||
) -> (Option<Vec<ReplySurbWithKeyRotation>>, usize) {
|
||||
/// Attempt to retrieve the specified number of reply SURBs (if at least that many are present)
|
||||
/// 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 {
|
||||
(None, self.items_left())
|
||||
} else {
|
||||
let surbs = self.data.drain(..amount).collect();
|
||||
(Some(surbs), self.items_left())
|
||||
let available_fresh = self.fresh_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<ReplySurbWithKeyRotation>, usize) {
|
||||
pub fn get_reply_surb(&mut self) -> (Option<RetrievedReplySurb>, usize) {
|
||||
(self.pop_surb(), self.items_left())
|
||||
}
|
||||
|
||||
fn pop_surb(&mut self) -> Option<ReplySurbWithKeyRotation> {
|
||||
self.data.pop_front()
|
||||
fn pop_surb(&mut self) -> Option<RetrievedReplySurb> {
|
||||
// prefer the 'fresh' data if available. otherwise fallback to the possibly stale entries
|
||||
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 {
|
||||
self.data.len()
|
||||
self.data.len() + self.possibly_stale.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
|
||||
pub fn insert_reply_surbs<I: IntoIterator<Item = ReplySurbWithKeyRotation>>(
|
||||
pub(crate) fn insert_fresh_reply_surbs<I: IntoIterator<Item = ReplySurbWithKeyRotation>>(
|
||||
&mut self,
|
||||
surbs: I,
|
||||
) {
|
||||
let mut v = surbs.into_iter().collect::<VecDeque<_>>();
|
||||
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());
|
||||
self.data.append(&mut v);
|
||||
self.surbs_last_received_at_timestamp = OffsetDateTime::now_utc().unix_timestamp();
|
||||
self.surbs_last_received_at = received_at;
|
||||
trace!("we now have {} surbs!", self.data.len());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,7 +272,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
) -> Result<(), GatewayClientError> {
|
||||
if let Some(shared_key) = self.shared_key() {
|
||||
let encrypted = message.encrypt(&*shared_key)?;
|
||||
Box::pin(self.send_websocket_message(encrypted)).await?;
|
||||
Box::pin(self.send_websocket_message_without_response(encrypted)).await?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(GatewayClientError::ConnectionInInvalidState)
|
||||
@@ -330,9 +330,80 @@ 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,
|
||||
// as we need to be able to write the request and read the subsequent response
|
||||
pub async fn send_websocket_message(
|
||||
pub async fn send_websocket_message_with_response(
|
||||
&mut self,
|
||||
msg: impl Into<Message>,
|
||||
) -> Result<ServerResponse, GatewayClientError> {
|
||||
@@ -387,29 +458,6 @@ 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(
|
||||
&self,
|
||||
gateway_protocol: Option<u8>,
|
||||
@@ -535,7 +583,10 @@ impl<C, St> GatewayClient<C, St> {
|
||||
.encrypt(legacy_key)?;
|
||||
|
||||
info!("sending upgrade request and awaiting the acknowledgement back");
|
||||
let (ciphertext, nonce) = match self.send_websocket_message(upgrade_request).await? {
|
||||
let (ciphertext, nonce) = match self
|
||||
.send_websocket_message_with_response(upgrade_request)
|
||||
.await?
|
||||
{
|
||||
ServerResponse::EncryptedResponse { ciphertext, nonce } => (ciphertext, nonce),
|
||||
ServerResponse::Error { message } => {
|
||||
return Err(GatewayClientError::GatewayError(message))
|
||||
@@ -567,7 +618,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
&mut self,
|
||||
msg: ClientControlRequest,
|
||||
) -> Result<(), GatewayClientError> {
|
||||
match self.send_websocket_message(msg).await? {
|
||||
match self.send_websocket_message_with_response(msg).await? {
|
||||
ServerResponse::Authenticate {
|
||||
protocol_version,
|
||||
status,
|
||||
@@ -717,13 +768,16 @@ 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> {
|
||||
if !self.connection.is_established() {
|
||||
return Err(GatewayClientError::ConnectionNotEstablished);
|
||||
}
|
||||
|
||||
match self
|
||||
.send_websocket_message(ClientControlRequest::SupportedProtocol {})
|
||||
.send_websocket_message_with_non_send_response(
|
||||
ClientControlRequest::SupportedProtocol {},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
ServerResponse::SupportedProtocol { version } => Ok(version),
|
||||
@@ -740,7 +794,10 @@ impl<C, St> GatewayClient<C, St> {
|
||||
credential,
|
||||
self.shared_key.as_ref().unwrap(),
|
||||
)?;
|
||||
let bandwidth_remaining = match self.send_websocket_message(msg).await? {
|
||||
let bandwidth_remaining = match self
|
||||
.send_websocket_message_with_non_send_response(msg)
|
||||
.await?
|
||||
{
|
||||
ServerResponse::Bandwidth { available_total } => Ok(available_total),
|
||||
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
|
||||
ServerResponse::TypedError { error } => {
|
||||
@@ -758,7 +815,10 @@ impl<C, St> GatewayClient<C, St> {
|
||||
|
||||
async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> {
|
||||
let msg = ClientControlRequest::ClaimFreeTestnetBandwidth;
|
||||
let bandwidth_remaining = match self.send_websocket_message(msg).await? {
|
||||
let bandwidth_remaining = match self
|
||||
.send_websocket_message_with_non_send_response(msg)
|
||||
.await?
|
||||
{
|
||||
ServerResponse::Bandwidth { available_total } => Ok(available_total),
|
||||
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
|
||||
other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
|
||||
|
||||
@@ -28,6 +28,7 @@ pub struct Config {
|
||||
pub maximum_reconnection_backoff: Duration,
|
||||
pub initial_connection_timeout: Duration,
|
||||
pub maximum_connection_buffer_size: usize,
|
||||
pub use_legacy_packet_encoding: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -36,12 +37,14 @@ impl Config {
|
||||
maximum_reconnection_backoff: Duration,
|
||||
initial_connection_timeout: Duration,
|
||||
maximum_connection_buffer_size: usize,
|
||||
use_legacy_packet_encoding: bool,
|
||||
) -> Self {
|
||||
Config {
|
||||
initial_reconnection_backoff,
|
||||
maximum_reconnection_backoff,
|
||||
initial_connection_timeout,
|
||||
maximum_connection_buffer_size,
|
||||
use_legacy_packet_encoding,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -267,7 +270,12 @@ impl SendWithoutResponse for Client {
|
||||
fn send_without_response(&self, packet: MixPacket) -> io::Result<()> {
|
||||
let address = packet.next_hop_address();
|
||||
trace!("Sending packet to {address}");
|
||||
let framed_packet = FramedNymPacket::from(packet);
|
||||
|
||||
// TODO: optimisation for the future: rather than constantly using legacy encoding,
|
||||
// once we're addressing by node_id (and thus have full node info here),
|
||||
// we could simply infer supported encoding based on their version
|
||||
let framed_packet =
|
||||
FramedNymPacket::from_mix_packet(packet, self.config.use_legacy_packet_encoding);
|
||||
|
||||
let Some(sender) = self.active_connections.get_mut(&address) else {
|
||||
// there was never a connection to begin with
|
||||
@@ -328,6 +336,7 @@ mod tests {
|
||||
maximum_reconnection_backoff: Duration::from_millis(300_000),
|
||||
initial_connection_timeout: Duration::from_millis(1_500),
|
||||
maximum_connection_buffer_size: 128,
|
||||
use_legacy_packet_encoding: false,
|
||||
},
|
||||
NoiseConfig::new(
|
||||
Arc::new(x25519::KeyPair::new(&mut rng)),
|
||||
|
||||
@@ -72,7 +72,7 @@ macro_rules! collect_paged_skimmed_v2 {
|
||||
.$f(false, Some(page), None, $self.use_bincode)
|
||||
.await?;
|
||||
|
||||
if metadata != res.metadata {
|
||||
if !metadata.consistency_check(&res.metadata) {
|
||||
return Err(ValidatorClientError::InconsistentPagedMetadata);
|
||||
}
|
||||
|
||||
@@ -471,12 +471,12 @@ impl NymApiClient {
|
||||
pub async fn get_all_basic_entry_assigned_nodes(
|
||||
&self,
|
||||
) -> Result<Vec<SkimmedNode>, ValidatorClientError> {
|
||||
self.get_all_basic_entry_assigned_nodes_v2()
|
||||
self.get_all_basic_entry_assigned_nodes_with_metadata()
|
||||
.await
|
||||
.map(|res| res.nodes)
|
||||
}
|
||||
|
||||
pub async fn get_all_basic_entry_assigned_nodes_v2(
|
||||
pub async fn get_all_basic_entry_assigned_nodes_with_metadata(
|
||||
&self,
|
||||
) -> Result<SkimmedNodesWithMetadata, ValidatorClientError> {
|
||||
collect_paged_skimmed_v2!(self, get_basic_entry_assigned_nodes_v2)
|
||||
|
||||
@@ -389,7 +389,6 @@ pub trait NymApiClientExt: ApiClient {
|
||||
routes::NYM_NODES_ROUTES,
|
||||
"skimmed",
|
||||
"entry-gateways",
|
||||
"all",
|
||||
],
|
||||
¶ms,
|
||||
)
|
||||
|
||||
@@ -41,6 +41,11 @@ pub trait DkgQueryClient {
|
||||
self.query_dkg_contract(request).await
|
||||
}
|
||||
|
||||
async fn get_epoch_at_height(&self, height: u64) -> Result<Option<Epoch>, NyxdError> {
|
||||
let request = DkgQueryMsg::GetEpochStateAtHeight { height };
|
||||
self.query_dkg_contract(request).await
|
||||
}
|
||||
|
||||
async fn can_advance_state(&self) -> Result<StateAdvanceResponse, NyxdError> {
|
||||
let request = DkgQueryMsg::CanAdvanceState {};
|
||||
self.query_dkg_contract(request).await
|
||||
@@ -87,6 +92,34 @@ pub trait DkgQueryClient {
|
||||
self.query_dkg_contract(request).await
|
||||
}
|
||||
|
||||
async fn get_epoch_dealers_paged(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PagedDealerResponse, NyxdError> {
|
||||
let request = DkgQueryMsg::GetEpochDealers {
|
||||
epoch_id,
|
||||
start_after,
|
||||
limit,
|
||||
};
|
||||
self.query_dkg_contract(request).await
|
||||
}
|
||||
|
||||
async fn get_epoch_dealers_addresses_paged(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
start_after: Option<String>,
|
||||
limit: Option<u32>,
|
||||
) -> Result<PagedDealerResponse, NyxdError> {
|
||||
let request = DkgQueryMsg::GetEpochDealersAddresses {
|
||||
epoch_id,
|
||||
start_after,
|
||||
limit,
|
||||
};
|
||||
self.query_dkg_contract(request).await
|
||||
}
|
||||
|
||||
async fn get_dealer_indices_paged(
|
||||
&self,
|
||||
start_after: Option<String>,
|
||||
@@ -208,6 +241,20 @@ pub trait PagedDkgQueryClient: DkgQueryClient {
|
||||
collect_paged!(self, get_current_dealers_paged, dealers)
|
||||
}
|
||||
|
||||
async fn get_all_epoch_dealers(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<Vec<DealerDetails>, NyxdError> {
|
||||
collect_paged!(self, get_epoch_dealers_paged, dealers, epoch_id)
|
||||
}
|
||||
|
||||
async fn get_all_epoch_dealers_addresses(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<Vec<DealerDetails>, NyxdError> {
|
||||
collect_paged!(self, get_epoch_dealers_addresses_paged, dealers, epoch_id)
|
||||
}
|
||||
|
||||
async fn get_all_dealer_indices(&self) -> Result<Vec<(Addr, NodeIndex)>, NyxdError> {
|
||||
collect_paged!(self, get_dealer_indices_paged, indices)
|
||||
}
|
||||
@@ -257,6 +304,9 @@ mod tests {
|
||||
match msg {
|
||||
DkgQueryMsg::GetState {} => client.get_state().ignore(),
|
||||
DkgQueryMsg::GetCurrentEpochState {} => client.get_current_epoch().ignore(),
|
||||
DkgQueryMsg::GetEpochStateAtHeight { height } => {
|
||||
client.get_epoch_at_height(height).ignore()
|
||||
}
|
||||
DkgQueryMsg::CanAdvanceState {} => client.can_advance_state().ignore(),
|
||||
DkgQueryMsg::GetCurrentEpochThreshold {} => {
|
||||
client.get_current_epoch_threshold().ignore()
|
||||
@@ -276,6 +326,20 @@ mod tests {
|
||||
DkgQueryMsg::GetCurrentDealers { limit, start_after } => client
|
||||
.get_current_dealers_paged(start_after, limit)
|
||||
.ignore(),
|
||||
QueryMsg::GetEpochDealers {
|
||||
epoch_id,
|
||||
limit,
|
||||
start_after,
|
||||
} => client
|
||||
.get_epoch_dealers_paged(epoch_id, start_after, limit)
|
||||
.ignore(),
|
||||
QueryMsg::GetEpochDealersAddresses {
|
||||
epoch_id,
|
||||
limit,
|
||||
start_after,
|
||||
} => client
|
||||
.get_epoch_dealers_addresses_paged(epoch_id, start_after, limit)
|
||||
.ignore(),
|
||||
DkgQueryMsg::GetDealerIndices { limit, start_after } => {
|
||||
client.get_dealer_indices_paged(start_after, limit).ignore()
|
||||
}
|
||||
|
||||
@@ -55,6 +55,14 @@ impl DealerDetailsResponse {
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct PagedDealerAddressesResponse {
|
||||
pub dealers: Vec<Addr>,
|
||||
|
||||
/// Field indicating paging information for the following queries if the caller wishes to get further entries.
|
||||
pub start_next_after: Option<Addr>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct PagedDealerResponse {
|
||||
pub dealers: Vec<DealerDetails>,
|
||||
|
||||
@@ -12,8 +12,8 @@ use cosmwasm_schema::cw_serde;
|
||||
#[cfg(feature = "schema")]
|
||||
use crate::{
|
||||
dealer::{
|
||||
DealerDetailsResponse, PagedDealerIndexResponse, PagedDealerResponse,
|
||||
RegisteredDealerDetails,
|
||||
DealerDetailsResponse, PagedDealerAddressesResponse, PagedDealerIndexResponse,
|
||||
PagedDealerResponse, RegisteredDealerDetails,
|
||||
},
|
||||
dealing::{
|
||||
DealerDealingsStatusResponse, DealingChunkResponse, DealingChunkStatusResponse,
|
||||
@@ -84,6 +84,9 @@ pub enum QueryMsg {
|
||||
#[cfg_attr(feature = "schema", returns(Epoch))]
|
||||
GetCurrentEpochState {},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(Option<Epoch>))]
|
||||
GetEpochStateAtHeight { height: u64 },
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(u64))]
|
||||
GetCurrentEpochThreshold {},
|
||||
|
||||
@@ -102,6 +105,20 @@ pub enum QueryMsg {
|
||||
#[cfg_attr(feature = "schema", returns(DealerDetailsResponse))]
|
||||
GetDealerDetails { dealer_address: String },
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(PagedDealerAddressesResponse))]
|
||||
GetEpochDealersAddresses {
|
||||
epoch_id: EpochId,
|
||||
limit: Option<u32>,
|
||||
start_after: Option<String>,
|
||||
},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(PagedDealerResponse))]
|
||||
GetEpochDealers {
|
||||
epoch_id: EpochId,
|
||||
limit: Option<u32>,
|
||||
start_after: Option<String>,
|
||||
},
|
||||
|
||||
#[cfg_attr(feature = "schema", returns(PagedDealerResponse))]
|
||||
GetCurrentDealers {
|
||||
limit: Option<u32>,
|
||||
|
||||
@@ -63,7 +63,7 @@ impl PersistentStorage {
|
||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||
.synchronous(SqliteSynchronous::Normal)
|
||||
.auto_vacuum(SqliteAutoVacuum::Incremental)
|
||||
.filename(&database_path)
|
||||
.filename(database_path)
|
||||
.create_if_missing(true)
|
||||
.disable_statement_logging();
|
||||
|
||||
@@ -75,8 +75,7 @@ impl PersistentStorage {
|
||||
}
|
||||
};
|
||||
|
||||
let connection_pool =
|
||||
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
|
||||
let connection_pool = SqlitePoolGuard::new(connection_pool);
|
||||
|
||||
if let Err(err) = sqlx::migrate!("./migrations").run(&*connection_pool).await {
|
||||
error!("Failed to perform migration on the SQLx database: {err}");
|
||||
|
||||
@@ -11,9 +11,11 @@ rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
bs58 = { workspace = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
cw-utils = { workspace = true }
|
||||
dyn-clone = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
si-scale = { workspace = true }
|
||||
|
||||
@@ -7,25 +7,36 @@ use crate::ClientBandwidth;
|
||||
use nym_credentials::ecash::utils::ecash_today;
|
||||
use nym_credentials_interface::Bandwidth;
|
||||
use nym_gateway_requests::ServerResponse;
|
||||
use nym_gateway_storage::GatewayStorage;
|
||||
use nym_gateway_storage::traits::BandwidthGatewayStorage;
|
||||
use si_scale::helpers::bibytes2;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::*;
|
||||
|
||||
const FREE_TESTNET_BANDWIDTH_VALUE: Bandwidth = Bandwidth::new_unchecked(64 * 1024 * 1024 * 1024); // 64GB
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct BandwidthStorageManager {
|
||||
pub(crate) storage: GatewayStorage,
|
||||
pub(crate) storage: Box<dyn BandwidthGatewayStorage + Send + Sync>,
|
||||
pub(crate) client_bandwidth: ClientBandwidth,
|
||||
pub(crate) client_id: i64,
|
||||
pub(crate) bandwidth_cfg: BandwidthFlushingBehaviourConfig,
|
||||
pub(crate) only_coconut_credentials: bool,
|
||||
}
|
||||
|
||||
impl Clone for BandwidthStorageManager {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
storage: dyn_clone::clone_box(&*self.storage),
|
||||
client_bandwidth: self.client_bandwidth.clone(),
|
||||
client_id: self.client_id,
|
||||
bandwidth_cfg: self.bandwidth_cfg,
|
||||
only_coconut_credentials: self.only_coconut_credentials,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BandwidthStorageManager {
|
||||
pub fn new(
|
||||
storage: GatewayStorage,
|
||||
storage: Box<dyn BandwidthGatewayStorage + Send + Sync>,
|
||||
client_bandwidth: ClientBandwidth,
|
||||
client_id: i64,
|
||||
bandwidth_cfg: BandwidthFlushingBehaviourConfig,
|
||||
|
||||
@@ -2,12 +2,14 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::Error;
|
||||
use async_trait::async_trait;
|
||||
use credential_sender::CredentialHandler;
|
||||
use credential_sender::CredentialHandlerConfig;
|
||||
use error::EcashTicketError;
|
||||
use futures::channel::mpsc::{self, UnboundedSender};
|
||||
use nym_credentials::CredentialSpendingData;
|
||||
use nym_credentials_interface::{ClientTicket, CompactEcashError, NymPayInfo, VerificationKeyAuth};
|
||||
use nym_gateway_storage::traits::BandwidthGatewayStorage;
|
||||
use nym_gateway_storage::GatewayStorage;
|
||||
use nym_validator_client::nym_api::EpochId;
|
||||
use nym_validator_client::DirectSigningHttpRpcNyxdClient;
|
||||
@@ -20,6 +22,7 @@ pub mod credential_sender;
|
||||
pub mod error;
|
||||
mod helpers;
|
||||
mod state;
|
||||
pub mod traits;
|
||||
|
||||
pub const TIME_RANGE_SEC: i64 = 30;
|
||||
|
||||
@@ -31,44 +34,21 @@ pub struct EcashManager {
|
||||
cred_sender: UnboundedSender<ClientTicket>,
|
||||
}
|
||||
|
||||
impl EcashManager {
|
||||
pub async fn new(
|
||||
credential_handler_cfg: CredentialHandlerConfig,
|
||||
nyxd_client: DirectSigningHttpRpcNyxdClient,
|
||||
pk_bytes: [u8; 32],
|
||||
shutdown: nym_task::TaskClient,
|
||||
storage: GatewayStorage,
|
||||
) -> Result<Self, Error> {
|
||||
let shared_state = SharedState::new(nyxd_client, storage).await?;
|
||||
|
||||
let (cred_sender, cred_receiver) = mpsc::unbounded();
|
||||
|
||||
let cs =
|
||||
CredentialHandler::new(credential_handler_cfg, cred_receiver, shared_state.clone())
|
||||
.await?;
|
||||
cs.start(shutdown);
|
||||
|
||||
Ok(EcashManager {
|
||||
shared_state,
|
||||
pk_bytes,
|
||||
pay_infos: Default::default(),
|
||||
cred_sender,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn verification_key(
|
||||
#[async_trait]
|
||||
impl traits::EcashManager for EcashManager {
|
||||
async fn verification_key(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError> {
|
||||
self.shared_state.verification_key(epoch_id).await
|
||||
}
|
||||
|
||||
pub fn storage(&self) -> &GatewayStorage {
|
||||
&self.shared_state.storage
|
||||
fn storage(&self) -> Box<dyn BandwidthGatewayStorage + Send + Sync> {
|
||||
dyn_clone::clone_box(&*self.shared_state.storage)
|
||||
}
|
||||
|
||||
//Check for duplicate pay_info, then check the payment, then insert pay_info if everything succeeded
|
||||
pub async fn check_payment(
|
||||
async fn check_payment(
|
||||
&self,
|
||||
credential: &CredentialSpendingData,
|
||||
aggregated_verification_key: &VerificationKeyAuth,
|
||||
@@ -88,6 +68,40 @@ impl EcashManager {
|
||||
.await
|
||||
}
|
||||
|
||||
fn async_verify(&self, ticket: ClientTicket) {
|
||||
// TODO: I guess do something for shutdowns
|
||||
let _ = self
|
||||
.cred_sender
|
||||
.unbounded_send(ticket)
|
||||
.inspect_err(|_| error!("failed to send the client ticket for verification task"));
|
||||
}
|
||||
}
|
||||
|
||||
impl EcashManager {
|
||||
pub async fn new(
|
||||
credential_handler_cfg: CredentialHandlerConfig,
|
||||
nyxd_client: DirectSigningHttpRpcNyxdClient,
|
||||
pk_bytes: [u8; 32],
|
||||
shutdown: nym_task::TaskClient,
|
||||
storage: GatewayStorage,
|
||||
) -> Result<Self, Error> {
|
||||
let shared_state = SharedState::new(nyxd_client, Box::new(storage)).await?;
|
||||
|
||||
let (cred_sender, cred_receiver) = mpsc::unbounded();
|
||||
|
||||
let cs =
|
||||
CredentialHandler::new(credential_handler_cfg, cred_receiver, shared_state.clone())
|
||||
.await?;
|
||||
cs.start(shutdown);
|
||||
|
||||
Ok(EcashManager {
|
||||
shared_state,
|
||||
pk_bytes,
|
||||
pay_infos: Default::default(),
|
||||
cred_sender,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn verify_pay_info(&self, pay_info: NymPayInfo) -> Result<usize, EcashTicketError> {
|
||||
//Public key check
|
||||
if pay_info.pk() != self.pk_bytes {
|
||||
@@ -152,12 +166,86 @@ impl EcashManager {
|
||||
inner.insert(index, pay_info);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn async_verify(&self, ticket: ClientTicket) {
|
||||
// TODO: I guess do something for shutdowns
|
||||
let _ = self
|
||||
.cred_sender
|
||||
.unbounded_send(ticket)
|
||||
.inspect_err(|_| error!("failed to send the client ticket for verification task"));
|
||||
pub struct MockEcashManager {
|
||||
verfication_key: tokio::sync::RwLock<VerificationKeyAuth>,
|
||||
storage: Box<dyn BandwidthGatewayStorage + Send + Sync>,
|
||||
}
|
||||
|
||||
impl MockEcashManager {
|
||||
pub fn new(storage: Box<dyn BandwidthGatewayStorage + Send + Sync>) -> Self {
|
||||
Self {
|
||||
verfication_key: tokio::sync::RwLock::new(
|
||||
VerificationKeyAuth::from_bytes(&[
|
||||
129, 187, 76, 12, 1, 51, 46, 26, 132, 205, 148, 109, 140, 131, 50, 119, 45,
|
||||
128, 51, 218, 106, 70, 181, 74, 244, 38, 162, 62, 42, 12, 5, 100, 7, 136, 32,
|
||||
155, 18, 219, 195, 182, 3, 56, 168, 16, 93, 154, 249, 230, 16, 202, 90, 134,
|
||||
246, 25, 98, 6, 175, 215, 188, 239, 71, 84, 66, 1, 43, 66, 197, 180, 216, 80,
|
||||
55, 185, 140, 216, 14, 48, 244, 214, 20, 68, 106, 41, 48, 252, 188, 181, 231,
|
||||
170, 23, 211, 215, 12, 91, 147, 47, 7, 4, 0, 0, 0, 0, 0, 0, 0, 174, 31, 237,
|
||||
215, 159, 183, 71, 125, 90, 147, 84, 78, 49, 216, 66, 232, 92, 206, 41, 230,
|
||||
239, 209, 211, 166, 131, 190, 148, 36, 225, 194, 146, 6, 120, 34, 194, 5, 154,
|
||||
155, 234, 41, 191, 119, 227, 51, 91, 128, 151, 240, 129, 208, 253, 171, 234,
|
||||
170, 71, 139, 251, 78, 49, 35, 218, 16, 77, 150, 177, 204, 83, 210, 67, 147,
|
||||
66, 162, 58, 25, 96, 168, 61, 180, 92, 21, 18, 78, 194, 98, 176, 123, 122, 176,
|
||||
81, 150, 187, 20, 64, 69, 0, 134, 142, 3, 84, 108, 3, 55, 107, 111, 73, 31, 46,
|
||||
51, 225, 248, 202, 173, 194, 24, 104, 96, 31, 61, 24, 140, 220, 31, 176, 200,
|
||||
30, 217, 66, 58, 11, 181, 158, 196, 179, 199, 177, 7, 210, 4, 119, 142, 149,
|
||||
59, 3, 186, 145, 27, 230, 125, 230, 246, 197, 196, 119, 70, 239, 115, 99, 215,
|
||||
63, 205, 63, 74, 108, 201, 42, 226, 150, 137, 3, 157, 45, 25, 163, 54, 107,
|
||||
153, 61, 141, 64, 207, 139, 41, 203, 39, 36, 97, 181, 72, 206, 235, 221, 178,
|
||||
171, 60, 4, 6, 170, 181, 213, 10, 216, 53, 28, 32, 33, 41, 224, 60, 247, 206,
|
||||
137, 108, 251, 229, 234, 112, 65, 145, 124, 212, 125, 116, 154, 114, 2, 125,
|
||||
202, 24, 25, 196, 219, 104, 200, 131, 133, 180, 39, 21, 144, 204, 8, 151, 218,
|
||||
99, 64, 209, 47, 5, 42, 13, 214, 139, 54, 112, 224, 53, 238, 250, 56, 42, 105,
|
||||
15, 21, 238, 99, 225, 79, 121, 104, 155, 230, 243, 133, 47, 39, 147, 98, 45,
|
||||
113, 137, 200, 102, 151, 122, 174, 9, 250, 17, 138, 191, 129, 202, 244, 107,
|
||||
75, 48, 141, 136, 89, 168, 124, 88, 174, 251, 17, 35, 146, 88, 76, 134, 102,
|
||||
105, 204, 16, 176, 214, 63, 13, 170, 225, 250, 112, 7, 237, 161, 160, 15, 71,
|
||||
10, 130, 137, 69, 186, 64, 223, 188, 5, 5, 228, 57, 214, 134, 247, 20, 171,
|
||||
140, 43, 230, 57, 29, 127, 136, 169, 80, 14, 137, 130, 200, 205, 222, 81, 143,
|
||||
40, 77, 68, 197, 91, 142, 91, 84, 164, 15, 133, 242, 149, 255, 173, 201, 108,
|
||||
208, 23, 188, 230, 158, 146, 54, 198, 52, 148, 123, 202, 52, 222, 50, 4, 62,
|
||||
211, 208, 176, 61, 104, 151, 227, 192, 224, 200, 132, 53, 187, 240, 254, 150,
|
||||
60, 30, 140, 11, 63, 71, 12, 30, 233, 255, 144, 250, 16, 81, 38, 33, 9, 185,
|
||||
195, 214, 0, 119, 117, 94, 100, 103, 144, 10, 189, 65, 113, 114, 192, 11, 177,
|
||||
214, 223, 218, 36, 139, 183, 2, 206, 247, 245, 88, 62, 231, 183, 50, 46, 95,
|
||||
202, 152, 82, 244, 80, 173, 192, 147, 51, 248, 46, 181, 194, 205, 233, 67, 144,
|
||||
155, 250, 142, 124, 71, 9, 136, 142, 88, 29, 99, 222, 43, 181, 172, 120, 187,
|
||||
179, 172, 240, 231, 57, 236, 195, 158, 182, 203, 19, 49, 220, 180, 212, 101,
|
||||
105, 239, 58, 215, 0, 50, 100, 172, 29, 236, 170, 108, 129, 150, 5, 64, 238,
|
||||
59, 50, 4, 21, 131, 197, 142, 191, 76, 101, 140, 133, 112, 38, 235, 113, 203,
|
||||
22, 161, 204, 84, 73, 125, 219, 70, 62, 67, 119, 52, 130, 208, 180, 231, 78,
|
||||
141, 181, 13, 207, 196, 126, 159, 70, 34, 195, 70,
|
||||
])
|
||||
.unwrap(),
|
||||
),
|
||||
storage: dyn_clone::clone_box(&*storage),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl traits::EcashManager for MockEcashManager {
|
||||
async fn verification_key(
|
||||
&self,
|
||||
_epoch_id: EpochId,
|
||||
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError> {
|
||||
Ok(self.verfication_key.read().await)
|
||||
}
|
||||
|
||||
fn storage(&self) -> Box<dyn BandwidthGatewayStorage + Send + Sync> {
|
||||
dyn_clone::clone_box(&*self.storage)
|
||||
}
|
||||
|
||||
async fn check_payment(
|
||||
&self,
|
||||
_credential: &CredentialSpendingData,
|
||||
_aggregated_verification_key: &VerificationKeyAuth,
|
||||
) -> Result<(), EcashTicketError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn async_verify(&self, _ticket: ClientTicket) {}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::Error;
|
||||
use cosmwasm_std::{from_json, CosmosMsg, WasmMsg};
|
||||
use nym_credentials_interface::VerificationKeyAuth;
|
||||
use nym_ecash_contract_common::msg::ExecuteMsg;
|
||||
use nym_gateway_storage::GatewayStorage;
|
||||
use nym_gateway_storage::traits::BandwidthGatewayStorage;
|
||||
use nym_validator_client::coconut::all_ecash_api_clients;
|
||||
use nym_validator_client::nym_api::EpochId;
|
||||
use nym_validator_client::nyxd::contract_traits::{
|
||||
@@ -22,18 +22,28 @@ use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard};
|
||||
use tracing::{error, trace, warn};
|
||||
|
||||
// state shared by different subtasks dealing with credentials
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct SharedState {
|
||||
pub(crate) nyxd_client: Arc<RwLock<DirectSigningHttpRpcNyxdClient>>,
|
||||
pub(crate) address: AccountId,
|
||||
pub(crate) epoch_data: Arc<RwLock<BTreeMap<EpochId, EpochState>>>,
|
||||
pub(crate) storage: GatewayStorage,
|
||||
pub(crate) storage: Box<dyn BandwidthGatewayStorage + Send + Sync>,
|
||||
}
|
||||
|
||||
impl Clone for SharedState {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
nyxd_client: self.nyxd_client.clone(),
|
||||
address: self.address.clone(),
|
||||
epoch_data: self.epoch_data.clone(),
|
||||
storage: dyn_clone::clone_box(&*self.storage),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SharedState {
|
||||
pub(crate) async fn new(
|
||||
nyxd_client: DirectSigningHttpRpcNyxdClient,
|
||||
storage: GatewayStorage,
|
||||
storage: Box<dyn BandwidthGatewayStorage + Send + Sync>,
|
||||
) -> Result<Self, Error> {
|
||||
let address = nyxd_client.address();
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
use async_trait::async_trait;
|
||||
use nym_credentials::CredentialSpendingData;
|
||||
use nym_credentials_interface::{ClientTicket, VerificationKeyAuth};
|
||||
use nym_gateway_storage::traits::BandwidthGatewayStorage;
|
||||
use nym_validator_client::nym_api::EpochId;
|
||||
use tokio::sync::RwLockReadGuard;
|
||||
|
||||
use crate::ecash::error::EcashTicketError;
|
||||
|
||||
#[async_trait]
|
||||
pub trait EcashManager {
|
||||
async fn verification_key(
|
||||
&self,
|
||||
epoch_id: EpochId,
|
||||
) -> Result<RwLockReadGuard<VerificationKeyAuth>, EcashTicketError>;
|
||||
fn storage(&self) -> Box<dyn BandwidthGatewayStorage + Send + Sync>;
|
||||
async fn check_payment(
|
||||
&self,
|
||||
credential: &CredentialSpendingData,
|
||||
aggregated_verification_key: &VerificationKeyAuth,
|
||||
) -> Result<(), EcashTicketError>;
|
||||
fn async_verify(&self, ticket: ClientTicket);
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::ecash::traits::EcashManager;
|
||||
use bandwidth_storage_manager::BandwidthStorageManager;
|
||||
use ecash::EcashManager;
|
||||
use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime};
|
||||
use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType};
|
||||
use nym_gateway_requests::models::CredentialSpendingRequest;
|
||||
@@ -20,14 +20,14 @@ pub mod error;
|
||||
|
||||
pub struct CredentialVerifier {
|
||||
credential: CredentialSpendingRequest,
|
||||
ecash_verifier: Arc<EcashManager>,
|
||||
ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
|
||||
bandwidth_storage_manager: BandwidthStorageManager,
|
||||
}
|
||||
|
||||
impl CredentialVerifier {
|
||||
pub fn new(
|
||||
credential: CredentialSpendingRequest,
|
||||
ecash_verifier: Arc<EcashManager>,
|
||||
ecash_verifier: Arc<dyn EcashManager + Send + Sync>,
|
||||
bandwidth_storage_manager: BandwidthStorageManager,
|
||||
) -> Self {
|
||||
CredentialVerifier {
|
||||
|
||||
@@ -112,6 +112,10 @@ impl ServerResponse {
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_send(&self) -> bool {
|
||||
matches!(self, ServerResponse::Send { .. })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ServerResponse> for Message {
|
||||
|
||||
@@ -9,8 +9,10 @@ edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
bincode = { workspace = true }
|
||||
defguard_wireguard_rs = { workspace = true }
|
||||
dyn-clone = { workspace = true }
|
||||
sqlx = { workspace = true, features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
@@ -21,6 +23,7 @@ sqlx = { workspace = true, features = [
|
||||
] }
|
||||
time = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["sync"], optional = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
nym-credentials-interface = { path = "../credentials-interface" }
|
||||
@@ -35,3 +38,7 @@ sqlx = { workspace = true, features = [
|
||||
"macros",
|
||||
"migrate",
|
||||
] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
mock = ["tokio"]
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
DELETE FROM wireguard_peer WHERE client_id IS NULL;
|
||||
|
||||
CREATE TABLE wireguard_peer_new
|
||||
(
|
||||
public_key TEXT NOT NULL PRIMARY KEY UNIQUE,
|
||||
allowed_ips BLOB NOT NULL,
|
||||
client_id INTEGER REFERENCES clients(id) NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO wireguard_peer_new (public_key, allowed_ips, client_id)
|
||||
SELECT public_key, allowed_ips, client_id FROM wireguard_peer;
|
||||
|
||||
DROP TABLE wireguard_peer;
|
||||
ALTER TABLE wireguard_peer_new RENAME TO wireguard_peer;
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
use std::str::FromStr;
|
||||
|
||||
use nym_credentials_interface::TicketType;
|
||||
|
||||
use crate::models::Client;
|
||||
|
||||
#[derive(Debug, PartialEq, sqlx::Type)]
|
||||
@@ -15,6 +17,17 @@ pub enum ClientType {
|
||||
ExitWireguard,
|
||||
}
|
||||
|
||||
impl From<TicketType> for ClientType {
|
||||
fn from(value: TicketType) -> Self {
|
||||
match value {
|
||||
TicketType::V1MixnetEntry => ClientType::EntryMixnet,
|
||||
TicketType::V1MixnetExit => ClientType::ExitMixnet,
|
||||
TicketType::V1WireguardEntry => ClientType::EntryWireguard,
|
||||
TicketType::V1WireguardExit => ClientType::ExitWireguard,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ClientType {
|
||||
type Err = &'static str;
|
||||
|
||||
|
||||
@@ -20,6 +20,18 @@ pub enum GatewayStorageError {
|
||||
#[error("the stored data associated with ticket {ticket_id} is malformed!")]
|
||||
MalformedStoredTicketData { ticket_id: i64 },
|
||||
|
||||
#[error("Failed to convert from type of database: {0}")]
|
||||
TypeConversion(String),
|
||||
#[error("Failed to convert from type of database: {field_key}")]
|
||||
TypeConversion { field_key: &'static str },
|
||||
|
||||
#[error("Serialization failure for {field_key}")]
|
||||
Serialize {
|
||||
field_key: &'static str,
|
||||
source: bincode::Error,
|
||||
},
|
||||
|
||||
#[error("Deserialization failure for {field_key}")]
|
||||
Deserialize {
|
||||
field_key: &'static str,
|
||||
source: bincode::Error,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use async_trait::async_trait;
|
||||
use bandwidth::BandwidthManager;
|
||||
use clients::{ClientManager, ClientType};
|
||||
use models::{
|
||||
@@ -15,10 +16,10 @@ use sqlx::{
|
||||
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
|
||||
ConnectOptions,
|
||||
};
|
||||
use std::path::Path;
|
||||
use std::{path::Path, time::Duration};
|
||||
use tickets::TicketStorageManager;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{debug, error};
|
||||
use tracing::{debug, error, log::LevelFilter};
|
||||
|
||||
pub mod bandwidth;
|
||||
mod clients;
|
||||
@@ -27,11 +28,21 @@ mod inboxes;
|
||||
pub mod models;
|
||||
mod shared_keys;
|
||||
mod tickets;
|
||||
pub mod traits;
|
||||
mod wireguard_peers;
|
||||
|
||||
pub use error::GatewayStorageError;
|
||||
pub use inboxes::InboxManager;
|
||||
|
||||
use crate::traits::{BandwidthGatewayStorage, InboxGatewayStorage, SharedKeyGatewayStorage};
|
||||
|
||||
fn make_bincode_serializer() -> impl bincode::Options {
|
||||
use bincode::Options;
|
||||
bincode::DefaultOptions::new()
|
||||
.with_big_endian()
|
||||
.with_varint_encoding()
|
||||
}
|
||||
|
||||
// note that clone here is fine as upon cloning the same underlying pool will be used
|
||||
#[derive(Clone)]
|
||||
pub struct GatewayStorage {
|
||||
@@ -71,6 +82,21 @@ impl GatewayStorage {
|
||||
&self.wireguard_peer_manager
|
||||
}
|
||||
|
||||
pub async fn handle_forget_me(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
let client_id = self.get_mixnet_client_id(client_address).await?;
|
||||
self.inbox_manager()
|
||||
.remove_messages_for_client(&client_address.as_base58_string())
|
||||
.await?;
|
||||
self.bandwidth_manager().remove_client(client_id).await?;
|
||||
self.shared_key_manager()
|
||||
.remove_shared_keys(&client_address.as_base58_string())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Initialises `PersistentStorage` using the provided path.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -92,6 +118,7 @@ impl GatewayStorage {
|
||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||
.synchronous(SqliteSynchronous::Normal)
|
||||
.auto_vacuum(SqliteAutoVacuum::Incremental)
|
||||
.log_slow_statements(LevelFilter::Warn, Duration::from_millis(250))
|
||||
.filename(database_path)
|
||||
.create_if_missing(true)
|
||||
.disable_statement_logging();
|
||||
@@ -123,8 +150,9 @@ impl GatewayStorage {
|
||||
}
|
||||
}
|
||||
|
||||
impl GatewayStorage {
|
||||
pub async fn get_mixnet_client_id(
|
||||
#[async_trait]
|
||||
impl SharedKeyGatewayStorage for GatewayStorage {
|
||||
async fn get_mixnet_client_id(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
) -> Result<i64, GatewayStorageError> {
|
||||
@@ -134,22 +162,7 @@ impl GatewayStorage {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn handle_forget_me(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
let client_id = self.get_mixnet_client_id(client_address).await?;
|
||||
self.inbox_manager()
|
||||
.remove_messages_for_client(&client_address.as_base58_string())
|
||||
.await?;
|
||||
self.bandwidth_manager().remove_client(client_id).await?;
|
||||
self.shared_key_manager()
|
||||
.remove_shared_keys(&client_address.as_base58_string())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn insert_shared_keys(
|
||||
async fn insert_shared_keys(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
shared_keys: &SharedGatewayKey,
|
||||
@@ -178,7 +191,7 @@ impl GatewayStorage {
|
||||
Ok(client_id)
|
||||
}
|
||||
|
||||
pub async fn get_shared_keys(
|
||||
async fn get_shared_keys(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
) -> Result<Option<PersistedSharedKeys>, GatewayStorageError> {
|
||||
@@ -190,7 +203,7 @@ impl GatewayStorage {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn remove_shared_keys(
|
||||
async fn remove_shared_keys(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
@@ -200,7 +213,7 @@ impl GatewayStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_last_used_authentication_timestamp(
|
||||
async fn update_last_used_authentication_timestamp(
|
||||
&self,
|
||||
client_id: i64,
|
||||
last_used_authentication_timestamp: OffsetDateTime,
|
||||
@@ -214,12 +227,15 @@ impl GatewayStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_client(&self, client_id: i64) -> Result<Option<Client>, GatewayStorageError> {
|
||||
async fn get_client(&self, client_id: i64) -> Result<Option<Client>, GatewayStorageError> {
|
||||
let client = self.client_manager.get_client(client_id).await?;
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn store_message(
|
||||
#[async_trait]
|
||||
impl InboxGatewayStorage for GatewayStorage {
|
||||
async fn store_message(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
message: Vec<u8>,
|
||||
@@ -230,7 +246,7 @@ impl GatewayStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn retrieve_messages(
|
||||
async fn retrieve_messages(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
start_after: Option<i64>,
|
||||
@@ -242,19 +258,22 @@ impl GatewayStorage {
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
pub async fn remove_messages(&self, ids: Vec<i64>) -> Result<(), GatewayStorageError> {
|
||||
async fn remove_messages(&self, ids: Vec<i64>) -> Result<(), GatewayStorageError> {
|
||||
for id in ids {
|
||||
self.inbox_manager.remove_message(id).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), GatewayStorageError> {
|
||||
#[async_trait]
|
||||
impl BandwidthGatewayStorage for GatewayStorage {
|
||||
async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), GatewayStorageError> {
|
||||
self.bandwidth_manager.insert_new_client(client_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn set_expiration(
|
||||
async fn set_expiration(
|
||||
&self,
|
||||
client_id: i64,
|
||||
expiration: OffsetDateTime,
|
||||
@@ -265,12 +284,12 @@ impl GatewayStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn reset_bandwidth(&self, client_id: i64) -> Result<(), GatewayStorageError> {
|
||||
async fn reset_bandwidth(&self, client_id: i64) -> Result<(), GatewayStorageError> {
|
||||
self.bandwidth_manager.reset_bandwidth(client_id).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_available_bandwidth(
|
||||
async fn get_available_bandwidth(
|
||||
&self,
|
||||
client_id: i64,
|
||||
) -> Result<Option<PersistedBandwidth>, GatewayStorageError> {
|
||||
@@ -280,7 +299,7 @@ impl GatewayStorage {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn increase_bandwidth(
|
||||
async fn increase_bandwidth(
|
||||
&self,
|
||||
client_id: i64,
|
||||
amount: i64,
|
||||
@@ -291,7 +310,7 @@ impl GatewayStorage {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn revoke_ticket_bandwidth(
|
||||
async fn revoke_ticket_bandwidth(
|
||||
&self,
|
||||
ticket_id: i64,
|
||||
amount: i64,
|
||||
@@ -302,7 +321,7 @@ impl GatewayStorage {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn decrease_bandwidth(
|
||||
async fn decrease_bandwidth(
|
||||
&self,
|
||||
client_id: i64,
|
||||
amount: i64,
|
||||
@@ -313,7 +332,7 @@ impl GatewayStorage {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn insert_epoch_signers(
|
||||
async fn insert_epoch_signers(
|
||||
&self,
|
||||
epoch_id: i64,
|
||||
signer_ids: Vec<i64>,
|
||||
@@ -324,7 +343,7 @@ impl GatewayStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn insert_received_ticket(
|
||||
async fn insert_received_ticket(
|
||||
&self,
|
||||
client_id: i64,
|
||||
received_at: OffsetDateTime,
|
||||
@@ -344,11 +363,11 @@ impl GatewayStorage {
|
||||
Ok(ticket_id)
|
||||
}
|
||||
|
||||
pub async fn contains_ticket(&self, serial_number: &[u8]) -> Result<bool, GatewayStorageError> {
|
||||
async fn contains_ticket(&self, serial_number: &[u8]) -> Result<bool, GatewayStorageError> {
|
||||
Ok(self.ticket_manager.has_ticket_data(serial_number).await?)
|
||||
}
|
||||
|
||||
pub async fn insert_ticket_verification(
|
||||
async fn insert_ticket_verification(
|
||||
&self,
|
||||
ticket_id: i64,
|
||||
signer_id: i64,
|
||||
@@ -361,7 +380,7 @@ impl GatewayStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> {
|
||||
async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> {
|
||||
// set the ticket as rejected
|
||||
self.ticket_manager.set_rejected_ticket(ticket_id).await?;
|
||||
|
||||
@@ -372,7 +391,7 @@ impl GatewayStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> {
|
||||
async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> {
|
||||
// 1. insert into verified table
|
||||
self.ticket_manager
|
||||
.insert_verified_ticket(ticket_id)
|
||||
@@ -386,7 +405,7 @@ impl GatewayStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn remove_verified_ticket_binary_data(
|
||||
async fn remove_verified_ticket_binary_data(
|
||||
&self,
|
||||
ticket_id: i64,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
@@ -396,7 +415,7 @@ impl GatewayStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_all_verified_tickets_with_sn(
|
||||
async fn get_all_verified_tickets_with_sn(
|
||||
&self,
|
||||
) -> Result<Vec<VerifiedTicket>, GatewayStorageError> {
|
||||
Ok(self
|
||||
@@ -405,7 +424,7 @@ impl GatewayStorage {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn get_all_proposed_tickets_with_sn(
|
||||
async fn get_all_proposed_tickets_with_sn(
|
||||
&self,
|
||||
proposal_id: u32,
|
||||
) -> Result<Vec<VerifiedTicket>, GatewayStorageError> {
|
||||
@@ -415,7 +434,7 @@ impl GatewayStorage {
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn insert_redemption_proposal(
|
||||
async fn insert_redemption_proposal(
|
||||
&self,
|
||||
tickets: &[VerifiedTicket],
|
||||
proposal_id: u32,
|
||||
@@ -438,7 +457,7 @@ impl GatewayStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn clear_post_proposal_data(
|
||||
async fn clear_post_proposal_data(
|
||||
&self,
|
||||
proposal_id: u32,
|
||||
resolved_at: OffsetDateTime,
|
||||
@@ -462,13 +481,11 @@ impl GatewayStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn latest_proposal(&self) -> Result<Option<RedemptionProposal>, GatewayStorageError> {
|
||||
async fn latest_proposal(&self) -> Result<Option<RedemptionProposal>, GatewayStorageError> {
|
||||
Ok(self.ticket_manager.get_latest_redemption_proposal().await?)
|
||||
}
|
||||
|
||||
pub async fn get_all_unverified_tickets(
|
||||
&self,
|
||||
) -> Result<Vec<ClientTicket>, GatewayStorageError> {
|
||||
async fn get_all_unverified_tickets(&self) -> Result<Vec<ClientTicket>, GatewayStorageError> {
|
||||
self.ticket_manager
|
||||
.get_unverified_tickets()
|
||||
.await?
|
||||
@@ -477,21 +494,21 @@ impl GatewayStorage {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn get_all_unresolved_proposals(&self) -> Result<Vec<i64>, GatewayStorageError> {
|
||||
async fn get_all_unresolved_proposals(&self) -> Result<Vec<i64>, GatewayStorageError> {
|
||||
Ok(self
|
||||
.ticket_manager
|
||||
.get_all_unresolved_redemption_proposal_ids()
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn get_votes(&self, ticket_id: i64) -> Result<Vec<i64>, GatewayStorageError> {
|
||||
async fn get_votes(&self, ticket_id: i64) -> Result<Vec<i64>, GatewayStorageError> {
|
||||
Ok(self
|
||||
.ticket_manager
|
||||
.get_verification_votes(ticket_id)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn get_signers(&self, epoch_id: i64) -> Result<Vec<i64>, GatewayStorageError> {
|
||||
async fn get_signers(&self, epoch_id: i64) -> Result<Vec<i64>, GatewayStorageError> {
|
||||
Ok(self.ticket_manager.get_epoch_signers(epoch_id).await?)
|
||||
}
|
||||
|
||||
@@ -500,34 +517,20 @@ impl GatewayStorage {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `peer`: wireguard peer data to be stored
|
||||
/// * `with_client_id`: if the peer should have a corresponding client_id
|
||||
/// (created with entry wireguard ticket) or live without one (or with an
|
||||
/// exiting one), for temporary backwards compatibility.
|
||||
pub async fn insert_wireguard_peer(
|
||||
async fn insert_wireguard_peer(
|
||||
&self,
|
||||
peer: &defguard_wireguard_rs::host::Peer,
|
||||
with_client_id: bool,
|
||||
) -> Result<Option<i64>, GatewayStorageError> {
|
||||
client_type: ClientType,
|
||||
) -> Result<i64, GatewayStorageError> {
|
||||
let client_id = match self
|
||||
.wireguard_peer_manager
|
||||
.retrieve_peer(&peer.public_key.to_string())
|
||||
.await?
|
||||
{
|
||||
Some(peer) => peer.client_id,
|
||||
_ => {
|
||||
if with_client_id {
|
||||
Some(
|
||||
self.client_manager
|
||||
.insert_client(ClientType::EntryWireguard)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
None => self.client_manager.insert_client(client_type).await?,
|
||||
};
|
||||
let mut peer = WireguardPeer::from(peer.clone());
|
||||
peer.client_id = client_id;
|
||||
let peer = WireguardPeer::from_defguard_peer(peer.clone(), client_id)?;
|
||||
self.wireguard_peer_manager.insert_peer(&peer).await?;
|
||||
Ok(client_id)
|
||||
}
|
||||
@@ -537,7 +540,7 @@ impl GatewayStorage {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `peer_public_key`: wireguard public key of the peer to be retrieved.
|
||||
pub async fn get_wireguard_peer(
|
||||
async fn get_wireguard_peer(
|
||||
&self,
|
||||
peer_public_key: &str,
|
||||
) -> Result<Option<WireguardPeer>, GatewayStorageError> {
|
||||
@@ -549,7 +552,7 @@ impl GatewayStorage {
|
||||
}
|
||||
|
||||
/// Retrieves all wireguard peers.
|
||||
pub async fn get_all_wireguard_peers(&self) -> Result<Vec<WireguardPeer>, GatewayStorageError> {
|
||||
async fn get_all_wireguard_peers(&self) -> Result<Vec<WireguardPeer>, GatewayStorageError> {
|
||||
let ret = self.wireguard_peer_manager.retrieve_all_peers().await?;
|
||||
Ok(ret)
|
||||
}
|
||||
@@ -559,7 +562,7 @@ impl GatewayStorage {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `peer_public_key`: wireguard public key of the peer to be removed.
|
||||
pub async fn remove_wireguard_peer(
|
||||
async fn remove_wireguard_peer(
|
||||
&self,
|
||||
peer_public_key: &str,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::time::SystemTime;
|
||||
|
||||
use crate::error::GatewayStorageError;
|
||||
use crate::{error::GatewayStorageError, make_bincode_serializer};
|
||||
use nym_credentials_interface::{AvailableBandwidth, ClientTicket, CredentialSpendingData};
|
||||
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
|
||||
use sqlx::FromRow;
|
||||
@@ -112,35 +110,24 @@ impl TryFrom<UnverifiedTicketData> for ClientTicket {
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct WireguardPeer {
|
||||
pub public_key: String,
|
||||
pub preshared_key: Option<String>,
|
||||
pub protocol_version: Option<i64>,
|
||||
pub endpoint: Option<String>,
|
||||
pub last_handshake: Option<OffsetDateTime>,
|
||||
pub tx_bytes: i64,
|
||||
pub rx_bytes: i64,
|
||||
pub persistent_keepalive_interval: Option<i64>,
|
||||
pub allowed_ips: Vec<u8>,
|
||||
pub client_id: Option<i64>,
|
||||
pub client_id: i64,
|
||||
}
|
||||
|
||||
impl From<defguard_wireguard_rs::host::Peer> for WireguardPeer {
|
||||
fn from(value: defguard_wireguard_rs::host::Peer) -> Self {
|
||||
WireguardPeer {
|
||||
impl WireguardPeer {
|
||||
pub fn from_defguard_peer(
|
||||
value: defguard_wireguard_rs::host::Peer,
|
||||
client_id: i64,
|
||||
) -> Result<Self, crate::error::GatewayStorageError> {
|
||||
Ok(WireguardPeer {
|
||||
public_key: value.public_key.to_string(),
|
||||
preshared_key: value.preshared_key.as_ref().map(|k| k.to_string()),
|
||||
protocol_version: value.protocol_version.map(|v| v as i64),
|
||||
endpoint: value.endpoint.map(|e| e.to_string()),
|
||||
last_handshake: value.last_handshake.map(OffsetDateTime::from),
|
||||
tx_bytes: value.tx_bytes as i64,
|
||||
rx_bytes: value.rx_bytes as i64,
|
||||
persistent_keepalive_interval: value.persistent_keepalive_interval.map(|v| v as i64),
|
||||
allowed_ips: bincode::Options::serialize(
|
||||
bincode::DefaultOptions::new(),
|
||||
&value.allowed_ips,
|
||||
)
|
||||
.unwrap_or_default(),
|
||||
client_id: None,
|
||||
}
|
||||
allowed_ips: bincode::Options::serialize(make_bincode_serializer(), &value.allowed_ips)
|
||||
.map_err(|source| crate::error::GatewayStorageError::Serialize {
|
||||
field_key: "allowed_ips",
|
||||
source,
|
||||
})?,
|
||||
client_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,49 +136,20 @@ impl TryFrom<WireguardPeer> for defguard_wireguard_rs::host::Peer {
|
||||
|
||||
fn try_from(value: WireguardPeer) -> Result<Self, Self::Error> {
|
||||
Ok(Self {
|
||||
public_key: value
|
||||
.public_key
|
||||
.as_str()
|
||||
.try_into()
|
||||
.map_err(|e| Self::Error::TypeConversion(format!("public key {e}")))?,
|
||||
preshared_key: value
|
||||
.preshared_key
|
||||
.as_deref()
|
||||
.map(TryFrom::try_from)
|
||||
.transpose()
|
||||
.map_err(|e| Self::Error::TypeConversion(format!("preshared key {e}")))?,
|
||||
protocol_version: value
|
||||
.protocol_version
|
||||
.map(TryFrom::try_from)
|
||||
.transpose()
|
||||
.map_err(|e| Self::Error::TypeConversion(format!("protocol version {e}")))?,
|
||||
endpoint: value
|
||||
.endpoint
|
||||
.as_deref()
|
||||
.map(|e| e.parse())
|
||||
.transpose()
|
||||
.map_err(|e| Self::Error::TypeConversion(format!("endpoint {e}")))?,
|
||||
last_handshake: value.last_handshake.map(SystemTime::from),
|
||||
tx_bytes: value
|
||||
.tx_bytes
|
||||
.try_into()
|
||||
.map_err(|e| Self::Error::TypeConversion(format!("tx bytes {e}")))?,
|
||||
rx_bytes: value
|
||||
.rx_bytes
|
||||
.try_into()
|
||||
.map_err(|e| Self::Error::TypeConversion(format!("rx bytes {e}")))?,
|
||||
persistent_keepalive_interval: value
|
||||
.persistent_keepalive_interval
|
||||
.map(TryFrom::try_from)
|
||||
.transpose()
|
||||
.map_err(|e| {
|
||||
Self::Error::TypeConversion(format!("persistent keepalive interval {e}"))
|
||||
})?,
|
||||
public_key: value.public_key.as_str().try_into().map_err(|_| {
|
||||
Self::Error::TypeConversion {
|
||||
field_key: "public_key",
|
||||
}
|
||||
})?,
|
||||
allowed_ips: bincode::Options::deserialize(
|
||||
bincode::DefaultOptions::new(),
|
||||
&value.allowed_ips,
|
||||
)
|
||||
.map_err(|e| Self::Error::TypeConversion(format!("allowed ips {e}")))?,
|
||||
.map_err(|source| Self::Error::Deserialize {
|
||||
field_key: "allowed_ips",
|
||||
source,
|
||||
})?,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,511 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use async_trait::async_trait;
|
||||
use nym_credentials_interface::ClientTicket;
|
||||
use nym_gateway_requests::SharedGatewayKey;
|
||||
use nym_sphinx::DestinationAddressBytes;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::{
|
||||
clients::ClientType,
|
||||
models::{
|
||||
Client, PersistedBandwidth, PersistedSharedKeys, RedemptionProposal, StoredMessage,
|
||||
VerifiedTicket, WireguardPeer,
|
||||
},
|
||||
GatewayStorageError,
|
||||
};
|
||||
|
||||
#[async_trait]
|
||||
pub trait SharedKeyGatewayStorage {
|
||||
async fn get_mixnet_client_id(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
) -> Result<i64, GatewayStorageError>;
|
||||
async fn insert_shared_keys(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
shared_keys: &SharedGatewayKey,
|
||||
) -> Result<i64, GatewayStorageError>;
|
||||
async fn get_shared_keys(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
) -> Result<Option<PersistedSharedKeys>, GatewayStorageError>;
|
||||
#[allow(dead_code)]
|
||||
async fn remove_shared_keys(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
) -> Result<(), GatewayStorageError>;
|
||||
async fn update_last_used_authentication_timestamp(
|
||||
&self,
|
||||
client_id: i64,
|
||||
last_used_authentication_timestamp: OffsetDateTime,
|
||||
) -> Result<(), GatewayStorageError>;
|
||||
async fn get_client(&self, client_id: i64) -> Result<Option<Client>, GatewayStorageError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait InboxGatewayStorage {
|
||||
async fn store_message(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
message: Vec<u8>,
|
||||
) -> Result<(), GatewayStorageError>;
|
||||
async fn retrieve_messages(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
start_after: Option<i64>,
|
||||
) -> Result<(Vec<StoredMessage>, Option<i64>), GatewayStorageError>;
|
||||
async fn remove_messages(&self, ids: Vec<i64>) -> Result<(), GatewayStorageError>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait BandwidthGatewayStorage: dyn_clone::DynClone {
|
||||
async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), GatewayStorageError>;
|
||||
async fn set_expiration(
|
||||
&self,
|
||||
client_id: i64,
|
||||
expiration: OffsetDateTime,
|
||||
) -> Result<(), GatewayStorageError>;
|
||||
async fn reset_bandwidth(&self, client_id: i64) -> Result<(), GatewayStorageError>;
|
||||
async fn get_available_bandwidth(
|
||||
&self,
|
||||
client_id: i64,
|
||||
) -> Result<Option<PersistedBandwidth>, GatewayStorageError>;
|
||||
async fn increase_bandwidth(
|
||||
&self,
|
||||
client_id: i64,
|
||||
amount: i64,
|
||||
) -> Result<i64, GatewayStorageError>;
|
||||
async fn revoke_ticket_bandwidth(
|
||||
&self,
|
||||
ticket_id: i64,
|
||||
amount: i64,
|
||||
) -> Result<(), GatewayStorageError>;
|
||||
async fn decrease_bandwidth(
|
||||
&self,
|
||||
client_id: i64,
|
||||
amount: i64,
|
||||
) -> Result<i64, GatewayStorageError>;
|
||||
|
||||
async fn insert_epoch_signers(
|
||||
&self,
|
||||
epoch_id: i64,
|
||||
signer_ids: Vec<i64>,
|
||||
) -> Result<(), GatewayStorageError>;
|
||||
async fn insert_received_ticket(
|
||||
&self,
|
||||
client_id: i64,
|
||||
received_at: OffsetDateTime,
|
||||
serial_number: Vec<u8>,
|
||||
data: Vec<u8>,
|
||||
) -> Result<i64, GatewayStorageError>;
|
||||
async fn contains_ticket(&self, serial_number: &[u8]) -> Result<bool, GatewayStorageError>;
|
||||
async fn insert_ticket_verification(
|
||||
&self,
|
||||
ticket_id: i64,
|
||||
signer_id: i64,
|
||||
verified_at: OffsetDateTime,
|
||||
accepted: bool,
|
||||
) -> Result<(), GatewayStorageError>;
|
||||
async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError>;
|
||||
async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError>;
|
||||
async fn remove_verified_ticket_binary_data(
|
||||
&self,
|
||||
ticket_id: i64,
|
||||
) -> Result<(), GatewayStorageError>;
|
||||
async fn get_all_verified_tickets_with_sn(
|
||||
&self,
|
||||
) -> Result<Vec<VerifiedTicket>, GatewayStorageError>;
|
||||
async fn get_all_proposed_tickets_with_sn(
|
||||
&self,
|
||||
proposal_id: u32,
|
||||
) -> Result<Vec<VerifiedTicket>, GatewayStorageError>;
|
||||
async fn insert_redemption_proposal(
|
||||
&self,
|
||||
tickets: &[VerifiedTicket],
|
||||
proposal_id: u32,
|
||||
created_at: OffsetDateTime,
|
||||
) -> Result<(), GatewayStorageError>;
|
||||
async fn clear_post_proposal_data(
|
||||
&self,
|
||||
proposal_id: u32,
|
||||
resolved_at: OffsetDateTime,
|
||||
rejected: bool,
|
||||
) -> Result<(), GatewayStorageError>;
|
||||
async fn latest_proposal(&self) -> Result<Option<RedemptionProposal>, GatewayStorageError>;
|
||||
async fn get_all_unverified_tickets(&self) -> Result<Vec<ClientTicket>, GatewayStorageError>;
|
||||
async fn get_all_unresolved_proposals(&self) -> Result<Vec<i64>, GatewayStorageError>;
|
||||
async fn get_votes(&self, ticket_id: i64) -> Result<Vec<i64>, GatewayStorageError>;
|
||||
async fn get_signers(&self, epoch_id: i64) -> Result<Vec<i64>, GatewayStorageError>;
|
||||
|
||||
/// Insert a wireguard peer in the storage.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `peer`: wireguard peer data to be stored
|
||||
async fn insert_wireguard_peer(
|
||||
&self,
|
||||
peer: &defguard_wireguard_rs::host::Peer,
|
||||
client_type: ClientType,
|
||||
) -> Result<i64, GatewayStorageError>;
|
||||
|
||||
/// Tries to retrieve a particular peer with the given public key.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `peer_public_key`: wireguard public key of the peer to be retrieved.
|
||||
async fn get_wireguard_peer(
|
||||
&self,
|
||||
peer_public_key: &str,
|
||||
) -> Result<Option<WireguardPeer>, GatewayStorageError>;
|
||||
|
||||
/// Retrieves all wireguard peers.
|
||||
async fn get_all_wireguard_peers(&self) -> Result<Vec<WireguardPeer>, GatewayStorageError>;
|
||||
|
||||
/// Remove a wireguard peer from the storage.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `peer_public_key`: wireguard public key of the peer to be removed.
|
||||
async fn remove_wireguard_peer(&self, peer_public_key: &str)
|
||||
-> Result<(), GatewayStorageError>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "mock")]
|
||||
pub mod mock {
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::*;
|
||||
|
||||
struct EcashSigner {
|
||||
_epoch_id: i64,
|
||||
_signer_id: i64,
|
||||
}
|
||||
|
||||
struct ReceivedTicket {
|
||||
client_id: i64,
|
||||
_received_at: OffsetDateTime,
|
||||
rejected: Option<bool>,
|
||||
}
|
||||
|
||||
struct TicketData {
|
||||
serial_number: Vec<u8>,
|
||||
data: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
struct TicketVerification {
|
||||
_ticket_id: i64,
|
||||
_signer_id: i64,
|
||||
_verified_at: OffsetDateTime,
|
||||
_accepted: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct MockGatewayStorage {
|
||||
available_bandwidth: HashMap<i64, PersistedBandwidth>,
|
||||
ecash_signers: Vec<EcashSigner>,
|
||||
received_ticket: HashMap<i64, ReceivedTicket>,
|
||||
ticket_data: HashMap<i64, TicketData>,
|
||||
ticket_verification: HashMap<i64, TicketVerification>,
|
||||
verified_tickets: Vec<i64>,
|
||||
wireguard_peers: HashMap<String, WireguardPeer>,
|
||||
clients: HashMap<i64, String>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl BandwidthGatewayStorage for Arc<RwLock<MockGatewayStorage>> {
|
||||
async fn create_bandwidth_entry(&self, client_id: i64) -> Result<(), GatewayStorageError> {
|
||||
self.write().await.available_bandwidth.insert(
|
||||
client_id,
|
||||
PersistedBandwidth {
|
||||
client_id,
|
||||
available: 0,
|
||||
expiration: Some(OffsetDateTime::UNIX_EPOCH),
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_expiration(
|
||||
&self,
|
||||
client_id: i64,
|
||||
expiration: OffsetDateTime,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
if let Some(bw) = self.write().await.available_bandwidth.get_mut(&client_id) {
|
||||
bw.expiration = Some(expiration);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn reset_bandwidth(&self, client_id: i64) -> Result<(), GatewayStorageError> {
|
||||
if let Some(bw) = self.write().await.available_bandwidth.get_mut(&client_id) {
|
||||
bw.available = 0;
|
||||
bw.expiration = Some(OffsetDateTime::UNIX_EPOCH);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_available_bandwidth(
|
||||
&self,
|
||||
client_id: i64,
|
||||
) -> Result<Option<PersistedBandwidth>, GatewayStorageError> {
|
||||
Ok(self
|
||||
.read()
|
||||
.await
|
||||
.available_bandwidth
|
||||
.get(&client_id)
|
||||
.cloned())
|
||||
}
|
||||
|
||||
async fn increase_bandwidth(
|
||||
&self,
|
||||
client_id: i64,
|
||||
amount: i64,
|
||||
) -> Result<i64, GatewayStorageError> {
|
||||
self.write()
|
||||
.await
|
||||
.available_bandwidth
|
||||
.get_mut(&client_id)
|
||||
.map(|bw| {
|
||||
bw.available += amount;
|
||||
bw.available
|
||||
})
|
||||
.ok_or(GatewayStorageError::InternalDatabaseError(
|
||||
sqlx::Error::RowNotFound,
|
||||
))
|
||||
}
|
||||
|
||||
async fn revoke_ticket_bandwidth(
|
||||
&self,
|
||||
ticket_id: i64,
|
||||
amount: i64,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
let mut guard = self.write().await;
|
||||
if let Some(client_id) = guard
|
||||
.received_ticket
|
||||
.get(&ticket_id)
|
||||
.map(|ticket| ticket.client_id)
|
||||
{
|
||||
if let Some(bw) = guard.available_bandwidth.get_mut(&client_id) {
|
||||
bw.available -= amount;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn decrease_bandwidth(
|
||||
&self,
|
||||
client_id: i64,
|
||||
amount: i64,
|
||||
) -> Result<i64, GatewayStorageError> {
|
||||
self.write()
|
||||
.await
|
||||
.available_bandwidth
|
||||
.get_mut(&client_id)
|
||||
.map(|bw| {
|
||||
bw.available -= amount;
|
||||
bw.available
|
||||
})
|
||||
.ok_or(GatewayStorageError::InternalDatabaseError(
|
||||
sqlx::Error::RowNotFound,
|
||||
))
|
||||
}
|
||||
|
||||
async fn insert_epoch_signers(
|
||||
&self,
|
||||
_epoch_id: i64,
|
||||
signer_ids: Vec<i64>,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
self.write()
|
||||
.await
|
||||
.ecash_signers
|
||||
.extend(signer_ids.iter().map(|signer_id| EcashSigner {
|
||||
_epoch_id,
|
||||
_signer_id: *signer_id,
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn insert_received_ticket(
|
||||
&self,
|
||||
client_id: i64,
|
||||
_received_at: OffsetDateTime,
|
||||
serial_number: Vec<u8>,
|
||||
data: Vec<u8>,
|
||||
) -> Result<i64, GatewayStorageError> {
|
||||
let mut guard = self.write().await;
|
||||
let ticket_id = guard.received_ticket.len() as i64;
|
||||
guard.received_ticket.insert(
|
||||
ticket_id,
|
||||
ReceivedTicket {
|
||||
client_id,
|
||||
_received_at,
|
||||
rejected: None,
|
||||
},
|
||||
);
|
||||
guard.ticket_data.insert(
|
||||
ticket_id,
|
||||
TicketData {
|
||||
serial_number,
|
||||
data: Some(data),
|
||||
},
|
||||
);
|
||||
Ok(ticket_id)
|
||||
}
|
||||
|
||||
async fn contains_ticket(&self, serial_number: &[u8]) -> Result<bool, GatewayStorageError> {
|
||||
Ok(self
|
||||
.read()
|
||||
.await
|
||||
.ticket_data
|
||||
.values()
|
||||
.any(|ticket_data| ticket_data.serial_number == serial_number))
|
||||
}
|
||||
|
||||
async fn insert_ticket_verification(
|
||||
&self,
|
||||
_ticket_id: i64,
|
||||
_signer_id: i64,
|
||||
_verified_at: OffsetDateTime,
|
||||
_accepted: bool,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
self.write().await.ticket_verification.insert(
|
||||
_ticket_id,
|
||||
TicketVerification {
|
||||
_ticket_id,
|
||||
_signer_id,
|
||||
_verified_at,
|
||||
_accepted,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_rejected_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> {
|
||||
let mut guard = self.write().await;
|
||||
if let Some(ticket) = guard.received_ticket.get_mut(&ticket_id) {
|
||||
ticket.rejected = Some(true);
|
||||
}
|
||||
guard.ticket_data.remove(&ticket_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_verified_ticket(&self, ticket_id: i64) -> Result<(), GatewayStorageError> {
|
||||
let mut guard = self.write().await;
|
||||
guard.verified_tickets.push(ticket_id);
|
||||
guard.ticket_verification.remove(&ticket_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_verified_ticket_binary_data(
|
||||
&self,
|
||||
ticket_id: i64,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
if let Some(ticket) = self.write().await.ticket_data.get_mut(&ticket_id) {
|
||||
ticket.data = None;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_all_verified_tickets_with_sn(
|
||||
&self,
|
||||
) -> Result<Vec<VerifiedTicket>, GatewayStorageError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_all_proposed_tickets_with_sn(
|
||||
&self,
|
||||
_proposal_id: u32,
|
||||
) -> Result<Vec<VerifiedTicket>, GatewayStorageError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn insert_redemption_proposal(
|
||||
&self,
|
||||
_tickets: &[VerifiedTicket],
|
||||
_proposal_id: u32,
|
||||
_created_at: OffsetDateTime,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn clear_post_proposal_data(
|
||||
&self,
|
||||
_proposal_id: u32,
|
||||
_resolved_at: OffsetDateTime,
|
||||
_rejected: bool,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn latest_proposal(&self) -> Result<Option<RedemptionProposal>, GatewayStorageError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_all_unverified_tickets(
|
||||
&self,
|
||||
) -> Result<Vec<ClientTicket>, GatewayStorageError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_all_unresolved_proposals(&self) -> Result<Vec<i64>, GatewayStorageError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_votes(&self, _ticket_id: i64) -> Result<Vec<i64>, GatewayStorageError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn get_signers(&self, _epoch_id: i64) -> Result<Vec<i64>, GatewayStorageError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn insert_wireguard_peer(
|
||||
&self,
|
||||
peer: &defguard_wireguard_rs::host::Peer,
|
||||
client_type: ClientType,
|
||||
) -> Result<i64, GatewayStorageError> {
|
||||
let mut guard = self.write().await;
|
||||
let client_id =
|
||||
if let Some(peer) = guard.wireguard_peers.get(&peer.public_key.to_string()) {
|
||||
peer.client_id
|
||||
} else {
|
||||
let client_id = guard.clients.len() as i64;
|
||||
guard.clients.insert(client_id, client_type.to_string());
|
||||
client_id
|
||||
};
|
||||
guard.wireguard_peers.insert(
|
||||
peer.public_key.to_string(),
|
||||
WireguardPeer::from_defguard_peer(peer.clone(), client_id)?,
|
||||
);
|
||||
Ok(client_id)
|
||||
}
|
||||
|
||||
async fn get_wireguard_peer(
|
||||
&self,
|
||||
peer_public_key: &str,
|
||||
) -> Result<Option<WireguardPeer>, GatewayStorageError> {
|
||||
Ok(self
|
||||
.read()
|
||||
.await
|
||||
.wireguard_peers
|
||||
.get(peer_public_key)
|
||||
.cloned())
|
||||
}
|
||||
|
||||
async fn get_all_wireguard_peers(&self) -> Result<Vec<WireguardPeer>, GatewayStorageError> {
|
||||
todo!()
|
||||
}
|
||||
|
||||
async fn remove_wireguard_peer(
|
||||
&self,
|
||||
peer_public_key: &str,
|
||||
) -> Result<(), GatewayStorageError> {
|
||||
self.write().await.wireguard_peers.remove(peer_public_key);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -27,15 +27,18 @@ impl WgPeerManager {
|
||||
pub(crate) async fn insert_peer(&self, peer: &WireguardPeer) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT OR IGNORE INTO wireguard_peer(public_key, preshared_key, protocol_version, endpoint, last_handshake, tx_bytes, rx_bytes, persistent_keepalive_interval, allowed_ips, client_id)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
INSERT OR IGNORE INTO wireguard_peer(public_key, allowed_ips, client_id)
|
||||
VALUES (?, ?, ?);
|
||||
|
||||
UPDATE wireguard_peer
|
||||
SET preshared_key = ?, protocol_version = ?, endpoint = ?, last_handshake = ?, tx_bytes = ?, rx_bytes = ?, persistent_keepalive_interval = ?, allowed_ips = ?, client_id = ?
|
||||
SET allowed_ips = ?, client_id = ?
|
||||
WHERE public_key = ?
|
||||
"#,
|
||||
peer.public_key, peer.preshared_key, peer.protocol_version, peer.endpoint, peer.last_handshake, peer.tx_bytes, peer.rx_bytes, peer.persistent_keepalive_interval, peer.allowed_ips, peer.client_id,
|
||||
peer.preshared_key, peer.protocol_version, peer.endpoint, peer.last_handshake, peer.tx_bytes, peer.rx_bytes, peer.persistent_keepalive_interval, peer.allowed_ips, peer.client_id,
|
||||
peer.public_key,
|
||||
peer.allowed_ips,
|
||||
peer.client_id,
|
||||
peer.allowed_ips,
|
||||
peer.client_id,
|
||||
peer.public_key,
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
|
||||
@@ -131,7 +131,8 @@ impl NoiseConfig {
|
||||
}
|
||||
|
||||
// Only for phased update
|
||||
//SW This can lead to some troubles if two nodes shares the same IP and one support Noise but not the other. This in only for the progressive update though and there is no workaround
|
||||
//SW This can lead to some troubles if two nodes share the same IP and one support Noise but not the other.
|
||||
// This in only for the progressive update though and there is no workaround
|
||||
pub(crate) fn get_noise_support(&self, ip_addr: IpAddr) -> Option<NoiseVersion> {
|
||||
let plain_ip_support = self.network.support.inner.load().get(&ip_addr).copied();
|
||||
|
||||
|
||||
@@ -72,11 +72,7 @@ pub struct FragmentIdentifier {
|
||||
|
||||
impl fmt::Display for FragmentIdentifier {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"Fragment Identifier: id: {} position: {}",
|
||||
self.set_id, self.fragment_position
|
||||
)
|
||||
write!(f, "{} @ {}", self.set_id, self.fragment_position)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ use bytes::{BufMut, BytesMut};
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
use nym_sphinx_params::key_rotation::SphinxKeyRotation;
|
||||
use nym_sphinx_params::packet_sizes::PacketSize;
|
||||
use nym_sphinx_params::packet_version::{PacketVersion, CURRENT_PACKET_VERSION};
|
||||
use nym_sphinx_params::packet_version::{
|
||||
PacketVersion, CURRENT_PACKET_VERSION, LEGACY_PACKET_VERSION,
|
||||
};
|
||||
use nym_sphinx_params::PacketType;
|
||||
use nym_sphinx_types::NymPacket;
|
||||
|
||||
@@ -19,26 +21,25 @@ pub struct FramedNymPacket {
|
||||
pub(crate) packet: NymPacket,
|
||||
}
|
||||
|
||||
impl From<MixPacket> for FramedNymPacket {
|
||||
fn from(packet: MixPacket) -> Self {
|
||||
let typ = packet.packet_type();
|
||||
let rot = packet.key_rotation();
|
||||
FramedNymPacket::new(packet.into_packet(), typ, rot)
|
||||
}
|
||||
}
|
||||
|
||||
impl FramedNymPacket {
|
||||
pub fn new(
|
||||
packet: NymPacket,
|
||||
packet_type: PacketType,
|
||||
key_rotation: SphinxKeyRotation,
|
||||
use_legacy_packet_encoding: bool,
|
||||
) -> Self {
|
||||
// If this fails somebody is using the library in a super incorrect way, because they
|
||||
// already managed to somehow create a sphinx packet
|
||||
let packet_size = PacketSize::get_type(packet.len()).unwrap();
|
||||
|
||||
let packet_version = if use_legacy_packet_encoding {
|
||||
LEGACY_PACKET_VERSION
|
||||
} else {
|
||||
PacketVersion::new()
|
||||
};
|
||||
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::new(),
|
||||
packet_version,
|
||||
packet_size,
|
||||
key_rotation,
|
||||
packet_type,
|
||||
@@ -47,6 +48,12 @@ impl FramedNymPacket {
|
||||
FramedNymPacket { header, packet }
|
||||
}
|
||||
|
||||
pub fn from_mix_packet(packet: MixPacket, use_legacy_packet_encoding: bool) -> Self {
|
||||
let typ = packet.packet_type();
|
||||
let rot = packet.key_rotation();
|
||||
FramedNymPacket::new(packet.into_packet(), typ, rot, use_legacy_packet_encoding)
|
||||
}
|
||||
|
||||
pub fn header(&self) -> Header {
|
||||
self.header
|
||||
}
|
||||
|
||||
@@ -15,6 +15,24 @@ pub enum SphinxKeyRotation {
|
||||
EvenRotation = 2,
|
||||
}
|
||||
|
||||
impl SphinxKeyRotation {
|
||||
pub fn from_key_rotation_id(rotation_id: u32) -> Self {
|
||||
rotation_id.into()
|
||||
}
|
||||
|
||||
pub fn is_unknown(&self) -> bool {
|
||||
matches!(self, SphinxKeyRotation::Unknown)
|
||||
}
|
||||
|
||||
pub fn is_even(&self) -> bool {
|
||||
matches!(self, SphinxKeyRotation::EvenRotation)
|
||||
}
|
||||
|
||||
pub fn is_odd(&self) -> bool {
|
||||
matches!(self, SphinxKeyRotation::OddRotation)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("{received} is not a valid encoding of a sphinx key rotation")]
|
||||
pub struct InvalidSphinxKeyRotation {
|
||||
|
||||
@@ -10,7 +10,7 @@ use thiserror::Error;
|
||||
// - packet_version (starting with v1.1.0)
|
||||
// - packet_size indicator
|
||||
// - packet_type
|
||||
// - sphinx key rotation (starting with v1.12.0/v1.13.0 - either Cheddar or Dolcelatte release)
|
||||
// - sphinx key rotation (starting with v1.13.0 - the Dolcelatte release)
|
||||
|
||||
// it also just so happens that the only valid values for packet_size indicator include values 1-6
|
||||
// therefore if we receive byte `7` (or larger than that) we'll know we received a versioned packet,
|
||||
@@ -22,6 +22,9 @@ pub const CURRENT_PACKET_VERSION_NUMBER: u8 = KEY_ROTATION_VERSION_NUMBER;
|
||||
pub const CURRENT_PACKET_VERSION: PacketVersion =
|
||||
PacketVersion::unchecked(CURRENT_PACKET_VERSION_NUMBER);
|
||||
|
||||
pub const LEGACY_PACKET_VERSION: PacketVersion =
|
||||
PacketVersion::unchecked(INITIAL_PACKET_VERSION_NUMBER);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct PacketVersion(u8);
|
||||
|
||||
|
||||
@@ -324,7 +324,13 @@ async fn persist_commits(
|
||||
} => (validator_address, timestamp, signature),
|
||||
};
|
||||
|
||||
let validator = crate::helpers::validator_info(*validator_id, validators)?;
|
||||
let validator = match crate::helpers::validator_info(*validator_id, validators) {
|
||||
Ok(validator_info) => validator_info,
|
||||
Err(err) => {
|
||||
error!("{err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let validator_address = crate::helpers::validator_consensus_address(*validator_id)?;
|
||||
|
||||
if signature.is_none() {
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::{TaskClient, TaskManager};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::StreamExt;
|
||||
use std::future::Future;
|
||||
use std::mem;
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
@@ -185,12 +186,21 @@ impl ShutdownDropGuard {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ShutdownSignals(JoinSet<()>);
|
||||
|
||||
impl ShutdownSignals {
|
||||
pub async fn wait_for_signal(&mut self) {
|
||||
self.0.join_next().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ShutdownManager {
|
||||
pub root_token: ShutdownToken,
|
||||
|
||||
legacy_task_manager: Option<TaskManager>,
|
||||
|
||||
shutdown_signals: JoinSet<()>,
|
||||
shutdown_signals: ShutdownSignals,
|
||||
|
||||
// the reason I'm not using a `JoinSet` is because it forces us to use futures with the same `::Output` type
|
||||
tracker: TaskTracker,
|
||||
@@ -261,7 +271,7 @@ impl ShutdownManager {
|
||||
F: Send + 'static,
|
||||
{
|
||||
let shutdown_token = self.root_token.clone();
|
||||
self.shutdown_signals.spawn(async move {
|
||||
self.shutdown_signals.0.spawn(async move {
|
||||
shutdown.await;
|
||||
|
||||
info!("sending cancellation after receiving shutdown signal");
|
||||
@@ -356,9 +366,20 @@ impl ShutdownManager {
|
||||
wait_futures.next().await;
|
||||
}
|
||||
|
||||
pub async fn wait_for_shutdown_signal(mut self) {
|
||||
self.shutdown_signals.join_next().await;
|
||||
pub fn detach_shutdown_signals(&mut self) -> ShutdownSignals {
|
||||
mem::take(&mut self.shutdown_signals)
|
||||
}
|
||||
|
||||
pub fn replace_shutdown_signals(&mut self, signals: ShutdownSignals) {
|
||||
self.shutdown_signals = signals;
|
||||
}
|
||||
|
||||
// cancellation safe
|
||||
pub async fn wait_for_shutdown_signal(&mut self) {
|
||||
self.shutdown_signals.0.join_next().await;
|
||||
}
|
||||
|
||||
pub async fn perform_shutdown(mut self) {
|
||||
if let Some(legacy_manager) = self.legacy_task_manager.as_mut() {
|
||||
info!("attempting to shutdown legacy tasks");
|
||||
let _ = legacy_manager.signal_shutdown();
|
||||
@@ -367,4 +388,10 @@ impl ShutdownManager {
|
||||
info!("waiting for tasks to finish... (press ctrl-c to force)");
|
||||
self.finish_shutdown().await;
|
||||
}
|
||||
|
||||
pub async fn run_until_shutdown(mut self) {
|
||||
self.wait_for_shutdown_signal().await;
|
||||
|
||||
self.perform_shutdown().await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ rand = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
time = { workspace = true, features = ["serde"] }
|
||||
|
||||
# 'serde' feature
|
||||
serde_json = { workspace = true, optional = true }
|
||||
@@ -26,7 +27,6 @@ tsify = { workspace = true, features = ["js"], optional = true }
|
||||
wasm-bindgen = { workspace = true, optional = true }
|
||||
|
||||
## internal
|
||||
nym-config = { path = "../config" }
|
||||
nym-crypto = { path = "../crypto" }
|
||||
nym-mixnet-contract-common = { path = "../cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-sphinx-addressing = { path = "../nymsphinx/addressing" }
|
||||
@@ -34,7 +34,6 @@ nym-sphinx-types = { path = "../nymsphinx/types", features = [
|
||||
"sphinx",
|
||||
"outfox",
|
||||
] }
|
||||
nym-sphinx-routing = { path = "../nymsphinx/routing" }
|
||||
|
||||
|
||||
# I'm not sure how to feel about pulling in this dependency here...
|
||||
|
||||
@@ -13,6 +13,7 @@ use std::borrow::Borrow;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Display;
|
||||
use std::net::IpAddr;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{debug, trace, warn};
|
||||
|
||||
pub use crate::node::{EntryDetails, RoutingNode, SupportedRoles};
|
||||
@@ -95,17 +96,25 @@ pub type MixLayer = u8;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
pub struct NymTopologyMetadata {
|
||||
key_rotation_id: u32,
|
||||
pub key_rotation_id: u32,
|
||||
// we have to keep track of key rotation id anyway, so we might as well also include the epoch id
|
||||
// to keep track of the data staleness
|
||||
absolute_epoch_id: EpochId,
|
||||
pub absolute_epoch_id: EpochId,
|
||||
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub refreshed_at: OffsetDateTime,
|
||||
}
|
||||
|
||||
impl NymTopologyMetadata {
|
||||
pub fn new(key_rotation_id: u32, absolute_epoch_id: EpochId) -> Self {
|
||||
pub fn new(
|
||||
key_rotation_id: u32,
|
||||
absolute_epoch_id: EpochId,
|
||||
refreshed_at: impl Into<OffsetDateTime>,
|
||||
) -> Self {
|
||||
NymTopologyMetadata {
|
||||
key_rotation_id,
|
||||
absolute_epoch_id,
|
||||
refreshed_at: refreshed_at.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,6 +125,7 @@ impl Default for NymTopologyMetadata {
|
||||
NymTopologyMetadata {
|
||||
key_rotation_id: u32::MAX,
|
||||
absolute_epoch_id: 0,
|
||||
refreshed_at: OffsetDateTime::now_utc(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,6 +179,10 @@ impl NymRouteProvider {
|
||||
self.topology.metadata.absolute_epoch_id
|
||||
}
|
||||
|
||||
pub fn metadata(&self) -> NymTopologyMetadata {
|
||||
self.topology.metadata
|
||||
}
|
||||
|
||||
pub fn new_empty(ignore_egress_epoch_roles: bool) -> NymRouteProvider {
|
||||
let this: Self = NymTopology::default().into();
|
||||
this.with_ignore_egress_epoch_roles(ignore_egress_epoch_roles)
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::NymTopology;
|
||||
use crate::{NymTopology, NymTopologyMetadata};
|
||||
pub use async_trait::async_trait;
|
||||
use nym_api_requests::nym_nodes::NodesResponseMetadata;
|
||||
|
||||
// hehe, wasm
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -47,3 +48,15 @@ impl TopologyProvider for HardcodedTopologyProvider {
|
||||
Some(self.topology.clone())
|
||||
}
|
||||
}
|
||||
|
||||
// helper trait to convert between nym-api response and the topology metadata
|
||||
// (we don't want to be importing any of those in the other crates)
|
||||
pub trait ToTopologyMetadata {
|
||||
fn to_topology_metadata(&self) -> NymTopologyMetadata;
|
||||
}
|
||||
|
||||
impl ToTopologyMetadata for NodesResponseMetadata {
|
||||
fn to_topology_metadata(&self) -> NymTopologyMetadata {
|
||||
NymTopologyMetadata::new(self.rotation_id, self.absolute_epoch_id, self.refreshed_at)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -508,9 +508,9 @@ pub struct ReplySurbsWasm {
|
||||
/// deciding it's never going to get them and would drop all pending messages
|
||||
pub maximum_reply_surb_drop_waiting_period_ms: u32,
|
||||
|
||||
/// Defines maximum amount of time given reply surb is going to be valid for.
|
||||
/// This is going to be superseded by key rotation once implemented.
|
||||
pub maximum_reply_surb_age_ms: u32,
|
||||
/// Defines maximum number of times the client is going to re-request reply surbs
|
||||
/// for clearing pending messages before giving up after making no progress.
|
||||
pub maximum_reply_surbs_rerequests: usize,
|
||||
|
||||
/// 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.
|
||||
@@ -519,9 +519,6 @@ pub struct ReplySurbsWasm {
|
||||
/// Defines how many mix nodes the reply surb should go through.
|
||||
/// If not set, the default value is going to be used.
|
||||
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 ReplySurbsWasm {
|
||||
@@ -546,14 +543,11 @@ impl From<ReplySurbsWasm> for ConfigReplySurbs {
|
||||
maximum_reply_surb_drop_waiting_period: Duration::from_millis(
|
||||
reply_surbs.maximum_reply_surb_drop_waiting_period_ms as u64,
|
||||
),
|
||||
maximum_reply_surb_age: Duration::from_millis(
|
||||
reply_surbs.maximum_reply_surb_age_ms as u64,
|
||||
),
|
||||
maximum_reply_surbs_rerequests: reply_surbs.maximum_reply_surbs_rerequests,
|
||||
maximum_reply_key_age: Duration::from_millis(
|
||||
reply_surbs.maximum_reply_key_age_ms as u64,
|
||||
),
|
||||
surb_mix_hops: reply_surbs.surb_mix_hops,
|
||||
fresh_sender_tags: reply_surbs.fresh_sender_tags,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -574,10 +568,9 @@ impl From<ConfigReplySurbs> for ReplySurbsWasm {
|
||||
maximum_reply_surb_drop_waiting_period_ms: reply_surbs
|
||||
.maximum_reply_surb_drop_waiting_period
|
||||
.as_millis() as u32,
|
||||
maximum_reply_surb_age_ms: reply_surbs.maximum_reply_surb_age.as_millis() as u32,
|
||||
maximum_reply_surbs_rerequests: reply_surbs.maximum_reply_surbs_rerequests,
|
||||
maximum_reply_key_age_ms: reply_surbs.maximum_reply_key_age.as_millis() as u32,
|
||||
surb_mix_hops: reply_surbs.surb_mix_hops,
|
||||
fresh_sender_tags: reply_surbs.fresh_sender_tags,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,16 +383,16 @@ pub struct ReplySurbsWasmOverride {
|
||||
#[tsify(optional)]
|
||||
pub maximum_reply_surb_drop_waiting_period_ms: Option<u32>,
|
||||
|
||||
/// Defines maximum amount of time given reply surb is going to be valid for.
|
||||
/// This is going to be superseded by key rotation once implemented.
|
||||
#[tsify(optional)]
|
||||
pub maximum_reply_surb_age_ms: Option<u32>,
|
||||
|
||||
/// 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.
|
||||
#[tsify(optional)]
|
||||
pub maximum_reply_key_age_ms: Option<u32>,
|
||||
|
||||
/// Defines maximum number of times the client is going to re-request reply surbs
|
||||
/// for clearing pending messages before giving up after making no progress.
|
||||
#[tsify(optional)]
|
||||
pub maximum_reply_surbs_rerequests: Option<usize>,
|
||||
|
||||
#[tsify(optional)]
|
||||
pub surb_mix_hops: Option<u8>,
|
||||
|
||||
@@ -429,14 +429,13 @@ impl From<ReplySurbsWasmOverride> for ReplySurbsWasm {
|
||||
maximum_reply_surb_drop_waiting_period_ms: value
|
||||
.maximum_reply_surb_drop_waiting_period_ms
|
||||
.unwrap_or(def.maximum_reply_surb_drop_waiting_period_ms),
|
||||
maximum_reply_surb_age_ms: value
|
||||
.maximum_reply_surb_age_ms
|
||||
.unwrap_or(def.maximum_reply_surb_age_ms),
|
||||
maximum_reply_key_age_ms: value
|
||||
.maximum_reply_key_age_ms
|
||||
.unwrap_or(def.maximum_reply_key_age_ms),
|
||||
surb_mix_hops: value.surb_mix_hops,
|
||||
fresh_sender_tags: value.fresh_sender_tags,
|
||||
maximum_reply_surbs_rerequests: value
|
||||
.maximum_reply_surbs_rerequests
|
||||
.unwrap_or(def.maximum_reply_surbs_rerequests),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ use nym_client_core::init::{
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use nym_topology::wasm_helpers::WasmFriendlyNymTopology;
|
||||
use nym_topology::{NymTopology, NymTopologyMetadata, RoutingNode};
|
||||
use nym_topology::{NymTopology, RoutingNode};
|
||||
use nym_validator_client::client::IdentityKey;
|
||||
use nym_validator_client::{NymApiClient, UserAgent};
|
||||
use rand::thread_rng;
|
||||
@@ -29,6 +29,7 @@ use wasm_bindgen_futures::future_to_promise;
|
||||
use wasm_utils::error::PromisableResult;
|
||||
|
||||
pub use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage;
|
||||
use nym_topology::provider_trait::ToTopologyMetadata;
|
||||
use wasm_utils::{console_log, console_warn};
|
||||
|
||||
// don't get too excited about the name, under the hood it's just a big fat placeholder
|
||||
@@ -79,21 +80,19 @@ pub async fn current_network_topology_async(
|
||||
let metadata = mixnodes_res.metadata;
|
||||
let mixnodes = mixnodes_res.nodes;
|
||||
|
||||
let gateways_res = api_client.get_all_basic_entry_assigned_nodes_v2().await?;
|
||||
if gateways_res.metadata != metadata {
|
||||
let gateways_res = api_client
|
||||
.get_all_basic_entry_assigned_nodes_with_metadata()
|
||||
.await?;
|
||||
if !gateways_res.metadata.consistency_check(&metadata) {
|
||||
console_warn!("inconsistent nodes metadata between mixnodes and gateways calls! {metadata:?} and {:?}", gateways_res.metadata);
|
||||
return Err(WasmCoreError::UnavailableNetworkTopology);
|
||||
}
|
||||
|
||||
let gateways = gateways_res.nodes;
|
||||
|
||||
let topology = NymTopology::new(
|
||||
NymTopologyMetadata::new(metadata.rotation_id, metadata.absolute_epoch_id),
|
||||
rewarded_set,
|
||||
Vec::new(),
|
||||
)
|
||||
.with_skimmed_nodes(&mixnodes)
|
||||
.with_skimmed_nodes(&gateways);
|
||||
let topology = NymTopology::new(metadata.to_topology_metadata(), rewarded_set, Vec::new())
|
||||
.with_skimmed_nodes(&mixnodes)
|
||||
.with_skimmed_nodes(&gateways);
|
||||
|
||||
Ok(topology.into())
|
||||
}
|
||||
|
||||
@@ -11,11 +11,13 @@ license.workspace = true
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
bincode = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
dashmap = { workspace = true }
|
||||
defguard_wireguard_rs = { workspace = true }
|
||||
dyn-clone = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
# The latest version on crates.io at the time of writing this (6.0.0) has a
|
||||
# version mismatch with x25519-dalek/curve25519-dalek that is resolved in the
|
||||
@@ -37,3 +39,11 @@ nym-network-defaults = { path = "../network-defaults" }
|
||||
nym-task = { path = "../task" }
|
||||
nym-wireguard-types = { path = "../wireguard-types" }
|
||||
nym-node-metrics = { path = "../../nym-node/nym-node-metrics" }
|
||||
|
||||
[dev-dependencies]
|
||||
nym-gateway-storage = { path = "../gateway-storage", features = ["mock"] }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
mock = ["nym-gateway-storage/mock"]
|
||||
|
||||
|
||||
@@ -3,18 +3,18 @@
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("traffic byte data needs to be increasing")]
|
||||
InconsistentConsumedBytes,
|
||||
|
||||
#[error("{0}")]
|
||||
Defguard(#[from] defguard_wireguard_rs::error::WireguardInterfaceError),
|
||||
|
||||
#[error("internal {0}")]
|
||||
Internal(String),
|
||||
|
||||
#[error("storage should have the requested bandwidht entry")]
|
||||
#[error("storage should have the requested bandwidth entry")]
|
||||
MissingClientBandwidthEntry,
|
||||
|
||||
#[error("kernel should have the requested client entry: {0}")]
|
||||
MissingClientKernelEntry(String),
|
||||
|
||||
#[error("{0}")]
|
||||
GatewayStorage(#[from] nym_gateway_storage::error::GatewayStorageError),
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user