Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 600bf42a95 | |||
| 748e3e4248 | |||
| 8cf1b6427a | |||
| 7a888c6fdf | |||
| 9a9bb89d89 | |||
| 4cc14ddcc4 | |||
| 2dbf9d97cb | |||
| 91b6f3cc3e | |||
| 84cccffcbd | |||
| af16b3f059 | |||
| b1cde0716e | |||
| 45bcdb03d8 | |||
| 44682b5ef0 | |||
| 51c9b012e2 | |||
| 50b1175622 | |||
| 29ee5984fb | |||
| e542b25ffc | |||
| 516d3f04cf | |||
| 08c09781c7 | |||
| c92de832e4 | |||
| d9d62195cb | |||
| da9115d51b | |||
| 1367cad99d | |||
| 4f6d65ab95 | |||
| 4292d8ac03 | |||
| dcb6de2421 | |||
| 1f5ed41bb3 | |||
| 091e98aa74 | |||
| 0e38126fc5 | |||
| ecbe192a88 | |||
| f0ee49788c | |||
| d2ff3cb88d | |||
| 873d15a5e1 | |||
| 53792cc839 | |||
| 415ef1bf13 | |||
| a4f6426bf9 | |||
| 0870911b3c | |||
| 9f23887cc0 | |||
| 8ab269fa05 | |||
| 7b75f22a8e | |||
| ca0449e03d | |||
| 224e63d275 | |||
| 3d77283056 | |||
| 7cc473005b | |||
| f874284850 | |||
| 7b6077ba64 | |||
| b4865520a4 | |||
| f52ebfb9c3 | |||
| 6ca2a3c539 | |||
| 717c9066d6 | |||
| 2760a17323 | |||
| 4e9f1bc0ed | |||
| d35023d14b | |||
| 400aa6ba6d | |||
| 2ba74ae120 | |||
| 9a4293a5b9 | |||
| cdddb44099 |
@@ -16,8 +16,12 @@ jobs:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUSTUP_PERMIT_COPY_RENAME: 1
|
||||
steps:
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev squashfs-tools
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y libdbus-1-dev libmnl-dev libnftnl-dev \
|
||||
libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev jq \
|
||||
libgtk-3-dev squashfs-tools libayatana-appindicator3-dev make libfuse2 unzip librsvg2-dev file \
|
||||
libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
|
||||
continue-on-error: true
|
||||
|
||||
- name: Check out repository code
|
||||
|
||||
@@ -18,11 +18,7 @@ jobs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.id }}
|
||||
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].created_at }}
|
||||
version: ${{ steps.release-info.outputs.version }}
|
||||
filename: ${{ steps.release-info.outputs.filename }}
|
||||
file_hash: ${{ steps.release-info.outputs.file_hash }}
|
||||
release_tag: ${{ github.ref_name }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -33,10 +29,16 @@ jobs:
|
||||
node-version: 21
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Add Rust target for x86_64-apple-darwin
|
||||
run: rustup target add x86_64-apple-darwin
|
||||
|
||||
- name: Set Cargo build target to x86_64
|
||||
run: echo "CARGO_BUILD_TARGET=x86_64-apple-darwin" >> $GITHUB_ENV
|
||||
|
||||
- name: Install the Apple developer certificate for code signing
|
||||
env:
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
@@ -66,12 +68,6 @@ jobs:
|
||||
fileName: '.env'
|
||||
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
|
||||
|
||||
- name: Add Rust target for x86_64-apple-darwin
|
||||
run: rustup target add x86_64-apple-darwin
|
||||
|
||||
- name: Set Cargo build target to x86_64
|
||||
run: echo "CARGO_BUILD_TARGET=x86_64-apple-darwin" >> $GITHUB_ENV
|
||||
|
||||
- name: Yarn cache clean
|
||||
shell: bash
|
||||
run: cd .. && yarn cache clean
|
||||
@@ -94,10 +90,22 @@ jobs:
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.APPLE_IDENTITY_ID }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
# Tauri v2 specific environment variables
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
TAURI_NOTARIZATION_USERNAME: ${{ secrets.APPLE_ID }}
|
||||
TAURI_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||
TAURI_NOTARIZATION_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
run: |
|
||||
yarn build-macx86
|
||||
yarn build-macx86
|
||||
|
||||
- name: Create app tarball
|
||||
run: |
|
||||
# Navigate to where the app bundle is and create the tarball
|
||||
cd target/x86_64-apple-darwin/release/bundle/macos
|
||||
echo "Creating tarball from app bundle"
|
||||
tar -czf nym-wallet.app.tar.gz NymWallet.app
|
||||
cd -
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
@@ -120,22 +128,10 @@ jobs:
|
||||
nym-wallet/target/x86_64-apple-darwin/release/bundle/dmg/*.dmg
|
||||
nym-wallet/target/x86_64-apple-darwin/release/bundle/macos/*.app.tar.gz*
|
||||
|
||||
- name: Deploy artifacts to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-avzr"
|
||||
SOURCE: "nym-wallet/target/x86_64-apple-darwin/release/bundle/macos/nym-wallet.app.tar.gz"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/${{ github.ref_name }}/nym-wallet
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
|
||||
push-release-data:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/release-calculate-hash.yml
|
||||
needs: publish-tauri
|
||||
with:
|
||||
release_tag: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
|
||||
secrets: inherit
|
||||
@@ -3,71 +3,108 @@ on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: nym-wallet
|
||||
|
||||
jobs:
|
||||
publish-tauri:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [custom-ubuntu-22.04]
|
||||
platform: [ubuntu-22.04]
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.id }}
|
||||
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].created_at }}
|
||||
version: ${{ steps.release-info.outputs.version }}
|
||||
filename: ${{ steps.release-info.outputs.filename }}
|
||||
file_hash: ${{ steps.release-info.outputs.file_hash }}
|
||||
|
||||
release_tag: ${{ github.ref_name }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Tauri dependencies
|
||||
run: >
|
||||
sudo apt-get update &&
|
||||
sudo apt-get install -y webkit2gtk-4.0
|
||||
continue-on-error: true
|
||||
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install -y libdbus-1-dev libmnl-dev libnftnl-dev \
|
||||
libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev jq \
|
||||
libgtk-3-dev squashfs-tools libayatana-appindicator3-dev make libfuse2 unzip librsvg2-dev file \
|
||||
libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
|
||||
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
|
||||
cache: 'yarn'
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
|
||||
- name: Install project dependencies
|
||||
shell: bash
|
||||
run: cd .. && yarn --network-timeout 100000
|
||||
|
||||
|
||||
- name: Install app dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Create env file
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
with:
|
||||
fileName: '.env'
|
||||
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
|
||||
|
||||
|
||||
- name: Build app
|
||||
run: yarn build
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
|
||||
- name: Check bundle directory
|
||||
run: |
|
||||
echo "Checking bundle directory structure"
|
||||
ls -la target/release/bundle || echo "Bundle directory not found"
|
||||
if [ -d "target/release/bundle/appimage" ]; then
|
||||
echo "AppImage bundle directory exists, checking contents:"
|
||||
ls -la target/release/bundle/appimage
|
||||
else
|
||||
echo "AppImage bundle directory not found, checking alternatives:"
|
||||
find target/release/bundle -type d -name "*appimage*" -o -name "*AppImage*" || echo "No AppImage directories found"
|
||||
find target/release/bundle -name "*.AppImage" -o -name "*.appimage" || echo "No AppImage files found"
|
||||
fi
|
||||
|
||||
- name: Create AppImage tarball if needed
|
||||
run: |
|
||||
# Find the AppImage file
|
||||
APPIMAGE_FILE=$(find target/release/bundle -name "*.AppImage" | head -n 1)
|
||||
if [ -n "$APPIMAGE_FILE" ]; then
|
||||
echo "Found AppImage file: $APPIMAGE_FILE"
|
||||
APPIMAGE_DIR=$(dirname "$APPIMAGE_FILE")
|
||||
APPIMAGE_NAME=$(basename "$APPIMAGE_FILE")
|
||||
|
||||
# Create tarball if it doesn't exist
|
||||
if [ ! -f "${APPIMAGE_FILE}.tar.gz" ]; then
|
||||
echo "Creating tarball for $APPIMAGE_NAME"
|
||||
cd "$APPIMAGE_DIR"
|
||||
tar -czf "${APPIMAGE_NAME}.tar.gz" "$APPIMAGE_NAME"
|
||||
cd -
|
||||
echo "Created tarball: ${APPIMAGE_FILE}.tar.gz"
|
||||
else
|
||||
echo "Tarball already exists: ${APPIMAGE_FILE}.tar.gz"
|
||||
fi
|
||||
else
|
||||
echo "WARNING: No AppImage file found!"
|
||||
fi
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nym-wallet_1.0.0_amd64.AppImage.tar.gz
|
||||
path: nym-wallet/target/release/bundle/appimage/nym-wallet*.AppImage.tar.gz
|
||||
name: nym-wallet-appimage.tar.gz
|
||||
path: |
|
||||
nym-wallet/target/release/bundle/appimage/*.AppImage.tar.gz
|
||||
nym-wallet/target/release/bundle/*/nym-wallet*.AppImage.tar.gz
|
||||
retention-days: 30
|
||||
|
||||
|
||||
- id: create-release
|
||||
name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@v2
|
||||
@@ -75,24 +112,26 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
nym-wallet/target/release/bundle/appimage/*.AppImage
|
||||
nym-wallet/target/release/bundle/appimage/*.AppImage.tar.gz*
|
||||
|
||||
- name: Deploy artifacts to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-avzr"
|
||||
SOURCE: "nym-wallet/target/release/bundle/appimage/nym-wallet*.AppImage.tar.gz"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/${{ github.ref_name }}/nym-wallet
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
|
||||
nym-wallet/target/release/bundle/appimage/*.AppImage.tar.gz
|
||||
nym-wallet/target/release/bundle/*/nym-wallet*.AppImage
|
||||
nym-wallet/target/release/bundle/*/nym-wallet*.AppImage.tar.gz
|
||||
|
||||
- name: Find AppImage tarball path for deployment
|
||||
id: find-appimage
|
||||
run: |
|
||||
APPIMAGE_TARBALL=$(find target/release/bundle -name "*.AppImage.tar.gz" | head -n 1)
|
||||
if [ -n "$APPIMAGE_TARBALL" ]; then
|
||||
echo "Found AppImage tarball: $APPIMAGE_TARBALL"
|
||||
echo "appimage_path=$APPIMAGE_TARBALL" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "WARNING: No AppImage tarball found for deployment!"
|
||||
echo "appimage_path=target/release/bundle/appimage/nym-wallet*.AppImage.tar.gz" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
push-release-data:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/release-calculate-hash.yml
|
||||
needs: publish-tauri
|
||||
with:
|
||||
release_tag: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
|
||||
secrets: inherit
|
||||
@@ -1,6 +1,12 @@
|
||||
name: publish-nym-wallet-win11
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
sign:
|
||||
description: "Sign this build using SSL.com. Signing is billed per signature so be careful"
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
@@ -18,53 +24,61 @@ jobs:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.id }}
|
||||
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].created_at }}
|
||||
version: ${{ steps.release-info.outputs.version }}
|
||||
filename: ${{ steps.release-info.outputs.filename }}
|
||||
file_hash: ${{ steps.release-info.outputs.file_hash }}
|
||||
release_tag: ${{ github.ref_name }}
|
||||
|
||||
steps:
|
||||
- name: Clean up first
|
||||
continue-on-error: true
|
||||
working-directory: .
|
||||
run: |
|
||||
cd ..
|
||||
del /s /q /A:H nym
|
||||
rmdir /s /q nym
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Import signing certificate
|
||||
env:
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||
run: |
|
||||
New-Item -ItemType directory -Path certificate
|
||||
Set-Content -Path certificate/tempCert.txt -Value $env:WINDOWS_CERTIFICATE
|
||||
certutil -decode certificate/tempCert.txt certificate/certificate.pfx
|
||||
Remove-Item -path certificate -include tempCert.txt
|
||||
Import-PfxCertificate -FilePath certificate/certificate.pfx -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
|
||||
- name: Install Rust stable
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Setup MSBuild.exe
|
||||
uses: microsoft/setup-msbuild@v2
|
||||
|
||||
- name: Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 21
|
||||
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
|
||||
- name: Create env file
|
||||
uses: timheuer/base64-to-file@v1.2
|
||||
with:
|
||||
fileName: '.env'
|
||||
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
|
||||
|
||||
- name: Install Yarn
|
||||
run: npm install -g yarn
|
||||
|
||||
- name: Download EV CodeSignTool from ssl.com
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
shell: bash
|
||||
run: |
|
||||
curl -L0 https://www.ssl.com/download/codesigntool-for-linux-and-macos/ -o codesigntool.zip
|
||||
unzip codesigntool.zip
|
||||
- name: Get EV certificate credential id
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
id: get_credential_ids
|
||||
shell: bash
|
||||
run: |
|
||||
echo "SSL_COM_CREDENTIAL_ID=$(./CodeSignTool.sh get_credential_ids -username=${{ secrets.SSL_COM_USERNAME }} -password=${{ secrets.SSL_COM_PASSWORD }} | sed -n '1!p' | sed 's/- //')" >> "$GITHUB_OUTPUT"
|
||||
- name: Add custom sign command to tauri.conf.json
|
||||
working-directory: nym-wallet/src-tauri
|
||||
if: ${{ inputs.sign }}
|
||||
shell: bash
|
||||
run: |
|
||||
yq eval --inplace '.bundle.windows +=
|
||||
{
|
||||
"signCommand": {
|
||||
"cmd": "C:\Program Files\Git\bin\bash.EXE",
|
||||
"args": [
|
||||
"/c/actions-runner/_work/nym/nym/nym-wallet/src-tauri/CodeSignTool.sh",
|
||||
"sign",
|
||||
"-username ${{ secrets.SSL_COM_USERNAME }}",
|
||||
"-password ${{ secrets.SSL_COM_PASSWORD }}",
|
||||
"-credential_id ${{ steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}",
|
||||
"-totp_secret ${{ secrets.SSL_COM_TOTP_SECRET }}",
|
||||
"-program_name NymWallet",
|
||||
"-input_file_path",
|
||||
"%1",
|
||||
"-override"
|
||||
]
|
||||
}
|
||||
}' tauri.conf.json
|
||||
- name: Install project dependencies
|
||||
shell: bash
|
||||
run: cd .. && yarn --network-timeout 100000
|
||||
@@ -77,18 +91,50 @@ jobs:
|
||||
shell: bash
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ENABLE_CODE_SIGNING: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
|
||||
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
run: yarn build
|
||||
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
|
||||
SSL_COM_USERNAME: ${{ inputs.sign && secrets.SSL_COM_USERNAME }}
|
||||
SSL_COM_PASSWORD: ${{ inputs.sign && secrets.SSL_COM_PASSWORD }}
|
||||
SSL_COM_CREDENTIAL_ID: ${{ inputs.sign && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
|
||||
SSL_COM_TOTP_SECRET: ${{ inputs.sign && secrets.SSL_COM_TOTP_SECRET }}
|
||||
run: |
|
||||
echo "Starting build process..."
|
||||
yarn build
|
||||
|
||||
- name: Check bundle directory
|
||||
shell: bash
|
||||
run: |
|
||||
echo "Checking bundle directory structure"
|
||||
|
||||
# Check standard location
|
||||
if [ -d "target/release/bundle" ]; then
|
||||
echo "Found bundle directory at standard location"
|
||||
ls -la target/release/bundle || echo "Failed to list bundle directory"
|
||||
fi
|
||||
|
||||
# Check src-tauri location
|
||||
if [ -d "src-tauri/target/release/bundle" ]; then
|
||||
echo "Found bundle directory in src-tauri"
|
||||
ls -la src-tauri/target/release/bundle || echo "Failed to list src-tauri bundle directory"
|
||||
|
||||
# Use this path for future steps
|
||||
echo "BUNDLE_PATH=src-tauri/target/release/bundle" >> $GITHUB_ENV
|
||||
else
|
||||
echo "Using standard bundle path"
|
||||
echo "BUNDLE_PATH=target/release/bundle" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
# Check for MSI files in any location
|
||||
find . -name "*.msi" -type f
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nym-wallet_1.0.0_x64_en-US.msi
|
||||
path: nym-wallet/target/release/bundle/msi/nym-wallet_1.*.msi
|
||||
name: nym-wallet.msi
|
||||
path: |
|
||||
nym-wallet/${{ env.BUNDLE_PATH }}/msi/*.msi
|
||||
nym-wallet/${{ env.BUNDLE_PATH }}/*/nym-wallet*.msi
|
||||
nym-wallet/src-tauri/target/release/bundle/msi/*.msi
|
||||
retention-days: 30
|
||||
|
||||
- id: create-release
|
||||
@@ -97,25 +143,28 @@ jobs:
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
nym-wallet/target/release/bundle/msi/*.msi
|
||||
nym-wallet/target/release/bundle/msi/*.msi.zip*
|
||||
|
||||
- name: Deploy artifacts to CI www
|
||||
continue-on-error: true
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
|
||||
ARGS: "-avzr"
|
||||
SOURCE: "nym-wallet/target/release/bundle/msi/nym-wallet_1.*.msi"
|
||||
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
|
||||
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/${{ github.ref_name }}/nym-wallet
|
||||
EXCLUDE: "/dist/, /node_modules/"
|
||||
nym-wallet/${{ env.BUNDLE_PATH }}/msi/*.msi
|
||||
nym-wallet/${{ env.BUNDLE_PATH }}/msi/*.msi.zip*
|
||||
nym-wallet/${{ env.BUNDLE_PATH }}/*/nym-wallet*.msi
|
||||
nym-wallet/src-tauri/target/release/bundle/msi/*.msi
|
||||
|
||||
- name: Find MSI path for deployment
|
||||
id: find-msi
|
||||
shell: bash
|
||||
run: |
|
||||
MSI_FILE=$(find . -name "*.msi" -type f | head -n 1)
|
||||
if [ -n "$MSI_FILE" ]; then
|
||||
echo "Found MSI file: $MSI_FILE"
|
||||
echo "msi_path=$MSI_FILE" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "WARNING: No MSI file found for deployment!"
|
||||
echo "msi_path=${{ env.BUNDLE_PATH }}/msi/nym-wallet*.msi" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
push-release-data:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
uses: ./.github/workflows/release-calculate-hash.yml
|
||||
needs: publish-tauri
|
||||
with:
|
||||
release_tag: ${{ github.ref_name }}
|
||||
secrets: inherit
|
||||
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
|
||||
secrets: inherit
|
||||
@@ -908,6 +908,16 @@ dependencies = [
|
||||
"generic-array 0.14.7",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bloomfilter"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f6d7f06817e48ea4e17532fa61bc4e8b9a101437f0623f69d2ea54284f3a817"
|
||||
dependencies = [
|
||||
"getrandom 0.2.15",
|
||||
"siphasher 1.0.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bls12_381"
|
||||
version = "0.8.0"
|
||||
@@ -1564,6 +1574,7 @@ dependencies = [
|
||||
"ciborium",
|
||||
"clap",
|
||||
"criterion-plot",
|
||||
"futures",
|
||||
"is-terminal",
|
||||
"itertools 0.10.5",
|
||||
"num-traits",
|
||||
@@ -1576,6 +1587,7 @@ dependencies = [
|
||||
"serde_derive",
|
||||
"serde_json",
|
||||
"tinytemplate",
|
||||
"tokio",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
@@ -6115,15 +6127,18 @@ dependencies = [
|
||||
"axum 0.7.9",
|
||||
"bip39",
|
||||
"blake2 0.8.1",
|
||||
"bloomfilter",
|
||||
"bs58",
|
||||
"cargo_metadata 0.18.1",
|
||||
"celes",
|
||||
"chacha",
|
||||
"clap",
|
||||
"colored",
|
||||
"criterion",
|
||||
"csv",
|
||||
"cupid",
|
||||
"futures",
|
||||
"hkdf",
|
||||
"human-repr",
|
||||
"humantime-serde",
|
||||
"indicatif",
|
||||
@@ -6163,6 +6178,7 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2 0.10.8",
|
||||
"sysinfo",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
@@ -6713,8 +6729,6 @@ name = "nym-sphinx-framing"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"log",
|
||||
"nym-metrics",
|
||||
"nym-sphinx-acknowledgements",
|
||||
"nym-sphinx-addressing",
|
||||
"nym-sphinx-forwarding",
|
||||
@@ -6723,6 +6737,7 @@ dependencies = [
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9246,9 +9261,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sphinx-packet"
|
||||
version = "0.3.2"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c23047e0cf36ff6904603f499fd13153425cdf5ba47bfbaedbc999da0bd92f4e"
|
||||
checksum = "b63a72efe7dce8a546d5cb855e60699ae69203d0d7e4335a654eb87e93d7d141"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"arrayref",
|
||||
@@ -9267,6 +9282,7 @@ dependencies = [
|
||||
"sha2 0.10.8",
|
||||
"subtle 2.6.1",
|
||||
"x25519-dalek",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10061,9 +10077,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.44.1"
|
||||
version = "1.44.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a"
|
||||
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
|
||||
@@ -204,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 = "1.0.14"
|
||||
bloomfilter = "3.0.1"
|
||||
bs58 = "0.5.1"
|
||||
bytecodec = "0.4.15"
|
||||
bytes = "1.10.1"
|
||||
@@ -303,9 +303,6 @@ rand_seeder = "0.2.3"
|
||||
rayon = "1.5.1"
|
||||
regex = "1.10.6"
|
||||
reqwest = { version = "0.12.15", default-features = false }
|
||||
rocket = "0.5.0"
|
||||
rocket_cors = "0.6.0"
|
||||
rocket_okapi = "0.8.0"
|
||||
rs_merkle = "1.5.0"
|
||||
safer-ffi = "0.1.13"
|
||||
schemars = "0.8.22"
|
||||
@@ -320,7 +317,7 @@ serde_with = "3.9.0"
|
||||
serde_yaml = "0.9.25"
|
||||
sha2 = "0.10.8"
|
||||
si-scale = "0.2.3"
|
||||
sphinx-packet = "=0.3.2"
|
||||
sphinx-packet = "=0.5.0"
|
||||
sqlx = "0.7.4"
|
||||
strum = "0.26"
|
||||
strum_macros = "0.26"
|
||||
|
||||
@@ -10,9 +10,6 @@ pub enum MixProcessingError {
|
||||
#[error("failed to recover the expected SURB-Ack packet: {0}")]
|
||||
MalformedSurbAck(#[from] SurbAckRecoveryError),
|
||||
|
||||
#[error("the received packet was set to use the very old and very much deprecated 'VPN' mode")]
|
||||
ReceivedOldTypeVpnPacket,
|
||||
|
||||
#[error("failed to process received Nym packet: {0}")]
|
||||
NymPacketProcessingError(#[from] PacketProcessingError),
|
||||
}
|
||||
|
||||
@@ -37,32 +37,3 @@ impl TicketTypeRepr {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Constants for bloom filter for double spending detection
|
||||
//Chosen for FP of
|
||||
//Calculator at https://hur.st/bloomfilter/
|
||||
pub const ECASH_DS_BLOOMFILTER_PARAMS: BloomfilterParameters = BloomfilterParameters {
|
||||
num_hashes: 10,
|
||||
bitmap_size: 1_500_000_000,
|
||||
sip_keys: [
|
||||
(12345678910111213141, 1415926535897932384),
|
||||
(7182818284590452353, 3571113171923293137),
|
||||
],
|
||||
};
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct BloomfilterParameters {
|
||||
pub num_hashes: u32,
|
||||
pub bitmap_size: u64,
|
||||
pub sip_keys: [(u64, u64); 2],
|
||||
}
|
||||
|
||||
impl BloomfilterParameters {
|
||||
pub const fn byte_size(&self) -> u64 {
|
||||
self.bitmap_size / 8
|
||||
}
|
||||
|
||||
pub const fn default_ecash() -> Self {
|
||||
ECASH_DS_BLOOMFILTER_PARAMS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,6 +363,7 @@ impl MetricsController {
|
||||
buffer
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn to_writer(&self, writer: &mut dyn std::io::Write) {
|
||||
let metrics = self.gather();
|
||||
match writer.write_all(&metrics) {
|
||||
@@ -371,6 +372,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn register_int_gauge<'a>(&self, name: &str, help: impl Into<Option<&'a str>>) {
|
||||
let Some(metric) = Metric::new_int_gauge(name, help.into().unwrap_or(name)) else {
|
||||
return;
|
||||
@@ -378,6 +380,7 @@ impl MetricsController {
|
||||
self.register_metric(metric);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn register_float_gauge<'a>(&self, name: &str, help: impl Into<Option<&'a str>>) {
|
||||
let Some(metric) = Metric::new_float_gauge(name, help.into().unwrap_or(name)) else {
|
||||
return;
|
||||
@@ -385,6 +388,7 @@ impl MetricsController {
|
||||
self.register_metric(metric);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn register_int_counter<'a>(&self, name: &str, help: impl Into<Option<&'a str>>) {
|
||||
let Some(metric) = Metric::new_int_counter(name, help.into().unwrap_or(name)) else {
|
||||
return;
|
||||
@@ -392,6 +396,7 @@ impl MetricsController {
|
||||
self.register_metric(metric);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn register_histogram<'a>(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -404,6 +409,7 @@ impl MetricsController {
|
||||
self.register_metric(metric);
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn set(&self, name: &str, value: i64) -> bool {
|
||||
if let Some(metric) = self.registry_index.get(name) {
|
||||
metric.set(value);
|
||||
@@ -413,6 +419,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn set_float(&self, name: &str, value: f64) -> bool {
|
||||
if let Some(metric) = self.registry_index.get(name) {
|
||||
metric.set_float(value);
|
||||
@@ -422,6 +429,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn add_to_histogram(&self, name: &str, value: f64) -> bool {
|
||||
if let Some(metric) = self.registry_index.get(name) {
|
||||
metric.add_histogram_observation(value);
|
||||
@@ -431,12 +439,14 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn start_timer(&self, name: &str) -> Option<HistogramTimer> {
|
||||
self.registry_index
|
||||
.get(name)
|
||||
.and_then(|metric| metric.start_timer())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn inc(&self, name: &str) -> bool {
|
||||
if let Some(metric) = self.registry_index.get(name) {
|
||||
metric.inc();
|
||||
@@ -446,6 +456,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn inc_by(&self, name: &str, value: i64) -> bool {
|
||||
if let Some(metric) = self.registry_index.get(name) {
|
||||
metric.inc_by(value);
|
||||
@@ -455,6 +466,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn maybe_register_and_set<'a>(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -468,6 +480,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn maybe_register_and_set_float<'a>(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -481,6 +494,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn maybe_register_and_add_to_histogram<'a>(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -495,6 +509,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn maybe_register_and_inc<'a>(&self, name: &str, help: impl Into<Option<&'a str>>) {
|
||||
if !self.inc(name) {
|
||||
let help = help.into();
|
||||
@@ -503,6 +518,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn maybe_register_and_inc_by<'a>(
|
||||
&self,
|
||||
name: &str,
|
||||
@@ -516,6 +532,7 @@ impl MetricsController {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub fn register_metric(&self, metric: impl Into<Metric>) {
|
||||
let m = metric.into();
|
||||
let fq_name = m.fq_name();
|
||||
|
||||
@@ -57,8 +57,6 @@ impl SurbAck {
|
||||
let packet_size = match packet_type {
|
||||
PacketType::Outfox => surb_ack_payload.len().max(MIN_PACKET_SIZE),
|
||||
PacketType::Mix => PacketSize::AckPacket.payload_size(),
|
||||
#[allow(deprecated)]
|
||||
PacketType::Vpn => PacketSize::AckPacket.payload_size(),
|
||||
};
|
||||
|
||||
let surb_ack_packet = match packet_type {
|
||||
@@ -75,14 +73,6 @@ impl SurbAck {
|
||||
&destination,
|
||||
&delays,
|
||||
)?,
|
||||
#[allow(deprecated)]
|
||||
PacketType::Vpn => NymPacket::sphinx_build(
|
||||
packet_size,
|
||||
surb_ack_payload,
|
||||
&route,
|
||||
&destination,
|
||||
&delays,
|
||||
)?,
|
||||
};
|
||||
|
||||
// in our case, the last hop is a gateway that does NOT do any delays
|
||||
@@ -106,8 +96,6 @@ impl SurbAck {
|
||||
PacketSize::OutfoxAckPacket.size() + MAX_NODE_ADDRESS_UNPADDED_LEN
|
||||
}
|
||||
PacketType::Mix => PacketSize::AckPacket.size() + MAX_NODE_ADDRESS_UNPADDED_LEN,
|
||||
#[allow(deprecated)]
|
||||
PacketType::Vpn => PacketSize::AckPacket.size() + MAX_NODE_ADDRESS_UNPADDED_LEN,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,8 +127,6 @@ impl SurbAck {
|
||||
let packet = match packet_type {
|
||||
PacketType::Outfox => NymPacket::outfox_from_bytes(&b[address_offset..])?,
|
||||
PacketType::Mix => NymPacket::sphinx_from_bytes(&b[address_offset..])?,
|
||||
#[allow(deprecated)]
|
||||
PacketType::Vpn => NymPacket::sphinx_from_bytes(&b[address_offset..])?,
|
||||
};
|
||||
|
||||
Ok((address, packet))
|
||||
|
||||
@@ -132,14 +132,6 @@ where
|
||||
&destination,
|
||||
&delays,
|
||||
)?,
|
||||
#[allow(deprecated)]
|
||||
PacketType::Vpn => NymPacket::sphinx_build(
|
||||
packet_size.payload_size(),
|
||||
packet_payload,
|
||||
&route,
|
||||
&destination,
|
||||
&delays,
|
||||
)?,
|
||||
PacketType::Outfox => NymPacket::outfox_build(
|
||||
packet_payload,
|
||||
&route,
|
||||
|
||||
@@ -11,12 +11,11 @@ repository = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
thiserror = { workspace = true }
|
||||
log = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
|
||||
nym-sphinx-types = { path = "../types", features = ["sphinx", "outfox"] }
|
||||
nym-sphinx-params = { path = "../params", features = ["sphinx", "outfox"] }
|
||||
nym-sphinx-forwarding = { path = "../forwarding" }
|
||||
nym-metrics = { path = "../../nym-metrics" }
|
||||
nym-sphinx-addressing = { path = "../addressing" }
|
||||
nym-sphinx-acknowledgements = { path = "../acknowledgements" }
|
||||
|
||||
|
||||
@@ -85,8 +85,6 @@ impl Decoder for NymCodec {
|
||||
match header.packet_type {
|
||||
PacketType::Outfox => NymPacket::outfox_from_bytes(slice)?,
|
||||
PacketType::Mix => NymPacket::sphinx_from_bytes(slice)?,
|
||||
#[allow(deprecated)]
|
||||
PacketType::Vpn => NymPacket::sphinx_from_bytes(slice)?,
|
||||
}
|
||||
} else {
|
||||
return Ok(None);
|
||||
|
||||
@@ -47,6 +47,14 @@ impl FramedNymPacket {
|
||||
pub fn into_inner(self) -> NymPacket {
|
||||
self.packet
|
||||
}
|
||||
|
||||
pub fn packet(&self) -> &NymPacket {
|
||||
&self.packet
|
||||
}
|
||||
|
||||
pub fn is_sphinx(&self) -> bool {
|
||||
self.packet.is_sphinx()
|
||||
}
|
||||
}
|
||||
|
||||
// Contains any metadata that might be useful for sending between mix nodes.
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
use log::{debug, error, info, trace};
|
||||
// Copyright 2021-2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::packet::FramedNymPacket;
|
||||
use nym_sphinx_acknowledgements::surb_ack::{SurbAck, SurbAckRecoveryError};
|
||||
use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError};
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
use nym_sphinx_params::{PacketSize, PacketType};
|
||||
use nym_sphinx_types::header::shared_secret::ExpandedSharedSecret;
|
||||
use nym_sphinx_types::{
|
||||
Delay as SphinxDelay, DestinationAddressBytes, NodeAddressBytes, NymPacket, NymPacketError,
|
||||
NymProcessedPacket, OutfoxError, PrivateKey, ProcessedPacketData, SphinxError,
|
||||
Version as SphinxPacketVersion,
|
||||
NymProcessedPacket, OutfoxError, OutfoxProcessedPacket, PrivateKey, ProcessedPacketData,
|
||||
SphinxError, Version as SphinxPacketVersion, REPLAY_TAG_SIZE,
|
||||
};
|
||||
use std::fmt::Display;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::packet::FramedNymPacket;
|
||||
use nym_metrics::nanos;
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
use tracing::{debug, error, info, trace};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MixProcessingResultData {
|
||||
@@ -49,6 +51,26 @@ pub struct MixProcessingResult {
|
||||
pub processing_data: MixProcessingResultData,
|
||||
}
|
||||
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug)]
|
||||
pub enum PartialMixProcessingResult {
|
||||
Sphinx {
|
||||
expanded_shared_secret: ExpandedSharedSecret,
|
||||
},
|
||||
Outfox,
|
||||
}
|
||||
|
||||
impl PartialMixProcessingResult {
|
||||
pub fn replay_tag(&self) -> Option<&[u8; REPLAY_TAG_SIZE]> {
|
||||
match self {
|
||||
PartialMixProcessingResult::Sphinx {
|
||||
expanded_shared_secret,
|
||||
} => Some(expanded_shared_secret.replay_tag()),
|
||||
PartialMixProcessingResult::Outfox => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type ForwardAck = MixPacket;
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -75,59 +97,192 @@ pub enum PacketProcessingError {
|
||||
#[error("failed to recover the expected SURB-Ack packet: {0}")]
|
||||
MalformedSurbAck(#[from] SurbAckRecoveryError),
|
||||
|
||||
#[error("the received packet was set to use the very old and very much deprecated 'VPN' mode")]
|
||||
ReceivedOldTypeVpnPacket,
|
||||
|
||||
#[error("failed to process received outfox packet: {0}")]
|
||||
OutfoxProcessingError(#[from] OutfoxError),
|
||||
|
||||
#[error("attempted to partially process an outfox packet")]
|
||||
PartialOutfoxProcessing,
|
||||
|
||||
#[error("this packet has already been processed before")]
|
||||
PacketReplay,
|
||||
}
|
||||
|
||||
pub struct PartiallyUnwrappedPacket {
|
||||
received_data: FramedNymPacket,
|
||||
partial_result: PartialMixProcessingResult,
|
||||
}
|
||||
|
||||
impl PartiallyUnwrappedPacket {
|
||||
/// Attempt to partially unwrap received packet to derive relevant keys
|
||||
/// to allow us to reject it for obvious bad behaviour (like replay or invalid mac)
|
||||
/// without performing full processing
|
||||
pub fn new(
|
||||
received_data: FramedNymPacket,
|
||||
sphinx_key: &PrivateKey,
|
||||
) -> Result<Self, PacketProcessingError> {
|
||||
let partial_result = match received_data.packet() {
|
||||
NymPacket::Sphinx(packet) => {
|
||||
let expanded_shared_secret =
|
||||
packet.header.compute_expanded_shared_secret(sphinx_key);
|
||||
|
||||
// don't continue if the header is malformed
|
||||
packet
|
||||
.header
|
||||
.ensure_header_integrity(&expanded_shared_secret)?;
|
||||
|
||||
PartialMixProcessingResult::Sphinx {
|
||||
expanded_shared_secret,
|
||||
}
|
||||
}
|
||||
|
||||
NymPacket::Outfox(_) => PartialMixProcessingResult::Outfox,
|
||||
};
|
||||
Ok(PartiallyUnwrappedPacket {
|
||||
received_data,
|
||||
partial_result,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn finalise_unwrapping(self) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
let packet_size = self.received_data.packet_size();
|
||||
let packet_type = self.received_data.packet_type();
|
||||
|
||||
let packet = self.received_data.into_inner();
|
||||
|
||||
// currently partial unwrapping is only implemented for sphinx packets.
|
||||
// attempting to call it for anything else should result in a failure
|
||||
let (
|
||||
NymPacket::Sphinx(packet),
|
||||
PartialMixProcessingResult::Sphinx {
|
||||
expanded_shared_secret,
|
||||
},
|
||||
) = (packet, self.partial_result)
|
||||
else {
|
||||
return Err(PacketProcessingError::PartialOutfoxProcessing);
|
||||
};
|
||||
let processed_packet = packet.process_with_expanded_secret(&expanded_shared_secret)?;
|
||||
wrap_processed_sphinx_packet(processed_packet, packet_size, packet_type)
|
||||
}
|
||||
|
||||
pub fn replay_tag(&self) -> Option<&[u8; REPLAY_TAG_SIZE]> {
|
||||
self.partial_result.replay_tag()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(FramedNymPacket, PartialMixProcessingResult)> for PartiallyUnwrappedPacket {
|
||||
fn from(
|
||||
(received_data, partial_result): (FramedNymPacket, PartialMixProcessingResult),
|
||||
) -> Self {
|
||||
PartiallyUnwrappedPacket {
|
||||
received_data,
|
||||
partial_result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_framed_packet(
|
||||
received: FramedNymPacket,
|
||||
sphinx_key: &PrivateKey,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
nanos!("process_received", {
|
||||
let packet_size = received.packet_size();
|
||||
let packet_type = received.packet_type();
|
||||
let packet_size = received.packet_size();
|
||||
let packet_type = received.packet_type();
|
||||
|
||||
// unwrap the sphinx packet and if possible and appropriate, cache keys
|
||||
let processed_packet = perform_framed_unwrapping(received, sphinx_key)?;
|
||||
// unwrap the sphinx packet
|
||||
let processed_packet = perform_framed_unwrapping(received, sphinx_key)?;
|
||||
|
||||
// for forward packets, extract next hop and set delay (but do NOT delay here)
|
||||
// for final packets, extract SURBAck
|
||||
let final_processing_result =
|
||||
perform_final_processing(processed_packet, packet_size, packet_type);
|
||||
|
||||
if final_processing_result.is_err() {
|
||||
error!("{:?}", final_processing_result)
|
||||
}
|
||||
|
||||
final_processing_result
|
||||
})
|
||||
// for forward packets, extract next hop and set delay (but do NOT delay here)
|
||||
// for final packets, extract SURBAck
|
||||
perform_final_processing(processed_packet, packet_size, packet_type)
|
||||
}
|
||||
|
||||
fn perform_framed_unwrapping(
|
||||
received: FramedNymPacket,
|
||||
sphinx_key: &PrivateKey,
|
||||
) -> Result<NymProcessedPacket, PacketProcessingError> {
|
||||
nanos!("perform_initial_unwrapping", {
|
||||
let packet = received.into_inner();
|
||||
perform_framed_packet_processing(packet, sphinx_key)
|
||||
})
|
||||
let packet = received.into_inner();
|
||||
perform_framed_packet_processing(packet, sphinx_key)
|
||||
}
|
||||
|
||||
fn perform_framed_packet_processing(
|
||||
packet: NymPacket,
|
||||
sphinx_key: &PrivateKey,
|
||||
) -> Result<NymProcessedPacket, PacketProcessingError> {
|
||||
nanos!("perform_initial_packet_processing", {
|
||||
packet.process(sphinx_key).map_err(|err| {
|
||||
debug!("Failed to unwrap NymPacket packet: {err}");
|
||||
PacketProcessingError::NymPacketProcessingError(err)
|
||||
})
|
||||
packet.process(sphinx_key).map_err(|err| {
|
||||
debug!("Failed to unwrap NymPacket packet: {err}");
|
||||
PacketProcessingError::NymPacketProcessingError(err)
|
||||
})
|
||||
}
|
||||
|
||||
fn wrap_processed_sphinx_packet(
|
||||
packet: nym_sphinx_types::ProcessedPacket,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
let processing_data = match packet.data {
|
||||
ProcessedPacketData::ForwardHop {
|
||||
next_hop_packet,
|
||||
next_hop_address,
|
||||
delay,
|
||||
} => process_forward_hop(
|
||||
NymPacket::Sphinx(next_hop_packet),
|
||||
next_hop_address,
|
||||
delay,
|
||||
packet_type,
|
||||
),
|
||||
// right now there's no use for the surb_id included in the header - probably it should get removed from the
|
||||
// sphinx all together?
|
||||
ProcessedPacketData::FinalHop {
|
||||
destination,
|
||||
identifier: _,
|
||||
payload,
|
||||
} => process_final_hop(
|
||||
destination,
|
||||
payload.recover_plaintext()?,
|
||||
packet_size,
|
||||
packet_type,
|
||||
),
|
||||
}?;
|
||||
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Sphinx(packet.version),
|
||||
processing_data,
|
||||
})
|
||||
}
|
||||
|
||||
fn wrap_processed_outfox_packet(
|
||||
packet: OutfoxProcessedPacket,
|
||||
packet_size: PacketSize,
|
||||
packet_type: PacketType,
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
let next_address = *packet.next_address();
|
||||
let packet = packet.into_packet();
|
||||
if packet.is_final_hop() {
|
||||
let processing_data = process_final_hop(
|
||||
DestinationAddressBytes::from_bytes(next_address),
|
||||
packet.recover_plaintext()?.to_vec(),
|
||||
packet_size,
|
||||
packet_type,
|
||||
)?;
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Outfox,
|
||||
processing_data,
|
||||
})
|
||||
} else {
|
||||
let packet = MixPacket::new(
|
||||
NymNodeRoutingAddress::try_from_bytes(&next_address)?,
|
||||
NymPacket::Outfox(packet),
|
||||
PacketType::Outfox,
|
||||
);
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Outfox,
|
||||
processing_data: MixProcessingResultData::ForwardHop {
|
||||
packet,
|
||||
delay: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn perform_final_processing(
|
||||
packet: NymProcessedPacket,
|
||||
packet_size: PacketSize,
|
||||
@@ -135,64 +290,10 @@ fn perform_final_processing(
|
||||
) -> Result<MixProcessingResult, PacketProcessingError> {
|
||||
match packet {
|
||||
NymProcessedPacket::Sphinx(packet) => {
|
||||
let processing_data = match packet.data {
|
||||
ProcessedPacketData::ForwardHop {
|
||||
next_hop_packet,
|
||||
next_hop_address,
|
||||
delay,
|
||||
} => process_forward_hop(
|
||||
NymPacket::Sphinx(next_hop_packet),
|
||||
next_hop_address,
|
||||
delay,
|
||||
packet_type,
|
||||
),
|
||||
// right now there's no use for the surb_id included in the header - probably it should get removed from the
|
||||
// sphinx all together?
|
||||
ProcessedPacketData::FinalHop {
|
||||
destination,
|
||||
identifier: _,
|
||||
payload,
|
||||
} => process_final_hop(
|
||||
destination,
|
||||
payload.recover_plaintext()?,
|
||||
packet_size,
|
||||
packet_type,
|
||||
),
|
||||
}?;
|
||||
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Sphinx(packet.version),
|
||||
processing_data,
|
||||
})
|
||||
wrap_processed_sphinx_packet(packet, packet_size, packet_type)
|
||||
}
|
||||
NymProcessedPacket::Outfox(packet) => {
|
||||
let next_address = *packet.next_address();
|
||||
let packet = packet.into_packet();
|
||||
if packet.is_final_hop() {
|
||||
let processing_data = process_final_hop(
|
||||
DestinationAddressBytes::from_bytes(next_address),
|
||||
packet.recover_plaintext()?.to_vec(),
|
||||
packet_size,
|
||||
packet_type,
|
||||
)?;
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Outfox,
|
||||
processing_data,
|
||||
})
|
||||
} else {
|
||||
let packet = MixPacket::new(
|
||||
NymNodeRoutingAddress::try_from_bytes(&next_address)?,
|
||||
NymPacket::Outfox(packet),
|
||||
PacketType::Outfox,
|
||||
);
|
||||
Ok(MixProcessingResult {
|
||||
packet_version: MixPacketVersion::Outfox,
|
||||
processing_data: MixProcessingResultData::ForwardHop {
|
||||
packet,
|
||||
delay: None,
|
||||
},
|
||||
})
|
||||
}
|
||||
wrap_processed_outfox_packet(packet, packet_size, packet_type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,9 +272,6 @@ impl PacketSize {
|
||||
let overhead = match packet_type {
|
||||
#[cfg(feature = "sphinx")]
|
||||
PacketType::Mix => SPHINX_PACKET_OVERHEAD,
|
||||
#[allow(deprecated)]
|
||||
#[cfg(feature = "sphinx")]
|
||||
PacketType::Vpn => SPHINX_PACKET_OVERHEAD,
|
||||
#[cfg(feature = "outfox")]
|
||||
PacketType::Outfox => OUTFOX_PACKET_OVERHEAD,
|
||||
_ => 0,
|
||||
|
||||
@@ -27,11 +27,6 @@ pub enum PacketType {
|
||||
#[serde(alias = "sphinx")]
|
||||
Mix = 0,
|
||||
|
||||
/// Represents a packet that should be sent through the network as fast as possible.
|
||||
#[deprecated]
|
||||
#[serde(rename = "unsupported-mix-vpn")]
|
||||
Vpn = 1,
|
||||
|
||||
/// Abusing this to add Outfox support
|
||||
#[serde(rename = "outfox")]
|
||||
Outfox = 2,
|
||||
@@ -41,8 +36,6 @@ impl fmt::Display for PacketType {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
PacketType::Mix => write!(f, "Mix"),
|
||||
#[allow(deprecated)]
|
||||
PacketType::Vpn => write!(f, "Vpn"),
|
||||
PacketType::Outfox => write!(f, "Outfox"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,14 +270,6 @@ pub trait FragmentPreparer {
|
||||
&destination,
|
||||
&delays,
|
||||
)?,
|
||||
#[allow(deprecated)]
|
||||
PacketType::Vpn => NymPacket::sphinx_build(
|
||||
packet_size.payload_size(),
|
||||
packet_payload,
|
||||
&route,
|
||||
&destination,
|
||||
&delays,
|
||||
)?,
|
||||
};
|
||||
|
||||
// from the previously constructed route extract the first hop
|
||||
|
||||
@@ -5,7 +5,7 @@ use std::{array::TryFromSliceError, fmt};
|
||||
use thiserror::Error;
|
||||
|
||||
#[cfg(feature = "outfox")]
|
||||
use nym_outfox::packet::{OutfoxPacket, OutfoxProcessedPacket};
|
||||
pub use nym_outfox::packet::{OutfoxPacket, OutfoxProcessedPacket};
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
pub use sphinx_packet::{SphinxPacket, SphinxPacketBuilder};
|
||||
@@ -21,7 +21,7 @@ pub use nym_outfox::{
|
||||
pub use sphinx_packet::{
|
||||
constants::{
|
||||
self, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, MAX_PATH_LENGTH, NODE_ADDRESS_LENGTH,
|
||||
PAYLOAD_KEY_SIZE,
|
||||
PAYLOAD_KEY_SIZE, REPLAY_TAG_SIZE,
|
||||
},
|
||||
crypto::{self, PrivateKey, PublicKey},
|
||||
header::{self, delays, delays::Delay, ProcessedHeader, SphinxHeader, HEADER_SIZE},
|
||||
@@ -176,10 +176,15 @@ impl NymPacket {
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
pub fn as_sphinx_packet(self) -> Option<SphinxPacket> {
|
||||
pub fn to_sphinx_packet(self) -> Option<SphinxPacket> {
|
||||
match self {
|
||||
NymPacket::Sphinx(packet) => Some(packet),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "sphinx")]
|
||||
pub fn is_sphinx(&self) -> bool {
|
||||
matches!(self, NymPacket::Sphinx(_))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +209,7 @@ impl ShutdownManager {
|
||||
legacy_task_manager: None,
|
||||
shutdown_signals: Default::default(),
|
||||
tracker: Default::default(),
|
||||
max_shutdown_duration: Default::default(),
|
||||
max_shutdown_duration: Duration::from_secs(10),
|
||||
};
|
||||
|
||||
// we need to add an explicit watcher for the cancellation token being cancelled
|
||||
|
||||
@@ -5,9 +5,7 @@ import { MyTab } from 'components/generic-tabs.tsx';
|
||||
|
||||
# Pre-built Binaries
|
||||
|
||||
This page is for operators who prefer to download ready made binaries. The [Github releases page](https://github.com/nymtech/nym/releases) has pre-built binaries which work on **Ubuntu 22.04 and Debian 12 upwards**, but at this stage cannot be guaranteed to work everywhere.
|
||||
|
||||
They should work on any distro with `libc` >= v2.33 and above.
|
||||
This page is for operators who prefer to download ready made binaries. The [Github releases page](https://github.com/nymtech/nym/releases) has pre-built binaries which should work on Ubuntu 22.04 and other Debian-based systems, but at this stage cannot be guaranteed to work everywhere.
|
||||
|
||||
If the pre-built binaries don't work or are unavailable for your system, you will need to build the platform yourself.
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ anyhow.workspace = true
|
||||
arc-swap = { workspace = true }
|
||||
bip39 = { workspace = true, features = ["zeroize"] }
|
||||
bs58.workspace = true
|
||||
bloomfilter = { workspace = true }
|
||||
celes = { workspace = true } # country codes
|
||||
colored = { workspace = true }
|
||||
csv = { workspace = true }
|
||||
@@ -95,24 +96,25 @@ nym-ip-packet-router = { path = "../service-providers/ip-packet-router" }
|
||||
|
||||
# throughput tester to recreate lioness
|
||||
# we don't care about particular versions - just pull whatever is used by sphinx
|
||||
[dependencies.lioness]
|
||||
version = "*"
|
||||
|
||||
[dependencies.chacha]
|
||||
version = "*"
|
||||
|
||||
[dependencies.arrayref]
|
||||
version = "*"
|
||||
|
||||
[dependencies.blake2]
|
||||
version = "*"
|
||||
lioness = "*"
|
||||
chacha = "*"
|
||||
arrayref = "*"
|
||||
blake2 = "=0.8.1"
|
||||
sha2 = { workspace = true }
|
||||
hkdf = { workspace = true }
|
||||
|
||||
[[bench]]
|
||||
name = "benchmarks"
|
||||
harness = false
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
# temporary bonding information v1 (to grab and parse nym-mixnode and nym-gateway package versions)
|
||||
cargo_metadata = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { workspace = true, features = ["async_tokio"] }
|
||||
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
// unwraps in tests/benches are fine...
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use bloomfilter::Bloom;
|
||||
use criterion::{black_box, criterion_group, criterion_main, BatchSize, Criterion};
|
||||
use nym_sphinx_types::REPLAY_TAG_SIZE;
|
||||
use rand::{thread_rng, Rng};
|
||||
use std::sync::Mutex;
|
||||
|
||||
pub fn uncontested_bloomfilter_check(c: &mut Criterion) {
|
||||
let mut bloomfilter = Bloom::new_for_fp_rate(725760000, 1e-5).unwrap();
|
||||
c.bench_function("bf_725760000_1e-5_check", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let mut rng = thread_rng();
|
||||
let mut reply_tag = [0; REPLAY_TAG_SIZE];
|
||||
rng.fill(&mut reply_tag);
|
||||
reply_tag
|
||||
},
|
||||
|replay_tag| {
|
||||
black_box(bloomfilter.check_and_set(&replay_tag));
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
pub fn uncontested_bloomfilter_check_with_exclusive_mutex(c: &mut Criterion) {
|
||||
let bloomfilter = Mutex::new(Bloom::new_for_fp_rate(725760000, 1e-5).unwrap());
|
||||
c.bench_function("bf_725760000_1e-5_uncontested_std_mutex_check", |b| {
|
||||
b.iter_batched(
|
||||
|| {
|
||||
let mut rng = thread_rng();
|
||||
let mut reply_tag = [0; REPLAY_TAG_SIZE];
|
||||
rng.fill(&mut reply_tag);
|
||||
reply_tag
|
||||
},
|
||||
|replay_tag| {
|
||||
black_box(bloomfilter.lock().unwrap().check_and_set(&replay_tag));
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
pub fn uncontested_bloomfilter_check_with_exclusive_tokio_mutex(c: &mut Criterion) {
|
||||
let bloomfilter = tokio::sync::Mutex::new(Bloom::new_for_fp_rate(725760000, 1e-5).unwrap());
|
||||
let runtime = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
c.bench_function("bf_725760000_1e-5_uncontested_tokio_mutex_check", |b| {
|
||||
b.to_async(&runtime).iter_batched(
|
||||
|| {
|
||||
let mut rng = thread_rng();
|
||||
let mut reply_tag = [0; REPLAY_TAG_SIZE];
|
||||
rng.fill(&mut reply_tag);
|
||||
reply_tag
|
||||
},
|
||||
async |replay_tag| {
|
||||
black_box(bloomfilter.lock().await.check_and_set(&replay_tag));
|
||||
},
|
||||
BatchSize::SmallInput,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
nym_node_benches,
|
||||
uncontested_bloomfilter_check,
|
||||
uncontested_bloomfilter_check_with_exclusive_mutex,
|
||||
uncontested_bloomfilter_check_with_exclusive_tokio_mutex
|
||||
);
|
||||
|
||||
// TODO: somehow bench heavily contested cases...
|
||||
|
||||
criterion_main!(nym_node_benches);
|
||||
@@ -46,6 +46,13 @@ impl MixingStats {
|
||||
.store(update_timestamp, Ordering::Release);
|
||||
}
|
||||
|
||||
pub fn ingress_replayed_packet(&self, source: IpAddr) {
|
||||
self.ingress
|
||||
.replayed_packets_received
|
||||
.fetch_add(1, Ordering::Relaxed);
|
||||
self.ingress.senders.entry(source).or_default().replayed += 1;
|
||||
}
|
||||
|
||||
pub fn ingress_malformed_packet(&self, source: IpAddr) {
|
||||
self.ingress
|
||||
.malformed_packets_received
|
||||
@@ -197,6 +204,7 @@ pub struct IngressRecipientStats {
|
||||
pub forward_packets: IngressPacketsStats,
|
||||
pub final_hop_packets: IngressPacketsStats,
|
||||
pub malformed: usize,
|
||||
pub replayed: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Copy, Clone, Hash, PartialEq, Eq)]
|
||||
@@ -232,6 +240,9 @@ pub struct IngressMixingStats {
|
||||
// packets that failed to get unwrapped
|
||||
malformed_packets_received: AtomicUsize,
|
||||
|
||||
// packets that were already received and processed before
|
||||
replayed_packets_received: AtomicUsize,
|
||||
|
||||
// (forward) packets that had invalid, i.e. too large, delays
|
||||
excessive_delay_packets: AtomicUsize,
|
||||
|
||||
@@ -253,6 +264,10 @@ impl IngressMixingStats {
|
||||
self.final_hop_packets_received.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn replayed_packets_received(&self) -> usize {
|
||||
self.replayed_packets_received.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
pub fn malformed_packets_received(&self) -> usize {
|
||||
self.malformed_packets_received.load(Ordering::Relaxed)
|
||||
}
|
||||
|
||||
@@ -39,6 +39,9 @@ pub enum PrometheusMetric {
|
||||
#[strum(props(help = "The number of ingress final hop sphinx packets received"))]
|
||||
MixnetIngressFinalHopPacketsReceived,
|
||||
|
||||
#[strum(props(help = "The number of ingress replayed sphinx packets received"))]
|
||||
MixnetIngressReplayedPacketsReceived,
|
||||
|
||||
#[strum(props(help = "The number of ingress malformed sphinx packets received"))]
|
||||
MixnetIngressMalformedPacketsReceived,
|
||||
|
||||
@@ -208,6 +211,9 @@ impl PrometheusMetric {
|
||||
PrometheusMetric::MixnetIngressFinalHopPacketsReceived => {
|
||||
Metric::new_int_gauge(&name, help)
|
||||
}
|
||||
PrometheusMetric::MixnetIngressReplayedPacketsReceived => {
|
||||
Metric::new_int_gauge(&name, help)
|
||||
}
|
||||
PrometheusMetric::MixnetIngressMalformedPacketsReceived => {
|
||||
Metric::new_int_gauge(&name, help)
|
||||
}
|
||||
@@ -382,7 +388,7 @@ mod tests {
|
||||
// a sanity check for anyone adding new metrics. if this test fails,
|
||||
// make sure any methods on `PrometheusMetric` enum don't need updating
|
||||
// or require custom Display impl
|
||||
assert_eq!(38, PrometheusMetric::COUNT)
|
||||
assert_eq!(39, PrometheusMetric::COUNT)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -40,6 +40,9 @@ pub mod packets {
|
||||
// packets that failed to get unwrapped
|
||||
pub malformed_packets_received: usize,
|
||||
|
||||
// packets that were already received and processed before
|
||||
pub replayed_packets_received: usize,
|
||||
|
||||
// (forward) packets that had invalid, i.e. too large, delays
|
||||
pub excessive_delay_packets: usize,
|
||||
|
||||
|
||||
@@ -167,7 +167,7 @@ impl Args {
|
||||
)
|
||||
.with_host(self.host.build_config_section())
|
||||
.with_http(self.http.build_config_section())
|
||||
.with_mixnet(self.mixnet.build_config_section())
|
||||
.with_mixnet(self.mixnet.build_config_section(&data_dir))
|
||||
.with_wireguard(self.wireguard.build_config_section(&data_dir))
|
||||
.with_storage_paths(NymNodePaths::new(&data_dir))
|
||||
.with_verloc(self.verloc.build_config_section())
|
||||
|
||||
@@ -220,12 +220,20 @@ pub(crate) struct MixnetArgs {
|
||||
env = NYMNODE_UNSAFE_DISABLE_NOISE
|
||||
)]
|
||||
pub(crate) unsafe_disable_noise: bool,
|
||||
|
||||
/// Specifies whether this node should **NOT** be using replay protection
|
||||
#[clap(
|
||||
hide = true,
|
||||
long,
|
||||
env = NYMNODE_UNSAFE_DISABLE_REPLAY_PROTECTION
|
||||
)]
|
||||
pub(crate) unsafe_disable_replay_protection: bool,
|
||||
}
|
||||
|
||||
impl MixnetArgs {
|
||||
// TODO: could we perhaps make a clap error here and call `safe_exit` instead?
|
||||
pub(crate) fn build_config_section(self) -> config::Mixnet {
|
||||
self.override_config_section(config::Mixnet::default())
|
||||
pub(crate) fn build_config_section<P: AsRef<Path>>(self, data_dir: P) -> config::Mixnet {
|
||||
self.override_config_section(config::Mixnet::new_default(data_dir))
|
||||
}
|
||||
|
||||
pub(crate) fn override_config_section(self, mut section: config::Mixnet) -> config::Mixnet {
|
||||
@@ -244,6 +252,9 @@ impl MixnetArgs {
|
||||
if self.unsafe_disable_noise {
|
||||
section.debug.unsafe_disable_noise = true
|
||||
}
|
||||
if self.unsafe_disable_replay_protection {
|
||||
section.replay_protection.debug.unsafe_disabled = true
|
||||
}
|
||||
section
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::config::persistence::NymNodePaths;
|
||||
use crate::config::persistence::{NymNodePaths, ReplayProtectionPaths};
|
||||
use crate::config::template::CONFIG_TEMPLATE;
|
||||
use crate::error::NymNodeError;
|
||||
use celes::Country;
|
||||
use clap::ValueEnum;
|
||||
use human_repr::HumanCount;
|
||||
use nym_bin_common::logging::LoggingSettings;
|
||||
use nym_config::defaults::{
|
||||
mainnet, var_names, DEFAULT_MIX_LISTENING_PORT, DEFAULT_NYM_NODE_HTTP_PORT,
|
||||
@@ -26,6 +27,7 @@ use std::fmt::{Display, Formatter};
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use sysinfo::System;
|
||||
use tracing::{debug, error};
|
||||
use url::Url;
|
||||
|
||||
@@ -42,6 +44,7 @@ pub mod upgrade_helpers;
|
||||
pub use crate::config::gateway_tasks::GatewayTasksConfig;
|
||||
pub use crate::config::metrics::MetricsConfig;
|
||||
pub use crate::config::service_providers::ServiceProvidersConfig;
|
||||
use crate::node::replay_protection::{bitmap_size, items_in_bloomfilter};
|
||||
|
||||
const DEFAULT_NYMNODES_DIR: &str = "nym-nodes";
|
||||
|
||||
@@ -264,7 +267,9 @@ impl ConfigBuilder {
|
||||
modes: self.modes,
|
||||
host: self.host.unwrap_or_default(),
|
||||
http: self.http.unwrap_or_default(),
|
||||
mixnet: self.mixnet.unwrap_or_default(),
|
||||
mixnet: self
|
||||
.mixnet
|
||||
.unwrap_or_else(|| Mixnet::new_default(&self.data_dir)),
|
||||
verloc: self.verloc.unwrap_or_default(),
|
||||
wireguard: self
|
||||
.wireguard
|
||||
@@ -417,6 +422,12 @@ impl Config {
|
||||
pub fn read_from_toml_file<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> {
|
||||
Self::read_from_path(path)
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), NymNodeError> {
|
||||
self.mixnet.validate()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: this is very much a WIP. we need proper ssl certificate support here
|
||||
@@ -496,7 +507,6 @@ impl Default for Http {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Mixnet {
|
||||
/// Address this node will bind to for listening for mixnet packets
|
||||
@@ -516,13 +526,35 @@ pub struct Mixnet {
|
||||
/// Addresses to nyxd which the node uses to interact with the nyx chain.
|
||||
pub nyxd_urls: Vec<Url>,
|
||||
|
||||
/// Settings for controlling replay detection
|
||||
pub replay_protection: ReplayProtection,
|
||||
|
||||
#[serde(default)]
|
||||
pub debug: MixnetDebug,
|
||||
}
|
||||
|
||||
impl Mixnet {
|
||||
pub fn validate(&self) -> Result<(), NymNodeError> {
|
||||
if self.nym_api_urls.is_empty() {
|
||||
return Err(NymNodeError::config_validation_failure(
|
||||
"no nym api urls provided",
|
||||
));
|
||||
}
|
||||
|
||||
if self.nyxd_urls.is_empty() {
|
||||
return Err(NymNodeError::config_validation_failure(
|
||||
"no nyxd urls provided",
|
||||
));
|
||||
}
|
||||
|
||||
self.replay_protection.validate()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct MixnetDebug {
|
||||
/// Specifies the duration of time this node is willing to delay a forward packet for.
|
||||
#[serde(with = "humantime_serde")]
|
||||
@@ -549,6 +581,148 @@ pub struct MixnetDebug {
|
||||
pub unsafe_disable_noise: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Serialize)]
|
||||
pub struct ReplayProtection {
|
||||
/// Paths for current bloomfilters
|
||||
pub storage_paths: persistence::ReplayProtectionPaths,
|
||||
|
||||
#[serde(default)]
|
||||
pub debug: ReplayProtectionDebug,
|
||||
}
|
||||
|
||||
impl ReplayProtection {
|
||||
pub fn new_default<P: AsRef<Path>>(data_dir: P) -> Self {
|
||||
ReplayProtection {
|
||||
storage_paths: ReplayProtectionPaths::new(data_dir),
|
||||
debug: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ReplayProtection {
|
||||
pub fn validate(&self) -> Result<(), NymNodeError> {
|
||||
self.debug.validate()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Deserialize, PartialEq, Serialize)]
|
||||
#[serde(default)]
|
||||
pub struct ReplayProtectionDebug {
|
||||
/// Specifies whether this node should **NOT** use replay protection
|
||||
pub unsafe_disabled: bool,
|
||||
|
||||
/// How long the processing task is willing to skip mutex acquisition before it will block the thread
|
||||
/// until it actually obtains it
|
||||
pub maximum_replay_detection_deferral: Duration,
|
||||
|
||||
/// How many packets the processing task is willing to queue before it will block the thread
|
||||
/// until it obtains the mutex
|
||||
pub maximum_replay_detection_pending_packets: usize,
|
||||
|
||||
/// Probability of false positives, fraction between 0 and 1 or a number indicating 1-in-p
|
||||
pub false_positive_rate: f64,
|
||||
|
||||
/// Defines initial expected number of packets this node will process a second,
|
||||
/// so that an initial bloomfilter could be established.
|
||||
/// As the node is running and BF are cleared, the value will be adjusted dynamically
|
||||
pub initial_expected_packets_per_second: usize,
|
||||
|
||||
/// Defines minimum expected number of packets this node will process a second
|
||||
/// when used for calculating the BF size after reset.
|
||||
/// This is to avoid degenerate cases where node receives 0 packets (because say it's misconfigured)
|
||||
/// and it constructs an empty bloomfilter.
|
||||
pub bloomfilter_minimum_packets_per_second_size: usize,
|
||||
|
||||
/// Specifies the amount the bloomfilter size is going to get multiplied by after each reset.
|
||||
/// It's performed in case the traffic rates increase before the next bloomfilter update.
|
||||
pub bloomfilter_size_multiplier: f64,
|
||||
|
||||
// NOTE: this field is temporary until replay detection bloomfilter rotation is tied
|
||||
// to key rotation
|
||||
/// Specifies how often the bloomfilter is cleared
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub bloomfilter_reset_rate: Duration,
|
||||
|
||||
/// Specifies how often the bloomfilter is flushed to disk for recovery in case of a crash
|
||||
#[serde(with = "humantime_serde")]
|
||||
pub bloomfilter_disk_flushing_rate: Duration,
|
||||
}
|
||||
|
||||
impl ReplayProtectionDebug {
|
||||
pub const DEFAULT_MAXIMUM_REPLAY_DETECTION_DEFERRAL: Duration = Duration::from_millis(50);
|
||||
|
||||
pub const DEFAULT_MAXIMUM_REPLAY_DETECTION_PENDING_PACKETS: usize = 100;
|
||||
|
||||
// 12% (completely arbitrary)
|
||||
pub const DEFAULT_BLOOMFILTER_SIZE_MULTIPLIER: f64 = 1.12;
|
||||
|
||||
// 10^-5
|
||||
pub const DEFAULT_REPLAY_DETECTION_FALSE_POSITIVE_RATE: f64 = 1e-5;
|
||||
|
||||
// 25h (key rotation will be happening every 24h + 1h of overlap)
|
||||
pub const DEFAULT_REPLAY_DETECTION_BF_RESET_RATE: Duration = Duration::from_secs(25 * 60 * 60);
|
||||
|
||||
// we must have some reasonable balance between losing values and trashing the disk.
|
||||
// since on average HDD it would take ~30s to save a 2GB bloomfilter
|
||||
pub const DEFAULT_BF_DISK_FLUSHING_RATE: Duration = Duration::from_secs(10 * 60);
|
||||
|
||||
// this value will have to be adjusted in the future
|
||||
pub const DEFAULT_INITIAL_EXPECTED_PACKETS_PER_SECOND: usize = 2000;
|
||||
|
||||
pub const DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE: usize = 200;
|
||||
|
||||
pub fn validate(&self) -> Result<(), NymNodeError> {
|
||||
if self.false_positive_rate >= 1.0 || self.false_positive_rate <= 0.0 {
|
||||
return Err(NymNodeError::config_validation_failure(
|
||||
"false positive rate for replay detection can't be larger than (or equal to) 1 or smaller than (or equal to) 0",
|
||||
));
|
||||
}
|
||||
|
||||
let items_in_filter = items_in_bloomfilter(
|
||||
self.bloomfilter_reset_rate,
|
||||
self.initial_expected_packets_per_second,
|
||||
);
|
||||
let bitmap_size = bitmap_size(self.false_positive_rate, items_in_filter);
|
||||
let bloomfilter_size = bitmap_size / 8;
|
||||
|
||||
let mut sys_info = System::new();
|
||||
sys_info.refresh_memory();
|
||||
|
||||
// we'll need 2x size of the bloomfilter
|
||||
// as during key transition we'll have to simultaneously use two filters
|
||||
// plus we also need to make a memcopy during disk flush
|
||||
let required_memory = 2 * bloomfilter_size;
|
||||
|
||||
let memory = sys_info.available_memory();
|
||||
if (memory as usize) < required_memory {
|
||||
return Err(NymNodeError::config_validation_failure(
|
||||
format!("system does not have sufficient memory to allocate required replay protection bloomfilters. {} is available whilst at least {} is needed",memory.human_count_bytes(), required_memory.human_count_bytes())));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ReplayProtectionDebug {
|
||||
fn default() -> Self {
|
||||
ReplayProtectionDebug {
|
||||
unsafe_disabled: false,
|
||||
maximum_replay_detection_deferral: Self::DEFAULT_MAXIMUM_REPLAY_DETECTION_DEFERRAL,
|
||||
maximum_replay_detection_pending_packets:
|
||||
Self::DEFAULT_MAXIMUM_REPLAY_DETECTION_PENDING_PACKETS,
|
||||
false_positive_rate: Self::DEFAULT_REPLAY_DETECTION_FALSE_POSITIVE_RATE,
|
||||
initial_expected_packets_per_second: Self::DEFAULT_INITIAL_EXPECTED_PACKETS_PER_SECOND,
|
||||
bloomfilter_minimum_packets_per_second_size:
|
||||
Self::DEFAULT_BLOOMFILTER_MINIMUM_PACKETS_PER_SECOND_SIZE,
|
||||
bloomfilter_size_multiplier: Self::DEFAULT_BLOOMFILTER_SIZE_MULTIPLIER,
|
||||
bloomfilter_reset_rate: Self::DEFAULT_REPLAY_DETECTION_BF_RESET_RATE,
|
||||
bloomfilter_disk_flushing_rate: Self::DEFAULT_BF_DISK_FLUSHING_RATE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MixnetDebug {
|
||||
// given that genuine clients are using mean delay of 50ms,
|
||||
// the probability of them delaying for over 10s is 10^-87
|
||||
@@ -574,8 +748,8 @@ impl Default for MixnetDebug {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Mixnet {
|
||||
fn default() -> Self {
|
||||
impl Mixnet {
|
||||
pub fn new_default<P: AsRef<Path>>(data_dir: P) -> Self {
|
||||
// SAFETY:
|
||||
// our hardcoded values should always be valid
|
||||
#[allow(clippy::expect_used)]
|
||||
@@ -598,6 +772,7 @@ impl Default for Mixnet {
|
||||
announce_port: None,
|
||||
nym_api_urls,
|
||||
nyxd_urls,
|
||||
replay_protection: ReplayProtection::new_default(data_dir),
|
||||
debug: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ mod old_config_v4;
|
||||
mod old_config_v5;
|
||||
mod old_config_v6;
|
||||
mod old_config_v7;
|
||||
mod old_config_v8;
|
||||
|
||||
pub use old_config_v1::try_upgrade_config_v1;
|
||||
pub use old_config_v2::try_upgrade_config_v2;
|
||||
@@ -16,3 +17,4 @@ pub use old_config_v4::try_upgrade_config_v4;
|
||||
pub use old_config_v5::try_upgrade_config_v5;
|
||||
pub use old_config_v6::try_upgrade_config_v6;
|
||||
pub use old_config_v7::try_upgrade_config_v7;
|
||||
pub use old_config_v8::try_upgrade_config_v8;
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::config::authenticator::{Authenticator, AuthenticatorDebug};
|
||||
use crate::config::gateway_tasks::ZkNymTicketHandlerDebug;
|
||||
use crate::config::service_providers::{
|
||||
IpPacketRouter, IpPacketRouterDebug, NetworkRequester, NetworkRequesterDebug,
|
||||
use crate::config::old_configs::old_config_v8::{
|
||||
AuthenticatorDebugV8, AuthenticatorPathsV8, AuthenticatorV8, ConfigV8,
|
||||
GatewayTasksConfigDebugV8, GatewayTasksConfigV8, GatewayTasksPathsV8, HostV8, HttpV8,
|
||||
IpPacketRouterDebugV8, IpPacketRouterPathsV8, IpPacketRouterV8, KeysPathsV8, LoggingSettingsV8,
|
||||
MixnetDebugV8, MixnetV8, NetworkRequesterDebugV8, NetworkRequesterPathsV8, NetworkRequesterV8,
|
||||
NodeModesV8, NymNodePathsV8, ServiceProvidersConfigDebugV8, ServiceProvidersConfigV8,
|
||||
ServiceProvidersPathsV8, VerlocDebugV8, VerlocV8, WireguardPathsV8, WireguardV8,
|
||||
ZkNymTicketHandlerDebugV8,
|
||||
};
|
||||
use crate::config::*;
|
||||
use crate::error::{EntryGatewayError, NymNodeError};
|
||||
use crate::error::NymNodeError;
|
||||
use celes::Country;
|
||||
use clap::ValueEnum;
|
||||
use gateway_tasks::DEFAULT_WS_PORT;
|
||||
use nym_client_core_config_types::{
|
||||
disk_persistence::{ClientKeysPaths, CommonClientPaths},
|
||||
DebugConfig as ClientDebugConfig,
|
||||
};
|
||||
use nym_client_core_config_types::DebugConfig as ClientDebugConfig;
|
||||
use nym_config::defaults::{mainnet, var_names};
|
||||
use nym_config::helpers::inaddr_any;
|
||||
use nym_config::{
|
||||
@@ -24,17 +22,13 @@ use nym_config::{
|
||||
serde_helpers::{de_maybe_port, de_maybe_stringified},
|
||||
};
|
||||
use nym_config::{parse_urls, read_config_from_toml_file};
|
||||
use persistence::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::create_dir_all;
|
||||
use std::env;
|
||||
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Duration;
|
||||
use std::{env, fs, io};
|
||||
use tracing::info;
|
||||
use tracing::{debug, instrument};
|
||||
use url::Url;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -132,14 +126,6 @@ impl From<&[NodeModeV7]> for NodeModesV7 {
|
||||
}
|
||||
|
||||
impl NodeModesV7 {
|
||||
pub fn any_enabled(&self) -> bool {
|
||||
self.mixnode || self.entry || self.exit
|
||||
}
|
||||
|
||||
pub fn standalone_exit(&self) -> bool {
|
||||
!self.mixnode && !self.entry && self.exit
|
||||
}
|
||||
|
||||
pub fn with_mode(&mut self, mode: NodeModeV7) -> &mut Self {
|
||||
match mode {
|
||||
NodeModeV7::Mixnode => self.with_mixnode(),
|
||||
@@ -149,10 +135,6 @@ impl NodeModesV7 {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn expects_final_hop_traffic(&self) -> bool {
|
||||
self.entry || self.exit
|
||||
}
|
||||
|
||||
pub fn with_mixnode(&mut self) -> &mut Self {
|
||||
self.mixnode = true;
|
||||
self
|
||||
@@ -320,45 +302,6 @@ pub struct KeysPathsV7 {
|
||||
pub public_x25519_noise_key_file: PathBuf,
|
||||
}
|
||||
|
||||
impl KeysPathsV7 {
|
||||
pub fn new<P: AsRef<Path>>(data_dir: P) -> Self {
|
||||
let data_dir = data_dir.as_ref();
|
||||
|
||||
KeysPathsV7 {
|
||||
private_ed25519_identity_key_file: data_dir
|
||||
.join(DEFAULT_ED25519_PRIVATE_IDENTITY_KEY_FILENAME),
|
||||
public_ed25519_identity_key_file: data_dir
|
||||
.join(DEFAULT_ED25519_PUBLIC_IDENTITY_KEY_FILENAME),
|
||||
private_x25519_sphinx_key_file: data_dir
|
||||
.join(DEFAULT_X25519_PRIVATE_SPHINX_KEY_FILENAME),
|
||||
public_x25519_sphinx_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_SPHINX_KEY_FILENAME),
|
||||
private_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PRIVATE_NOISE_KEY_FILENAME),
|
||||
public_x25519_noise_key_file: data_dir.join(DEFAULT_X25519_PUBLIC_NOISE_KEY_FILENAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ed25519_identity_storage_paths(&self) -> nym_pemstore::KeyPairPath {
|
||||
nym_pemstore::KeyPairPath::new(
|
||||
&self.private_ed25519_identity_key_file,
|
||||
&self.public_ed25519_identity_key_file,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn x25519_sphinx_storage_paths(&self) -> nym_pemstore::KeyPairPath {
|
||||
nym_pemstore::KeyPairPath::new(
|
||||
&self.private_x25519_sphinx_key_file,
|
||||
&self.public_x25519_sphinx_key_file,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn x25519_noise_storage_paths(&self) -> nym_pemstore::KeyPairPath {
|
||||
nym_pemstore::KeyPairPath::new(
|
||||
&self.private_x25519_noise_key_file,
|
||||
&self.public_x25519_noise_key_file,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NymNodePathsV7 {
|
||||
@@ -701,56 +644,6 @@ pub struct NetworkRequesterPathsV7 {
|
||||
// it's possible we might have to add credential storage here for return tickets
|
||||
}
|
||||
|
||||
impl NetworkRequesterPathsV7 {
|
||||
pub fn new<P: AsRef<Path>>(data_dir: P) -> Self {
|
||||
let data_dir = data_dir.as_ref();
|
||||
NetworkRequesterPathsV7 {
|
||||
private_ed25519_identity_key_file: data_dir
|
||||
.join(DEFAULT_ED25519_NR_PRIVATE_IDENTITY_KEY_FILENAME),
|
||||
public_ed25519_identity_key_file: data_dir
|
||||
.join(DEFAULT_ED25519_NR_PUBLIC_IDENTITY_KEY_FILENAME),
|
||||
private_x25519_diffie_hellman_key_file: data_dir
|
||||
.join(DEFAULT_X25519_NR_PRIVATE_DH_KEY_FILENAME),
|
||||
public_x25519_diffie_hellman_key_file: data_dir
|
||||
.join(DEFAULT_X25519_NR_PUBLIC_DH_KEY_FILENAME),
|
||||
ack_key_file: data_dir.join(DEFAULT_NR_ACK_KEY_FILENAME),
|
||||
reply_surb_database: data_dir.join(DEFAULT_NR_REPLY_SURB_DB_FILENAME),
|
||||
gateway_registrations: data_dir.join(DEFAULT_NR_GATEWAYS_DB_FILENAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_common_client_paths(&self) -> CommonClientPaths {
|
||||
CommonClientPaths {
|
||||
keys: ClientKeysPaths {
|
||||
private_identity_key_file: self.private_ed25519_identity_key_file.clone(),
|
||||
public_identity_key_file: self.public_ed25519_identity_key_file.clone(),
|
||||
private_encryption_key_file: self.private_x25519_diffie_hellman_key_file.clone(),
|
||||
public_encryption_key_file: self.public_x25519_diffie_hellman_key_file.clone(),
|
||||
ack_key_file: self.ack_key_file.clone(),
|
||||
},
|
||||
gateway_registrations: self.gateway_registrations.clone(),
|
||||
|
||||
// not needed for embedded providers
|
||||
credentials_database: Default::default(),
|
||||
reply_surb_database: self.reply_surb_database.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ed25519_identity_storage_paths(&self) -> nym_pemstore::KeyPairPath {
|
||||
nym_pemstore::KeyPairPath::new(
|
||||
&self.private_ed25519_identity_key_file,
|
||||
&self.public_ed25519_identity_key_file,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn x25519_diffie_hellman_storage_paths(&self) -> nym_pemstore::KeyPairPath {
|
||||
nym_pemstore::KeyPairPath::new(
|
||||
&self.private_x25519_diffie_hellman_key_file,
|
||||
&self.public_x25519_diffie_hellman_key_file,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct IpPacketRouterPathsV7 {
|
||||
@@ -781,56 +674,6 @@ pub struct IpPacketRouterPathsV7 {
|
||||
// it's possible we might have to add credential storage here for return tickets
|
||||
}
|
||||
|
||||
impl IpPacketRouterPathsV7 {
|
||||
pub fn new<P: AsRef<Path>>(data_dir: P) -> Self {
|
||||
let data_dir = data_dir.as_ref();
|
||||
IpPacketRouterPathsV7 {
|
||||
private_ed25519_identity_key_file: data_dir
|
||||
.join(DEFAULT_ED25519_IPR_PRIVATE_IDENTITY_KEY_FILENAME),
|
||||
public_ed25519_identity_key_file: data_dir
|
||||
.join(DEFAULT_ED25519_IPR_PUBLIC_IDENTITY_KEY_FILENAME),
|
||||
private_x25519_diffie_hellman_key_file: data_dir
|
||||
.join(DEFAULT_X25519_IPR_PRIVATE_DH_KEY_FILENAME),
|
||||
public_x25519_diffie_hellman_key_file: data_dir
|
||||
.join(DEFAULT_X25519_IPR_PUBLIC_DH_KEY_FILENAME),
|
||||
ack_key_file: data_dir.join(DEFAULT_IPR_ACK_KEY_FILENAME),
|
||||
reply_surb_database: data_dir.join(DEFAULT_IPR_REPLY_SURB_DB_FILENAME),
|
||||
gateway_registrations: data_dir.join(DEFAULT_IPR_GATEWAYS_DB_FILENAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_common_client_paths(&self) -> CommonClientPaths {
|
||||
CommonClientPaths {
|
||||
keys: ClientKeysPaths {
|
||||
private_identity_key_file: self.private_ed25519_identity_key_file.clone(),
|
||||
public_identity_key_file: self.public_ed25519_identity_key_file.clone(),
|
||||
private_encryption_key_file: self.private_x25519_diffie_hellman_key_file.clone(),
|
||||
public_encryption_key_file: self.public_x25519_diffie_hellman_key_file.clone(),
|
||||
ack_key_file: self.ack_key_file.clone(),
|
||||
},
|
||||
gateway_registrations: self.gateway_registrations.clone(),
|
||||
|
||||
// not needed for embedded providers
|
||||
credentials_database: Default::default(),
|
||||
reply_surb_database: self.reply_surb_database.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ed25519_identity_storage_paths(&self) -> nym_pemstore::KeyPairPath {
|
||||
nym_pemstore::KeyPairPath::new(
|
||||
&self.private_ed25519_identity_key_file,
|
||||
&self.public_ed25519_identity_key_file,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn x25519_diffie_hellman_storage_paths(&self) -> nym_pemstore::KeyPairPath {
|
||||
nym_pemstore::KeyPairPath::new(
|
||||
&self.private_x25519_diffie_hellman_key_file,
|
||||
&self.public_x25519_diffie_hellman_key_file,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AuthenticatorPathsV7 {
|
||||
@@ -861,56 +704,6 @@ pub struct AuthenticatorPathsV7 {
|
||||
// it's possible we might have to add credential storage here for return tickets
|
||||
}
|
||||
|
||||
impl AuthenticatorPathsV7 {
|
||||
pub fn new<P: AsRef<Path>>(data_dir: P) -> Self {
|
||||
let data_dir = data_dir.as_ref();
|
||||
AuthenticatorPathsV7 {
|
||||
private_ed25519_identity_key_file: data_dir
|
||||
.join(DEFAULT_ED25519_AUTH_PRIVATE_IDENTITY_KEY_FILENAME),
|
||||
public_ed25519_identity_key_file: data_dir
|
||||
.join(DEFAULT_ED25519_AUTH_PUBLIC_IDENTITY_KEY_FILENAME),
|
||||
private_x25519_diffie_hellman_key_file: data_dir
|
||||
.join(DEFAULT_X25519_AUTH_PRIVATE_DH_KEY_FILENAME),
|
||||
public_x25519_diffie_hellman_key_file: data_dir
|
||||
.join(DEFAULT_X25519_AUTH_PUBLIC_DH_KEY_FILENAME),
|
||||
ack_key_file: data_dir.join(DEFAULT_AUTH_ACK_KEY_FILENAME),
|
||||
reply_surb_database: data_dir.join(DEFAULT_AUTH_REPLY_SURB_DB_FILENAME),
|
||||
gateway_registrations: data_dir.join(DEFAULT_AUTH_GATEWAYS_DB_FILENAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_common_client_paths(&self) -> CommonClientPaths {
|
||||
CommonClientPaths {
|
||||
keys: ClientKeysPaths {
|
||||
private_identity_key_file: self.private_ed25519_identity_key_file.clone(),
|
||||
public_identity_key_file: self.public_ed25519_identity_key_file.clone(),
|
||||
private_encryption_key_file: self.private_x25519_diffie_hellman_key_file.clone(),
|
||||
public_encryption_key_file: self.public_x25519_diffie_hellman_key_file.clone(),
|
||||
ack_key_file: self.ack_key_file.clone(),
|
||||
},
|
||||
gateway_registrations: self.gateway_registrations.clone(),
|
||||
|
||||
// not needed for embedded providers
|
||||
credentials_database: Default::default(),
|
||||
reply_surb_database: self.reply_surb_database.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ed25519_identity_storage_paths(&self) -> nym_pemstore::KeyPairPath {
|
||||
nym_pemstore::KeyPairPath::new(
|
||||
&self.private_ed25519_identity_key_file,
|
||||
&self.public_ed25519_identity_key_file,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn x25519_diffie_hellman_storage_paths(&self) -> nym_pemstore::KeyPairPath {
|
||||
nym_pemstore::KeyPairPath::new(
|
||||
&self.private_x25519_diffie_hellman_key_file,
|
||||
&self.public_x25519_diffie_hellman_key_file,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ExitGatewayPathsV7 {
|
||||
@@ -1106,53 +899,6 @@ pub struct GatewayTasksPathsV7 {
|
||||
pub cosmos_mnemonic: PathBuf,
|
||||
}
|
||||
|
||||
impl GatewayTasksPathsV7 {
|
||||
pub fn new<P: AsRef<Path>>(data_dir: P) -> Self {
|
||||
GatewayTasksPathsV7 {
|
||||
clients_storage: data_dir.as_ref().join(DEFAULT_CLIENTS_STORAGE_FILENAME),
|
||||
stats_storage: data_dir.as_ref().join(DEFAULT_STATS_STORAGE_FILENAME),
|
||||
cosmos_mnemonic: data_dir.as_ref().join(DEFAULT_MNEMONIC_FILENAME),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_mnemonic_from_file(&self) -> Result<Zeroizing<bip39::Mnemonic>, EntryGatewayError> {
|
||||
let stringified =
|
||||
Zeroizing::new(fs::read_to_string(&self.cosmos_mnemonic).map_err(|source| {
|
||||
EntryGatewayError::MnemonicLoadFailure {
|
||||
path: self.cosmos_mnemonic.clone(),
|
||||
source,
|
||||
}
|
||||
})?);
|
||||
|
||||
Ok(Zeroizing::new(bip39::Mnemonic::parse::<&str>(
|
||||
stringified.as_ref(),
|
||||
)?))
|
||||
}
|
||||
|
||||
pub fn save_mnemonic_to_file(
|
||||
&self,
|
||||
mnemonic: &bip39::Mnemonic,
|
||||
) -> Result<(), EntryGatewayError> {
|
||||
// wrapper for io errors
|
||||
fn _save_to_file(path: &Path, mnemonic: &bip39::Mnemonic) -> io::Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
create_dir_all(parent)?;
|
||||
}
|
||||
info!("saving entry gateway mnemonic to '{}'", path.display());
|
||||
|
||||
let stringified = Zeroizing::new(mnemonic.to_string());
|
||||
fs::write(path, &stringified)
|
||||
}
|
||||
|
||||
_save_to_file(&self.cosmos_mnemonic, mnemonic).map_err(|source| {
|
||||
EntryGatewayError::MnemonicSaveFailure {
|
||||
path: self.cosmos_mnemonic.clone(),
|
||||
source,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct StaleMessageDebugV7 {
|
||||
/// Specifies how often the clean-up task should check for stale data.
|
||||
@@ -1258,19 +1004,6 @@ pub struct GatewayTasksConfigV7 {
|
||||
pub debug: GatewayTasksConfigDebugV7,
|
||||
}
|
||||
|
||||
impl GatewayTasksConfigV7 {
|
||||
pub fn new_default<P: AsRef<Path>>(data_dir: P) -> Self {
|
||||
GatewayTasksConfigV7 {
|
||||
storage_paths: GatewayTasksPathsV7::new(data_dir),
|
||||
enforce_zk_nyms: false,
|
||||
bind_address: SocketAddr::new(in6addr_any_init(), DEFAULT_WS_PORT),
|
||||
announce_ws_port: None,
|
||||
announce_wss_port: None,
|
||||
debug: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ServiceProvidersPathsV7 {
|
||||
@@ -1288,19 +1021,6 @@ pub struct ServiceProvidersPathsV7 {
|
||||
pub authenticator: AuthenticatorPathsV7,
|
||||
}
|
||||
|
||||
impl ServiceProvidersPathsV7 {
|
||||
pub fn new<P: AsRef<Path>>(data_dir: P) -> Self {
|
||||
let data_dir = data_dir.as_ref();
|
||||
ServiceProvidersPathsV7 {
|
||||
clients_storage: data_dir.join(DEFAULT_CLIENTS_STORAGE_FILENAME),
|
||||
stats_storage: data_dir.join(DEFAULT_STATS_STORAGE_FILENAME),
|
||||
network_requester: NetworkRequesterPathsV7::new(data_dir),
|
||||
ip_packet_router: IpPacketRouterPathsV7::new(data_dir),
|
||||
authenticator: AuthenticatorPathsV7::new(data_dir),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ServiceProvidersConfigDebugV7 {
|
||||
@@ -1342,25 +1062,6 @@ pub struct ServiceProvidersConfigV7 {
|
||||
pub debug: ServiceProvidersConfigDebugV7,
|
||||
}
|
||||
|
||||
impl ServiceProvidersConfigV7 {
|
||||
pub fn new_default<P: AsRef<Path>>(data_dir: P) -> Self {
|
||||
#[allow(clippy::expect_used)]
|
||||
// SAFETY:
|
||||
// we expect our default values to be well-formed
|
||||
ServiceProvidersConfigV7 {
|
||||
storage_paths: ServiceProvidersPathsV7::new(data_dir),
|
||||
open_proxy: false,
|
||||
upstream_exit_policy_url: mainnet::EXIT_POLICY_URL
|
||||
.parse()
|
||||
.expect("invalid default exit policy URL"),
|
||||
network_requester: Default::default(),
|
||||
ip_packet_router: Default::default(),
|
||||
authenticator: Default::default(),
|
||||
debug: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct MetricsConfigV7 {
|
||||
@@ -1498,7 +1199,7 @@ impl ConfigV7 {
|
||||
pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
path: P,
|
||||
prev_config: Option<ConfigV7>,
|
||||
) -> Result<Config, NymNodeError> {
|
||||
) -> Result<ConfigV8, NymNodeError> {
|
||||
debug!("attempting to load v7 config...");
|
||||
|
||||
let old_cfg = if let Some(prev_config) = prev_config {
|
||||
@@ -1507,20 +1208,20 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
ConfigV7::read_from_path(&path)?
|
||||
};
|
||||
|
||||
let cfg = Config {
|
||||
let cfg = ConfigV8 {
|
||||
save_path: old_cfg.save_path,
|
||||
id: old_cfg.id,
|
||||
modes: NodeModes {
|
||||
modes: NodeModesV8 {
|
||||
mixnode: old_cfg.modes.mixnode,
|
||||
entry: old_cfg.modes.entry,
|
||||
exit: old_cfg.modes.exit,
|
||||
},
|
||||
host: Host {
|
||||
host: HostV8 {
|
||||
public_ips: old_cfg.host.public_ips,
|
||||
hostname: old_cfg.host.hostname,
|
||||
location: old_cfg.host.location,
|
||||
},
|
||||
mixnet: Mixnet {
|
||||
mixnet: MixnetV8 {
|
||||
bind_address: {
|
||||
if old_cfg.mixnet.bind_address.ip().is_unspecified() {
|
||||
SocketAddr::new(in6addr_any_init(), old_cfg.mixnet.bind_address.port())
|
||||
@@ -1531,7 +1232,7 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
announce_port: old_cfg.mixnet.announce_port,
|
||||
nym_api_urls: old_cfg.mixnet.nym_api_urls,
|
||||
nyxd_urls: old_cfg.mixnet.nyxd_urls,
|
||||
debug: MixnetDebug {
|
||||
debug: MixnetDebugV8 {
|
||||
maximum_forward_packet_delay: old_cfg.mixnet.debug.maximum_forward_packet_delay,
|
||||
packet_forwarding_initial_backoff: old_cfg
|
||||
.mixnet
|
||||
@@ -1546,8 +1247,8 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
unsafe_disable_noise: old_cfg.mixnet.debug.unsafe_disable_noise,
|
||||
},
|
||||
},
|
||||
storage_paths: NymNodePaths {
|
||||
keys: KeysPaths {
|
||||
storage_paths: NymNodePathsV8 {
|
||||
keys: KeysPathsV8 {
|
||||
private_ed25519_identity_key_file: old_cfg
|
||||
.storage_paths
|
||||
.keys
|
||||
@@ -1575,7 +1276,7 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
},
|
||||
description: old_cfg.storage_paths.description,
|
||||
},
|
||||
http: Http {
|
||||
http: HttpV8 {
|
||||
bind_address: {
|
||||
if old_cfg.http.bind_address.ip().is_unspecified() {
|
||||
SocketAddr::new(in6addr_any_init(), old_cfg.http.bind_address.port())
|
||||
@@ -1590,7 +1291,7 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
expose_crypto_hardware: old_cfg.http.expose_crypto_hardware,
|
||||
..Default::default()
|
||||
},
|
||||
verloc: Verloc {
|
||||
verloc: VerlocV8 {
|
||||
bind_address: {
|
||||
if old_cfg.verloc.bind_address.ip().is_unspecified() {
|
||||
SocketAddr::new(in6addr_any_init(), old_cfg.verloc.bind_address.port())
|
||||
@@ -1599,7 +1300,7 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
}
|
||||
},
|
||||
announce_port: old_cfg.verloc.announce_port,
|
||||
debug: VerlocDebug {
|
||||
debug: VerlocDebugV8 {
|
||||
packets_per_node: old_cfg.verloc.debug.packets_per_node,
|
||||
connection_timeout: old_cfg.verloc.debug.connection_timeout,
|
||||
packet_timeout: old_cfg.verloc.debug.packet_timeout,
|
||||
@@ -1609,7 +1310,7 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
retry_timeout: old_cfg.verloc.debug.retry_timeout,
|
||||
},
|
||||
},
|
||||
wireguard: Wireguard {
|
||||
wireguard: WireguardV8 {
|
||||
enabled: old_cfg.wireguard.enabled,
|
||||
bind_address: {
|
||||
if old_cfg.wireguard.bind_address.ip().is_unspecified() {
|
||||
@@ -1623,7 +1324,7 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
announced_port: old_cfg.wireguard.announced_port,
|
||||
private_network_prefix_v4: old_cfg.wireguard.private_network_prefix_v4,
|
||||
private_network_prefix_v6: old_cfg.wireguard.private_network_prefix_v6,
|
||||
storage_paths: WireguardPaths {
|
||||
storage_paths: WireguardPathsV8 {
|
||||
private_diffie_hellman_key_file: old_cfg
|
||||
.wireguard
|
||||
.storage_paths
|
||||
@@ -1634,8 +1335,8 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
.public_diffie_hellman_key_file,
|
||||
},
|
||||
},
|
||||
gateway_tasks: GatewayTasksConfig {
|
||||
storage_paths: GatewayTasksPaths {
|
||||
gateway_tasks: GatewayTasksConfigV8 {
|
||||
storage_paths: GatewayTasksPathsV8 {
|
||||
clients_storage: old_cfg.gateway_tasks.storage_paths.clients_storage,
|
||||
stats_storage: old_cfg.gateway_tasks.storage_paths.stats_storage,
|
||||
cosmos_mnemonic: old_cfg.gateway_tasks.storage_paths.cosmos_mnemonic,
|
||||
@@ -1653,9 +1354,9 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
},
|
||||
announce_ws_port: old_cfg.gateway_tasks.announce_ws_port,
|
||||
announce_wss_port: old_cfg.gateway_tasks.announce_wss_port,
|
||||
debug: gateway_tasks::Debug {
|
||||
debug: GatewayTasksConfigDebugV8 {
|
||||
message_retrieval_limit: old_cfg.gateway_tasks.debug.message_retrieval_limit,
|
||||
zk_nym_tickets: ZkNymTicketHandlerDebug {
|
||||
zk_nym_tickets: ZkNymTicketHandlerDebugV8 {
|
||||
revocation_bandwidth_penalty: old_cfg
|
||||
.gateway_tasks
|
||||
.debug
|
||||
@@ -1681,11 +1382,11 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
service_providers: ServiceProvidersConfig {
|
||||
storage_paths: ServiceProvidersPaths {
|
||||
service_providers: ServiceProvidersConfigV8 {
|
||||
storage_paths: ServiceProvidersPathsV8 {
|
||||
clients_storage: old_cfg.service_providers.storage_paths.clients_storage,
|
||||
stats_storage: old_cfg.service_providers.storage_paths.stats_storage,
|
||||
network_requester: NetworkRequesterPaths {
|
||||
network_requester: NetworkRequesterPathsV8 {
|
||||
private_ed25519_identity_key_file: old_cfg
|
||||
.service_providers
|
||||
.storage_paths
|
||||
@@ -1722,7 +1423,7 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
.network_requester
|
||||
.gateway_registrations,
|
||||
},
|
||||
ip_packet_router: IpPacketRouterPaths {
|
||||
ip_packet_router: IpPacketRouterPathsV8 {
|
||||
private_ed25519_identity_key_file: old_cfg
|
||||
.service_providers
|
||||
.storage_paths
|
||||
@@ -1759,7 +1460,7 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
.ip_packet_router
|
||||
.gateway_registrations,
|
||||
},
|
||||
authenticator: AuthenticatorPaths {
|
||||
authenticator: AuthenticatorPathsV8 {
|
||||
private_ed25519_identity_key_file: old_cfg
|
||||
.service_providers
|
||||
.storage_paths
|
||||
@@ -1799,8 +1500,8 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
},
|
||||
open_proxy: old_cfg.service_providers.open_proxy,
|
||||
upstream_exit_policy_url: old_cfg.service_providers.upstream_exit_policy_url,
|
||||
network_requester: NetworkRequester {
|
||||
debug: NetworkRequesterDebug {
|
||||
network_requester: NetworkRequesterV8 {
|
||||
debug: NetworkRequesterDebugV8 {
|
||||
enabled: old_cfg.service_providers.network_requester.debug.enabled,
|
||||
disable_poisson_rate: old_cfg
|
||||
.service_providers
|
||||
@@ -1814,8 +1515,8 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
.client_debug,
|
||||
},
|
||||
},
|
||||
ip_packet_router: IpPacketRouter {
|
||||
debug: IpPacketRouterDebug {
|
||||
ip_packet_router: IpPacketRouterV8 {
|
||||
debug: IpPacketRouterDebugV8 {
|
||||
enabled: old_cfg.service_providers.ip_packet_router.debug.enabled,
|
||||
disable_poisson_rate: old_cfg
|
||||
.service_providers
|
||||
@@ -1829,8 +1530,8 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
.client_debug,
|
||||
},
|
||||
},
|
||||
authenticator: Authenticator {
|
||||
debug: AuthenticatorDebug {
|
||||
authenticator: AuthenticatorV8 {
|
||||
debug: AuthenticatorDebugV8 {
|
||||
enabled: old_cfg.service_providers.authenticator.debug.enabled,
|
||||
disable_poisson_rate: old_cfg
|
||||
.service_providers
|
||||
@@ -1840,12 +1541,12 @@ pub async fn try_upgrade_config_v7<P: AsRef<Path>>(
|
||||
client_debug: old_cfg.service_providers.authenticator.debug.client_debug,
|
||||
},
|
||||
},
|
||||
debug: service_providers::Debug {
|
||||
debug: ServiceProvidersConfigDebugV8 {
|
||||
message_retrieval_limit: old_cfg.service_providers.debug.message_retrieval_limit,
|
||||
},
|
||||
},
|
||||
metrics: Default::default(),
|
||||
logging: LoggingSettings {},
|
||||
logging: LoggingSettingsV8 {},
|
||||
debug: Default::default(),
|
||||
};
|
||||
Ok(cfg)
|
||||
|
||||
@@ -55,6 +55,12 @@ pub const DEFAULT_AUTH_GATEWAYS_DB_FILENAME: &str = "auth_gateways_info_store.sq
|
||||
pub const DEFAULT_X25519_WG_DH_KEY_FILENAME: &str = "x25519_wg_dh";
|
||||
pub const DEFAULT_X25519_WG_PUBLIC_DH_KEY_FILENAME: &str = "x25519_wg_dh.pub";
|
||||
|
||||
// Replay Detection
|
||||
pub const DEFAULT_RD_BLOOMFILTER_SUBDIR: &str = "replay-detection";
|
||||
pub const DEFAULT_RD_BLOOMFILTER_FILE_EXT: &str = "bloom";
|
||||
pub const DEFAULT_RD_BLOOMFILTER_FLUSH_FILE_EXT: &str = "flush";
|
||||
pub const CURRENT_RD_BLOOMFILTER_FILENAME: &str = "current";
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NymNodePaths {
|
||||
@@ -490,3 +496,32 @@ impl WireguardPaths {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ReplayProtectionPaths {
|
||||
/// Path to the directory storing currently used bloomfilter(s).
|
||||
pub current_bloomfilters_directory: PathBuf,
|
||||
}
|
||||
|
||||
impl ReplayProtectionPaths {
|
||||
pub fn current_bloomfilter_filepath(&self) -> PathBuf {
|
||||
self.current_bloomfilters_directory
|
||||
.join(CURRENT_RD_BLOOMFILTER_FILENAME)
|
||||
.with_extension(DEFAULT_RD_BLOOMFILTER_FILE_EXT)
|
||||
}
|
||||
|
||||
pub fn current_bloomfilter_being_flushed_filepath(&self) -> PathBuf {
|
||||
self.current_bloomfilters_directory
|
||||
.join(CURRENT_RD_BLOOMFILTER_FILENAME)
|
||||
.with_extension(DEFAULT_RD_BLOOMFILTER_FLUSH_FILE_EXT)
|
||||
}
|
||||
}
|
||||
|
||||
impl ReplayProtectionPaths {
|
||||
pub fn new<P: AsRef<Path>>(data_dir: P) -> Self {
|
||||
ReplayProtectionPaths {
|
||||
current_bloomfilters_directory: data_dir.as_ref().join(DEFAULT_RD_BLOOMFILTER_SUBDIR),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,12 @@ nyxd_urls = [
|
||||
{{#each mixnet.nyxd_urls }}'{{this}}',{{/each}}
|
||||
]
|
||||
|
||||
[mixnet.replay_protection]
|
||||
|
||||
[mixnet.replay_protection.storage_paths]
|
||||
# Path to the directory storing currently used bloomfilter(s).
|
||||
current_bloomfilters_directory = '{{ mixnet.replay_protection.storage_paths.current_bloomfilters_directory }}'
|
||||
|
||||
# Storage paths to persistent nym-node data, such as its long term keys.
|
||||
[storage_paths]
|
||||
|
||||
|
||||
@@ -15,7 +15,8 @@ async fn try_upgrade_config(path: &Path) -> Result<(), NymNodeError> {
|
||||
let cfg = try_upgrade_config_v4(path, cfg).await.ok();
|
||||
let cfg = try_upgrade_config_v5(path, cfg).await.ok();
|
||||
let cfg = try_upgrade_config_v6(path, cfg).await.ok();
|
||||
match try_upgrade_config_v7(path, cfg).await {
|
||||
let cfg = try_upgrade_config_v7(path, cfg).await.ok();
|
||||
match try_upgrade_config_v8(path, cfg).await {
|
||||
Ok(cfg) => cfg.save(),
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to finish upgrade - {e}");
|
||||
@@ -35,5 +36,7 @@ pub async fn try_load_current_config<P: AsRef<Path>>(
|
||||
}
|
||||
|
||||
try_upgrade_config(config_path.as_ref()).await?;
|
||||
Config::read_from_toml_file(config_path)
|
||||
let loaded = Config::read_from_toml_file(config_path)?;
|
||||
loaded.validate()?;
|
||||
Ok(loaded)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ pub mod vars {
|
||||
pub const NYMNODE_NYM_APIS_ARG: &str = "NYMNODE_NYM_APIS";
|
||||
pub const NYMNODE_NYXD_URLS_ARG: &str = "NYMNODE_NYXD";
|
||||
pub const NYMNODE_UNSAFE_DISABLE_NOISE: &str = "UNSAFE_DISABLE_NOISE";
|
||||
pub const NYMNODE_UNSAFE_DISABLE_REPLAY_PROTECTION: &str = "UNSAFE_DISABLE_REPLAY_PROTECTION";
|
||||
|
||||
// wireguard:
|
||||
pub const NYMNODE_WG_ENABLED_ARG: &str = "NYMNODE_WG_ENABLED";
|
||||
|
||||
@@ -82,6 +82,9 @@ pub enum NymNodeError {
|
||||
source: io::Error,
|
||||
},
|
||||
|
||||
#[error("failed to validate loaded config: {error}")]
|
||||
ConfigValidationFailure { error: String },
|
||||
|
||||
#[error("the node description file is malformed: {source}")]
|
||||
MalformedDescriptionFile {
|
||||
#[source]
|
||||
@@ -148,6 +151,12 @@ pub enum NymNodeError {
|
||||
)]
|
||||
InitialTopologyQueryFailure { source: ValidatorClientError },
|
||||
|
||||
#[error("experienced critical failure with the replay detection bloomfilter: {message}")]
|
||||
BloomfilterFailure { message: &'static str },
|
||||
|
||||
#[error("failed to save/load the bloomfilter: {source} using path: {}", path.display())]
|
||||
BloomfilterIoFailure { source: io::Error, path: PathBuf },
|
||||
|
||||
#[error(transparent)]
|
||||
GatewayFailure(#[from] nym_gateway::GatewayError),
|
||||
|
||||
@@ -168,6 +177,18 @@ pub enum NymNodeError {
|
||||
FailedUpgrade,
|
||||
}
|
||||
|
||||
impl NymNodeError {
|
||||
pub fn config_validation_failure<S: Into<String>>(error: S) -> Self {
|
||||
NymNodeError::ConfigValidationFailure {
|
||||
error: error.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bloomfilter_failure(message: &'static str) -> Self {
|
||||
NymNodeError::BloomfilterFailure { message }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum EntryGatewayError {
|
||||
#[error(transparent)]
|
||||
|
||||
@@ -38,6 +38,7 @@ fn build_response(metrics: &NymNodeMetrics) -> PacketsStats {
|
||||
forward_hop_packets_received: metrics.mixnet.ingress.forward_hop_packets_received(),
|
||||
final_hop_packets_received: metrics.mixnet.ingress.final_hop_packets_received(),
|
||||
malformed_packets_received: metrics.mixnet.ingress.malformed_packets_received(),
|
||||
replayed_packets_received: metrics.mixnet.ingress.replayed_packets_received(),
|
||||
excessive_delay_packets: metrics.mixnet.ingress.excessive_delay_packets(),
|
||||
forward_hop_packets_dropped: metrics.mixnet.ingress.forward_hop_packets_dropped(),
|
||||
final_hop_packets_dropped: metrics.mixnet.ingress.final_hop_packets_dropped(),
|
||||
|
||||
@@ -64,6 +64,10 @@ impl OnUpdateMetricsHandler for PrometheusGlobalNodeMetricsRegistryUpdater {
|
||||
MixnetIngressMalformedPacketsReceived,
|
||||
self.metrics.mixnet.ingress.malformed_packets_received() as i64,
|
||||
);
|
||||
self.prometheus_wrapper.set(
|
||||
MixnetIngressReplayedPacketsReceived,
|
||||
self.metrics.mixnet.ingress.replayed_packets_received() as i64,
|
||||
);
|
||||
self.prometheus_wrapper.set(
|
||||
MixnetIngressExcessiveDelayPacketsReceived,
|
||||
self.metrics.mixnet.ingress.excessive_delay_packets() as i64,
|
||||
|
||||
@@ -3,24 +3,69 @@
|
||||
|
||||
use crate::node::mixnet::shared::SharedData;
|
||||
use futures::StreamExt;
|
||||
use nym_metrics::nanos;
|
||||
use nym_sphinx_forwarding::packet::MixPacket;
|
||||
use nym_sphinx_framing::codec::NymCodec;
|
||||
use nym_sphinx_framing::packet::FramedNymPacket;
|
||||
use nym_sphinx_framing::processing::{
|
||||
process_framed_packet, MixProcessingResultData, ProcessedFinalHop,
|
||||
process_framed_packet, MixProcessingResult, MixProcessingResultData, PacketProcessingError,
|
||||
PartiallyUnwrappedPacket, ProcessedFinalHop,
|
||||
};
|
||||
use nym_sphinx_types::Delay;
|
||||
use nym_sphinx_types::{Delay, REPLAY_TAG_SIZE};
|
||||
use std::mem;
|
||||
use std::net::SocketAddr;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::time::Instant;
|
||||
use tokio_util::codec::Framed;
|
||||
use tracing::{debug, error, instrument, trace};
|
||||
use tracing::{debug, error, instrument, trace, warn};
|
||||
|
||||
struct PendingReplayCheckPackets {
|
||||
packets: Vec<PartiallyUnwrappedPacket>,
|
||||
last_acquired_mutex: Instant,
|
||||
}
|
||||
|
||||
impl PendingReplayCheckPackets {
|
||||
fn new() -> PendingReplayCheckPackets {
|
||||
PendingReplayCheckPackets {
|
||||
packets: vec![],
|
||||
last_acquired_mutex: Instant::now(),
|
||||
}
|
||||
}
|
||||
|
||||
fn reset(&mut self, now: Instant) -> Vec<PartiallyUnwrappedPacket> {
|
||||
self.last_acquired_mutex = now;
|
||||
mem::take(&mut self.packets)
|
||||
}
|
||||
|
||||
fn push(&mut self, now: Instant, packet: PartiallyUnwrappedPacket) {
|
||||
if self.packets.is_empty() {
|
||||
self.last_acquired_mutex = now;
|
||||
}
|
||||
self.packets.push(packet);
|
||||
}
|
||||
|
||||
fn replay_tags(&self) -> Vec<&[u8; REPLAY_TAG_SIZE]> {
|
||||
let mut replay_tags = Vec::with_capacity(self.packets.len());
|
||||
for packet in &self.packets {
|
||||
let Some(replay_tag) = packet.replay_tag() else {
|
||||
error!(
|
||||
"corrupted batch of {} packets - replay tag was missing",
|
||||
self.packets.len()
|
||||
);
|
||||
return Vec::new();
|
||||
};
|
||||
replay_tags.push(replay_tag);
|
||||
}
|
||||
replay_tags
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct ConnectionHandler {
|
||||
shared: SharedData,
|
||||
mixnet_connection: Framed<TcpStream, NymCodec>,
|
||||
remote_address: SocketAddr,
|
||||
|
||||
// packets pending for replay detection
|
||||
pending_packets: PendingReplayCheckPackets,
|
||||
}
|
||||
|
||||
impl Drop for ConnectionHandler {
|
||||
@@ -45,6 +90,7 @@ impl ConnectionHandler {
|
||||
shared: SharedData {
|
||||
processing_config: shared.processing_config,
|
||||
sphinx_keys: shared.sphinx_keys.clone(),
|
||||
replay_protection_filter: shared.replay_protection_filter.clone(),
|
||||
mixnet_forwarder: shared.mixnet_forwarder.clone(),
|
||||
final_hop: shared.final_hop.clone(),
|
||||
metrics: shared.metrics.clone(),
|
||||
@@ -52,6 +98,7 @@ impl ConnectionHandler {
|
||||
},
|
||||
remote_address,
|
||||
mixnet_connection: Framed::new(tcp_stream, NymCodec),
|
||||
pending_packets: PendingReplayCheckPackets::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,9 +107,8 @@ impl ConnectionHandler {
|
||||
/// the skew caused by being stuck in the channel queue.
|
||||
/// This method also clamps the maximum allowed delay so that nobody could send a bunch of packets
|
||||
/// with, for example, delays of 1 year thus causing denial of service
|
||||
fn create_delay_target(&self, delay: Option<Delay>) -> Option<Instant> {
|
||||
fn create_delay_target(&self, now: Instant, delay: Option<Delay>) -> Option<Instant> {
|
||||
let delay = delay?.to_duration();
|
||||
let now = Instant::now();
|
||||
|
||||
let delay = if delay > self.shared.processing_config.maximum_packet_delay {
|
||||
self.shared.processing_config.maximum_packet_delay
|
||||
@@ -77,14 +123,14 @@ impl ConnectionHandler {
|
||||
Some(now + delay)
|
||||
}
|
||||
|
||||
fn handle_forward_packet(&self, mix_packet: MixPacket, delay: Option<Delay>) {
|
||||
fn handle_forward_packet(&self, now: Instant, mix_packet: MixPacket, delay: Option<Delay>) {
|
||||
if !self.shared.processing_config.forward_hop_processing_enabled {
|
||||
trace!("this nym-node does not support forward hop packets");
|
||||
self.shared.dropped_forward_packet(self.remote_address.ip());
|
||||
return;
|
||||
}
|
||||
|
||||
let forward_instant = self.create_delay_target(delay);
|
||||
let forward_instant = self.create_delay_target(now, delay);
|
||||
self.shared.forward_mix_packet(mix_packet, forward_instant);
|
||||
}
|
||||
|
||||
@@ -128,33 +174,188 @@ impl ConnectionHandler {
|
||||
self.shared.forward_ack_packet(final_hop_data.forward_ack);
|
||||
}
|
||||
|
||||
#[instrument(skip(self, packet), level = "debug")]
|
||||
async fn handle_received_nym_packet(&self, packet: FramedNymPacket) {
|
||||
// TODO: here be replay attack detection with bloomfilters and all the fancy stuff
|
||||
//
|
||||
fn within_deferral_threshold(&self, now: Instant) -> bool {
|
||||
let time_threshold = now
|
||||
.saturating_duration_since(self.pending_packets.last_acquired_mutex)
|
||||
<= self
|
||||
.shared
|
||||
.processing_config
|
||||
.maximum_replay_detection_deferral;
|
||||
|
||||
nanos!("handle_received_nym_packet", {
|
||||
// 1. attempt to unwrap the packet
|
||||
let count_threshold = self.pending_packets.packets.len()
|
||||
< self
|
||||
.shared
|
||||
.processing_config
|
||||
.maximum_replay_detection_pending_packets;
|
||||
|
||||
// time threshold is ignored if we currently have 0 packets queued up
|
||||
if self.pending_packets.packets.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
trace!(
|
||||
"within deferral time threshold: {time_threshold}, count threshold: {count_threshold}"
|
||||
);
|
||||
|
||||
if !time_threshold {
|
||||
warn!(
|
||||
"{}: time failure - {}",
|
||||
self.remote_address,
|
||||
self.pending_packets.packets.len()
|
||||
)
|
||||
}
|
||||
|
||||
if !count_threshold {
|
||||
warn!("{}, count failure", self.remote_address)
|
||||
}
|
||||
|
||||
time_threshold && count_threshold
|
||||
}
|
||||
|
||||
async fn handle_received_packet_with_replay_detection(
|
||||
&mut self,
|
||||
now: Instant,
|
||||
packet: FramedNymPacket,
|
||||
) {
|
||||
// 1. derive and expand shared secret
|
||||
// also check the header integrity
|
||||
let partially_unwrapped = match PartiallyUnwrappedPacket::new(
|
||||
packet,
|
||||
self.shared.sphinx_keys.private_key().as_ref(),
|
||||
) {
|
||||
Ok(unwrapped) => unwrapped,
|
||||
Err(err) => {
|
||||
trace!("failed to process received mix packet: {err}");
|
||||
self.shared
|
||||
.metrics
|
||||
.mixnet
|
||||
.ingress_malformed_packet(self.remote_address.ip());
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
self.pending_packets.push(now, partially_unwrapped);
|
||||
|
||||
// 2. check for packet replay
|
||||
// 2.1 first try it without locking
|
||||
if self.handle_pending_packets_batch_no_locking(now).await {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.2 if we're within deferral threshold, just leave it queued up for another call
|
||||
if self.within_deferral_threshold(now) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 2.3. otherwise block until we obtain the lock and clear the whole batch
|
||||
self.handle_pending_packets_batch(now).await;
|
||||
}
|
||||
|
||||
async fn handle_unwrapped_packet(
|
||||
&self,
|
||||
now: Instant,
|
||||
unwrapped_packet: Result<MixProcessingResult, PacketProcessingError>,
|
||||
) {
|
||||
// 2. increment our favourite metrics stats
|
||||
self.shared
|
||||
.update_metrics(&unwrapped_packet, self.remote_address.ip());
|
||||
|
||||
// 3. forward the packet to the relevant sink (if enabled)
|
||||
match unwrapped_packet {
|
||||
Err(err) => trace!("failed to process received mix packet: {err}"),
|
||||
Ok(processed_packet) => match processed_packet.processing_data {
|
||||
MixProcessingResultData::ForwardHop { packet, delay } => {
|
||||
self.handle_forward_packet(now, packet, delay);
|
||||
}
|
||||
MixProcessingResultData::FinalHop { final_hop_data } => {
|
||||
self.handle_final_hop(final_hop_data).await;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_post_replay_detection_packets(
|
||||
&self,
|
||||
now: Instant,
|
||||
packets: Vec<PartiallyUnwrappedPacket>,
|
||||
replay_check_results: Vec<bool>,
|
||||
) {
|
||||
for (packet, replayed) in packets.into_iter().zip(replay_check_results) {
|
||||
let unwrapped_packet = if replayed {
|
||||
Err(PacketProcessingError::PacketReplay)
|
||||
} else {
|
||||
packet.finalise_unwrapping()
|
||||
};
|
||||
|
||||
self.handle_unwrapped_packet(now, unwrapped_packet).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_pending_packets_batch_no_locking(&mut self, now: Instant) -> bool {
|
||||
let replay_tags = self.pending_packets.replay_tags();
|
||||
if replay_tags.is_empty() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let replay_check_results = match self
|
||||
.shared
|
||||
.replay_protection_filter
|
||||
.batch_try_check_and_set(&replay_tags)
|
||||
{
|
||||
None => return false,
|
||||
Some(Ok(replay_check_results)) => replay_check_results,
|
||||
Some(Err(_)) => {
|
||||
// our mutex got poisoned - we have to shut down
|
||||
error!("CRITICAL FAILURE: replay bloomfilter mutex poisoning!");
|
||||
self.shared.shutdown.cancel();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
let batch = self.pending_packets.reset(now);
|
||||
self.handle_post_replay_detection_packets(now, batch, replay_check_results)
|
||||
.await;
|
||||
true
|
||||
}
|
||||
|
||||
async fn handle_pending_packets_batch(&mut self, now: Instant) {
|
||||
let batch = self.pending_packets.reset(now);
|
||||
let replay_tags = self.pending_packets.replay_tags();
|
||||
if replay_tags.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(replay_check_results) = self
|
||||
.shared
|
||||
.replay_protection_filter
|
||||
.batch_check_and_set(&replay_tags)
|
||||
else {
|
||||
// our mutex got poisoned - we have to shut down
|
||||
error!("CRITICAL FAILURE: replay bloomfilter mutex poisoning!");
|
||||
self.shared.shutdown.cancel();
|
||||
return;
|
||||
};
|
||||
|
||||
self.handle_post_replay_detection_packets(now, batch, replay_check_results)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[instrument(skip(self, packet), level = "debug")]
|
||||
async fn handle_received_nym_packet(&mut self, packet: FramedNymPacket) {
|
||||
let now = Instant::now();
|
||||
|
||||
// 1. attempt to unwrap the packet
|
||||
// if it's a sphinx packet attempt to do pre-processing and replay detection
|
||||
if packet.is_sphinx() && !self.shared.replay_protection_filter.disabled() {
|
||||
self.handle_received_packet_with_replay_detection(now, packet)
|
||||
.await;
|
||||
} else {
|
||||
// otherwise just skip that whole procedure and go straight to payload unwrapping
|
||||
// (assuming the basic framing is valid)
|
||||
let unwrapped_packet =
|
||||
process_framed_packet(packet, self.shared.sphinx_keys.private_key().as_ref());
|
||||
|
||||
// 2. increment our favourite metrics stats
|
||||
self.shared
|
||||
.update_metrics(&unwrapped_packet, self.remote_address.ip());
|
||||
|
||||
// 3. forward the packet to the relevant sink (if enabled)
|
||||
match unwrapped_packet {
|
||||
Err(err) => trace!("failed to process received mix packet: {err}"),
|
||||
Ok(processed_packet) => match processed_packet.processing_data {
|
||||
MixProcessingResultData::ForwardHop { packet, delay } => {
|
||||
self.handle_forward_packet(packet, delay);
|
||||
}
|
||||
MixProcessingResultData::FinalHop { final_hop_data } => {
|
||||
self.handle_final_hop(final_hop_data).await;
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
self.handle_unwrapped_packet(now, unwrapped_packet).await;
|
||||
};
|
||||
}
|
||||
|
||||
#[instrument(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::shared_network::RoutingFilter;
|
||||
use crate::node::routing_filter::RoutingFilter;
|
||||
use futures::StreamExt;
|
||||
use nym_mixnet_client::forwarder::{
|
||||
mix_forwarding_channels, MixForwardingReceiver, MixForwardingSender, PacketToForward,
|
||||
@@ -60,14 +60,6 @@ impl<C, F> PacketForwarder<C, F> {
|
||||
{
|
||||
let next_hop = packet.next_hop();
|
||||
|
||||
if !self.routing_filter.should_route(next_hop.as_ref().ip()) {
|
||||
debug!("dropping packet as the egress address does not belong to any known node");
|
||||
self.metrics
|
||||
.mixnet
|
||||
.egress_dropped_forward_packet(next_hop.into());
|
||||
return;
|
||||
}
|
||||
|
||||
let packet_type = packet.packet_type();
|
||||
let packet = packet.into_packet();
|
||||
|
||||
@@ -110,6 +102,16 @@ impl<C, F> PacketForwarder<C, F> {
|
||||
C: SendWithoutResponse,
|
||||
F: RoutingFilter,
|
||||
{
|
||||
let next_hop = new_packet.packet.next_hop();
|
||||
|
||||
if !self.routing_filter.should_route(next_hop.as_ref().ip()) {
|
||||
debug!("dropping packet as the egress address does not belong to any known node");
|
||||
self.metrics
|
||||
.mixnet
|
||||
.egress_dropped_forward_packet(next_hop.into());
|
||||
return;
|
||||
}
|
||||
|
||||
// in case of a zero delay packet, don't bother putting it in the delay queue,
|
||||
// just forward it immediately
|
||||
if let Some(instant) = new_packet.forward_delay_target {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use crate::config::Config;
|
||||
use crate::node::mixnet::handler::ConnectionHandler;
|
||||
use crate::node::mixnet::SharedFinalHopData;
|
||||
use crate::node::replay_protection::bloomfilter::ReplayProtectionBloomfilter;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_gateway::node::GatewayStorageError;
|
||||
use nym_mixnet_client::forwarder::{MixForwardingSender, PacketToForward};
|
||||
@@ -29,6 +30,13 @@ pub(crate) mod final_hop;
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct ProcessingConfig {
|
||||
pub(crate) maximum_packet_delay: Duration,
|
||||
/// how long the task is willing to skip mutex acquisition before it will block the thread
|
||||
/// until it actually obtains it
|
||||
pub(crate) maximum_replay_detection_deferral: Duration,
|
||||
|
||||
/// how many packets the task is willing to queue before it will block the thread
|
||||
/// until it obtains the mutex
|
||||
pub(crate) maximum_replay_detection_pending_packets: usize,
|
||||
|
||||
pub(crate) forward_hop_processing_enabled: bool,
|
||||
pub(crate) final_hop_processing_enabled: bool,
|
||||
@@ -38,6 +46,16 @@ impl ProcessingConfig {
|
||||
pub(crate) fn new(config: &Config) -> Self {
|
||||
ProcessingConfig {
|
||||
maximum_packet_delay: config.mixnet.debug.maximum_forward_packet_delay,
|
||||
maximum_replay_detection_deferral: config
|
||||
.mixnet
|
||||
.replay_protection
|
||||
.debug
|
||||
.maximum_replay_detection_deferral,
|
||||
maximum_replay_detection_pending_packets: config
|
||||
.mixnet
|
||||
.replay_protection
|
||||
.debug
|
||||
.maximum_replay_detection_pending_packets,
|
||||
forward_hop_processing_enabled: config.modes.mixnode,
|
||||
final_hop_processing_enabled: config.modes.expects_final_hop_traffic()
|
||||
|| config.wireguard.enabled,
|
||||
@@ -49,6 +67,7 @@ impl ProcessingConfig {
|
||||
pub(crate) struct SharedData {
|
||||
pub(super) processing_config: ProcessingConfig,
|
||||
pub(super) sphinx_keys: Arc<x25519::KeyPair>,
|
||||
pub(super) replay_protection_filter: ReplayProtectionBloomfilter,
|
||||
|
||||
// used for FORWARD mix packets and FINAL ack packets
|
||||
pub(super) mixnet_forwarder: MixForwardingSender,
|
||||
@@ -71,6 +90,7 @@ impl SharedData {
|
||||
pub(crate) fn new(
|
||||
processing_config: ProcessingConfig,
|
||||
x25519_keys: Arc<x25519::KeyPair>,
|
||||
replay_protection_filter: ReplayProtectionBloomfilter,
|
||||
mixnet_forwarder: MixForwardingSender,
|
||||
final_hop: SharedFinalHopData,
|
||||
metrics: NymNodeMetrics,
|
||||
@@ -79,6 +99,7 @@ impl SharedData {
|
||||
SharedData {
|
||||
processing_config,
|
||||
sphinx_keys: x25519_keys,
|
||||
replay_protection_filter,
|
||||
mixnet_forwarder,
|
||||
final_hop,
|
||||
metrics,
|
||||
|
||||
@@ -28,9 +28,10 @@ use crate::node::metrics::handler::pending_egress_packets_updater::PendingEgress
|
||||
use crate::node::mixnet::packet_forwarding::PacketForwarder;
|
||||
use crate::node::mixnet::shared::ProcessingConfig;
|
||||
use crate::node::mixnet::SharedFinalHopData;
|
||||
use crate::node::shared_network::{
|
||||
CachedNetwork, CachedTopologyProvider, NetworkRefresher, OpenFilter, RoutingFilter,
|
||||
};
|
||||
use crate::node::replay_protection::background_task::ReplayProtectionBackgroundTask;
|
||||
use crate::node::replay_protection::bloomfilter::ReplayProtectionBloomfilter;
|
||||
use crate::node::routing_filter::{OpenFilter, RoutingFilter};
|
||||
use crate::node::shared_network::{CachedNetwork, CachedTopologyProvider, NetworkRefresher};
|
||||
use nym_bin_common::bin_info;
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_gateway::node::{ActiveClientsStore, GatewayTasksBuilder};
|
||||
@@ -69,6 +70,8 @@ pub mod helpers;
|
||||
pub(crate) mod http;
|
||||
pub(crate) mod metrics;
|
||||
pub(crate) mod mixnet;
|
||||
pub(crate) mod replay_protection;
|
||||
mod routing_filter;
|
||||
mod shared_network;
|
||||
|
||||
pub struct GatewayTasksData {
|
||||
@@ -979,12 +982,35 @@ impl NymNode {
|
||||
events_sender
|
||||
}
|
||||
|
||||
pub(crate) fn start_mixnet_listener<F>(
|
||||
pub(crate) async fn setup_replay_detection(
|
||||
&self,
|
||||
) -> Result<ReplayProtectionBloomfilter, NymNodeError> {
|
||||
if self.config.mixnet.replay_protection.debug.unsafe_disabled {
|
||||
return Ok(ReplayProtectionBloomfilter::new_disabled());
|
||||
}
|
||||
|
||||
// create the background task for the bloomfilter
|
||||
// to reset it and flush it to disk
|
||||
let mut replay_detection_background = ReplayProtectionBackgroundTask::new(
|
||||
&self.config,
|
||||
self.metrics.clone(),
|
||||
self.shutdown_manager
|
||||
.clone_token("replay-detection-background"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let replay_protection_bloomfilter = replay_detection_background.global_bloomfilter();
|
||||
self.shutdown_manager
|
||||
.spawn(async move { replay_detection_background.run().await });
|
||||
Ok(replay_protection_bloomfilter)
|
||||
}
|
||||
|
||||
pub(crate) async fn start_mixnet_listener<F>(
|
||||
&self,
|
||||
active_clients_store: &ActiveClientsStore,
|
||||
routing_filter: F,
|
||||
shutdown: ShutdownToken,
|
||||
) -> (MixForwardingSender, ActiveConnections)
|
||||
) -> Result<(MixForwardingSender, ActiveConnections), NymNodeError>
|
||||
where
|
||||
F: RoutingFilter + Send + Sync + 'static,
|
||||
{
|
||||
@@ -1012,6 +1038,7 @@ impl NymNode {
|
||||
);
|
||||
let active_connections = mixnet_client.active_connections();
|
||||
|
||||
let replay_protection_bloomfilter = self.setup_replay_detection().await?;
|
||||
let mut packet_forwarder = PacketForwarder::new(
|
||||
mixnet_client,
|
||||
routing_filter,
|
||||
@@ -1029,6 +1056,7 @@ impl NymNode {
|
||||
let shared = mixnet::SharedData::new(
|
||||
processing_config,
|
||||
self.x25519_sphinx_keys.clone(),
|
||||
replay_protection_bloomfilter,
|
||||
mix_packet_sender.clone(),
|
||||
final_hop_data,
|
||||
self.metrics.clone(),
|
||||
@@ -1036,7 +1064,7 @@ impl NymNode {
|
||||
);
|
||||
|
||||
mixnet::Listener::new(self.config.mixnet.bind_address, shared).start();
|
||||
(mix_packet_sender, active_connections)
|
||||
Ok((mix_packet_sender, active_connections))
|
||||
}
|
||||
|
||||
pub(crate) async fn run_minimal_mixnet_processing(self) -> Result<(), NymNodeError> {
|
||||
@@ -1044,7 +1072,8 @@ impl NymNode {
|
||||
&ActiveClientsStore::new(),
|
||||
OpenFilter,
|
||||
self.shutdown_manager.clone_token("mixnet-traffic"),
|
||||
);
|
||||
)
|
||||
.await?;
|
||||
|
||||
self.shutdown_manager.close();
|
||||
self.shutdown_manager.wait_for_shutdown_signal().await;
|
||||
@@ -1081,11 +1110,13 @@ impl NymNode {
|
||||
let network_refresher = self.build_network_refresher().await?;
|
||||
let active_clients_store = ActiveClientsStore::new();
|
||||
|
||||
let (mix_packet_sender, active_egress_mixnet_connections) = self.start_mixnet_listener(
|
||||
&active_clients_store,
|
||||
network_refresher.routing_filter(),
|
||||
self.shutdown_manager.clone_token("mixnet-traffic"),
|
||||
);
|
||||
let (mix_packet_sender, active_egress_mixnet_connections) = self
|
||||
.start_mixnet_listener(
|
||||
&active_clients_store,
|
||||
network_refresher.routing_filter(),
|
||||
self.shutdown_manager.clone_token("mixnet-traffic"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let metrics_sender = self.setup_metrics_backend(
|
||||
active_clients_store.clone(),
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::error::NymNodeError;
|
||||
use crate::node::replay_protection::bloomfilter::ReplayProtectionBloomfilter;
|
||||
use crate::node::replay_protection::items_in_bloomfilter;
|
||||
use human_repr::HumanCount;
|
||||
use nym_node_metrics::NymNodeMetrics;
|
||||
use nym_task::ShutdownToken;
|
||||
use std::cmp::max;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
use tokio::time::{interval, Instant};
|
||||
use tracing::{error, info, trace, warn};
|
||||
|
||||
struct LastResetData {
|
||||
packets_received_at_last_reset: usize,
|
||||
reset_time: Instant,
|
||||
}
|
||||
|
||||
struct ReplayProtectionBackgroundTaskConfig {
|
||||
current_bloomfilter_path: PathBuf,
|
||||
current_bloomfilter_temp_flush_path: PathBuf,
|
||||
|
||||
false_positive_rate: f64,
|
||||
filter_reset_rate: Duration,
|
||||
disk_flushing_rate: Duration,
|
||||
bloomfilter_size_multiplier: f64,
|
||||
minimum_bloomfilter_packets_per_second: usize,
|
||||
}
|
||||
|
||||
impl From<&Config> for ReplayProtectionBackgroundTaskConfig {
|
||||
fn from(config: &Config) -> Self {
|
||||
ReplayProtectionBackgroundTaskConfig {
|
||||
current_bloomfilter_path: config
|
||||
.mixnet
|
||||
.replay_protection
|
||||
.storage_paths
|
||||
.current_bloomfilter_filepath(),
|
||||
current_bloomfilter_temp_flush_path: config
|
||||
.mixnet
|
||||
.replay_protection
|
||||
.storage_paths
|
||||
.current_bloomfilter_being_flushed_filepath(),
|
||||
false_positive_rate: config.mixnet.replay_protection.debug.false_positive_rate,
|
||||
filter_reset_rate: config.mixnet.replay_protection.debug.bloomfilter_reset_rate,
|
||||
disk_flushing_rate: config
|
||||
.mixnet
|
||||
.replay_protection
|
||||
.debug
|
||||
.bloomfilter_disk_flushing_rate,
|
||||
bloomfilter_size_multiplier: config
|
||||
.mixnet
|
||||
.replay_protection
|
||||
.debug
|
||||
.bloomfilter_size_multiplier,
|
||||
minimum_bloomfilter_packets_per_second: config
|
||||
.mixnet
|
||||
.replay_protection
|
||||
.debug
|
||||
.bloomfilter_minimum_packets_per_second_size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// background task responsible for periodically flushing the bloomfilter to disk
|
||||
// as well as clearing it up on the specified timer
|
||||
// (in the future this will be enforced by key rotation)
|
||||
pub struct ReplayProtectionBackgroundTask {
|
||||
config: ReplayProtectionBackgroundTaskConfig,
|
||||
last_reset: LastResetData,
|
||||
|
||||
filter: ReplayProtectionBloomfilter,
|
||||
metrics: NymNodeMetrics,
|
||||
shutdown_token: ShutdownToken,
|
||||
}
|
||||
|
||||
impl ReplayProtectionBackgroundTask {
|
||||
pub(crate) async fn new(
|
||||
config: &Config,
|
||||
metrics: NymNodeMetrics,
|
||||
shutdown_token: ShutdownToken,
|
||||
) -> Result<Self, NymNodeError> {
|
||||
let task_config: ReplayProtectionBackgroundTaskConfig = config.into();
|
||||
|
||||
if task_config.current_bloomfilter_temp_flush_path.exists() {
|
||||
error!(
|
||||
"bloomfilter didn't get successfully flushed to disk and its data got corrupted"
|
||||
);
|
||||
fs::remove_file(&task_config.current_bloomfilter_temp_flush_path).map_err(|source| {
|
||||
NymNodeError::BloomfilterIoFailure {
|
||||
source,
|
||||
path: task_config.current_bloomfilter_temp_flush_path.clone(),
|
||||
}
|
||||
})?
|
||||
}
|
||||
|
||||
// if there's nothing on disk, we must create a new filter
|
||||
let bloomfilter = if task_config.current_bloomfilter_path.exists() {
|
||||
ReplayProtectionBloomfilter::load(&task_config.current_bloomfilter_path).await?
|
||||
} else {
|
||||
let bf_items = items_in_bloomfilter(
|
||||
task_config.filter_reset_rate,
|
||||
config
|
||||
.mixnet
|
||||
.replay_protection
|
||||
.debug
|
||||
.initial_expected_packets_per_second,
|
||||
);
|
||||
|
||||
ReplayProtectionBloomfilter::new_empty(bf_items, task_config.false_positive_rate)?
|
||||
};
|
||||
|
||||
Ok(ReplayProtectionBackgroundTask {
|
||||
config: task_config,
|
||||
last_reset: LastResetData {
|
||||
packets_received_at_last_reset: 0,
|
||||
reset_time: Instant::now(),
|
||||
},
|
||||
filter: bloomfilter,
|
||||
metrics,
|
||||
shutdown_token,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn global_bloomfilter(&self) -> ReplayProtectionBloomfilter {
|
||||
self.filter.clone()
|
||||
}
|
||||
|
||||
async fn flush_to_disk(&self) -> Result<(), NymNodeError> {
|
||||
if let Some(temp_parent) = self.config.current_bloomfilter_temp_flush_path.parent() {
|
||||
fs::create_dir_all(temp_parent).map_err(|source| {
|
||||
NymNodeError::BloomfilterIoFailure {
|
||||
source,
|
||||
path: temp_parent.to_path_buf(),
|
||||
}
|
||||
})?
|
||||
}
|
||||
if let Some(current_parent) = self.config.current_bloomfilter_temp_flush_path.parent() {
|
||||
fs::create_dir_all(current_parent).map_err(|source| {
|
||||
NymNodeError::BloomfilterIoFailure {
|
||||
source,
|
||||
path: current_parent.to_path_buf(),
|
||||
}
|
||||
})?
|
||||
}
|
||||
|
||||
// because it takes a while to actually write the file to disk,
|
||||
// we first write bytes to temporary location,
|
||||
// and then we move it to the correct path
|
||||
let temp = &self.config.current_bloomfilter_temp_flush_path;
|
||||
self.filter.flush_to_disk(temp).await?;
|
||||
fs::rename(temp, &self.config.current_bloomfilter_path).map_err(|source| {
|
||||
NymNodeError::BloomfilterIoFailure {
|
||||
source,
|
||||
path: self.config.current_bloomfilter_path.clone(),
|
||||
}
|
||||
})?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reset_bloomfilter(&mut self) -> Result<(), NymNodeError> {
|
||||
// 1. determine parameters for new bloomfilter
|
||||
let received = self.metrics.mixnet.ingress.forward_hop_packets_received()
|
||||
+ self.metrics.mixnet.ingress.final_hop_packets_received();
|
||||
|
||||
let time_delta = self.last_reset.reset_time.elapsed();
|
||||
let received_since_last_reset = received - self.last_reset.packets_received_at_last_reset;
|
||||
let received_per_second =
|
||||
(received_since_last_reset as f64 / time_delta.as_secs_f64()).round() as usize;
|
||||
|
||||
let bf_received = max(
|
||||
received_per_second,
|
||||
self.config.minimum_bloomfilter_packets_per_second,
|
||||
);
|
||||
let items_in_new_filter = items_in_bloomfilter(self.config.filter_reset_rate, bf_received);
|
||||
let adjusted =
|
||||
(items_in_new_filter as f64 * self.config.bloomfilter_size_multiplier).round() as usize;
|
||||
|
||||
info!(
|
||||
"resetting bloom filter. new expected number of packets: {} that preserve fp rate of {}",
|
||||
adjusted.human_count_bare(),
|
||||
self.config.false_positive_rate
|
||||
);
|
||||
|
||||
// 2. update the filter
|
||||
self.last_reset.reset_time = Instant::now();
|
||||
self.last_reset.packets_received_at_last_reset = received_since_last_reset;
|
||||
|
||||
// if this fails with the mutex getting poisoned, the next received packet is going to cause
|
||||
// a shutdown, so we don't have to propagate it here
|
||||
self.filter.reset(adjusted, self.config.false_positive_rate)
|
||||
}
|
||||
|
||||
pub(crate) async fn run(&mut self) {
|
||||
let mut reset_timer = interval(self.config.filter_reset_rate);
|
||||
reset_timer.reset();
|
||||
|
||||
let mut flush_timer = interval(self.config.disk_flushing_rate);
|
||||
flush_timer.reset();
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = self.shutdown_token.cancelled() => {
|
||||
trace!("ReplayProtectionBackgroundTask: Received shutdown");
|
||||
break;
|
||||
}
|
||||
_ = reset_timer.tick() => {
|
||||
if let Err(err) = self.reset_bloomfilter() {
|
||||
error!("failed to reset the bloomfilter: {err}")
|
||||
}
|
||||
}
|
||||
_ = flush_timer.tick() => {
|
||||
if let Err(err) = self.flush_to_disk().await {
|
||||
error!("failed to flush bloomfilter to disk: {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("SHUTDOWN: flushing replay detection bloomfilter to disk. this might take a while. DO NOT INTERRUPT THIS PROCESS");
|
||||
if let Err(err) = self.flush_to_disk().await {
|
||||
warn!("failed to flush replay detection bloom filter on shutdown: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::error::NymNodeError;
|
||||
use bloomfilter::Bloom;
|
||||
use human_repr::HumanDuration;
|
||||
use nym_sphinx_types::REPLAY_TAG_SIZE;
|
||||
use std::path::Path;
|
||||
use std::sync::{Arc, PoisonError, TryLockError};
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tokio::time::Instant;
|
||||
use tracing::{debug, info};
|
||||
|
||||
// it appears that now std Mutex is faster (or comparable) to parking_lot
|
||||
// in high contention situations: https://github.com/rust-lang/rust/pull/95035#issuecomment-1073966631
|
||||
// (tokio's async Mutex has too much overhead due to the number of access required)
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ReplayProtectionBloomfilter {
|
||||
disabled: bool,
|
||||
inner: Arc<std::sync::Mutex<ReplayProtectionBloomfilterInner>>,
|
||||
}
|
||||
|
||||
impl ReplayProtectionBloomfilter {
|
||||
pub(crate) fn new_empty(items_count: usize, fp_p: f64) -> Result<Self, NymNodeError> {
|
||||
Ok(ReplayProtectionBloomfilter {
|
||||
disabled: false,
|
||||
inner: Arc::new(std::sync::Mutex::new(ReplayProtectionBloomfilterInner {
|
||||
current_filter: Bloom::new_for_fp_rate(items_count, fp_p)
|
||||
.map_err(NymNodeError::bloomfilter_failure)?,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
// SAFETY: the hardcoded values of 1,1 are valid
|
||||
#[allow(clippy::unwrap_used)]
|
||||
pub(crate) fn new_disabled() -> Self {
|
||||
// well, technically it's not fully empty, but the memory footprint is negligible
|
||||
ReplayProtectionBloomfilter {
|
||||
disabled: true,
|
||||
inner: Arc::new(std::sync::Mutex::new(ReplayProtectionBloomfilterInner {
|
||||
current_filter: Bloom::new(1, 1).unwrap(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn disabled(&self) -> bool {
|
||||
self.disabled
|
||||
}
|
||||
|
||||
pub(crate) fn reset(&self, items_count: usize, fp_p: f64) -> Result<(), NymNodeError> {
|
||||
// 1. build the new filter
|
||||
let new_inner = ReplayProtectionBloomfilterInner {
|
||||
current_filter: Bloom::new_for_fp_rate(items_count, fp_p)
|
||||
.map_err(NymNodeError::bloomfilter_failure)?,
|
||||
};
|
||||
|
||||
// 2. swap it
|
||||
let mut guard = self
|
||||
.inner
|
||||
.lock()
|
||||
.map_err(|_| NymNodeError::BloomfilterFailure {
|
||||
message: "mutex got poisoned",
|
||||
})?;
|
||||
|
||||
*guard = new_inner;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// NOTE: with key rotations we'll have to check whether the file is still valid and which
|
||||
// key it corresponds to, but that's a future problem
|
||||
pub(crate) async fn load<P: AsRef<Path>>(path: P) -> Result<Self, NymNodeError> {
|
||||
info!("attempting to load prior replay detection bloomfilter...");
|
||||
let path = path.as_ref();
|
||||
let mut file =
|
||||
File::open(path)
|
||||
.await
|
||||
.map_err(|source| NymNodeError::BloomfilterIoFailure {
|
||||
source,
|
||||
path: path.to_path_buf(),
|
||||
})?;
|
||||
|
||||
let mut buf = Vec::new();
|
||||
file.read_to_end(&mut buf)
|
||||
.await
|
||||
.map_err(|source| NymNodeError::BloomfilterIoFailure {
|
||||
source,
|
||||
path: path.to_path_buf(),
|
||||
})?;
|
||||
|
||||
Ok(ReplayProtectionBloomfilter {
|
||||
disabled: false,
|
||||
inner: Arc::new(std::sync::Mutex::new(ReplayProtectionBloomfilterInner {
|
||||
current_filter: Bloom::from_bytes(buf)
|
||||
.map_err(NymNodeError::bloomfilter_failure)?,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
// average HDD has the write speed of ~80MB/s so a 2GB bloomfilter would take almost 30s to write...
|
||||
// and this function is explicitly async and using tokio's async operations, because otherwise
|
||||
// we'd have to go through the whole hassle of using spawn_blocking and awaiting that one instead
|
||||
pub(crate) async fn flush_to_disk<P: AsRef<Path>>(&self, path: P) -> Result<(), NymNodeError> {
|
||||
debug!("flushing replay protection bloomfilter to disk...");
|
||||
let start = Instant::now();
|
||||
let path = path.as_ref();
|
||||
|
||||
let mut file =
|
||||
File::create(path)
|
||||
.await
|
||||
.map_err(|source| NymNodeError::BloomfilterIoFailure {
|
||||
source,
|
||||
path: path.to_path_buf(),
|
||||
})?;
|
||||
let data = self.bytes().map_err(|_| NymNodeError::BloomfilterFailure {
|
||||
message: "mutex got poisoned",
|
||||
})?;
|
||||
file.write_all(&data)
|
||||
.await
|
||||
.map_err(|source| NymNodeError::BloomfilterIoFailure {
|
||||
source,
|
||||
path: path.to_path_buf(),
|
||||
})?;
|
||||
|
||||
let elapsed = start.elapsed();
|
||||
|
||||
info!(
|
||||
"flushed replay protection bloomfilter to disk. it took: {}",
|
||||
elapsed.human_duration()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
struct ReplayProtectionBloomfilterInner {
|
||||
// metadata to do with epochs, etc.
|
||||
current_filter: Bloom<[u8; REPLAY_TAG_SIZE]>,
|
||||
// overlap_filter: bloomfilter::Bloom<[u8; REPLAY_TAG_SIZE]>,
|
||||
}
|
||||
|
||||
impl ReplayProtectionBloomfilter {
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn check_and_set(
|
||||
&self,
|
||||
replay_tag: &[u8; REPLAY_TAG_SIZE],
|
||||
) -> Result<bool, PoisonError<()>> {
|
||||
let Ok(mut guard) = self.inner.lock() else {
|
||||
return Err(PoisonError::new(()));
|
||||
};
|
||||
|
||||
Ok(guard.current_filter.check_and_set(replay_tag))
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn try_check_and_set(
|
||||
&self,
|
||||
replay_tag: &[u8; REPLAY_TAG_SIZE],
|
||||
) -> Option<Result<bool, PoisonError<()>>> {
|
||||
let mut guard = match self.inner.try_lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(TryLockError::Poisoned(_)) => return Some(Err(PoisonError::new(()))),
|
||||
Err(TryLockError::WouldBlock) => return None,
|
||||
};
|
||||
|
||||
Some(Ok(guard.current_filter.check_and_set(replay_tag)))
|
||||
}
|
||||
|
||||
pub(crate) fn batch_try_check_and_set(
|
||||
&self,
|
||||
reply_tags: &[&[u8; REPLAY_TAG_SIZE]],
|
||||
) -> Option<Result<Vec<bool>, PoisonError<()>>> {
|
||||
let mut guard = match self.inner.try_lock() {
|
||||
Ok(guard) => guard,
|
||||
Err(TryLockError::Poisoned(_)) => return Some(Err(PoisonError::new(()))),
|
||||
Err(TryLockError::WouldBlock) => return None,
|
||||
};
|
||||
|
||||
let mut result = Vec::with_capacity(reply_tags.len());
|
||||
for tag in reply_tags {
|
||||
result.push(guard.current_filter.check_and_set(tag));
|
||||
}
|
||||
|
||||
// for testing throughput without disabling checks:
|
||||
// return Some(Ok(vec![false; reply_tags.len()]));
|
||||
|
||||
Some(Ok(result))
|
||||
}
|
||||
|
||||
pub(crate) fn batch_check_and_set(
|
||||
&self,
|
||||
reply_tags: &[&[u8; REPLAY_TAG_SIZE]],
|
||||
) -> Result<Vec<bool>, PoisonError<()>> {
|
||||
let Ok(mut guard) = self.inner.lock() else {
|
||||
return Err(PoisonError::new(()));
|
||||
};
|
||||
|
||||
let mut result = Vec::with_capacity(reply_tags.len());
|
||||
for tag in reply_tags {
|
||||
result.push(guard.current_filter.check_and_set(tag));
|
||||
}
|
||||
|
||||
// for testing throughput without disabling checks:
|
||||
// return Ok(vec![false; reply_tags.len()]);
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn clear(&self) -> Result<(), PoisonError<()>> {
|
||||
let mut guard = self.inner.lock().map_err(|_| PoisonError::new(()))?;
|
||||
guard.current_filter.clear();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// due to the size of the bloomfilter, extra caution has to be applied when using this method
|
||||
// note: we're not getting reference to bytes as this method is used when flushing data to the disk
|
||||
// (which takes ~30s) and we can't block the mutex for that long.
|
||||
fn bytes(&self) -> Result<Vec<u8>, PoisonError<()>> {
|
||||
let guard = self.inner.lock().map_err(|_| PoisonError::new(()))?;
|
||||
Ok(guard.current_filter.to_bytes())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::f64::consts::LN_2;
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) mod background_task;
|
||||
pub(crate) mod bloomfilter;
|
||||
|
||||
pub fn bitmap_size(false_positive_rate: f64, items_in_filter: usize) -> usize {
|
||||
/// Equivalent to ln(1 / 2^ln(2)) = −ln^2(2)
|
||||
const NEG_LN_2_POW_2: f64 = -0.48045301391820144f64;
|
||||
|
||||
assert!(items_in_filter < f64::MAX.floor() as usize);
|
||||
|
||||
((items_in_filter as f64 * false_positive_rate.ln()) / NEG_LN_2_POW_2).ceil() as usize
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn num_of_hash_functions(items_in_filter: usize, bitmap_size: usize) -> usize {
|
||||
((bitmap_size as f64 / items_in_filter as f64) * LN_2).round() as usize
|
||||
}
|
||||
|
||||
pub fn items_in_bloomfilter(reset_rate: Duration, packets_per_second: usize) -> usize {
|
||||
reset_rate.as_secs() as usize * packets_per_second
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn calculating_bitmap_size() {
|
||||
let fpr = 1e-5;
|
||||
let items_in_filter = 725760000;
|
||||
let expected_bitmap_size = 17391129920;
|
||||
|
||||
assert_eq!(bitmap_size(fpr, items_in_filter), expected_bitmap_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn calculating_number_of_hash_functions() {
|
||||
let items_in_filter = 725760000;
|
||||
let bitmap_size = 17391129920;
|
||||
let expected_hashes = 17;
|
||||
|
||||
assert_eq!(
|
||||
num_of_hash_functions(items_in_filter, bitmap_size),
|
||||
expected_hashes
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use std::net::IpAddr;
|
||||
|
||||
pub(crate) mod network_filter;
|
||||
|
||||
pub(crate) trait RoutingFilter {
|
||||
fn should_route(&self, ip: IpAddr) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
pub(crate) struct OpenFilter;
|
||||
|
||||
impl RoutingFilter for OpenFilter {
|
||||
fn should_route(&self, _: IpAddr) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
// #[derive(Default)]
|
||||
// pub(crate) struct ComposedRoutingFilter {
|
||||
// layers: Vec<Box<dyn RoutingFilter + Send + Sync + 'static>>,
|
||||
// }
|
||||
//
|
||||
// impl ComposedRoutingFilter {
|
||||
// pub(crate) fn new() -> Self {
|
||||
// Self::default()
|
||||
// }
|
||||
//
|
||||
// pub(crate) fn with_filter<F: RoutingFilter + Send + Sync + 'static>(
|
||||
// mut self,
|
||||
// filter: F,
|
||||
// ) -> Self {
|
||||
// self.layers.push(Box::new(filter));
|
||||
// self
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// impl RoutingFilter for ComposedRoutingFilter {
|
||||
// fn should_route(&self, ip: IpAddr) -> bool {
|
||||
// self.layers.iter().all(|l| l.should_route(ip))
|
||||
// }
|
||||
// }
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::node::mixnet::packet_forwarding::global::is_global_ip;
|
||||
use crate::node::routing_filter::RoutingFilter;
|
||||
use arc_swap::ArcSwap;
|
||||
use std::collections::HashSet;
|
||||
use std::net::IpAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
impl RoutingFilter for NetworkRoutingFilter {
|
||||
fn should_route(&self, ip: IpAddr) -> bool {
|
||||
// only allow non-global ips on testnets
|
||||
if self.testnet_mode && !is_global_ip(&ip) {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.attempt_resolve(ip).should_route()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct NetworkRoutingFilter {
|
||||
testnet_mode: bool,
|
||||
|
||||
pub(crate) resolved: KnownNodes,
|
||||
|
||||
// while this is technically behind a lock, it should not be called too often as once resolved it will
|
||||
// be present on the arcswap in either allowed or denied section
|
||||
pub(crate) pending: UnknownNodes,
|
||||
}
|
||||
|
||||
impl NetworkRoutingFilter {
|
||||
pub(crate) fn new_empty(testnet_mode: bool) -> Self {
|
||||
NetworkRoutingFilter {
|
||||
testnet_mode,
|
||||
resolved: Default::default(),
|
||||
pending: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn attempt_resolve(&self, ip: IpAddr) -> Resolution {
|
||||
if self.resolved.inner.allowed.load().contains(&ip) {
|
||||
Resolution::Accept
|
||||
} else if self.resolved.inner.denied.load().contains(&ip) {
|
||||
Resolution::Deny
|
||||
} else {
|
||||
self.pending.try_insert(ip);
|
||||
Resolution::Unknown
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn allowed_nodes_copy(&self) -> HashSet<IpAddr> {
|
||||
self.resolved.inner.allowed.load_full().as_ref().clone()
|
||||
}
|
||||
|
||||
pub(crate) fn denied_nodes_copy(&self) -> HashSet<IpAddr> {
|
||||
self.resolved.inner.denied.load_full().as_ref().clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub(crate) struct UnknownNodes(Arc<RwLock<HashSet<IpAddr>>>);
|
||||
|
||||
impl UnknownNodes {
|
||||
fn try_insert(&self, ip: IpAddr) {
|
||||
// if we can immediately grab the lock to push it into the pending queue, amazing, let's do it
|
||||
// otherwise we can do it next time we see this ip
|
||||
// (if we can't hold the lock, it means it's being updated at this very moment which is actually a good thing)
|
||||
if let Ok(mut guard) = self.0.try_write() {
|
||||
guard.insert(ip);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn clear(&self) {
|
||||
self.0.write().await.clear();
|
||||
}
|
||||
|
||||
pub(crate) async fn nodes(&self) -> HashSet<IpAddr> {
|
||||
self.0.read().await.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// for now we don't care about keys, etc.
|
||||
// we only want to know if given ip belongs to a known node
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub(crate) struct KnownNodes {
|
||||
inner: Arc<KnownNodesInner>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct KnownNodesInner {
|
||||
allowed: ArcSwap<HashSet<IpAddr>>,
|
||||
denied: ArcSwap<HashSet<IpAddr>>,
|
||||
}
|
||||
|
||||
pub(crate) enum Resolution {
|
||||
Unknown,
|
||||
Deny,
|
||||
Accept,
|
||||
}
|
||||
|
||||
impl From<bool> for Resolution {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
Resolution::Accept
|
||||
} else {
|
||||
Resolution::Deny
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolution {
|
||||
pub(crate) fn should_route(&self) -> bool {
|
||||
matches!(self, Resolution::Accept)
|
||||
}
|
||||
}
|
||||
|
||||
impl KnownNodes {
|
||||
pub(crate) fn swap_allowed(&self, new: HashSet<IpAddr>) {
|
||||
self.inner.allowed.store(Arc::new(new))
|
||||
}
|
||||
|
||||
pub(crate) fn swap_denied(&self, new: HashSet<IpAddr>) {
|
||||
self.inner.denied.store(Arc::new(new))
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::error::NymNodeError;
|
||||
use crate::node::mixnet::packet_forwarding::global::is_global_ip;
|
||||
use arc_swap::ArcSwap;
|
||||
use crate::node::routing_filter::network_filter::NetworkRoutingFilter;
|
||||
use async_trait::async_trait;
|
||||
use nym_gateway::node::UserAgent;
|
||||
use nym_node_metrics::prometheus_wrapper::{PrometheusMetric, PROMETHEUS_METRICS};
|
||||
@@ -23,129 +22,6 @@ use tracing::log::error;
|
||||
use tracing::{debug, trace, warn};
|
||||
use url::Url;
|
||||
|
||||
pub(crate) trait RoutingFilter {
|
||||
fn should_route(&self, ip: IpAddr) -> bool;
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Default)]
|
||||
pub(crate) struct OpenFilter;
|
||||
|
||||
impl RoutingFilter for OpenFilter {
|
||||
fn should_route(&self, _: IpAddr) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
impl RoutingFilter for NetworkRoutingFilter {
|
||||
fn should_route(&self, ip: IpAddr) -> bool {
|
||||
// only allow non-global ips on testnets
|
||||
if self.testnet_mode && !is_global_ip(&ip) {
|
||||
return true;
|
||||
}
|
||||
|
||||
self.attempt_resolve(ip).should_route()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct NetworkRoutingFilter {
|
||||
testnet_mode: bool,
|
||||
|
||||
resolved: KnownNodes,
|
||||
|
||||
// while this is technically behind a lock, it should not be called too often as once resolved it will
|
||||
// be present on the arcswap in either allowed or denied section
|
||||
pending: UnknownNodes,
|
||||
}
|
||||
|
||||
impl NetworkRoutingFilter {
|
||||
fn new_empty(testnet_mode: bool) -> Self {
|
||||
NetworkRoutingFilter {
|
||||
testnet_mode,
|
||||
resolved: Default::default(),
|
||||
pending: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn attempt_resolve(&self, ip: IpAddr) -> Resolution {
|
||||
if self.resolved.inner.allowed.load().contains(&ip) {
|
||||
Resolution::Accept
|
||||
} else if self.resolved.inner.denied.load().contains(&ip) {
|
||||
Resolution::Deny
|
||||
} else {
|
||||
self.pending.try_insert(ip);
|
||||
Resolution::Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
struct UnknownNodes(Arc<RwLock<HashSet<IpAddr>>>);
|
||||
|
||||
impl UnknownNodes {
|
||||
fn try_insert(&self, ip: IpAddr) {
|
||||
// if we can immediately grab the lock to push it into the pending queue, amazing, let's do it
|
||||
// otherwise we can do it next time we see this ip
|
||||
// (if we can't hold the lock, it means it's being updated at this very moment which is actually a good thing)
|
||||
if let Ok(mut guard) = self.0.try_write() {
|
||||
guard.insert(ip);
|
||||
}
|
||||
}
|
||||
|
||||
async fn clear(&self) {
|
||||
self.0.write().await.clear();
|
||||
}
|
||||
|
||||
async fn nodes(&self) -> HashSet<IpAddr> {
|
||||
self.0.read().await.clone()
|
||||
}
|
||||
}
|
||||
|
||||
// for now we don't care about keys, etc.
|
||||
// we only want to know if given ip belongs to a known node
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub(crate) struct KnownNodes {
|
||||
inner: Arc<KnownNodesInner>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct KnownNodesInner {
|
||||
allowed: ArcSwap<HashSet<IpAddr>>,
|
||||
denied: ArcSwap<HashSet<IpAddr>>,
|
||||
}
|
||||
|
||||
pub(crate) enum Resolution {
|
||||
Unknown,
|
||||
Deny,
|
||||
Accept,
|
||||
}
|
||||
|
||||
impl From<bool> for Resolution {
|
||||
fn from(value: bool) -> Self {
|
||||
if value {
|
||||
Resolution::Accept
|
||||
} else {
|
||||
Resolution::Deny
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Resolution {
|
||||
pub(crate) fn should_route(&self) -> bool {
|
||||
matches!(self, Resolution::Accept)
|
||||
}
|
||||
}
|
||||
|
||||
impl KnownNodes {
|
||||
fn swap_allowed(&self, new: HashSet<IpAddr>) {
|
||||
self.inner.allowed.store(Arc::new(new))
|
||||
}
|
||||
|
||||
fn swap_denied(&self, new: HashSet<IpAddr>) {
|
||||
self.inner.denied.store(Arc::new(new))
|
||||
}
|
||||
}
|
||||
|
||||
struct NodesQuerier {
|
||||
client: NymApiClient,
|
||||
nym_api_urls: Vec<Url>,
|
||||
@@ -316,26 +192,6 @@ impl NetworkRefresher {
|
||||
Ok(this)
|
||||
}
|
||||
|
||||
fn allowed_nodes_copy(&self) -> HashSet<IpAddr> {
|
||||
self.routing_filter
|
||||
.resolved
|
||||
.inner
|
||||
.allowed
|
||||
.load_full()
|
||||
.as_ref()
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn denied_nodes_copy(&self) -> HashSet<IpAddr> {
|
||||
self.routing_filter
|
||||
.resolved
|
||||
.inner
|
||||
.denied
|
||||
.load_full()
|
||||
.as_ref()
|
||||
.clone()
|
||||
}
|
||||
|
||||
async fn inspect_pending(&mut self) {
|
||||
let to_resolve = self.routing_filter.pending.nodes().await;
|
||||
|
||||
@@ -344,8 +200,8 @@ impl NetworkRefresher {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut allowed = self.allowed_nodes_copy();
|
||||
let mut denied = self.denied_nodes_copy();
|
||||
let mut allowed = self.routing_filter().allowed_nodes_copy();
|
||||
let mut denied = self.routing_filter().denied_nodes_copy();
|
||||
|
||||
// short circuit: check if the pending nodes are not already resolved
|
||||
// (it could happen due to lack of full sync between pending lock and arcswap(s))
|
||||
@@ -389,7 +245,7 @@ impl NetworkRefresher {
|
||||
.collect::<HashSet<_>>();
|
||||
|
||||
let pending = self.routing_filter.pending.nodes().await;
|
||||
let mut current_denied = self.denied_nodes_copy();
|
||||
let mut current_denied = self.routing_filter.denied_nodes_copy();
|
||||
|
||||
for allowed in &known_nodes {
|
||||
// if some node has become known, it should be removed from the denied set
|
||||
|
||||
@@ -7,6 +7,7 @@ use arrayref::array_ref;
|
||||
use blake2::VarBlake2b;
|
||||
use chacha::ChaCha;
|
||||
use futures::{stream, SinkExt, Stream, StreamExt};
|
||||
use hkdf::Hkdf;
|
||||
use human_repr::{HumanCount, HumanDuration, HumanThroughput};
|
||||
use lioness::Lioness;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
@@ -15,13 +16,15 @@ use nym_sphinx_framing::codec::{NymCodec, NymCodecError};
|
||||
use nym_sphinx_framing::packet::FramedNymPacket;
|
||||
use nym_sphinx_params::PacketSize;
|
||||
use nym_sphinx_routing::generate_hop_delays;
|
||||
use nym_sphinx_types::constants::{EXPANDED_SHARED_SECRET_LENGTH, HKDF_INPUT_SEED};
|
||||
use nym_sphinx_types::header::keys::PayloadKey;
|
||||
use nym_sphinx_types::{
|
||||
Destination, DestinationAddressBytes, Node, NymPacket, SphinxHeader,
|
||||
DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH,
|
||||
Destination, DestinationAddressBytes, Node, NymPacket, DESTINATION_ADDRESS_LENGTH,
|
||||
IDENTIFIER_LENGTH,
|
||||
};
|
||||
use nym_task::ShutdownToken;
|
||||
use rand::rngs::OsRng;
|
||||
use sha2::Sha256;
|
||||
use std::net::SocketAddr;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll, Waker};
|
||||
@@ -96,6 +99,18 @@ pub(crate) struct ThroughputTestingClient {
|
||||
payload_key: PayloadKey,
|
||||
}
|
||||
|
||||
fn rederive_lioness_payload_key(shared_secret: &[u8; 32]) -> PayloadKey {
|
||||
let hkdf = Hkdf::<Sha256>::new(None, shared_secret);
|
||||
|
||||
// expanded shared secret
|
||||
let mut output = [0u8; EXPANDED_SHARED_SECRET_LENGTH];
|
||||
// SAFETY: the length of the provided okm is within the allowed range
|
||||
#[allow(clippy::unwrap_used)]
|
||||
hkdf.expand(HKDF_INPUT_SEED, &mut output).unwrap();
|
||||
|
||||
*array_ref!(&output, 32, 192)
|
||||
}
|
||||
|
||||
impl ThroughputTestingClient {
|
||||
pub(crate) async fn try_create(
|
||||
initial_sending_delay: Duration,
|
||||
@@ -145,16 +160,17 @@ impl ThroughputTestingClient {
|
||||
|
||||
// SAFETY: we constructed a sphinx packet...
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let sphinx_packet = forward_packet.as_sphinx_packet().unwrap();
|
||||
let sphinx_packet = forward_packet.to_sphinx_packet().unwrap();
|
||||
let header = &sphinx_packet.header;
|
||||
|
||||
// derive the routing keys of our node so we could tag the payload to figure out latency
|
||||
// derive the expanded shared secret for our node so we could tag the payload to figure out latency
|
||||
// by tagging the packet
|
||||
let routing_keys = SphinxHeader::compute_routing_keys(
|
||||
&header.shared_secret,
|
||||
(&node_keys.private_key()).as_ref(),
|
||||
);
|
||||
let payload_key = routing_keys.payload_key;
|
||||
let shared_secret = node_keys
|
||||
.private_key()
|
||||
.as_ref()
|
||||
.diffie_hellman(&header.shared_secret);
|
||||
let payload_key = rederive_lioness_payload_key(shared_secret.as_bytes());
|
||||
|
||||
let unwrapped_payload = sphinx_packet.payload.unwrap(&payload_key)?;
|
||||
let unwrapped_forward_payload_bytes = unwrapped_payload.into_bytes();
|
||||
|
||||
@@ -271,7 +287,7 @@ impl ThroughputTestingClient {
|
||||
let inner = received.into_inner();
|
||||
// safety: we sent a sphinx packet...
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let sphinx = inner.as_sphinx_packet().unwrap();
|
||||
let sphinx = inner.to_sphinx_packet().unwrap();
|
||||
let tag = PacketTag::from_bytes(sphinx.payload.as_bytes());
|
||||
|
||||
self.stats.new_received(tag.elapsed_nanos());
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@nymproject/nym-wallet-app",
|
||||
"version": "1.2.17",
|
||||
"version": "1.2.18",
|
||||
"license": "MIT",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "run-s webpack:prod tauri:build",
|
||||
"dev": "run-p tauri:dev webpack:dev",
|
||||
"build-macx86": "run-s webpack:prod tauri:buildx86",
|
||||
"dev": "run-p tauri:dev webpack:dev",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"prebuild": "yarn --cwd .. build",
|
||||
@@ -15,14 +15,15 @@
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"storybook:build": "build-storybook",
|
||||
"tauri:build": "yarn tauri build",
|
||||
"tauri:buildx86": "yarn tauri build --target x86_64-apple-darwin",
|
||||
"tauri:dev": "yarn tauri dev",
|
||||
"tauri:buildx86": "yarn tauri build --target x86_64-apple-darwin",
|
||||
"tsc": "tsc --noEmit true",
|
||||
"tsc:watch": "tsc --noEmit true --watch",
|
||||
"webpack:dev": "yarn webpack serve --config webpack.dev.js",
|
||||
"webpack:prod": "yarn webpack --progress --config webpack.prod.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/helper-simple-access": "^7.25.9",
|
||||
"@emotion/react": "^11.7.0",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@hookform/resolvers": "^2.8.0",
|
||||
@@ -35,7 +36,11 @@
|
||||
"@nymproject/react": "^1.0.0",
|
||||
"@nymproject/types": "^1.0.0",
|
||||
"@storybook/react": "^6.5.15",
|
||||
"@tauri-apps/api": "^1.2.0",
|
||||
"@tauri-apps/api": "^2.4.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.2.2",
|
||||
"@tauri-apps/plugin-opener": "^2.2.6",
|
||||
"@tauri-apps/plugin-shell": "^2.2.1",
|
||||
"@tauri-apps/plugin-updater": "^2.0.0",
|
||||
"@tauri-apps/tauri-forage": "^1.0.0-beta.2",
|
||||
"big.js": "^6.2.1",
|
||||
"bs58": "^4.0.1",
|
||||
@@ -69,7 +74,7 @@
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
|
||||
"@storybook/react": "^6.5.15",
|
||||
"@svgr/webpack": "^6.1.1",
|
||||
"@tauri-apps/cli": "^1.0.5",
|
||||
"@tauri-apps/cli": "^2.4.0",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.0.0",
|
||||
"@types/big.js": "^6.1.6",
|
||||
@@ -127,4 +132,4 @@
|
||||
"webpack-merge": "^5.8.0"
|
||||
},
|
||||
"private": false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
[capabilities]
|
||||
shell = { open = true }
|
||||
@@ -1,25 +1,27 @@
|
||||
[package]
|
||||
name = "nym_wallet"
|
||||
version = "1.2.17"
|
||||
name = "NymWallet"
|
||||
version = "1.2.18"
|
||||
description = "Nym Native Wallet"
|
||||
authors = ["Nym Technologies SA"]
|
||||
license = ""
|
||||
repository = ""
|
||||
default-run = "nym_wallet"
|
||||
default-run = "NymWallet"
|
||||
edition = "2021"
|
||||
build = "src/build.rs"
|
||||
rust-version = "1.76"
|
||||
rust-version = "1.85"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "=1.2.1", features = [] }
|
||||
|
||||
tauri-codegen = "=1.2.1"
|
||||
tauri-macros = "=1.2.1"
|
||||
tauri-build = { version = "2.1.1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.68"
|
||||
tauri-plugin-updater = "2.7.0"
|
||||
tauri-plugin-clipboard-manager = "2.0.0"
|
||||
tauri-plugin-shell = "2.2.1"
|
||||
tauri-plugin-process = "2.2.1"
|
||||
tauri-plugin-opener = "2.2.6"
|
||||
bip39 = { version = "2.0.0", features = ["zeroize", "rand"] }
|
||||
cfg-if = "1.0.0"
|
||||
colored = "2.0"
|
||||
@@ -31,6 +33,7 @@ futures = "0.3.15"
|
||||
itertools = "0.10"
|
||||
log = { version = "0.4", features = ["serde"] }
|
||||
once_cell = "1.7.2"
|
||||
open = "5.3.2"
|
||||
pretty_env_logger = "0.4"
|
||||
reqwest = { version = "0.12.4", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -38,11 +41,11 @@ serde_json = "1.0"
|
||||
serde_repr = "0.1"
|
||||
strum = { version = "0.23", features = ["derive"] }
|
||||
tap = "1"
|
||||
tauri = { version = "=1.2.3", features = ["clipboard-all", "shell-open", "updater", "window-maximize", "window-print"] }
|
||||
tauri = { version = "2", features = [] }
|
||||
#tendermint-rpc = "0.23.0"
|
||||
time = { version = "0.3.30", features = ["local-offset"] }
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.10", features = ["full"] }
|
||||
tokio = { version = "1.44", features = ["full"] }
|
||||
toml = "0.5.8"
|
||||
url = "2.2"
|
||||
k256 = { version = "0.13", features = ["ecdsa", "sha256"] }
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "main-capability",
|
||||
"description": "Default capability for Nym Wallet main window",
|
||||
"windows": [
|
||||
"main",
|
||||
"nymWalletApp",
|
||||
"log"
|
||||
],
|
||||
"platforms": [
|
||||
"linux",
|
||||
"macOS",
|
||||
"windows"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"core:path:default",
|
||||
"core:event:default",
|
||||
"core:window:default",
|
||||
"core:app:default",
|
||||
"core:resources:default",
|
||||
"core:menu:default",
|
||||
"core:tray:default",
|
||||
"opener:allow-open-url",
|
||||
"opener:allow-default-urls",
|
||||
"core:window:allow-set-title",
|
||||
"core:app:allow-version",
|
||||
"clipboard-manager:allow-read-text",
|
||||
"clipboard-manager:allow-write-text",
|
||||
"updater:default",
|
||||
"updater:allow-check",
|
||||
"updater:allow-download",
|
||||
"updater:allow-download-and-install",
|
||||
"updater:allow-install",
|
||||
"core:event:allow-listen"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"main-capability":{"identifier":"main-capability","description":"Default capability for Nym Wallet main window","local":true,"windows":["main","nymWalletApp","log"],"permissions":["core:default","core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core:tray:default","opener:allow-open-url","opener:allow-default-urls","core:window:allow-set-title","core:app:allow-version","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","updater:default","updater:allow-check","updater:allow-download","updater:allow-download-and-install","updater:allow-install","core:event:allow-listen"],"platforms":["linux","macOS","windows"]}}
|
||||
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 557 B After Width: | Height: | Size: 1008 B |
@@ -1,24 +0,0 @@
|
||||
# Regenerating icons
|
||||
|
||||
> **Note**: This is likely to be temporary until `tauri icon` is put back into the CLI.
|
||||
|
||||
The Tauri Docs say to use the CLI to generate icons: https://tauri.studio/docs/api/cli/#icon. However `1.0.0-rc.X` appears to not have this command. `1.0.0-beta.6` does 🎉!
|
||||
|
||||
Do the following to regenerate the icons:
|
||||
|
||||
```
|
||||
cd ~
|
||||
git clone nym ...
|
||||
cd nym
|
||||
docker run -v "$(pwd)":/workspace -it node:16 /bin/bash
|
||||
npm i -g @tauri-apps/cli@1.0.0-beta.6
|
||||
cd /workspace/nym-wallet/src-tauri
|
||||
tauri icon /workspace/assets/appicon/appicon.png
|
||||
exit
|
||||
```
|
||||
|
||||
Reasons to use docker:
|
||||
|
||||
- you can't destroy your dev environments `npm` cache
|
||||
- if you mess it up, kill the container, try again
|
||||
- inside the `src-tauri` directory, `node` will resolve to the nearest `node_modules` directory and you'll get the wrong `tauri` cli
|
||||
|
Before Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 502 B |
|
Before Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 701 B |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 4.0 KiB |
|
Before Width: | Height: | Size: 765 B |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 401 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 401 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
@@ -105,7 +105,17 @@ impl NetworkConfig {
|
||||
|
||||
impl Config {
|
||||
fn root_directory() -> PathBuf {
|
||||
tauri::api::path::config_dir().expect("Failed to get config directory")
|
||||
// tauri v1 (via `tauri::api::path::config_dir()`) was internally calling `dirs_next::config_dir()`
|
||||
// which ultimately was getting resolved to
|
||||
// - **Linux:** Resolves to `$XDG_CONFIG_HOME` or `$HOME/.config`.
|
||||
// - **macOS:** Resolves to `$HOME/Library/Application Support`.
|
||||
// - **Windows:** Resolves to `{FOLDERID_RoamingAppData}`.
|
||||
//
|
||||
// tauri v2 calls `dirs::config_dir().ok_or(Error::UnknownPath)` which ultimately does the same thing,
|
||||
// however, it changed its API so that it's called on a `PathResolver`.
|
||||
// but, to instantiate one here would be a hassle as we don't need those specific functionalities,
|
||||
// so let's just recreate tauri's behaviour
|
||||
dirs::config_dir().expect("Failed to get config directory")
|
||||
}
|
||||
|
||||
fn config_directory() -> PathBuf {
|
||||
@@ -243,6 +253,7 @@ impl Config {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::to_string_in_format_args)]
|
||||
pub fn set_default_nyxd_url(&mut self, nyxd_url: Url, network: &WalletNetwork) {
|
||||
log::debug!(
|
||||
"set default nyxd URL for {network} {}",
|
||||
@@ -300,7 +311,7 @@ impl Config {
|
||||
self.networks.get(&network.as_key()).and_then(|config| {
|
||||
log::debug!(
|
||||
"get selected nyxd url for {} {:?}",
|
||||
network.to_string(),
|
||||
network,
|
||||
config.selected_nyxd_url,
|
||||
);
|
||||
config.selected_nyxd_url.clone()
|
||||
@@ -311,7 +322,7 @@ impl Config {
|
||||
self.networks.get(&network.as_key()).and_then(|config| {
|
||||
log::debug!(
|
||||
"get default nyxd url for {} {:?}",
|
||||
network.to_string(),
|
||||
network,
|
||||
config.default_nyxd_url,
|
||||
);
|
||||
config.default_nyxd_url.clone()
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::str::FromStr;
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
use serde::Serialize;
|
||||
use serde_repr::{Deserialize_repr, Serialize_repr};
|
||||
use tauri::Manager;
|
||||
use tauri::Emitter;
|
||||
use time::{format_description, OffsetDateTime};
|
||||
|
||||
fn formatted_time() -> String {
|
||||
@@ -61,7 +61,7 @@ pub fn setup_logging(app_handle: tauri::AppHandle) -> Result<(), log::SetLoggerE
|
||||
message: record.args().to_string(),
|
||||
level: record.level().into(),
|
||||
};
|
||||
app_handle.emit_all("log://log", msg).unwrap();
|
||||
app_handle.emit("log://log", msg).unwrap();
|
||||
}));
|
||||
|
||||
base_config
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
windows_subsystem = "windows"
|
||||
)]
|
||||
|
||||
use tauri::{Manager, Menu};
|
||||
|
||||
use nym_mixnet_contract_common::{Gateway, MixNode};
|
||||
use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder};
|
||||
use tauri::Manager;
|
||||
use tauri_plugin_opener::init as init_opener;
|
||||
use tauri_plugin_shell::init as init_shell;
|
||||
use tauri_plugin_updater::Builder as UpdaterBuilder;
|
||||
|
||||
use crate::menu::AddDefaultSubmenus;
|
||||
use crate::menu::SHOW_LOG_WINDOW;
|
||||
use crate::operations::app;
|
||||
use crate::operations::help;
|
||||
use crate::operations::mixnet;
|
||||
@@ -34,8 +37,13 @@ fn main() {
|
||||
|
||||
let context = tauri::generate_context!();
|
||||
tauri::Builder::default()
|
||||
.plugin(init_shell())
|
||||
.plugin(init_opener())
|
||||
.plugin(UpdaterBuilder::new().build())
|
||||
.plugin(tauri_plugin_clipboard_manager::init())
|
||||
.manage(WalletState::default())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
app::link::open_url,
|
||||
app::version::check_version,
|
||||
mixnet::account::add_account_for_password,
|
||||
mixnet::account::archive_wallet_file,
|
||||
@@ -121,7 +129,6 @@ fn main() {
|
||||
nym_api::status::compute_mixnode_reward_estimation,
|
||||
nym_api::status::gateway_core_node_status,
|
||||
nym_api::status::mixnode_core_node_status,
|
||||
nym_api::status::mixnode_inclusion_probability,
|
||||
nym_api::status::mixnode_reward_estimation,
|
||||
nym_api::status::mixnode_stake_saturation,
|
||||
nym_api::status::mixnode_status,
|
||||
@@ -210,13 +217,31 @@ fn main() {
|
||||
app::react::set_react_state,
|
||||
app::react::get_react_state,
|
||||
])
|
||||
.menu(Menu::os_default(&context.package_info().name).add_default_app_submenus())
|
||||
.on_menu_event(|event| {
|
||||
if event.menu_item_id() == menu::SHOW_LOG_WINDOW {
|
||||
let _r = help::log::help_log_toggle_window(event.window().app_handle());
|
||||
.menu(|app| {
|
||||
// Create a menu builder
|
||||
let menu_builder = MenuBuilder::new(app);
|
||||
if ::std::env::var("NYM_WALLET_ENABLE_LOG").is_ok() {
|
||||
let help_text = MenuItemBuilder::with_id(SHOW_LOG_WINDOW, "Show logs")
|
||||
.build(app)
|
||||
.expect("Failed to create menu item");
|
||||
|
||||
let submenu = SubmenuBuilder::new(app, "Help")
|
||||
.items(&[&help_text])
|
||||
.build()
|
||||
.expect("Failed to create help submenu");
|
||||
|
||||
menu_builder.item(&submenu).build()
|
||||
} else {
|
||||
// Build a default menu without the submenu
|
||||
menu_builder.build()
|
||||
}
|
||||
})
|
||||
.setup(|app| Ok(log::setup_logging(app.app_handle())?))
|
||||
.on_menu_event(|app, event| {
|
||||
if event.id() == SHOW_LOG_WINDOW {
|
||||
let _r = help::log::help_log_toggle_window(app.app_handle().clone());
|
||||
}
|
||||
})
|
||||
.setup(|app| Ok(log::setup_logging(app.app_handle().clone())?))
|
||||
.run(context)
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
use tauri::Menu;
|
||||
use tauri::{CustomMenuItem, Submenu};
|
||||
use tauri::menu::Menu;
|
||||
use tauri::menu::{MenuBuilder, MenuItemBuilder, SubmenuBuilder};
|
||||
|
||||
pub const SHOW_LOG_WINDOW: &str = "show_log_window";
|
||||
|
||||
pub trait AddDefaultSubmenus {
|
||||
#[allow(dead_code)]
|
||||
fn add_default_app_submenus(self) -> Self;
|
||||
}
|
||||
|
||||
impl AddDefaultSubmenus for Menu {
|
||||
impl<R: tauri::Runtime> AddDefaultSubmenus for Menu<R> {
|
||||
#[allow(dead_code)]
|
||||
fn add_default_app_submenus(self) -> Self {
|
||||
if ::std::env::var("NYM_WALLET_ENABLE_LOG").is_ok() {
|
||||
let submenu = Submenu::new(
|
||||
"Help",
|
||||
Menu::new().add_item(CustomMenuItem::new(SHOW_LOG_WINDOW, "Show logs")),
|
||||
);
|
||||
return self.add_submenu(submenu);
|
||||
let app_handle = self.app_handle();
|
||||
|
||||
let help_text = MenuItemBuilder::with_id(SHOW_LOG_WINDOW, "Show logs")
|
||||
.build(app_handle)
|
||||
.expect("Failed to create menu item");
|
||||
|
||||
let submenu = SubmenuBuilder::new(app_handle, "Help")
|
||||
.items(&[&help_text])
|
||||
.build()
|
||||
.expect("Failed to create help submenu");
|
||||
|
||||
let menu_builder = MenuBuilder::new(app_handle);
|
||||
|
||||
match menu_builder.item(&submenu).build() {
|
||||
Ok(new_menu) => new_menu,
|
||||
Err(_) => self,
|
||||
}
|
||||
} else {
|
||||
self
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_url(url: String, app_handle: tauri::AppHandle) -> Result<(), String> {
|
||||
println!("Opening URL: {}", url);
|
||||
|
||||
match app_handle.opener().open_url(&url, None::<&str>) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => Err(format!("Failed to open URL: {}", err)),
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod link;
|
||||
pub mod react;
|
||||
pub mod version;
|
||||
pub mod window;
|
||||
|
||||
@@ -3,27 +3,46 @@
|
||||
|
||||
use crate::error::BackendError;
|
||||
use nym_wallet_types::app::AppVersion;
|
||||
use tauri_plugin_updater::UpdaterExt;
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_version(handle: tauri::AppHandle) -> Result<AppVersion, BackendError> {
|
||||
log::info!(">>> Getting app version info");
|
||||
let res = tauri::updater::builder(handle)
|
||||
.check()
|
||||
.await
|
||||
.map(|u| AppVersion {
|
||||
current_version: u.current_version().to_string(),
|
||||
latest_version: u.latest_version().to_owned(),
|
||||
is_update_available: u.is_update_available(),
|
||||
|
||||
let updater = handle.updater().map_err(|e| {
|
||||
log::error!("Failed to get updater: {}", e);
|
||||
BackendError::CheckAppVersionError
|
||||
})?;
|
||||
|
||||
// Then check for updates
|
||||
let update_info = updater.check().await.map_err(|e| {
|
||||
log::error!("An error occurred while checking for app update {}", e);
|
||||
BackendError::CheckAppVersionError
|
||||
})?;
|
||||
|
||||
// Process the result
|
||||
if let Some(update) = update_info {
|
||||
log::debug!(
|
||||
"<<< update available: [true], current version {}, latest version {}",
|
||||
update.current_version,
|
||||
update.version
|
||||
);
|
||||
Ok(AppVersion {
|
||||
current_version: update.current_version.to_string(),
|
||||
latest_version: update.version,
|
||||
is_update_available: true,
|
||||
})
|
||||
.map_err(|e| {
|
||||
log::error!("An error ocurred while checking for app update {}", e);
|
||||
BackendError::CheckAppVersionError
|
||||
})?;
|
||||
log::debug!(
|
||||
"<<< update available: [{}], current version {}, latest version {}",
|
||||
res.is_update_available,
|
||||
res.current_version,
|
||||
res.latest_version
|
||||
);
|
||||
Ok(res)
|
||||
} else {
|
||||
// No update available
|
||||
let current_version = handle.package_info().version.to_string();
|
||||
log::debug!(
|
||||
"<<< update available: [false], current version {}",
|
||||
current_version
|
||||
);
|
||||
Ok(AppVersion {
|
||||
current_version: current_version.clone(),
|
||||
latest_version: current_version,
|
||||
is_update_available: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,30 +26,30 @@ async fn create_window(
|
||||
) -> Result<(), BackendError> {
|
||||
// create the new window first, to stop the app process from exiting
|
||||
log::info!("Creating {} window...", new_window_label);
|
||||
match tauri::WindowBuilder::new(
|
||||
match tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
new_window_label,
|
||||
tauri::WindowUrl::App(new_window_url.into()),
|
||||
tauri::WebviewUrl::App(new_window_url.into()),
|
||||
)
|
||||
.title("Nym Wallet")
|
||||
.build()
|
||||
{
|
||||
Ok(window) => {
|
||||
if let Err(err) = window.set_focus() {
|
||||
log::error!("Unable to focus log window: {err}");
|
||||
log::error!("Unable to focus window: {err}");
|
||||
}
|
||||
if let Err(err) = window.maximize() {
|
||||
log::error!("Could not maximize window: {err}");
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("Unable to create log window: {err}");
|
||||
log::error!("Unable to create window: {err}");
|
||||
return Err(BackendError::NewWindowError);
|
||||
}
|
||||
}
|
||||
|
||||
// close the old window
|
||||
match app_handle.windows().get(try_close_window_label) {
|
||||
match app_handle.get_webview_window(try_close_window_label) {
|
||||
Some(try_close_window) => {
|
||||
if let Err(err) = try_close_window.close() {
|
||||
log::error!("Could not close window: {err}")
|
||||
|
||||
@@ -3,7 +3,7 @@ use tauri::Manager;
|
||||
|
||||
#[tauri::command]
|
||||
pub fn help_log_toggle_window(app_handle: tauri::AppHandle) -> Result<(), BackendError> {
|
||||
if let Some(current_log_window) = app_handle.windows().get("log") {
|
||||
if let Some(current_log_window) = app_handle.get_webview_window("log") {
|
||||
log::info!("Closing log window...");
|
||||
if let Err(err) = current_log_window.close() {
|
||||
log::error!("Unable to close log window: {err}");
|
||||
@@ -12,9 +12,13 @@ pub fn help_log_toggle_window(app_handle: tauri::AppHandle) -> Result<(), Backen
|
||||
}
|
||||
|
||||
log::info!("Creating log window...");
|
||||
match tauri::WindowBuilder::new(&app_handle, "log", tauri::WindowUrl::App("log.html".into()))
|
||||
.title("Nym Wallet Logs")
|
||||
.build()
|
||||
match tauri::WebviewWindowBuilder::new(
|
||||
&app_handle,
|
||||
"log",
|
||||
tauri::WebviewUrl::App("log.html".into()),
|
||||
)
|
||||
.title("Nym Wallet Logs")
|
||||
.build()
|
||||
{
|
||||
Ok(window) => {
|
||||
if let Err(err) = window.set_focus() {
|
||||
|
||||
@@ -407,14 +407,22 @@ pub async fn get_all_mix_delegations(
|
||||
d.height
|
||||
);
|
||||
let timestamp = client
|
||||
.nyxd
|
||||
.get_block_timestamp(Some(d.height as u32))
|
||||
.await
|
||||
.tap_err(|err| {
|
||||
.nyxd
|
||||
.get_block_timestamp(Some(d.height as u32))
|
||||
.await
|
||||
.tap_err(|err| {
|
||||
let error_message = err.to_string();
|
||||
// Check if the error is related to height not being available (pruning)
|
||||
if error_message.contains("height") && error_message.contains("not available") {
|
||||
let str_err = "Due to pruning strategies from validators, please navigate to the Settings tab and change your RPC node for your validator to retrieve your delegations.";
|
||||
log::error!(" <<< {}", str_err);
|
||||
error_strings.push(str_err.to_string());
|
||||
} else {
|
||||
let str_err = format!("Failed to get block timestamp for height = {} for delegation to mix_id = {}. Error: {}", d.height, d.mix_id, err);
|
||||
log::error!(" <<< {}", str_err);
|
||||
error_strings.push(str_err);
|
||||
}).ok();
|
||||
}
|
||||
}).ok();
|
||||
let delegated_on_iso_datetime = timestamp.map(|ts| ts.to_rfc3339());
|
||||
log::trace!(
|
||||
" <<< timestamp = {:?}, delegated_on_iso_datetime = {:?}",
|
||||
|
||||
@@ -32,7 +32,7 @@ pub async fn send(
|
||||
.nyxd
|
||||
.send(&to_address, vec![amount_base], memo, fee)
|
||||
.await?;
|
||||
log::info!("<<< tx hash = {}", raw_res.hash.to_string());
|
||||
log::info!("<<< tx hash = {}", raw_res.hash);
|
||||
let res = SendTxResult::new(
|
||||
raw_res,
|
||||
TransactionDetails::new(amount, from_address, to_address.to_string()),
|
||||
|
||||
@@ -109,18 +109,6 @@ pub async fn mixnode_stake_saturation(
|
||||
.await?)
|
||||
}
|
||||
|
||||
// TODO: fix later (yeah...)
|
||||
#[allow(deprecated)]
|
||||
#[tauri::command]
|
||||
pub async fn mixnode_inclusion_probability(
|
||||
mix_id: NodeId,
|
||||
state: tauri::State<'_, WalletState>,
|
||||
) -> Result<nym_validator_client::models::InclusionProbabilityResponse, BackendError> {
|
||||
Ok(api_client!(state)
|
||||
.get_mixnode_inclusion_probability(mix_id)
|
||||
.await?)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_nymnode_role(
|
||||
node_id: NodeId,
|
||||
|
||||
@@ -40,7 +40,17 @@ pub(crate) const DEFAULT_LOGIN_ID: &str = "default";
|
||||
pub(crate) const DEFAULT_FIRST_ACCOUNT_NAME: &str = "Account 1";
|
||||
|
||||
fn get_storage_directory() -> Result<PathBuf, BackendError> {
|
||||
tauri::api::path::local_data_dir()
|
||||
// tauri v1 (via `tauri::api::path::local_data_dir()`) was internally calling `dirs_next::data_local_dir()`
|
||||
// which ultimately was getting resolved to
|
||||
// - **Linux:** Resolves to `$XDG_DATA_HOME` or `$HOME/.local/share`.
|
||||
// - **macOS:** Resolves to `$HOME/Library/Application Support`.
|
||||
// - **Windows:** Resolves to `{FOLDERID_LocalAppData}`.
|
||||
//
|
||||
// tauri v2 calls `dirs::data_local_dir().ok_or(Error::UnknownPath)` which ultimately does the same thing,
|
||||
// however, it changed its API so that it's called on a `PathResolver`.
|
||||
// but, to instantiate one here would be a hassle as we don't need those specific functionalities,
|
||||
// so let's just recreate tauri's behaviour
|
||||
dirs::data_local_dir()
|
||||
.map(|dir| dir.join(STORAGE_DIR_NAME))
|
||||
.ok_or(BackendError::UnknownStorageDirectory)
|
||||
}
|
||||
|
||||
@@ -1,78 +1,70 @@
|
||||
{
|
||||
"package": {
|
||||
"productName": "nym-wallet",
|
||||
"version": "1.2.17"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
"devPath": "http://localhost:9000",
|
||||
"beforeDevCommand": "",
|
||||
"beforeBuildCommand": ""
|
||||
},
|
||||
"tauri": {
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"identifier": "net.nymtech.wallet",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": [],
|
||||
"externalBin": [],
|
||||
"copyright": "Copyright © 2021-2023 Nym Technologies SA",
|
||||
"category": "Business",
|
||||
"shortDescription": "Nym desktop wallet allows you to manage your NYM tokens",
|
||||
"longDescription": "",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"windows": {
|
||||
"certificateThumbprint": "6DB77B1F529A0804FE0E6843A3EB8A8CECFFD408",
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "http://timestamp.comodoca.com"
|
||||
},
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": [],
|
||||
"externalBin": [],
|
||||
"copyright": "Copyright © 2021-2025 Nym Technologies SA",
|
||||
"category": "Business",
|
||||
"shortDescription": "Nym desktop wallet allows you to manage your NYM tokens",
|
||||
"longDescription": "",
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "15.3.2",
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": "Developer ID Application: Nym Technologies SA (VW5DZLFHM5)",
|
||||
"entitlements": null
|
||||
},
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": []
|
||||
},
|
||||
"macOS": {
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "",
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": "Developer ID Application: Nym Technologies SA (VW5DZLFHM5)",
|
||||
"entitlements": null
|
||||
},
|
||||
"windows": {
|
||||
"certificateThumbprint": "6DB77B1F529A0804FE0E6843A3EB8A8CECFFD408",
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "http://timestamp.comodoca.com"
|
||||
}
|
||||
},
|
||||
"createUpdaterArtifacts": "v1Compatible"
|
||||
},
|
||||
"build": {
|
||||
"beforeBuildCommand": "",
|
||||
"frontendDist": "../dist",
|
||||
"beforeDevCommand": "",
|
||||
"devUrl": "http://localhost:9000"
|
||||
},
|
||||
"productName": "NymWallet",
|
||||
"mainBinaryName": "NymWallet",
|
||||
"version": "1.2.18",
|
||||
"identifier": "net.nymtech.wallet",
|
||||
"plugins": {
|
||||
"updater": {
|
||||
"active": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IENCNzQ2M0E5N0VFODE2NApSV1JrZ2U2WE9rYTNETTg1OTBKdE5uWUEra0hML2syOVUvQ2lxZmFZRzZ1T3NWbGM0eVRzUTVhVwo=",
|
||||
"endpoints": [
|
||||
"https://nymtech.net/.wellknown/wallet/updater.json"
|
||||
]
|
||||
}
|
||||
},
|
||||
"app": {
|
||||
"security": {
|
||||
"capabilities": [
|
||||
"main-capability"
|
||||
],
|
||||
"dialog": true,
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IENCNzQ2M0E5N0VFODE2NApSV1JrZ2U2WE9rYTNETTg1OTBKdE5uWUEra0hML2syOVUvQ2lxZmFZRzZ1T3NWbGM0eVRzUTVhVwo="
|
||||
},
|
||||
"allowlist": {
|
||||
"window": {
|
||||
"maximize": true,
|
||||
"print": true
|
||||
},
|
||||
"clipboard": {
|
||||
"all": true
|
||||
},
|
||||
"shell": {
|
||||
"open": true
|
||||
}
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'; connect-src ipc: http://ipc.localhost"
|
||||
},
|
||||
"windows": [
|
||||
{
|
||||
"title": "Nym Wallet",
|
||||
"width": 1268,
|
||||
"height": 768,
|
||||
"resizable": true
|
||||
"resizable": true,
|
||||
"useHttpsScheme": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": "default-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self' img-src: 'self'"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useContext } from 'react';
|
||||
import EditIcon from '@mui/icons-material/Create';
|
||||
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
|
||||
import {
|
||||
Box,
|
||||
IconButton,
|
||||
@@ -9,6 +10,8 @@ import {
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
Typography,
|
||||
alpha,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { useClipboard } from 'use-clipboard-copy';
|
||||
import { AccountsContext } from 'src/context';
|
||||
@@ -25,29 +28,94 @@ export const AccountItem = ({
|
||||
}) => {
|
||||
const { selectedAccount, setDialogToDisplay, setAccountMnemonic, handleAccountToEdit } = useContext(AccountsContext);
|
||||
const { copy, copied } = useClipboard({ copiedTimeout: 1000 });
|
||||
const theme = useTheme();
|
||||
|
||||
const isSelected = selectedAccount?.id === name;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
disablePadding
|
||||
disableGutters
|
||||
sx={selectedAccount?.id === name ? { bgcolor: 'rgba(33, 208, 115, 0.1)' } : {}}
|
||||
sx={{
|
||||
borderRadius: 2,
|
||||
my: 0.5,
|
||||
mx: 1,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
bgcolor: isSelected
|
||||
? alpha(theme.palette.nym.highlight, theme.palette.mode === 'dark' ? 0.15 : 0.08)
|
||||
: 'transparent',
|
||||
borderLeft: isSelected ? `3px solid ${theme.palette.nym.highlight}` : '3px solid transparent',
|
||||
}}
|
||||
secondaryAction={
|
||||
<IconButton
|
||||
sx={{ mr: 2, color: 'nym.text.dark' }}
|
||||
onClick={() => {
|
||||
handleAccountToEdit(name);
|
||||
setDialogToDisplay('Edit');
|
||||
}}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{isSelected && (
|
||||
<CheckCircleIcon
|
||||
sx={{
|
||||
color: theme.palette.nym.highlight,
|
||||
fontSize: 18,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
sx={{
|
||||
mr: 1.5,
|
||||
color: theme.palette.mode === 'dark' ? 'nym.text.dark' : theme.palette.text.primary,
|
||||
backgroundColor: alpha(theme.palette.text.primary, 0.05),
|
||||
'&:hover': {
|
||||
backgroundColor: alpha(theme.palette.text.primary, 0.1),
|
||||
},
|
||||
width: 30,
|
||||
height: 30,
|
||||
}}
|
||||
onClick={() => {
|
||||
handleAccountToEdit(name);
|
||||
setDialogToDisplay('Edit');
|
||||
}}
|
||||
>
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<ListItemButton disableRipple onClick={onSelectAccount}>
|
||||
<ListItemAvatar sx={{ minWidth: 0, mr: 2 }}>
|
||||
<ListItemButton
|
||||
disableRipple
|
||||
onClick={onSelectAccount}
|
||||
sx={{
|
||||
py: 1,
|
||||
transition: 'background-color 0.2s',
|
||||
'&:hover': {
|
||||
backgroundColor: isSelected
|
||||
? alpha(theme.palette.nym.highlight, theme.palette.mode === 'dark' ? 0.2 : 0.12)
|
||||
: alpha(theme.palette.nym.nymWallet.hover.background, 0.5),
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* Account avatar with box wrapper to apply styling */}
|
||||
<ListItemAvatar
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
mr: 2,
|
||||
'& .MuiAvatar-root': {
|
||||
border: isSelected ? `2px solid ${theme.palette.nym.highlight}` : '2px solid transparent',
|
||||
transition: 'all 0.2s',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<AccountAvatar name={name} />
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={name}
|
||||
primary={
|
||||
<Typography
|
||||
variant="subtitle1"
|
||||
sx={{
|
||||
fontWeight: isSelected ? 600 : 400,
|
||||
color: theme.palette.text.primary,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Typography>
|
||||
}
|
||||
secondary={
|
||||
<Box>
|
||||
<Tooltip title={copied ? 'Copied!' : `Click to copy address ${address}`}>
|
||||
@@ -58,7 +126,18 @@ export const AccountItem = ({
|
||||
e.stopPropagation();
|
||||
copy(address);
|
||||
}}
|
||||
sx={{ '&:hover': { color: 'grey.900' } }}
|
||||
sx={{
|
||||
fontFamily: 'monospace',
|
||||
cursor: 'pointer',
|
||||
color:
|
||||
theme.palette.mode === 'dark'
|
||||
? theme.palette.nym.nymWallet.text.muted
|
||||
: alpha(theme.palette.text.primary, 0.7),
|
||||
'&:hover': {
|
||||
color: theme.palette.text.primary,
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{address}
|
||||
</Typography>
|
||||
@@ -67,7 +146,19 @@ export const AccountItem = ({
|
||||
<Typography
|
||||
variant="body2"
|
||||
component="span"
|
||||
sx={{ textDecoration: 'underline', mb: 0.5, '&:hover': { color: 'primary.main' } }}
|
||||
sx={{
|
||||
textDecoration: 'underline',
|
||||
mb: 0.5,
|
||||
cursor: 'pointer',
|
||||
color:
|
||||
theme.palette.mode === 'dark'
|
||||
? alpha(theme.palette.nym.highlight, 0.9)
|
||||
: theme.palette.nym.highlight,
|
||||
'&:hover': {
|
||||
color: theme.palette.nym.highlight,
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
onClick={(e: React.MouseEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
setDialogToDisplay('Mnemonic');
|
||||
|
||||
@@ -2,7 +2,6 @@ import React, { useContext, useState } from 'react';
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Paper,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
@@ -10,9 +9,11 @@ import {
|
||||
IconButton,
|
||||
Typography,
|
||||
Divider,
|
||||
alpha,
|
||||
useTheme,
|
||||
List,
|
||||
} from '@mui/material';
|
||||
import { Add, ArrowDownwardSharp, Close } from '@mui/icons-material';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Add, ArrowDownwardSharp, Close, SwapHorizOutlined } from '@mui/icons-material';
|
||||
import { AccountsContext } from 'src/context';
|
||||
import { AccountItem } from '../AccountItem';
|
||||
import { ConfirmPasswordModal } from './ConfirmPasswordModal';
|
||||
@@ -52,23 +53,91 @@ export const AccountsModal = () => {
|
||||
open={dialogToDisplay === 'Accounts'}
|
||||
onClose={handleClose}
|
||||
fullWidth
|
||||
maxWidth="sm"
|
||||
PaperProps={{
|
||||
style: { border: `1px solid ${theme.palette.nym.nymWallet.modal.border}` },
|
||||
elevation: 4,
|
||||
sx: {
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
border: `1px solid ${theme.palette.nym.nymWallet.modal.border}`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
maxHeight: '80vh', // Limit maximum height
|
||||
...(theme.palette.mode === 'dark' && {
|
||||
backgroundImage: 'linear-gradient(180deg, rgba(50, 55, 61, 0.8), rgba(36, 43, 45, 0.95))',
|
||||
}),
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Paper>
|
||||
<DialogTitle>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Typography variant="h6">Accounts</Typography>
|
||||
<IconButton onClick={handleClose}>
|
||||
<Close />
|
||||
</IconButton>
|
||||
<DialogTitle sx={{ pb: 1, flexShrink: 0 }}>
|
||||
<Box display="flex" justifyContent="space-between" alignItems="center">
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<SwapHorizOutlined
|
||||
sx={{
|
||||
color: theme.palette.nym.highlight,
|
||||
backgroundColor: alpha(theme.palette.nym.highlight, 0.1),
|
||||
borderRadius: '50%',
|
||||
p: 0.5,
|
||||
fontSize: 24,
|
||||
}}
|
||||
/>
|
||||
<Typography variant="h6" fontWeight={600}>
|
||||
Accounts
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography fontSize="small" sx={{ color: 'grey.600' }}>
|
||||
Switch between accounts
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent sx={{ padding: 0 }}>
|
||||
<IconButton
|
||||
onClick={handleClose}
|
||||
size="small"
|
||||
sx={{
|
||||
backgroundColor: alpha(theme.palette.text.primary, 0.05),
|
||||
'&:hover': {
|
||||
backgroundColor: alpha(theme.palette.text.primary, 0.1),
|
||||
},
|
||||
width: 30,
|
||||
height: 30,
|
||||
}}
|
||||
>
|
||||
<Close fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{
|
||||
color:
|
||||
theme.palette.mode === 'dark'
|
||||
? theme.palette.nym.nymWallet.text.muted
|
||||
: alpha(theme.palette.text.primary, 0.6),
|
||||
pl: 4.5,
|
||||
}}
|
||||
>
|
||||
Switch between accounts
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent
|
||||
sx={{
|
||||
px: 1,
|
||||
pt: 0,
|
||||
flexGrow: 1,
|
||||
overflowY: 'auto', // Enable vertical scrolling
|
||||
minHeight: '100px', // Ensure minimum height for content
|
||||
'&::-webkit-scrollbar': {
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
},
|
||||
'&::-webkit-scrollbar-thumb': {
|
||||
backgroundColor:
|
||||
theme.palette.mode === 'dark'
|
||||
? alpha(theme.palette.nym.nymWallet.background.greyStroke, 0.8)
|
||||
: alpha(theme.palette.nym.nymWallet.background.greyStroke, 0.5),
|
||||
borderRadius: '4px',
|
||||
},
|
||||
'&::-webkit-scrollbar-track': {
|
||||
backgroundColor: 'transparent',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<List sx={{ py: 1 }}>
|
||||
{accounts?.map(({ id, address }) => (
|
||||
<AccountItem
|
||||
name={id}
|
||||
@@ -81,22 +150,64 @@ export const AccountsModal = () => {
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</DialogContent>
|
||||
<Divider variant="middle" sx={{ mt: 3 }} />
|
||||
<DialogActions sx={{ p: 3 }}>
|
||||
<Button startIcon={<ArrowDownwardSharp />} onClick={() => setDialogToDisplay('Import')}>
|
||||
</List>
|
||||
</DialogContent>
|
||||
|
||||
<Box sx={{ flexShrink: 0 }}>
|
||||
<Divider
|
||||
variant="middle"
|
||||
sx={{
|
||||
my: 1.5,
|
||||
opacity: 0.6,
|
||||
}}
|
||||
/>
|
||||
|
||||
<DialogActions
|
||||
sx={{
|
||||
p: 3,
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
startIcon={<ArrowDownwardSharp />}
|
||||
onClick={() => setDialogToDisplay('Import')}
|
||||
sx={{
|
||||
borderRadius: 1.5,
|
||||
transition: 'all 0.2s',
|
||||
px: 2,
|
||||
py: 0.75,
|
||||
color: theme.palette.text.primary,
|
||||
'&:hover': {
|
||||
backgroundColor: alpha(theme.palette.text.primary, 0.05),
|
||||
},
|
||||
}}
|
||||
>
|
||||
Import account
|
||||
</Button>
|
||||
<Button
|
||||
disableElevation
|
||||
variant="contained"
|
||||
startIcon={<Add fontSize="small" />}
|
||||
startIcon={<Add fontSize="medium" />}
|
||||
onClick={() => setDialogToDisplay('Add')}
|
||||
sx={{
|
||||
px: 2,
|
||||
py: 0.75,
|
||||
borderRadius: 1.5,
|
||||
background: theme.palette.nym.nymWallet.gradients.primary || theme.palette.nym.highlight,
|
||||
fontWeight: 600,
|
||||
boxShadow: 'none',
|
||||
transition: 'all 0.2s',
|
||||
'&:hover': {
|
||||
boxShadow:
|
||||
theme.palette.mode === 'dark' ? '0 4px 12px rgba(0, 0, 0, 0.2)' : '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||||
transform: 'translateY(-1px)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
Create account
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Paper>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
import React, { useContext } from 'react';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Box, Stack, Tooltip, Typography } from '@mui/material';
|
||||
import { Box, Stack, Tooltip, Typography, Skeleton } from '@mui/material';
|
||||
import { format } from 'date-fns';
|
||||
import { AppContext } from 'src/context';
|
||||
|
||||
@@ -17,7 +16,10 @@ const Marker: FCWithChildren<{ tooltipText: string; color: string; position: str
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
export const VestingTimeline: FCWithChildren<{ percentageComplete: number }> = ({ percentageComplete }) => {
|
||||
export const VestingTimeline: FCWithChildren<{ percentageComplete: number; isLoading?: boolean }> = ({
|
||||
percentageComplete,
|
||||
isLoading,
|
||||
}) => {
|
||||
const {
|
||||
userBalance: { currentVestingPeriod, vestingAccountInfo },
|
||||
} = useContext(AppContext);
|
||||
@@ -32,34 +34,48 @@ export const VestingTimeline: FCWithChildren<{ percentageComplete: number }> = (
|
||||
return (
|
||||
<Box>
|
||||
<Stack direction="row" gap={1} alignItems="center">
|
||||
<Typography variant="body2">{percentageComplete}%</Typography>
|
||||
<svg width="100%" height="12">
|
||||
<rect y="2" width="100%" height="6" rx="0" fill="#E6E6E6" />
|
||||
<rect y="2" width={`${percentageComplete}%`} height="6" rx="0" fill={theme.palette.success.main} />
|
||||
{vestingAccountInfo?.periods.map((period, i, arr) => (
|
||||
<Marker
|
||||
position={`${calculateMarkerPosition(arr.length, i)}%`}
|
||||
color={
|
||||
Math.ceil(+percentageComplete) >= calculateMarkerPosition(arr.length, i)
|
||||
? theme.palette.success.main
|
||||
: '#B9B9B9'
|
||||
}
|
||||
tooltipText={format(new Date(Number(period.start_time) * 1000), 'HH:mm do MMM yyyy')}
|
||||
key={i}
|
||||
/>
|
||||
))}
|
||||
<Marker
|
||||
position="calc(100% - 4px)"
|
||||
color={percentageComplete === 100 ? theme.palette.success.main : '#B9B9B9'}
|
||||
tooltipText="End of vesting schedule"
|
||||
/>
|
||||
</svg>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Skeleton width={40} />
|
||||
<Skeleton width="100%" height={12} />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Typography variant="body2">{percentageComplete}%</Typography>
|
||||
<svg width="100%" height="12">
|
||||
<rect y="2" width="100%" height="6" rx="0" fill="#E6E6E6" />
|
||||
<rect y="2" width={`${percentageComplete}%`} height="6" rx="0" fill={theme.palette.success.main} />
|
||||
{vestingAccountInfo?.periods.map((period, i, arr) => (
|
||||
<Marker
|
||||
position={`${calculateMarkerPosition(arr.length, i)}%`}
|
||||
color={
|
||||
Math.ceil(+percentageComplete) >= calculateMarkerPosition(arr.length, i)
|
||||
? theme.palette.success.main
|
||||
: '#B9B9B9'
|
||||
}
|
||||
tooltipText={format(new Date(Number(period.start_time) * 1000), 'HH:mm do MMM yyyy')}
|
||||
key={`period-${period.start_time}`}
|
||||
/>
|
||||
))}
|
||||
<Marker
|
||||
position="calc(100% - 4px)"
|
||||
color={percentageComplete === 100 ? theme.palette.success.main : '#B9B9B9'}
|
||||
tooltipText="End of vesting schedule"
|
||||
/>
|
||||
</svg>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
{!!nextPeriod && (
|
||||
{!!nextPeriod && !isLoading && (
|
||||
<Typography variant="caption" sx={{ color: 'nym.text.muted', ml: 6 }}>
|
||||
Next vesting period: {format(new Date(nextPeriod * 1000), 'HH:mm do MMM yyyy')}
|
||||
</Typography>
|
||||
)}
|
||||
{isLoading && (
|
||||
<Box sx={{ ml: 6, mt: 1 }}>
|
||||
<Skeleton width={240} height={14} />
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Card, Stack, Button } from '@mui/material';
|
||||
import { Card, Stack, Button, Skeleton } from '@mui/material';
|
||||
import { ModalListItem } from 'src/components/Modals/ModalListItem';
|
||||
|
||||
export const TokenTransfer = ({
|
||||
@@ -7,20 +7,27 @@ export const TokenTransfer = ({
|
||||
unlockedTokens,
|
||||
unlockedRewards,
|
||||
unlockedTransferable,
|
||||
isLoading,
|
||||
}: {
|
||||
unlockedTokens?: string;
|
||||
unlockedRewards?: string;
|
||||
unlockedTransferable?: string;
|
||||
onTransfer: () => void;
|
||||
isLoading?: boolean;
|
||||
}) => (
|
||||
<Card variant="outlined" sx={{ p: 3, height: '100%' }}>
|
||||
<Stack justifyContent="space-between" sx={{ height: '100%' }}>
|
||||
<Stack gap={1} sx={{ mb: 2 }}>
|
||||
<ModalListItem label="Unlocked tokens" value={unlockedTokens} />
|
||||
<ModalListItem label="Unlocked rewards" value={unlockedRewards} divider />
|
||||
<ModalListItem fontSize={16} label="Transferable tokens" value={unlockedTransferable} fontWeight={600} />
|
||||
<ModalListItem label="Unlocked tokens" value={isLoading ? <Skeleton width={80} /> : unlockedTokens} />
|
||||
<ModalListItem label="Unlocked rewards" value={isLoading ? <Skeleton width={80} /> : unlockedRewards} divider />
|
||||
<ModalListItem
|
||||
fontSize={16}
|
||||
label="Transferable tokens"
|
||||
value={isLoading ? <Skeleton width={100} /> : unlockedTransferable}
|
||||
fontWeight={600}
|
||||
/>
|
||||
</Stack>
|
||||
<Button size="large" fullWidth variant="contained" onClick={onTransfer} disableElevation>
|
||||
<Button size="large" fullWidth variant="contained" onClick={onTransfer} disableElevation disabled={isLoading}>
|
||||
Transfer
|
||||
</Button>
|
||||
</Stack>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Typography,
|
||||
TableCellProps,
|
||||
Card,
|
||||
Skeleton,
|
||||
} from '@mui/material';
|
||||
import { Period } from '@nymproject/types';
|
||||
import { AppContext } from 'src/context';
|
||||
@@ -28,7 +29,7 @@ const vestingPeriod = (current?: Period, original?: number) => {
|
||||
return 'N/A';
|
||||
};
|
||||
|
||||
export const VestingSchedule = () => {
|
||||
export const VestingSchedule = ({ isLoading }: { isLoading?: boolean }) => {
|
||||
const { userBalance, clientDetails } = useContext(AppContext);
|
||||
const [vestedPercentage, setVestedPercentage] = useState(0);
|
||||
|
||||
@@ -73,8 +74,14 @@ export const VestingSchedule = () => {
|
||||
textTransform: 'uppercase',
|
||||
}}
|
||||
>
|
||||
{userBalance.tokenAllocation?.vesting || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
|
||||
{clientDetails?.display_mix_denom.toUpperCase()}
|
||||
{isLoading ? (
|
||||
<Skeleton width={100} />
|
||||
) : (
|
||||
<>
|
||||
{userBalance.tokenAllocation?.vesting || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
|
||||
{clientDetails?.display_mix_denom.toUpperCase()}
|
||||
</>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="left"
|
||||
@@ -83,7 +90,11 @@ export const VestingSchedule = () => {
|
||||
borderBottom: 'none',
|
||||
}}
|
||||
>
|
||||
{vestingPeriod(userBalance.currentVestingPeriod, userBalance.originalVesting?.number_of_periods)}
|
||||
{isLoading ? (
|
||||
<Skeleton width={40} />
|
||||
) : (
|
||||
vestingPeriod(userBalance.currentVestingPeriod, userBalance.originalVesting?.number_of_periods)
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
sx={{
|
||||
@@ -93,8 +104,14 @@ export const VestingSchedule = () => {
|
||||
}}
|
||||
align="right"
|
||||
>
|
||||
{userBalance.tokenAllocation?.vested || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
|
||||
{clientDetails?.display_mix_denom.toUpperCase()}
|
||||
{isLoading ? (
|
||||
<Skeleton width={100} />
|
||||
) : (
|
||||
<>
|
||||
{userBalance.tokenAllocation?.vested || 'n/a'} / {userBalance.originalVesting?.amount.amount}{' '}
|
||||
{clientDetails?.display_mix_denom.toUpperCase()}
|
||||
</>
|
||||
)}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
@@ -103,7 +120,7 @@ export const VestingSchedule = () => {
|
||||
<Typography variant="body2" sx={{ color: 'nym.text.muted', mb: 3 }}>
|
||||
Percentage
|
||||
</Typography>
|
||||
<VestingTimeline percentageComplete={vestedPercentage} />
|
||||
<VestingTimeline percentageComplete={vestedPercentage} isLoading={isLoading} />
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { ConfirmationModal } from 'src/components';
|
||||
|
||||
export type TTransactionDetails = { amount: string; url: string };
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { Box, Button, Typography } from '@mui/material';
|
||||
import { NymCard } from '../NymCard';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Box, Button, Stack, Tooltip, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { urls } from 'src/context';
|
||||
import { NymCard } from 'src/components';
|
||||
import { Network } from 'src/types';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Box, Button, Chip, Stack, Tooltip, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { TauriLink as Link } from 'src/components/TauriLinkWrapper';
|
||||
import { Network } from 'src/types';
|
||||
import { urls } from 'src/context';
|
||||
import { NymCard } from 'src/components';
|
||||
|
||||