Compare commits

..

3 Commits

Author SHA1 Message Date
mfahampshire 59ebfbf50f update 2.32 to 2.33 2025-04-11 14:50:43 +02:00
mfahampshire bb1b2a75dd fix fatfinger greater than 2025-04-11 14:46:23 +02:00
mfahampshire da3cbe095b libc version comment 2025-04-11 14:21:32 +02:00
400 changed files with 24757 additions and 20248 deletions
-2
View File
@@ -102,8 +102,6 @@ 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
+2 -6
View File
@@ -16,12 +16,8 @@ jobs:
CARGO_TERM_COLOR: always
RUSTUP_PERMIT_COPY_RENAME: 1
steps:
- 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: 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
continue-on-error: true
- name: Check out repository code
@@ -1,47 +0,0 @@
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
+29 -25
View File
@@ -18,7 +18,11 @@ jobs:
runs-on: ${{ matrix.platform }}
outputs:
release_tag: ${{ github.ref_name }}
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 }}
steps:
- uses: actions/checkout@v4
@@ -29,16 +33,10 @@ jobs:
node-version: 21
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
uses: actions-rs/toolchain@v1
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 }}
@@ -68,6 +66,12 @@ 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
@@ -90,22 +94,10 @@ jobs:
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_IDENTITY_ID }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_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 }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
run: |
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 -
yarn build-macx86
- name: Upload Artifact
uses: actions/upload-artifact@v4
@@ -128,10 +120,22 @@ 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: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
secrets: inherit
release_tag: ${{ github.ref_name }}
secrets: inherit
+42 -81
View File
@@ -3,108 +3,71 @@ 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: [ubuntu-22.04]
platform: [custom-ubuntu-22.04]
runs-on: ${{ matrix.platform }}
outputs:
release_tag: ${{ github.ref_name }}
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 }}
steps:
- uses: actions/checkout@v4
- 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: Tauri dependencies
run: >
sudo apt-get update &&
sudo apt-get install -y webkit2gtk-4.0
continue-on-error: true
- name: Node
uses: actions/setup-node@v4
with:
node-version: 21
cache: 'yarn'
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
uses: actions-rs/toolchain@v1
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-appimage.tar.gz
path: |
nym-wallet/target/release/bundle/appimage/*.AppImage.tar.gz
nym-wallet/target/release/bundle/*/nym-wallet*.AppImage.tar.gz
name: nym-wallet_1.0.0_amd64.AppImage.tar.gz
path: nym-wallet/target/release/bundle/appimage/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
@@ -112,26 +75,24 @@ jobs:
with:
files: |
nym-wallet/target/release/bundle/appimage/*.AppImage
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
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/"
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: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
secrets: inherit
release_tag: ${{ github.ref_name }}
secrets: inherit
+61 -110
View File
@@ -1,12 +1,6 @@
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]
@@ -24,61 +18,53 @@ jobs:
runs-on: ${{ matrix.platform }}
outputs:
release_tag: ${{ github.ref_name }}
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 }}
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: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v2
- 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: Node
uses: actions/setup-node@v4
with:
node-version: 21
- 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 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: Install project dependencies
shell: bash
run: cd .. && yarn --network-timeout 100000
@@ -91,50 +77,18 @@ jobs:
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
SSL_COM_USERNAME: ${{ inputs.sign && secrets.SSL_COM_USERNAME }}
SSL_COM_PASSWORD: ${{ inputs.sign && secrets.SSL_COM_PASSWORD }}
SSL_COM_CREDENTIAL_ID: ${{ inputs.sign && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
SSL_COM_TOTP_SECRET: ${{ inputs.sign && secrets.SSL_COM_TOTP_SECRET }}
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
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
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
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
name: nym-wallet_1.0.0_x64_en-US.msi
path: nym-wallet/target/release/bundle/msi/nym-wallet_1.*.msi
retention-days: 30
- id: create-release
@@ -143,28 +97,25 @@ jobs:
if: github.event_name == 'release'
with:
files: |
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
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/"
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: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
secrets: inherit
release_tag: ${{ github.ref_name }}
secrets: inherit
Generated
+55 -41
View File
@@ -908,16 +908,6 @@ 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"
@@ -1574,7 +1564,6 @@ dependencies = [
"ciborium",
"clap",
"criterion-plot",
"futures",
"is-terminal",
"itertools 0.10.5",
"num-traits",
@@ -1587,7 +1576,6 @@ dependencies = [
"serde_derive",
"serde_json",
"tinytemplate",
"tokio",
"walkdir",
]
@@ -1603,9 +1591,9 @@ dependencies = [
[[package]]
name = "crossbeam-channel"
version = "0.5.15"
version = "0.5.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
dependencies = [
"crossbeam-utils",
]
@@ -2185,12 +2173,6 @@ 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"
@@ -4703,7 +4685,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "nym-api"
version = "1.1.56"
version = "1.1.55"
dependencies = [
"anyhow",
"async-trait",
@@ -4714,7 +4696,6 @@ dependencies = [
"bip39",
"bs58",
"cfg-if",
"chrono",
"clap",
"console-subscriber",
"cosmwasm-std",
@@ -4724,7 +4705,6 @@ dependencies = [
"cw4",
"dashmap",
"dirs",
"dotenv",
"futures",
"getset",
"humantime-serde",
@@ -4957,7 +4937,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.53"
version = "1.1.52"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -5040,7 +5020,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.53"
version = "1.1.52"
dependencies = [
"bs58",
"clap",
@@ -5082,6 +5062,7 @@ dependencies = [
"async-trait",
"base64 0.22.1",
"bs58",
"cfg-if",
"clap",
"comfy-table",
"futures",
@@ -5096,10 +5077,12 @@ 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",
@@ -5120,6 +5103,7 @@ dependencies = [
"serde_json",
"sha2 0.10.8",
"si-scale",
"tap",
"tempfile",
"thiserror 2.0.12",
"time",
@@ -5141,6 +5125,7 @@ version = "0.1.0"
dependencies = [
"humantime-serde",
"nym-config",
"nym-country-group",
"nym-pemstore",
"nym-sphinx-addressing",
"nym-sphinx-params",
@@ -5282,6 +5267,14 @@ dependencies = [
"vergen",
]
[[package]]
name = "nym-country-group"
version = "0.1.0"
dependencies = [
"serde",
"tracing",
]
[[package]]
name = "nym-cpp-ffi"
version = "0.1.2"
@@ -5558,6 +5551,31 @@ 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"
@@ -6037,7 +6055,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.1.54"
version = "1.1.53"
dependencies = [
"addr",
"anyhow",
@@ -6088,7 +6106,7 @@ dependencies = [
[[package]]
name = "nym-node"
version = "1.9.0"
version = "1.8.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -6097,18 +6115,15 @@ 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",
@@ -6148,7 +6163,6 @@ dependencies = [
"rand 0.8.5",
"serde",
"serde_json",
"sha2 0.10.8",
"sysinfo",
"thiserror 2.0.12",
"time",
@@ -6221,7 +6235,7 @@ dependencies = [
[[package]]
name = "nym-node-status-api"
version = "2.1.0"
version = "2.0.0"
dependencies = [
"ammonia",
"anyhow",
@@ -6237,6 +6251,7 @@ dependencies = [
"nym-bin-common",
"nym-contracts-common",
"nym-crypto",
"nym-explorer-client",
"nym-http-api-client",
"nym-network-defaults",
"nym-node-metrics",
@@ -6482,7 +6497,7 @@ dependencies = [
[[package]]
name = "nym-socks5-client"
version = "1.1.53"
version = "1.1.52"
dependencies = [
"bs58",
"clap",
@@ -6645,7 +6660,6 @@ dependencies = [
"rand_chacha 0.3.1",
"serde",
"thiserror 2.0.12",
"tracing",
"wasm-bindgen",
]
@@ -6699,6 +6713,8 @@ name = "nym-sphinx-framing"
version = "0.1.0"
dependencies = [
"bytes",
"log",
"nym-metrics",
"nym-sphinx-acknowledgements",
"nym-sphinx-addressing",
"nym-sphinx-forwarding",
@@ -6707,7 +6723,6 @@ dependencies = [
"thiserror 2.0.12",
"tokio",
"tokio-util",
"tracing",
]
[[package]]
@@ -7086,7 +7101,7 @@ dependencies = [
[[package]]
name = "nymvisor"
version = "0.1.18"
version = "0.1.17"
dependencies = [
"anyhow",
"bytes",
@@ -9231,9 +9246,9 @@ dependencies = [
[[package]]
name = "sphinx-packet"
version = "0.6.0"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c26f0c20d909fdda1c5d0ece3973127ca421984d55b000215df365e93722fc6e"
checksum = "c23047e0cf36ff6904603f499fd13153425cdf5ba47bfbaedbc999da0bd92f4e"
dependencies = [
"aes",
"arrayref",
@@ -9252,7 +9267,6 @@ dependencies = [
"sha2 0.10.8",
"subtle 2.6.1",
"x25519-dalek",
"zeroize",
]
[[package]]
@@ -10047,9 +10061,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.44.2"
version = "1.44.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
dependencies = [
"backtrace",
"bytes",
+10 -7
View File
@@ -39,6 +39,7 @@ 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",
@@ -96,6 +97,9 @@ members = [
"common/wireguard",
"common/wireguard-types",
"documentation/autodoc",
# "explorer-api",
# "explorer-api/explorer-api-requests",
# "explorer-api/explorer-client",
"gateway",
"integrations/bity",
"nym-api",
@@ -200,7 +204,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 = "3.0.1"
bloomfilter = "1.0.14"
bs58 = "0.5.1"
bytecodec = "0.4.15"
bytes = "1.10.1"
@@ -266,6 +270,7 @@ 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"
@@ -298,6 +303,9 @@ 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"
@@ -312,7 +320,7 @@ serde_with = "3.9.0"
serde_yaml = "0.9.25"
sha2 = "0.10.8"
si-scale = "0.2.3"
sphinx-packet = "=0.6.0"
sphinx-packet = "=0.3.2"
sqlx = "0.7.4"
strum = "0.26"
strum_macros = "0.26"
@@ -402,11 +410,6 @@ 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
+1 -2
View File
@@ -168,9 +168,8 @@ 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:
dotenv -f envs/sandbox.env -- cargo test --test public-api-tests
cd nym-api/tests/functional_test && yarn test:qa
# Build debian package, and update PPA
deb-cli: build-nym-cli
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.53"
version = "1.1.52"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.53"
version = "1.1.52"
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"
+1
View File
@@ -87,6 +87,7 @@ 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,
+22 -1
View File
@@ -16,7 +16,8 @@ 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::config::ForgetMe;
use nym_client_core::client::topology_control::geo_aware_provider::CountryGroup;
use nym_client_core::config::{ForgetMe, GroupBy, TopologyStructure};
use nym_config::OptionalSet;
use nym_sphinx::addressing::Recipient;
use nym_sphinx::params::{PacketSize, PacketType};
@@ -106,6 +107,7 @@ 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>,
@@ -136,6 +138,21 @@ 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 {
@@ -159,6 +176,10 @@ 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)
+13
View File
@@ -6,6 +6,7 @@ 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;
@@ -36,6 +37,10 @@ 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)]
@@ -54,6 +59,7 @@ 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,
@@ -64,6 +70,13 @@ 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);
+4
View File
@@ -12,6 +12,7 @@ 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 }
@@ -23,6 +24,7 @@ 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"] }
@@ -33,7 +35,9 @@ 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,6 +14,7 @@ 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
+39 -13
View File
@@ -65,10 +65,11 @@ 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 exists
/// Time interval between reporting statistics to the given provider if it exist
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)]
@@ -257,6 +258,15 @@ 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()
@@ -405,14 +415,6 @@ 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,
}
@@ -440,10 +442,6 @@ 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,
}
}
}
@@ -548,6 +546,9 @@ 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,
@@ -569,6 +570,30 @@ 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 {
@@ -576,6 +601,7 @@ 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,
+14 -29
View File
@@ -2,9 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
use crate::old::v5::{
AcknowledgementsV5, ClientV5, ConfigV5, CountryGroupV5, CoverTrafficV5, DebugConfigV5,
GatewayConnectionV5, GroupByV5, ReplySurbsV5, TopologyStructureV5, TopologyV5, TrafficV5,
AcknowledgementsV5, ClientV5, ConfigV5, 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};
@@ -368,47 +369,31 @@ 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(CountryGroupV4),
CountryGroup(CountryGroup),
NymAddress(Recipient),
}
impl From<GroupByV4> for GroupByV5 {
fn from(value: GroupByV4) -> Self {
match value {
GroupByV4::CountryGroup(country) => GroupByV5::CountryGroup(country.into()),
GroupByV4::CountryGroup(country) => GroupByV5::CountryGroup(country),
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 {
+29 -12
View File
@@ -2,8 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use crate::{
Acknowledgements, Client, Config, CoverTraffic, DebugConfig, GatewayConnection, ReplySurbs,
Topology, Traffic,
Acknowledgements, Client, Config, CountryGroup, CoverTraffic, DebugConfig, GatewayConnection,
GroupBy, ReplySurbs, Topology, TopologyStructure, Traffic,
};
use nym_sphinx_addressing::Recipient;
use nym_sphinx_params::{PacketSize, PacketType};
@@ -146,6 +146,7 @@ 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 {
@@ -371,24 +372,40 @@ pub enum TopologyStructureV5 {
GeoAware(GroupByV5),
}
#[derive(Copy, Clone, Hash, PartialEq, Eq, Serialize, Deserialize, Debug)]
pub enum CountryGroupV5 {
Europe,
NorthAmerica,
SouthAmerica,
Oceania,
Asia,
Africa,
Unknown,
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()),
}
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum GroupByV5 {
CountryGroup(CountryGroupV5),
CountryGroup(CountryGroup),
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,12 +552,18 @@ 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(|| {
Box::new(NymApiTopologyProvider::new(
custom_provider.unwrap_or_else(|| match config_topology.topology_structure {
config::TopologyStructure::NymApi => 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,10 +62,6 @@ 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,
@@ -134,7 +130,6 @@ 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,
@@ -187,7 +182,6 @@ 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,10 +109,6 @@ 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 {
@@ -122,7 +118,6 @@ impl Config {
average_packet_delay: Duration,
average_ack_delay: Duration,
deterministic_route_selection: bool,
use_legacy_reply_surb_format: bool,
) -> Self {
Config {
ack_key,
@@ -132,7 +127,6 @@ impl Config {
average_ack_delay,
primary_packet_size: PacketSize::default(),
secondary_packet_size: None,
use_legacy_sphinx_format: use_legacy_reply_surb_format,
}
}
@@ -192,7 +186,6 @@ where
config.sender_address,
config.average_packet_delay,
config.average_ack_delay,
config.use_legacy_sphinx_format,
);
MessageHandler {
config,
@@ -261,11 +254,9 @@ 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(
self.config.use_legacy_sphinx_format,
amount,
topology,
)?;
let reply_surbs = self
.message_preparer
.generate_reply_surbs(amount, topology)?;
let reply_keys = reply_surbs
.iter()
@@ -531,7 +522,6 @@ 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,
));
@@ -569,12 +559,8 @@ where
.generate_reply_surbs_with_keys(num_reply_surbs as usize)
.await?;
let message = NymMessage::new_repliable(RepliableMessage::new_data(
self.config.use_legacy_sphinx_format,
message,
sender_tag,
reply_surbs,
));
let message =
NymMessage::new_repliable(RepliableMessage::new_data(message, sender_tag, reply_surbs));
self.try_split_and_send_non_reply_message(
message,
@@ -99,7 +99,6 @@ 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,7 +252,6 @@ 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(content) => {
let reply_surbs = content.reply_surbs;
let message = content.message;
RepliableMessageContent::Data {
message,
reply_surbs,
} => {
trace!(
"received message that also contained additional {} reply surbs from {:?}!",
reply_surbs.len(),
@@ -264,9 +264,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
(reply_surbs, false)
}
RepliableMessageContent::AdditionalSurbs(content) => {
let reply_surbs = content.reply_surbs;
RepliableMessageContent::AdditionalSurbs { reply_surbs } => {
trace!(
"received additional {} reply surbs from {:?}!",
reply_surbs.len(),
@@ -274,37 +272,9 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
);
(reply_surbs, true)
}
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;
RepliableMessageContent::Heartbeat {
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)
}
@@ -0,0 +1,214 @@
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,8 +17,11 @@ 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;
+15
View File
@@ -0,0 +1,15 @@
[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
+158
View File
@@ -0,0 +1,158 @@
// 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,21 +172,6 @@ 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,24 +95,6 @@ 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,13 +70,6 @@ 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,16 +145,6 @@ 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> {
-5
View File
@@ -37,11 +37,6 @@ 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,6 +10,9 @@ 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),
}
+29
View File
@@ -37,3 +37,32 @@ 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
}
}
-10
View File
@@ -39,10 +39,6 @@ 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>,
@@ -61,7 +57,6 @@ where
deterministic_route_selection: bool,
average_packet_delay: Duration,
average_ack_delay: Duration,
use_legacy_sphinx_format: bool,
ack_key: Arc<AckKey>,
) -> Self {
Self {
@@ -72,7 +67,6 @@ where
deterministic_route_selection,
average_packet_delay,
average_ack_delay,
use_legacy_sphinx_format,
ack_key,
}
}
@@ -251,10 +245,6 @@ 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
}
-17
View File
@@ -363,7 +363,6 @@ 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) {
@@ -372,7 +371,6 @@ 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;
@@ -380,7 +378,6 @@ 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;
@@ -388,7 +385,6 @@ 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;
@@ -396,7 +392,6 @@ impl MetricsController {
self.register_metric(metric);
}
#[inline(always)]
pub fn register_histogram<'a>(
&self,
name: &str,
@@ -409,7 +404,6 @@ 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);
@@ -419,7 +413,6 @@ 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);
@@ -429,7 +422,6 @@ 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);
@@ -439,14 +431,12 @@ 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();
@@ -456,7 +446,6 @@ 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);
@@ -466,7 +455,6 @@ impl MetricsController {
}
}
#[inline(always)]
pub fn maybe_register_and_set<'a>(
&self,
name: &str,
@@ -480,7 +468,6 @@ impl MetricsController {
}
}
#[inline(always)]
pub fn maybe_register_and_set_float<'a>(
&self,
name: &str,
@@ -494,7 +481,6 @@ impl MetricsController {
}
}
#[inline(always)]
pub fn maybe_register_and_add_to_histogram<'a>(
&self,
name: &str,
@@ -509,7 +495,6 @@ 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();
@@ -518,7 +503,6 @@ impl MetricsController {
}
}
#[inline(always)]
pub fn maybe_register_and_inc_by<'a>(
&self,
name: &str,
@@ -532,7 +516,6 @@ impl MetricsController {
}
}
#[inline(always)]
pub fn register_metric(&self, metric: impl Into<Metric>) {
let m = metric.into();
let fq_name = m.fq_name();
@@ -37,10 +37,8 @@ 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],
@@ -59,6 +57,8 @@ 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,7 +69,14 @@ impl SurbAck {
Some(packet_size),
)?,
PacketType::Mix => NymPacket::sphinx_build(
use_legacy_sphinx_format,
packet_size,
surb_ack_payload,
&route,
&destination,
&delays,
)?,
#[allow(deprecated)]
PacketType::Vpn => NymPacket::sphinx_build(
packet_size,
surb_ack_payload,
&route,
@@ -99,6 +106,8 @@ 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,
}
}
@@ -130,6 +139,8 @@ 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))
+1 -1
View File
@@ -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())
Self::try_from_bytes(value.as_bytes_ref())
}
}
@@ -12,7 +12,6 @@ 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,25 +1,20 @@
// Copyright 2021-2025 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2021-2023 - 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, NymNodeRoutingAddressError, MAX_NODE_ADDRESS_UNPADDED_LEN,
};
use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN};
use nym_sphinx_params::packet_sizes::PacketSize;
use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm};
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_sphinx_types::{NymPacket, SURBMaterial, SphinxError, SURB};
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::Duration;
use std::time;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -36,9 +31,6 @@ 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),
}
@@ -88,10 +80,6 @@ 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();
@@ -103,8 +91,7 @@ impl ReplySurb {
pub fn construct<R>(
rng: &mut R,
recipient: &Recipient,
average_delay: Duration,
use_legacy_surb_format: bool,
average_delay: time::Duration,
topology: &NymRouteProvider,
) -> Result<Self, NymTopologyError>
where
@@ -114,10 +101,7 @@ impl ReplySurb {
let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len());
let destination = recipient.as_sphinx_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)
}
let surb_material = SURBMaterial::new(route, delays, destination);
// this can't fail as we know we have a valid route to gateway and have correct number of delays
Ok(ReplySurb {
@@ -126,10 +110,14 @@ impl ReplySurb {
})
}
/// Returns the expected number of bytes the [`ReplySURB`] will take after serialization using the new encoding format.
/// Returns the expected number of bytes the [`ReplySURB`] will take after serialization.
/// Useful for deserialization from a bytes stream.
pub fn v2_serialised_len(num_hops: u8) -> usize {
Self::BASE_OVERHEAD + num_hops as usize * PAYLOAD_KEY_SEED_SIZE
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 encryption_key(&self) -> &SurbEncryptionKey {
@@ -155,12 +143,7 @@ impl ReplySurb {
let surb = match SURB::from_bytes(&bytes[SurbEncryptionKeySize::USIZE..]) {
Err(err) => return Err(ReplySurbError::RecoveryError(err)),
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(surb) => surb,
};
Ok(ReplySurb {
@@ -8,15 +8,9 @@ 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)]
@@ -109,23 +103,31 @@ pub struct RepliableMessage {
impl Display for RepliableMessage {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self.content {
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)
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,
)
}
}
}
@@ -133,43 +135,26 @@ 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 {
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,
content: RepliableMessageContent::Data {
message: data,
reply_surbs,
},
}
}
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,
content: RepliableMessageContent::AdditionalSurbs { reply_surbs },
}
}
@@ -207,18 +192,35 @@ impl RepliableMessage {
}
}
#[derive(Debug)]
// 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))
}
#[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 {
@@ -231,11 +233,6 @@ 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 }),
}
}
@@ -244,24 +241,58 @@ 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(DataV1),
AdditionalSurbs(AdditionalSurbsV1),
Heartbeat(HeartbeatV1),
DataV2(DataV2),
AdditionalSurbsV2(AdditionalSurbsV2),
HeartbeatV2(HeartbeatV2),
Data {
message: Vec<u8>,
reply_surbs: Vec<ReplySurb>,
},
AdditionalSurbs {
reply_surbs: Vec<ReplySurb>,
},
Heartbeat {
additional_reply_surbs: Vec<ReplySurb>,
},
}
impl RepliableMessageContent {
pub fn into_bytes(self) -> Vec<u8> {
match self {
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(),
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()
}
}
}
@@ -273,25 +304,19 @@ impl RepliableMessageContent {
return Err(InvalidReplyRequestError::RequestTooShortToDeserialize);
}
let (reply_surbs, n) = recover_reply_surbs(bytes)?;
match tag {
RepliableMessageContentTag::Data => {
Ok(RepliableMessageContent::Data(DataV1::from_bytes(bytes)?))
RepliableMessageContentTag::Data => Ok(RepliableMessageContent::Data {
message: bytes[n..].to_vec(),
reply_surbs,
}),
RepliableMessageContentTag::AdditionalSurbs => {
Ok(RepliableMessageContent::AdditionalSurbs { 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)?,
)),
RepliableMessageContentTag::Heartbeat => Ok(RepliableMessageContent::Heartbeat {
additional_reply_surbs: reply_surbs,
}),
}
}
@@ -302,22 +327,30 @@ 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(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(),
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()
}
}
}
}
@@ -481,22 +514,18 @@ 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, X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION,
SURBMaterial, NODE_ADDRESS_LENGTH,
};
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)
@@ -538,9 +567,11 @@ mod tests {
}
}
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)
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)
.map(|_| Delay::new_from_nanos(rng.next_u64()))
.collect();
let mut destination_bytes = [0u8; 32];
@@ -554,58 +585,50 @@ mod tests {
identifier_bytes,
);
let mut surb_material = SURBMaterial::new(route, delays, destination);
if legacy {
surb_material =
surb_material.with_version(X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION);
}
let surb = SURBMaterial::new(route, delays, destination)
.construct_SURB()
.unwrap();
ReplySurb {
surb: surb_material.construct_SURB().unwrap(),
surb,
encryption_key: SurbEncryptionKey::new(rng),
}
}
pub(super) fn reply_surbs(
rng: &mut ChaCha20Rng,
n: usize,
legacy: bool,
hops: u8,
) -> Vec<ReplySurb> {
pub(super) fn reply_surbs(rng: &mut ChaCha20Rng, n: usize) -> Vec<ReplySurb> {
let mut surbs = Vec::with_capacity(n);
for _ in 0..n {
surbs.push(reply_surb(rng, legacy, hops))
surbs.push(reply_surb(rng))
}
surbs
}
pub(super) fn repliable_content_data_v1(
pub(super) fn repliable_content_data(
rng: &mut ChaCha20Rng,
msg_len: usize,
surbs: usize,
) -> RepliableMessageContent {
RepliableMessageContent::Data(DataV1 {
RepliableMessageContent::Data {
message: random_vec_u8(rng, msg_len),
reply_surbs: reply_surbs(rng, surbs, true, LEGACY_HOPS),
})
reply_surbs: reply_surbs(rng, surbs),
}
}
pub(super) fn repliable_content_surbs_v1(
pub(super) fn repliable_content_surbs(
rng: &mut ChaCha20Rng,
surbs: usize,
) -> RepliableMessageContent {
RepliableMessageContent::AdditionalSurbs(AdditionalSurbsV1 {
reply_surbs: reply_surbs(rng, surbs, true, LEGACY_HOPS),
})
RepliableMessageContent::AdditionalSurbs {
reply_surbs: reply_surbs(rng, surbs),
}
}
pub(super) fn repliable_content_heartbeat_v1(
pub(super) fn repliable_content_heartbeat(
rng: &mut ChaCha20Rng,
surbs: usize,
) -> RepliableMessageContent {
RepliableMessageContent::Heartbeat(HeartbeatV1 {
additional_reply_surbs: reply_surbs(rng, surbs, true, LEGACY_HOPS),
})
RepliableMessageContent::Heartbeat {
additional_reply_surbs: reply_surbs(rng, surbs),
}
}
pub(super) fn reply_content_data(
@@ -626,70 +649,37 @@ 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_for_v1_messages() {
fn serialized_size_matches_actual_serialization() {
let mut rng = fixtures::test_rng();
let data1 = RepliableMessage {
sender_tag: fixtures::sender_tag(&mut rng),
content: fixtures::repliable_content_data_v1(&mut rng, 10000, 0),
content: fixtures::repliable_content_data(&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_v1(&mut rng, 10, 100),
content: fixtures::repliable_content_data(&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_v1(&mut rng, 100000, 1000),
content: fixtures::repliable_content_data(&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_v1(&mut rng, 1),
content: fixtures::repliable_content_surbs(&mut rng, 1),
};
assert_eq!(
additional_surbs1.serialized_size(),
@@ -698,7 +688,7 @@ mod tests {
let additional_surbs2 = RepliableMessage {
sender_tag: fixtures::sender_tag(&mut rng),
content: fixtures::repliable_content_surbs_v1(&mut rng, 1000),
content: fixtures::repliable_content_surbs(&mut rng, 1000),
};
assert_eq!(
additional_surbs2.serialized_size(),
@@ -707,173 +697,53 @@ mod tests {
let heartbeat1 = RepliableMessage {
sender_tag: fixtures::sender_tag(&mut rng),
content: fixtures::repliable_content_heartbeat_v1(&mut rng, 1),
content: fixtures::repliable_content_heartbeat(&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_v1(&mut rng, 1000),
content: fixtures::repliable_content_heartbeat(&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_for_v1_messages() {
fn serialized_size_matches_actual_serialization() {
let mut rng = fixtures::test_rng();
let data1 = fixtures::repliable_content_data_v1(&mut rng, 10000, 0);
let data1 = fixtures::repliable_content_data(&mut rng, 10000, 0);
assert_eq!(data1.serialized_size(), data1.into_bytes().len());
let data2 = fixtures::repliable_content_data_v1(&mut rng, 10, 100);
let data2 = fixtures::repliable_content_data(&mut rng, 10, 100);
assert_eq!(data2.serialized_size(), data2.into_bytes().len());
let data3 = fixtures::repliable_content_data_v1(&mut rng, 100000, 1000);
let data3 = fixtures::repliable_content_data(&mut rng, 100000, 1000);
assert_eq!(data3.serialized_size(), data3.into_bytes().len());
let additional_surbs1 = fixtures::repliable_content_surbs_v1(&mut rng, 1);
let additional_surbs1 = fixtures::repliable_content_surbs(&mut rng, 1);
assert_eq!(
additional_surbs1.serialized_size(),
additional_surbs1.into_bytes().len()
);
let additional_surbs2 = fixtures::repliable_content_surbs_v1(&mut rng, 1000);
let additional_surbs2 = fixtures::repliable_content_surbs(&mut rng, 1000);
assert_eq!(
additional_surbs2.serialized_size(),
additional_surbs2.into_bytes().len()
);
let heartbeat1 = fixtures::repliable_content_heartbeat_v1(&mut rng, 1);
let heartbeat1 = fixtures::repliable_content_heartbeat(&mut rng, 1);
assert_eq!(heartbeat1.serialized_size(), heartbeat1.into_bytes().len());
let heartbeat2 = fixtures::repliable_content_heartbeat_v1(&mut rng, 1000);
let heartbeat2 = fixtures::repliable_content_heartbeat(&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)]
@@ -1,176 +0,0 @@
// 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)
}
}
@@ -1,187 +0,0 @@
// 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)
}
}
+8 -5
View File
@@ -34,7 +34,6 @@ 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,
@@ -46,7 +45,6 @@ where
{
Ok(SurbAck::construct(
rng,
use_legacy_sphinx_format,
full_address,
ack_key,
COVER_FRAG_ID.to_bytes(),
@@ -59,7 +57,6 @@ 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,
@@ -74,7 +71,6 @@ 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,
@@ -130,7 +126,14 @@ where
// once merged, that's an easy rng injection point for sphinx packets : )
let packet = match packet_type {
PacketType::Mix => NymPacket::sphinx_build(
use_legacy_sphinx_format,
packet_size.payload_size(),
packet_payload,
&route,
&destination,
&delays,
)?,
#[allow(deprecated)]
PacketType::Vpn => NymPacket::sphinx_build(
packet_size.payload_size(),
packet_payload,
&route,
+2 -1
View File
@@ -11,11 +11,12 @@ repository = { workspace = true }
bytes = { workspace = true }
tokio-util = { workspace = true, features = ["codec"] }
thiserror = { workspace = true }
tracing = { workspace = true }
log = { 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" }
+95 -33
View File
@@ -5,7 +5,6 @@ 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;
@@ -14,25 +13,16 @@ 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("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}")]
#[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")]
@@ -66,7 +56,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::SIZE + PacketSize::AckPacket.size());
src.reserve(Header::LEGACY_SIZE + PacketSize::AckPacket.size());
return Ok(None);
}
@@ -78,7 +68,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
@@ -87,7 +77,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,
@@ -95,6 +85,8 @@ 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);
@@ -114,11 +106,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 = Header::SIZE + next_header.packet_size.size();
allocate_for_next_packet = next_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
@@ -209,15 +201,8 @@ mod packet_encoding {
SphinxDelay::new_from_nanos(42),
SphinxDelay::new_from_nanos(42),
];
NymPacket::sphinx_build(
false,
size.payload_size(),
b"foomp",
&route,
&destination,
&delays,
)
.unwrap()
NymPacket::sphinx_build(size.payload_size(), b"foomp", &route, &destination, &delays)
.unwrap()
}
#[test]
@@ -269,10 +254,34 @@ mod packet_encoding {
assert!(NymCodec.decode(&mut empty_bytes).unwrap().is_none());
assert_eq!(
empty_bytes.capacity(),
Header::SIZE + PacketSize::AckPacket.size()
Header::LEGACY_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
@@ -285,7 +294,7 @@ mod packet_encoding {
];
for packet_size in packet_sizes {
let header = Header {
packet_version: PacketVersion::new(),
packet_version: PacketVersion::Versioned(123),
packet_size,
..Default::default()
};
@@ -293,10 +302,33 @@ mod packet_encoding {
header.encode(&mut bytes);
assert!(NymCodec.decode(&mut bytes).unwrap().is_none());
assert_eq!(bytes.capacity(), Header::SIZE + packet_size.size())
assert_eq!(
bytes.capacity(),
Header::VERSIONED_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
@@ -310,10 +342,40 @@ mod packet_encoding {
assert!(NymCodec.decode(&mut bytes).unwrap().is_some());
assert_eq!(
bytes.capacity(),
Header::SIZE + PacketSize::AckPacket.size()
Header::VERSIONED_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
@@ -333,7 +395,7 @@ mod packet_encoding {
let mut bytes = BytesMut::new();
NymCodec.encode(first_packet, &mut bytes).unwrap();
bytes.put_u8(PacketVersion::new().as_u8());
bytes.put_u8(PacketVersion::new_versioned(123).as_u8().unwrap());
bytes.put_u8(packet_size as u8);
bytes.put_u8(PacketType::default() as u8);
assert!(NymCodec.decode(&mut bytes).unwrap().is_some());
+60 -63
View File
@@ -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, CURRENT_PACKET_VERSION};
use nym_sphinx_params::packet_version::PacketVersion;
use nym_sphinx_params::PacketType;
use nym_sphinx_types::NymPacket;
@@ -47,14 +47,6 @@ 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.
@@ -81,7 +73,8 @@ pub struct Header {
}
impl Header {
pub(crate) const SIZE: usize = 3;
pub(crate) const LEGACY_SIZE: usize = 2;
pub(crate) const VERSIONED_SIZE: usize = 3;
pub fn outfox() -> Header {
Header {
@@ -91,39 +84,53 @@ impl Header {
}
}
pub(crate) fn encode(&self, dst: &mut BytesMut) {
dst.reserve(Self::SIZE);
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.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::SIZE {
if src.len() < Self::LEGACY_SIZE {
// can't do anything if we don't have enough bytes - but reserve enough for the next call
src.reserve(Self::SIZE);
src.reserve(Self::LEGACY_SIZE);
return Ok(None);
}
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,
});
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])?,
}))
}
Ok(Some(Header {
packet_version,
packet_size: PacketSize::try_from(src[1])?,
packet_type: PacketType::try_from(src[2])?,
}))
}
}
@@ -150,7 +157,7 @@ mod header_encoding {
// due to the hack used to get legacy mode compatibility
let mut bytes = BytesMut::from(
[
PacketVersion::new().as_u8(),
PacketVersion::new_versioned(123).as_u8().unwrap(),
unknown_packet_size,
PacketType::default() as u8,
]
@@ -165,14 +172,7 @@ 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(
[
PacketVersion::new().as_u8(),
PacketSize::default() as u8,
unknown_packet_type,
]
.as_ref(),
);
let mut bytes = BytesMut::from([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::SIZE);
assert!(empty_bytes.capacity() > Header::LEGACY_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::SIZE);
assert!(empty_bytes.capacity() > Header::LEGACY_SIZE);
}
#[test]
fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_() {
fn header_encoding_reserves_enough_bytes_for_full_sphinx_packet_in_legacy_mode() {
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::new(),
packet_version: PacketVersion::Legacy,
packet_size,
..Default::default()
};
@@ -211,26 +211,23 @@ mod header_encoding {
}
#[test]
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"),
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())
}
}
}
+91 -192
View File
@@ -1,20 +1,18 @@
// Copyright 2021-2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::packet::FramedNymPacket;
use log::{debug, error, info, trace};
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, OutfoxProcessedPacket, PrivateKey, ProcessedPacketData,
SphinxError, Version as SphinxPacketVersion, REPLAY_TAG_SIZE,
NymProcessedPacket, OutfoxError, PrivateKey, ProcessedPacketData, SphinxError,
Version as SphinxPacketVersion,
};
use std::fmt::Display;
use thiserror::Error;
use tracing::{debug, error, info, trace};
use crate::packet::FramedNymPacket;
use nym_metrics::nanos;
use nym_sphinx_forwarding::packet::MixPacket;
#[derive(Debug)]
pub enum MixProcessingResultData {
@@ -51,26 +49,6 @@ 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)]
@@ -97,190 +75,57 @@ 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> {
let packet_size = received.packet_size();
let packet_type = received.packet_type();
nanos!("process_received", {
let packet_size = received.packet_size();
let packet_type = received.packet_type();
// unwrap the sphinx packet
let processed_packet = perform_framed_unwrapping(received, sphinx_key)?;
// unwrap the sphinx packet and if possible and appropriate, cache keys
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
perform_final_processing(processed_packet, packet_size, packet_type)
// 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
})
}
fn perform_framed_unwrapping(
received: FramedNymPacket,
sphinx_key: &PrivateKey,
) -> Result<NymProcessedPacket, PacketProcessingError> {
let packet = received.into_inner();
perform_framed_packet_processing(packet, sphinx_key)
nanos!("perform_initial_unwrapping", {
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> {
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,
nanos!("perform_initial_packet_processing", {
packet.process(sphinx_key).map_err(|err| {
debug!("Failed to unwrap NymPacket packet: {err}");
PacketProcessingError::NymPacketProcessingError(err)
})
} 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(
@@ -290,10 +135,64 @@ fn perform_final_processing(
) -> Result<MixProcessingResult, PacketProcessingError> {
match packet {
NymProcessedPacket::Sphinx(packet) => {
wrap_processed_sphinx_packet(packet, packet_size, packet_type)
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,
})
}
NymProcessedPacket::Outfox(packet) => {
wrap_processed_outfox_packet(packet, packet_size, packet_type)
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,
},
})
}
}
}
}
+11
View File
@@ -22,6 +22,17 @@ 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,6 +272,9 @@ 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,6 +27,11 @@ 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,
@@ -36,6 +41,8 @@ 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"),
}
}
+29 -43
View File
@@ -2,70 +2,56 @@
// SPDX-License-Identifier: Apache-2.0
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use thiserror::Error;
// 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;
use crate::{PacketSize, CURRENT_PACKET_VERSION_NUMBER};
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(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),
}
#[derive(Debug, Error)]
#[error("attempted to use legacy packet version")]
pub struct InvalidPacketVersion;
impl PacketVersion {
pub fn new() -> Self {
PacketVersion(CURRENT_PACKET_VERSION_NUMBER)
Self::new_versioned(CURRENT_PACKET_VERSION_NUMBER)
}
const fn unchecked(version: u8) -> PacketVersion {
PacketVersion(version)
pub fn new_legacy() -> Self {
PacketVersion::Legacy
}
pub fn as_u8(&self) -> u8 {
(*self).into()
pub fn new_versioned(version: u8) -> Self {
PacketVersion::Versioned(version)
}
}
impl TryFrom<u8> for PacketVersion {
type Error = InvalidPacketVersion;
pub fn is_legacy(&self) -> bool {
matches!(self, PacketVersion::Legacy)
}
fn try_from(value: u8) -> Result<Self, Self::Error> {
if value < INITIAL_PACKET_VERSION_NUMBER {
return Err(InvalidPacketVersion);
pub fn as_u8(&self) -> Option<u8> {
match self {
PacketVersion::Legacy => None,
PacketVersion::Versioned(version) => Some(*version),
}
Ok(PacketVersion(value))
}
}
impl From<PacketVersion> for u8 {
fn from(packet_version: PacketVersion) -> Self {
packet_version.0
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 Default for PacketVersion {
fn default() -> Self {
PacketVersion::new()
PacketVersion::Versioned(CURRENT_PACKET_VERSION_NUMBER)
}
}
+1 -3
View File
@@ -113,8 +113,7 @@ impl NymMessage {
match self {
NymMessage::Plain(data) => data,
NymMessage::Repliable(repliable) => match repliable.content {
RepliableMessageContent::Data(content) => content.message,
RepliableMessageContent::DataV2(content) => content.message,
RepliableMessageContent::Data { message, .. } => message,
_ => Vec::new(),
},
NymMessage::Reply(reply) => match reply.content {
@@ -310,7 +309,6 @@ 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![],
+25 -17
View File
@@ -51,14 +51,29 @@ 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,
@@ -68,11 +83,9 @@ 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(),
@@ -251,7 +264,14 @@ pub trait FragmentPreparer {
Some(packet_size.plaintext_size()),
)?,
PacketType::Mix => NymPacket::sphinx_build(
self.use_legacy_sphinx_format(),
packet_size.payload_size(),
packet_payload,
&route,
&destination,
&delays,
)?,
#[allow(deprecated)]
PacketType::Vpn => NymPacket::sphinx_build(
packet_size.payload_size(),
packet_payload,
&route,
@@ -311,10 +331,6 @@ 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,
}
@@ -328,7 +344,6 @@ 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();
@@ -338,7 +353,6 @@ where
sender_address,
average_packet_delay,
average_ack_delay,
use_legacy_sphinx_format,
nonce,
}
}
@@ -350,7 +364,6 @@ where
pub fn generate_reply_surbs(
&mut self,
use_legacy_reply_surb_format: bool,
amount: usize,
topology: &NymRouteProvider,
) -> Result<Vec<ReplySurb>, NymTopologyError> {
@@ -360,7 +373,6 @@ where
&mut self.rng,
&self.sender_address,
self.average_packet_delay,
use_legacy_reply_surb_format,
topology,
)?;
reply_surbs.push(reply_surb)
@@ -442,10 +454,6 @@ 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
}
+10 -26
View File
@@ -5,7 +5,7 @@ use std::{array::TryFromSliceError, fmt};
use thiserror::Error;
#[cfg(feature = "outfox")]
pub use nym_outfox::packet::{OutfoxPacket, OutfoxProcessedPacket};
use nym_outfox::packet::{OutfoxPacket, OutfoxProcessedPacket};
#[cfg(feature = "sphinx")]
pub use sphinx_packet::{SphinxPacket, SphinxPacketBuilder};
@@ -21,18 +21,15 @@ pub use nym_outfox::{
pub use sphinx_packet::{
constants::{
self, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, MAX_PATH_LENGTH, NODE_ADDRESS_LENGTH,
PAYLOAD_KEY_SIZE, REPLAY_TAG_SIZE,
PAYLOAD_KEY_SIZE,
},
crypto::{self, PrivateKey, PublicKey},
header::{self, delays, delays::Delay, ProcessedHeader, SphinxHeader, HEADER_SIZE},
packet::builder::DEFAULT_PAYLOAD_SIZE,
payload::{
key::{PayloadKey, PayloadKeySeed},
Payload, PAYLOAD_OVERHEAD_SIZE,
},
payload::{Payload, PAYLOAD_OVERHEAD_SIZE},
route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier},
surb::{SURBMaterial, SURB},
version::*,
version::Version,
Error as SphinxError, ProcessedPacket, ProcessedPacketData,
};
@@ -87,25 +84,17 @@ 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> {
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,
)?))
Ok(NymPacket::Sphinx(
SphinxPacketBuilder::new()
.with_payload_size(size)
.build_packet(message, route, destination, delays)?,
))
}
#[cfg(feature = "sphinx")]
pub fn sphinx_from_bytes(bytes: &[u8]) -> Result<NymPacket, NymPacketError> {
@@ -187,15 +176,10 @@ impl NymPacket {
}
#[cfg(feature = "sphinx")]
pub fn to_sphinx_packet(self) -> Option<SphinxPacket> {
pub fn as_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(_))
}
}
+1 -1
View File
@@ -209,7 +209,7 @@ impl ShutdownManager {
legacy_task_manager: None,
shutdown_signals: Default::default(),
tracker: Default::default(),
max_shutdown_duration: Duration::from_secs(10),
max_shutdown_duration: Default::default(),
};
// we need to add an explicit watcher for the cancellation token being cancelled
+1 -10
View File
@@ -181,14 +181,6 @@ 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,
}
@@ -222,7 +214,6 @@ 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,
}
}
@@ -238,7 +229,6 @@ 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,
}
@@ -433,6 +423,7 @@ 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,15 +109,6 @@ 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>,
@@ -141,9 +132,6 @@ 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,7 +5,9 @@ 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 should work on Ubuntu 22.04 and other Debian-based systems, but at this stage cannot be guaranteed to work everywhere.
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.
If the pre-built binaries don't work or are unavailable for your system, you will need to build the platform yourself.
+42
View File
@@ -0,0 +1,42 @@
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
+2
View File
@@ -0,0 +1,2 @@
# The path to the geoip database file
GEOIP_DB_PATH=./geo_ip/GeoLite2-City.mmdb
+4
View File
@@ -0,0 +1,4 @@
target
explorer-api-state.json
/geo_ip
!.env.dev
+39
View File
@@ -0,0 +1,39 @@
[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"] }
+60
View File
@@ -0,0 +1,60 @@
# 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
@@ -0,0 +1,13 @@
[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 }
@@ -0,0 +1,85 @@
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,
}
+18
View File
@@ -0,0 +1,18 @@
[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"] }
+118
View File
@@ -0,0 +1,118 @@
#[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)
}
+105
View File
@@ -0,0 +1,105 @@
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
}
}
+40
View File
@@ -0,0 +1,40 @@
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!"),
))
}
+19
View File
@@ -0,0 +1,19 @@
// 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>,
}
@@ -0,0 +1,37 @@
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()
}
}
@@ -0,0 +1,83 @@
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;
}
}
@@ -0,0 +1,254 @@
// 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");
}
}
@@ -0,0 +1,22 @@
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)
}
@@ -0,0 +1,4 @@
pub mod country_nodes_distribution;
pub mod distribution;
pub mod geolocate;
pub mod http;
+24
View File
@@ -0,0 +1,24 @@
// 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))
}
+8
View File
@@ -0,0 +1,8 @@
// 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>;
+3
View File
@@ -0,0 +1,3 @@
pub(crate) mod http;
pub(crate) mod location;
pub(crate) mod models;
+160
View File
@@ -0,0 +1,160 @@
// 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());
}
}
}
}
+204
View File
@@ -0,0 +1,204 @@
// 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
View File
@@ -0,0 +1 @@
pub(crate) mod location;
+76
View File
@@ -0,0 +1,76 @@
// 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
View File
@@ -0,0 +1 @@
pub(crate) mod location;
+38
View File
@@ -0,0 +1,38 @@
// 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);
}
}
}
+104
View File
@@ -0,0 +1,104 @@
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()
}]
}
+8
View File
@@ -0,0 +1,8 @@
use rocket_okapi::swagger_ui::SwaggerUIConfig;
pub(crate) fn get_docs() -> SwaggerUIConfig {
SwaggerUIConfig {
url: "../v1/openapi.json".to_owned(),
..Default::default()
}
}
+40
View File
@@ -0,0 +1,40 @@
// 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
}
}
}
+87
View File
@@ -0,0 +1,87 @@
#[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");
}
}
+41
View File
@@ -0,0 +1,41 @@
// 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()
}
+58
View File
@@ -0,0 +1,58 @@
// 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,
})
}
+197
View File
@@ -0,0 +1,197 @@
// 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))
}
}
}
+4
View File
@@ -0,0 +1,4 @@
pub(crate) mod delegations;
pub(crate) mod econ_stats;
pub(crate) mod http;
pub mod models;
+174
View File
@@ -0,0 +1,174 @@
// 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)
}
+90
View File
@@ -0,0 +1,90 @@
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()
}
+8
View File
@@ -0,0 +1,8 @@
// 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>;
+12
View File
@@ -0,0 +1,12 @@
// 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);
+235
View File
@@ -0,0 +1,235 @@
// 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