Compare commits
119 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 660463908d | |||
| 0be844e015 | |||
| efa6e7d7c7 | |||
| 33c783bb7c | |||
| 16059211b9 | |||
| bb6c920767 | |||
| 8c4df963c9 | |||
| af737596ca | |||
| af2c4f50b6 | |||
| 02ed64557d | |||
| 38dabd8d0d | |||
| d9de5cfa33 | |||
| bdfbfde463 | |||
| 5179f38ad2 | |||
| f4e9abcd22 | |||
| 46ebd84b02 | |||
| d8d2f99a18 | |||
| cd3ec5f3bd | |||
| 32a16ef025 | |||
| 6af4e44f55 | |||
| 3cddc594b4 | |||
| d11aaed392 | |||
| 1bead28150 | |||
| 735bed5cd7 | |||
| 12e0d34885 | |||
| 43af3b8a3b | |||
| 8ff96b11c9 | |||
| df453158d6 | |||
| abeeadb661 | |||
| 752fe7fa0f | |||
| c5ec682088 | |||
| 58a569cd26 | |||
| 2e767a2586 | |||
| dc772d8759 | |||
| 9e70c7a32d | |||
| ba5e86e842 | |||
| b7313656e9 | |||
| 2eb695088f | |||
| eb612d47c0 | |||
| 2ba7b26e5d | |||
| 4cd0f7b56f | |||
| 600bf42a95 | |||
| 748e3e4248 | |||
| 8cf1b6427a | |||
| 7a888c6fdf | |||
| 9a9bb89d89 | |||
| 4cc14ddcc4 | |||
| 2dbf9d97cb | |||
| 91b6f3cc3e | |||
| 84cccffcbd | |||
| 7de346cf89 | |||
| d6c40aee01 | |||
| af16b3f059 | |||
| b1cde0716e | |||
| 45bcdb03d8 | |||
| 0841b8701d | |||
| 7ae228d8f4 | |||
| 916d33c8c0 | |||
| 9b4b2d1a46 | |||
| aef0a52c4b | |||
| 44682b5ef0 | |||
| f282ffd8a6 | |||
| dfbeb8b1f8 | |||
| fc06fe39a2 | |||
| caa94c142f | |||
| 1a5c54084e | |||
| 49d203e18d | |||
| 51c9b012e2 | |||
| 50b1175622 | |||
| 29ee5984fb | |||
| e542b25ffc | |||
| 516d3f04cf | |||
| 9225e0a630 | |||
| 08c09781c7 | |||
| 36a4d96f34 | |||
| 139c911350 | |||
| c92de832e4 | |||
| d9d62195cb | |||
| da9115d51b | |||
| bfddc1e4c1 | |||
| 080d75204e | |||
| 1367cad99d | |||
| 4f6d65ab95 | |||
| 4292d8ac03 | |||
| dcb6de2421 | |||
| 1f5ed41bb3 | |||
| 091e98aa74 | |||
| 0e38126fc5 | |||
| ecbe192a88 | |||
| f0ee49788c | |||
| d2ff3cb88d | |||
| 873d15a5e1 | |||
| 53792cc839 | |||
| 415ef1bf13 | |||
| edfe29b738 | |||
| a4f6426bf9 | |||
| 0870911b3c | |||
| 9f23887cc0 | |||
| 8ab269fa05 | |||
| 7b75f22a8e | |||
| ca0449e03d | |||
| 224e63d275 | |||
| 3d77283056 | |||
| 7cc473005b | |||
| f874284850 | |||
| 7b6077ba64 | |||
| 0d4188785b | |||
| 86c05267c2 | |||
| b4865520a4 | |||
| f52ebfb9c3 | |||
| 6ca2a3c539 | |||
| 717c9066d6 | |||
| 2760a17323 | |||
| 4e9f1bc0ed | |||
| d35023d14b | |||
| 400aa6ba6d | |||
| 2ba74ae120 | |||
| 9a4293a5b9 | |||
| cdddb44099 |
@@ -102,6 +102,8 @@ jobs:
|
||||
- name: Run all tests
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
uses: actions-rs/cargo@v1
|
||||
env:
|
||||
NYM_API: https://sandbox-nym-api1.nymtech.net/api
|
||||
with:
|
||||
command: test
|
||||
args: --workspace
|
||||
|
||||
@@ -16,8 +16,12 @@ jobs:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
steps:
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y libdbus-1-dev libmnl-dev libnftnl-dev \
|
||||
libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev jq \
|
||||
libgtk-3-dev squashfs-tools libayatana-appindicator3-dev make libfuse2 unzip librsvg2-dev file \
|
||||
libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
|
||||
continue-on-error: true
|
||||
|
||||
- name: Check out repository code
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
name: Integration Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "nym-api/**"
|
||||
- "tests/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
integration-tests:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
API_BASE_URL: http://localhost:8000
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
|
||||
- name: Install dependencies
|
||||
run: sudo apt-get update && sudo apt-get install -y pkg-config libssl-dev
|
||||
|
||||
- name: Build nym-api
|
||||
run: cargo build --package nym-api
|
||||
|
||||
- name: Run nym-api in the background
|
||||
run: |
|
||||
./target/debug/nym-api &
|
||||
|
||||
- name: Wait for nym-api to come alive
|
||||
run: |
|
||||
for i in {1..20}; do
|
||||
curl -sSf http://localhost:8000/v1/status/config-score-details && break
|
||||
echo "Waiting for nym-api to start..."
|
||||
sleep 2
|
||||
done
|
||||
|
||||
- name: Run integration tests
|
||||
env:
|
||||
NYM_API: https://sandbox-nym-api1.nymtech.net/api
|
||||
run: cargo test --test public-api-tests -- --nocapture
|
||||
@@ -18,11 +18,7 @@ jobs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.id }}
|
||||
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].created_at }}
|
||||
version: ${{ steps.release-info.outputs.version }}
|
||||
filename: ${{ steps.release-info.outputs.filename }}
|
||||
file_hash: ${{ steps.release-info.outputs.file_hash }}
|
||||
release_tag: ${{ github.ref_name }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -33,10 +29,16 @@ jobs:
|
||||
node-version: 21
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Add Rust target for x86_64-apple-darwin
|
||||
run: rustup target add x86_64-apple-darwin
|
||||
|
||||
- name: Set Cargo build target to x86_64
|
||||
run: echo "CARGO_BUILD_TARGET=x86_64-apple-darwin" >> $GITHUB_ENV
|
||||
|
||||
- name: Install the Apple developer certificate for code signing
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
@@ -66,12 +68,6 @@ jobs:
|
||||
fileName: '.env'
|
||||
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
|
||||
|
||||
- name: Add Rust target for x86_64-apple-darwin
|
||||
run: rustup target add x86_64-apple-darwin
|
||||
|
||||
- name: Set Cargo build target to x86_64
|
||||
run: echo "CARGO_BUILD_TARGET=x86_64-apple-darwin" >> $GITHUB_ENV
|
||||
|
||||
- name: Yarn cache clean
|
||||
shell: bash
|
||||
run: cd .. && yarn cache clean
|
||||
@@ -94,10 +90,22 @@ jobs:
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_IDENTITY_ID }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
# Tauri v2 specific environment variables
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
TAURI_NOTARIZATION_USERNAME: ${{ secrets.APPLE_ID }}
|
||||
TAURI_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
TAURI_NOTARIZATION_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
yarn build-macx86
|
||||
yarn build-macx86
|
||||
|
||||
- name: Create app tarball
|
||||
run: |
|
||||
# Navigate to where the app bundle is and create the tarball
|
||||
cd target/x86_64-apple-darwin/release/bundle/macos
|
||||
echo "Creating tarball from app bundle"
|
||||
tar -czf nym-wallet.app.tar.gz NymWallet.app
|
||||
cd -
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -120,22 +128,10 @@ jobs:
|
||||
nym-wallet/target/x86_64-apple-darwin/release/bundle/dmg/*.dmg
|
||||
nym-wallet/target/x86_64-apple-darwin/release/bundle/macos/*.app.tar.gz*
|
||||
|
||||
- name: Deploy artifacts to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-avzr"
|
||||
SOURCE: "nym-wallet/target/x86_64-apple-darwin/release/bundle/macos/nym-wallet.app.tar.gz"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/${{ github.ref_name }}/nym-wallet
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
|
||||
push-release-data:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/release-calculate-hash.yml
|
||||
needs: publish-tauri
|
||||
with:
|
||||
release_tag: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
|
||||
secrets: inherit
|
||||
@@ -3,71 +3,108 @@ on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: nym-wallet
|
||||
|
||||
jobs:
|
||||
publish-tauri:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [custom-ubuntu-22.04]
|
||||
platform: [ubuntu-22.04]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.id }}
|
||||
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].created_at }}
|
||||
version: ${{ steps.release-info.outputs.version }}
|
||||
filename: ${{ steps.release-info.outputs.filename }}
|
||||
file_hash: ${{ steps.release-info.outputs.file_hash }}
|
||||
|
||||
release_tag: ${{ github.ref_name }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Tauri dependencies
|
||||
run: >
|
||||
sudo apt-get update &&
|
||||
sudo apt-get install -y webkit2gtk-4.0
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y libdbus-1-dev libmnl-dev libnftnl-dev \
|
||||
libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev jq \
|
||||
libgtk-3-dev squashfs-tools libayatana-appindicator3-dev make libfuse2 unzip librsvg2-dev file \
|
||||
libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
|
||||
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
|
||||
- name: Install project dependencies
|
||||
shell: bash
|
||||
run: cd .. && yarn --network-timeout 100000
|
||||
|
||||
|
||||
- name: Install app dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Create env file
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
with:
|
||||
fileName: '.env'
|
||||
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
|
||||
|
||||
|
||||
- name: Build app
|
||||
run: yarn build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: Check bundle directory
|
||||
run: |
|
||||
echo "Checking bundle directory structure"
|
||||
ls -la target/release/bundle || echo "Bundle directory not found"
|
||||
if [ -d "target/release/bundle/appimage" ]; then
|
||||
echo "AppImage bundle directory exists, checking contents:"
|
||||
ls -la target/release/bundle/appimage
|
||||
else
|
||||
echo "AppImage bundle directory not found, checking alternatives:"
|
||||
find target/release/bundle -type d -name "*appimage*" -o -name "*AppImage*" || echo "No AppImage directories found"
|
||||
find target/release/bundle -name "*.AppImage" -o -name "*.appimage" || echo "No AppImage files found"
|
||||
fi
|
||||
|
||||
- name: Create AppImage tarball if needed
|
||||
run: |
|
||||
# Find the AppImage file
|
||||
APPIMAGE_FILE=$(find target/release/bundle -name "*.AppImage" | head -n 1)
|
||||
if [ -n "$APPIMAGE_FILE" ]; then
|
||||
echo "Found AppImage file: $APPIMAGE_FILE"
|
||||
APPIMAGE_DIR=$(dirname "$APPIMAGE_FILE")
|
||||
APPIMAGE_NAME=$(basename "$APPIMAGE_FILE")
|
||||
|
||||
# Create tarball if it doesn't exist
|
||||
if [ ! -f "${APPIMAGE_FILE}.tar.gz" ]; then
|
||||
echo "Creating tarball for $APPIMAGE_NAME"
|
||||
cd "$APPIMAGE_DIR"
|
||||
tar -czf "${APPIMAGE_NAME}.tar.gz" "$APPIMAGE_NAME"
|
||||
cd -
|
||||
echo "Created tarball: ${APPIMAGE_FILE}.tar.gz"
|
||||
else
|
||||
echo "Tarball already exists: ${APPIMAGE_FILE}.tar.gz"
|
||||
fi
|
||||
else
|
||||
echo "WARNING: No AppImage file found!"
|
||||
fi
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nym-wallet_1.0.0_amd64.AppImage.tar.gz
|
||||
path: nym-wallet/target/release/bundle/appimage/nym-wallet*.AppImage.tar.gz
|
||||
name: nym-wallet-appimage.tar.gz
|
||||
path: |
|
||||
nym-wallet/target/release/bundle/appimage/*.AppImage.tar.gz
|
||||
nym-wallet/target/release/bundle/*/nym-wallet*.AppImage.tar.gz
|
||||
retention-days: 30
|
||||
|
||||
|
||||
- id: create-release
|
||||
name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v2
|
||||
@@ -75,24 +112,26 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
nym-wallet/target/release/bundle/appimage/*.AppImage
|
||||
nym-wallet/target/release/bundle/appimage/*.AppImage.tar.gz*
|
||||
|
||||
- name: Deploy artifacts to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-avzr"
|
||||
SOURCE: "nym-wallet/target/release/bundle/appimage/nym-wallet*.AppImage.tar.gz"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/${{ github.ref_name }}/nym-wallet
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
|
||||
nym-wallet/target/release/bundle/appimage/*.AppImage.tar.gz
|
||||
nym-wallet/target/release/bundle/*/nym-wallet*.AppImage
|
||||
nym-wallet/target/release/bundle/*/nym-wallet*.AppImage.tar.gz
|
||||
|
||||
- name: Find AppImage tarball path for deployment
|
||||
id: find-appimage
|
||||
run: |
|
||||
APPIMAGE_TARBALL=$(find target/release/bundle -name "*.AppImage.tar.gz" | head -n 1)
|
||||
if [ -n "$APPIMAGE_TARBALL" ]; then
|
||||
echo "Found AppImage tarball: $APPIMAGE_TARBALL"
|
||||
echo "appimage_path=$APPIMAGE_TARBALL" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "WARNING: No AppImage tarball found for deployment!"
|
||||
echo "appimage_path=target/release/bundle/appimage/nym-wallet*.AppImage.tar.gz" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
push-release-data:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/release-calculate-hash.yml
|
||||
needs: publish-tauri
|
||||
with:
|
||||
release_tag: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
|
||||
secrets: inherit
|
||||
@@ -1,6 +1,12 @@
|
||||
name: publish-nym-wallet-win11
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sign:
|
||||
description: "Sign this build using SSL.com. Signing is billed per signature so be careful"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
@@ -18,53 +24,61 @@ jobs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.id }}
|
||||
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].created_at }}
|
||||
version: ${{ steps.release-info.outputs.version }}
|
||||
filename: ${{ steps.release-info.outputs.filename }}
|
||||
file_hash: ${{ steps.release-info.outputs.file_hash }}
|
||||
release_tag: ${{ github.ref_name }}
|
||||
|
||||
steps:
|
||||
- name: Clean up first
|
||||
continue-on-error: true
|
||||
working-directory: .
|
||||
run: |
|
||||
cd ..
|
||||
del /s /q /A:H nym
|
||||
rmdir /s /q nym
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Import signing certificate
|
||||
env:
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
New-Item -ItemType directory -Path certificate
|
||||
Set-Content -Path certificate/tempCert.txt -Value $env:WINDOWS_CERTIFICATE
|
||||
certutil -decode certificate/tempCert.txt certificate/certificate.pfx
|
||||
Remove-Item -path certificate -include tempCert.txt
|
||||
Import-PfxCertificate -FilePath certificate/certificate.pfx -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Setup MSBuild.exe
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Create env file
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
with:
|
||||
fileName: '.env'
|
||||
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Download EV CodeSignTool from ssl.com
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
shell: bash
|
||||
run: |
|
||||
curl -L0 https://www.ssl.com/download/codesigntool-for-linux-and-macos/ -o codesigntool.zip
|
||||
unzip codesigntool.zip
|
||||
- name: Get EV certificate credential id
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
id: get_credential_ids
|
||||
shell: bash
|
||||
run: |
|
||||
echo "SSL_COM_CREDENTIAL_ID=$(./CodeSignTool.sh get_credential_ids -username=${{ secrets.SSL_COM_USERNAME }} -password=${{ secrets.SSL_COM_PASSWORD }} | sed -n '1!p' | sed 's/- //')" >> "$GITHUB_OUTPUT"
|
||||
- name: Add custom sign command to tauri.conf.json
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
shell: bash
|
||||
run: |
|
||||
yq eval --inplace '.bundle.windows +=
|
||||
{
|
||||
"signCommand": {
|
||||
"cmd": "C:\Program Files\Git\bin\bash.EXE",
|
||||
"args": [
|
||||
"/c/actions-runner/_work/nym/nym/nym-wallet/src-tauri/CodeSignTool.sh",
|
||||
"sign",
|
||||
"-username ${{ secrets.SSL_COM_USERNAME }}",
|
||||
"-password ${{ secrets.SSL_COM_PASSWORD }}",
|
||||
"-credential_id ${{ steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}",
|
||||
"-totp_secret ${{ secrets.SSL_COM_TOTP_SECRET }}",
|
||||
"-program_name NymWallet",
|
||||
"-input_file_path",
|
||||
"%1",
|
||||
"-override"
|
||||
]
|
||||
}
|
||||
}' tauri.conf.json
|
||||
- name: Install project dependencies
|
||||
shell: bash
|
||||
run: cd .. && yarn --network-timeout 100000
|
||||
@@ -77,18 +91,50 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
run: yarn build
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
SSL_COM_USERNAME: ${{ inputs.sign && secrets.SSL_COM_USERNAME }}
|
||||
SSL_COM_PASSWORD: ${{ inputs.sign && secrets.SSL_COM_PASSWORD }}
|
||||
SSL_COM_CREDENTIAL_ID: ${{ inputs.sign && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
|
||||
SSL_COM_TOTP_SECRET: ${{ inputs.sign && secrets.SSL_COM_TOTP_SECRET }}
|
||||
run: |
|
||||
echo "Starting build process..."
|
||||
yarn build
|
||||
|
||||
- name: Check bundle directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Checking bundle directory structure"
|
||||
|
||||
# Check standard location
|
||||
if [ -d "target/release/bundle" ]; then
|
||||
echo "Found bundle directory at standard location"
|
||||
ls -la target/release/bundle || echo "Failed to list bundle directory"
|
||||
fi
|
||||
|
||||
# Check src-tauri location
|
||||
if [ -d "src-tauri/target/release/bundle" ]; then
|
||||
echo "Found bundle directory in src-tauri"
|
||||
ls -la src-tauri/target/release/bundle || echo "Failed to list src-tauri bundle directory"
|
||||
|
||||
# Use this path for future steps
|
||||
echo "BUNDLE_PATH=src-tauri/target/release/bundle" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Using standard bundle path"
|
||||
echo "BUNDLE_PATH=target/release/bundle" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# Check for MSI files in any location
|
||||
find . -name "*.msi" -type f
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nym-wallet_1.0.0_x64_en-US.msi
|
||||
path: nym-wallet/target/release/bundle/msi/nym-wallet_1.*.msi
|
||||
name: nym-wallet.msi
|
||||
path: |
|
||||
nym-wallet/${{ env.BUNDLE_PATH }}/msi/*.msi
|
||||
nym-wallet/${{ env.BUNDLE_PATH }}/*/nym-wallet*.msi
|
||||
nym-wallet/src-tauri/target/release/bundle/msi/*.msi
|
||||
retention-days: 30
|
||||
|
||||
- id: create-release
|
||||
@@ -97,25 +143,28 @@ jobs:
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
nym-wallet/target/release/bundle/msi/*.msi
|
||||
nym-wallet/target/release/bundle/msi/*.msi.zip*
|
||||
|
||||
- name: Deploy artifacts to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-avzr"
|
||||
SOURCE: "nym-wallet/target/release/bundle/msi/nym-wallet_1.*.msi"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/${{ github.ref_name }}/nym-wallet
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
nym-wallet/${{ env.BUNDLE_PATH }}/msi/*.msi
|
||||
nym-wallet/${{ env.BUNDLE_PATH }}/msi/*.msi.zip*
|
||||
nym-wallet/${{ env.BUNDLE_PATH }}/*/nym-wallet*.msi
|
||||
nym-wallet/src-tauri/target/release/bundle/msi/*.msi
|
||||
|
||||
- name: Find MSI path for deployment
|
||||
id: find-msi
|
||||
shell: bash
|
||||
run: |
|
||||
MSI_FILE=$(find . -name "*.msi" -type f | head -n 1)
|
||||
if [ -n "$MSI_FILE" ]; then
|
||||
echo "Found MSI file: $MSI_FILE"
|
||||
echo "msi_path=$MSI_FILE" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "WARNING: No MSI file found for deployment!"
|
||||
echo "msi_path=${{ env.BUNDLE_PATH }}/msi/nym-wallet*.msi" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
push-release-data:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/release-calculate-hash.yml
|
||||
needs: publish-tauri
|
||||
with:
|
||||
release_tag: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
|
||||
secrets: inherit
|
||||
Generated
+41
-55
@@ -908,6 +908,16 @@ dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bloomfilter"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f6d7f06817e48ea4e17532fa61bc4e8b9a101437f0623f69d2ea54284f3a817"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
"siphasher 1.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bls12_381"
|
||||
version = "0.8.0"
|
||||
@@ -1564,6 +1574,7 @@ dependencies = [
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"futures",
|
||||
"is-terminal",
|
||||
"itertools 0.10.5",
|
||||
"num-traits",
|
||||
@@ -1576,6 +1587,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"tokio",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -1591,9 +1603,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-channel"
|
||||
version = "0.5.14"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
|
||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
@@ -2173,6 +2185,12 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10"
|
||||
|
||||
[[package]]
|
||||
name = "dotenv"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
@@ -4685,7 +4703,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "nym-api"
|
||||
version = "1.1.55"
|
||||
version = "1.1.56"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -4696,6 +4714,7 @@ dependencies = [
|
||||
"bip39",
|
||||
"bs58",
|
||||
"cfg-if",
|
||||
"chrono",
|
||||
"clap",
|
||||
"console-subscriber",
|
||||
"cosmwasm-std",
|
||||
@@ -4705,6 +4724,7 @@ dependencies = [
|
||||
"cw4",
|
||||
"dashmap",
|
||||
"dirs",
|
||||
"dotenv",
|
||||
"futures",
|
||||
"getset",
|
||||
"humantime-serde",
|
||||
@@ -4937,7 +4957,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-cli"
|
||||
version = "1.1.52"
|
||||
version = "1.1.53"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
@@ -5020,7 +5040,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client"
|
||||
version = "1.1.52"
|
||||
version = "1.1.53"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -5062,7 +5082,6 @@ dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"bs58",
|
||||
"cfg-if",
|
||||
"clap",
|
||||
"comfy-table",
|
||||
"futures",
|
||||
@@ -5077,12 +5096,10 @@ dependencies = [
|
||||
"nym-client-core-gateways-storage",
|
||||
"nym-client-core-surb-storage",
|
||||
"nym-config",
|
||||
"nym-country-group",
|
||||
"nym-credential-storage",
|
||||
"nym-credentials-interface",
|
||||
"nym-crypto",
|
||||
"nym-ecash-time",
|
||||
"nym-explorer-client",
|
||||
"nym-gateway-client",
|
||||
"nym-gateway-requests",
|
||||
"nym-http-api-client",
|
||||
@@ -5103,7 +5120,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"sha2 0.10.8",
|
||||
"si-scale",
|
||||
"tap",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
@@ -5125,7 +5141,6 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"humantime-serde",
|
||||
"nym-config",
|
||||
"nym-country-group",
|
||||
"nym-pemstore",
|
||||
"nym-sphinx-addressing",
|
||||
"nym-sphinx-params",
|
||||
@@ -5267,14 +5282,6 @@ dependencies = [
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-country-group"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-cpp-ffi"
|
||||
version = "0.1.2"
|
||||
@@ -5551,31 +5558,6 @@ dependencies = [
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-explorer-api-requests"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nym-api-requests",
|
||||
"nym-contracts-common",
|
||||
"nym-mixnet-contract-common",
|
||||
"schemars",
|
||||
"serde",
|
||||
"ts-rs",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-explorer-client"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"nym-explorer-api-requests",
|
||||
"reqwest 0.12.15",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-ffi-shared"
|
||||
version = "0.2.1"
|
||||
@@ -6055,7 +6037,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-requester"
|
||||
version = "1.1.53"
|
||||
version = "1.1.54"
|
||||
dependencies = [
|
||||
"addr",
|
||||
"anyhow",
|
||||
@@ -6106,7 +6088,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node"
|
||||
version = "1.8.0"
|
||||
version = "1.9.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -6115,15 +6097,18 @@ dependencies = [
|
||||
"axum 0.7.9",
|
||||
"bip39",
|
||||
"blake2 0.8.1",
|
||||
"bloomfilter",
|
||||
"bs58",
|
||||
"cargo_metadata 0.18.1",
|
||||
"celes",
|
||||
"chacha",
|
||||
"clap",
|
||||
"colored",
|
||||
"criterion",
|
||||
"csv",
|
||||
"cupid",
|
||||
"futures",
|
||||
"hkdf",
|
||||
"human-repr",
|
||||
"humantime-serde",
|
||||
"indicatif",
|
||||
@@ -6163,6 +6148,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.8",
|
||||
"sysinfo",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
@@ -6235,7 +6221,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-api"
|
||||
version = "2.0.0"
|
||||
version = "2.1.0"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
@@ -6251,7 +6237,6 @@ dependencies = [
|
||||
"nym-bin-common",
|
||||
"nym-contracts-common",
|
||||
"nym-crypto",
|
||||
"nym-explorer-client",
|
||||
"nym-http-api-client",
|
||||
"nym-network-defaults",
|
||||
"nym-node-metrics",
|
||||
@@ -6497,7 +6482,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.52"
|
||||
version = "1.1.53"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -6660,6 +6645,7 @@ dependencies = [
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
"tracing",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
@@ -6713,8 +6699,6 @@ name = "nym-sphinx-framing"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"log",
|
||||
"nym-metrics",
|
||||
"nym-sphinx-acknowledgements",
|
||||
"nym-sphinx-addressing",
|
||||
"nym-sphinx-forwarding",
|
||||
@@ -6723,6 +6707,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7101,7 +7086,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nymvisor"
|
||||
version = "0.1.17"
|
||||
version = "0.1.18"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -9246,9 +9231,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sphinx-packet"
|
||||
version = "0.3.2"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c23047e0cf36ff6904603f499fd13153425cdf5ba47bfbaedbc999da0bd92f4e"
|
||||
checksum = "c26f0c20d909fdda1c5d0ece3973127ca421984d55b000215df365e93722fc6e"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arrayref",
|
||||
@@ -9267,6 +9252,7 @@ dependencies = [
|
||||
"sha2 0.10.8",
|
||||
"subtle 2.6.1",
|
||||
"x25519-dalek",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10061,9 +10047,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.44.1"
|
||||
version = "1.44.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
|
||||
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
|
||||
+7
-10
@@ -39,7 +39,6 @@ members = [
|
||||
"common/cosmwasm-smart-contracts/mixnet-contract",
|
||||
"common/cosmwasm-smart-contracts/multisig-contract",
|
||||
"common/cosmwasm-smart-contracts/vesting-contract",
|
||||
"common/country-group",
|
||||
"common/credential-storage",
|
||||
"common/credential-utils",
|
||||
"common/credential-verification",
|
||||
@@ -97,9 +96,6 @@ members = [
|
||||
"common/wireguard",
|
||||
"common/wireguard-types",
|
||||
"documentation/autodoc",
|
||||
# "explorer-api",
|
||||
# "explorer-api/explorer-api-requests",
|
||||
# "explorer-api/explorer-client",
|
||||
"gateway",
|
||||
"integrations/bity",
|
||||
"nym-api",
|
||||
@@ -204,7 +200,7 @@ bip39 = { version = "2.0.0", features = ["zeroize"] }
|
||||
bit-vec = "0.7.0" # can we unify those?
|
||||
bitvec = "1.0.0"
|
||||
blake3 = "1.7.0"
|
||||
bloomfilter = "1.0.14"
|
||||
bloomfilter = "3.0.1"
|
||||
bs58 = "0.5.1"
|
||||
bytecodec = "0.4.15"
|
||||
bytes = "1.10.1"
|
||||
@@ -270,7 +266,6 @@ indicatif = "0.17.11"
|
||||
inquire = "0.6.2"
|
||||
ip_network = "0.4.1"
|
||||
ipnetwork = "0.20"
|
||||
isocountry = "0.3.2"
|
||||
itertools = "0.14.0"
|
||||
k256 = "0.13"
|
||||
lazy_static = "1.5.0"
|
||||
@@ -303,9 +298,6 @@ rand_seeder = "0.2.3"
|
||||
rayon = "1.5.1"
|
||||
regex = "1.10.6"
|
||||
reqwest = { version = "0.12.15", default-features = false }
|
||||
rocket = "0.5.0"
|
||||
rocket_cors = "0.6.0"
|
||||
rocket_okapi = "0.8.0"
|
||||
rs_merkle = "1.5.0"
|
||||
safer-ffi = "0.1.13"
|
||||
schemars = "0.8.22"
|
||||
@@ -320,7 +312,7 @@ serde_with = "3.9.0"
|
||||
serde_yaml = "0.9.25"
|
||||
sha2 = "0.10.8"
|
||||
si-scale = "0.2.3"
|
||||
sphinx-packet = "=0.3.2"
|
||||
sphinx-packet = "=0.6.0"
|
||||
sqlx = "0.7.4"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
@@ -410,6 +402,11 @@ wasm-bindgen-futures = "0.4.49"
|
||||
wasmtimer = "0.4.1"
|
||||
web-sys = "0.3.76"
|
||||
|
||||
|
||||
# for local development:
|
||||
#[patch.crates-io]
|
||||
#sphinx-packet = { path = "../sphinx" }
|
||||
|
||||
# Profile settings for individual crates
|
||||
|
||||
# Compile-time verified queries do quite a bit of work at compile time. Incremental
|
||||
|
||||
@@ -168,8 +168,9 @@ generate-typescript:
|
||||
cd tools/ts-rs-cli && cargo run && cd ../..
|
||||
yarn types:lint:fix
|
||||
|
||||
# Run the integration tests for public nym-api endpoints
|
||||
run-api-tests:
|
||||
cd nym-api/tests/functional_test && yarn test:qa
|
||||
dotenv -f envs/sandbox.env -- cargo test --test public-api-tests
|
||||
|
||||
# Build debian package, and update PPA
|
||||
deb-cli: build-nym-cli
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.52"
|
||||
version = "1.1.53"
|
||||
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.52"
|
||||
version = "1.1.53"
|
||||
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"
|
||||
|
||||
@@ -87,7 +87,6 @@ impl From<Init> for OverrideConfig {
|
||||
use_anonymous_replies: init_config.use_reply_surbs,
|
||||
fastmode: init_config.common_args.fastmode,
|
||||
no_cover: init_config.common_args.no_cover,
|
||||
geo_routing: None,
|
||||
medium_toggle: false,
|
||||
nyxd_urls: init_config.common_args.nyxd_urls,
|
||||
enabled_credentials_mode: init_config.common_args.enabled_credentials_mode,
|
||||
|
||||
@@ -16,8 +16,7 @@ use nym_bin_common::bin_info;
|
||||
use nym_bin_common::completions::{fig_generate, ArgShell};
|
||||
use nym_client_core::cli_helpers::CliClient;
|
||||
use nym_client_core::client::base_client::storage::migration_helpers::v1_1_33;
|
||||
use nym_client_core::client::topology_control::geo_aware_provider::CountryGroup;
|
||||
use nym_client_core::config::{ForgetMe, GroupBy, TopologyStructure};
|
||||
use nym_client_core::config::ForgetMe;
|
||||
use nym_config::OptionalSet;
|
||||
use nym_sphinx::addressing::Recipient;
|
||||
use nym_sphinx::params::{PacketSize, PacketType};
|
||||
@@ -107,7 +106,6 @@ pub(crate) struct OverrideConfig {
|
||||
use_anonymous_replies: Option<bool>,
|
||||
fastmode: bool,
|
||||
no_cover: bool,
|
||||
geo_routing: Option<CountryGroup>,
|
||||
medium_toggle: bool,
|
||||
nyxd_urls: Option<Vec<url::Url>>,
|
||||
enabled_credentials_mode: Option<bool>,
|
||||
@@ -138,21 +136,6 @@ pub(crate) fn override_config(config: Config, args: OverrideConfig) -> Config {
|
||||
let secondary_packet_size = args.medium_toggle.then_some(PacketSize::ExtendedPacket16);
|
||||
let no_per_hop_delays = args.medium_toggle;
|
||||
|
||||
let topology_structure = if args.medium_toggle {
|
||||
// Use the location of the network-requester
|
||||
let address = config
|
||||
.core
|
||||
.socks5
|
||||
.provider_mix_address
|
||||
.parse()
|
||||
.expect("failed to parse provider mix address");
|
||||
TopologyStructure::GeoAware(GroupBy::NymAddress(address))
|
||||
} else if let Some(code) = args.geo_routing {
|
||||
TopologyStructure::GeoAware(GroupBy::CountryGroup(code))
|
||||
} else {
|
||||
TopologyStructure::default()
|
||||
};
|
||||
|
||||
let packet_type = if args.outfox {
|
||||
PacketType::Outfox
|
||||
} else {
|
||||
@@ -176,10 +159,6 @@ pub(crate) fn override_config(config: Config, args: OverrideConfig) -> Config {
|
||||
// NOTE: see comment above about the order of the other disble cover traffic config
|
||||
.with_base(BaseClientConfig::with_disabled_cover_traffic, args.no_cover)
|
||||
.with_base(BaseClientConfig::with_packet_type, packet_type)
|
||||
.with_base(
|
||||
BaseClientConfig::with_topology_structure,
|
||||
topology_structure,
|
||||
)
|
||||
.with_base(BaseClientConfig::with_forget_me, args.forget_me)
|
||||
.with_optional(Config::with_anonymous_replies, args.use_anonymous_replies)
|
||||
.with_optional(Config::with_port, args.port)
|
||||
|
||||
@@ -6,7 +6,6 @@ use crate::commands::{override_config, OverrideConfig};
|
||||
use clap::Args;
|
||||
use nym_client_core::cli_helpers::client_run::CommonClientRunArgs;
|
||||
use nym_client_core::client::base_client::storage::OnDiskPersistent;
|
||||
use nym_client_core::client::topology_control::geo_aware_provider::CountryGroup;
|
||||
use nym_socks5_client_core::NymClient;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use std::net::IpAddr;
|
||||
@@ -37,10 +36,6 @@ pub(crate) struct Run {
|
||||
#[clap(long)]
|
||||
host: Option<IpAddr>,
|
||||
|
||||
/// Set geo-aware mixnode selection when sending mixnet traffic, for experiments only.
|
||||
#[clap(long, hide = true, value_parser = validate_country_group, group="routing")]
|
||||
geo_routing: Option<CountryGroup>,
|
||||
|
||||
/// Enable medium mixnet traffic, for experiments only.
|
||||
/// This includes things like disabling cover traffic, no per hop delays, etc.
|
||||
#[clap(long, hide = true)]
|
||||
@@ -59,7 +54,6 @@ impl From<Run> for OverrideConfig {
|
||||
use_anonymous_replies: run_config.use_anonymous_replies,
|
||||
fastmode: run_config.common_args.fastmode,
|
||||
no_cover: run_config.common_args.no_cover,
|
||||
geo_routing: run_config.geo_routing,
|
||||
medium_toggle: run_config.medium_toggle,
|
||||
nyxd_urls: run_config.common_args.nyxd_urls,
|
||||
enabled_credentials_mode: run_config.common_args.enabled_credentials_mode,
|
||||
@@ -70,13 +64,6 @@ impl From<Run> for OverrideConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_country_group(s: &str) -> Result<CountryGroup, String> {
|
||||
match s.parse() {
|
||||
Ok(cg) => Ok(cg),
|
||||
Err(_) => Err(format!("failed to parse country group: {}", s)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn execute(args: Run) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
eprintln!("Starting client {}...", args.common_args.id);
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ license.workspace = true
|
||||
async-trait = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
bs58 = { workspace = true }
|
||||
cfg-if = { workspace = true }
|
||||
clap = { workspace = true, optional = true }
|
||||
comfy-table = { workspace = true, optional = true }
|
||||
futures = { workspace = true }
|
||||
@@ -24,7 +23,6 @@ serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
si-scale = { workspace = true }
|
||||
tap = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
tokio = { workspace = true, features = ["macros"] }
|
||||
@@ -35,9 +33,7 @@ zeroize = { workspace = true }
|
||||
nym-id = { path = "../nym-id" }
|
||||
nym-bandwidth-controller = { path = "../bandwidth-controller" }
|
||||
nym-config = { path = "../config" }
|
||||
nym-country-group = { path = "../country-group" }
|
||||
nym-crypto = { path = "../crypto" }
|
||||
nym-explorer-client = { path = "../../explorer-api/explorer-client" }
|
||||
nym-gateway-client = { path = "../client-libs/gateway-client" }
|
||||
nym-gateway-requests = { path = "../gateway-requests" }
|
||||
nym-http-api-client = { path = "../http-api-client" }
|
||||
|
||||
@@ -14,7 +14,6 @@ url = { workspace = true, features = ["serde"] }
|
||||
|
||||
nym-config = { path = "../../config" }
|
||||
|
||||
nym-country-group = { path = "../../country-group" }
|
||||
nym-pemstore = { path = "../../pemstore", optional = true }
|
||||
|
||||
# those are pulling so many deps T.T
|
||||
|
||||
@@ -65,11 +65,10 @@ const DEFAULT_MAXIMUM_REPLY_KEY_AGE: Duration = Duration::from_secs(24 * 60 * 60
|
||||
|
||||
// stats reporting related
|
||||
|
||||
/// Time interval between reporting statistics to the given provider if it exist
|
||||
/// Time interval between reporting statistics to the given provider if it exists
|
||||
const STATS_REPORT_INTERVAL_SECS: Duration = Duration::from_secs(300);
|
||||
|
||||
use crate::error::InvalidTrafficModeFailure;
|
||||
pub use nym_country_group::CountryGroup;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -258,15 +257,6 @@ impl Config {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_topology_structure(mut self, topology_structure: TopologyStructure) -> Self {
|
||||
self.set_topology_structure(topology_structure);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_topology_structure(&mut self, topology_structure: TopologyStructure) {
|
||||
self.debug.topology.topology_structure = topology_structure;
|
||||
}
|
||||
|
||||
pub fn with_no_per_hop_delays(mut self, no_per_hop_delays: bool) -> Self {
|
||||
if no_per_hop_delays {
|
||||
self.set_no_per_hop_delays()
|
||||
@@ -415,6 +405,14 @@ pub struct Traffic {
|
||||
/// Do not set it unless you understand the consequences of that change.
|
||||
pub secondary_packet_size: Option<PacketSize>,
|
||||
|
||||
/// Specify whether any constructed sphinx packets should use the legacy format,
|
||||
/// where the payload keys are explicitly attached rather than using the seeds
|
||||
/// this affects any forward packets, acks and reply surbs
|
||||
/// this flag should remain disabled until sufficient number of nodes on the network has upgraded
|
||||
/// and support updated format.
|
||||
/// in the case of reply surbs, the recipient must also understand the new encoding
|
||||
pub use_legacy_sphinx_format: bool,
|
||||
|
||||
pub packet_type: PacketType,
|
||||
}
|
||||
|
||||
@@ -442,6 +440,10 @@ impl Default for Traffic {
|
||||
primary_packet_size: PacketSize::RegularPacket,
|
||||
secondary_packet_size: None,
|
||||
packet_type: PacketType::Mix,
|
||||
|
||||
// we should use the legacy format until sufficient number of nodes understand the
|
||||
// improved encoding
|
||||
use_legacy_sphinx_format: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -546,9 +548,6 @@ pub struct Topology {
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub max_startup_gateway_waiting_period: Duration,
|
||||
|
||||
/// Specifies the mixnode topology to be used for sending packets.
|
||||
pub topology_structure: TopologyStructure,
|
||||
|
||||
/// Specifies a minimum performance of a mixnode that is used on route construction.
|
||||
/// This setting is only applicable when `NymApi` topology is used.
|
||||
pub minimum_mixnode_performance: u8,
|
||||
@@ -570,30 +569,6 @@ pub struct Topology {
|
||||
pub ignore_ingress_epoch_role: bool,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Default, Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum TopologyStructure {
|
||||
#[default]
|
||||
NymApi,
|
||||
GeoAware(GroupBy),
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum GroupBy {
|
||||
CountryGroup(CountryGroup),
|
||||
NymAddress(Recipient),
|
||||
}
|
||||
|
||||
impl std::fmt::Display for GroupBy {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
GroupBy::CountryGroup(group) => write!(f, "group: {group}"),
|
||||
GroupBy::NymAddress(address) => write!(f, "address: {address}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Topology {
|
||||
fn default() -> Self {
|
||||
Topology {
|
||||
@@ -601,7 +576,6 @@ impl Default for Topology {
|
||||
topology_resolution_timeout: DEFAULT_TOPOLOGY_RESOLUTION_TIMEOUT,
|
||||
disable_refreshing: false,
|
||||
max_startup_gateway_waiting_period: DEFAULT_MAX_STARTUP_GATEWAY_WAITING_PERIOD,
|
||||
topology_structure: TopologyStructure::default(),
|
||||
minimum_mixnode_performance: DEFAULT_MIN_MIXNODE_PERFORMANCE,
|
||||
minimum_gateway_performance: DEFAULT_MIN_GATEWAY_PERFORMANCE,
|
||||
use_extended_topology: false,
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::old::v5::{
|
||||
AcknowledgementsV5, ClientV5, ConfigV5, CoverTrafficV5, DebugConfigV5, GatewayConnectionV5,
|
||||
GroupByV5, ReplySurbsV5, TopologyStructureV5, TopologyV5, TrafficV5,
|
||||
AcknowledgementsV5, ClientV5, ConfigV5, CountryGroupV5, CoverTrafficV5, DebugConfigV5,
|
||||
GatewayConnectionV5, GroupByV5, ReplySurbsV5, TopologyStructureV5, TopologyV5, TrafficV5,
|
||||
};
|
||||
use crate::CountryGroup;
|
||||
use nym_sphinx_addressing::Recipient;
|
||||
use nym_sphinx_params::{PacketSize, PacketType};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -369,31 +368,47 @@ impl From<TopologyStructureV4> for TopologyStructureV5 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
pub enum CountryGroupV4 {
|
||||
Europe,
|
||||
NorthAmerica,
|
||||
SouthAmerica,
|
||||
Oceania,
|
||||
Asia,
|
||||
Africa,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<CountryGroupV4> for CountryGroupV5 {
|
||||
fn from(value: CountryGroupV4) -> Self {
|
||||
match value {
|
||||
CountryGroupV4::Europe => CountryGroupV5::Europe,
|
||||
CountryGroupV4::NorthAmerica => CountryGroupV5::NorthAmerica,
|
||||
CountryGroupV4::SouthAmerica => CountryGroupV5::SouthAmerica,
|
||||
CountryGroupV4::Oceania => CountryGroupV5::Oceania,
|
||||
CountryGroupV4::Asia => CountryGroupV5::Asia,
|
||||
CountryGroupV4::Africa => CountryGroupV5::Africa,
|
||||
CountryGroupV4::Unknown => CountryGroupV5::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum GroupByV4 {
|
||||
CountryGroup(CountryGroup),
|
||||
CountryGroup(CountryGroupV4),
|
||||
NymAddress(Recipient),
|
||||
}
|
||||
|
||||
impl From<GroupByV4> for GroupByV5 {
|
||||
fn from(value: GroupByV4) -> Self {
|
||||
match value {
|
||||
GroupByV4::CountryGroup(country) => GroupByV5::CountryGroup(country),
|
||||
GroupByV4::CountryGroup(country) => GroupByV5::CountryGroup(country.into()),
|
||||
GroupByV4::NymAddress(addr) => GroupByV5::NymAddress(addr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for GroupByV4 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
GroupByV4::CountryGroup(group) => write!(f, "group: {}", group),
|
||||
GroupByV4::NymAddress(address) => write!(f, "address: {}", address),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TopologyV4 {
|
||||
fn default() -> Self {
|
||||
TopologyV4 {
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{
|
||||
Acknowledgements, Client, Config, CountryGroup, CoverTraffic, DebugConfig, GatewayConnection,
|
||||
GroupBy, ReplySurbs, Topology, TopologyStructure, Traffic,
|
||||
Acknowledgements, Client, Config, CoverTraffic, DebugConfig, GatewayConnection, ReplySurbs,
|
||||
Topology, Traffic,
|
||||
};
|
||||
use nym_sphinx_addressing::Recipient;
|
||||
use nym_sphinx_params::{PacketSize, PacketType};
|
||||
@@ -146,7 +146,6 @@ impl From<ConfigV5> for Config {
|
||||
.debug
|
||||
.topology
|
||||
.max_startup_gateway_waiting_period,
|
||||
topology_structure: value.debug.topology.topology_structure.into(),
|
||||
..Default::default()
|
||||
},
|
||||
reply_surbs: ReplySurbs {
|
||||
@@ -372,40 +371,24 @@ pub enum TopologyStructureV5 {
|
||||
GeoAware(GroupByV5),
|
||||
}
|
||||
|
||||
impl From<TopologyStructureV5> for TopologyStructure {
|
||||
fn from(value: TopologyStructureV5) -> Self {
|
||||
match value {
|
||||
TopologyStructureV5::NymApi => TopologyStructure::NymApi,
|
||||
TopologyStructureV5::GeoAware(group_by) => TopologyStructure::GeoAware(group_by.into()),
|
||||
}
|
||||
}
|
||||
#[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
pub enum CountryGroupV5 {
|
||||
Europe,
|
||||
NorthAmerica,
|
||||
SouthAmerica,
|
||||
Oceania,
|
||||
Asia,
|
||||
Africa,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub enum GroupByV5 {
|
||||
CountryGroup(CountryGroup),
|
||||
CountryGroup(CountryGroupV5),
|
||||
NymAddress(Recipient),
|
||||
}
|
||||
|
||||
impl From<GroupByV5> for GroupBy {
|
||||
fn from(value: GroupByV5) -> Self {
|
||||
match value {
|
||||
GroupByV5::CountryGroup(country) => GroupBy::CountryGroup(country),
|
||||
GroupByV5::NymAddress(addr) => GroupBy::NymAddress(addr),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for GroupByV5 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
GroupByV5::CountryGroup(group) => write!(f, "group: {}", group),
|
||||
GroupByV5::NymAddress(address) => write!(f, "address: {}", address),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TopologyV5 {
|
||||
fn default() -> Self {
|
||||
TopologyV5 {
|
||||
|
||||
@@ -552,18 +552,12 @@ where
|
||||
user_agent: Option<UserAgent>,
|
||||
) -> Box<dyn TopologyProvider + Send + Sync> {
|
||||
// if no custom provider was ... provided ..., create one using nym-api
|
||||
custom_provider.unwrap_or_else(|| match config_topology.topology_structure {
|
||||
config::TopologyStructure::NymApi => Box::new(NymApiTopologyProvider::new(
|
||||
custom_provider.unwrap_or_else(|| {
|
||||
Box::new(NymApiTopologyProvider::new(
|
||||
config_topology,
|
||||
nym_api_urls,
|
||||
user_agent,
|
||||
)),
|
||||
config::TopologyStructure::GeoAware(group_by) => {
|
||||
warn!("using deprecated 'GeoAware' topology provider - this option will be removed very soon");
|
||||
|
||||
#[allow(deprecated)]
|
||||
Box::new(crate::client::topology_control::GeoAwareTopologyProvider::new(nym_api_urls, group_by))
|
||||
}
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,10 @@ where
|
||||
/// Optional secondary predefined packet size used for the loop cover messages.
|
||||
secondary_packet_size: Option<PacketSize>,
|
||||
|
||||
/// Specify whether any constructed packets should use the legacy format,
|
||||
/// where the payload keys are explicitly attached rather than using the seeds
|
||||
use_legacy_sphinx_format: bool,
|
||||
|
||||
packet_type: PacketType,
|
||||
|
||||
stats_tx: ClientStatsSender,
|
||||
@@ -130,6 +134,7 @@ impl LoopCoverTrafficStream<OsRng> {
|
||||
topology_access,
|
||||
primary_packet_size: traffic_config.primary_packet_size,
|
||||
secondary_packet_size: traffic_config.secondary_packet_size,
|
||||
use_legacy_sphinx_format: traffic_config.use_legacy_sphinx_format,
|
||||
packet_type: traffic_config.packet_type,
|
||||
stats_tx,
|
||||
task_client,
|
||||
@@ -182,6 +187,7 @@ impl LoopCoverTrafficStream<OsRng> {
|
||||
|
||||
let cover_message = match generate_loop_cover_packet(
|
||||
&mut self.rng,
|
||||
self.use_legacy_sphinx_format,
|
||||
topology_ref,
|
||||
&self.ack_key,
|
||||
&self.our_full_destination,
|
||||
|
||||
@@ -109,6 +109,10 @@ pub(crate) struct Config {
|
||||
|
||||
/// Optional secondary predefined packet size used for the encapsulated messages.
|
||||
secondary_packet_size: Option<PacketSize>,
|
||||
|
||||
/// Specify whether any constructed reply surbs should use the legacy format,
|
||||
/// where the payload keys are explicitly attached rather than using the seeds
|
||||
use_legacy_sphinx_format: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -118,6 +122,7 @@ impl Config {
|
||||
average_packet_delay: Duration,
|
||||
average_ack_delay: Duration,
|
||||
deterministic_route_selection: bool,
|
||||
use_legacy_reply_surb_format: bool,
|
||||
) -> Self {
|
||||
Config {
|
||||
ack_key,
|
||||
@@ -127,6 +132,7 @@ impl Config {
|
||||
average_ack_delay,
|
||||
primary_packet_size: PacketSize::default(),
|
||||
secondary_packet_size: None,
|
||||
use_legacy_sphinx_format: use_legacy_reply_surb_format,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,6 +192,7 @@ where
|
||||
config.sender_address,
|
||||
config.average_packet_delay,
|
||||
config.average_ack_delay,
|
||||
config.use_legacy_sphinx_format,
|
||||
);
|
||||
MessageHandler {
|
||||
config,
|
||||
@@ -254,9 +261,11 @@ where
|
||||
let topology_permit = self.topology_access.get_read_permit().await;
|
||||
let topology = self.get_topology(&topology_permit)?;
|
||||
|
||||
let reply_surbs = self
|
||||
.message_preparer
|
||||
.generate_reply_surbs(amount, topology)?;
|
||||
let reply_surbs = self.message_preparer.generate_reply_surbs(
|
||||
self.config.use_legacy_sphinx_format,
|
||||
amount,
|
||||
topology,
|
||||
)?;
|
||||
|
||||
let reply_keys = reply_surbs
|
||||
.iter()
|
||||
@@ -522,6 +531,7 @@ where
|
||||
self.generate_reply_surbs_with_keys(amount as usize).await?;
|
||||
|
||||
let message = NymMessage::new_repliable(RepliableMessage::new_additional_surbs(
|
||||
self.config.use_legacy_sphinx_format,
|
||||
sender_tag,
|
||||
reply_surbs,
|
||||
));
|
||||
@@ -559,8 +569,12 @@ where
|
||||
.generate_reply_surbs_with_keys(num_reply_surbs as usize)
|
||||
.await?;
|
||||
|
||||
let message =
|
||||
NymMessage::new_repliable(RepliableMessage::new_data(message, sender_tag, reply_surbs));
|
||||
let message = NymMessage::new_repliable(RepliableMessage::new_data(
|
||||
self.config.use_legacy_sphinx_format,
|
||||
message,
|
||||
sender_tag,
|
||||
reply_surbs,
|
||||
));
|
||||
|
||||
self.try_split_and_send_non_reply_message(
|
||||
message,
|
||||
|
||||
@@ -99,6 +99,7 @@ impl<'a> From<&'a Config> for message_handler::Config {
|
||||
cfg.traffic.average_packet_delay,
|
||||
cfg.acks.average_ack_delay,
|
||||
cfg.traffic.deterministic_route_selection,
|
||||
cfg.traffic.use_legacy_sphinx_format,
|
||||
)
|
||||
.with_custom_primary_packet_size(cfg.traffic.primary_packet_size)
|
||||
.with_custom_secondary_packet_size(cfg.traffic.secondary_packet_size)
|
||||
|
||||
@@ -252,6 +252,7 @@ where
|
||||
(
|
||||
generate_loop_cover_packet(
|
||||
&mut self.rng,
|
||||
self.config.traffic.use_legacy_sphinx_format,
|
||||
topology_ref,
|
||||
&self.config.ack_key,
|
||||
&self.config.our_full_destination,
|
||||
|
||||
@@ -250,10 +250,10 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
|
||||
let mut reconstructed = Vec::new();
|
||||
for msg in msgs {
|
||||
let (reply_surbs, from_surb_request) = match msg.content {
|
||||
RepliableMessageContent::Data {
|
||||
message,
|
||||
reply_surbs,
|
||||
} => {
|
||||
RepliableMessageContent::Data(content) => {
|
||||
let reply_surbs = content.reply_surbs;
|
||||
let message = content.message;
|
||||
|
||||
trace!(
|
||||
"received message that also contained additional {} reply surbs from {:?}!",
|
||||
reply_surbs.len(),
|
||||
@@ -264,7 +264,9 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
|
||||
|
||||
(reply_surbs, false)
|
||||
}
|
||||
RepliableMessageContent::AdditionalSurbs { reply_surbs } => {
|
||||
RepliableMessageContent::AdditionalSurbs(content) => {
|
||||
let reply_surbs = content.reply_surbs;
|
||||
|
||||
trace!(
|
||||
"received additional {} reply surbs from {:?}!",
|
||||
reply_surbs.len(),
|
||||
@@ -272,9 +274,37 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
|
||||
);
|
||||
(reply_surbs, true)
|
||||
}
|
||||
RepliableMessageContent::Heartbeat {
|
||||
additional_reply_surbs,
|
||||
} => {
|
||||
RepliableMessageContent::Heartbeat(content) => {
|
||||
let additional_reply_surbs = content.additional_reply_surbs;
|
||||
error!("received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)");
|
||||
(additional_reply_surbs, false)
|
||||
}
|
||||
RepliableMessageContent::DataV2(content) => {
|
||||
let reply_surbs = content.reply_surbs;
|
||||
let message = content.message;
|
||||
|
||||
trace!(
|
||||
"received message that also contained additional {} reply surbs from {:?}!",
|
||||
reply_surbs.len(),
|
||||
msg.sender_tag
|
||||
);
|
||||
|
||||
reconstructed.push(ReconstructedMessage::new(message, msg.sender_tag));
|
||||
|
||||
(reply_surbs, false)
|
||||
}
|
||||
RepliableMessageContent::AdditionalSurbsV2(content) => {
|
||||
let reply_surbs = content.reply_surbs;
|
||||
|
||||
trace!(
|
||||
"received additional {} reply surbs from {:?}!",
|
||||
reply_surbs.len(),
|
||||
msg.sender_tag
|
||||
);
|
||||
(reply_surbs, true)
|
||||
}
|
||||
RepliableMessageContent::HeartbeatV2(content) => {
|
||||
let additional_reply_surbs = content.additional_reply_surbs;
|
||||
error!("received a repliable heartbeat message - we don't know how to handle it yet (and we won't know until future PRs)");
|
||||
(additional_reply_surbs, false)
|
||||
}
|
||||
|
||||
@@ -1,214 +0,0 @@
|
||||
use crate::config::GroupBy;
|
||||
use log::{debug, error};
|
||||
use nym_explorer_client::{ExplorerClient, PrettyDetailedMixNodeBond};
|
||||
use nym_network_defaults::var_names::EXPLORER_API;
|
||||
use nym_topology::{
|
||||
provider_trait::{async_trait, TopologyProvider},
|
||||
NymTopology,
|
||||
};
|
||||
use nym_validator_client::client::NodeId;
|
||||
use rand::{prelude::SliceRandom, thread_rng};
|
||||
use std::collections::HashMap;
|
||||
use tap::TapOptional;
|
||||
use url::Url;
|
||||
|
||||
pub use nym_country_group::CountryGroup;
|
||||
|
||||
fn create_explorer_client() -> Option<ExplorerClient> {
|
||||
let Ok(explorer_api_url) = std::env::var(EXPLORER_API) else {
|
||||
error!("Missing EXPLORER_API");
|
||||
return None;
|
||||
};
|
||||
|
||||
let Ok(explorer_api_url) = explorer_api_url.parse() else {
|
||||
error!("Failed to parse EXPLORER_API");
|
||||
return None;
|
||||
};
|
||||
|
||||
log::debug!("Using explorer-api url: {}", explorer_api_url);
|
||||
let Ok(client) = nym_explorer_client::ExplorerClient::new(explorer_api_url) else {
|
||||
error!("Failed to create explorer-api client");
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(client)
|
||||
}
|
||||
|
||||
fn group_mixnodes_by_country_code(
|
||||
mixnodes: Vec<PrettyDetailedMixNodeBond>,
|
||||
) -> HashMap<CountryGroup, Vec<NodeId>> {
|
||||
mixnodes
|
||||
.into_iter()
|
||||
.fold(HashMap::<CountryGroup, Vec<NodeId>>::new(), |mut acc, m| {
|
||||
if let Some(ref location) = m.location {
|
||||
let country_code = location.two_letter_iso_country_code.clone();
|
||||
let group_code = CountryGroup::new(country_code.as_str());
|
||||
let mixnodes = acc.entry(group_code).or_default();
|
||||
mixnodes.push(m.mix_id);
|
||||
}
|
||||
acc
|
||||
})
|
||||
}
|
||||
|
||||
fn log_mixnode_distribution(mixnodes: &HashMap<CountryGroup, Vec<NodeId>>) {
|
||||
let mixnode_distribution = mixnodes
|
||||
.iter()
|
||||
.map(|(k, v)| format!("{}: {}", k, v.len()))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
debug!("Mixnode distribution - {}", mixnode_distribution);
|
||||
}
|
||||
|
||||
fn check_layer_integrity(topology: NymTopology) -> Result<(), ()> {
|
||||
if topology.ensure_minimally_routable().is_err() {
|
||||
error!("Layer is missing in topology!");
|
||||
return Err(());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[deprecated(note = "use NymApiTopologyProvider instead as explorer API will soon be removed")]
|
||||
pub struct GeoAwareTopologyProvider {
|
||||
validator_client: nym_validator_client::client::NymApiClient,
|
||||
filter_on: GroupBy,
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
impl GeoAwareTopologyProvider {
|
||||
pub fn new(mut nym_api_urls: Vec<Url>, filter_on: GroupBy) -> GeoAwareTopologyProvider {
|
||||
log::info!(
|
||||
"Creating geo-aware topology provider with filter on {}",
|
||||
filter_on
|
||||
);
|
||||
nym_api_urls.shuffle(&mut thread_rng());
|
||||
|
||||
GeoAwareTopologyProvider {
|
||||
validator_client: nym_validator_client::client::NymApiClient::new(
|
||||
nym_api_urls[0].clone(),
|
||||
),
|
||||
filter_on,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_topology(&self) -> Option<NymTopology> {
|
||||
let rewarded_set = self
|
||||
.validator_client
|
||||
.get_current_rewarded_set()
|
||||
.await
|
||||
.inspect_err(|err| error!("failed to get current rewarded set: {err}"))
|
||||
.ok()?;
|
||||
|
||||
let mut topology = NymTopology::new_empty(rewarded_set);
|
||||
|
||||
let mixnodes = match self
|
||||
.validator_client
|
||||
.get_all_basic_active_mixing_assigned_nodes()
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
error!("failed to get network mixnodes - {err}");
|
||||
return None;
|
||||
}
|
||||
Ok(mixes) => mixes,
|
||||
};
|
||||
|
||||
let gateways = match self
|
||||
.validator_client
|
||||
.get_all_basic_entry_assigned_nodes()
|
||||
.await
|
||||
{
|
||||
Err(err) => {
|
||||
error!("failed to get network gateways - {err}");
|
||||
return None;
|
||||
}
|
||||
Ok(gateways) => gateways,
|
||||
};
|
||||
|
||||
// Also fetch mixnodes cached by explorer-api, with the purpose of getting their
|
||||
// geolocation.
|
||||
debug!("Fetching mixnodes from explorer-api...");
|
||||
let explorer_client = create_explorer_client()?;
|
||||
let Ok(mixnodes_from_explorer_api) = explorer_client.get_mixnodes().await else {
|
||||
error!("failed to get mixnodes from explorer-api");
|
||||
return None;
|
||||
};
|
||||
|
||||
debug!("Fetching gateways from explorer-api...");
|
||||
let Ok(gateways_from_explorer_api) = explorer_client.get_gateways().await else {
|
||||
error!("failed to get mixnodes from explorer-api");
|
||||
return None;
|
||||
};
|
||||
|
||||
// Determine what we should filter around
|
||||
let filter_on = match self.filter_on {
|
||||
GroupBy::CountryGroup(group) => group,
|
||||
GroupBy::NymAddress(recipient) => {
|
||||
// Convert recipient into a country group by extracting out the gateway part and
|
||||
// using that as the country code.
|
||||
let gateway = recipient.gateway().to_base58_string();
|
||||
|
||||
// Lookup the location of this gateway by using the location data from the
|
||||
// explorer-api
|
||||
let gateway_location = gateways_from_explorer_api
|
||||
.iter()
|
||||
.find(|g| g.gateway.identity_key == gateway)
|
||||
.and_then(|g| g.location.clone())
|
||||
.map(|location| location.two_letter_iso_country_code)
|
||||
.tap_none(|| error!("No location found for the gateway: {}", gateway))?;
|
||||
debug!(
|
||||
"Filtering on nym-address: {}, with location: {}",
|
||||
recipient, gateway_location
|
||||
);
|
||||
|
||||
CountryGroup::new(&gateway_location)
|
||||
}
|
||||
};
|
||||
debug!("Filter group: {}", filter_on);
|
||||
|
||||
// Partition mixnodes_from_explorer_api according to the value of
|
||||
// two_letter_iso_country_code.
|
||||
// NOTE: we construct the full distribution here, but only use the one we're interested in.
|
||||
// The reason we this instead of a straight filter is that this opens up the possibility to
|
||||
// complement a small grouping with mixnodes from adjecent countries.
|
||||
let mixnode_distribution = group_mixnodes_by_country_code(mixnodes_from_explorer_api);
|
||||
log_mixnode_distribution(&mixnode_distribution);
|
||||
|
||||
let Some(filtered_mixnode_ids) = mixnode_distribution.get(&filter_on) else {
|
||||
error!("no mixnodes found for: {}", filter_on);
|
||||
return None;
|
||||
};
|
||||
|
||||
let mixnodes = mixnodes
|
||||
.into_iter()
|
||||
.filter(|m| filtered_mixnode_ids.contains(&m.node_id))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
topology.add_skimmed_nodes(&mixnodes);
|
||||
topology.add_skimmed_nodes(&gateways);
|
||||
|
||||
// TODO: return real error type
|
||||
check_layer_integrity(topology.clone()).ok()?;
|
||||
|
||||
Some(topology)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[async_trait]
|
||||
impl TopologyProvider for GeoAwareTopologyProvider {
|
||||
// this will be manually refreshed on a timer specified inside mixnet client config
|
||||
async fn get_new_topology(&mut self) -> Option<NymTopology> {
|
||||
self.get_topology().await
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
#[async_trait(?Send)]
|
||||
impl TopologyProvider for GeoAwareTopologyProvider {
|
||||
// this will be manually refreshed on a timer specified inside mixnet client config
|
||||
async fn get_new_topology(&mut self) -> Option<NymTopology> {
|
||||
self.get_topology().await
|
||||
}
|
||||
}
|
||||
@@ -17,11 +17,8 @@ use tokio::time::sleep;
|
||||
use wasmtimer::tokio::sleep;
|
||||
|
||||
mod accessor;
|
||||
pub mod geo_aware_provider;
|
||||
pub mod nym_api_provider;
|
||||
|
||||
#[allow(deprecated)]
|
||||
pub use geo_aware_provider::GeoAwareTopologyProvider;
|
||||
pub use nym_api_provider::{Config as NymApiTopologyProviderConfig, NymApiTopologyProvider};
|
||||
pub use nym_topology::provider_trait::TopologyProvider;
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "nym-country-group"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
tracing.workspace = true
|
||||
@@ -1,158 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use tracing::info;
|
||||
|
||||
#[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Debug)]
|
||||
pub enum CountryGroup {
|
||||
Europe,
|
||||
NorthAmerica,
|
||||
SouthAmerica,
|
||||
Oceania,
|
||||
Asia,
|
||||
Africa,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl CountryGroup {
|
||||
// We map country codes into group, which initially are continent codes to a first approximation,
|
||||
// but we do it manually to reserve the right to tweak this distribution for our purposes.
|
||||
// NOTE: I did this quickly, and it's not a complete list of all countries, but only those that
|
||||
// were present in the network at the time. Please add more as needed.
|
||||
pub fn new(country_code: &str) -> Self {
|
||||
let country_code = country_code.to_uppercase();
|
||||
use CountryGroup::*;
|
||||
match country_code.as_ref() {
|
||||
// Europe
|
||||
"AT" => Europe,
|
||||
"BG" => Europe,
|
||||
"CH" => Europe,
|
||||
"CY" => Europe,
|
||||
"CZ" => Europe,
|
||||
"DE" => Europe,
|
||||
"DK" => Europe,
|
||||
"ES" => Europe,
|
||||
"FI" => Europe,
|
||||
"FR" => Europe,
|
||||
"GB" => Europe,
|
||||
"GR" => Europe,
|
||||
"IE" => Europe,
|
||||
"IT" => Europe,
|
||||
"LT" => Europe,
|
||||
"LU" => Europe,
|
||||
"LV" => Europe,
|
||||
"MD" => Europe,
|
||||
"MT" => Europe,
|
||||
"NL" => Europe,
|
||||
"NO" => Europe,
|
||||
"PL" => Europe,
|
||||
"RO" => Europe,
|
||||
"SE" => Europe,
|
||||
"SK" => Europe,
|
||||
"TR" => Europe,
|
||||
"UA" => Europe,
|
||||
|
||||
// North America
|
||||
"CA" => NorthAmerica,
|
||||
"MX" => NorthAmerica,
|
||||
"US" => NorthAmerica,
|
||||
|
||||
// South America
|
||||
"AR" => SouthAmerica,
|
||||
"BR" => SouthAmerica,
|
||||
"CL" => SouthAmerica,
|
||||
"CO" => SouthAmerica,
|
||||
"CR" => SouthAmerica,
|
||||
"GT" => SouthAmerica,
|
||||
|
||||
// Oceania
|
||||
"AU" => Oceania,
|
||||
|
||||
// Asia
|
||||
"AM" => Asia,
|
||||
"BH" => Asia,
|
||||
"CN" => Asia,
|
||||
"GE" => Asia,
|
||||
"HK" => Asia,
|
||||
"ID" => Asia,
|
||||
"IL" => Asia,
|
||||
"IN" => Asia,
|
||||
"JP" => Asia,
|
||||
"KH" => Asia,
|
||||
"KR" => Asia,
|
||||
"KZ" => Asia,
|
||||
"MY" => Asia,
|
||||
"RU" => Asia,
|
||||
"SG" => Asia,
|
||||
"TH" => Asia,
|
||||
"VN" => Asia,
|
||||
|
||||
// Africa
|
||||
"SC" => Africa,
|
||||
"UG" => Africa,
|
||||
"ZA" => Africa,
|
||||
|
||||
// And group level codes work too
|
||||
"EU" => Europe,
|
||||
"NA" => NorthAmerica,
|
||||
"SA" => SouthAmerica,
|
||||
"OC" => Oceania,
|
||||
"AS" => Asia,
|
||||
"AF" => Africa,
|
||||
|
||||
// And some aliases
|
||||
"EUROPE" => Europe,
|
||||
"NORTHAMERICA" => NorthAmerica,
|
||||
"SOUTHAMERICA" => SouthAmerica,
|
||||
"OCEANIA" => Oceania,
|
||||
"ASIA" => Asia,
|
||||
"AFRICA" => Africa,
|
||||
|
||||
_ => {
|
||||
info!("Unknown country code: {country_code}");
|
||||
Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CountryGroup {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
use CountryGroup::*;
|
||||
match self {
|
||||
Europe => write!(f, "EU"),
|
||||
NorthAmerica => write!(f, "NA"),
|
||||
SouthAmerica => write!(f, "SA"),
|
||||
Oceania => write!(f, "OC"),
|
||||
Asia => write!(f, "AS"),
|
||||
Africa => write!(f, "AF"),
|
||||
Unknown => write!(f, "Unknown"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::str::FromStr for CountryGroup {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let group = CountryGroup::new(s);
|
||||
if group == CountryGroup::Unknown {
|
||||
Err(())
|
||||
} else {
|
||||
Ok(group)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CountryGroup {
|
||||
#[allow(unused)]
|
||||
fn known(self) -> Option<CountryGroup> {
|
||||
use CountryGroup::*;
|
||||
match self {
|
||||
Europe | NorthAmerica | SouthAmerica | Oceania | Asia | Africa => Some(self),
|
||||
Unknown => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,6 +172,21 @@ impl MemoryEcachTicketbookManager {
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) async fn contains_ticketbook(&self, ticketbook: &IssuedTicketBook) -> bool {
|
||||
let ser = ticketbook.pack();
|
||||
let search_data = Zeroizing::new(ser.data);
|
||||
self.inner
|
||||
.read()
|
||||
.await
|
||||
.ticketbooks
|
||||
.iter()
|
||||
.any(|ticketbook| {
|
||||
let ser = ticketbook.1.ticketbook.pack();
|
||||
let data = Zeroizing::new(ser.data);
|
||||
search_data.eq(&data)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn get_ticketbooks_info(&self) -> Vec<BasicTicketbookInformation> {
|
||||
let guard = self.inner.read().await;
|
||||
|
||||
|
||||
@@ -95,6 +95,24 @@ impl SqliteEcashTicketbookManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn contains_ticketbook_data(&self, data: &[u8]) -> Result<bool, sqlx::Error> {
|
||||
let exists = sqlx::query(
|
||||
r#"
|
||||
SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM ecash_ticketbook
|
||||
WHERE ticketbook_data = ?
|
||||
)
|
||||
"#,
|
||||
)
|
||||
.bind(data)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await?
|
||||
.is_some();
|
||||
|
||||
Ok(exists)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_ticketbooks_info(
|
||||
&self,
|
||||
) -> Result<Vec<BasicTicketbookInformation>, sqlx::Error> {
|
||||
|
||||
@@ -70,6 +70,13 @@ impl Storage for EphemeralStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn contains_issued_ticketbook(
|
||||
&self,
|
||||
ticketbook: &IssuedTicketBook,
|
||||
) -> Result<bool, StorageError> {
|
||||
Ok(self.storage_manager.contains_ticketbook(ticketbook).await)
|
||||
}
|
||||
|
||||
async fn get_ticketbooks_info(
|
||||
&self,
|
||||
) -> Result<Vec<BasicTicketbookInformation>, Self::StorageError> {
|
||||
|
||||
@@ -145,6 +145,16 @@ impl Storage for PersistentStorage {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn contains_issued_ticketbook(
|
||||
&self,
|
||||
ticketbook: &IssuedTicketBook,
|
||||
) -> Result<bool, Self::StorageError> {
|
||||
let ser = ticketbook.pack();
|
||||
let data = Zeroizing::new(ser.data);
|
||||
|
||||
Ok(self.storage_manager.contains_ticketbook_data(&data).await?)
|
||||
}
|
||||
|
||||
async fn get_ticketbooks_info(
|
||||
&self,
|
||||
) -> Result<Vec<BasicTicketbookInformation>, Self::StorageError> {
|
||||
|
||||
@@ -37,6 +37,11 @@ pub trait Storage: Clone + Send + Sync {
|
||||
ticketbook: &IssuedTicketBook,
|
||||
) -> Result<(), Self::StorageError>;
|
||||
|
||||
async fn contains_issued_ticketbook(
|
||||
&self,
|
||||
ticketbook: &IssuedTicketBook,
|
||||
) -> Result<bool, Self::StorageError>;
|
||||
|
||||
async fn get_ticketbooks_info(
|
||||
&self,
|
||||
) -> Result<Vec<BasicTicketbookInformation>, Self::StorageError>;
|
||||
|
||||
@@ -10,9 +10,6 @@ pub enum MixProcessingError {
|
||||
#[error("failed to recover the expected SURB-Ack packet: {0}")]
|
||||
MalformedSurbAck(#[from] SurbAckRecoveryError),
|
||||
|
||||
#[error("the received packet was set to use the very old and very much deprecated 'VPN' mode")]
|
||||
ReceivedOldTypeVpnPacket,
|
||||
|
||||
#[error("failed to process received Nym packet: {0}")]
|
||||
NymPacketProcessingError(#[from] PacketProcessingError),
|
||||
}
|
||||
|
||||
@@ -37,32 +37,3 @@ impl TicketTypeRepr {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Constants for bloom filter for double spending detection
|
||||
//Chosen for FP of
|
||||
//Calculator at https://hur.st/bloomfilter/
|
||||
pub const ECASH_DS_BLOOMFILTER_PARAMS: BloomfilterParameters = BloomfilterParameters {
|
||||
num_hashes: 10,
|
||||
bitmap_size: 1_500_000_000,
|
||||
sip_keys: [
|
||||
(12345678910111213141, 1415926535897932384),
|
||||
(7182818284590452353, 3571113171923293137),
|
||||
],
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct BloomfilterParameters {
|
||||
pub num_hashes: u32,
|
||||
pub bitmap_size: u64,
|
||||
pub sip_keys: [(u64, u64); 2],
|
||||
}
|
||||
|
||||
impl BloomfilterParameters {
|
||||
pub const fn byte_size(&self) -> u64 {
|
||||
self.bitmap_size / 8
|
||||
}
|
||||
|
||||
pub const fn default_ecash() -> Self {
|
||||
ECASH_DS_BLOOMFILTER_PARAMS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@ pub struct NodeTester<R> {
|
||||
/// Average delay an acknowledgement packet is going to get delay at a single mixnode.
|
||||
average_ack_delay: Duration,
|
||||
|
||||
/// Specify whether any constructed packets should use the legacy format,
|
||||
/// where the payload keys are explicitly attached rather than using the seeds
|
||||
use_legacy_sphinx_format: bool,
|
||||
|
||||
// while acks are going to be ignored they still need to be constructed
|
||||
// so that the gateway would be able to correctly process and forward the message
|
||||
ack_key: Arc<AckKey>,
|
||||
@@ -57,6 +61,7 @@ where
|
||||
deterministic_route_selection: bool,
|
||||
average_packet_delay: Duration,
|
||||
average_ack_delay: Duration,
|
||||
use_legacy_sphinx_format: bool,
|
||||
ack_key: Arc<AckKey>,
|
||||
) -> Self {
|
||||
Self {
|
||||
@@ -67,6 +72,7 @@ where
|
||||
deterministic_route_selection,
|
||||
average_packet_delay,
|
||||
average_ack_delay,
|
||||
use_legacy_sphinx_format,
|
||||
ack_key,
|
||||
}
|
||||
}
|
||||
@@ -245,6 +251,10 @@ where
|
||||
impl<R: CryptoRng + Rng> FragmentPreparer for NodeTester<R> {
|
||||
type Rng = R;
|
||||
|
||||
fn use_legacy_sphinx_format(&self) -> bool {
|
||||
self.use_legacy_sphinx_format
|
||||
}
|
||||
|
||||
fn deterministic_route_selection(&self) -> bool {
|
||||
self.deterministic_route_selection
|
||||
}
|
||||
|
||||
@@ -363,6 +363,7 @@ impl MetricsController {
|
||||
buffer
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn to_writer(&self, writer: &mut dyn std::io::Write) {
|
||||
let metrics = self.gather();
|
||||
match writer.write_all(&metrics) {
|
||||
@@ -371,6 +372,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn register_int_gauge<'a>(&self, name: &str, help: impl Into<Option<&'a str>>) {
|
||||
let Some(metric) = Metric::new_int_gauge(name, help.into().unwrap_or(name)) else {
|
||||
return;
|
||||
@@ -378,6 +380,7 @@ impl MetricsController {
|
||||
self.register_metric(metric);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn register_float_gauge<'a>(&self, name: &str, help: impl Into<Option<&'a str>>) {
|
||||
let Some(metric) = Metric::new_float_gauge(name, help.into().unwrap_or(name)) else {
|
||||
return;
|
||||
@@ -385,6 +388,7 @@ impl MetricsController {
|
||||
self.register_metric(metric);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn register_int_counter<'a>(&self, name: &str, help: impl Into<Option<&'a str>>) {
|
||||
let Some(metric) = Metric::new_int_counter(name, help.into().unwrap_or(name)) else {
|
||||
return;
|
||||
@@ -392,6 +396,7 @@ impl MetricsController {
|
||||
self.register_metric(metric);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn register_histogram<'a>(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -404,6 +409,7 @@ impl MetricsController {
|
||||
self.register_metric(metric);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn set(&self, name: &str, value: i64) -> bool {
|
||||
if let Some(metric) = self.registry_index.get(name) {
|
||||
metric.set(value);
|
||||
@@ -413,6 +419,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn set_float(&self, name: &str, value: f64) -> bool {
|
||||
if let Some(metric) = self.registry_index.get(name) {
|
||||
metric.set_float(value);
|
||||
@@ -422,6 +429,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn add_to_histogram(&self, name: &str, value: f64) -> bool {
|
||||
if let Some(metric) = self.registry_index.get(name) {
|
||||
metric.add_histogram_observation(value);
|
||||
@@ -431,12 +439,14 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn start_timer(&self, name: &str) -> Option<HistogramTimer> {
|
||||
self.registry_index
|
||||
.get(name)
|
||||
.and_then(|metric| metric.start_timer())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn inc(&self, name: &str) -> bool {
|
||||
if let Some(metric) = self.registry_index.get(name) {
|
||||
metric.inc();
|
||||
@@ -446,6 +456,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn inc_by(&self, name: &str, value: i64) -> bool {
|
||||
if let Some(metric) = self.registry_index.get(name) {
|
||||
metric.inc_by(value);
|
||||
@@ -455,6 +466,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn maybe_register_and_set<'a>(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -468,6 +480,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn maybe_register_and_set_float<'a>(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -481,6 +494,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn maybe_register_and_add_to_histogram<'a>(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -495,6 +509,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn maybe_register_and_inc<'a>(&self, name: &str, help: impl Into<Option<&'a str>>) {
|
||||
if !self.inc(name) {
|
||||
let help = help.into();
|
||||
@@ -503,6 +518,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn maybe_register_and_inc_by<'a>(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -516,6 +532,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn register_metric(&self, metric: impl Into<Metric>) {
|
||||
let m = metric.into();
|
||||
let fq_name = m.fq_name();
|
||||
|
||||
@@ -37,8 +37,10 @@ pub enum SurbAckRecoveryError {
|
||||
}
|
||||
|
||||
impl SurbAck {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn construct<R>(
|
||||
rng: &mut R,
|
||||
use_legacy_sphinx_format: bool,
|
||||
recipient: &Recipient,
|
||||
ack_key: &AckKey,
|
||||
marshaled_fragment_id: [u8; 5],
|
||||
@@ -57,8 +59,6 @@ impl SurbAck {
|
||||
let packet_size = match packet_type {
|
||||
PacketType::Outfox => surb_ack_payload.len().max(MIN_PACKET_SIZE),
|
||||
PacketType::Mix => PacketSize::AckPacket.payload_size(),
|
||||
#[allow(deprecated)]
|
||||
PacketType::Vpn => PacketSize::AckPacket.payload_size(),
|
||||
};
|
||||
|
||||
let surb_ack_packet = match packet_type {
|
||||
@@ -69,14 +69,7 @@ impl SurbAck {
|
||||
Some(packet_size),
|
||||
)?,
|
||||
PacketType::Mix => NymPacket::sphinx_build(
|
||||
packet_size,
|
||||
surb_ack_payload,
|
||||
&route,
|
||||
&destination,
|
||||
&delays,
|
||||
)?,
|
||||
#[allow(deprecated)]
|
||||
PacketType::Vpn => NymPacket::sphinx_build(
|
||||
use_legacy_sphinx_format,
|
||||
packet_size,
|
||||
surb_ack_payload,
|
||||
&route,
|
||||
@@ -106,8 +99,6 @@ impl SurbAck {
|
||||
PacketSize::OutfoxAckPacket.size() + MAX_NODE_ADDRESS_UNPADDED_LEN
|
||||
}
|
||||
PacketType::Mix => PacketSize::AckPacket.size() + MAX_NODE_ADDRESS_UNPADDED_LEN,
|
||||
#[allow(deprecated)]
|
||||
PacketType::Vpn => PacketSize::AckPacket.size() + MAX_NODE_ADDRESS_UNPADDED_LEN,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,8 +130,6 @@ impl SurbAck {
|
||||
let packet = match packet_type {
|
||||
PacketType::Outfox => NymPacket::outfox_from_bytes(&b[address_offset..])?,
|
||||
PacketType::Mix => NymPacket::sphinx_from_bytes(&b[address_offset..])?,
|
||||
#[allow(deprecated)]
|
||||
PacketType::Vpn => NymPacket::sphinx_from_bytes(&b[address_offset..])?,
|
||||
};
|
||||
|
||||
Ok((address, packet))
|
||||
|
||||
@@ -199,7 +199,7 @@ impl TryFrom<NodeAddressBytes> for NymNodeRoutingAddress {
|
||||
type Error = NymNodeRoutingAddressError;
|
||||
|
||||
fn try_from(value: NodeAddressBytes) -> Result<Self, Self::Error> {
|
||||
Self::try_from_bytes(value.as_bytes_ref())
|
||||
Self::try_from_bytes(value.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ rand = { workspace = true }
|
||||
bs58 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
nym-crypto = { path = "../../crypto", features = ["stream_cipher", "rand"] }
|
||||
nym-sphinx-addressing = { path = "../addressing" }
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
// Copyright 2021-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// Copyright 2021-2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::encryption_key::{SurbEncryptionKey, SurbEncryptionKeyError, SurbEncryptionKeySize};
|
||||
use nym_crypto::{generic_array::typenum::Unsigned, Digest};
|
||||
use nym_sphinx_addressing::clients::Recipient;
|
||||
use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN};
|
||||
use nym_sphinx_addressing::nodes::{
|
||||
NymNodeRoutingAddress, NymNodeRoutingAddressError, MAX_NODE_ADDRESS_UNPADDED_LEN,
|
||||
};
|
||||
use nym_sphinx_params::packet_sizes::PacketSize;
|
||||
use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm};
|
||||
use nym_sphinx_types::{NymPacket, SURBMaterial, SphinxError, SURB};
|
||||
use nym_sphinx_types::constants::PAYLOAD_KEY_SEED_SIZE;
|
||||
use nym_sphinx_types::{
|
||||
NymPacket, SURBMaterial, SphinxError, HEADER_SIZE, NODE_ADDRESS_LENGTH, SURB,
|
||||
X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION,
|
||||
};
|
||||
use nym_topology::{NymRouteProvider, NymTopologyError};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use serde::de::{Error as SerdeError, Visitor};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
use std::fmt::{self, Formatter};
|
||||
use std::time;
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -31,6 +36,9 @@ pub enum ReplySurbError {
|
||||
#[error("failed to recover reply SURB from bytes: {0}")]
|
||||
RecoveryError(#[from] SphinxError),
|
||||
|
||||
#[error("failed to validate the first hop address of the recovered reply SURB: {0}")]
|
||||
MalformedSurbFirstHop(#[from] NymNodeRoutingAddressError),
|
||||
|
||||
#[error("failed to recover reply SURB encryption key from bytes: {0}")]
|
||||
InvalidEncryptionKeyData(#[from] SurbEncryptionKeyError),
|
||||
}
|
||||
@@ -80,6 +88,10 @@ impl<'de> Deserialize<'de> for ReplySurb {
|
||||
}
|
||||
|
||||
impl ReplySurb {
|
||||
/// base overhead of a reply surb that exists regardless of type or number of key materials.
|
||||
pub(crate) const BASE_OVERHEAD: usize =
|
||||
SurbEncryptionKeySize::USIZE + HEADER_SIZE + NODE_ADDRESS_LENGTH;
|
||||
|
||||
pub fn max_msg_len(packet_size: PacketSize) -> usize {
|
||||
// For detailed explanation (of ack overhead) refer to common\nymsphinx\src\preparer.rs::available_plaintext_per_packet()
|
||||
let ack_overhead = MAX_NODE_ADDRESS_UNPADDED_LEN + PacketSize::AckPacket.size();
|
||||
@@ -91,7 +103,8 @@ impl ReplySurb {
|
||||
pub fn construct<R>(
|
||||
rng: &mut R,
|
||||
recipient: &Recipient,
|
||||
average_delay: time::Duration,
|
||||
average_delay: Duration,
|
||||
use_legacy_surb_format: bool,
|
||||
topology: &NymRouteProvider,
|
||||
) -> Result<Self, NymTopologyError>
|
||||
where
|
||||
@@ -101,7 +114,10 @@ impl ReplySurb {
|
||||
let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len());
|
||||
let destination = recipient.as_sphinx_destination();
|
||||
|
||||
let surb_material = SURBMaterial::new(route, delays, destination);
|
||||
let mut surb_material = SURBMaterial::new(route, delays, destination);
|
||||
if use_legacy_surb_format {
|
||||
surb_material = surb_material.with_version(X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION)
|
||||
}
|
||||
|
||||
// this can't fail as we know we have a valid route to gateway and have correct number of delays
|
||||
Ok(ReplySurb {
|
||||
@@ -110,14 +126,10 @@ impl ReplySurb {
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the expected number of bytes the [`ReplySURB`] will take after serialization.
|
||||
/// Returns the expected number of bytes the [`ReplySURB`] will take after serialization using the new encoding format.
|
||||
/// Useful for deserialization from a bytes stream.
|
||||
pub fn serialized_len() -> usize {
|
||||
use nym_sphinx_types::{HEADER_SIZE, NODE_ADDRESS_LENGTH, PAYLOAD_KEY_SIZE};
|
||||
|
||||
// the SURB itself consists of SURB_header, first hop address and set of payload keys
|
||||
// for each hop (3x mix + egress)
|
||||
SurbEncryptionKeySize::USIZE + HEADER_SIZE + NODE_ADDRESS_LENGTH + 4 * PAYLOAD_KEY_SIZE
|
||||
pub fn v2_serialised_len(num_hops: u8) -> usize {
|
||||
Self::BASE_OVERHEAD + num_hops as usize * PAYLOAD_KEY_SEED_SIZE
|
||||
}
|
||||
|
||||
pub fn encryption_key(&self) -> &SurbEncryptionKey {
|
||||
@@ -143,7 +155,12 @@ impl ReplySurb {
|
||||
|
||||
let surb = match SURB::from_bytes(&bytes[SurbEncryptionKeySize::USIZE..]) {
|
||||
Err(err) => return Err(ReplySurbError::RecoveryError(err)),
|
||||
Ok(surb) => surb,
|
||||
Ok(surb) => {
|
||||
// we can't really check fully validity of the header, but at the very least we could make a sanity check
|
||||
// to make sure the first hop address is a valid socket address
|
||||
let _ = NymNodeRoutingAddress::try_from(surb.first_hop())?;
|
||||
surb
|
||||
}
|
||||
};
|
||||
|
||||
Ok(ReplySurb {
|
||||
|
||||
+301
-171
@@ -8,9 +8,15 @@ use std::fmt::{Display, Formatter};
|
||||
use std::mem;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::requests::v1::{AdditionalSurbsV1, DataV1, HeartbeatV1};
|
||||
use crate::requests::v2::{AdditionalSurbsV2, DataV2, HeartbeatV2};
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
pub(crate) mod v1;
|
||||
pub(crate) mod v2;
|
||||
|
||||
pub const SENDER_TAG_SIZE: usize = 16;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -103,31 +109,23 @@ pub struct RepliableMessage {
|
||||
impl Display for RepliableMessage {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
match &self.content {
|
||||
RepliableMessageContent::Data {
|
||||
message,
|
||||
reply_surbs,
|
||||
} => write!(
|
||||
f,
|
||||
"repliable {:.2} kiB data message with {} reply surbs attached from {}",
|
||||
message.len() as f64 / 1024.0,
|
||||
reply_surbs.len(),
|
||||
self.sender_tag,
|
||||
),
|
||||
RepliableMessageContent::AdditionalSurbs { reply_surbs } => write!(
|
||||
f,
|
||||
"repliable additional surbs message ({} reply surbs attached) from {}",
|
||||
reply_surbs.len(),
|
||||
self.sender_tag,
|
||||
),
|
||||
RepliableMessageContent::Heartbeat {
|
||||
additional_reply_surbs,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"repliable heartbeat message ({} reply surbs attached) from {}",
|
||||
additional_reply_surbs.len(),
|
||||
self.sender_tag,
|
||||
)
|
||||
RepliableMessageContent::Data(content) => {
|
||||
write!(f, "{content} from {}", self.sender_tag)
|
||||
}
|
||||
RepliableMessageContent::AdditionalSurbs(content) => {
|
||||
write!(f, "{content} from {}", self.sender_tag)
|
||||
}
|
||||
RepliableMessageContent::Heartbeat(content) => {
|
||||
write!(f, "{content} from {}", self.sender_tag)
|
||||
}
|
||||
RepliableMessageContent::DataV2(content) => {
|
||||
write!(f, "{content} from {}", self.sender_tag)
|
||||
}
|
||||
RepliableMessageContent::AdditionalSurbsV2(content) => {
|
||||
write!(f, "{content} from {}", self.sender_tag)
|
||||
}
|
||||
RepliableMessageContent::HeartbeatV2(content) => {
|
||||
write!(f, "{content} from {}", self.sender_tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,26 +133,43 @@ impl Display for RepliableMessage {
|
||||
|
||||
impl RepliableMessage {
|
||||
pub fn new_data(
|
||||
use_legacy_surb_format: bool,
|
||||
data: Vec<u8>,
|
||||
sender_tag: AnonymousSenderTag,
|
||||
reply_surbs: Vec<ReplySurb>,
|
||||
) -> Self {
|
||||
RepliableMessage {
|
||||
sender_tag,
|
||||
content: RepliableMessageContent::Data {
|
||||
let content = if use_legacy_surb_format {
|
||||
RepliableMessageContent::Data(DataV1 {
|
||||
message: data,
|
||||
reply_surbs,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
RepliableMessageContent::DataV2(DataV2 {
|
||||
message: data,
|
||||
reply_surbs,
|
||||
})
|
||||
};
|
||||
|
||||
RepliableMessage {
|
||||
sender_tag,
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new_additional_surbs(
|
||||
use_legacy_surb_format: bool,
|
||||
sender_tag: AnonymousSenderTag,
|
||||
reply_surbs: Vec<ReplySurb>,
|
||||
) -> Self {
|
||||
let content = if use_legacy_surb_format {
|
||||
RepliableMessageContent::AdditionalSurbs(AdditionalSurbsV1 { reply_surbs })
|
||||
} else {
|
||||
RepliableMessageContent::AdditionalSurbsV2(AdditionalSurbsV2 { reply_surbs })
|
||||
};
|
||||
|
||||
RepliableMessage {
|
||||
sender_tag,
|
||||
content: RepliableMessageContent::AdditionalSurbs { reply_surbs },
|
||||
content,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,35 +207,18 @@ impl RepliableMessage {
|
||||
}
|
||||
}
|
||||
|
||||
// this recovery code is shared between all variants containing reply surbs
|
||||
fn recover_reply_surbs(bytes: &[u8]) -> Result<(Vec<ReplySurb>, usize), InvalidReplyRequestError> {
|
||||
let mut consumed = mem::size_of::<u32>();
|
||||
if bytes.len() < consumed {
|
||||
return Err(InvalidReplyRequestError::RequestTooShortToDeserialize);
|
||||
}
|
||||
let num_surbs = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
|
||||
let surb_size = ReplySurb::serialized_len();
|
||||
if bytes[consumed..].len() < num_surbs as usize * surb_size {
|
||||
return Err(InvalidReplyRequestError::RequestTooShortToDeserialize);
|
||||
}
|
||||
|
||||
let mut reply_surbs = Vec::with_capacity(num_surbs as usize);
|
||||
for _ in 0..num_surbs as usize {
|
||||
let surb_bytes = &bytes[consumed..consumed + surb_size];
|
||||
let reply_surb = ReplySurb::from_bytes(surb_bytes)?;
|
||||
reply_surbs.push(reply_surb);
|
||||
|
||||
consumed += surb_size;
|
||||
}
|
||||
|
||||
Ok((reply_surbs, consumed))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[repr(u8)]
|
||||
enum RepliableMessageContentTag {
|
||||
Data = 0,
|
||||
AdditionalSurbs = 1,
|
||||
Heartbeat = 2,
|
||||
|
||||
// updated variants that slightly change SURB encoding
|
||||
// to allow for variable number of hops as well as using payload key seeds
|
||||
DataV2 = 3,
|
||||
AdditionalSurbsV2 = 4,
|
||||
HeartbeatV2 = 5,
|
||||
}
|
||||
|
||||
impl TryFrom<u8> for RepliableMessageContentTag {
|
||||
@@ -233,6 +231,11 @@ impl TryFrom<u8> for RepliableMessageContentTag {
|
||||
Ok(Self::AdditionalSurbs)
|
||||
}
|
||||
_ if value == (RepliableMessageContentTag::Heartbeat as u8) => Ok(Self::Heartbeat),
|
||||
_ if value == (RepliableMessageContentTag::DataV2 as u8) => Ok(Self::DataV2),
|
||||
_ if value == (RepliableMessageContentTag::AdditionalSurbsV2 as u8) => {
|
||||
Ok(Self::AdditionalSurbsV2)
|
||||
}
|
||||
_ if value == (RepliableMessageContentTag::HeartbeatV2 as u8) => Ok(Self::HeartbeatV2),
|
||||
val => Err(InvalidReplyRequestError::InvalidRepliableContentTag { received: val }),
|
||||
}
|
||||
}
|
||||
@@ -241,58 +244,24 @@ impl TryFrom<u8> for RepliableMessageContentTag {
|
||||
// sent by original sender that initialised the communication that knows address of the remote
|
||||
#[derive(Debug)]
|
||||
pub enum RepliableMessageContent {
|
||||
Data {
|
||||
message: Vec<u8>,
|
||||
reply_surbs: Vec<ReplySurb>,
|
||||
},
|
||||
AdditionalSurbs {
|
||||
reply_surbs: Vec<ReplySurb>,
|
||||
},
|
||||
Heartbeat {
|
||||
additional_reply_surbs: Vec<ReplySurb>,
|
||||
},
|
||||
Data(DataV1),
|
||||
AdditionalSurbs(AdditionalSurbsV1),
|
||||
Heartbeat(HeartbeatV1),
|
||||
|
||||
DataV2(DataV2),
|
||||
AdditionalSurbsV2(AdditionalSurbsV2),
|
||||
HeartbeatV2(HeartbeatV2),
|
||||
}
|
||||
|
||||
impl RepliableMessageContent {
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
match self {
|
||||
RepliableMessageContent::Data {
|
||||
message,
|
||||
reply_surbs,
|
||||
} => {
|
||||
let num_surbs = reply_surbs.len() as u32;
|
||||
|
||||
num_surbs
|
||||
.to_be_bytes()
|
||||
.into_iter()
|
||||
.chain(reply_surbs.into_iter().flat_map(|s| s.to_bytes()))
|
||||
.chain(message)
|
||||
.collect()
|
||||
}
|
||||
RepliableMessageContent::AdditionalSurbs { reply_surbs } => {
|
||||
let num_surbs = reply_surbs.len() as u32;
|
||||
|
||||
num_surbs
|
||||
.to_be_bytes()
|
||||
.into_iter()
|
||||
.chain(reply_surbs.into_iter().flat_map(|s| s.to_bytes()))
|
||||
.collect()
|
||||
}
|
||||
RepliableMessageContent::Heartbeat {
|
||||
additional_reply_surbs,
|
||||
} => {
|
||||
let num_surbs = additional_reply_surbs.len() as u32;
|
||||
|
||||
num_surbs
|
||||
.to_be_bytes()
|
||||
.into_iter()
|
||||
.chain(
|
||||
additional_reply_surbs
|
||||
.into_iter()
|
||||
.flat_map(|s| s.to_bytes()),
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
RepliableMessageContent::Data(content) => content.into_bytes(),
|
||||
RepliableMessageContent::AdditionalSurbs(content) => content.into_bytes(),
|
||||
RepliableMessageContent::Heartbeat(content) => content.into_bytes(),
|
||||
RepliableMessageContent::DataV2(content) => content.into_bytes(),
|
||||
RepliableMessageContent::AdditionalSurbsV2(content) => content.into_bytes(),
|
||||
RepliableMessageContent::HeartbeatV2(content) => content.into_bytes(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -304,19 +273,25 @@ impl RepliableMessageContent {
|
||||
return Err(InvalidReplyRequestError::RequestTooShortToDeserialize);
|
||||
}
|
||||
|
||||
let (reply_surbs, n) = recover_reply_surbs(bytes)?;
|
||||
|
||||
match tag {
|
||||
RepliableMessageContentTag::Data => Ok(RepliableMessageContent::Data {
|
||||
message: bytes[n..].to_vec(),
|
||||
reply_surbs,
|
||||
}),
|
||||
RepliableMessageContentTag::AdditionalSurbs => {
|
||||
Ok(RepliableMessageContent::AdditionalSurbs { reply_surbs })
|
||||
RepliableMessageContentTag::Data => {
|
||||
Ok(RepliableMessageContent::Data(DataV1::from_bytes(bytes)?))
|
||||
}
|
||||
RepliableMessageContentTag::Heartbeat => Ok(RepliableMessageContent::Heartbeat {
|
||||
additional_reply_surbs: reply_surbs,
|
||||
}),
|
||||
RepliableMessageContentTag::AdditionalSurbs => Ok(
|
||||
RepliableMessageContent::AdditionalSurbs(AdditionalSurbsV1::from_bytes(bytes)?),
|
||||
),
|
||||
RepliableMessageContentTag::Heartbeat => Ok(RepliableMessageContent::Heartbeat(
|
||||
HeartbeatV1::from_bytes(bytes)?,
|
||||
)),
|
||||
RepliableMessageContentTag::DataV2 => {
|
||||
Ok(RepliableMessageContent::DataV2(DataV2::from_bytes(bytes)?))
|
||||
}
|
||||
RepliableMessageContentTag::AdditionalSurbsV2 => Ok(
|
||||
RepliableMessageContent::AdditionalSurbsV2(AdditionalSurbsV2::from_bytes(bytes)?),
|
||||
),
|
||||
RepliableMessageContentTag::HeartbeatV2 => Ok(RepliableMessageContent::HeartbeatV2(
|
||||
HeartbeatV2::from_bytes(bytes)?,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,30 +302,22 @@ impl RepliableMessageContent {
|
||||
RepliableMessageContentTag::AdditionalSurbs
|
||||
}
|
||||
RepliableMessageContent::Heartbeat { .. } => RepliableMessageContentTag::Heartbeat,
|
||||
RepliableMessageContent::DataV2(_) => RepliableMessageContentTag::DataV2,
|
||||
RepliableMessageContent::AdditionalSurbsV2(_) => {
|
||||
RepliableMessageContentTag::AdditionalSurbsV2
|
||||
}
|
||||
RepliableMessageContent::HeartbeatV2(_) => RepliableMessageContentTag::HeartbeatV2,
|
||||
}
|
||||
}
|
||||
|
||||
fn serialized_size(&self) -> usize {
|
||||
match self {
|
||||
RepliableMessageContent::Data {
|
||||
message,
|
||||
reply_surbs,
|
||||
} => {
|
||||
let num_reply_surbs_tag = mem::size_of::<u32>();
|
||||
num_reply_surbs_tag
|
||||
+ reply_surbs.len() * ReplySurb::serialized_len()
|
||||
+ message.len()
|
||||
}
|
||||
RepliableMessageContent::AdditionalSurbs { reply_surbs } => {
|
||||
let num_reply_surbs_tag = mem::size_of::<u32>();
|
||||
num_reply_surbs_tag + reply_surbs.len() * ReplySurb::serialized_len()
|
||||
}
|
||||
RepliableMessageContent::Heartbeat {
|
||||
additional_reply_surbs,
|
||||
} => {
|
||||
let num_reply_surbs_tag = mem::size_of::<u32>();
|
||||
num_reply_surbs_tag + additional_reply_surbs.len() * ReplySurb::serialized_len()
|
||||
}
|
||||
RepliableMessageContent::Data(content) => content.serialized_len(),
|
||||
RepliableMessageContent::AdditionalSurbs(content) => content.serialized_len(),
|
||||
RepliableMessageContent::Heartbeat(content) => content.serialized_len(),
|
||||
RepliableMessageContent::DataV2(content) => content.serialized_len(),
|
||||
RepliableMessageContent::AdditionalSurbsV2(content) => content.serialized_len(),
|
||||
RepliableMessageContent::HeartbeatV2(content) => content.serialized_len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -514,18 +481,22 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
mod fixtures {
|
||||
use crate::requests::v1::{AdditionalSurbsV1, DataV1, HeartbeatV1};
|
||||
use crate::requests::v2::{AdditionalSurbsV2, DataV2, HeartbeatV2};
|
||||
use crate::requests::{AnonymousSenderTag, RepliableMessageContent, ReplyMessageContent};
|
||||
use crate::{ReplySurb, SurbEncryptionKey};
|
||||
use nym_crypto::asymmetric::{encryption, identity};
|
||||
use nym_sphinx_addressing::clients::Recipient;
|
||||
use nym_sphinx_types::{
|
||||
Delay, Destination, DestinationAddressBytes, Node, NodeAddressBytes, PrivateKey,
|
||||
SURBMaterial, NODE_ADDRESS_LENGTH,
|
||||
SURBMaterial, NODE_ADDRESS_LENGTH, X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION,
|
||||
};
|
||||
use rand::{Rng, RngCore};
|
||||
use rand_chacha::rand_core::SeedableRng;
|
||||
use rand_chacha::ChaCha20Rng;
|
||||
|
||||
pub(crate) const LEGACY_HOPS: u8 = 4;
|
||||
|
||||
pub(super) fn test_rng() -> ChaCha20Rng {
|
||||
let dummy_seed = [42u8; 32];
|
||||
ChaCha20Rng::from_seed(dummy_seed)
|
||||
@@ -567,11 +538,9 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn reply_surb(rng: &mut ChaCha20Rng) -> ReplySurb {
|
||||
// due to gateway
|
||||
const HOPS: u8 = 4;
|
||||
let route = (0..HOPS).map(|_| node(rng)).collect();
|
||||
let delays = (0..HOPS)
|
||||
pub(super) fn reply_surb(rng: &mut ChaCha20Rng, legacy: bool, hops: u8) -> ReplySurb {
|
||||
let route = (0..hops).map(|_| node(rng)).collect();
|
||||
let delays = (0..hops)
|
||||
.map(|_| Delay::new_from_nanos(rng.next_u64()))
|
||||
.collect();
|
||||
let mut destination_bytes = [0u8; 32];
|
||||
@@ -585,50 +554,58 @@ mod tests {
|
||||
identifier_bytes,
|
||||
);
|
||||
|
||||
let surb = SURBMaterial::new(route, delays, destination)
|
||||
.construct_SURB()
|
||||
.unwrap();
|
||||
let mut surb_material = SURBMaterial::new(route, delays, destination);
|
||||
if legacy {
|
||||
surb_material =
|
||||
surb_material.with_version(X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION);
|
||||
}
|
||||
|
||||
ReplySurb {
|
||||
surb,
|
||||
surb: surb_material.construct_SURB().unwrap(),
|
||||
encryption_key: SurbEncryptionKey::new(rng),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn reply_surbs(rng: &mut ChaCha20Rng, n: usize) -> Vec<ReplySurb> {
|
||||
pub(super) fn reply_surbs(
|
||||
rng: &mut ChaCha20Rng,
|
||||
n: usize,
|
||||
legacy: bool,
|
||||
hops: u8,
|
||||
) -> Vec<ReplySurb> {
|
||||
let mut surbs = Vec::with_capacity(n);
|
||||
for _ in 0..n {
|
||||
surbs.push(reply_surb(rng))
|
||||
surbs.push(reply_surb(rng, legacy, hops))
|
||||
}
|
||||
surbs
|
||||
}
|
||||
|
||||
pub(super) fn repliable_content_data(
|
||||
pub(super) fn repliable_content_data_v1(
|
||||
rng: &mut ChaCha20Rng,
|
||||
msg_len: usize,
|
||||
surbs: usize,
|
||||
) -> RepliableMessageContent {
|
||||
RepliableMessageContent::Data {
|
||||
RepliableMessageContent::Data(DataV1 {
|
||||
message: random_vec_u8(rng, msg_len),
|
||||
reply_surbs: reply_surbs(rng, surbs),
|
||||
}
|
||||
reply_surbs: reply_surbs(rng, surbs, true, LEGACY_HOPS),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn repliable_content_surbs(
|
||||
pub(super) fn repliable_content_surbs_v1(
|
||||
rng: &mut ChaCha20Rng,
|
||||
surbs: usize,
|
||||
) -> RepliableMessageContent {
|
||||
RepliableMessageContent::AdditionalSurbs {
|
||||
reply_surbs: reply_surbs(rng, surbs),
|
||||
}
|
||||
RepliableMessageContent::AdditionalSurbs(AdditionalSurbsV1 {
|
||||
reply_surbs: reply_surbs(rng, surbs, true, LEGACY_HOPS),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn repliable_content_heartbeat(
|
||||
pub(super) fn repliable_content_heartbeat_v1(
|
||||
rng: &mut ChaCha20Rng,
|
||||
surbs: usize,
|
||||
) -> RepliableMessageContent {
|
||||
RepliableMessageContent::Heartbeat {
|
||||
additional_reply_surbs: reply_surbs(rng, surbs),
|
||||
}
|
||||
RepliableMessageContent::Heartbeat(HeartbeatV1 {
|
||||
additional_reply_surbs: reply_surbs(rng, surbs, true, LEGACY_HOPS),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn reply_content_data(
|
||||
@@ -649,37 +626,70 @@ mod tests {
|
||||
amount: surbs,
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn repliable_content_data_v2(
|
||||
rng: &mut ChaCha20Rng,
|
||||
msg_len: usize,
|
||||
surbs: usize,
|
||||
surb_hops: u8,
|
||||
) -> RepliableMessageContent {
|
||||
RepliableMessageContent::DataV2(DataV2 {
|
||||
message: random_vec_u8(rng, msg_len),
|
||||
reply_surbs: reply_surbs(rng, surbs, false, surb_hops),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn repliable_content_surbs_v2(
|
||||
rng: &mut ChaCha20Rng,
|
||||
surbs: usize,
|
||||
surb_hops: u8,
|
||||
) -> RepliableMessageContent {
|
||||
RepliableMessageContent::AdditionalSurbsV2(AdditionalSurbsV2 {
|
||||
reply_surbs: reply_surbs(rng, surbs, false, surb_hops),
|
||||
})
|
||||
}
|
||||
|
||||
pub(super) fn repliable_content_heartbeat_v2(
|
||||
rng: &mut ChaCha20Rng,
|
||||
surbs: usize,
|
||||
surb_hops: u8,
|
||||
) -> RepliableMessageContent {
|
||||
RepliableMessageContent::HeartbeatV2(HeartbeatV2 {
|
||||
additional_reply_surbs: reply_surbs(rng, surbs, false, surb_hops),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod repliable_message {
|
||||
use super::*;
|
||||
use crate::requests::tests::fixtures::LEGACY_HOPS;
|
||||
|
||||
#[test]
|
||||
fn serialized_size_matches_actual_serialization() {
|
||||
fn serialized_size_matches_actual_serialization_for_v1_messages() {
|
||||
let mut rng = fixtures::test_rng();
|
||||
|
||||
let data1 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_data(&mut rng, 10000, 0),
|
||||
content: fixtures::repliable_content_data_v1(&mut rng, 10000, 0),
|
||||
};
|
||||
assert_eq!(data1.serialized_size(), data1.into_bytes().len());
|
||||
|
||||
let data2 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_data(&mut rng, 10, 100),
|
||||
content: fixtures::repliable_content_data_v1(&mut rng, 10, 100),
|
||||
};
|
||||
assert_eq!(data2.serialized_size(), data2.into_bytes().len());
|
||||
|
||||
let data3 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_data(&mut rng, 100000, 1000),
|
||||
content: fixtures::repliable_content_data_v1(&mut rng, 100000, 1000),
|
||||
};
|
||||
assert_eq!(data3.serialized_size(), data3.into_bytes().len());
|
||||
|
||||
let additional_surbs1 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_surbs(&mut rng, 1),
|
||||
content: fixtures::repliable_content_surbs_v1(&mut rng, 1),
|
||||
};
|
||||
assert_eq!(
|
||||
additional_surbs1.serialized_size(),
|
||||
@@ -688,7 +698,7 @@ mod tests {
|
||||
|
||||
let additional_surbs2 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_surbs(&mut rng, 1000),
|
||||
content: fixtures::repliable_content_surbs_v1(&mut rng, 1000),
|
||||
};
|
||||
assert_eq!(
|
||||
additional_surbs2.serialized_size(),
|
||||
@@ -697,53 +707,173 @@ mod tests {
|
||||
|
||||
let heartbeat1 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_heartbeat(&mut rng, 1),
|
||||
content: fixtures::repliable_content_heartbeat_v1(&mut rng, 1),
|
||||
};
|
||||
assert_eq!(heartbeat1.serialized_size(), heartbeat1.into_bytes().len());
|
||||
|
||||
let heartbeat2 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_heartbeat(&mut rng, 1000),
|
||||
content: fixtures::repliable_content_heartbeat_v1(&mut rng, 1000),
|
||||
};
|
||||
assert_eq!(heartbeat2.serialized_size(), heartbeat2.into_bytes().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_size_matches_actual_serialization_for_v2_messages() {
|
||||
let mut rng = fixtures::test_rng();
|
||||
|
||||
let data1 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_data_v2(&mut rng, 10000, 0, LEGACY_HOPS),
|
||||
};
|
||||
assert_eq!(data1.serialized_size(), data1.into_bytes().len());
|
||||
|
||||
let data2 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_data_v2(&mut rng, 10, 100, LEGACY_HOPS),
|
||||
};
|
||||
assert_eq!(data2.serialized_size(), data2.into_bytes().len());
|
||||
|
||||
let data3 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_data_v2(&mut rng, 100000, 1000, LEGACY_HOPS),
|
||||
};
|
||||
assert_eq!(data3.serialized_size(), data3.into_bytes().len());
|
||||
|
||||
let data4 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_data_v2(&mut rng, 100000, 1000, 1),
|
||||
};
|
||||
assert_eq!(data4.serialized_size(), data4.into_bytes().len());
|
||||
|
||||
let additional_surbs1 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_surbs_v2(&mut rng, 1, LEGACY_HOPS),
|
||||
};
|
||||
assert_eq!(
|
||||
additional_surbs1.serialized_size(),
|
||||
additional_surbs1.into_bytes().len()
|
||||
);
|
||||
|
||||
let additional_surbs2 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_surbs_v2(&mut rng, 1000, LEGACY_HOPS),
|
||||
};
|
||||
assert_eq!(
|
||||
additional_surbs2.serialized_size(),
|
||||
additional_surbs2.into_bytes().len()
|
||||
);
|
||||
|
||||
let additional_surbs3 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_surbs_v2(&mut rng, 1000, 1),
|
||||
};
|
||||
assert_eq!(
|
||||
additional_surbs3.serialized_size(),
|
||||
additional_surbs3.into_bytes().len()
|
||||
);
|
||||
|
||||
let heartbeat1 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_heartbeat_v2(&mut rng, 1, LEGACY_HOPS),
|
||||
};
|
||||
assert_eq!(heartbeat1.serialized_size(), heartbeat1.into_bytes().len());
|
||||
|
||||
let heartbeat2 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_heartbeat_v2(&mut rng, 1000, LEGACY_HOPS),
|
||||
};
|
||||
assert_eq!(heartbeat2.serialized_size(), heartbeat2.into_bytes().len());
|
||||
|
||||
let heartbeat3 = RepliableMessage {
|
||||
sender_tag: fixtures::sender_tag(&mut rng),
|
||||
content: fixtures::repliable_content_heartbeat_v2(&mut rng, 1000, 1),
|
||||
};
|
||||
assert_eq!(heartbeat3.serialized_size(), heartbeat3.into_bytes().len());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod repliable_message_content {
|
||||
use super::*;
|
||||
use crate::requests::tests::fixtures::LEGACY_HOPS;
|
||||
|
||||
#[test]
|
||||
fn serialized_size_matches_actual_serialization() {
|
||||
fn serialized_size_matches_actual_serialization_for_v1_messages() {
|
||||
let mut rng = fixtures::test_rng();
|
||||
|
||||
let data1 = fixtures::repliable_content_data(&mut rng, 10000, 0);
|
||||
let data1 = fixtures::repliable_content_data_v1(&mut rng, 10000, 0);
|
||||
assert_eq!(data1.serialized_size(), data1.into_bytes().len());
|
||||
|
||||
let data2 = fixtures::repliable_content_data(&mut rng, 10, 100);
|
||||
let data2 = fixtures::repliable_content_data_v1(&mut rng, 10, 100);
|
||||
assert_eq!(data2.serialized_size(), data2.into_bytes().len());
|
||||
|
||||
let data3 = fixtures::repliable_content_data(&mut rng, 100000, 1000);
|
||||
let data3 = fixtures::repliable_content_data_v1(&mut rng, 100000, 1000);
|
||||
assert_eq!(data3.serialized_size(), data3.into_bytes().len());
|
||||
|
||||
let additional_surbs1 = fixtures::repliable_content_surbs(&mut rng, 1);
|
||||
let additional_surbs1 = fixtures::repliable_content_surbs_v1(&mut rng, 1);
|
||||
assert_eq!(
|
||||
additional_surbs1.serialized_size(),
|
||||
additional_surbs1.into_bytes().len()
|
||||
);
|
||||
|
||||
let additional_surbs2 = fixtures::repliable_content_surbs(&mut rng, 1000);
|
||||
let additional_surbs2 = fixtures::repliable_content_surbs_v1(&mut rng, 1000);
|
||||
assert_eq!(
|
||||
additional_surbs2.serialized_size(),
|
||||
additional_surbs2.into_bytes().len()
|
||||
);
|
||||
|
||||
let heartbeat1 = fixtures::repliable_content_heartbeat(&mut rng, 1);
|
||||
let heartbeat1 = fixtures::repliable_content_heartbeat_v1(&mut rng, 1);
|
||||
assert_eq!(heartbeat1.serialized_size(), heartbeat1.into_bytes().len());
|
||||
|
||||
let heartbeat2 = fixtures::repliable_content_heartbeat(&mut rng, 1000);
|
||||
let heartbeat2 = fixtures::repliable_content_heartbeat_v1(&mut rng, 1000);
|
||||
assert_eq!(heartbeat2.serialized_size(), heartbeat2.into_bytes().len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn serialized_size_matches_actual_serialization_for_v2_messages() {
|
||||
let mut rng = fixtures::test_rng();
|
||||
|
||||
let data1 = fixtures::repliable_content_data_v2(&mut rng, 10000, 0, LEGACY_HOPS);
|
||||
assert_eq!(data1.serialized_size(), data1.into_bytes().len());
|
||||
|
||||
let data2 = fixtures::repliable_content_data_v2(&mut rng, 10, 100, LEGACY_HOPS);
|
||||
assert_eq!(data2.serialized_size(), data2.into_bytes().len());
|
||||
|
||||
let data3 = fixtures::repliable_content_data_v2(&mut rng, 100000, 1000, LEGACY_HOPS);
|
||||
assert_eq!(data3.serialized_size(), data3.into_bytes().len());
|
||||
|
||||
let data4 = fixtures::repliable_content_data_v2(&mut rng, 100000, 1000, 1);
|
||||
assert_eq!(data4.serialized_size(), data4.into_bytes().len());
|
||||
|
||||
let additional_surbs1 = fixtures::repliable_content_surbs_v2(&mut rng, 1, LEGACY_HOPS);
|
||||
assert_eq!(
|
||||
additional_surbs1.serialized_size(),
|
||||
additional_surbs1.into_bytes().len()
|
||||
);
|
||||
|
||||
let additional_surbs2 =
|
||||
fixtures::repliable_content_surbs_v2(&mut rng, 1000, LEGACY_HOPS);
|
||||
assert_eq!(
|
||||
additional_surbs2.serialized_size(),
|
||||
additional_surbs2.into_bytes().len()
|
||||
);
|
||||
|
||||
let additional_surbs3 = fixtures::repliable_content_surbs_v2(&mut rng, 1000, 1);
|
||||
assert_eq!(
|
||||
additional_surbs3.serialized_size(),
|
||||
additional_surbs3.into_bytes().len()
|
||||
);
|
||||
|
||||
let heartbeat1 = fixtures::repliable_content_heartbeat_v2(&mut rng, 1, LEGACY_HOPS);
|
||||
assert_eq!(heartbeat1.serialized_size(), heartbeat1.into_bytes().len());
|
||||
|
||||
let heartbeat2 = fixtures::repliable_content_heartbeat_v2(&mut rng, 1000, LEGACY_HOPS);
|
||||
assert_eq!(heartbeat2.serialized_size(), heartbeat2.into_bytes().len());
|
||||
|
||||
let heartbeat3 = fixtures::repliable_content_heartbeat_v2(&mut rng, 1000, 1);
|
||||
assert_eq!(heartbeat3.serialized_size(), heartbeat3.into_bytes().len());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -0,0 +1,176 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::requests::InvalidReplyRequestError;
|
||||
use crate::ReplySurb;
|
||||
use nym_sphinx_types::PAYLOAD_KEY_SIZE;
|
||||
use std::fmt::Display;
|
||||
use std::mem;
|
||||
use tracing::{error, warn};
|
||||
|
||||
const fn v1_reply_surb_serialised_len() -> usize {
|
||||
// the SURB itself consists of SURB_header, first hop address and set of payload keys
|
||||
// for each hop (3x mix + egress)
|
||||
ReplySurb::BASE_OVERHEAD + 4 * PAYLOAD_KEY_SIZE
|
||||
}
|
||||
|
||||
fn v1_reply_surbs_serialised_len(surbs: &[ReplySurb]) -> usize {
|
||||
// sanity checks; this should probably be removed later on
|
||||
if let Some(reply_surb) = surbs.first() {
|
||||
if reply_surb.surb.uses_key_seeds() {
|
||||
error!("using v1 surbs encoding with updated structure - the surbs will be unusable")
|
||||
}
|
||||
}
|
||||
|
||||
// when serialising surbs are always prepended with u32-encoded count
|
||||
4 + surbs.len() * v1_reply_surb_serialised_len()
|
||||
}
|
||||
|
||||
// this recovery code is shared between all legacy variants containing reply surbs
|
||||
// NUM_SURBS (u32) || SURB_DATA
|
||||
fn recover_reply_surbs_v1(
|
||||
bytes: &[u8],
|
||||
) -> Result<(Vec<ReplySurb>, usize), InvalidReplyRequestError> {
|
||||
let mut consumed = mem::size_of::<u32>();
|
||||
if bytes.len() < consumed {
|
||||
return Err(InvalidReplyRequestError::RequestTooShortToDeserialize);
|
||||
}
|
||||
let num_surbs = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
|
||||
|
||||
let surb_size = v1_reply_surb_serialised_len();
|
||||
if bytes[consumed..].len() < num_surbs as usize * surb_size {
|
||||
return Err(InvalidReplyRequestError::RequestTooShortToDeserialize);
|
||||
}
|
||||
|
||||
let mut reply_surbs = Vec::with_capacity(num_surbs as usize);
|
||||
for _ in 0..num_surbs as usize {
|
||||
let surb_bytes = &bytes[consumed..consumed + surb_size];
|
||||
let reply_surb = ReplySurb::from_bytes(surb_bytes)?;
|
||||
reply_surbs.push(reply_surb);
|
||||
|
||||
consumed += surb_size;
|
||||
}
|
||||
|
||||
Ok((reply_surbs, consumed))
|
||||
}
|
||||
|
||||
// length (u32) prefixed reply surbs with legacy serialisation of 4 hops and full payload keys attached
|
||||
fn reply_surbs_bytes_v1(reply_surbs: &[ReplySurb]) -> impl Iterator<Item = u8> + use<'_> {
|
||||
let num_surbs = reply_surbs.len() as u32;
|
||||
|
||||
num_surbs
|
||||
.to_be_bytes()
|
||||
.into_iter()
|
||||
.chain(reply_surbs.iter().flat_map(|s| s.to_bytes()))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DataV1 {
|
||||
pub message: Vec<u8>,
|
||||
pub reply_surbs: Vec<ReplySurb>,
|
||||
}
|
||||
|
||||
impl Display for DataV1 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(
|
||||
f,
|
||||
"V1 repliable {:.2} kiB data message with {} reply surbs attached",
|
||||
self.message.len() as f64 / 1024.0,
|
||||
self.reply_surbs.len(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AdditionalSurbsV1 {
|
||||
pub reply_surbs: Vec<ReplySurb>,
|
||||
}
|
||||
|
||||
impl Display for AdditionalSurbsV1 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(
|
||||
f,
|
||||
"V1 repliable additional surbs message ({} reply surbs attached)",
|
||||
self.reply_surbs.len(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HeartbeatV1 {
|
||||
pub additional_reply_surbs: Vec<ReplySurb>,
|
||||
}
|
||||
|
||||
impl Display for HeartbeatV1 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(
|
||||
f,
|
||||
"V1 repliable heartbeat message ({} reply surbs attached)",
|
||||
self.additional_reply_surbs.len(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl DataV1 {
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
reply_surbs_bytes_v1(&self.reply_surbs)
|
||||
.chain(self.message)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, InvalidReplyRequestError> {
|
||||
let (reply_surbs, n) = recover_reply_surbs_v1(bytes)?;
|
||||
Ok(DataV1 {
|
||||
message: bytes[n..].to_vec(),
|
||||
reply_surbs,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialized_len(&self) -> usize {
|
||||
v1_reply_surbs_serialised_len(&self.reply_surbs) + self.message.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl AdditionalSurbsV1 {
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
reply_surbs_bytes_v1(&self.reply_surbs).collect()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, InvalidReplyRequestError> {
|
||||
let (reply_surbs, n) = recover_reply_surbs_v1(bytes)?;
|
||||
if n != bytes.len() {
|
||||
let trailing = bytes.len() - n;
|
||||
|
||||
warn!("trailing {trailing} bytes after v1 additional surbs message");
|
||||
}
|
||||
|
||||
Ok(AdditionalSurbsV1 { reply_surbs })
|
||||
}
|
||||
|
||||
pub fn serialized_len(&self) -> usize {
|
||||
v1_reply_surbs_serialised_len(&self.reply_surbs)
|
||||
}
|
||||
}
|
||||
|
||||
impl HeartbeatV1 {
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
reply_surbs_bytes_v1(&self.additional_reply_surbs).collect()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, InvalidReplyRequestError> {
|
||||
let (additional_reply_surbs, n) = recover_reply_surbs_v1(bytes)?;
|
||||
if n != bytes.len() {
|
||||
let trailing = bytes.len() - n;
|
||||
|
||||
warn!("trailing {trailing} bytes after v1 heartbeat message");
|
||||
}
|
||||
|
||||
Ok(HeartbeatV1 {
|
||||
additional_reply_surbs,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialized_len(&self) -> usize {
|
||||
v1_reply_surbs_serialised_len(&self.additional_reply_surbs)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::requests::InvalidReplyRequestError;
|
||||
use crate::ReplySurb;
|
||||
use nym_sphinx_types::constants::PAYLOAD_KEY_SEED_SIZE;
|
||||
use std::fmt::Display;
|
||||
use std::iter::once;
|
||||
use tracing::{error, warn};
|
||||
|
||||
const fn v2_reply_surb_serialised_len(num_hops: u8) -> usize {
|
||||
ReplySurb::BASE_OVERHEAD + num_hops as usize * PAYLOAD_KEY_SEED_SIZE
|
||||
}
|
||||
|
||||
// sphinx doesn't support more than 5 hops (so cast to u8 is safe)
|
||||
// ASSUMPTION: all surbs are generated with the same parameters (if they're not, then the client is hurting itself)
|
||||
fn reply_surbs_hops(reply_surbs: &[ReplySurb]) -> u8 {
|
||||
reply_surbs
|
||||
.first()
|
||||
.map(|reply_surb| reply_surb.surb.materials_count() as u8)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn v2_reply_surbs_serialised_len(surbs: &[ReplySurb]) -> usize {
|
||||
let num_surbs = surbs.len();
|
||||
let num_hops = reply_surbs_hops(surbs);
|
||||
|
||||
// sanity checks; this should probably be removed later on
|
||||
if let Some(reply_surb) = surbs.first() {
|
||||
if !reply_surb.surb.uses_key_seeds() {
|
||||
error!("using v2 surbs encoding with legacy structure - the surbs will be unusable")
|
||||
}
|
||||
}
|
||||
|
||||
// when serialising surbs are always prepended with u16-encoded count an u8-encoded number of hops
|
||||
3 + num_surbs * v2_reply_surb_serialised_len(num_hops)
|
||||
}
|
||||
|
||||
// NUM_SURBS (u16) || HOPS (u8) || SURB_DATA
|
||||
fn recover_reply_surbs_v2(
|
||||
bytes: &[u8],
|
||||
) -> Result<(Vec<ReplySurb>, usize), InvalidReplyRequestError> {
|
||||
if bytes.len() < 2 {
|
||||
return Err(InvalidReplyRequestError::RequestTooShortToDeserialize);
|
||||
}
|
||||
|
||||
// we're not attaching more than 65k surbs...
|
||||
let num_surbs = u16::from_be_bytes([bytes[0], bytes[1]]);
|
||||
let num_hops = bytes[2];
|
||||
let mut consumed = 3;
|
||||
|
||||
let surb_size = ReplySurb::v2_serialised_len(num_hops);
|
||||
if bytes[consumed..].len() < num_surbs as usize * surb_size {
|
||||
return Err(InvalidReplyRequestError::RequestTooShortToDeserialize);
|
||||
}
|
||||
|
||||
let mut reply_surbs = Vec::with_capacity(num_surbs as usize);
|
||||
for _ in 0..num_surbs as usize {
|
||||
let surb_bytes = &bytes[consumed..consumed + surb_size];
|
||||
let reply_surb = ReplySurb::from_bytes(surb_bytes)?;
|
||||
reply_surbs.push(reply_surb);
|
||||
|
||||
consumed += surb_size;
|
||||
}
|
||||
|
||||
Ok((reply_surbs, consumed))
|
||||
}
|
||||
|
||||
fn reply_surbs_bytes_v2(reply_surbs: &[ReplySurb]) -> impl Iterator<Item = u8> + use<'_> {
|
||||
let num_surbs = reply_surbs.len() as u16;
|
||||
let num_hops = reply_surbs_hops(reply_surbs);
|
||||
|
||||
num_surbs
|
||||
.to_be_bytes()
|
||||
.into_iter()
|
||||
.chain(once(num_hops))
|
||||
.chain(reply_surbs.iter().flat_map(|surb| surb.to_bytes()))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DataV2 {
|
||||
pub message: Vec<u8>,
|
||||
pub reply_surbs: Vec<ReplySurb>,
|
||||
}
|
||||
|
||||
impl Display for DataV2 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(
|
||||
f,
|
||||
"V2 repliable {:.2} kiB data message with {} reply surbs attached",
|
||||
self.message.len() as f64 / 1024.0,
|
||||
self.reply_surbs.len(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AdditionalSurbsV2 {
|
||||
pub reply_surbs: Vec<ReplySurb>,
|
||||
}
|
||||
|
||||
impl Display for AdditionalSurbsV2 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(
|
||||
f,
|
||||
"V2 repliable additional surbs message ({} reply surbs attached)",
|
||||
self.reply_surbs.len(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HeartbeatV2 {
|
||||
pub additional_reply_surbs: Vec<ReplySurb>,
|
||||
}
|
||||
|
||||
impl Display for HeartbeatV2 {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(
|
||||
f,
|
||||
"V2 repliable heartbeat message ({} reply surbs attached)",
|
||||
self.additional_reply_surbs.len(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl DataV2 {
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
reply_surbs_bytes_v2(&self.reply_surbs)
|
||||
.chain(self.message)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, InvalidReplyRequestError> {
|
||||
let (reply_surbs, n) = recover_reply_surbs_v2(bytes)?;
|
||||
Ok(DataV2 {
|
||||
message: bytes[n..].to_vec(),
|
||||
reply_surbs,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialized_len(&self) -> usize {
|
||||
v2_reply_surbs_serialised_len(&self.reply_surbs) + self.message.len()
|
||||
}
|
||||
}
|
||||
|
||||
impl AdditionalSurbsV2 {
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
reply_surbs_bytes_v2(&self.reply_surbs).collect()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, InvalidReplyRequestError> {
|
||||
let (reply_surbs, n) = recover_reply_surbs_v2(bytes)?;
|
||||
if n != bytes.len() {
|
||||
let trailing = bytes.len() - n;
|
||||
warn!("trailing {trailing} bytes after v2 additional surbs message");
|
||||
}
|
||||
|
||||
Ok(AdditionalSurbsV2 { reply_surbs })
|
||||
}
|
||||
|
||||
pub fn serialized_len(&self) -> usize {
|
||||
v2_reply_surbs_serialised_len(&self.reply_surbs)
|
||||
}
|
||||
}
|
||||
|
||||
impl HeartbeatV2 {
|
||||
pub fn into_bytes(self) -> Vec<u8> {
|
||||
reply_surbs_bytes_v2(&self.additional_reply_surbs).collect()
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<Self, InvalidReplyRequestError> {
|
||||
let (additional_reply_surbs, n) = recover_reply_surbs_v2(bytes)?;
|
||||
if n != bytes.len() {
|
||||
let trailing = bytes.len() - n;
|
||||
warn!("trailing {trailing} bytes after v2 heartbeat message");
|
||||
}
|
||||
|
||||
Ok(HeartbeatV2 {
|
||||
additional_reply_surbs,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn serialized_len(&self) -> usize {
|
||||
v2_reply_surbs_serialised_len(&self.additional_reply_surbs)
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ pub enum CoverMessageError {
|
||||
|
||||
pub fn generate_loop_cover_surb_ack<R>(
|
||||
rng: &mut R,
|
||||
use_legacy_sphinx_format: bool,
|
||||
topology: &NymRouteProvider,
|
||||
ack_key: &AckKey,
|
||||
full_address: &Recipient,
|
||||
@@ -45,6 +46,7 @@ where
|
||||
{
|
||||
Ok(SurbAck::construct(
|
||||
rng,
|
||||
use_legacy_sphinx_format,
|
||||
full_address,
|
||||
ack_key,
|
||||
COVER_FRAG_ID.to_bytes(),
|
||||
@@ -57,6 +59,7 @@ where
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn generate_loop_cover_packet<R>(
|
||||
rng: &mut R,
|
||||
use_legacy_sphinx_format: bool,
|
||||
topology: &NymRouteProvider,
|
||||
ack_key: &AckKey,
|
||||
full_address: &Recipient,
|
||||
@@ -71,6 +74,7 @@ where
|
||||
// we don't care about total ack delay - we will not be retransmitting it anyway
|
||||
let (_, ack_bytes) = generate_loop_cover_surb_ack(
|
||||
rng,
|
||||
use_legacy_sphinx_format,
|
||||
topology,
|
||||
ack_key,
|
||||
full_address,
|
||||
@@ -126,14 +130,7 @@ where
|
||||
// once merged, that's an easy rng injection point for sphinx packets : )
|
||||
let packet = match packet_type {
|
||||
PacketType::Mix => NymPacket::sphinx_build(
|
||||
packet_size.payload_size(),
|
||||
packet_payload,
|
||||
&route,
|
||||
&destination,
|
||||
&delays,
|
||||
)?,
|
||||
#[allow(deprecated)]
|
||||
PacketType::Vpn => NymPacket::sphinx_build(
|
||||
use_legacy_sphinx_format,
|
||||
packet_size.payload_size(),
|
||||
packet_payload,
|
||||
&route,
|
||||
|
||||
@@ -11,12 +11,11 @@ repository = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
nym-sphinx-types = { path = "../types", features = ["sphinx", "outfox"] }
|
||||
nym-sphinx-params = { path = "../params", features = ["sphinx", "outfox"] }
|
||||
nym-sphinx-forwarding = { path = "../forwarding" }
|
||||
nym-metrics = { path = "../../nym-metrics" }
|
||||
nym-sphinx-addressing = { path = "../addressing" }
|
||||
nym-sphinx-acknowledgements = { path = "../acknowledgements" }
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::packet::{FramedNymPacket, Header};
|
||||
use bytes::{Buf, BufMut, BytesMut};
|
||||
use nym_sphinx_params::packet_sizes::{InvalidPacketSize, PacketSize};
|
||||
use nym_sphinx_params::packet_types::InvalidPacketType;
|
||||
use nym_sphinx_params::packet_version::{InvalidPacketVersion, PacketVersion};
|
||||
use nym_sphinx_params::PacketType;
|
||||
use nym_sphinx_types::{NymPacket, NymPacketError};
|
||||
use std::io;
|
||||
@@ -13,16 +14,25 @@ use tokio_util::codec::{Decoder, Encoder};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum NymCodecError {
|
||||
#[error("the packet size information was malformed - {0}")]
|
||||
#[error("the packet size information was malformed: {0}")]
|
||||
InvalidPacketSize(#[from] InvalidPacketSize),
|
||||
|
||||
#[error("the packet mode information was malformed - {0}")]
|
||||
#[error("the packet mode information was malformed: {0}")]
|
||||
InvalidPacketType(#[from] InvalidPacketType),
|
||||
|
||||
#[error("encountered an IO error - {0}")]
|
||||
#[error("the packet version information was malformed: {0}")]
|
||||
InvalidPacketVersion(#[from] InvalidPacketVersion),
|
||||
|
||||
#[error("received unsupported packet version {received}. max supported is {max_supported}")]
|
||||
UnsupportedPacketVersion {
|
||||
received: PacketVersion,
|
||||
max_supported: PacketVersion,
|
||||
},
|
||||
|
||||
#[error("encountered an IO error: {0}")]
|
||||
IoError(#[from] io::Error),
|
||||
|
||||
#[error("encountered a packet error - {0}")]
|
||||
#[error("encountered a packet error: {0}")]
|
||||
NymPacket(#[from] NymPacketError),
|
||||
|
||||
#[error("could not convert to bytes")]
|
||||
@@ -56,7 +66,7 @@ impl Decoder for NymCodec {
|
||||
if src.is_empty() {
|
||||
// can't do anything if we have no bytes, but let's reserve enough for the most
|
||||
// conservative case, i.e. receiving an ack packet
|
||||
src.reserve(Header::LEGACY_SIZE + PacketSize::AckPacket.size());
|
||||
src.reserve(Header::SIZE + PacketSize::AckPacket.size());
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
@@ -68,7 +78,7 @@ impl Decoder for NymCodec {
|
||||
};
|
||||
|
||||
let packet_size = header.packet_size.size();
|
||||
let frame_len = header.size() + packet_size;
|
||||
let frame_len = Header::SIZE + packet_size;
|
||||
|
||||
if src.len() < frame_len {
|
||||
// we don't have enough bytes to read the rest of frame
|
||||
@@ -77,7 +87,7 @@ impl Decoder for NymCodec {
|
||||
}
|
||||
|
||||
// advance buffer past the header - at this point we have enough bytes
|
||||
src.advance(header.size());
|
||||
src.advance(Header::SIZE);
|
||||
let packet_bytes = src.split_to(packet_size);
|
||||
let packet = if let Some(slice) = packet_bytes.get(..) {
|
||||
// here it could be debatable whether stream is corrupt or not,
|
||||
@@ -85,8 +95,6 @@ impl Decoder for NymCodec {
|
||||
match header.packet_type {
|
||||
PacketType::Outfox => NymPacket::outfox_from_bytes(slice)?,
|
||||
PacketType::Mix => NymPacket::sphinx_from_bytes(slice)?,
|
||||
#[allow(deprecated)]
|
||||
PacketType::Vpn => NymPacket::sphinx_from_bytes(slice)?,
|
||||
}
|
||||
} else {
|
||||
return Ok(None);
|
||||
@@ -106,11 +114,11 @@ impl Decoder for NymCodec {
|
||||
// we also assume the next packet coming from the same client will use exactly the same versioning
|
||||
// as the current packet
|
||||
|
||||
let mut allocate_for_next_packet = header.size() + PacketSize::AckPacket.size();
|
||||
let mut allocate_for_next_packet = Header::SIZE + PacketSize::AckPacket.size();
|
||||
if !src.is_empty() {
|
||||
match Header::decode(src) {
|
||||
Ok(Some(next_header)) => {
|
||||
allocate_for_next_packet = next_header.size() + next_header.packet_size.size();
|
||||
allocate_for_next_packet = Header::SIZE + next_header.packet_size.size();
|
||||
}
|
||||
Ok(None) => {
|
||||
// we don't have enough information to know how much to reserve, fallback to the ack case
|
||||
@@ -201,8 +209,15 @@ mod packet_encoding {
|
||||
SphinxDelay::new_from_nanos(42),
|
||||
SphinxDelay::new_from_nanos(42),
|
||||
];
|
||||
NymPacket::sphinx_build(size.payload_size(), b"foomp", &route, &destination, &delays)
|
||||
.unwrap()
|
||||
NymPacket::sphinx_build(
|
||||
false,
|
||||
size.payload_size(),
|
||||
b"foomp",
|
||||
&route,
|
||||
&destination,
|
||||
&delays,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -254,34 +269,10 @@ mod packet_encoding {
|
||||
assert!(NymCodec.decode(&mut empty_bytes).unwrap().is_none());
|
||||
assert_eq!(
|
||||
empty_bytes.capacity(),
|
||||
Header::LEGACY_SIZE + PacketSize::AckPacket.size()
|
||||
Header::SIZE + PacketSize::AckPacket.size()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_bytes_with_legacy_header() {
|
||||
// if header gets decoded there should be enough bytes for the entire frame
|
||||
let packet_sizes = vec![
|
||||
PacketSize::AckPacket,
|
||||
PacketSize::RegularPacket,
|
||||
PacketSize::ExtendedPacket8,
|
||||
PacketSize::ExtendedPacket16,
|
||||
PacketSize::ExtendedPacket32,
|
||||
];
|
||||
for packet_size in packet_sizes {
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::Legacy,
|
||||
packet_size,
|
||||
..Default::default()
|
||||
};
|
||||
let mut bytes = BytesMut::new();
|
||||
header.encode(&mut bytes);
|
||||
assert!(NymCodec.decode(&mut bytes).unwrap().is_none());
|
||||
|
||||
assert_eq!(bytes.capacity(), Header::LEGACY_SIZE + packet_size.size())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_bytes_with_versioned_header() {
|
||||
// if header gets decoded there should be enough bytes for the entire frame
|
||||
@@ -294,7 +285,7 @@ mod packet_encoding {
|
||||
];
|
||||
for packet_size in packet_sizes {
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::Versioned(123),
|
||||
packet_version: PacketVersion::new(),
|
||||
packet_size,
|
||||
..Default::default()
|
||||
};
|
||||
@@ -302,33 +293,10 @@ mod packet_encoding {
|
||||
header.encode(&mut bytes);
|
||||
assert!(NymCodec.decode(&mut bytes).unwrap().is_none());
|
||||
|
||||
assert_eq!(
|
||||
bytes.capacity(),
|
||||
Header::VERSIONED_SIZE + packet_size.size()
|
||||
)
|
||||
assert_eq!(bytes.capacity(), Header::SIZE + packet_size.size())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_full_frame_with_legacy_header() {
|
||||
// if full frame is used exactly, there should be enough space for header + ack packet
|
||||
let packet = FramedNymPacket {
|
||||
header: Header {
|
||||
packet_version: PacketVersion::Legacy,
|
||||
..Default::default()
|
||||
},
|
||||
packet: make_valid_sphinx_packet(Default::default()),
|
||||
};
|
||||
|
||||
let mut bytes = BytesMut::new();
|
||||
NymCodec.encode(packet, &mut bytes).unwrap();
|
||||
assert!(NymCodec.decode(&mut bytes).unwrap().is_some());
|
||||
assert_eq!(
|
||||
bytes.capacity(),
|
||||
Header::LEGACY_SIZE + PacketSize::AckPacket.size()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_full_frame_with_versioned_header() {
|
||||
// if full frame is used exactly, there should be enough space for header + ack packet
|
||||
@@ -342,40 +310,10 @@ mod packet_encoding {
|
||||
assert!(NymCodec.decode(&mut bytes).unwrap().is_some());
|
||||
assert_eq!(
|
||||
bytes.capacity(),
|
||||
Header::VERSIONED_SIZE + PacketSize::AckPacket.size()
|
||||
Header::SIZE + PacketSize::AckPacket.size()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_full_frame_with_extra_bytes_with_legacy_header() {
|
||||
// if there was at least 2 byte left, there should be enough space for entire next frame
|
||||
let packet_sizes = vec![
|
||||
PacketSize::AckPacket,
|
||||
PacketSize::RegularPacket,
|
||||
PacketSize::ExtendedPacket8,
|
||||
PacketSize::ExtendedPacket16,
|
||||
PacketSize::ExtendedPacket32,
|
||||
];
|
||||
|
||||
for packet_size in packet_sizes {
|
||||
let first_packet = FramedNymPacket {
|
||||
header: Header {
|
||||
packet_version: PacketVersion::Legacy,
|
||||
..Default::default()
|
||||
},
|
||||
packet: make_valid_sphinx_packet(Default::default()),
|
||||
};
|
||||
|
||||
let mut bytes = BytesMut::new();
|
||||
NymCodec.encode(first_packet, &mut bytes).unwrap();
|
||||
bytes.put_u8(packet_size as u8);
|
||||
bytes.put_u8(PacketType::default() as u8);
|
||||
assert!(NymCodec.decode(&mut bytes).unwrap().is_some());
|
||||
|
||||
assert!(bytes.capacity() >= Header::LEGACY_SIZE + packet_size.size())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn for_full_frame_with_extra_bytes_with_versioned_header() {
|
||||
// if there was at least 3 byte left, there should be enough space for entire next frame
|
||||
@@ -395,7 +333,7 @@ mod packet_encoding {
|
||||
|
||||
let mut bytes = BytesMut::new();
|
||||
NymCodec.encode(first_packet, &mut bytes).unwrap();
|
||||
bytes.put_u8(PacketVersion::new_versioned(123).as_u8().unwrap());
|
||||
bytes.put_u8(PacketVersion::new().as_u8());
|
||||
bytes.put_u8(packet_size as u8);
|
||||
bytes.put_u8(PacketType::default() as u8);
|
||||
assert!(NymCodec.decode(&mut bytes).unwrap().is_some());
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
use crate::codec::NymCodecError;
|
||||
use bytes::{BufMut, BytesMut};
|
||||
use nym_sphinx_params::packet_sizes::PacketSize;
|
||||
use nym_sphinx_params::packet_version::PacketVersion;
|
||||
use nym_sphinx_params::packet_version::{PacketVersion, CURRENT_PACKET_VERSION};
|
||||
use nym_sphinx_params::PacketType;
|
||||
use nym_sphinx_types::NymPacket;
|
||||
|
||||
@@ -47,6 +47,14 @@ impl FramedNymPacket {
|
||||
pub fn into_inner(self) -> NymPacket {
|
||||
self.packet
|
||||
}
|
||||
|
||||
pub fn packet(&self) -> &NymPacket {
|
||||
&self.packet
|
||||
}
|
||||
|
||||
pub fn is_sphinx(&self) -> bool {
|
||||
self.packet.is_sphinx()
|
||||
}
|
||||
}
|
||||
|
||||
// Contains any metadata that might be useful for sending between mix nodes.
|
||||
@@ -73,8 +81,7 @@ pub struct Header {
|
||||
}
|
||||
|
||||
impl Header {
|
||||
pub(crate) const LEGACY_SIZE: usize = 2;
|
||||
pub(crate) const VERSIONED_SIZE: usize = 3;
|
||||
pub(crate) const SIZE: usize = 3;
|
||||
|
||||
pub fn outfox() -> Header {
|
||||
Header {
|
||||
@@ -84,53 +91,39 @@ impl Header {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn size(&self) -> usize {
|
||||
if self.packet_version.is_legacy() {
|
||||
Self::LEGACY_SIZE
|
||||
} else {
|
||||
Self::VERSIONED_SIZE
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn encode(&self, dst: &mut BytesMut) {
|
||||
// we reserve one byte for `packet_size` and the other for `mode`
|
||||
dst.reserve(Self::LEGACY_SIZE);
|
||||
if let Some(version) = self.packet_version.as_u8() {
|
||||
dst.reserve(Self::VERSIONED_SIZE);
|
||||
dst.put_u8(version)
|
||||
}
|
||||
dst.reserve(Self::SIZE);
|
||||
|
||||
dst.put_u8(self.packet_version.as_u8());
|
||||
dst.put_u8(self.packet_size as u8);
|
||||
dst.put_u8(self.packet_type as u8);
|
||||
|
||||
// reserve bytes for the actual packet
|
||||
dst.reserve(self.packet_size.size());
|
||||
}
|
||||
|
||||
pub(crate) fn decode(src: &mut BytesMut) -> Result<Option<Self>, NymCodecError> {
|
||||
if src.len() < Self::LEGACY_SIZE {
|
||||
if src.len() < Self::SIZE {
|
||||
// can't do anything if we don't have enough bytes - but reserve enough for the next call
|
||||
src.reserve(Self::LEGACY_SIZE);
|
||||
src.reserve(Self::SIZE);
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let packet_version = PacketVersion::from(src[0]);
|
||||
if packet_version.is_legacy() {
|
||||
Ok(Some(Header {
|
||||
packet_version,
|
||||
packet_size: PacketSize::try_from(src[0])?,
|
||||
packet_type: PacketType::try_from(src[1])?,
|
||||
}))
|
||||
} else if src.len() < Self::VERSIONED_SIZE {
|
||||
// we're missing that 1 byte to read the full header...
|
||||
src.reserve(Self::VERSIONED_SIZE);
|
||||
Ok(None)
|
||||
} else {
|
||||
Ok(Some(Header {
|
||||
packet_version,
|
||||
packet_size: PacketSize::try_from(src[1])?,
|
||||
packet_type: PacketType::try_from(src[2])?,
|
||||
}))
|
||||
let packet_version = PacketVersion::try_from(src[0])?;
|
||||
if packet_version > CURRENT_PACKET_VERSION {
|
||||
// received an unsupported packet version - we don't know how it's meant to look like!
|
||||
// (this is in preparation for the dual support of breaking sphinx changes)
|
||||
return Err(NymCodecError::UnsupportedPacketVersion {
|
||||
received: packet_version,
|
||||
max_supported: CURRENT_PACKET_VERSION,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Some(Header {
|
||||
packet_version,
|
||||
packet_size: PacketSize::try_from(src[1])?,
|
||||
packet_type: PacketType::try_from(src[2])?,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +150,7 @@ mod header_encoding {
|
||||
// due to the hack used to get legacy mode compatibility
|
||||
let mut bytes = BytesMut::from(
|
||||
[
|
||||
PacketVersion::new_versioned(123).as_u8().unwrap(),
|
||||
PacketVersion::new().as_u8(),
|
||||
unknown_packet_size,
|
||||
PacketType::default() as u8,
|
||||
]
|
||||
@@ -172,7 +165,14 @@ mod header_encoding {
|
||||
// make sure this is still 'unknown' for if we make changes in the future
|
||||
assert!(PacketType::try_from(unknown_packet_type).is_err());
|
||||
|
||||
let mut bytes = BytesMut::from([PacketSize::default() as u8, unknown_packet_type].as_ref());
|
||||
let mut bytes = BytesMut::from(
|
||||
[
|
||||
PacketVersion::new().as_u8(),
|
||||
PacketSize::default() as u8,
|
||||
unknown_packet_type,
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
assert!(Header::decode(&mut bytes).is_err())
|
||||
}
|
||||
|
||||
@@ -181,16 +181,16 @@ mod header_encoding {
|
||||
let mut empty_bytes = BytesMut::new();
|
||||
let decode_attempt_1 = Header::decode(&mut empty_bytes).unwrap();
|
||||
assert!(decode_attempt_1.is_none());
|
||||
assert!(empty_bytes.capacity() > Header::LEGACY_SIZE);
|
||||
assert!(empty_bytes.capacity() > Header::SIZE);
|
||||
|
||||
let mut empty_bytes = BytesMut::with_capacity(1);
|
||||
let decode_attempt_2 = Header::decode(&mut empty_bytes).unwrap();
|
||||
assert!(decode_attempt_2.is_none());
|
||||
assert!(empty_bytes.capacity() > Header::LEGACY_SIZE);
|
||||
assert!(empty_bytes.capacity() > Header::SIZE);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_in_legacy_mode() {
|
||||
fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_() {
|
||||
let packet_sizes = vec![
|
||||
PacketSize::AckPacket,
|
||||
PacketSize::RegularPacket,
|
||||
@@ -200,7 +200,7 @@ mod header_encoding {
|
||||
];
|
||||
for packet_size in packet_sizes {
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::Legacy,
|
||||
packet_version: PacketVersion::new(),
|
||||
packet_size,
|
||||
..Default::default()
|
||||
};
|
||||
@@ -211,23 +211,26 @@ mod header_encoding {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_in_versioned_mode() {
|
||||
let packet_sizes = vec![
|
||||
PacketSize::AckPacket,
|
||||
PacketSize::RegularPacket,
|
||||
PacketSize::ExtendedPacket8,
|
||||
PacketSize::ExtendedPacket16,
|
||||
PacketSize::ExtendedPacket32,
|
||||
];
|
||||
for packet_size in packet_sizes {
|
||||
let header = Header {
|
||||
packet_version: PacketVersion::Versioned(123),
|
||||
packet_size,
|
||||
..Default::default()
|
||||
};
|
||||
let mut bytes = BytesMut::new();
|
||||
header.encode(&mut bytes);
|
||||
assert_eq!(bytes.capacity(), bytes.len() + packet_size.size())
|
||||
fn header_decoding_will_reject_future_versions() {
|
||||
let future_version = PacketVersion::try_from(123).unwrap();
|
||||
|
||||
let unchecked_header = Header {
|
||||
packet_version: future_version,
|
||||
packet_size: PacketSize::RegularPacket,
|
||||
packet_type: PacketType::Mix,
|
||||
};
|
||||
let mut bytes = BytesMut::new();
|
||||
unchecked_header.encode(&mut bytes);
|
||||
|
||||
match Header::decode(&mut bytes).unwrap_err() {
|
||||
NymCodecError::UnsupportedPacketVersion {
|
||||
received,
|
||||
max_supported,
|
||||
} => {
|
||||
assert_eq!(received, future_version);
|
||||
assert_eq!(max_supported, CURRENT_PACKET_VERSION);
|
||||
}
|
||||
_ => panic!("unexpected error variant"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
use log::{debug, error, info, trace};
|
||||
// Copyright 2021-2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::packet::FramedNymPacket;
|
||||
use nym_sphinx_acknowledgements::surb_ack::{SurbAck, SurbAckRecoveryError};
|
||||
use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError};
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
use nym_sphinx_params::{PacketSize, PacketType};
|
||||
use nym_sphinx_types::header::shared_secret::ExpandedSharedSecret;
|
||||
use nym_sphinx_types::{
|
||||
Delay as SphinxDelay, DestinationAddressBytes, NodeAddressBytes, NymPacket, NymPacketError,
|
||||
NymProcessedPacket, OutfoxError, PrivateKey, ProcessedPacketData, SphinxError,
|
||||
Version as SphinxPacketVersion,
|
||||
NymProcessedPacket, OutfoxError, OutfoxProcessedPacket, PrivateKey, ProcessedPacketData,
|
||||
SphinxError, Version as SphinxPacketVersion, REPLAY_TAG_SIZE,
|
||||
};
|
||||
use std::fmt::Display;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::packet::FramedNymPacket;
|
||||
use nym_metrics::nanos;
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
use tracing::{debug, error, info, trace};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MixProcessingResultData {
|
||||
@@ -49,6 +51,26 @@ pub struct MixProcessingResult {
|
||||
pub processing_data: MixProcessingResultData,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum PartialMixProcessingResult {
|
||||
Sphinx {
|
||||
expanded_shared_secret: ExpandedSharedSecret,
|
||||
},
|
||||
Outfox,
|
||||
}
|
||||
|
||||
impl PartialMixProcessingResult {
|
||||
pub fn replay_tag(&self) -> Option<&[u8; REPLAY_TAG_SIZE]> {
|
||||
match self {
|
||||
PartialMixProcessingResult::Sphinx {
|
||||
expanded_shared_secret,
|
||||
} => Some(expanded_shared_secret.replay_tag()),
|
||||
PartialMixProcessingResult::Outfox => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ForwardAck = MixPacket;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -75,59 +97,192 @@ pub enum PacketProcessingError {
|
||||
#[error("failed to recover the expected SURB-Ack packet: {0}")]
|
||||
MalformedSurbAck(#[from] SurbAckRecoveryError),
|
||||
|
||||
#[error("the received packet was set to use the very old and very much deprecated 'VPN' mode")]
|
||||
ReceivedOldTypeVpnPacket,
|
||||
|
||||
#[error("failed to process received outfox packet: {0}")]
|
||||
OutfoxProcessingError(#[from] OutfoxError),
|
||||
|
||||
#[error("attempted to partially process an outfox packet")]
|
||||
PartialOutfoxProcessing,
|
||||
|
||||
#[error("this packet has already been processed before")]
|
||||
PacketReplay,
|
||||
}
|
||||
|
||||
pub struct PartiallyUnwrappedPacket {
|
||||
received_data: FramedNymPacket,
|
||||
partial_result: PartialMixProcessingResult,
|
||||
}
|
||||
|
||||
impl PartiallyUnwrappedPacket {
|
||||
/// Attempt to partially unwrap received packet to derive relevant keys
|
||||
/// to allow us to reject it for obvious bad behaviour (like replay or invalid mac)
|
||||
/// without performing full processing
|
||||
pub fn new(
|
||||
received_data: FramedNymPacket,
|
||||
sphinx_key: &PrivateKey,
|
||||
) -> Result<Self, PacketProcessingError> {
|
||||
let partial_result = match received_data.packet() {
|
||||
NymPacket::Sphinx(packet) => {
|
||||
let expanded_shared_secret =
|
||||
packet.header.compute_expanded_shared_secret(sphinx_key);
|
||||
|
||||
// don't continue if the header is malformed
|
||||
packet
|
||||
.header
|
||||
.ensure_header_integrity(&expanded_shared_secret)?;
|
||||
|
||||
PartialMixProcessingResult::Sphinx {
|
||||
expanded_shared_secret,
|
||||
}
|
||||
}
|
||||
|
||||
NymPacket::Outfox(_) => PartialMixProcessingResult::Outfox,
|
||||
};
|
||||
Ok(PartiallyUnwrappedPacket {
|
||||
received_data,
|
||||
partial_result,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn finalise_unwrapping(self) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
let packet_size = self.received_data.packet_size();
|
||||
let packet_type = self.received_data.packet_type();
|
||||
|
||||
let packet = self.received_data.into_inner();
|
||||
|
||||
// currently partial unwrapping is only implemented for sphinx packets.
|
||||
// attempting to call it for anything else should result in a failure
|
||||
let (
|
||||
NymPacket::Sphinx(packet),
|
||||
PartialMixProcessingResult::Sphinx {
|
||||
expanded_shared_secret,
|
||||
},
|
||||
) = (packet, self.partial_result)
|
||||
else {
|
||||
return Err(PacketProcessingError::PartialOutfoxProcessing);
|
||||
};
|
||||
let processed_packet = packet.process_with_expanded_secret(&expanded_shared_secret)?;
|
||||
wrap_processed_sphinx_packet(processed_packet, packet_size, packet_type)
|
||||
}
|
||||
|
||||
pub fn replay_tag(&self) -> Option<&[u8; REPLAY_TAG_SIZE]> {
|
||||
self.partial_result.replay_tag()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(FramedNymPacket, PartialMixProcessingResult)> for PartiallyUnwrappedPacket {
|
||||
fn from(
|
||||
(received_data, partial_result): (FramedNymPacket, PartialMixProcessingResult),
|
||||
) -> Self {
|
||||
PartiallyUnwrappedPacket {
|
||||
received_data,
|
||||
partial_result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_framed_packet(
|
||||
received: FramedNymPacket,
|
||||
sphinx_key: &PrivateKey,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
nanos!("process_received", {
|
||||
let packet_size = received.packet_size();
|
||||
let packet_type = received.packet_type();
|
||||
let packet_size = received.packet_size();
|
||||
let packet_type = received.packet_type();
|
||||
|
||||
// unwrap the sphinx packet and if possible and appropriate, cache keys
|
||||
let processed_packet = perform_framed_unwrapping(received, sphinx_key)?;
|
||||
// unwrap the sphinx packet
|
||||
let processed_packet = perform_framed_unwrapping(received, sphinx_key)?;
|
||||
|
||||
// for forward packets, extract next hop and set delay (but do NOT delay here)
|
||||
// for final packets, extract SURBAck
|
||||
let final_processing_result =
|
||||
perform_final_processing(processed_packet, packet_size, packet_type);
|
||||
|
||||
if final_processing_result.is_err() {
|
||||
error!("{:?}", final_processing_result)
|
||||
}
|
||||
|
||||
final_processing_result
|
||||
})
|
||||
// for forward packets, extract next hop and set delay (but do NOT delay here)
|
||||
// for final packets, extract SURBAck
|
||||
perform_final_processing(processed_packet, packet_size, packet_type)
|
||||
}
|
||||
|
||||
fn perform_framed_unwrapping(
|
||||
received: FramedNymPacket,
|
||||
sphinx_key: &PrivateKey,
|
||||
) -> Result<NymProcessedPacket, PacketProcessingError> {
|
||||
nanos!("perform_initial_unwrapping", {
|
||||
let packet = received.into_inner();
|
||||
perform_framed_packet_processing(packet, sphinx_key)
|
||||
})
|
||||
let packet = received.into_inner();
|
||||
perform_framed_packet_processing(packet, sphinx_key)
|
||||
}
|
||||
|
||||
fn perform_framed_packet_processing(
|
||||
packet: NymPacket,
|
||||
sphinx_key: &PrivateKey,
|
||||
) -> Result<NymProcessedPacket, PacketProcessingError> {
|
||||
nanos!("perform_initial_packet_processing", {
|
||||
packet.process(sphinx_key).map_err(|err| {
|
||||
debug!("Failed to unwrap NymPacket packet: {err}");
|
||||
PacketProcessingError::NymPacketProcessingError(err)
|
||||
})
|
||||
packet.process(sphinx_key).map_err(|err| {
|
||||
debug!("Failed to unwrap NymPacket packet: {err}");
|
||||
PacketProcessingError::NymPacketProcessingError(err)
|
||||
})
|
||||
}
|
||||
|
||||
fn wrap_processed_sphinx_packet(
|
||||
packet: nym_sphinx_types::ProcessedPacket,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
let processing_data = match packet.data {
|
||||
ProcessedPacketData::ForwardHop {
|
||||
next_hop_packet,
|
||||
next_hop_address,
|
||||
delay,
|
||||
} => process_forward_hop(
|
||||
NymPacket::Sphinx(next_hop_packet),
|
||||
next_hop_address,
|
||||
delay,
|
||||
packet_type,
|
||||
),
|
||||
// right now there's no use for the surb_id included in the header - probably it should get removed from the
|
||||
// sphinx all together?
|
||||
ProcessedPacketData::FinalHop {
|
||||
destination,
|
||||
identifier: _,
|
||||
payload,
|
||||
} => process_final_hop(
|
||||
destination,
|
||||
payload.recover_plaintext()?,
|
||||
packet_size,
|
||||
packet_type,
|
||||
),
|
||||
}?;
|
||||
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Sphinx(packet.version),
|
||||
processing_data,
|
||||
})
|
||||
}
|
||||
|
||||
fn wrap_processed_outfox_packet(
|
||||
packet: OutfoxProcessedPacket,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
let next_address = *packet.next_address();
|
||||
let packet = packet.into_packet();
|
||||
if packet.is_final_hop() {
|
||||
let processing_data = process_final_hop(
|
||||
DestinationAddressBytes::from_bytes(next_address),
|
||||
packet.recover_plaintext()?.to_vec(),
|
||||
packet_size,
|
||||
packet_type,
|
||||
)?;
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Outfox,
|
||||
processing_data,
|
||||
})
|
||||
} else {
|
||||
let packet = MixPacket::new(
|
||||
NymNodeRoutingAddress::try_from_bytes(&next_address)?,
|
||||
NymPacket::Outfox(packet),
|
||||
PacketType::Outfox,
|
||||
);
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Outfox,
|
||||
processing_data: MixProcessingResultData::ForwardHop {
|
||||
packet,
|
||||
delay: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn perform_final_processing(
|
||||
packet: NymProcessedPacket,
|
||||
packet_size: PacketSize,
|
||||
@@ -135,64 +290,10 @@ fn perform_final_processing(
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
match packet {
|
||||
NymProcessedPacket::Sphinx(packet) => {
|
||||
let processing_data = match packet.data {
|
||||
ProcessedPacketData::ForwardHop {
|
||||
next_hop_packet,
|
||||
next_hop_address,
|
||||
delay,
|
||||
} => process_forward_hop(
|
||||
NymPacket::Sphinx(next_hop_packet),
|
||||
next_hop_address,
|
||||
delay,
|
||||
packet_type,
|
||||
),
|
||||
// right now there's no use for the surb_id included in the header - probably it should get removed from the
|
||||
// sphinx all together?
|
||||
ProcessedPacketData::FinalHop {
|
||||
destination,
|
||||
identifier: _,
|
||||
payload,
|
||||
} => process_final_hop(
|
||||
destination,
|
||||
payload.recover_plaintext()?,
|
||||
packet_size,
|
||||
packet_type,
|
||||
),
|
||||
}?;
|
||||
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Sphinx(packet.version),
|
||||
processing_data,
|
||||
})
|
||||
wrap_processed_sphinx_packet(packet, packet_size, packet_type)
|
||||
}
|
||||
NymProcessedPacket::Outfox(packet) => {
|
||||
let next_address = *packet.next_address();
|
||||
let packet = packet.into_packet();
|
||||
if packet.is_final_hop() {
|
||||
let processing_data = process_final_hop(
|
||||
DestinationAddressBytes::from_bytes(next_address),
|
||||
packet.recover_plaintext()?.to_vec(),
|
||||
packet_size,
|
||||
packet_type,
|
||||
)?;
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Outfox,
|
||||
processing_data,
|
||||
})
|
||||
} else {
|
||||
let packet = MixPacket::new(
|
||||
NymNodeRoutingAddress::try_from_bytes(&next_address)?,
|
||||
NymPacket::Outfox(packet),
|
||||
PacketType::Outfox,
|
||||
);
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Outfox,
|
||||
processing_data: MixProcessingResultData::ForwardHop {
|
||||
packet,
|
||||
delay: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
wrap_processed_outfox_packet(packet, packet_size, packet_type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,17 +22,6 @@ pub mod packet_version;
|
||||
pub const FRAG_ID_LEN: usize = 5;
|
||||
pub type SerializedFragmentIdentifier = [u8; FRAG_ID_LEN];
|
||||
|
||||
// wait, wait, but why are we starting with version 7?
|
||||
// when packet header gets serialized, the following bytes (in that order) are put onto the wire:
|
||||
// - packet_version (starting with v1.1.0)
|
||||
// - packet_size indicator
|
||||
// - packet_type
|
||||
// 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,
|
||||
// otherwise we should treat it as legacy
|
||||
/// Increment it whenever we perform any breaking change in the wire format!
|
||||
const CURRENT_PACKET_VERSION_NUMBER: u8 = 7;
|
||||
|
||||
// TODO: ask @AP about the choice of below algorithms
|
||||
|
||||
/// Hashing algorithm used during hkdf for ephemeral shared key generation per sphinx packet payload.
|
||||
|
||||
@@ -272,9 +272,6 @@ impl PacketSize {
|
||||
let overhead = match packet_type {
|
||||
#[cfg(feature = "sphinx")]
|
||||
PacketType::Mix => SPHINX_PACKET_OVERHEAD,
|
||||
#[allow(deprecated)]
|
||||
#[cfg(feature = "sphinx")]
|
||||
PacketType::Vpn => SPHINX_PACKET_OVERHEAD,
|
||||
#[cfg(feature = "outfox")]
|
||||
PacketType::Outfox => OUTFOX_PACKET_OVERHEAD,
|
||||
_ => 0,
|
||||
|
||||
@@ -27,11 +27,6 @@ pub enum PacketType {
|
||||
#[serde(alias = "sphinx")]
|
||||
Mix = 0,
|
||||
|
||||
/// Represents a packet that should be sent through the network as fast as possible.
|
||||
#[deprecated]
|
||||
#[serde(rename = "unsupported-mix-vpn")]
|
||||
Vpn = 1,
|
||||
|
||||
/// Abusing this to add Outfox support
|
||||
#[serde(rename = "outfox")]
|
||||
Outfox = 2,
|
||||
@@ -41,8 +36,6 @@ impl fmt::Display for PacketType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
PacketType::Mix => write!(f, "Mix"),
|
||||
#[allow(deprecated)]
|
||||
PacketType::Vpn => write!(f, "Vpn"),
|
||||
PacketType::Outfox => write!(f, "Outfox"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,56 +2,70 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{PacketSize, CURRENT_PACKET_VERSION_NUMBER};
|
||||
// wait, wait, but why are we starting with version 7?
|
||||
// when packet header gets serialized, the following bytes (in that order) are put onto the wire:
|
||||
// - packet_version (starting with v1.1.0)
|
||||
// - packet_size indicator
|
||||
// - packet_type
|
||||
// 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,
|
||||
// otherwise we should treat it as legacy
|
||||
/// Increment it whenever we perform any breaking change in the wire format!
|
||||
pub const INITIAL_PACKET_VERSION_NUMBER: u8 = 7;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PacketVersion {
|
||||
// this will allow updated mixnodes to still understand packets from before the update
|
||||
Legacy,
|
||||
Versioned(u8),
|
||||
pub const CURRENT_PACKET_VERSION_NUMBER: u8 = INITIAL_PACKET_VERSION_NUMBER;
|
||||
pub const CURRENT_PACKET_VERSION: PacketVersion =
|
||||
PacketVersion::unchecked(CURRENT_PACKET_VERSION_NUMBER);
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||
pub struct PacketVersion(u8);
|
||||
|
||||
impl Display for PacketVersion {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
self.0.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
#[error("attempted to use legacy packet version")]
|
||||
pub struct InvalidPacketVersion;
|
||||
|
||||
impl PacketVersion {
|
||||
pub fn new() -> Self {
|
||||
Self::new_versioned(CURRENT_PACKET_VERSION_NUMBER)
|
||||
PacketVersion(CURRENT_PACKET_VERSION_NUMBER)
|
||||
}
|
||||
|
||||
pub fn new_legacy() -> Self {
|
||||
PacketVersion::Legacy
|
||||
const fn unchecked(version: u8) -> PacketVersion {
|
||||
PacketVersion(version)
|
||||
}
|
||||
|
||||
pub fn new_versioned(version: u8) -> Self {
|
||||
PacketVersion::Versioned(version)
|
||||
}
|
||||
|
||||
pub fn is_legacy(&self) -> bool {
|
||||
matches!(self, PacketVersion::Legacy)
|
||||
}
|
||||
|
||||
pub fn as_u8(&self) -> Option<u8> {
|
||||
match self {
|
||||
PacketVersion::Legacy => None,
|
||||
PacketVersion::Versioned(version) => Some(*version),
|
||||
}
|
||||
pub fn as_u8(&self) -> u8 {
|
||||
(*self).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u8> for PacketVersion {
|
||||
fn from(v: u8) -> Self {
|
||||
match v {
|
||||
n if n == PacketSize::RegularPacket as u8 => PacketVersion::Legacy,
|
||||
n if n == PacketSize::AckPacket as u8 => PacketVersion::Legacy,
|
||||
n if n == PacketSize::ExtendedPacket8 as u8 => PacketVersion::Legacy,
|
||||
n if n == PacketSize::ExtendedPacket16 as u8 => PacketVersion::Legacy,
|
||||
n if n == PacketSize::ExtendedPacket32 as u8 => PacketVersion::Legacy,
|
||||
n => PacketVersion::Versioned(n),
|
||||
impl TryFrom<u8> for PacketVersion {
|
||||
type Error = InvalidPacketVersion;
|
||||
|
||||
fn try_from(value: u8) -> Result<Self, Self::Error> {
|
||||
if value < INITIAL_PACKET_VERSION_NUMBER {
|
||||
return Err(InvalidPacketVersion);
|
||||
}
|
||||
Ok(PacketVersion(value))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PacketVersion> for u8 {
|
||||
fn from(packet_version: PacketVersion) -> Self {
|
||||
packet_version.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PacketVersion {
|
||||
fn default() -> Self {
|
||||
PacketVersion::Versioned(CURRENT_PACKET_VERSION_NUMBER)
|
||||
PacketVersion::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,7 +113,8 @@ impl NymMessage {
|
||||
match self {
|
||||
NymMessage::Plain(data) => data,
|
||||
NymMessage::Repliable(repliable) => match repliable.content {
|
||||
RepliableMessageContent::Data { message, .. } => message,
|
||||
RepliableMessageContent::Data(content) => content.message,
|
||||
RepliableMessageContent::DataV2(content) => content.message,
|
||||
_ => Vec::new(),
|
||||
},
|
||||
NymMessage::Reply(reply) => match reply.content {
|
||||
@@ -309,6 +310,7 @@ mod tests {
|
||||
// a single variant for each repliable and reply is enough as they are more thoroughly tested
|
||||
// internally
|
||||
let repliable = NymMessage::new_repliable(RepliableMessage::new_data(
|
||||
true,
|
||||
vec![1, 2, 3, 4, 5],
|
||||
[42u8; 16].into(),
|
||||
vec![],
|
||||
|
||||
@@ -51,29 +51,14 @@ impl From<PreparedFragment> for MixPacket {
|
||||
pub trait FragmentPreparer {
|
||||
type Rng: CryptoRng + Rng;
|
||||
|
||||
fn use_legacy_sphinx_format(&self) -> bool;
|
||||
|
||||
fn deterministic_route_selection(&self) -> bool;
|
||||
fn rng(&mut self) -> &mut Self::Rng;
|
||||
fn nonce(&self) -> i32;
|
||||
fn average_packet_delay(&self) -> Duration;
|
||||
fn average_ack_delay(&self) -> Duration;
|
||||
|
||||
fn generate_reply_surbs(
|
||||
&mut self,
|
||||
amount: usize,
|
||||
topology: &NymRouteProvider,
|
||||
reply_recipient: &Recipient,
|
||||
) -> Result<Vec<ReplySurb>, NymTopologyError> {
|
||||
let mut reply_surbs = Vec::with_capacity(amount);
|
||||
let packet_delay = self.average_packet_delay();
|
||||
for _ in 0..amount {
|
||||
let reply_surb =
|
||||
ReplySurb::construct(self.rng(), reply_recipient, packet_delay, topology)?;
|
||||
reply_surbs.push(reply_surb)
|
||||
}
|
||||
|
||||
Ok(reply_surbs)
|
||||
}
|
||||
|
||||
fn generate_surb_ack(
|
||||
&mut self,
|
||||
recipient: &Recipient,
|
||||
@@ -83,9 +68,11 @@ pub trait FragmentPreparer {
|
||||
packet_type: PacketType,
|
||||
) -> Result<SurbAck, NymTopologyError> {
|
||||
let ack_delay = self.average_ack_delay();
|
||||
let use_legacy_sphinx_format = self.use_legacy_sphinx_format();
|
||||
|
||||
SurbAck::construct(
|
||||
self.rng(),
|
||||
use_legacy_sphinx_format,
|
||||
recipient,
|
||||
ack_key,
|
||||
fragment_id.to_bytes(),
|
||||
@@ -264,14 +251,7 @@ pub trait FragmentPreparer {
|
||||
Some(packet_size.plaintext_size()),
|
||||
)?,
|
||||
PacketType::Mix => NymPacket::sphinx_build(
|
||||
packet_size.payload_size(),
|
||||
packet_payload,
|
||||
&route,
|
||||
&destination,
|
||||
&delays,
|
||||
)?,
|
||||
#[allow(deprecated)]
|
||||
PacketType::Vpn => NymPacket::sphinx_build(
|
||||
self.use_legacy_sphinx_format(),
|
||||
packet_size.payload_size(),
|
||||
packet_payload,
|
||||
&route,
|
||||
@@ -331,6 +311,10 @@ pub struct MessagePreparer<R> {
|
||||
/// Average delay an acknowledgement packet is going to get delay at a single mixnode.
|
||||
average_ack_delay: Duration,
|
||||
|
||||
/// Specify whether any constructed packets should use the legacy format,
|
||||
/// where the payload keys are explicitly attached rather than using the seeds
|
||||
use_legacy_sphinx_format: bool,
|
||||
|
||||
nonce: i32,
|
||||
}
|
||||
|
||||
@@ -344,6 +328,7 @@ where
|
||||
sender_address: Recipient,
|
||||
average_packet_delay: Duration,
|
||||
average_ack_delay: Duration,
|
||||
use_legacy_sphinx_format: bool,
|
||||
) -> Self {
|
||||
let mut rng = rng;
|
||||
let nonce = rng.gen();
|
||||
@@ -353,6 +338,7 @@ where
|
||||
sender_address,
|
||||
average_packet_delay,
|
||||
average_ack_delay,
|
||||
use_legacy_sphinx_format,
|
||||
nonce,
|
||||
}
|
||||
}
|
||||
@@ -364,6 +350,7 @@ where
|
||||
|
||||
pub fn generate_reply_surbs(
|
||||
&mut self,
|
||||
use_legacy_reply_surb_format: bool,
|
||||
amount: usize,
|
||||
topology: &NymRouteProvider,
|
||||
) -> Result<Vec<ReplySurb>, NymTopologyError> {
|
||||
@@ -373,6 +360,7 @@ where
|
||||
&mut self.rng,
|
||||
&self.sender_address,
|
||||
self.average_packet_delay,
|
||||
use_legacy_reply_surb_format,
|
||||
topology,
|
||||
)?;
|
||||
reply_surbs.push(reply_surb)
|
||||
@@ -454,6 +442,10 @@ where
|
||||
impl<R: CryptoRng + Rng> FragmentPreparer for MessagePreparer<R> {
|
||||
type Rng = R;
|
||||
|
||||
fn use_legacy_sphinx_format(&self) -> bool {
|
||||
self.use_legacy_sphinx_format
|
||||
}
|
||||
|
||||
fn deterministic_route_selection(&self) -> bool {
|
||||
self.deterministic_route_selection
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{array::TryFromSliceError, fmt};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(feature = "outfox")]
|
||||
use nym_outfox::packet::{OutfoxPacket, OutfoxProcessedPacket};
|
||||
pub use nym_outfox::packet::{OutfoxPacket, OutfoxProcessedPacket};
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
pub use sphinx_packet::{SphinxPacket, SphinxPacketBuilder};
|
||||
@@ -21,15 +21,18 @@ pub use nym_outfox::{
|
||||
pub use sphinx_packet::{
|
||||
constants::{
|
||||
self, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, MAX_PATH_LENGTH, NODE_ADDRESS_LENGTH,
|
||||
PAYLOAD_KEY_SIZE,
|
||||
PAYLOAD_KEY_SIZE, REPLAY_TAG_SIZE,
|
||||
},
|
||||
crypto::{self, PrivateKey, PublicKey},
|
||||
header::{self, delays, delays::Delay, ProcessedHeader, SphinxHeader, HEADER_SIZE},
|
||||
packet::builder::DEFAULT_PAYLOAD_SIZE,
|
||||
payload::{Payload, PAYLOAD_OVERHEAD_SIZE},
|
||||
payload::{
|
||||
key::{PayloadKey, PayloadKeySeed},
|
||||
Payload, PAYLOAD_OVERHEAD_SIZE,
|
||||
},
|
||||
route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier},
|
||||
surb::{SURBMaterial, SURB},
|
||||
version::Version,
|
||||
version::*,
|
||||
Error as SphinxError, ProcessedPacket, ProcessedPacketData,
|
||||
};
|
||||
|
||||
@@ -84,17 +87,25 @@ impl fmt::Debug for NymPacket {
|
||||
impl NymPacket {
|
||||
#[cfg(feature = "sphinx")]
|
||||
pub fn sphinx_build<M: AsRef<[u8]>>(
|
||||
use_legacy_sphinx_format: bool,
|
||||
size: usize,
|
||||
message: M,
|
||||
route: &[Node],
|
||||
destination: &Destination,
|
||||
delays: &[Delay],
|
||||
) -> Result<NymPacket, NymPacketError> {
|
||||
Ok(NymPacket::Sphinx(
|
||||
SphinxPacketBuilder::new()
|
||||
.with_payload_size(size)
|
||||
.build_packet(message, route, destination, delays)?,
|
||||
))
|
||||
let mut builder = SphinxPacketBuilder::new().with_payload_size(size);
|
||||
|
||||
if use_legacy_sphinx_format {
|
||||
builder = builder.with_version(X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION)
|
||||
};
|
||||
|
||||
Ok(NymPacket::Sphinx(builder.build_packet(
|
||||
message,
|
||||
route,
|
||||
destination,
|
||||
delays,
|
||||
)?))
|
||||
}
|
||||
#[cfg(feature = "sphinx")]
|
||||
pub fn sphinx_from_bytes(bytes: &[u8]) -> Result<NymPacket, NymPacketError> {
|
||||
@@ -176,10 +187,15 @@ impl NymPacket {
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
pub fn as_sphinx_packet(self) -> Option<SphinxPacket> {
|
||||
pub fn to_sphinx_packet(self) -> Option<SphinxPacket> {
|
||||
match self {
|
||||
NymPacket::Sphinx(packet) => Some(packet),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
pub fn is_sphinx(&self) -> bool {
|
||||
matches!(self, NymPacket::Sphinx(_))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ impl ShutdownManager {
|
||||
legacy_task_manager: None,
|
||||
shutdown_signals: Default::default(),
|
||||
tracker: Default::default(),
|
||||
max_shutdown_duration: Default::default(),
|
||||
max_shutdown_duration: Duration::from_secs(10),
|
||||
};
|
||||
|
||||
// we need to add an explicit watcher for the cancellation token being cancelled
|
||||
|
||||
@@ -181,6 +181,14 @@ pub struct TrafficWasm {
|
||||
/// Controls whether the sent sphinx packet use the NON-DEFAULT bigger size.
|
||||
pub use_extended_packet_size: bool,
|
||||
|
||||
/// Specify whether any constructed sphinx packets should use the legacy format,
|
||||
/// where the payload keys are explicitly attached rather than using the seeds
|
||||
/// this affects any forward packets, acks and reply surbs
|
||||
/// this flag should remain disabled until sufficient number of nodes on the network has upgraded
|
||||
/// and support updated format.
|
||||
/// in the case of reply surbs, the recipient must also understand the new encoding
|
||||
pub use_legacy_sphinx_format: bool,
|
||||
|
||||
/// Controls whether the sent packets should use outfox as opposed to the default sphinx.
|
||||
pub use_outfox: bool,
|
||||
}
|
||||
@@ -214,6 +222,7 @@ impl From<TrafficWasm> for ConfigTraffic {
|
||||
.disable_main_poisson_packet_distribution,
|
||||
primary_packet_size: PacketSize::RegularPacket,
|
||||
secondary_packet_size: use_extended_packet_size,
|
||||
use_legacy_sphinx_format: traffic.use_legacy_sphinx_format,
|
||||
packet_type,
|
||||
}
|
||||
}
|
||||
@@ -229,6 +238,7 @@ impl From<ConfigTraffic> for TrafficWasm {
|
||||
maximum_number_of_retransmissions: traffic.maximum_number_of_retransmissions,
|
||||
disable_main_poisson_packet_distribution: traffic
|
||||
.disable_main_poisson_packet_distribution,
|
||||
use_legacy_sphinx_format: traffic.use_legacy_sphinx_format,
|
||||
use_extended_packet_size: traffic.secondary_packet_size.is_some(),
|
||||
use_outfox: traffic.packet_type == PacketType::Outfox,
|
||||
}
|
||||
@@ -423,7 +433,6 @@ impl From<TopologyWasm> for ConfigTopology {
|
||||
max_startup_gateway_waiting_period: Duration::from_millis(
|
||||
topology.max_startup_gateway_waiting_period_ms as u64,
|
||||
),
|
||||
topology_structure: Default::default(),
|
||||
minimum_mixnode_performance: topology.minimum_mixnode_performance,
|
||||
minimum_gateway_performance: topology.minimum_gateway_performance,
|
||||
use_extended_topology: topology.use_extended_topology,
|
||||
|
||||
@@ -109,6 +109,15 @@ pub struct TrafficWasmOverride {
|
||||
#[tsify(optional)]
|
||||
pub use_extended_packet_size: Option<bool>,
|
||||
|
||||
/// Specify whether any constructed sphinx packets should use the legacy format,
|
||||
/// where the payload keys are explicitly attached rather than using the seeds
|
||||
/// this affects any forward packets, acks and reply surbs
|
||||
/// this flag should remain disabled until sufficient number of nodes on the network has upgraded
|
||||
/// and support updated format.
|
||||
/// in the case of reply surbs, the recipient must also understand the new encoding
|
||||
#[tsify(optional)]
|
||||
pub use_legacy_sphinx_format: Option<bool>,
|
||||
|
||||
/// Controls whether the sent packets should use outfox as opposed to the default sphinx.
|
||||
#[tsify(optional)]
|
||||
pub use_outfox: Option<bool>,
|
||||
@@ -132,6 +141,9 @@ impl From<TrafficWasmOverride> for TrafficWasm {
|
||||
disable_main_poisson_packet_distribution: value
|
||||
.disable_main_poisson_packet_distribution
|
||||
.unwrap_or(def.disable_main_poisson_packet_distribution),
|
||||
use_legacy_sphinx_format: value
|
||||
.use_legacy_sphinx_format
|
||||
.unwrap_or(def.use_legacy_sphinx_format),
|
||||
use_extended_packet_size: value
|
||||
.use_extended_packet_size
|
||||
.unwrap_or(def.use_extended_packet_size),
|
||||
|
||||
@@ -5,9 +5,7 @@ import { MyTab } from 'components/generic-tabs.tsx';
|
||||
|
||||
# Pre-built Binaries
|
||||
|
||||
This page is for operators who prefer to download ready made binaries. The [Github releases page](https://github.com/nymtech/nym/releases) has pre-built binaries which work on **Ubuntu 22.04 and Debian 12 upwards**, but at this stage cannot be guaranteed to work everywhere.
|
||||
|
||||
They should work on any distro with `libc` >= v2.33 and above.
|
||||
This page is for operators who prefer to download ready made binaries. The [Github releases page](https://github.com/nymtech/nym/releases) has pre-built binaries which should work on Ubuntu 22.04 and other Debian-based systems, but at this stage cannot be guaranteed to work everywhere.
|
||||
|
||||
If the pre-built binaries don't work or are unavailable for your system, you will need to build the platform yourself.
|
||||
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
CONFIGURED=true
|
||||
|
||||
RUST_LOG=info
|
||||
RUST_BACKTRACE=1
|
||||
|
||||
BECH32_PREFIX=n
|
||||
MIX_DENOM=unym
|
||||
MIX_DENOM_DISPLAY=nym
|
||||
STAKE_DENOM=unyx
|
||||
STAKE_DENOM_DISPLAY=nyx
|
||||
DENOMS_EXPONENT=6
|
||||
|
||||
REWARDING_VALIDATOR_ADDRESS=n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa
|
||||
MIXNET_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav
|
||||
VESTING_CONTRACT_ADDRESS=n1unyuj8qnmygvzuex3dwmg9yzt9alhvyeat0uu0jedg2wj33efl5qackslz
|
||||
ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jlwz9
|
||||
GROUP_CONTRACT_ADDRESS=n1ewmwz97xm0h8rdk8sw7h9mwn866qkx9hl9zlmagqfkhuzvwk5hhq844ue9
|
||||
MULTISIG_CONTRACT_ADDRESS=n1tz0setr8vkh9udp8xyxgpqc89ns27k4d0jx2h942hr0ax63yjhmqz6xct8
|
||||
COCONUT_DKG_CONTRACT_ADDRESS=n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865utqj5elvh
|
||||
|
||||
|
||||
STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0"
|
||||
NYXD="https://rpc.sandbox.nymtech.net"
|
||||
NYM_API="https://sandbox-nym-api1.nymtech.net/api"
|
||||
|
||||
GEOIP_DB_PATH=geo_ip/GeoLite2-City.mmdb
|
||||
# MaxMind account ID
|
||||
# TODO replace with your own account ID
|
||||
GEOIPUPDATE_ACCOUNT_ID=xxx
|
||||
# MaxMind license key
|
||||
# TODO replace with your own license key
|
||||
GEOIPUPDATE_LICENSE_KEY=xxx
|
||||
# List of space-separated database edition IDs. Edition IDs may
|
||||
# consist of letters, digits, and dashes. For example, GeoIP2-City
|
||||
# would download the GeoIP2 City database (GeoIP2-City).
|
||||
GEOIPUPDATE_EDITION_IDS=GeoLite2-City
|
||||
# The number of hours between geoipupdate runs. If this is not set
|
||||
# or is set to 0, geoipupdate will run once and exit.
|
||||
GEOIPUPDATE_FREQUENCY=72
|
||||
# The path to the directory where geoipupdate will download the
|
||||
# database.
|
||||
GEOIP_DB_DIRECTORY=./geo_ip
|
||||
@@ -1,2 +0,0 @@
|
||||
# The path to the geoip database file
|
||||
GEOIP_DB_PATH=./geo_ip/GeoLite2-City.mmdb
|
||||
@@ -1,4 +0,0 @@
|
||||
target
|
||||
explorer-api-state.json
|
||||
/geo_ip
|
||||
!.env.dev
|
||||
@@ -1,39 +0,0 @@
|
||||
[package]
|
||||
name = "explorer-api"
|
||||
version = "1.1.48"
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
chrono = { workspace = true, features = ["serde"] }
|
||||
clap = { workspace = true, features = ["cargo", "derive"] }
|
||||
dotenvy = { workspace = true }
|
||||
humantime-serde = { workspace = true }
|
||||
isocountry = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
log = { workspace = true }
|
||||
maxminddb = { workspace = true }
|
||||
okapi = { workspace = true, features = ["impl_json_schema"] }
|
||||
pretty_env_logger = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
rand_pcg = { workspace = true }
|
||||
rand_seeder = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
rocket = { workspace = true, features = ["json"] }
|
||||
rocket_cors = { workspace = true }
|
||||
rocket_okapi = { workspace = true, features = ["swagger"] }
|
||||
schemars = { workspace = true, features = ["preserve_order"] }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
nym-bin-common = { path = "../common/bin-common"}
|
||||
nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-explorer-api-requests = { path = "explorer-api-requests" }
|
||||
nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-network-defaults = { path = "../common/network-defaults" }
|
||||
nym-task = { path = "../common/task" }
|
||||
nym-validator-client = { path = "../common/client-libs/validator-client", features=["http-client"] }
|
||||
@@ -1,60 +0,0 @@
|
||||
# Network Explorer API
|
||||
|
||||
An API that provides data for the [Network Explorer](../explorer).
|
||||
|
||||
Features:
|
||||
|
||||
- geolocates mixnodes using https://dev.maxmind.com/geoip/geolite2-free-geolocation-data
|
||||
- calculates how many nodes are in each country
|
||||
- proxies mixnode API requests to add HTTPS
|
||||
|
||||
## Development
|
||||
|
||||
Several environment variables are required. They can be
|
||||
provisioned via a `.env` file. For convenience a `.env.dev` is
|
||||
provided, just copy its content into `.env`.
|
||||
|
||||
Follow the steps to setup the geoip database.
|
||||
|
||||
## GeoIP db install/update
|
||||
|
||||
A geoip database needs to be installed.
|
||||
|
||||
We use https://github.com/maxmind/geoipupdate to automatically
|
||||
download and update GeoLite2 binary database. For convenience we
|
||||
run it as a docker container.
|
||||
|
||||
At the root of the repo, inside the `docker-compose.yml`, there
|
||||
is a docker service `geoipupdate` for it.
|
||||
|
||||
Supposed you provided an `.env` file with **all the environment
|
||||
variables** listed in `.env.sample-dev` (found at the root),
|
||||
simply run the service through docker:
|
||||
|
||||
```shell
|
||||
docker compose up -d geoipupdate
|
||||
```
|
||||
|
||||
Running this command will automatically install (and update) the
|
||||
db file inside the directory path provided by `GEOIP_DB_DIRECTORY`
|
||||
env variable.
|
||||
|
||||
## Running
|
||||
|
||||
When starting the explorer-api, supply the environment variable
|
||||
`GEOIP_DB_PATH`, pointing to the GeoLite2 binary database file.
|
||||
It should be previously installed thanks to `geoipupdate` service.
|
||||
|
||||
Note: As mentioned above the explorer-api binary reads the provided `.env` file.
|
||||
|
||||
Run as a service and reverse proxy with `nginx` to add `https` with Lets Encrypt.
|
||||
|
||||
Setup nginx to inject the request IP to the header `X-Real-IP`.
|
||||
|
||||
# TODO / Known Issues
|
||||
|
||||
## TODO
|
||||
|
||||
- record the number of mixnodes on a given date and write to a file for later retrieval
|
||||
- dependency injection
|
||||
- tests
|
||||
@@ -1,13 +0,0 @@
|
||||
[package]
|
||||
name = "nym-explorer-api-requests"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
nym-api-requests = { path = "../../nym-api/nym-api-requests" }
|
||||
schemars = { workspace = true, features = ["preserve_order"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
ts-rs = { workspace = true, optional = true }
|
||||
@@ -1,85 +0,0 @@
|
||||
use nym_api_requests::models::{DescribedNodeType, NodePerformance, NymNodeData};
|
||||
use nym_contracts_common::Percent;
|
||||
use nym_mixnet_contract_common::{
|
||||
Addr, Coin, Delegation, Gateway, LegacyMixLayer, MixNode, NodeId, NodeRewarding, NymNodeBond,
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum MixnodeStatus {
|
||||
Active, // in both the active set and the rewarded set
|
||||
Standby, // only in the rewarded set
|
||||
Inactive, // in neither the rewarded set nor the active set
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PrettyDetailedMixNodeBond {
|
||||
pub mix_id: NodeId,
|
||||
pub location: Option<Location>,
|
||||
pub status: MixnodeStatus,
|
||||
pub pledge_amount: Coin,
|
||||
pub total_delegation: Coin,
|
||||
pub owner: Addr,
|
||||
pub layer: LegacyMixLayer,
|
||||
pub mix_node: MixNode,
|
||||
pub stake_saturation: f32,
|
||||
pub uncapped_saturation: f32,
|
||||
pub avg_uptime: u8,
|
||||
pub node_performance: NodePerformance,
|
||||
pub estimated_operator_apy: f64,
|
||||
pub estimated_delegators_apy: f64,
|
||||
pub operating_cost: Coin,
|
||||
pub profit_margin_percent: Percent,
|
||||
pub family_id: Option<u16>,
|
||||
pub blacklisted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, JsonSchema, Serialize, Deserialize)]
|
||||
pub struct Location {
|
||||
pub two_letter_iso_country_code: String,
|
||||
pub three_letter_iso_country_code: String,
|
||||
pub country_name: String,
|
||||
pub latitude: Option<f64>,
|
||||
pub longitude: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct PrettyDetailedGatewayBond {
|
||||
pub pledge_amount: Coin,
|
||||
pub owner: Addr,
|
||||
pub block_height: u64,
|
||||
pub gateway: Gateway,
|
||||
pub proxy: Option<Addr>,
|
||||
pub location: Option<Location>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct NymNodeWithDescriptionAndLocation {
|
||||
pub node_id: NodeId,
|
||||
pub contract_node_type: Option<DescribedNodeType>,
|
||||
pub description: Option<NymNodeData>,
|
||||
pub bond_information: NymNodeBond,
|
||||
pub rewarding_details: NodeRewarding,
|
||||
pub location: Option<Location>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct NymNodeWithDescriptionAndLocationAndDelegations {
|
||||
pub node_id: NodeId,
|
||||
pub contract_node_type: Option<DescribedNodeType>,
|
||||
pub description: Option<NymNodeData>,
|
||||
pub bond_information: NymNodeBond,
|
||||
pub rewarding_details: NodeRewarding,
|
||||
pub location: Option<Location>,
|
||||
pub delegations: Option<Vec<Delegation>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct NymVestingAccount {
|
||||
pub locked: Coin,
|
||||
pub vested: Coin,
|
||||
pub vesting: Coin,
|
||||
pub spendable: Coin,
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
[package]
|
||||
name = "nym-explorer-client"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
nym-explorer-api-requests = { path = "../explorer-api-requests" }
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
serde.workspace = true
|
||||
thiserror.workspace = true
|
||||
url.workspace = true
|
||||
tracing = {workspace = true, features = ["attributes"]}
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
@@ -1,118 +0,0 @@
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use std::time::Duration;
|
||||
|
||||
use reqwest::StatusCode;
|
||||
use thiserror::Error;
|
||||
use tracing::instrument;
|
||||
use url::Url;
|
||||
|
||||
// Re-export request types
|
||||
pub use nym_explorer_api_requests::{
|
||||
Location, PrettyDetailedGatewayBond, PrettyDetailedMixNodeBond,
|
||||
};
|
||||
|
||||
// Paths
|
||||
const API_VERSION: &str = "v1";
|
||||
const TMP: &str = "tmp";
|
||||
const UNSTABLE: &str = "unstable";
|
||||
const MIXNODES: &str = "mix-nodes";
|
||||
const GATEWAYS: &str = "gateways";
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
const REQUEST_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ExplorerApiError {
|
||||
#[error("REST request error: {0}")]
|
||||
ReqwestError(#[from] reqwest::Error),
|
||||
|
||||
#[error("URL parse error: {0}")]
|
||||
UrlParseError(#[from] url::ParseError),
|
||||
|
||||
#[error("not found")]
|
||||
NotFound,
|
||||
|
||||
#[error("request failure: {0}")]
|
||||
RequestFailure(String),
|
||||
}
|
||||
|
||||
pub struct ExplorerClient {
|
||||
url: Url,
|
||||
client: reqwest::Client,
|
||||
}
|
||||
|
||||
impl ExplorerClient {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn new(url: url::Url) -> Result<Self, ExplorerApiError> {
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(REQUEST_TIMEOUT)
|
||||
.build()?;
|
||||
Ok(Self { client, url })
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub fn new_with_timeout(url: url::Url, timeout: Duration) -> Result<Self, ExplorerApiError> {
|
||||
let client = reqwest::Client::builder().timeout(timeout).build()?;
|
||||
Ok(Self { client, url })
|
||||
}
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub fn new(url: url::Url) -> Result<Self, ExplorerApiError> {
|
||||
let client = reqwest::Client::builder().build()?;
|
||||
Ok(Self { client, url })
|
||||
}
|
||||
|
||||
async fn send_get_request(
|
||||
&self,
|
||||
paths: &[&str],
|
||||
) -> Result<reqwest::Response, ExplorerApiError> {
|
||||
let url = combine_url(self.url.clone(), paths)?;
|
||||
tracing::debug!("Sending GET request");
|
||||
Ok(self.client.get(url).send().await?)
|
||||
}
|
||||
|
||||
#[instrument(level = "trace", skip_all, fields(paths=?paths))]
|
||||
async fn query_explorer_api<T>(&self, paths: &[&str]) -> Result<T, ExplorerApiError>
|
||||
where
|
||||
T: std::fmt::Debug,
|
||||
T: for<'a> serde::Deserialize<'a>,
|
||||
{
|
||||
let response = self.send_get_request(paths).await?;
|
||||
if response.status().is_success() {
|
||||
let res = response.json::<T>().await?;
|
||||
tracing::trace!("Got response: {res:?}");
|
||||
Ok(res)
|
||||
} else if response.status() == StatusCode::NOT_FOUND {
|
||||
Err(ExplorerApiError::NotFound)
|
||||
} else {
|
||||
let status = response.status();
|
||||
let err_msg = format!("{}: {}", response.text().await?, status);
|
||||
Err(ExplorerApiError::RequestFailure(err_msg))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_mixnodes(&self) -> Result<Vec<PrettyDetailedMixNodeBond>, ExplorerApiError> {
|
||||
self.query_explorer_api(&[API_VERSION, MIXNODES]).await
|
||||
}
|
||||
|
||||
pub async fn get_gateways(&self) -> Result<Vec<PrettyDetailedGatewayBond>, ExplorerApiError> {
|
||||
self.query_explorer_api(&[API_VERSION, GATEWAYS]).await
|
||||
}
|
||||
|
||||
pub async fn unstable_get_gateways(
|
||||
&self,
|
||||
) -> Result<Vec<PrettyDetailedGatewayBond>, ExplorerApiError> {
|
||||
self.query_explorer_api(&[API_VERSION, TMP, UNSTABLE, GATEWAYS])
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
fn combine_url(mut base_url: Url, paths: &[&str]) -> Result<Url, ExplorerApiError> {
|
||||
{
|
||||
let mut segments = base_url.path_segments_mut().expect("failed to parse url");
|
||||
for path in paths {
|
||||
segments.push(path);
|
||||
}
|
||||
}
|
||||
Ok(base_url)
|
||||
}
|
||||
Vendored
-105
@@ -1,105 +0,0 @@
|
||||
use std::borrow::Borrow;
|
||||
use std::collections::HashMap;
|
||||
use std::hash::Hash;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
const DEFAULT_CACHE_VALIDITY: Duration = Duration::from_secs(60);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Cache<K, V: Clone> {
|
||||
inner: HashMap<K, CacheItem<V>>,
|
||||
cache_validity_duration: Duration,
|
||||
}
|
||||
|
||||
impl<K, V: Clone> Cache<K, V>
|
||||
where
|
||||
K: Eq + Hash,
|
||||
{
|
||||
pub(crate) fn new() -> Self {
|
||||
Cache {
|
||||
inner: HashMap::new(),
|
||||
cache_validity_duration: DEFAULT_CACHE_VALIDITY,
|
||||
}
|
||||
}
|
||||
|
||||
// it felt like this might be an useful addition if we want to keep our caches with different
|
||||
// validity durations
|
||||
#[allow(unused)]
|
||||
pub(crate) fn with_validity_duration(mut self, new_cache_validity: Duration) -> Self {
|
||||
self.cache_validity_duration = new_cache_validity;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
pub(crate) fn get_all(&self) -> Vec<V> {
|
||||
self.inner
|
||||
.values()
|
||||
.map(|cache_item| cache_item.value.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn get<Q>(&self, key: &Q) -> Option<V>
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
Q: ?Sized + Hash + Eq,
|
||||
{
|
||||
self.inner
|
||||
.get(key)
|
||||
.filter(|cache_item| cache_item.valid_until >= SystemTime::now())
|
||||
.map(|cache_item| cache_item.value.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn set(&mut self, key: K, value: V) {
|
||||
self.inner.insert(
|
||||
key,
|
||||
CacheItem {
|
||||
valid_until: SystemTime::now() + self.cache_validity_duration,
|
||||
value,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn remove<Q>(&mut self, key: &Q) -> Option<V>
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
Q: ?Sized + Hash + Eq,
|
||||
{
|
||||
self.inner.remove(key).map(|item| item.value)
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) fn remove_if_expired<Q>(&mut self, key: &Q) -> Option<V>
|
||||
where
|
||||
K: Borrow<Q>,
|
||||
Q: ?Sized + Hash + Eq,
|
||||
{
|
||||
if self.inner.get(key)?.has_expired() {
|
||||
self.remove(key)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
// it seems like something should be running on timer calling this method on all of our caches
|
||||
#[allow(unused)]
|
||||
pub(crate) fn remove_all_expired(&mut self) {
|
||||
self.inner.retain(|_, v| !v.has_expired())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CacheItem<T> {
|
||||
pub(crate) value: T,
|
||||
pub(crate) valid_until: std::time::SystemTime,
|
||||
}
|
||||
|
||||
impl<T> CacheItem<T> {
|
||||
fn has_expired(&self) -> bool {
|
||||
let now = SystemTime::now();
|
||||
self.valid_until < now
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
use nym_network_defaults::{
|
||||
var_names::{NYM_API, NYXD},
|
||||
NymNetworkDetails,
|
||||
};
|
||||
use nym_validator_client::QueryHttpRpcValidatorClient;
|
||||
use reqwest::Url;
|
||||
use std::{str::FromStr, sync::Arc};
|
||||
|
||||
// since this is just a query client, we don't need any locking mechanism to keep sequence numbers in check
|
||||
// nor we need to access any of its methods taking mutable reference (like updating api URL)
|
||||
// when that becomes a requirement, we would simply put an extra RwLock (or Mutex) in here
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ThreadsafeValidatorClient(pub(crate) Arc<QueryHttpRpcValidatorClient>);
|
||||
|
||||
impl ThreadsafeValidatorClient {
|
||||
pub(crate) fn new() -> Self {
|
||||
new_validator_client()
|
||||
}
|
||||
|
||||
pub(crate) fn api_endpoint(&self) -> &Url {
|
||||
self.0.nym_api.current_url()
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_validator_client() -> ThreadsafeValidatorClient {
|
||||
let nyxd_url = Url::from_str(&std::env::var(NYXD).expect("nyxd validator not set"))
|
||||
.expect("nyxd validator not in url format");
|
||||
let api_url = Url::from_str(&std::env::var(NYM_API).expect("nyxd validator not set"))
|
||||
.expect("nyxd validator not in url format");
|
||||
|
||||
let details = NymNetworkDetails::new_from_env();
|
||||
let client_config = nym_validator_client::Config::try_from_nym_network_details(&details)
|
||||
.expect("failed to construct valid validator client config with the provided network")
|
||||
.with_urls(nyxd_url, api_url);
|
||||
|
||||
ThreadsafeValidatorClient(Arc::new(
|
||||
nym_validator_client::Client::new_query(client_config).expect("Failed to connect to nyxd!"),
|
||||
))
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// Copyright 2022-2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use clap::Parser;
|
||||
use nym_bin_common::bin_info;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
fn pretty_build_info_static() -> &'static str {
|
||||
static PRETTY_BUILD_INFORMATION: OnceLock<String> = OnceLock::new();
|
||||
PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print())
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author = "Nymtech", version, about, long_version = pretty_build_info_static())]
|
||||
pub(crate) struct Cli {
|
||||
/// Path pointing to an env file that configures the explorer api.
|
||||
#[clap(short, long)]
|
||||
pub(crate) config_env_file: Option<std::path::PathBuf>,
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
pub type CountryNodesDistribution = HashMap<String, u32>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ThreadsafeCountryNodesDistribution {
|
||||
inner: Arc<RwLock<CountryNodesDistribution>>,
|
||||
}
|
||||
|
||||
impl ThreadsafeCountryNodesDistribution {
|
||||
pub(crate) fn new() -> Self {
|
||||
ThreadsafeCountryNodesDistribution {
|
||||
inner: Arc::new(RwLock::new(CountryNodesDistribution::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_from_distribution(
|
||||
country_node_distribution: CountryNodesDistribution,
|
||||
) -> Self {
|
||||
ThreadsafeCountryNodesDistribution {
|
||||
inner: Arc::new(RwLock::new(country_node_distribution)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn set_all(&mut self, country_node_distribution: CountryNodesDistribution) {
|
||||
self.inner
|
||||
.write()
|
||||
.await
|
||||
.clone_from(&country_node_distribution)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_all(&self) -> HashMap<String, u32> {
|
||||
self.inner.read().await.clone()
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
use log::info;
|
||||
use nym_task::TaskClient;
|
||||
|
||||
use crate::country_statistics::country_nodes_distribution::CountryNodesDistribution;
|
||||
use crate::COUNTRY_DATA_REFRESH_INTERVAL;
|
||||
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
|
||||
pub(crate) struct CountryStatisticsDistributionTask {
|
||||
state: ExplorerApiStateContext,
|
||||
shutdown: TaskClient,
|
||||
}
|
||||
|
||||
impl CountryStatisticsDistributionTask {
|
||||
pub(crate) fn new(state: ExplorerApiStateContext, shutdown: TaskClient) -> Self {
|
||||
CountryStatisticsDistributionTask { state, shutdown }
|
||||
}
|
||||
|
||||
pub(crate) fn start(mut self) {
|
||||
info!("Spawning mix node country distribution task runner...");
|
||||
tokio::spawn(async move {
|
||||
let mut interval_timer = tokio::time::interval(std::time::Duration::from_secs(
|
||||
COUNTRY_DATA_REFRESH_INTERVAL,
|
||||
));
|
||||
while !self.shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = interval_timer.tick() => {
|
||||
self.calculate_nodes_per_country().await;
|
||||
}
|
||||
_ = self.shutdown.recv() => {
|
||||
trace!("Listener: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Retrieves the current list of mixnodes from the validators and calculates how many nodes are in each country
|
||||
async fn calculate_nodes_per_country(&mut self) {
|
||||
let cache = self.state.inner.mixnodes.get_locations().await;
|
||||
let nym_nodes = self
|
||||
.state
|
||||
.inner
|
||||
.nymnodes
|
||||
.get_bonded_nymnodes_locations()
|
||||
.await;
|
||||
|
||||
let mut three_letter_iso_country_codes: Vec<String> = cache
|
||||
.values()
|
||||
.flat_map(|i| {
|
||||
i.location
|
||||
.as_ref()
|
||||
.map(|j| j.three_letter_iso_country_code.clone())
|
||||
})
|
||||
.collect();
|
||||
|
||||
for node in nym_nodes {
|
||||
three_letter_iso_country_codes.push(node.three_letter_iso_country_code);
|
||||
}
|
||||
|
||||
let mut distribution = CountryNodesDistribution::new();
|
||||
|
||||
info!("Calculating country distribution from located mixnodes...");
|
||||
for three_letter_iso_country_code in three_letter_iso_country_codes {
|
||||
*(distribution.entry(three_letter_iso_country_code)).or_insert(0) += 1;
|
||||
}
|
||||
|
||||
// replace the shared distribution to be the new distribution
|
||||
self.state
|
||||
.inner
|
||||
.country_node_distribution
|
||||
.set_all(distribution)
|
||||
.await;
|
||||
|
||||
info!(
|
||||
"Mixnode country distribution done: {:?}",
|
||||
self.state.inner.country_node_distribution.get_all().await
|
||||
);
|
||||
|
||||
// keep state on disk, so that when this process dies it can start up again and users get some data
|
||||
self.state.write_to_file().await;
|
||||
}
|
||||
}
|
||||
@@ -1,254 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
use log::{info, warn};
|
||||
use nym_explorer_api_requests::Location;
|
||||
use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT;
|
||||
use nym_task::TaskClient;
|
||||
|
||||
pub(crate) struct GeoLocateTask {
|
||||
state: ExplorerApiStateContext,
|
||||
shutdown: TaskClient,
|
||||
}
|
||||
|
||||
impl GeoLocateTask {
|
||||
pub(crate) fn new(state: ExplorerApiStateContext, shutdown: TaskClient) -> Self {
|
||||
GeoLocateTask { state, shutdown }
|
||||
}
|
||||
|
||||
pub(crate) fn start(mut self) {
|
||||
info!("Spawning locator task runner...");
|
||||
tokio::spawn(async move {
|
||||
let mut interval_timer = tokio::time::interval(std::time::Duration::from_millis(50));
|
||||
while !self.shutdown.is_shutdown() {
|
||||
tokio::select! {
|
||||
_ = interval_timer.tick() => {
|
||||
self.locate_mix_nodes().await;
|
||||
self.locate_gateways().await;
|
||||
self.locate_nym_nodes().await;
|
||||
}
|
||||
_ = self.shutdown.recv() => {
|
||||
trace!("Listener: Received shutdown");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async fn locate_mix_nodes(&mut self) {
|
||||
// I'm unwrapping to the default value to get rid of an extra indentation level from the `if let Some(...) = ...`
|
||||
// If the value is None, we'll unwrap to an empty hashmap and the `values()` loop won't do any work anyway
|
||||
let mixnode_bonds = self
|
||||
.state
|
||||
.inner
|
||||
.mixnodes
|
||||
.get_mixnodes()
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let geo_ip = self.state.inner.geo_ip.0.clone();
|
||||
|
||||
for (i, cache_item) in mixnode_bonds.values().enumerate() {
|
||||
if self
|
||||
.state
|
||||
.inner
|
||||
.mixnodes
|
||||
.is_location_valid(cache_item.mix_id())
|
||||
.await
|
||||
{
|
||||
// when the cached location is valid, don't locate and continue to next mix node
|
||||
continue;
|
||||
}
|
||||
|
||||
match geo_ip.query(
|
||||
&cache_item.mix_node().host,
|
||||
Some(cache_item.mix_node().mix_port),
|
||||
) {
|
||||
Ok(opt) => match opt {
|
||||
Some(location) => {
|
||||
let location: Location = location.into();
|
||||
|
||||
trace!(
|
||||
"{} mix nodes already located. Ip {} is located in {:#?}",
|
||||
i,
|
||||
cache_item.mix_node().host,
|
||||
location.three_letter_iso_country_code,
|
||||
);
|
||||
|
||||
if i > 0 && (i % 100) == 0 {
|
||||
info!("Located {} mixnodes...", i + 1,);
|
||||
}
|
||||
|
||||
self.state
|
||||
.inner
|
||||
.mixnodes
|
||||
.set_location(cache_item.mix_id(), Some(location))
|
||||
.await;
|
||||
|
||||
// one node has been located, so return out of the loop
|
||||
return;
|
||||
}
|
||||
None => {
|
||||
warn!("❌ Location for {} not found.", cache_item.mix_node().host);
|
||||
self.state
|
||||
.inner
|
||||
.mixnodes
|
||||
.set_location(cache_item.mix_id(), None)
|
||||
.await;
|
||||
}
|
||||
},
|
||||
Err(_e) => {
|
||||
// warn!(
|
||||
// "❌ Oh no! Location for {} failed. Error: {:#?}",
|
||||
// cache_item.mix_node().host,
|
||||
// e
|
||||
// );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
trace!("All mix nodes located");
|
||||
}
|
||||
|
||||
async fn locate_nym_nodes(&mut self) {
|
||||
// I'm unwrapping to the default value to get rid of an extra indentation level from the `if let Some(...) = ...`
|
||||
// If the value is None, we'll unwrap to an empty hashmap and the `values()` loop won't do any work anyway
|
||||
let nym_nodes = self.state.inner.nymnodes.get_bonded_nymnodes().await;
|
||||
|
||||
let geo_ip = self.state.inner.geo_ip.0.clone();
|
||||
|
||||
for (i, cache_item) in nym_nodes.values().enumerate() {
|
||||
if self
|
||||
.state
|
||||
.inner
|
||||
.nymnodes
|
||||
.is_location_valid(cache_item.node_id())
|
||||
.await
|
||||
{
|
||||
// when the cached location is valid, don't locate and continue to next mix node
|
||||
continue;
|
||||
}
|
||||
|
||||
let bonded_host = &cache_item.bond_information.node.host;
|
||||
|
||||
match geo_ip.query(
|
||||
bonded_host,
|
||||
Some(
|
||||
cache_item
|
||||
.bond_information
|
||||
.node
|
||||
.custom_http_port
|
||||
.unwrap_or(DEFAULT_NYM_NODE_HTTP_PORT),
|
||||
),
|
||||
) {
|
||||
Ok(opt) => match opt {
|
||||
Some(location) => {
|
||||
let location: Location = location.into();
|
||||
|
||||
trace!(
|
||||
"{} mix nodes already located. host {} is located in {:#?}",
|
||||
i,
|
||||
bonded_host,
|
||||
location.three_letter_iso_country_code,
|
||||
);
|
||||
|
||||
if i > 0 && (i % 100) == 0 {
|
||||
info!("Located {} nym-nodes...", i + 1,);
|
||||
}
|
||||
|
||||
self.state
|
||||
.inner
|
||||
.nymnodes
|
||||
.set_location(cache_item.node_id(), Some(location))
|
||||
.await;
|
||||
|
||||
// one node has been located, so return out of the loop
|
||||
return;
|
||||
}
|
||||
None => {
|
||||
warn!("❌ Location for {bonded_host} not found.");
|
||||
self.state
|
||||
.inner
|
||||
.nymnodes
|
||||
.set_location(cache_item.node_id(), None)
|
||||
.await;
|
||||
}
|
||||
},
|
||||
Err(_e) => {
|
||||
// warn!(
|
||||
// "❌ Oh no! Location for {} failed. Error: {:#?}",
|
||||
// cache_item.mix_node().host,
|
||||
// e
|
||||
// );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
trace!("All nym-nodes nodes located");
|
||||
}
|
||||
|
||||
async fn locate_gateways(&mut self) {
|
||||
let gateways = self.state.inner.gateways.get_gateways().await;
|
||||
|
||||
let geo_ip = self.state.inner.geo_ip.0.clone();
|
||||
|
||||
for (i, cache_item) in gateways.iter().enumerate() {
|
||||
if self
|
||||
.state
|
||||
.inner
|
||||
.gateways
|
||||
.is_location_valid(cache_item.identity().to_owned())
|
||||
.await
|
||||
{
|
||||
// when the cached location is valid, don't locate and continue to next gateway
|
||||
continue;
|
||||
}
|
||||
|
||||
match geo_ip.query(&cache_item.gateway.host, Some(cache_item.gateway.mix_port)) {
|
||||
Ok(opt) => match opt {
|
||||
Some(location) => {
|
||||
let location: Location = location.into();
|
||||
|
||||
trace!(
|
||||
"{} gateways already located. Ip {} is located in {:#?}",
|
||||
i,
|
||||
cache_item.gateway.host,
|
||||
location.three_letter_iso_country_code,
|
||||
);
|
||||
|
||||
if i > 0 && (i % 100) == 0 {
|
||||
info!("Located {} gateways...", i + 1,);
|
||||
}
|
||||
|
||||
self.state
|
||||
.inner
|
||||
.gateways
|
||||
.set_location(cache_item.identity().to_owned(), Some(location))
|
||||
.await;
|
||||
|
||||
// one node has been located, so return out of the loop
|
||||
return;
|
||||
}
|
||||
None => {
|
||||
warn!("❌ Location for {} not found.", cache_item.gateway.host);
|
||||
self.state
|
||||
.inner
|
||||
.gateways
|
||||
.set_location(cache_item.identity().to_owned(), None)
|
||||
.await;
|
||||
}
|
||||
},
|
||||
Err(_e) => {
|
||||
// warn!(
|
||||
// "❌ Oh no! Location for {} failed. Error: {:#?}",
|
||||
// cache_item.gateway.host,
|
||||
// e
|
||||
// );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
trace!("All gateways located");
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
use crate::country_statistics::country_nodes_distribution::CountryNodesDistribution;
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::{Route, State};
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
use rocket_okapi::openapi_get_routes_spec;
|
||||
use rocket_okapi::settings::OpenApiSettings;
|
||||
|
||||
pub fn country_statistics_make_default_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![settings: index]
|
||||
}
|
||||
|
||||
// We could either separate stuff by structure (like this, http is separate), or we could just
|
||||
// stick the http route directly into each sub-application (e.g. put this file into the
|
||||
// "country_statistics" module directly
|
||||
#[openapi(tag = "country_statistics")]
|
||||
#[get("/")]
|
||||
pub(crate) async fn index(
|
||||
state: &State<ExplorerApiStateContext>,
|
||||
) -> Json<CountryNodesDistribution> {
|
||||
Json(state.inner.country_node_distribution.get_all().await)
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub mod country_nodes_distribution;
|
||||
pub mod distribution;
|
||||
pub mod geolocate;
|
||||
pub mod http;
|
||||
@@ -1,24 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_explorer_api_requests::PrettyDetailedGatewayBond;
|
||||
use rocket::response::status::NotFound;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::{Route, State};
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
use rocket_okapi::openapi_get_routes_spec;
|
||||
use rocket_okapi::settings::OpenApiSettings;
|
||||
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
|
||||
pub fn gateways_make_default_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![settings: list]
|
||||
}
|
||||
|
||||
#[openapi(tag = "gateways")]
|
||||
#[get("/")]
|
||||
pub(crate) async fn list(
|
||||
state: &State<ExplorerApiStateContext>,
|
||||
) -> Result<Json<Vec<PrettyDetailedGatewayBond>>, NotFound<String>> {
|
||||
Ok(Json(state.inner.gateways.get_detailed_gateways().await))
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_contracts_common::IdentityKey;
|
||||
|
||||
use crate::location::LocationCache;
|
||||
|
||||
pub(crate) type GatewayLocationCache = LocationCache<IdentityKey>;
|
||||
@@ -1,3 +0,0 @@
|
||||
pub(crate) mod http;
|
||||
pub(crate) mod location;
|
||||
pub(crate) mod models;
|
||||
@@ -1,160 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{cache::Cache, location::LocationCacheItem};
|
||||
use nym_contracts_common::IdentityKey;
|
||||
use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond};
|
||||
use nym_mixnet_contract_common::GatewayBond;
|
||||
use nym_validator_client::models::GatewayBondAnnotated;
|
||||
use serde::Serialize;
|
||||
use std::{sync::Arc, time::SystemTime};
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use super::location::GatewayLocationCache;
|
||||
|
||||
pub(crate) struct GatewayCache {
|
||||
pub(crate) gateways: Cache<IdentityKey, GatewayBondAnnotated>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, JsonSchema)]
|
||||
pub(crate) struct GatewaySummary {
|
||||
pub count: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ThreadsafeGatewayCache {
|
||||
gateways: Arc<RwLock<GatewayCache>>,
|
||||
legacy_gateway_bonds: Arc<RwLock<GatewayCache>>,
|
||||
locations: Arc<RwLock<GatewayLocationCache>>,
|
||||
}
|
||||
|
||||
impl ThreadsafeGatewayCache {
|
||||
pub(crate) fn new() -> Self {
|
||||
ThreadsafeGatewayCache {
|
||||
gateways: Arc::new(RwLock::new(GatewayCache {
|
||||
gateways: Cache::new(),
|
||||
})),
|
||||
legacy_gateway_bonds: Arc::new(RwLock::new(GatewayCache {
|
||||
gateways: Cache::new(),
|
||||
})),
|
||||
locations: Arc::new(RwLock::new(GatewayLocationCache::new())),
|
||||
}
|
||||
}
|
||||
|
||||
fn create_detailed_gateway(
|
||||
&self,
|
||||
bond: GatewayBond,
|
||||
location: Option<&LocationCacheItem>,
|
||||
) -> PrettyDetailedGatewayBond {
|
||||
PrettyDetailedGatewayBond {
|
||||
pledge_amount: bond.pledge_amount,
|
||||
owner: bond.owner,
|
||||
block_height: bond.block_height,
|
||||
gateway: bond.gateway,
|
||||
proxy: bond.proxy,
|
||||
location: location.and_then(|l| l.location.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_gateways(&self) -> Vec<GatewayBond> {
|
||||
self.gateways
|
||||
.read()
|
||||
.await
|
||||
.gateways
|
||||
.get_all()
|
||||
.iter()
|
||||
.map(|g| g.gateway_bond.bond.clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_detailed_gateways(&self) -> Vec<PrettyDetailedGatewayBond> {
|
||||
let gateways_guard = self.gateways.read().await;
|
||||
let location_guard = self.locations.read().await;
|
||||
|
||||
gateways_guard
|
||||
.gateways
|
||||
.get_all()
|
||||
.iter()
|
||||
.map(|bond| {
|
||||
let location = location_guard.get(bond.identity());
|
||||
self.create_detailed_gateway(bond.gateway_bond.bond.to_owned(), location)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_legacy_detailed_gateways(&self) -> Vec<PrettyDetailedGatewayBond> {
|
||||
let legacy_gateways = self.legacy_gateway_bonds.read().await;
|
||||
let location_guard = self.locations.read().await;
|
||||
|
||||
legacy_gateways
|
||||
.gateways
|
||||
.get_all()
|
||||
.iter()
|
||||
.map(|bond| {
|
||||
let location = location_guard.get(bond.identity());
|
||||
self.create_detailed_gateway(bond.gateway_bond.bond.to_owned(), location)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_gateway_summary(&self) -> GatewaySummary {
|
||||
GatewaySummary {
|
||||
count: self.gateways.read().await.gateways.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_with_location_cache(locations: GatewayLocationCache) -> Self {
|
||||
ThreadsafeGatewayCache {
|
||||
gateways: Arc::new(RwLock::new(GatewayCache {
|
||||
gateways: Cache::new(),
|
||||
})),
|
||||
legacy_gateway_bonds: Arc::new(RwLock::new(GatewayCache {
|
||||
gateways: Cache::new(),
|
||||
})),
|
||||
locations: Arc::new(RwLock::new(locations)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn is_location_valid(&self, identity_key: IdentityKey) -> bool {
|
||||
self.locations
|
||||
.read()
|
||||
.await
|
||||
.get(&identity_key)
|
||||
.is_some_and(|cache_item| cache_item.valid_until > SystemTime::now())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_locations(&self) -> GatewayLocationCache {
|
||||
self.locations.read().await.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn set_location(&self, identy_key: IdentityKey, location: Option<Location>) {
|
||||
// cache the location for this mix node so that it can be used when the mix node list is refreshed
|
||||
self.locations
|
||||
.write()
|
||||
.await
|
||||
.insert(identy_key, LocationCacheItem::new_from_location(location));
|
||||
}
|
||||
|
||||
pub(crate) async fn update_cache(
|
||||
&self,
|
||||
gateways: Vec<GatewayBondAnnotated>,
|
||||
legacy_gateway_bonds: Vec<GatewayBond>,
|
||||
) {
|
||||
let mut guard = self.gateways.write().await;
|
||||
let mut guard_legacy_gateways = self.legacy_gateway_bonds.write().await;
|
||||
|
||||
for gateway in gateways {
|
||||
guard
|
||||
.gateways
|
||||
.set(gateway.gateway_bond.gateway.identity_key.clone(), gateway)
|
||||
}
|
||||
|
||||
for legacy_gateway in legacy_gateway_bonds {
|
||||
if let Some(g) = guard.gateways.get(&legacy_gateway.gateway.identity_key) {
|
||||
guard_legacy_gateways
|
||||
.gateways
|
||||
.set(legacy_gateway.gateway.identity_key, g.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,204 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::helpers::{append_ip_to_file, failed_ips_filepath};
|
||||
use isocountry::CountryCode;
|
||||
use log::warn;
|
||||
use maxminddb::{geoip2::City, MaxMindDBError, Reader};
|
||||
use std::collections::HashSet;
|
||||
use std::fs::File;
|
||||
use std::io::{self, BufRead};
|
||||
use std::path::Path;
|
||||
use std::sync::Mutex;
|
||||
use std::{
|
||||
net::{IpAddr, ToSocketAddrs},
|
||||
str::FromStr,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
const DEFAULT_DATABASE_PATH: &str = "./geo_ip/GeoLite2-City.mmdb";
|
||||
const FAKE_PORT: u16 = 1234;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum GeoIpError {
|
||||
NoValidIP,
|
||||
InternalError,
|
||||
}
|
||||
|
||||
// The current State implementation does not allow to fail on state
|
||||
// creation, ie. returning Result<>. To avoid to use unwrap family,
|
||||
// as a workaround, wrap the state inside an Option<>
|
||||
// If Reader::open_readfile fails for some reason db will be set to None
|
||||
// and an error will be logged.
|
||||
pub(crate) struct GeoIp {
|
||||
pub(crate) db: Option<Reader<Vec<u8>>>,
|
||||
failed_addresses: FailedIpAddresses,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ThreadsafeGeoIp(pub Arc<GeoIp>);
|
||||
|
||||
pub(crate) struct Location {
|
||||
/// two-letter country code (ISO 3166-1 alpha-2)
|
||||
pub(crate) iso_alpha2: String,
|
||||
/// three-letter country code (ISO 3166-1 alpha-3)
|
||||
pub(crate) iso_alpha3: String,
|
||||
/// English country short name (ISO 3166-1)
|
||||
pub(crate) name: String,
|
||||
pub(crate) latitude: Option<f64>,
|
||||
pub(crate) longitude: Option<f64>,
|
||||
}
|
||||
|
||||
pub(crate) struct FailedIpAddresses {
|
||||
failed_ips: Mutex<HashSet<String>>,
|
||||
}
|
||||
|
||||
impl FailedIpAddresses {
|
||||
pub fn new() -> Self {
|
||||
let mut failed_ips = HashSet::new();
|
||||
let file_path = failed_ips_filepath();
|
||||
|
||||
if Path::new(&file_path).exists() {
|
||||
if let Ok(file) = File::open(&file_path) {
|
||||
let lines = io::BufReader::new(file).lines();
|
||||
for ip in lines.map_while(Result::ok) {
|
||||
failed_ips.insert(ip);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FailedIpAddresses {
|
||||
failed_ips: Mutex::new(failed_ips),
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<Location> for nym_explorer_api_requests::Location {
|
||||
fn from(location: Location) -> Self {
|
||||
nym_explorer_api_requests::Location {
|
||||
country_name: location.name,
|
||||
two_letter_iso_country_code: location.iso_alpha2,
|
||||
three_letter_iso_country_code: location.iso_alpha3,
|
||||
latitude: location.latitude,
|
||||
longitude: location.longitude,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GeoIp {
|
||||
pub fn new() -> Self {
|
||||
let db_path = std::env::var("GEOIP_DB_PATH").unwrap_or_else(|e| {
|
||||
warn!(
|
||||
"Env variable GEOIP_DB_PATH is not set: {} - Fallback to {}",
|
||||
e, DEFAULT_DATABASE_PATH
|
||||
);
|
||||
DEFAULT_DATABASE_PATH.to_string()
|
||||
});
|
||||
let reader = Reader::open_readfile(&db_path)
|
||||
.map_err(|e| {
|
||||
error!("Fail to open GeoLite2 database file {}: {}", db_path, e);
|
||||
})
|
||||
.ok();
|
||||
|
||||
let failed_addresses = FailedIpAddresses::new();
|
||||
|
||||
GeoIp {
|
||||
db: reader,
|
||||
failed_addresses,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_failed_ip(&self, address: &str) {
|
||||
if let Ok(mut failed_ips_guard) = self.failed_addresses.failed_ips.lock() {
|
||||
if failed_ips_guard.insert(address.to_string()) {
|
||||
append_ip_to_file(address);
|
||||
}
|
||||
} else {
|
||||
error!("Failed to acquire lock on failed_ips");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn query(&self, address: &str, port: Option<u16>) -> Result<Option<Location>, GeoIpError> {
|
||||
let p = port.unwrap_or(FAKE_PORT);
|
||||
let ip_result: Result<IpAddr, GeoIpError> = FromStr::from_str(address).or_else(|_| {
|
||||
debug!(
|
||||
"Fail to create IpAddr from {}. Trying using internal lookup...",
|
||||
&address
|
||||
);
|
||||
match (address, p).to_socket_addrs() {
|
||||
Ok(mut addrs) => {
|
||||
if let Some(socket_addr) = addrs.next() {
|
||||
let ip = socket_addr.ip();
|
||||
debug!("Internal lookup succeeded, resolved ip: {}", ip);
|
||||
Ok(ip)
|
||||
} else {
|
||||
debug!("Fail to resolve IP address from {}:{}", &address, p);
|
||||
self.handle_failed_ip(address);
|
||||
Err(GeoIpError::NoValidIP)
|
||||
}
|
||||
}
|
||||
Err(_) => {
|
||||
debug!("Fail to resolve IP address from {}:{}.", &address, p);
|
||||
self.handle_failed_ip(address);
|
||||
Err(GeoIpError::NoValidIP)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let ip = ip_result?;
|
||||
|
||||
let result = self
|
||||
.db
|
||||
.as_ref()
|
||||
.ok_or_else(|| {
|
||||
error!("No registered GeoIP database");
|
||||
GeoIpError::InternalError
|
||||
})?
|
||||
.lookup::<City>(ip);
|
||||
match &result {
|
||||
Ok(v) => Ok(Some(
|
||||
Location::try_from(v).map_err(|_| GeoIpError::InternalError)?,
|
||||
)),
|
||||
Err(e) => match e {
|
||||
MaxMindDBError::AddressNotFoundError(_) => Ok(None),
|
||||
_ => Err(GeoIpError::InternalError),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&City<'_>> for Location {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(city: &City) -> Result<Self, Self::Error> {
|
||||
let data = city.country.as_ref().ok_or_else(|| {
|
||||
warn!("No Country data found");
|
||||
"No Country data found"
|
||||
})?;
|
||||
let iso_alpha2 = String::from(data.iso_code.ok_or_else(|| {
|
||||
warn!("No iso alpha-2 code found in Country data {:#?}", data);
|
||||
"No iso alpha-2 code found in Country data"
|
||||
})?);
|
||||
let iso_codes = CountryCode::for_alpha2(&iso_alpha2).map_err(|e| {
|
||||
let message = format!(
|
||||
"Fail to get iso codes from iso alpha-2 country code {}: {}",
|
||||
&iso_alpha2, e
|
||||
);
|
||||
warn!("{}", &message);
|
||||
message
|
||||
})?;
|
||||
|
||||
Ok(Location {
|
||||
iso_alpha2,
|
||||
iso_alpha3: String::from(iso_codes.alpha3()),
|
||||
name: String::from(iso_codes.name()),
|
||||
latitude: city.location.as_ref().and_then(|l| l.latitude),
|
||||
longitude: city.location.as_ref().and_then(|l| l.longitude),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ThreadsafeGeoIp {
|
||||
pub fn new() -> Self {
|
||||
ThreadsafeGeoIp(Arc::new(GeoIp::new()))
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub(crate) mod location;
|
||||
@@ -1,76 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::geo_ip::location::{GeoIpError, Location};
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
use rocket::http::Status;
|
||||
use rocket::request::FromRequest;
|
||||
use rocket::request::Outcome;
|
||||
use rocket::Request;
|
||||
use rocket_okapi::gen::OpenApiGenerator;
|
||||
use rocket_okapi::request::{OpenApiFromRequest, RequestHeaderInput};
|
||||
|
||||
const IP_HEADER: &str = "X-Real-IP";
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum LocationError {
|
||||
NoIP,
|
||||
LocationNotFound,
|
||||
InternalError,
|
||||
}
|
||||
|
||||
fn find_location(request: &Request<'_>) -> Result<Location, (Status, LocationError)> {
|
||||
let ip = request
|
||||
.headers()
|
||||
.get_one(IP_HEADER)
|
||||
.map(|f| f.to_string())
|
||||
.ok_or_else(|| {
|
||||
error!("Header not found, {}", IP_HEADER);
|
||||
(Status::Forbidden, LocationError::NoIP)
|
||||
})?;
|
||||
|
||||
let geo_ip = &request
|
||||
.rocket()
|
||||
.state::<ExplorerApiStateContext>()
|
||||
.ok_or((Status::InternalServerError, LocationError::InternalError))? // should never fail
|
||||
.inner
|
||||
.geo_ip;
|
||||
|
||||
let location = geo_ip
|
||||
.0
|
||||
.clone()
|
||||
.query(&ip, None)
|
||||
.map_err(|e| match e {
|
||||
GeoIpError::NoValidIP => (Status::Forbidden, LocationError::NoIP),
|
||||
GeoIpError::InternalError => {
|
||||
(Status::InternalServerError, LocationError::InternalError)
|
||||
}
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
warn!("Fail to find a matching location for {}", ip);
|
||||
(Status::Forbidden, LocationError::LocationNotFound)
|
||||
})?;
|
||||
Ok(location)
|
||||
}
|
||||
|
||||
#[rocket::async_trait]
|
||||
impl<'r> FromRequest<'r> for Location {
|
||||
type Error = LocationError;
|
||||
|
||||
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
|
||||
match find_location(request) {
|
||||
Ok(loc) => Outcome::Success(loc),
|
||||
Err(e) => Outcome::Error(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenApiFromRequest<'_> for Location {
|
||||
fn from_request_input(
|
||||
_gen: &mut OpenApiGenerator,
|
||||
_name: String,
|
||||
_required: bool,
|
||||
) -> rocket_okapi::Result<RequestHeaderInput> {
|
||||
Ok(RequestHeaderInput::None)
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
pub(crate) mod location;
|
||||
@@ -1,38 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_mixnet_contract_common::{Decimal, Fraction};
|
||||
use std::env;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub(crate) fn best_effort_small_dec_to_f64(dec: Decimal) -> f64 {
|
||||
let num = dec.numerator().u128() as f64;
|
||||
let den = dec.denominator().u128() as f64;
|
||||
num / den
|
||||
}
|
||||
|
||||
pub fn failed_ips_filepath() -> String {
|
||||
let home_dir = env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
let mut path = PathBuf::from(home_dir);
|
||||
path.push("failed_ips.txt");
|
||||
path.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
pub fn append_ip_to_file(address: &str) {
|
||||
match OpenOptions::new()
|
||||
.append(true)
|
||||
.create(true)
|
||||
.open(failed_ips_filepath())
|
||||
{
|
||||
Ok(mut file) => {
|
||||
if let Err(e) = writeln!(file, "{}", address) {
|
||||
error!("Failed to write to file: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to open or create the file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
use log::info;
|
||||
use okapi::openapi3::OpenApi;
|
||||
use rocket::http::Method;
|
||||
use rocket::{Build, Request, Rocket};
|
||||
use rocket_cors::{AllowedHeaders, AllowedOrigins};
|
||||
use rocket_okapi::swagger_ui::make_swagger_ui;
|
||||
|
||||
use crate::country_statistics::http::country_statistics_make_default_routes;
|
||||
use crate::gateways::http::gateways_make_default_routes;
|
||||
use crate::http::swagger::get_docs;
|
||||
use crate::mix_node::http::mix_node_make_default_routes;
|
||||
use crate::mix_nodes::http::mix_nodes_make_default_routes;
|
||||
use crate::overview::http::overview_make_default_routes;
|
||||
use crate::ping::http::ping_make_default_routes;
|
||||
use crate::service_providers::http::service_providers_make_default_routes;
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
use crate::unstable::http::unstable_temp_make_default_routes;
|
||||
use crate::validators::http::validators_make_default_routes;
|
||||
|
||||
mod swagger;
|
||||
|
||||
pub(crate) fn start(state: ExplorerApiStateContext) {
|
||||
tokio::spawn(async move {
|
||||
info!("Starting up...");
|
||||
configure_rocket(state).launch().await
|
||||
});
|
||||
}
|
||||
|
||||
fn configure_rocket(state: ExplorerApiStateContext) -> Rocket<Build> {
|
||||
let allowed_origins = AllowedOrigins::all();
|
||||
|
||||
// You can also deserialize this
|
||||
let cors = rocket_cors::CorsOptions {
|
||||
allowed_origins,
|
||||
allowed_methods: vec![Method::Get].into_iter().map(From::from).collect(),
|
||||
allowed_headers: AllowedHeaders::some(&["*"]),
|
||||
allow_credentials: true,
|
||||
..Default::default()
|
||||
}
|
||||
.to_cors()
|
||||
.unwrap();
|
||||
|
||||
let openapi_settings = rocket_okapi::settings::OpenApiSettings::default();
|
||||
let config = rocket::config::Config::release_default();
|
||||
let mut building_rocket = rocket::build().configure(config);
|
||||
|
||||
let custom_route_spec = (vec![], custom_openapi_spec());
|
||||
|
||||
mount_endpoints_and_merged_docs! {
|
||||
building_rocket,
|
||||
"/v1".to_owned(),
|
||||
openapi_settings,
|
||||
"/" => custom_route_spec,
|
||||
"/countries" => country_statistics_make_default_routes(&openapi_settings),
|
||||
"/gateways" => gateways_make_default_routes(&openapi_settings),
|
||||
"/mix-node" => mix_node_make_default_routes(&openapi_settings),
|
||||
"/mix-nodes" => mix_nodes_make_default_routes(&openapi_settings),
|
||||
"/overview" => overview_make_default_routes(&openapi_settings),
|
||||
"/ping" => ping_make_default_routes(&openapi_settings),
|
||||
"/validators" => validators_make_default_routes(&openapi_settings),
|
||||
"/service-providers" => service_providers_make_default_routes(&openapi_settings),
|
||||
"/tmp/unstable" => unstable_temp_make_default_routes(&openapi_settings),
|
||||
};
|
||||
|
||||
building_rocket
|
||||
.mount("/swagger", make_swagger_ui(&get_docs()))
|
||||
.register("/", catchers![not_found])
|
||||
.manage(state)
|
||||
.attach(cors)
|
||||
}
|
||||
|
||||
#[catch(404)]
|
||||
pub(crate) fn not_found(req: &Request<'_>) -> String {
|
||||
format!("I couldn't find '{}'. Try something else?", req.uri())
|
||||
}
|
||||
|
||||
fn custom_openapi_spec() -> OpenApi {
|
||||
use rocket_okapi::okapi::openapi3::*;
|
||||
OpenApi {
|
||||
openapi: OpenApi::default_version(),
|
||||
info: Info {
|
||||
title: "Network Explorer API".to_owned(),
|
||||
description: None,
|
||||
terms_of_service: None,
|
||||
contact: None,
|
||||
license: None,
|
||||
version: env!("CARGO_PKG_VERSION").to_owned(),
|
||||
..Default::default()
|
||||
},
|
||||
servers: get_servers(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_servers() -> Vec<rocket_okapi::okapi::openapi3::Server> {
|
||||
if std::env::var_os("CARGO").is_some() {
|
||||
return vec![];
|
||||
}
|
||||
vec![rocket_okapi::okapi::openapi3::Server {
|
||||
url: std::env::var("OPEN_API_BASE").unwrap_or_else(|_| "/api/v1/".to_owned()),
|
||||
description: Some("API".to_owned()),
|
||||
..Default::default()
|
||||
}]
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
use rocket_okapi::swagger_ui::SwaggerUIConfig;
|
||||
|
||||
pub(crate) fn get_docs() -> SwaggerUIConfig {
|
||||
SwaggerUIConfig {
|
||||
url: "../v1/openapi.json".to_owned(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_explorer_api_requests::Location;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
pub(crate) type LocationCache<T> = HashMap<T, LocationCacheItem>;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct GeoLocation {
|
||||
pub(crate) ip: String,
|
||||
pub(crate) country_code: String,
|
||||
pub(crate) country_name: String,
|
||||
pub(crate) region_code: String,
|
||||
pub(crate) region_name: String,
|
||||
pub(crate) city: String,
|
||||
pub(crate) zip_code: String,
|
||||
pub(crate) time_zone: String,
|
||||
pub(crate) latitude: f32,
|
||||
pub(crate) longitude: f32,
|
||||
pub(crate) metro_code: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub(crate) struct LocationCacheItem {
|
||||
pub(crate) location: Option<Location>,
|
||||
pub(crate) valid_until: SystemTime,
|
||||
}
|
||||
|
||||
impl LocationCacheItem {
|
||||
pub(crate) fn new_from_location(location: Option<Location>) -> Self {
|
||||
LocationCacheItem {
|
||||
location,
|
||||
valid_until: SystemTime::now() + Duration::from_secs(60 * 60 * 24), // valid for 1 day
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
#[macro_use]
|
||||
extern crate rocket;
|
||||
#[macro_use]
|
||||
extern crate rocket_okapi;
|
||||
|
||||
use clap::Parser;
|
||||
use dotenvy::dotenv;
|
||||
use log::info;
|
||||
use nym_bin_common::logging::setup_logging;
|
||||
use nym_network_defaults::setup_env;
|
||||
use nym_task::TaskManager;
|
||||
|
||||
pub(crate) mod cache;
|
||||
mod client;
|
||||
pub(crate) mod commands;
|
||||
mod country_statistics;
|
||||
mod gateways;
|
||||
mod geo_ip;
|
||||
mod guards;
|
||||
mod helpers;
|
||||
mod http;
|
||||
mod location;
|
||||
mod mix_node;
|
||||
pub(crate) mod mix_nodes;
|
||||
mod overview;
|
||||
mod ping;
|
||||
pub(crate) mod service_providers;
|
||||
mod state;
|
||||
mod tasks;
|
||||
mod unstable;
|
||||
mod validators;
|
||||
|
||||
const COUNTRY_DATA_REFRESH_INTERVAL: u64 = 60 * 15; // every 15 minutes
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
dotenv().ok();
|
||||
setup_logging();
|
||||
let args = commands::Cli::parse();
|
||||
setup_env(args.config_env_file);
|
||||
let mut explorer_api = ExplorerApi::new();
|
||||
explorer_api.run().await;
|
||||
}
|
||||
|
||||
pub struct ExplorerApi {
|
||||
state: state::ExplorerApiStateContext,
|
||||
}
|
||||
|
||||
impl ExplorerApi {
|
||||
fn new() -> ExplorerApi {
|
||||
ExplorerApi {
|
||||
state: state::ExplorerApiStateContext::new(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(&mut self) {
|
||||
info!("Explorer API starting up...");
|
||||
|
||||
let nym_api_url = self.state.inner.validator_client.api_endpoint();
|
||||
info!("Using validator API - {}", nym_api_url);
|
||||
|
||||
let shutdown = TaskManager::default();
|
||||
|
||||
// spawn concurrent tasks
|
||||
crate::tasks::ExplorerApiTasks::new(self.state.clone(), shutdown.subscribe()).start();
|
||||
country_statistics::distribution::CountryStatisticsDistributionTask::new(
|
||||
self.state.clone(),
|
||||
shutdown.subscribe(),
|
||||
)
|
||||
.start();
|
||||
country_statistics::geolocate::GeoLocateTask::new(self.state.clone(), shutdown.subscribe())
|
||||
.start();
|
||||
|
||||
// Rocket handles shutdown on it's own, but its shutdown handling should be incorporated
|
||||
// with that of the rest of the tasks.
|
||||
// Currently it's runtime is forcefully terminated once the explorer-api exits.
|
||||
http::start(self.state.clone());
|
||||
|
||||
// wait for user to press ctrl+C
|
||||
self.wait_for_interrupt(shutdown).await
|
||||
}
|
||||
|
||||
async fn wait_for_interrupt(&self, mut shutdown: TaskManager) {
|
||||
let _res = shutdown.catch_interrupt().await;
|
||||
log::info!("Stopping explorer API");
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use super::models::SummedDelegations;
|
||||
use crate::client::ThreadsafeValidatorClient;
|
||||
use itertools::Itertools;
|
||||
use nym_mixnet_contract_common::{Delegation, NodeId};
|
||||
use nym_validator_client::nyxd::contract_traits::PagedMixnetQueryClient;
|
||||
|
||||
pub(crate) async fn get_single_mixnode_delegations(
|
||||
client: &ThreadsafeValidatorClient,
|
||||
mix_id: NodeId,
|
||||
) -> Vec<Delegation> {
|
||||
match client
|
||||
.0
|
||||
.nyxd
|
||||
.get_all_single_mixnode_delegations(mix_id)
|
||||
.await
|
||||
{
|
||||
Ok(result) => result,
|
||||
Err(e) => {
|
||||
error!("Could not get delegations for mix node {}: {:?}", mix_id, e);
|
||||
vec![]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_single_mixnode_delegations_summed(
|
||||
client: &ThreadsafeValidatorClient,
|
||||
mix_id: NodeId,
|
||||
) -> Vec<SummedDelegations> {
|
||||
let delegations_by_owner = get_single_mixnode_delegations(client, mix_id)
|
||||
.await
|
||||
.into_iter()
|
||||
.into_group_map_by(|delegation| delegation.owner.clone());
|
||||
|
||||
delegations_by_owner
|
||||
.iter()
|
||||
.filter_map(|(_, delegations)| SummedDelegations::from(delegations))
|
||||
.collect()
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::client::ThreadsafeValidatorClient;
|
||||
use crate::helpers::best_effort_small_dec_to_f64;
|
||||
use crate::mix_node::models::EconomicDynamicsStats;
|
||||
use nym_contracts_common::truncate_decimal;
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
use nym_validator_client::client::NymApiClientExt;
|
||||
|
||||
// use deprecated method as hopefully this whole API will be sunset soon-enough...
|
||||
// and we're only getting info for legacy node so the relevant data should still exist
|
||||
#[allow(deprecated)]
|
||||
pub(crate) async fn retrieve_mixnode_econ_stats(
|
||||
client: &ThreadsafeValidatorClient,
|
||||
mix_id: NodeId,
|
||||
) -> Option<EconomicDynamicsStats> {
|
||||
let stake_saturation = client
|
||||
.0
|
||||
.nym_api
|
||||
.get_mixnode_stake_saturation(mix_id)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
let inclusion_probability = client
|
||||
.0
|
||||
.nym_api
|
||||
.get_mixnode_inclusion_probability(mix_id)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
let reward_estimation = client
|
||||
.0
|
||||
.nym_api
|
||||
.get_mixnode_reward_estimation(mix_id)
|
||||
.await
|
||||
.ok()?;
|
||||
|
||||
let uptime_response = client.0.nym_api.get_mixnode_avg_uptime(mix_id).await.ok()?;
|
||||
|
||||
Some(EconomicDynamicsStats {
|
||||
stake_saturation: best_effort_small_dec_to_f64(stake_saturation.saturation) as f32,
|
||||
uncapped_saturation: best_effort_small_dec_to_f64(stake_saturation.uncapped_saturation)
|
||||
as f32,
|
||||
active_set_inclusion_probability: inclusion_probability.in_active,
|
||||
reserve_set_inclusion_probability: inclusion_probability.in_reserve,
|
||||
// drop precision for compatibility sake
|
||||
estimated_total_node_reward: truncate_decimal(
|
||||
reward_estimation.estimation.total_node_reward,
|
||||
)
|
||||
.u128() as u64,
|
||||
estimated_operator_reward: truncate_decimal(reward_estimation.estimation.operator).u128()
|
||||
as u64,
|
||||
estimated_delegators_reward: truncate_decimal(reward_estimation.estimation.delegates).u128()
|
||||
as u64,
|
||||
current_interval_uptime: uptime_response.avg_uptime,
|
||||
})
|
||||
}
|
||||
@@ -1,197 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::mix_node::delegations::{
|
||||
get_single_mixnode_delegations, get_single_mixnode_delegations_summed,
|
||||
};
|
||||
use crate::mix_node::econ_stats::retrieve_mixnode_econ_stats;
|
||||
use crate::mix_node::models::{
|
||||
EconomicDynamicsStats, NodeDescription, NodeStats, SummedDelegations,
|
||||
};
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
use nym_explorer_api_requests::PrettyDetailedMixNodeBond;
|
||||
use nym_mixnet_contract_common::{Delegation, NodeId};
|
||||
use reqwest::Error as ReqwestError;
|
||||
use rocket::response::status::NotFound;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::{Route, State};
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
use rocket_okapi::openapi_get_routes_spec;
|
||||
use rocket_okapi::settings::OpenApiSettings;
|
||||
|
||||
pub fn mix_node_make_default_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![
|
||||
settings: get_delegations,
|
||||
get_delegations_summed,
|
||||
get_by_id,
|
||||
get_description,
|
||||
get_stats,
|
||||
get_economic_dynamics_stats,
|
||||
]
|
||||
}
|
||||
|
||||
async fn get_mix_node_description(host: &str, port: u16) -> Result<NodeDescription, ReqwestError> {
|
||||
reqwest::get(format!("http://{host}:{port}/description"))
|
||||
.await?
|
||||
.json::<NodeDescription>()
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_mix_node_stats(host: &str, port: u16) -> Result<NodeStats, ReqwestError> {
|
||||
reqwest::get(format!("http://{host}:{port}/stats"))
|
||||
.await?
|
||||
.json::<NodeStats>()
|
||||
.await
|
||||
}
|
||||
|
||||
#[openapi(tag = "mix_node")]
|
||||
#[get("/<mix_id>")]
|
||||
pub(crate) async fn get_by_id(
|
||||
mix_id: NodeId,
|
||||
state: &State<ExplorerApiStateContext>,
|
||||
) -> Result<Json<PrettyDetailedMixNodeBond>, NotFound<String>> {
|
||||
match state.inner.mixnodes.get_detailed_mixnode(mix_id).await {
|
||||
Some(mixnode) => Ok(Json(mixnode)),
|
||||
None => Err(NotFound("Mixnode not found".to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
#[openapi(tag = "mix_node")]
|
||||
#[get("/<mix_id>/delegations")]
|
||||
pub(crate) async fn get_delegations(
|
||||
mix_id: NodeId,
|
||||
state: &State<ExplorerApiStateContext>,
|
||||
) -> Json<Vec<Delegation>> {
|
||||
Json(get_single_mixnode_delegations(&state.inner.validator_client, mix_id).await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "mix_node")]
|
||||
#[get("/<mix_id>/delegations/summed")]
|
||||
pub(crate) async fn get_delegations_summed(
|
||||
mix_id: NodeId,
|
||||
state: &State<ExplorerApiStateContext>,
|
||||
) -> Json<Vec<SummedDelegations>> {
|
||||
Json(get_single_mixnode_delegations_summed(&state.inner.validator_client, mix_id).await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "mix_node")]
|
||||
#[get("/<mix_id>/description")]
|
||||
pub(crate) async fn get_description(
|
||||
mix_id: NodeId,
|
||||
state: &State<ExplorerApiStateContext>,
|
||||
) -> Option<Json<NodeDescription>> {
|
||||
match state.inner.mixnode.clone().get_description(mix_id).await {
|
||||
Some(cache_value) => {
|
||||
trace!("Returning cached value for {}", mix_id);
|
||||
Some(Json(cache_value))
|
||||
}
|
||||
None => {
|
||||
trace!("No valid cache value for {}", mix_id);
|
||||
match state.inner.get_mix_node(mix_id).await {
|
||||
Some(bond) => {
|
||||
match get_mix_node_description(
|
||||
&bond.mix_node().host,
|
||||
bond.mix_node().http_api_port,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
// cache the response and return as the HTTP response
|
||||
state
|
||||
.inner
|
||||
.mixnode
|
||||
.set_description(mix_id, response.clone())
|
||||
.await;
|
||||
Some(Json(response))
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Unable to get description for {} on {}:{} -> {}",
|
||||
mix_id,
|
||||
bond.mix_node().host,
|
||||
bond.mix_node().http_api_port,
|
||||
e
|
||||
);
|
||||
Option::None
|
||||
}
|
||||
}
|
||||
}
|
||||
None => Option::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[openapi(tag = "mix_node")]
|
||||
#[get("/<mix_id>/stats")]
|
||||
pub(crate) async fn get_stats(
|
||||
mix_id: NodeId,
|
||||
state: &State<ExplorerApiStateContext>,
|
||||
) -> Option<Json<NodeStats>> {
|
||||
match state.inner.mixnode.get_node_stats(mix_id).await {
|
||||
Some(cache_value) => {
|
||||
trace!("Returning cached value for {}", mix_id);
|
||||
Some(Json(cache_value))
|
||||
}
|
||||
None => {
|
||||
trace!("No valid cache value for {}", mix_id);
|
||||
match state.inner.get_mix_node(mix_id).await {
|
||||
Some(bond) => {
|
||||
match get_mix_node_stats(&bond.mix_node().host, bond.mix_node().http_api_port)
|
||||
.await
|
||||
{
|
||||
Ok(response) => {
|
||||
// cache the response and return as the HTTP response
|
||||
state
|
||||
.inner
|
||||
.mixnode
|
||||
.set_node_stats(mix_id, response.clone())
|
||||
.await;
|
||||
Some(Json(response))
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Unable to get description for {} on {}:{} -> {}",
|
||||
mix_id,
|
||||
bond.mix_node().host,
|
||||
bond.mix_node().http_api_port,
|
||||
e
|
||||
);
|
||||
Option::None
|
||||
}
|
||||
}
|
||||
}
|
||||
None => Option::None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[openapi(tag = "mix_node")]
|
||||
#[get("/<mix_id>/economic-dynamics-stats")]
|
||||
pub(crate) async fn get_economic_dynamics_stats(
|
||||
mix_id: NodeId,
|
||||
state: &State<ExplorerApiStateContext>,
|
||||
) -> Option<Json<EconomicDynamicsStats>> {
|
||||
match state.inner.mixnode.get_econ_stats(mix_id).await {
|
||||
Some(cache_value) => {
|
||||
trace!("Returning cached value for {}", mix_id);
|
||||
Some(Json(cache_value))
|
||||
}
|
||||
None => {
|
||||
trace!("No valid cache value for {}", mix_id);
|
||||
|
||||
// get fresh value from the validator API
|
||||
let econ_stats =
|
||||
retrieve_mixnode_econ_stats(&state.inner.validator_client, mix_id).await?;
|
||||
|
||||
// update cache
|
||||
state
|
||||
.inner
|
||||
.mixnode
|
||||
.set_econ_stats(mix_id, econ_stats.clone())
|
||||
.await;
|
||||
Some(Json(econ_stats))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
pub(crate) mod delegations;
|
||||
pub(crate) mod econ_stats;
|
||||
pub(crate) mod http;
|
||||
pub mod models;
|
||||
@@ -1,174 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#![allow(deprecated)]
|
||||
|
||||
use crate::cache::Cache;
|
||||
use nym_mixnet_contract_common::Delegation;
|
||||
use nym_mixnet_contract_common::{Addr, Coin, NodeId};
|
||||
use nym_validator_client::models::SelectionChance;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
|
||||
pub struct SummedDelegations {
|
||||
pub owner: Addr,
|
||||
pub mix_id: NodeId,
|
||||
pub amount: Coin,
|
||||
}
|
||||
|
||||
impl SummedDelegations {
|
||||
pub fn from(delegations: &[Delegation]) -> Option<Self> {
|
||||
let owner = get_common_owner(delegations)?;
|
||||
let mix_id = get_common_mix_id(delegations)?;
|
||||
let denom = get_common_denom(delegations)?;
|
||||
|
||||
let sum = delegations
|
||||
.iter()
|
||||
.map(|delegation| delegation.amount.amount)
|
||||
.sum();
|
||||
|
||||
let amount = Coin { denom, amount: sum };
|
||||
|
||||
Some(SummedDelegations {
|
||||
owner,
|
||||
mix_id,
|
||||
amount,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct MixNodeCache {
|
||||
pub(crate) descriptions: Cache<NodeId, NodeDescription>,
|
||||
pub(crate) node_stats: Cache<NodeId, NodeStats>,
|
||||
pub(crate) econ_stats: Cache<NodeId, EconomicDynamicsStats>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ThreadsafeMixNodeCache {
|
||||
inner: Arc<RwLock<MixNodeCache>>,
|
||||
}
|
||||
|
||||
impl ThreadsafeMixNodeCache {
|
||||
pub(crate) fn new() -> Self {
|
||||
ThreadsafeMixNodeCache {
|
||||
inner: Arc::new(RwLock::new(MixNodeCache {
|
||||
descriptions: Cache::new(),
|
||||
node_stats: Cache::new(),
|
||||
econ_stats: Cache::new(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_description(&self, mix_id: NodeId) -> Option<NodeDescription> {
|
||||
self.inner.read().await.descriptions.get(&mix_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_node_stats(&self, mix_id: NodeId) -> Option<NodeStats> {
|
||||
self.inner.read().await.node_stats.get(&mix_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_econ_stats(&self, mix_id: NodeId) -> Option<EconomicDynamicsStats> {
|
||||
self.inner.read().await.econ_stats.get(&mix_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn set_description(&self, mix_id: NodeId, description: NodeDescription) {
|
||||
self.inner
|
||||
.write()
|
||||
.await
|
||||
.descriptions
|
||||
.set(mix_id, description);
|
||||
}
|
||||
|
||||
pub(crate) async fn set_node_stats(&self, mix_id: NodeId, node_stats: NodeStats) {
|
||||
self.inner.write().await.node_stats.set(mix_id, node_stats);
|
||||
}
|
||||
|
||||
pub(crate) async fn set_econ_stats(&self, mix_id: NodeId, econ_stats: EconomicDynamicsStats) {
|
||||
self.inner.write().await.econ_stats.set(mix_id, econ_stats);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
|
||||
pub(crate) struct NodeDescription {
|
||||
pub(crate) name: String,
|
||||
pub(crate) description: String,
|
||||
pub(crate) link: String,
|
||||
pub(crate) location: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, Deserialize, JsonSchema)]
|
||||
pub(crate) struct NodeStats {
|
||||
#[serde(
|
||||
serialize_with = "humantime_serde::serialize",
|
||||
deserialize_with = "humantime_serde::deserialize"
|
||||
)]
|
||||
update_time: SystemTime,
|
||||
|
||||
#[serde(
|
||||
serialize_with = "humantime_serde::serialize",
|
||||
deserialize_with = "humantime_serde::deserialize"
|
||||
)]
|
||||
previous_update_time: SystemTime,
|
||||
|
||||
packets_received_since_startup: u64,
|
||||
packets_sent_since_startup: u64,
|
||||
packets_explicitly_dropped_since_startup: u64,
|
||||
packets_received_since_last_update: u64,
|
||||
packets_sent_since_last_update: u64,
|
||||
packets_explicitly_dropped_since_last_update: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub(crate) struct EconomicDynamicsStats {
|
||||
pub(crate) stake_saturation: f32,
|
||||
pub(crate) uncapped_saturation: f32,
|
||||
|
||||
pub(crate) active_set_inclusion_probability: SelectionChance,
|
||||
pub(crate) reserve_set_inclusion_probability: SelectionChance,
|
||||
|
||||
pub(crate) estimated_total_node_reward: u64,
|
||||
pub(crate) estimated_operator_reward: u64,
|
||||
pub(crate) estimated_delegators_reward: u64,
|
||||
|
||||
pub(crate) current_interval_uptime: u8,
|
||||
}
|
||||
|
||||
fn get_common_owner(delegations: &[Delegation]) -> Option<Addr> {
|
||||
let owner = delegations.iter().next()?.owner.clone();
|
||||
if delegations
|
||||
.iter()
|
||||
.any(|delegation| delegation.owner != owner)
|
||||
{
|
||||
log::warn!("Unexpected different owners when summing delegations");
|
||||
return None;
|
||||
}
|
||||
Some(owner)
|
||||
}
|
||||
|
||||
fn get_common_mix_id(delegations: &[Delegation]) -> Option<NodeId> {
|
||||
let mix_id = delegations.iter().next()?.node_id;
|
||||
if delegations
|
||||
.iter()
|
||||
.any(|delegation| delegation.node_id != mix_id)
|
||||
{
|
||||
log::warn!("Unexpected different node identities when summing delegations");
|
||||
return None;
|
||||
}
|
||||
Some(mix_id)
|
||||
}
|
||||
|
||||
fn get_common_denom(delegations: &[Delegation]) -> Option<String> {
|
||||
let denom = delegations.iter().next()?.amount.denom.clone();
|
||||
if delegations
|
||||
.iter()
|
||||
.any(|delegation| delegation.amount.denom != denom)
|
||||
{
|
||||
log::warn!("Unexpected different coin denom when summing delegations");
|
||||
return None;
|
||||
}
|
||||
Some(denom)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
use crate::mix_nodes::models::{MixNodeActiveSetSummary, MixNodeSummary};
|
||||
use crate::state::ExplorerApiStateContext;
|
||||
use nym_explorer_api_requests::{MixnodeStatus, PrettyDetailedMixNodeBond};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::{Route, State};
|
||||
use rocket_okapi::okapi::openapi3::OpenApi;
|
||||
use rocket_okapi::openapi_get_routes_spec;
|
||||
use rocket_okapi::settings::OpenApiSettings;
|
||||
|
||||
pub fn mix_nodes_make_default_routes(settings: &OpenApiSettings) -> (Vec<Route>, OpenApi) {
|
||||
openapi_get_routes_spec![
|
||||
settings: list,
|
||||
list_active_set,
|
||||
list_inactive_set,
|
||||
list_standby_set,
|
||||
summary
|
||||
]
|
||||
}
|
||||
|
||||
#[openapi(tag = "mix_nodes")]
|
||||
#[get("/")]
|
||||
pub(crate) async fn list(
|
||||
state: &State<ExplorerApiStateContext>,
|
||||
) -> Json<Vec<PrettyDetailedMixNodeBond>> {
|
||||
Json(state.inner.mixnodes.get_detailed_mixnodes().await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "mix_nodes")]
|
||||
#[get("/active-set/active")]
|
||||
pub(crate) async fn list_active_set(
|
||||
state: &State<ExplorerApiStateContext>,
|
||||
) -> Json<Vec<PrettyDetailedMixNodeBond>> {
|
||||
Json(get_mixnodes_by_status(
|
||||
state.inner.mixnodes.get_detailed_mixnodes().await,
|
||||
&MixnodeStatus::Active,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "mix_nodes")]
|
||||
#[get("/active-set/inactive")]
|
||||
pub(crate) async fn list_inactive_set(
|
||||
state: &State<ExplorerApiStateContext>,
|
||||
) -> Json<Vec<PrettyDetailedMixNodeBond>> {
|
||||
Json(get_mixnodes_by_status(
|
||||
state.inner.mixnodes.get_detailed_mixnodes().await,
|
||||
&MixnodeStatus::Inactive,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "mix_nodes")]
|
||||
#[get("/active-set/standby")]
|
||||
pub(crate) async fn list_standby_set(
|
||||
state: &State<ExplorerApiStateContext>,
|
||||
) -> Json<Vec<PrettyDetailedMixNodeBond>> {
|
||||
Json(get_mixnodes_by_status(
|
||||
state.inner.mixnodes.get_detailed_mixnodes().await,
|
||||
&MixnodeStatus::Standby,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "mix_nodes")]
|
||||
#[get("/summary")]
|
||||
pub(crate) async fn summary(state: &State<ExplorerApiStateContext>) -> Json<MixNodeSummary> {
|
||||
Json(get_mixnode_summary(state).await)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_mixnode_summary(state: &State<ExplorerApiStateContext>) -> MixNodeSummary {
|
||||
let mixnodes = state.inner.mixnodes.get_detailed_mixnodes().await;
|
||||
let active = get_mixnodes_by_status(mixnodes.clone(), &MixnodeStatus::Active).len();
|
||||
let standby = get_mixnodes_by_status(mixnodes.clone(), &MixnodeStatus::Standby).len();
|
||||
let inactive = get_mixnodes_by_status(mixnodes.clone(), &MixnodeStatus::Inactive).len();
|
||||
MixNodeSummary {
|
||||
count: mixnodes.len(),
|
||||
activeset: MixNodeActiveSetSummary {
|
||||
active,
|
||||
standby,
|
||||
inactive,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn get_mixnodes_by_status(
|
||||
all_mixnodes: Vec<PrettyDetailedMixNodeBond>,
|
||||
status: &MixnodeStatus,
|
||||
) -> Vec<PrettyDetailedMixNodeBond> {
|
||||
all_mixnodes
|
||||
.into_iter()
|
||||
.filter(|mixnode| &mixnode.status == status)
|
||||
.collect()
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_mixnet_contract_common::NodeId;
|
||||
|
||||
use crate::location::LocationCache;
|
||||
|
||||
pub(crate) type MixnodeLocationCache = LocationCache<NodeId>;
|
||||
@@ -1,12 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) mod http;
|
||||
pub(crate) mod location;
|
||||
pub(crate) mod models;
|
||||
pub(crate) mod utils;
|
||||
|
||||
pub(crate) const CACHE_REFRESH_RATE: Duration = Duration::from_secs(30);
|
||||
pub(crate) const CACHE_ENTRY_TTL: Duration = Duration::from_secs(60);
|
||||
@@ -1,235 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use super::location::MixnodeLocationCache;
|
||||
use crate::helpers::best_effort_small_dec_to_f64;
|
||||
use crate::location::LocationCacheItem;
|
||||
use crate::mix_nodes::CACHE_ENTRY_TTL;
|
||||
use nym_explorer_api_requests::{Location, MixnodeStatus, PrettyDetailedMixNodeBond};
|
||||
use nym_mixnet_contract_common::rewarding::helpers::truncate_reward;
|
||||
use nym_mixnet_contract_common::{MixNodeBond, NodeId};
|
||||
use nym_validator_client::models::MixNodeBondAnnotated;
|
||||
use serde::Serialize;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::sync::{RwLock, RwLockReadGuard};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, JsonSchema)]
|
||||
pub(crate) struct MixNodeActiveSetSummary {
|
||||
pub active: usize,
|
||||
pub standby: usize,
|
||||
pub inactive: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, JsonSchema)]
|
||||
pub(crate) struct MixNodeSummary {
|
||||
pub count: usize,
|
||||
pub activeset: MixNodeActiveSetSummary,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct MixNodesResult {
|
||||
pub(crate) valid_until: SystemTime,
|
||||
pub(crate) all_mixnodes: HashMap<NodeId, MixNodeBondAnnotated>,
|
||||
active_mixnodes: HashSet<NodeId>,
|
||||
rewarded_mixnodes: HashSet<NodeId>,
|
||||
}
|
||||
|
||||
impl MixNodesResult {
|
||||
fn new() -> Self {
|
||||
MixNodesResult {
|
||||
valid_until: SystemTime::now() - Duration::from_secs(60), // in the past
|
||||
all_mixnodes: HashMap::new(),
|
||||
active_mixnodes: HashSet::new(),
|
||||
rewarded_mixnodes: HashSet::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_node_status(&self, mix_id: NodeId) -> MixnodeStatus {
|
||||
if self.active_mixnodes.contains(&mix_id) {
|
||||
MixnodeStatus::Active
|
||||
} else if self.rewarded_mixnodes.contains(&mix_id) {
|
||||
MixnodeStatus::Standby
|
||||
} else {
|
||||
MixnodeStatus::Inactive
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid(&self) -> bool {
|
||||
self.valid_until >= SystemTime::now()
|
||||
}
|
||||
|
||||
fn get_mixnode(&self, mix_id: NodeId) -> Option<MixNodeBondAnnotated> {
|
||||
if self.is_valid() {
|
||||
self.all_mixnodes.get(&mix_id).cloned()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn get_mixnodes(&self) -> Option<HashMap<NodeId, MixNodeBondAnnotated>> {
|
||||
if self.is_valid() {
|
||||
Some(self.all_mixnodes.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ThreadsafeMixNodesCache {
|
||||
mixnodes: Arc<RwLock<MixNodesResult>>,
|
||||
legacy_mixnode_bonds: Arc<RwLock<MixNodesResult>>,
|
||||
locations: Arc<RwLock<MixnodeLocationCache>>,
|
||||
}
|
||||
|
||||
impl ThreadsafeMixNodesCache {
|
||||
pub(crate) fn new() -> Self {
|
||||
ThreadsafeMixNodesCache {
|
||||
mixnodes: Arc::new(RwLock::new(MixNodesResult::new())),
|
||||
legacy_mixnode_bonds: Arc::new(RwLock::new(MixNodesResult::new())),
|
||||
locations: Arc::new(RwLock::new(MixnodeLocationCache::new())),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_with_location_cache(locations: MixnodeLocationCache) -> Self {
|
||||
ThreadsafeMixNodesCache {
|
||||
mixnodes: Arc::new(RwLock::new(MixNodesResult::new())),
|
||||
legacy_mixnode_bonds: Arc::new(RwLock::new(MixNodesResult::new())),
|
||||
locations: Arc::new(RwLock::new(locations)),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn is_location_valid(&self, mix_id: NodeId) -> bool {
|
||||
self.locations
|
||||
.read()
|
||||
.await
|
||||
.get(&mix_id)
|
||||
.is_some_and(|cache_item| cache_item.valid_until > SystemTime::now())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_locations(&self) -> MixnodeLocationCache {
|
||||
self.locations.read().await.clone()
|
||||
}
|
||||
|
||||
pub(crate) async fn set_location(&self, mix_id: NodeId, location: Option<Location>) {
|
||||
// cache the location for this mix node so that it can be used when the mix node list is refreshed
|
||||
self.locations
|
||||
.write()
|
||||
.await
|
||||
.insert(mix_id, LocationCacheItem::new_from_location(location));
|
||||
}
|
||||
|
||||
pub(crate) async fn get_mixnode(&self, mix_id: NodeId) -> Option<MixNodeBondAnnotated> {
|
||||
self.mixnodes.read().await.get_mixnode(mix_id)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_mixnodes(&self) -> Option<HashMap<NodeId, MixNodeBondAnnotated>> {
|
||||
self.mixnodes.read().await.get_mixnodes()
|
||||
}
|
||||
|
||||
fn create_detailed_mixnode(
|
||||
&self,
|
||||
mix_id: NodeId,
|
||||
mixnodes_guard: &RwLockReadGuard<'_, MixNodesResult>,
|
||||
location: Option<&LocationCacheItem>,
|
||||
node: &MixNodeBondAnnotated,
|
||||
) -> PrettyDetailedMixNodeBond {
|
||||
let rewarding_info = &node.mixnode_details.rewarding_details;
|
||||
let denom = &rewarding_info.cost_params.interval_operating_cost.denom;
|
||||
|
||||
PrettyDetailedMixNodeBond {
|
||||
mix_id,
|
||||
location: location.and_then(|l| l.location.clone()),
|
||||
status: mixnodes_guard.determine_node_status(mix_id),
|
||||
pledge_amount: truncate_reward(rewarding_info.operator, denom),
|
||||
total_delegation: truncate_reward(rewarding_info.delegates, denom),
|
||||
owner: node.mixnode_details.bond_information.owner.clone(),
|
||||
layer: node.mixnode_details.bond_information.layer,
|
||||
mix_node: node.mixnode_details.bond_information.mix_node.clone(),
|
||||
avg_uptime: node.performance.round_to_integer(),
|
||||
node_performance: node.node_performance.clone(),
|
||||
stake_saturation: best_effort_small_dec_to_f64(node.stake_saturation) as f32,
|
||||
uncapped_saturation: best_effort_small_dec_to_f64(node.uncapped_stake_saturation)
|
||||
as f32,
|
||||
estimated_operator_apy: best_effort_small_dec_to_f64(node.estimated_operator_apy),
|
||||
estimated_delegators_apy: best_effort_small_dec_to_f64(node.estimated_delegators_apy),
|
||||
operating_cost: rewarding_info.cost_params.interval_operating_cost.clone(),
|
||||
profit_margin_percent: rewarding_info.cost_params.profit_margin_percent,
|
||||
family_id: None,
|
||||
blacklisted: node.blacklisted,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_detailed_mixnode(
|
||||
&self,
|
||||
mix_id: NodeId,
|
||||
) -> Option<PrettyDetailedMixNodeBond> {
|
||||
let mixnodes_guard = self.mixnodes.read().await;
|
||||
let location_guard = self.locations.read().await;
|
||||
|
||||
let bond = mixnodes_guard.get_mixnode(mix_id);
|
||||
let location = location_guard.get(&mix_id);
|
||||
|
||||
bond.map(|bond| self.create_detailed_mixnode(mix_id, &mixnodes_guard, location, &bond))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_detailed_mixnodes(&self) -> Vec<PrettyDetailedMixNodeBond> {
|
||||
let mixnodes_guard = self.mixnodes.read().await;
|
||||
let location_guard = self.locations.read().await;
|
||||
|
||||
mixnodes_guard
|
||||
.all_mixnodes
|
||||
.values()
|
||||
.map(|bond| {
|
||||
let location = location_guard.get(&bond.mix_id());
|
||||
self.create_detailed_mixnode(bond.mix_id(), &mixnodes_guard, location, bond)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn get_legacy_detailed_mixnodes(&self) -> Vec<PrettyDetailedMixNodeBond> {
|
||||
let legacy_mixnodes = self.legacy_mixnode_bonds.read().await;
|
||||
let location_guard = self.locations.read().await;
|
||||
|
||||
legacy_mixnodes
|
||||
.all_mixnodes
|
||||
.values()
|
||||
.map(|bond| {
|
||||
let location = location_guard.get(&bond.mix_id());
|
||||
self.create_detailed_mixnode(bond.mix_id(), &legacy_mixnodes, location, bond)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn update_cache(
|
||||
&self,
|
||||
all_bonds: Vec<MixNodeBondAnnotated>,
|
||||
rewarded_nodes: HashSet<NodeId>,
|
||||
active_nodes: HashSet<NodeId>,
|
||||
legacy_mixnode_bonds: Vec<MixNodeBond>,
|
||||
) {
|
||||
let mut guard = self.mixnodes.write().await;
|
||||
let mut guard_legacy_mixnodes = self.legacy_mixnode_bonds.write().await;
|
||||
|
||||
let legacy_mixnode_bond_ids: Vec<&NodeId> = legacy_mixnode_bonds
|
||||
.iter()
|
||||
.map(|bond| &bond.mix_id)
|
||||
.collect();
|
||||
|
||||
guard.all_mixnodes = all_bonds
|
||||
.into_iter()
|
||||
.map(|bond| (bond.mix_id(), bond))
|
||||
.collect();
|
||||
guard.rewarded_mixnodes = rewarded_nodes;
|
||||
guard.active_mixnodes = active_nodes;
|
||||
guard.valid_until = SystemTime::now() + CACHE_ENTRY_TTL;
|
||||
guard_legacy_mixnodes.all_mixnodes = guard
|
||||
.all_mixnodes
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter(|(node_id, _bond)| legacy_mixnode_bond_ids.iter().any(|i| **i == *node_id))
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user