Compare commits

...

57 Commits

Author SHA1 Message Date
Tommy Verrall 600bf42a95 conflicts 2025-04-09 12:51:31 +02:00
Tommy Verrall 748e3e4248 fix remaining lint and cargo clippy errors 2025-04-09 12:46:03 +02:00
dependabot[bot] 8cf1b6427a build(deps): bump tokio from 1.44.0 to 1.44.2 in /nym-wallet (#5694) 2025-04-09 12:40:37 +02:00
Tommy Verrall 7a888c6fdf fix wallet ci 2025-04-09 12:17:02 +02:00
Tommy Verrall 9a9bb89d89 fix lint again 2025-04-09 12:14:49 +02:00
Tommy Verrall 4cc14ddcc4 cargo fmt
- hopefully the last
2025-04-09 11:53:47 +02:00
Tommy Verrall 2dbf9d97cb yarn lint fix 2025-04-09 11:47:10 +02:00
Tommy Verrall 91b6f3cc3e paste not working from currency form
- removed shellhelper too
2025-04-09 11:22:09 +02:00
Tommy Verrall 84cccffcbd Fix PR comments
- removed the shell open in favour of the tauri plugin for opening
- cleaned up some code
- added a few packages
2025-04-09 10:27:25 +02:00
Tommy Verrall af16b3f059 first code review comments 2025-04-09 09:12:21 +02:00
Tommy Verrall b1cde0716e Fix delegation list 2025-04-08 20:10:05 +02:00
Tommy Verrall 45bcdb03d8 fix delegations page - after overflow 2025-04-08 19:29:32 +02:00
Tommy Verrall 44682b5ef0 removed duplicates and reverted back to 1.2.18 as a version 2025-04-08 18:46:52 +02:00
Tommy Verrall 51c9b012e2 merge conflicts 2025-04-08 16:50:45 +02:00
Tommy Verrall 50b1175622 Merge branch 'develop' into feature/test-v2 2025-04-08 16:40:00 +02:00
Tommy Verrall 29ee5984fb fix all workflows 2025-04-08 16:21:15 +02:00
Tommy Verrall e542b25ffc bump to version 2.0.0
- it's a big release therefore let's semver it correctly
2025-04-08 16:03:36 +02:00
Tommy Verrall 516d3f04cf No need to publish these to the build server just use the artifacts 2025-04-08 15:57:20 +02:00
Tommy Verrall 08c09781c7 Fixing all yarn lint errors 2025-04-08 14:36:42 +02:00
Tommy Verrall c92de832e4 remove arg 2025-04-08 12:12:13 +02:00
Tommy Verrall d9d62195cb try again 2025-04-08 12:05:28 +02:00
Tommy Verrall da9115d51b format 2025-04-08 11:58:48 +02:00
Tommy Verrall 1367cad99d another attempt 2025-04-08 11:54:47 +02:00
Tommy Verrall 4f6d65ab95 revert previous add more logging 2025-04-08 11:50:27 +02:00
Tommy Verrall 4292d8ac03 update windows build 2025-04-08 11:40:50 +02:00
Tommy Verrall dcb6de2421 tauri path 2025-04-08 11:22:38 +02:00
Tommy Verrall 1f5ed41bb3 correct tauri path 2025-04-08 11:21:53 +02:00
Tommy Verrall 091e98aa74 attempt windows build 2025-04-08 11:14:19 +02:00
Jędrzej Stuczyński 0e38126fc5 Feature/replay protection (#5682)
* remove old packettype + fix: apply routing filter BEFORE delaying

* updated sphinx crate for allow usage of reply tags

* full pipeline for placeholder checking of packet replay

* replay protection with batched insertion

* running background task for clearing/flushing the BF

* allow disabling the replay detection + cleanup

* allow unwrap in bench code
2025-04-08 09:50:25 +01:00
Tommy Verrall ecbe192a88 try 22.04 2025-04-08 10:20:50 +02:00
Tommy Verrall f0ee49788c test old runner first 2025-04-08 10:18:32 +02:00
Tommy Verrall d2ff3cb88d fix app deps 2025-04-08 10:15:27 +02:00
Tommy Verrall 873d15a5e1 update runner platform 2025-04-08 10:13:30 +02:00
Tommy Verrall 53792cc839 Update runner for linux 2025-04-08 10:00:22 +02:00
Tommy Verrall 415ef1bf13 attempt to push to ci 2025-04-08 09:53:35 +02:00
Tommy Verrall a4f6426bf9 Update account display 2025-04-08 09:32:46 +02:00
dependabot[bot] 0870911b3c build(deps): bump tokio from 1.44.1 to 1.44.2 (#5693) 2025-04-08 08:01:40 +02:00
Tommy Verrall 9f23887cc0 Input fields 2025-04-07 20:07:15 +02:00
Tommy Verrall 8ab269fa05 Jazz up receive modal 2025-04-07 17:16:22 +02:00
Tommy Verrall 7b75f22a8e Remove legacy 2025-04-07 15:27:54 +02:00
Tommy Verrall ca0449e03d Init clipboard manager 2025-04-07 14:22:55 +02:00
Tommy Verrall 224e63d275 Rename and update 2025-04-07 11:37:22 +02:00
Tommy Verrall 3d77283056 Add pruning warning errors 2025-04-07 10:29:03 +02:00
Tommy Verrall 7cc473005b More permissions errors
- fix more perm errors
- enabled the version in the wallet
2025-04-07 10:09:47 +02:00
Tommy Verrall f874284850 - Update beyond tauri v2
- use the latest and greatest
- fixed links to use the command shell
- app version changes, need to be fixed to allow the auto updater too work
2025-04-04 18:47:35 +02:00
Tommy Verrall 7b6077ba64 update to log in
- next up fix hyperlinks
2025-04-04 13:56:20 +02:00
Tommy Verrall b4865520a4 Revert "add the base points back in"
This reverts commit 400aa6ba6d.
2025-04-02 15:36:49 +02:00
Tommy Verrall f52ebfb9c3 Merge remote-tracking branch 'origin/feature/test-v2' into feature/test-v2 2025-04-02 15:34:12 +02:00
Tommy Verrall 6ca2a3c539 migrate to v2
- lots to check and do
2025-04-02 15:22:27 +02:00
Tommy Verrall 717c9066d6 Merge remote-tracking branch 'origin/feature/test-v2' into feature/test-v2 2025-04-02 15:18:26 +02:00
Tommy Verrall 2760a17323 add the base points back in
- now i've reverted back to the original two here, as the compiler is failing around `tauri::api::path` however, looking into the new design for the path resolver in tower this tasks, requires pratically changing the whole wallet_strorage and config set up
- it seems pretty straight forward https://v2.tauri.app/start/migrate/from-tauri-1/#migrate-path-to-tauri-manager - however, I would need a second set of eyes on this
2025-04-02 15:18:11 +02:00
Tommy Verrall 4e9f1bc0ed migrate to v2
- lots to check and do
2025-04-02 15:17:44 +02:00
Tommy Verrall d35023d14b Merge remote-tracking branch 'origin/feature/test-v2' into feature/test-v2 2025-04-02 15:14:02 +02:00
Tommy Verrall 400aa6ba6d add the base points back in
- now i've reverted back to the original two here, as the compiler is failing around `tauri::api::path` however, looking into the new design for the path resolver in tower this tasks, requires pratically changing the whole wallet_strorage and config set up
- it seems pretty straight forward https://v2.tauri.app/start/migrate/from-tauri-1/#migrate-path-to-tauri-manager - however, I would need a second set of eyes on this
2025-04-02 15:13:42 +02:00
Tommy Verrall 2ba74ae120 migrate to v2
- lots to check and do
2025-04-02 15:13:42 +02:00
Tommy Verrall 9a4293a5b9 add the base points back in
- now i've reverted back to the original two here, as the compiler is failing around `tauri::api::path` however, looking into the new design for the path resolver in tower this tasks, requires pratically changing the whole wallet_strorage and config set up
- it seems pretty straight forward https://v2.tauri.app/start/migrate/from-tauri-1/#migrate-path-to-tauri-manager - however, I would need a second set of eyes on this
2025-04-02 08:53:40 +02:00
Tommy Verrall cdddb44099 migrate to v2
- lots to check and do
2025-04-01 17:06:21 +02:00
149 changed files with 15315 additions and 4992 deletions
+6 -2
View File
@@ -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
+25 -29
View File
@@ -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
+81 -42
View File
@@ -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
+110 -61
View File
@@ -1,6 +1,12 @@
name: publish-nym-wallet-win11
on:
workflow_dispatch:
inputs:
sign:
description: "Sign this build using SSL.com. Signing is billed per signature so be careful"
required: false
type: boolean
default: true
release:
types: [created]
@@ -18,53 +24,61 @@ jobs:
runs-on: ${{ matrix.platform }}
outputs:
release_id: ${{ steps.create-release.outputs.id }}
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].created_at }}
version: ${{ steps.release-info.outputs.version }}
filename: ${{ steps.release-info.outputs.filename }}
file_hash: ${{ steps.release-info.outputs.file_hash }}
release_tag: ${{ github.ref_name }}
steps:
- name: Clean up first
continue-on-error: true
working-directory: .
run: |
cd ..
del /s /q /A:H nym
rmdir /s /q nym
- uses: actions/checkout@v4
- name: Import signing certificate
env:
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
run: |
New-Item -ItemType directory -Path certificate
Set-Content -Path certificate/tempCert.txt -Value $env:WINDOWS_CERTIFICATE
certutil -decode certificate/tempCert.txt certificate/certificate.pfx
Remove-Item -path certificate -include tempCert.txt
Import-PfxCertificate -FilePath certificate/certificate.pfx -CertStoreLocation Cert:\CurrentUser\My -Password (ConvertTo-SecureString -String $env:WINDOWS_CERTIFICATE_PASSWORD -Force -AsPlainText)
- name: Install Rust stable
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
- name: Setup MSBuild.exe
uses: microsoft/setup-msbuild@v2
- name: Node
uses: actions/setup-node@v4
with:
node-version: 21
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Create env file
uses: timheuer/base64-to-file@v1.2
with:
fileName: '.env'
encodedString: ${{ secrets.WALLET_ADMIN_ADDRESS }}
- name: Install Yarn
run: npm install -g yarn
- name: Download EV CodeSignTool from ssl.com
working-directory: nym-wallet/src-tauri
if: ${{ inputs.sign }}
shell: bash
run: |
curl -L0 https://www.ssl.com/download/codesigntool-for-linux-and-macos/ -o codesigntool.zip
unzip codesigntool.zip
- name: Get EV certificate credential id
working-directory: nym-wallet/src-tauri
if: ${{ inputs.sign }}
id: get_credential_ids
shell: bash
run: |
echo "SSL_COM_CREDENTIAL_ID=$(./CodeSignTool.sh get_credential_ids -username=${{ secrets.SSL_COM_USERNAME }} -password=${{ secrets.SSL_COM_PASSWORD }} | sed -n '1!p' | sed 's/- //')" >> "$GITHUB_OUTPUT"
- name: Add custom sign command to tauri.conf.json
working-directory: nym-wallet/src-tauri
if: ${{ inputs.sign }}
shell: bash
run: |
yq eval --inplace '.bundle.windows +=
{
"signCommand": {
"cmd": "C:\Program Files\Git\bin\bash.EXE",
"args": [
"/c/actions-runner/_work/nym/nym/nym-wallet/src-tauri/CodeSignTool.sh",
"sign",
"-username ${{ secrets.SSL_COM_USERNAME }}",
"-password ${{ secrets.SSL_COM_PASSWORD }}",
"-credential_id ${{ steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}",
"-totp_secret ${{ secrets.SSL_COM_TOTP_SECRET }}",
"-program_name NymWallet",
"-input_file_path",
"%1",
"-override"
]
}
}' tauri.conf.json
- name: Install project dependencies
shell: bash
run: cd .. && yarn --network-timeout 100000
@@ -77,18 +91,50 @@ jobs:
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ENABLE_CODE_SIGNING: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE: ${{ secrets.WINDOWS_CERTIFICATE }}
WINDOWS_CERTIFICATE_PASSWORD: ${{ secrets.WINDOWS_CERTIFICATE_PASSWORD }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
run: yarn build
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
SSL_COM_USERNAME: ${{ inputs.sign && secrets.SSL_COM_USERNAME }}
SSL_COM_PASSWORD: ${{ inputs.sign && secrets.SSL_COM_PASSWORD }}
SSL_COM_CREDENTIAL_ID: ${{ inputs.sign && steps.get_credential_ids.outputs.SSL_COM_CREDENTIAL_ID }}
SSL_COM_TOTP_SECRET: ${{ inputs.sign && secrets.SSL_COM_TOTP_SECRET }}
run: |
echo "Starting build process..."
yarn build
- name: Check bundle directory
shell: bash
run: |
echo "Checking bundle directory structure"
# Check standard location
if [ -d "target/release/bundle" ]; then
echo "Found bundle directory at standard location"
ls -la target/release/bundle || echo "Failed to list bundle directory"
fi
# Check src-tauri location
if [ -d "src-tauri/target/release/bundle" ]; then
echo "Found bundle directory in src-tauri"
ls -la src-tauri/target/release/bundle || echo "Failed to list src-tauri bundle directory"
# Use this path for future steps
echo "BUNDLE_PATH=src-tauri/target/release/bundle" >> $GITHUB_ENV
else
echo "Using standard bundle path"
echo "BUNDLE_PATH=target/release/bundle" >> $GITHUB_ENV
fi
# Check for MSI files in any location
find . -name "*.msi" -type f
- name: Upload Artifact
uses: actions/upload-artifact@v4
with:
name: nym-wallet_1.0.0_x64_en-US.msi
path: nym-wallet/target/release/bundle/msi/nym-wallet_1.*.msi
name: nym-wallet.msi
path: |
nym-wallet/${{ env.BUNDLE_PATH }}/msi/*.msi
nym-wallet/${{ env.BUNDLE_PATH }}/*/nym-wallet*.msi
nym-wallet/src-tauri/target/release/bundle/msi/*.msi
retention-days: 30
- id: create-release
@@ -97,25 +143,28 @@ jobs:
if: github.event_name == 'release'
with:
files: |
nym-wallet/target/release/bundle/msi/*.msi
nym-wallet/target/release/bundle/msi/*.msi.zip*
- name: Deploy artifacts to CI www
continue-on-error: true
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
ARGS: "-avzr"
SOURCE: "nym-wallet/target/release/bundle/msi/nym-wallet_1.*.msi"
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/builds/${{ github.ref_name }}/nym-wallet
EXCLUDE: "/dist/, /node_modules/"
nym-wallet/${{ env.BUNDLE_PATH }}/msi/*.msi
nym-wallet/${{ env.BUNDLE_PATH }}/msi/*.msi.zip*
nym-wallet/${{ env.BUNDLE_PATH }}/*/nym-wallet*.msi
nym-wallet/src-tauri/target/release/bundle/msi/*.msi
- name: Find MSI path for deployment
id: find-msi
shell: bash
run: |
MSI_FILE=$(find . -name "*.msi" -type f | head -n 1)
if [ -n "$MSI_FILE" ]; then
echo "Found MSI file: $MSI_FILE"
echo "msi_path=$MSI_FILE" >> $GITHUB_OUTPUT
else
echo "WARNING: No MSI file found for deployment!"
echo "msi_path=${{ env.BUNDLE_PATH }}/msi/nym-wallet*.msi" >> $GITHUB_OUTPUT
fi
push-release-data:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-wallet-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
uses: ./.github/workflows/release-calculate-hash.yml
needs: publish-tauri
with:
release_tag: ${{ github.ref_name }}
secrets: inherit
release_tag: ${{ needs.publish-tauri.outputs.release_tag || github.ref_name }}
secrets: inherit
Generated
+22 -6
View File
@@ -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",
+2 -5
View File
@@ -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),
}
-29
View File
@@ -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
}
}
+17
View File
@@ -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))
-8
View File
@@ -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,
+1 -2
View File
@@ -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" }
-2
View File
@@ -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);
+8
View File
@@ -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.
+192 -91
View File
@@ -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"),
}
}
-8
View File
@@ -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
+8 -3
View File
@@ -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(_))
}
}
+1 -1
View File
@@ -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
+13 -11
View File
@@ -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
+78
View File
@@ -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);
+15
View File
@@ -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,
+1 -1
View File
@@ -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())
+13 -2
View File
@@ -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
}
}
+181 -6
View File
@@ -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(),
}
}
+2
View File
@@ -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;
+41 -340
View File
@@ -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)
File diff suppressed because it is too large Load Diff
+35
View File
@@ -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),
}
}
}
+6
View File
@@ -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]
+5 -2
View File
@@ -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)
}
+1
View File
@@ -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";
+21
View File
@@ -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,
+233 -32
View File
@@ -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 {
+21
View File
@@ -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,
+43 -12
View File
@@ -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
);
}
}
+44
View File
@@ -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))
}
}
+4 -148
View File
@@ -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
+26 -10
View File
@@ -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());
+2765 -1518
View File
File diff suppressed because it is too large Load Diff
+11 -6
View File
@@ -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
}
}
+2
View File
@@ -0,0 +1,2 @@
[capabilities]
shell = { open = true }
+13 -10
View File
@@ -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"
]
}
File diff suppressed because one or more lines are too long
@@ -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"]}}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 557 B

After

Width:  |  Height:  |  Size: 1008 B

-24
View File
@@ -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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 765 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 401 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

+14 -3
View File
@@ -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()
+2 -2
View File
@@ -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
+34 -9
View File
@@ -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");
}
+23 -9
View File
@@ -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,
+11 -1
View File
@@ -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)
}
+52 -60
View File
@@ -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 -1
View File
@@ -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';
@@ -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';

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