Compare commits

..

36 Commits

Author SHA1 Message Date
durch a880b59f03 Force IPv4 2025-09-04 12:47:45 +02:00
import this 4062734a31 [DOCs/operators]: NIP-2 tokenomics update & fix csv2md bug (#6008) 2025-09-04 11:29:44 +02:00
import this ccd8ff26a3 Feature: Delegation program stake checker and adjuster (#5980)
* initialise stake adjustment program

* add readme file with a simple guide

* syntax

* syntax

* FINISHED: faster and returning more data

* change dwl link to develop branch
2025-09-03 16:06:06 +00:00
import this 43d043a9cd Feature: Nym node autorun CLI (#5916)
* initial commit - add prereqs install script

* add env vars prompt

* automate latest binary url env var

* add install node script

* add modes to nym-node install script

* start main cli framework

* adding branch var for easier deployment and testing

* add systemd config

* add proxy and wss setup script

* add landing page stub and fix nginx script

* add nginx setup

* fix typo

* add checks for existing dir and wg prompt

* add nginx commands

* add service file check

* add service file check

* convention alignment

* add checks to nginx setup

* cleanup old code

* add bonding prompt and nym node run fns

* fix syntax

* fix syntax

* fix syntax

* fix syntax

* fix syntax

* fix syntax

* fix syntax

* fix syntax

* add service script to init

* fix syntax

* fix syntax

* add chmod

* fix script logic

* syntax fix

* syntax fix

* silent mode trial

* fix evn prompt script

* make scripts interactive

* indent fix

* correct node-install script

* initial mixnode setup working - gws need more love

* fix bonding function

* syntax fix

* improve run noide as service script

* improve service script

* improve run service fn

* fix logic

* beautify

* beautify

* create run node as service script

* syntax fix

* attempt to resolve memory running out issue

* attempt to resolve memory running out issue

* attempt to resolve memory running out issue

* attempt to resolve memory running out issue

* attempt to resolve memory running out issue

* attempt to resolve memory running out issue

* attempt to resolve memory running out issue

* attempt to resolve memory running out issue

* setting wireguard

* solved memory issues

* rename landing page template

* modify wireguard enabled fn

* layout change

* syntax fix

* modify node setup script

* sync up envs

* return missing function

* fix urls

* fix network manager script execution

* fix wss and nginx

* fix layout

* tweak WG contion

* syntax fix

* add init placeholder

* syntax fix

* redefine wireguard check logic

* check if node exists

* add argparse and dev option

* styling

* add panic

* add error message

* improve logic

* improve logic

* add arg

* add dev arg for all levels

* add confirmation loop

* styling

* fix bonding question

* syntax edit

* syntax edit

* syntax edit

* refactor for already bonded nodes

* add default branch on top and define metavar

* fix node install script

* clean and prepare for review

* indentation fix

* fix nginx setup

* fix nginx setup

* style cleanup

* fix try error logic

* tune --dev option to run before command correctly

* fix y/n convention across the modules

* add explorer URL to the message

* minor layout fixes
2025-09-02 20:34:24 +00:00
Drazen Urch 3d6cf730c2 NS-API: Cast to BIGINT to make i64 work (#6003) 2025-09-02 18:35:25 +01:00
Jędrzej Stuczyński c0f8d98b63 bugfix: return from MixTrafficController if client request channel has closed (#6002) 2025-09-02 10:23:25 +01:00
Jędrzej Stuczyński 91995da4f1 chore: use updated version of simulate endpoint (#5988) 2025-09-02 10:12:52 +01:00
Jędrzej Stuczyński 01fa1df66c feat: shared library for attempting to retrieve update mode attestation (#5954)
* feat: shared library for attempting to retrieve update mode attestation

* clippy

* add nym- prefix to the crate name

* use pure-rust impl for jwt-simple
2025-09-02 09:28:32 +01:00
Jędrzej Stuczyński baddaaac22 feat: nym signers monitor (#5933)
* initialise nym-signers-monitor

* creating nyxd client

* performing checks

* sending notifications on failure

* rate limitting on notifications + clippy
2025-09-02 09:27:09 +01:00
elsirion 2c4b5f168b fix: use WASM compatible time API in client (#5948) 2025-09-02 09:26:06 +01:00
Bogdan-Ștefan Neacşu a557ac22c7 Revert "Create an axum_test client for more integrated unit testing (#5956)" (#5999)
This reverts commit efd61eb47c.
2025-09-01 15:37:10 +03:00
Jędrzej Stuczyński 55ef89178b chore: upgraded syn to 2.0 and removed nym-execute (#5998) 2025-09-01 12:59:13 +01:00
Jędrzej Stuczyński d97be2d8ef bugfix: Recipient deserialisation for deserialisers missing bytes specialisation (#5991)
* bugfix: Recipient deserialisation for deserialisers missing bytes specialisation

for example toml or json will just default to visit_seq ignoring bytes related optimisations

* clippy
2025-09-01 11:30:35 +01:00
Bogdan-Ștefan Neacşu efd61eb47c Create an axum_test client for more integrated unit testing (#5956) 2025-09-01 13:27:06 +03:00
benedetta davico 4a01973b31 Merge pull request #5981 from nymtech/benny/ns-api-ci-fix
Fix the ns api ci workflow
2025-09-01 11:02:21 +02:00
Mark Sinclair 9ad9c3b8e7 Bug fix: NS API monikers (#5990)
* node-status-api: fix missing monikers because of deserialisation issues from unstructured data

* node-status-api: bump version after bug fix monikers

---------

Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com>
2025-09-01 09:48:37 +01:00
dependabot[bot] 6706500132 build(deps): bump pbkdf2 from 3.1.2 to 3.1.3 (#5869)
Bumps [pbkdf2](https://github.com/crypto-browserify/pbkdf2) from 3.1.2 to 3.1.3.
- [Changelog](https://github.com/browserify/pbkdf2/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crypto-browserify/pbkdf2/compare/v3.1.2...v3.1.3)

---
updated-dependencies:
- dependency-name: pbkdf2
  dependency-version: 3.1.3
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 15:52:10 +01:00
dependabot[bot] 33fe059c28 Bump console from 0.15.11 to 0.16.0 (#5931)
Bumps [console](https://github.com/console-rs/console) from 0.15.11 to 0.16.0.
- [Release notes](https://github.com/console-rs/console/releases)
- [Changelog](https://github.com/console-rs/console/blob/main/CHANGELOG.md)
- [Commits](https://github.com/console-rs/console/compare/0.15.11...0.16.0)

---
updated-dependencies:
- dependency-name: console
  dependency-version: 0.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 14:24:03 +01:00
dependabot[bot] d6ed2b770b Bump indicatif from 0.17.11 to 0.18.0 (#5924)
Bumps [indicatif](https://github.com/console-rs/indicatif) from 0.17.11 to 0.18.0.
- [Release notes](https://github.com/console-rs/indicatif/releases)
- [Commits](https://github.com/console-rs/indicatif/compare/0.17.11...0.18.0)

---
updated-dependencies:
- dependency-name: indicatif
  dependency-version: 0.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 14:23:39 +01:00
dependabot[bot] 7c18a3dced Bump mock_instant from 0.5.3 to 0.6.0 (#5930)
Bumps [mock_instant](https://github.com/museun/mock_instant) from 0.5.3 to 0.6.0.
- [Commits](https://github.com/museun/mock_instant/commits/v0.6.0)

---
updated-dependencies:
- dependency-name: mock_instant
  dependency-version: 0.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 14:20:22 +01:00
dependabot[bot] 09475ab4e0 build(deps): bump mikefarah/yq from 4.45.4 to 4.47.1 (#5911)
Bumps [mikefarah/yq](https://github.com/mikefarah/yq) from 4.45.4 to 4.47.1.
- [Release notes](https://github.com/mikefarah/yq/releases)
- [Changelog](https://github.com/mikefarah/yq/blob/master/release_notes.txt)
- [Commits](https://github.com/mikefarah/yq/compare/v4.45.4...v4.47.1)

---
updated-dependencies:
- dependency-name: mikefarah/yq
  dependency-version: 4.47.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 13:36:39 +01:00
dependabot[bot] b7606cd2ef Bump defguard_wireguard_rs from v0.4.7 to v0.7.5 (#5928)
Bumps [defguard_wireguard_rs](https://github.com/DefGuard/wireguard-rs) from v0.4.7 to v0.7.5.
- [Release notes](https://github.com/DefGuard/wireguard-rs/releases)
- [Commits](https://github.com/DefGuard/wireguard-rs/compare/ef1cf3714629bf5016fb38cbb7320451dc69fb09...d090d2249e5bb3d4154f07de098387e2ab69bfdc)

---
updated-dependencies:
- dependency-name: defguard_wireguard_rs
  dependency-version: d090d2249e5bb3d4154f07de098387e2ab69bfdc
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 13:35:42 +01:00
dependabot[bot] 006a57312d Bump tokio from 1.46.1 to 1.47.1 (#5929)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.46.1 to 1.47.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.46.1...tokio-1.47.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.47.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 13:35:18 +01:00
dependabot[bot] 9b5aded8a5 build(deps): bump actions/download-artifact from 4 to 5 (#5939)
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 13:33:31 +01:00
dependabot[bot] f4a69636fe build(deps): bump actions/first-interaction from 1 to 3 (#5950)
Bumps [actions/first-interaction](https://github.com/actions/first-interaction) from 1 to 3.
- [Release notes](https://github.com/actions/first-interaction/releases)
- [Commits](https://github.com/actions/first-interaction/compare/v1...v3)

---
updated-dependencies:
- dependency-name: actions/first-interaction
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 13:31:29 +01:00
dependabot[bot] 0463d88646 Bump slab from 0.4.10 to 0.4.11 (#5952)
Bumps [slab](https://github.com/tokio-rs/slab) from 0.4.10 to 0.4.11.
- [Release notes](https://github.com/tokio-rs/slab/releases)
- [Changelog](https://github.com/tokio-rs/slab/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/slab/compare/v0.4.10...v0.4.11)

---
updated-dependencies:
- dependency-name: slab
  dependency-version: 0.4.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 13:31:02 +01:00
dependabot[bot] 534bf5d824 build(deps): bump actions/setup-java from 4 to 5 (#5975)
Bumps [actions/setup-java](https://github.com/actions/setup-java) from 4 to 5.
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-java
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 13:30:00 +01:00
dependabot[bot] 34684b14db Bump sha.js from 2.4.11 to 2.4.12 (#5983)
Bumps [sha.js](https://github.com/crypto-browserify/sha.js) from 2.4.11 to 2.4.12.
- [Changelog](https://github.com/browserify/sha.js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crypto-browserify/sha.js/compare/v2.4.11...v2.4.12)

---
updated-dependencies:
- dependency-name: sha.js
  dependency-version: 2.4.12
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-29 13:29:18 +01:00
Jędrzej Stuczyński b2266d04ef chore: internal hidden command to force advance nyx epoch (#5964) 2025-08-29 11:41:22 +01:00
Jędrzej Stuczyński 911b365609 chore: purge temp databases on build (#5984)
* purge any temp databases on build

* updated min rust version

* fixed clippy::manual_abs_diff' in verloc due to updated msrv

* wasm
2025-08-29 11:41:08 +01:00
Jędrzej Stuczyński e9acc014ed feat: credential proxy deposit pool (#5945)
* chore: rename VpnApiError to CredentialProxyError

* reorganise deposit flow

* updated sql tables et al.

* insert information about deposit usage failure

* remove old deposit maker

* nym credential proxy to monitor quorum state to stop issuance if it'd fail

* clippy

* target lock new modules

* windows clippy

* renamed migration file due to rebasing
2025-08-29 09:39:57 +01:00
Jędrzej Stuczyński 0f66e5a154 bugfix: make sure tables are removed in correct order to not trigger FK constraint issue (#5987) 2025-08-29 09:03:22 +01:00
Bogdan-Ștefan Neacşu f8337d9b38 Wireguard metadata client library (#5943) 2025-08-28 15:43:46 +03:00
Bogdan-Ștefan Neacşu 4fb252c44b Wireguard private metadata (#5915) 2025-08-28 15:14:52 +03:00
benedettadavico 66d0296f47 update dockerfile to pg 2025-08-26 16:02:11 +02:00
benedettadavico 03bbbf44e9 ns api ci fix 2025-08-26 16:02:11 +02:00
146 changed files with 5408 additions and 1571 deletions
@@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@v4
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
uses: mikefarah/yq@v4.47.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -16,7 +16,7 @@ jobs:
uses: actions/checkout@v4
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
uses: mikefarah/yq@v4.47.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
greeting:
runs-on: ubuntu-latest
steps:
- uses: actions/first-interaction@v1
- uses: actions/first-interaction@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
issue-message: 'Thank you for raising this issue'
+1 -1
View File
@@ -31,7 +31,7 @@ jobs:
- name: Check out repository code
uses: actions/checkout@v4
- name: Download report from previous job
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: report
path: .github/workflows/support-files/notifications
+5 -1
View File
@@ -30,11 +30,13 @@ jobs:
release_date: ${{ fromJSON(steps.create-release.outputs.assets)[0].published_at }}
client_hash: ${{ steps.binary-hashes.outputs.client_hash }}
nymvisor_hash: ${{ steps.binary-hashes.outputs.nymvisor_hash }}
nymnode_hash: ${{ steps.binary-hashes.outputs.nymnode_hash }}
socks5_hash: ${{ steps.binary-hashes.outputs.socks5_hash }}
netreq_hash: ${{ steps.binary-hashes.outputs.netreq_hash }}
cli_hash: ${{ steps.binary-hashes.outputs.cli_hash }}
client_version: ${{ steps.binary-versions.outputs.client_version }}
nymvisor_version: ${{ steps.binary-versions.outputs.nymvisor_version }}
nymnode_version: ${{ steps.binary-versions.outputs.nymnode_version }}
socks5_version: ${{ steps.binary-versions.outputs.socks5_version }}
netreq_version: ${{ steps.binary-versions.outputs.netreq_version }}
cli_version: ${{ steps.binary-versions.outputs.cli_version }}
@@ -54,7 +56,7 @@ jobs:
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: 1.88.0
toolchain: 1.86.0
override: true
- name: Build all binaries
@@ -74,6 +76,7 @@ jobs:
target/release/nym-network-requester
target/release/nym-cli
target/release/nymvisor
target/release/nym-node
retention-days: 30
- id: create-release
@@ -88,6 +91,7 @@ jobs:
target/release/nym-network-requester
target/release/nym-cli
target/release/nymvisor
target/release/nym-node
push-release-data-client:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
@@ -25,7 +25,7 @@ jobs:
uses: actions/checkout@v4
- name: Install Java
uses: actions/setup-java@v4
uses: actions/setup-java@v5
with:
distribution: "temurin"
java-version: "17"
@@ -91,7 +91,7 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
- name: Download binary artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v5
with:
name: nyms5-apk-arch64
path: apk
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
uses: mikefarah/yq@v4.47.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
uses: mikefarah/yq@v4.47.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
uses: mikefarah/yq@v4.47.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-network-monitor/Cargo.toml
+9 -5
View File
@@ -34,18 +34,22 @@ jobs:
- name: Get version from cargo.toml
id: get_version
run: |
yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
VERSION=$(yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml)
echo "result=$VERSION" >> $GITHUB_OUTPUT
- name: Set GIT_TAG variable
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
- name: Set RELEASE_TAG variable
- name: Initialise RELEASE_TAG
run: echo "RELEASE_TAG=" >> $GITHUB_ENV
- name: Set RELEASE_TAG for release
if: github.event.inputs.release_image == 'true'
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
- name: Set IMAGE_NAME_AND_TAGS variable
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
- name: New env vars
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
@@ -65,6 +69,6 @@ jobs:
- name: BuildAndPushImageOnHarbor
run: |
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile-pg . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
uses: mikefarah/yq@v4.47.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-api/Cargo.toml
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
uses: mikefarah/yq@v4.47.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
uses: mikefarah/yq@v4.47.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
uses: mikefarah/yq@v4.47.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
@@ -26,7 +26,7 @@ jobs:
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
uses: mikefarah/yq@v4.47.1
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
-42
View File
@@ -4,48 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.16-halloumi] (2025-09-16)
- Backport metadata endpoint ([#6010])
- bugfix: make sure tables are removed in correct order to not trigger FK constraint issue ([#5987])
- chore: move authenticator into gateway crate ([#5982])
- Fix the ns api ci workflow ([#5981])
- Remove freshness check on testrun submit ([#5977])
- Update sysinfo to the latest ([#5976])
- bugfix: manually calculate per node work on rewarded set changes ([#5972])
- fixing the ci for ns agent ([#5965])
- Feature/testing utils ([#5963])
- bugfix: fix ci-build for linux (and use updated runner) ([#5958])
- chore: updated refs to cheddar rev of nym repo ([#5955])
- http api client adjustment ([#5953])
- chore: fix rust 1.89 clippy issues ([#5944])
- Wireguard metadata client library ([#5943])
- chore: remove unused import ([#5942])
- feat: introduce additional checks when attempting to send to bounded channels ([#5941])
- Move credential verifier in peer controller ([#5938])
- change PK/FK on expiration date signatures tables ([#5934])
- Wireguard private metadata ([#5915])
[#6010]: https://github.com/nymtech/nym/pull/6010
[#5987]: https://github.com/nymtech/nym/pull/5987
[#5982]: https://github.com/nymtech/nym/pull/5982
[#5981]: https://github.com/nymtech/nym/pull/5981
[#5977]: https://github.com/nymtech/nym/pull/5977
[#5976]: https://github.com/nymtech/nym/pull/5976
[#5972]: https://github.com/nymtech/nym/pull/5972
[#5965]: https://github.com/nymtech/nym/pull/5965
[#5963]: https://github.com/nymtech/nym/pull/5963
[#5958]: https://github.com/nymtech/nym/pull/5958
[#5955]: https://github.com/nymtech/nym/pull/5955
[#5953]: https://github.com/nymtech/nym/pull/5953
[#5944]: https://github.com/nymtech/nym/pull/5944
[#5943]: https://github.com/nymtech/nym/pull/5943
[#5942]: https://github.com/nymtech/nym/pull/5942
[#5941]: https://github.com/nymtech/nym/pull/5941
[#5938]: https://github.com/nymtech/nym/pull/5938
[#5934]: https://github.com/nymtech/nym/pull/5934
[#5915]: https://github.com/nymtech/nym/pull/5915
## [2025.15-gruyere] (2025-08-20)
- Migrate strum to 0.27.2 ([#5960])
Generated
+235 -77
View File
@@ -847,6 +847,12 @@ dependencies = [
"serde",
]
[[package]]
name = "binstring"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0669d5a35b64fdb5ab7fb19cae13148b6b5cbdf4b8247faf54ece47f699c8cef"
[[package]]
name = "bip32"
version = "0.5.3"
@@ -942,6 +948,17 @@ dependencies = [
"digest 0.10.7",
]
[[package]]
name = "blake2b_simd"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06e903a20b159e944f91ec8499fe1e55651480c541ea0a584f5d967c49ad9d99"
dependencies = [
"arrayref",
"arrayvec",
"constant_time_eq",
]
[[package]]
name = "blake3"
version = "1.8.2"
@@ -1332,6 +1349,17 @@ version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
[[package]]
name = "coarsetime"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91849686042de1b41cd81490edc83afbcb0abe5a9b6f2c4114f23ce8cca1bcf4"
dependencies = [
"libc",
"wasix",
"wasm-bindgen",
]
[[package]]
name = "colorchoice"
version = "1.0.4"
@@ -1345,7 +1373,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [
"lazy_static",
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -1368,19 +1396,6 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "console"
version = "0.15.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
dependencies = [
"encode_unicode",
"libc",
"once_cell",
"unicode-width 0.2.1",
"windows-sys 0.59.0",
]
[[package]]
name = "console"
version = "0.16.0"
@@ -1868,6 +1883,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "ct-codecs"
version = "1.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8"
[[package]]
name = "ctr"
version = "0.9.2"
@@ -1898,7 +1919,7 @@ dependencies = [
"openssl-probe",
"openssl-sys",
"schannel",
"socket2",
"socket2 0.5.10",
"windows-sys 0.59.0",
]
@@ -2370,7 +2391,7 @@ version = "0.1.0"
dependencies = [
"cosmwasm-std",
"quote",
"syn 1.0.109",
"syn 2.0.104",
]
[[package]]
@@ -2424,6 +2445,16 @@ dependencies = [
"signature",
]
[[package]]
name = "ed25519-compact"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9b3460f44bea8cd47f45a0c70892f1eff856d97cd55358b2f73f663789f6190"
dependencies = [
"ct-codecs",
"getrandom 0.2.16",
]
[[package]]
name = "ed25519-consensus"
version = "2.1.0"
@@ -2488,6 +2519,8 @@ dependencies = [
"ff",
"generic-array 0.14.7",
"group",
"hkdf",
"pem-rfc7468",
"pkcs8",
"rand_core 0.6.4",
"sec1",
@@ -3349,6 +3382,30 @@ dependencies = [
"digest 0.10.7",
]
[[package]]
name = "hmac-sha1-compact"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18492c9f6f9a560e0d346369b665ad2bdbc89fa9bceca75796584e79042694c3"
[[package]]
name = "hmac-sha256"
version = "1.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad6880c8d4a9ebf39c6e8b77007ce223f646a4d21ce29d99f70cb16420545425"
dependencies = [
"digest 0.10.7",
]
[[package]]
name = "hmac-sha512"
version = "1.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e89e8d20b3799fa526152a5301a771eaaad80857f83e01b23216ceaafb2d9280"
dependencies = [
"digest 0.10.7",
]
[[package]]
name = "home"
version = "0.5.11"
@@ -3492,7 +3549,7 @@ dependencies = [
"httpdate",
"itoa",
"pin-project-lite",
"socket2",
"socket2 0.5.10",
"tokio",
"tower-service",
"tracing",
@@ -3582,7 +3639,7 @@ dependencies = [
"libc",
"percent-encoding",
"pin-project-lite",
"socket2",
"socket2 0.5.10",
"tokio",
"tower-service",
"tracing",
@@ -3827,26 +3884,13 @@ dependencies = [
"serde",
]
[[package]]
name = "indicatif"
version = "0.17.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235"
dependencies = [
"console 0.15.11",
"number_prefix",
"portable-atomic",
"unicode-width 0.2.1",
"web-time",
]
[[package]]
name = "indicatif"
version = "0.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70a646d946d06bedbbc4cac4c218acf4bbf2d87757a784857025f4d447e4e1cd"
dependencies = [
"console 0.16.0",
"console",
"portable-atomic",
"unicode-width 0.2.1",
"unit-prefix",
@@ -3947,7 +3991,7 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f"
dependencies = [
"socket2",
"socket2 0.5.10",
"widestring",
"windows-sys 0.48.0",
"winreg",
@@ -3986,7 +4030,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi",
"libc",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -4088,6 +4132,32 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "jwt-simple"
version = "0.12.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "731011e9647a71ff4f8474176ff6ce6e0d2de87a0173f15613af3a84c3e3401a"
dependencies = [
"anyhow",
"binstring",
"blake2b_simd",
"coarsetime",
"ct-codecs",
"ed25519-compact",
"hmac-sha1-compact",
"hmac-sha256",
"hmac-sha512",
"k256",
"p256",
"p384",
"rand 0.8.5",
"serde",
"serde_json",
"superboring",
"thiserror 2.0.12",
"zeroize",
]
[[package]]
name = "k256"
version = "0.13.4"
@@ -4519,9 +4589,9 @@ dependencies = [
[[package]]
name = "mock_instant"
version = "0.5.3"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e1d4c44418358edcac6e1d9ce59cea7fb38052429c7704033f1196f0c179e6a"
checksum = "dce6dd36094cac388f119d2e9dc82dc730ef91c32a6222170d630e5414b956e6"
[[package]]
name = "moka"
@@ -4817,15 +4887,9 @@ dependencies = [
"libc",
]
[[package]]
name = "number_prefix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]]
name = "nym-api"
version = "1.1.65"
version = "1.1.64"
dependencies = [
"anyhow",
"async-trait",
@@ -5016,7 +5080,7 @@ dependencies = [
[[package]]
name = "nym-cli"
version = "1.1.62"
version = "1.1.61"
dependencies = [
"anyhow",
"base64 0.22.1",
@@ -5098,7 +5162,7 @@ dependencies = [
[[package]]
name = "nym-client"
version = "1.1.62"
version = "1.1.61"
dependencies = [
"bs58",
"clap",
@@ -5211,6 +5275,7 @@ dependencies = [
name = "nym-client-core-gateways-storage"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"cosmrs",
"nym-crypto",
@@ -5229,6 +5294,7 @@ dependencies = [
name = "nym-client-core-surb-storage"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"dashmap",
"nym-crypto",
@@ -5371,7 +5437,7 @@ dependencies = [
[[package]]
name = "nym-credential-proxy"
version = "0.1.7"
version = "0.1.8"
dependencies = [
"anyhow",
"axum 0.7.9",
@@ -5389,6 +5455,7 @@ dependencies = [
"nym-credentials-interface",
"nym-crypto",
"nym-ecash-contract-common",
"nym-ecash-signer-check",
"nym-http-api-common",
"nym-network-defaults",
"nym-validator-client",
@@ -5439,6 +5506,7 @@ dependencies = [
name = "nym-credential-storage"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"bincode",
"log",
@@ -5553,6 +5621,7 @@ dependencies = [
"generic-array 0.14.7",
"hkdf",
"hmac",
"jwt-simple",
"nym-pemstore",
"nym-sphinx-types",
"rand 0.8.5",
@@ -5641,14 +5710,6 @@ dependencies = [
"time",
]
[[package]]
name = "nym-execute"
version = "0.1.0"
dependencies = [
"quote",
"syn 1.0.109",
]
[[package]]
name = "nym-exit-policy"
version = "0.1.0"
@@ -5808,6 +5869,7 @@ dependencies = [
name = "nym-gateway-stats-storage"
version = "0.1.0"
dependencies = [
"anyhow",
"nym-node-metrics",
"nym-sphinx",
"nym-statistics-common",
@@ -5823,6 +5885,7 @@ dependencies = [
name = "nym-gateway-storage"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"bincode",
"defguard_wireguard_rs",
@@ -6160,7 +6223,7 @@ dependencies = [
[[package]]
name = "nym-network-requester"
version = "1.1.63"
version = "1.1.62"
dependencies = [
"addr",
"anyhow",
@@ -6210,7 +6273,7 @@ dependencies = [
[[package]]
name = "nym-node"
version = "1.18.0"
version = "1.17.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -6235,7 +6298,7 @@ dependencies = [
"hkdf",
"human-repr",
"humantime-serde",
"indicatif 0.17.11",
"indicatif",
"ipnetwork",
"lioness",
"nym-bin-common",
@@ -6351,7 +6414,7 @@ dependencies = [
[[package]]
name = "nym-node-status-api"
version = "3.3.0"
version = "3.3.1"
dependencies = [
"ammonia",
"anyhow",
@@ -6673,9 +6736,29 @@ dependencies = [
"tokio",
]
[[package]]
name = "nym-signers-monitor"
version = "0.1.0"
dependencies = [
"anyhow",
"clap",
"humantime",
"itertools 0.14.0",
"nym-bin-common",
"nym-ecash-signer-check",
"nym-network-defaults",
"nym-task",
"nym-validator-client",
"time",
"tokio",
"tracing",
"url",
"zulip-client",
]
[[package]]
name = "nym-socks5-client"
version = "1.1.62"
version = "1.1.61"
dependencies = [
"bs58",
"clap",
@@ -6816,10 +6899,12 @@ dependencies = [
name = "nym-sphinx-addressing"
version = "0.1.0"
dependencies = [
"bincode",
"nym-crypto",
"nym-sphinx-types",
"rand 0.8.5",
"serde",
"serde_json",
"thiserror 2.0.12",
]
@@ -7109,6 +7194,22 @@ dependencies = [
"x25519-dalek",
]
[[package]]
name = "nym-upgrade-mode-check"
version = "0.1.0"
dependencies = [
"anyhow",
"jwt-simple",
"nym-crypto",
"nym-http-api-client",
"reqwest 0.12.22",
"serde",
"serde_json",
"thiserror 2.0.12",
"time",
"tracing",
]
[[package]]
name = "nym-validator-client"
version = "0.1.0"
@@ -7390,7 +7491,7 @@ dependencies = [
[[package]]
name = "nymvisor"
version = "0.1.27"
version = "0.1.26"
dependencies = [
"anyhow",
"bytes",
@@ -7453,6 +7554,7 @@ dependencies = [
name = "nyxd-scraper"
version = "0.1.0"
dependencies = [
"anyhow",
"async-trait",
"const_format",
"cosmrs",
@@ -7676,6 +7778,18 @@ dependencies = [
"sha2 0.10.9",
]
[[package]]
name = "p384"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6"
dependencies = [
"ecdsa",
"elliptic-curve",
"primeorder",
"sha2 0.10.9",
]
[[package]]
name = "pairing"
version = "0.23.0"
@@ -8244,7 +8358,7 @@ dependencies = [
"quinn-udp",
"rustc-hash",
"rustls 0.23.29",
"socket2",
"socket2 0.5.10",
"thiserror 2.0.12",
"tokio",
"tracing",
@@ -8281,9 +8395,9 @@ dependencies = [
"cfg_aliases",
"libc",
"once_cell",
"socket2",
"socket2 0.5.10",
"tracing",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -8656,6 +8770,7 @@ dependencies = [
"pkcs1",
"pkcs8",
"rand_core 0.6.4",
"sha2 0.10.9",
"signature",
"spki",
"subtle 2.6.1",
@@ -8767,7 +8882,7 @@ dependencies = [
"errno",
"libc",
"linux-raw-sys 0.4.15",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -9489,9 +9604,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
[[package]]
name = "slab"
version = "0.4.10"
version = "0.4.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
[[package]]
name = "sluice"
@@ -9567,6 +9682,16 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "socket2"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807"
dependencies = [
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "sphinx-packet"
version = "0.6.0"
@@ -9935,6 +10060,19 @@ version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142"
[[package]]
name = "superboring"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "515cce34a781d7250b8a65706e0f2a5b99236ea605cb235d4baed6685820478f"
dependencies = [
"getrandom 0.2.16",
"hmac-sha256",
"hmac-sha512",
"rand 0.8.5",
"rsa",
]
[[package]]
name = "syn"
version = "1.0.109"
@@ -10051,7 +10189,7 @@ dependencies = [
"getrandom 0.3.3",
"once_cell",
"rustix 1.0.8",
"windows-sys 0.59.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -10175,11 +10313,11 @@ dependencies = [
"bip39",
"bs58",
"clap",
"console 0.15.11",
"console",
"cw-utils",
"dkg-bypass-contract",
"humantime",
"indicatif 0.17.11",
"indicatif",
"nym-bin-common",
"nym-coconut-dkg-common",
"nym-compact-ecash",
@@ -10359,9 +10497,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.46.1"
version = "1.47.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
dependencies = [
"backtrace",
"bytes",
@@ -10372,10 +10510,10 @@ dependencies = [
"pin-project-lite",
"signal-hook-registry",
"slab",
"socket2",
"socket2 0.6.0",
"tokio-macros",
"tracing",
"windows-sys 0.52.0",
"windows-sys 0.59.0",
]
[[package]]
@@ -10409,7 +10547,7 @@ dependencies = [
"postgres-protocol",
"postgres-types",
"rand 0.9.2",
"socket2",
"socket2 0.5.10",
"tokio",
"tokio-util",
"whoami",
@@ -10585,7 +10723,7 @@ dependencies = [
"percent-encoding",
"pin-project",
"prost",
"socket2",
"socket2 0.5.10",
"tokio",
"tokio-stream",
"tower 0.4.13",
@@ -10736,7 +10874,7 @@ version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c714cc8fc46db04fcfddbd274c6ef59bebb1b435155984e7c6e89c3ce66f200"
dependencies = [
"indicatif 0.18.0",
"indicatif",
"tracing",
"tracing-core",
"tracing-subscriber",
@@ -11432,6 +11570,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasix"
version = "0.12.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1fbb4ef9bbca0c1170e0b00dd28abc9e3b68669821600cad1caaed606583c6d"
dependencies = [
"wasi 0.11.1+wasi-snapshot-preview1",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.100"
@@ -11732,7 +11879,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
"windows-sys 0.48.0",
]
[[package]]
@@ -11825,6 +11972,17 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-registry"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
dependencies = [
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-result"
version = "0.3.4"
+8 -8
View File
@@ -53,7 +53,6 @@ members = [
"common/ecash-signer-check",
"common/ecash-signer-check-types",
"common/ecash-time",
"common/execute",
"common/exit-policy",
"common/gateway-requests",
"common/gateway-stats-storage",
@@ -96,7 +95,7 @@ members = [
"common/ticketbooks-merkle",
"common/topology",
"common/tun",
"common/types",
"common/types", "common/upgrade-mode-check",
"common/verloc",
"common/wasm/client-core",
"common/wasm/storage",
@@ -123,7 +122,7 @@ members = [
"nym-node-status-api/nym-node-status-client",
"nym-node/nym-node-metrics",
"nym-node/nym-node-requests",
"nym-outfox",
"nym-outfox", "nym-signers-monitor",
"nym-statistics-api",
"nym-validator-rewarder",
"nyx-chain-watcher",
@@ -182,7 +181,7 @@ homepage = "https://nymtech.net"
documentation = "https://nymtech.net"
edition = "2021"
license = "Apache-2.0"
rust-version = "1.80"
rust-version = "1.81"
readme = "README.md"
[workspace.dependencies]
@@ -223,7 +222,7 @@ clap_complete = "4.5"
clap_complete_fig = "4.5"
colored = "2.2"
comfy-table = "7.1.4"
console = "0.15.11"
console = "0.16.0"
console-subscriber = "0.4.1"
console_error_panic_hook = "0.1"
const-str = "0.5.6"
@@ -270,11 +269,12 @@ humantime = "2.2.0"
humantime-serde = "1.1.1"
hyper = "1.6.0"
hyper-util = "0.1"
indicatif = "0.17.11"
indicatif = "0.18.0"
inquire = "0.6.2"
ip_network = "0.4.1"
ipnetwork = "0.20"
itertools = "0.14.0"
jwt-simple = { version = "0.12.12", default-features = false, features = ["pure-rust"] }
k256 = "0.13"
lazy_static = "1.5.0"
ledger-transport = "0.10.0"
@@ -326,14 +326,14 @@ sqlx = "0.8.6"
strum = "0.27.2"
strum_macros = "0.27.2"
subtle-encoding = "0.5"
syn = "1"
syn = "2"
sysinfo = "0.37.0"
tap = "1.0.1"
tar = "0.4.44"
tempfile = "3.20"
thiserror = "2.0"
time = "0.3.41"
tokio = "1.45"
tokio = "1.47"
tokio-postgres = "0.7"
tokio-stream = "0.1.17"
tokio-test = "0.4.4"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.62"
version = "1.1.61"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.62"
version = "1.1.61"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
@@ -3,6 +3,7 @@ name = "nym-client-core-gateways-storage"
version = "0.1.0"
edition = "2021"
license.workspace = true
rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -26,6 +27,7 @@ features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"]
optional = true
[build-dependencies]
anyhow = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
+13 -4
View File
@@ -2,23 +2,30 @@
// SPDX-License-Identifier: Apache-2.0
#[tokio::main]
async fn main() {
async fn main() -> anyhow::Result<()> {
#[cfg(feature = "fs-gateways-storage")]
{
use anyhow::Context;
use sqlx::{Connection, SqliteConnection};
use std::env;
let out_dir = env::var("OUT_DIR").unwrap();
let out_dir = env::var("OUT_DIR")?;
let database_path = format!("{out_dir}/gateways-storage-example.sqlite");
// remove the db file if it already existed from previous build
// in case it was from a different branch
if std::fs::exists(&database_path)? {
std::fs::remove_file(&database_path)?;
}
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
.await
.expect("Failed to create SQLx database connection");
.context("Failed to create SQLx database connection")?;
sqlx::migrate!("./fs_gateways_migrations")
.run(&mut conn)
.await
.expect("Failed to perform SQLx migrations");
.context("Failed to perform SQLx migrations")?;
#[cfg(target_family = "unix")]
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
@@ -28,4 +35,6 @@ async fn main() {
// not a valid windows path... but hey, it works...
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
}
Ok(())
}
@@ -175,6 +175,7 @@ impl MixTrafficController {
},
None => {
tracing::trace!("MixTrafficController, client request channel closed");
break
}
},
}
@@ -1,6 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::client::helpers::get_time_now;
use crate::client::replies::{
reply_controller::ReplyControllerSender, reply_storage::SentReplyKeys,
};
@@ -22,7 +23,7 @@ use nym_statistics_common::clients::{packet_statistics::PacketStatisticsEvent, C
use nym_task::TaskClient;
use std::collections::HashSet;
use std::sync::Arc;
use std::time::{Duration, Instant};
use std::time::Duration;
use tracing::*;
// The interval at which we check for stale buffers
@@ -54,7 +55,7 @@ struct ReceivedMessagesBufferInner<R: MessageReceiver> {
stats_tx: ClientStatsSender,
// Periodically check for stale buffers to clean up
last_stale_check: Instant,
last_stale_check: crate::client::helpers::Instant,
}
impl<R: MessageReceiver> ReceivedMessagesBufferInner<R> {
@@ -154,7 +155,7 @@ impl<R: MessageReceiver> ReceivedMessagesBufferInner<R> {
}
fn cleanup_stale_buffers(&mut self) {
let now = Instant::now();
let now = get_time_now();
if now - self.last_stale_check > STALE_BUFFER_CHECK_INTERVAL {
self.last_stale_check = now;
self.message_receiver
@@ -190,7 +191,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
message_sender: None,
recently_reconstructed: HashSet::new(),
stats_tx,
last_stale_check: Instant::now(),
last_stale_check: get_time_now(),
})),
reply_key_storage,
reply_controller_sender,
@@ -30,6 +30,7 @@ optional = true
path = "../../../sqlx-pool-guard"
[build-dependencies]
anyhow = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
+7 -4
View File
@@ -2,23 +2,24 @@
// SPDX-License-Identifier: Apache-2.0
#[tokio::main]
async fn main() {
async fn main() -> anyhow::Result<()> {
#[cfg(feature = "fs-surb-storage")]
{
use anyhow::Context;
use sqlx::{Connection, SqliteConnection};
use std::env;
let out_dir = env::var("OUT_DIR").unwrap();
let out_dir = env::var("OUT_DIR")?;
let database_path = format!("{out_dir}/fs-surbs-example.sqlite");
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
.await
.expect("Failed to create SQLx database connection");
.context("Failed to create SQLx database connection")?;
sqlx::migrate!("./fs_surbs_migrations")
.run(&mut conn)
.await
.expect("Failed to perform SQLx migrations");
.context("Failed to perform SQLx migrations")?;
#[cfg(target_family = "unix")]
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
@@ -28,4 +29,6 @@ async fn main() {
// not a valid windows path... but hey, it works...
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
}
Ok(())
}
@@ -28,7 +28,7 @@ use cosmrs::proto::cosmwasm::wasm::v1::{
QueryRawContractStateResponse, QuerySmartContractStateRequest, QuerySmartContractStateResponse,
};
use cosmrs::tendermint::{block, chain, Hash};
use cosmrs::{AccountId, Coin as CosmosCoin, Tx};
use cosmrs::{AccountId, Coin as CosmosCoin};
use prost::Message;
use serde::{Deserialize, Serialize};
@@ -556,23 +556,12 @@ pub trait CosmWasmClient: TendermintRpcClient {
Ok(serde_json::from_slice(&res.data)?)
}
// deprecation warning is due to the fact the protobuf files built were based on cosmos-sdk 0.44,
// where they prefer using tx_bytes directly. However, in 0.42, which we are using at the time
// of writing this, the option does not work
// TODO: we should really stop using the `tx` argument here and use `tx_bytes` exlusively,
// however, at the time of writing this update, while our QA and mainnet networks do support it,
// sandbox is still running old version of wasmd that lacks support for `tx_bytes`
#[allow(deprecated)]
async fn query_simulate(
&self,
tx: Option<Tx>,
tx_bytes: Vec<u8>,
) -> Result<SimulateResponse, NyxdError> {
async fn query_simulate(&self, tx_bytes: Vec<u8>) -> Result<SimulateResponse, NyxdError> {
let path = Some("/cosmos.tx.v1beta1.Service/Simulate".to_owned());
let req = SimulateRequest {
tx: tx.map(Into::into),
tx_bytes,
..Default::default()
};
let res = self
@@ -81,17 +81,14 @@ where
auth_info: single_unspecified_signer_auth(public_key, sequence_response.sequence),
signatures: vec![Vec::new()],
};
self.query_simulate(Some(partial_tx), Vec::new()).await
// for completion sake, once we're able to transition into using `tx_bytes`,
// we might want to use something like this instead:
// let tx_raw: tx::Raw = cosmrs::proto::cosmos::tx::v1beta1::TxRaw {
// body_bytes: partial_tx.body.into_bytes().unwrap(),
// auth_info_bytes: partial_tx.auth_info.into_bytes().unwrap(),
// signatures: partial_tx.signatures,
// }
// .into();
// self.query_simulate(None, tx_raw.to_bytes().unwrap()).await
let tx_raw: tx::Raw = cosmrs::proto::cosmos::tx::v1beta1::TxRaw {
body_bytes: partial_tx.body.into_bytes()?,
auth_info_bytes: partial_tx.auth_info.into_bytes()?,
signatures: partial_tx.signatures,
}
.into();
self.query_simulate(tx_raw.to_bytes()?).await
}
async fn upload(
+3
View File
@@ -4,6 +4,7 @@
use clap::{Args, Subcommand};
pub mod ecash;
pub mod nyx;
#[derive(Debug, Args)]
#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)]
@@ -16,4 +17,6 @@ pub struct Internal {
pub enum InternalCommands {
/// Ecash related internal commands
Ecash(ecash::InternalEcash),
Nyx(nyx::InternalNyx),
}
@@ -0,0 +1,116 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::context::SigningClient;
use anyhow::bail;
use clap::Parser;
use nym_mixnet_contract_common::nym_node::Role;
use nym_mixnet_contract_common::reward_params::NodeRewardingParameters;
use nym_mixnet_contract_common::{
EpochRewardedSet, EpochState, NodeId, RewardingParams, RoleAssignment,
};
use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt;
use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient};
use rand::prelude::*;
use rand::thread_rng;
#[derive(Debug, Parser)]
pub struct Args {}
fn choose_new_nodes(
params: &RewardingParams,
rewarded_set: &EpochRewardedSet,
role: Role,
) -> Vec<NodeId> {
let mut rng = thread_rng();
match role {
Role::EntryGateway => rewarded_set
.assignment
.entry_gateways
.choose_multiple(&mut rng, params.rewarded_set.entry_gateways as usize)
.copied()
.collect(),
Role::Layer1 => rewarded_set
.assignment
.layer1
.choose_multiple(&mut rng, params.rewarded_set.mixnodes as usize / 3)
.copied()
.collect(),
Role::Layer2 => rewarded_set
.assignment
.layer2
.choose_multiple(&mut rng, params.rewarded_set.mixnodes as usize / 3)
.copied()
.collect(),
Role::Layer3 => rewarded_set
.assignment
.layer3
.choose_multiple(&mut rng, params.rewarded_set.mixnodes as usize / 3)
.copied()
.collect(),
Role::ExitGateway => rewarded_set
.assignment
.exit_gateways
.choose_multiple(&mut rng, params.rewarded_set.exit_gateways as usize)
.copied()
.collect(),
Role::Standby => rewarded_set
.assignment
.standby
.choose_multiple(&mut rng, params.rewarded_set.standby as usize)
.copied()
.collect(),
}
}
pub async fn force_advance_epoch(_: Args, client: SigningClient) -> anyhow::Result<()> {
let current_epoch = client.get_current_interval_details().await?;
let epoch_status = client.get_current_epoch_status().await?;
if epoch_status.being_advanced_by.as_str() != client.address().to_string() {
bail!(
"this client is not authorised to perform any epoch operations. we need {}",
client.address()
)
}
let rewarding_params = client.get_rewarding_parameters().await?;
let current_rewarded_set = client.get_rewarded_set().await?;
if !current_epoch.is_current_epoch_over {
println!("the current epoch is not over yet - there's nothing to do")
}
// is this most efficient? no. but it's simple
loop {
let epoch_status = client.get_current_epoch_status().await?;
match epoch_status.state {
EpochState::InProgress => break,
EpochState::Rewarding { final_node_id, .. } => {
println!("rewarding {final_node_id} with big fat 0...");
client
.reward_node(
final_node_id,
NodeRewardingParameters::new(Default::default(), Default::default()),
None,
)
.await?;
}
EpochState::ReconcilingEvents => {
println!("trying to reconcile events...");
client.reconcile_epoch_events(None, None).await?;
}
EpochState::RoleAssignment { next } => {
let nodes = choose_new_nodes(&rewarding_params, &current_rewarded_set, next);
println!("assigning {nodes:?} as {next}");
client
.assign_roles(RoleAssignment { role: next, nodes }, None)
.await?;
}
}
}
Ok(())
}
+19
View File
@@ -0,0 +1,19 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use clap::{Args, Subcommand};
pub mod force_advance_epoch;
#[derive(Debug, Args)]
#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)]
pub struct InternalNyx {
#[clap(subcommand)]
pub command: InternalNyxCommands,
}
#[derive(Debug, Subcommand)]
pub enum InternalNyxCommands {
/// Attempt to force advance the current epoch
ForceAdvanceEpoch(force_advance_epoch::Args),
}
+2
View File
@@ -3,6 +3,7 @@ name = "nym-credential-storage"
version = "0.1.0"
edition = "2021"
license.workspace = true
rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -33,6 +34,7 @@ features = ["rt-multi-thread", "net", "signal", "fs"]
[build-dependencies]
anyhow = { workspace = true }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"sqlite",
+13 -4
View File
@@ -3,22 +3,29 @@
* SPDX-License-Identifier: Apache-2.0
*/
use anyhow::Context;
use sqlx::{Connection, SqliteConnection};
use std::env;
#[tokio::main]
async fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
async fn main() -> anyhow::Result<()> {
let out_dir = env::var("OUT_DIR")?;
let database_path = format!("{out_dir}/coconut-credential-example.sqlite");
// remove the db file if it already existed from previous build
// in case it was from a different branch
if std::fs::exists(&database_path)? {
std::fs::remove_file(&database_path)?;
}
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
.await
.expect("Failed to create SQLx database connection");
.context("Failed to create SQLx database connection")?;
sqlx::migrate!("./migrations")
.run(&mut conn)
.await
.expect("Failed to perform SQLx migrations");
.context("Failed to perform SQLx migrations")?;
#[cfg(target_family = "unix")]
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
@@ -27,4 +34,6 @@ async fn main() {
// for some strange reason we need to add a leading `/` to the windows path even though it's
// not a valid windows path... but hey, it works...
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
Ok(())
}
@@ -108,7 +108,7 @@ impl IssuanceTicketBook {
signing_request.withdrawal_request.clone(),
self.deposit_id,
request_signature,
signing_request.ecash_pub_key.clone(),
signing_request.ecash_pub_key,
signing_request.expiration_date,
signing_request.ticketbook_type,
)
+2
View File
@@ -18,6 +18,7 @@ digest = { workspace = true, optional = true }
generic-array = { workspace = true, optional = true }
hkdf = { workspace = true, optional = true }
hmac = { workspace = true, optional = true }
jwt-simple = { workspace = true, optional = true }
cipher = { workspace = true, optional = true }
x25519-dalek = { workspace = true, features = ["static_secrets"], optional = true }
ed25519-dalek = { workspace = true, features = ["rand_core"], optional = true }
@@ -39,6 +40,7 @@ rand_chacha = { workspace = true }
[features]
default = []
aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"]
naive_jwt = ["asymmetric", "jwt-simple"]
serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"]
asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"]
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array", "sha2"]
+82 -5
View File
@@ -2,8 +2,9 @@
// SPDX-License-Identifier: Apache-2.0
pub use ed25519_dalek::SignatureError;
use ed25519_dalek::{SecretKey, Signer, SigningKey};
pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH};
use ed25519_dalek::Signer;
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use std::fmt::{self, Debug, Display, Formatter};
use std::str::FromStr;
@@ -13,6 +14,9 @@ use zeroize::{Zeroize, ZeroizeOnDrop};
#[cfg(feature = "serde")]
pub mod serde_helpers;
#[cfg(feature = "serde")]
pub use serde_helpers::*;
#[cfg(feature = "sphinx")]
use nym_sphinx_types::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH};
@@ -81,8 +85,8 @@ impl KeyPair {
}
}
pub fn from_secret(secret: SecretKey, index: u32) -> Self {
let ed25519_signing_key = SigningKey::from(secret);
pub fn from_secret(secret: ed25519_dalek::SecretKey, index: u32) -> Self {
let ed25519_signing_key = ed25519_dalek::SigningKey::from(secret);
KeyPair {
private_key: PrivateKey(ed25519_signing_key.to_bytes()),
@@ -276,7 +280,7 @@ impl Display for PrivateKey {
impl<'a> From<&'a PrivateKey> for PublicKey {
fn from(pk: &'a PrivateKey) -> Self {
PublicKey(SigningKey::from_bytes(&pk.0).verifying_key())
PublicKey(ed25519_dalek::SigningKey::from_bytes(&pk.0).verifying_key())
}
}
@@ -320,7 +324,7 @@ impl PrivateKey {
}
pub fn sign<M: AsRef<[u8]>>(&self, message: M) -> Signature {
let signing_key: SigningKey = self.0.into();
let signing_key: ed25519_dalek::SigningKey = self.0.into();
let sig = signing_key.sign(message.as_ref());
Signature(sig)
}
@@ -425,9 +429,57 @@ impl<'d> Deserialize<'d> for Signature {
}
}
#[cfg(feature = "naive_jwt")]
impl PublicKey {
pub fn to_jwt_compatible_key(&self) -> jwt_simple::algorithms::Ed25519PublicKey {
(*self).into()
}
}
#[cfg(feature = "naive_jwt")]
impl From<PublicKey> for jwt_simple::algorithms::Ed25519PublicKey {
fn from(value: PublicKey) -> Self {
// SAFETY: we have a valid ed25519 pubkey, we're just changing to a different library wrapper
#[allow(clippy::unwrap_used)]
jwt_simple::algorithms::Ed25519PublicKey::from_bytes(&value.to_bytes()).unwrap()
}
}
#[cfg(feature = "naive_jwt")]
impl PrivateKey {
pub fn to_jwt_compatible_keys(&self) -> jwt_simple::algorithms::Ed25519KeyPair {
let pub_key = self.public_key();
let mut bytes = zeroize::Zeroizing::new([0u8; 64]);
bytes[..SECRET_KEY_LENGTH]
.copy_from_slice(zeroize::Zeroizing::new(self.to_bytes()).as_ref());
bytes[SECRET_KEY_LENGTH..].copy_from_slice(&pub_key.to_bytes());
// SAFETY: we have a valid ed25519 keys, we're just changing to a different library wrapper
#[allow(clippy::unwrap_used)]
jwt_simple::algorithms::Ed25519KeyPair::from_bytes(bytes.as_ref()).unwrap()
}
}
#[cfg(feature = "naive_jwt")]
impl KeyPair {
pub fn to_jwt_compatible_keys(&self) -> jwt_simple::algorithms::Ed25519KeyPair {
let mut bytes = zeroize::Zeroizing::new([0u8; 64]);
bytes[..SECRET_KEY_LENGTH]
.copy_from_slice(zeroize::Zeroizing::new(self.private_key.to_bytes()).as_ref());
bytes[SECRET_KEY_LENGTH..].copy_from_slice(&self.public_key.to_bytes());
// SAFETY: we have a valid ed25519 keys, we're just changing to a different library wrapper
#[allow(clippy::unwrap_used)]
jwt_simple::algorithms::Ed25519KeyPair::from_bytes(bytes.as_ref()).unwrap()
}
}
#[cfg(test)]
mod tests {
use super::*;
use rand::thread_rng;
fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
@@ -438,4 +490,29 @@ mod tests {
assert_zeroize::<PrivateKey>();
assert_zeroize_on_drop::<PrivateKey>();
}
#[test]
#[cfg(all(feature = "naive_jwt", feature = "rand"))]
fn check_jwt_key_compat_conversion() {
let mut rng = thread_rng();
let keys = KeyPair::new(&mut rng);
let jwt_keys = keys.to_jwt_compatible_keys();
// internally they're represented by hidden `Edwards25519KeyPair` (plus key_id)
// which has way nicer API for assertions
let jwt_keys_inner =
jwt_simple::algorithms::Edwards25519KeyPair::from_bytes(&jwt_keys.to_bytes()).unwrap();
let compact_ed25519 = jwt_keys_inner.as_ref();
assert!(compact_ed25519
.sk
.validate_public_key(&compact_ed25519.pk)
.is_ok());
let dummy_message = "hello world";
let sig1 = keys.private_key.sign(dummy_message).to_bytes();
let sig2 = compact_ed25519.sk.sign(dummy_message, None).to_vec();
assert_eq!(sig1.to_vec(), sig2);
}
}
@@ -157,6 +157,14 @@ impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC> {
pub fn malformed_details(&self) -> bool {
self.information.parse().is_err()
}
pub fn try_get_test_result(&self) -> Option<&SignerTestResult<LS, TS, LC, TC>> {
if let SignerStatus::Tested { result } = &self.status {
Some(result)
} else {
None
}
}
}
impl<LS, TS, LC, TC> SignerResult<LS, TS, LC, TC>
+37 -4
View File
@@ -15,6 +15,9 @@ use nym_validator_client::ecash::models::EcashSignerStatusResponse;
use nym_validator_client::models::{
ChainBlocksStatusResponse, ChainStatusResponse, SignerInformationResponse,
};
use nym_validator_client::nyxd::contract_traits::dkg_query_client::{
ContractVKShare, DealerDetails, Epoch,
};
mod client_check;
pub mod error;
@@ -48,7 +51,22 @@ pub async fn check_signers(
check_signers_with_client(&client).await
}
pub struct DkgDetails {
pub dkg_epoch: Epoch,
pub threshold: Option<u64>,
pub network_dealers: Vec<DealerDetails>,
pub submitted_shared: HashMap<u64, ContractVKShare>,
}
pub async fn check_signers_with_client<C>(client: &C) -> Result<SignersTestResult, SignerCheckError>
where
C: DkgQueryClient + Sync,
{
let dkg_details = dkg_details_with_client(client).await?;
check_known_dealers(dkg_details).await
}
pub async fn dkg_details_with_client<C>(client: &C) -> Result<DkgDetails, SignerCheckError>
where
C: DkgQueryClient + Sync,
{
@@ -79,16 +97,31 @@ where
.map(|share| (share.node_index, share))
.collect();
Ok(DkgDetails {
dkg_epoch,
threshold,
network_dealers: dealers,
submitted_shared: shares,
})
}
pub async fn check_known_dealers(
dkg_details: DkgDetails,
) -> Result<SignersTestResult, SignerCheckError> {
// 6. for each dealer attempt to perform the checks
let results = dealers
let results = dkg_details
.network_dealers
.into_iter()
.map(|d| {
let share = shares.get(&d.assigned_index);
check_client(d, dkg_epoch.epoch_id, share)
let share = dkg_details.submitted_shared.get(&d.assigned_index);
check_client(d, dkg_details.dkg_epoch.epoch_id, share)
})
.collect::<FuturesUnordered<_>>()
.collect::<Vec<_>>()
.await;
Ok(SignersTestResult { threshold, results })
Ok(SignersTestResult {
threshold: dkg_details.threshold,
results,
})
}
-12
View File
@@ -1,12 +0,0 @@
[package]
name = "nym-execute"
version = "0.1.0"
edition = "2021"
license.workspace = true
[lib]
proc-macro = true
[dependencies]
syn = { workspace = true, features = ["full"] }
quote = { workspace = true }
-110
View File
@@ -1,110 +0,0 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::{
parse_macro_input, Block, ExprMethodCall, FnArg, Ident, ItemFn, LitStr, ReturnType, Token,
VisPublic, Visibility,
};
#[proc_macro_attribute]
pub fn execute(attr: TokenStream, item: TokenStream) -> TokenStream {
let f = parse_macro_input!(item as ItemFn);
let target = parse_macro_input!(attr as LitStr).value();
let cl = if target == "mixnet" {
quote! {self.mixnet_contract_address()}
} else if target == "vesting" {
quote! {self.vesting_contract_address()}
} else {
panic!("Only `mixnet` and `vesting` targets are supported!")
};
let cl = proc_macro::TokenStream::from(cl);
let cl = parse_macro_input!(cl as ExprMethodCall);
let orig_f = f.clone();
let mut execute_f = f.clone();
let mut simulate_f = f.clone();
let name = f.sig.ident;
let name_str = name.to_string();
let call_args = f.sig.inputs.into_iter().filter_map(|arg| match arg {
FnArg::Receiver(_) => None,
FnArg::Typed(arg) => Some(arg.pat),
});
let execute_args = call_args.clone();
let simulate_args = call_args;
execute_f.sig.asyncness = Some(Token![async](execute_f.sig.ident.span()));
simulate_f.sig.asyncness = Some(Token![async](simulate_f.sig.ident.span()));
execute_f.vis = Visibility::Public(VisPublic {
pub_token: Token![pub](execute_f.sig.ident.span()),
});
simulate_f.vis = Visibility::Public(VisPublic {
pub_token: Token![pub](simulate_f.sig.ident.span()),
});
execute_f.sig.ident = Ident::new(
&format!("execute{}", execute_f.sig.ident),
execute_f.sig.ident.span(),
);
simulate_f.sig.ident = Ident::new(
&format!("simulate{}", simulate_f.sig.ident),
simulate_f.sig.ident.span(),
);
let execute_output = quote! {
-> Result<ExecuteResult, NyxdError>
};
let o_ts = proc_macro::TokenStream::from(execute_output);
execute_f.sig.output = parse_macro_input!(o_ts as ReturnType);
let simulate_output = quote! {
-> Result<SimulateResponse, NyxdError>
};
let o_ts = proc_macro::TokenStream::from(simulate_output);
simulate_f.sig.output = parse_macro_input!(o_ts as ReturnType);
let simulate_block = quote! {
{
let (msg, _fee) = self.#name(#(#simulate_args),*);
let msg = self.wrap_contract_execute_message(
#cl,
&msg,
vec![],
)?;
self.simulate(vec![msg]).await
}
};
let ts = proc_macro::TokenStream::from(simulate_block);
simulate_f.block = Box::new(parse_macro_input!(ts as Block));
let execute_block = quote! {
{
let (req, fee) = self.#name(#(#execute_args),*);
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier)));
self.client
.execute(
self.address(),
#cl,
&req,
fee,
#name_str,
vec![],
)
.await
}
};
let ts = proc_macro::TokenStream::from(execute_block);
execute_f.block = Box::new(parse_macro_input!(ts as Block));
let out = quote! {
#orig_f
#execute_f
#simulate_f
};
out.into()
}
+1 -1
View File
@@ -89,7 +89,7 @@ mod tests {
.unwrap();
let blind_sig = issue(
keypair.secret_key(),
sig_req.ecash_pub_key.clone(),
sig_req.ecash_pub_key,
&sig_req.withdrawal_request,
expiration_date.ecash_unix_timestamp(),
issuance.ticketbook_type().encode(),
+2
View File
@@ -7,6 +7,7 @@ homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[dependencies]
sqlx = { workspace = true, features = [
@@ -27,6 +28,7 @@ nym-statistics-common = { path = "../statistics" }
[build-dependencies]
anyhow = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
+13 -4
View File
@@ -1,22 +1,29 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use anyhow::Context;
use sqlx::{Connection, SqliteConnection};
use std::env;
#[tokio::main]
async fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
async fn main() -> anyhow::Result<()> {
let out_dir = env::var("OUT_DIR")?;
let database_path = format!("{out_dir}/gateway-stats-example.sqlite");
// remove the db file if it already existed from previous build
// in case it was from a different branch
if std::fs::exists(&database_path)? {
std::fs::remove_file(&database_path)?;
}
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
.await
.expect("Failed to create SQLx database connection");
.context("Failed to create SQLx database connection")?;
sqlx::migrate!("./migrations")
.run(&mut conn)
.await
.expect("Failed to perform SQLx migrations");
.context("Failed to perform SQLx migrations")?;
#[cfg(target_family = "unix")]
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
@@ -25,4 +32,6 @@ async fn main() {
// for some strange reason we need to add a leading `/` to the windows path even though it's
// not a valid windows path... but hey, it works...
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
Ok(())
}
+2
View File
@@ -7,6 +7,7 @@ homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
[dependencies]
async-trait = { workspace = true }
@@ -31,6 +32,7 @@ nym-gateway-requests = { path = "../gateway-requests" }
nym-sphinx = { path = "../nymsphinx" }
[build-dependencies]
anyhow = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
+13 -4
View File
@@ -1,22 +1,29 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use anyhow::Context;
use sqlx::{Connection, SqliteConnection};
use std::env;
#[tokio::main]
async fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
async fn main() -> anyhow::Result<()> {
let out_dir = env::var("OUT_DIR")?;
let database_path = format!("{out_dir}/gateway-example.sqlite");
// remove the db file if it already existed from previous build
// in case it was from a different branch
if std::fs::exists(&database_path)? {
std::fs::remove_file(&database_path)?;
}
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
.await
.expect("Failed to create SQLx database connection");
.context("Failed to create SQLx database connection")?;
sqlx::migrate!("./migrations")
.run(&mut conn)
.await
.expect("Failed to perform SQLx migrations");
.context("Failed to perform SQLx migrations")?;
#[cfg(target_family = "unix")]
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
@@ -25,4 +32,6 @@ async fn main() {
// for some strange reason we need to add a leading `/` to the windows path even though it's
// not a valid windows path... but hey, it works...
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
Ok(())
}
+5
View File
@@ -151,6 +151,7 @@ use reqwest::{RequestBuilder, Response};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::net::IpAddr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;
use thiserror::Error;
@@ -158,6 +159,7 @@ use tracing::{debug, instrument, warn};
#[cfg(not(target_arch = "wasm32"))]
use std::net::SocketAddr;
use std::str::FromStr;
use std::sync::Arc;
#[cfg(feature = "tunneling")]
@@ -412,11 +414,14 @@ impl ClientBuilder {
//
// I am going to leave these here anyways so that removing a decompression method
// from the features list will throw an error if it is not also removed here.
warn!("FORCING IPv4 CONNECTIONS");
reqwest::ClientBuilder::new()
.gzip(true)
.deflate(true)
.brotli(true)
.zstd(true)
.local_address(IpAddr::from_str("0.0.0.0:0").unwrap())
};
ClientBuilder {
@@ -319,9 +319,9 @@ mod tests {
let sk = grp.random_scalar();
let sk_user = SecretKeyUser { sk };
let pk_user = sk_user.public_key();
public_keys.push(pk_user.clone());
public_keys.push(pk_user);
}
public_keys.push(user_keypair.public_key().clone());
public_keys.push(user_keypair.public_key());
let (req, req_info) =
withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap();
@@ -462,9 +462,9 @@ mod tests {
let sk = grp.random_scalar();
let sk_user = SecretKeyUser { sk };
let pk_user = sk_user.public_key();
public_keys.push(pk_user.clone());
public_keys.push(pk_user);
}
public_keys.push(user_keypair.public_key().clone());
public_keys.push(user_keypair.public_key());
let (req, req_info) =
withdrawal_request(user_keypair.secret_key(), expiration_date, t_type).unwrap();
@@ -401,7 +401,7 @@ impl Bytable for SecretKeyUser {
impl Base58 for SecretKeyUser {}
#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize)]
pub struct PublicKeyUser {
pub(crate) pk: G1Projective,
}
@@ -554,7 +554,7 @@ impl KeyPairUser {
}
pub fn public_key(&self) -> PublicKeyUser {
self.public_key.clone()
self.public_key
}
pub fn to_bytes(&self) -> Vec<u8> {
+3
View File
@@ -16,3 +16,6 @@ thiserror = { workspace = true }
[dev-dependencies]
rand = { workspace = true }
nym-crypto = { path = "../../crypto", features = ["rand"] }
bincode = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true, features = ["derive"] }
+86 -2
View File
@@ -7,7 +7,7 @@
use crate::nodes::{NodeIdentity, NODE_IDENTITY_SIZE};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_sphinx_types::Destination;
use serde::de::{Error as SerdeError, Unexpected, Visitor};
use serde::de::{Error as SerdeError, SeqAccess, Unexpected, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt::{self, Formatter};
use std::str::FromStr;
@@ -64,7 +64,7 @@ impl<'de> Deserialize<'de> for Recipient {
{
struct RecipientVisitor;
impl Visitor<'_> for RecipientVisitor {
impl<'de> Visitor<'de> for RecipientVisitor {
type Value = Recipient;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
@@ -90,6 +90,42 @@ impl<'de> Deserialize<'de> for Recipient {
)
})
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: SeqAccess<'de>,
{
// if we know the size hint, check if it matches expectation,
// otherwise return an error
if let Some(size_hint) = seq.size_hint() {
if size_hint != Recipient::LEN {
return Err(SerdeError::invalid_length(size_hint, &self));
}
}
let mut recipient_bytes = [0u8; Recipient::LEN];
// clippy's suggestion is completely wrong and it iterates wrong sequence
#[allow(clippy::needless_range_loop)]
for i in 0..Recipient::LEN {
let Some(elem) = seq.next_element::<u8>()? else {
return Err(SerdeError::invalid_length(i + 1, &self));
};
recipient_bytes[i] = elem;
}
// make sure there are no trailing bytes
if seq.next_element::<u8>()?.is_some() {
return Err(SerdeError::invalid_length(Recipient::LEN + 1, &self));
}
Recipient::try_from_bytes(recipient_bytes).map_err(|_| {
SerdeError::invalid_value(
Unexpected::Other("At least one of the curve points was malformed"),
&self,
)
})
}
}
deserializer.deserialize_bytes(RecipientVisitor)
@@ -245,6 +281,18 @@ impl FromStr for Recipient {
mod tests {
use super::*;
fn mock_recipient() -> Recipient {
Recipient::try_from_bytes([
67, 5, 132, 146, 3, 236, 116, 89, 254, 57, 131, 159, 69, 181, 55, 208, 12, 108, 136,
83, 58, 76, 171, 195, 31, 98, 92, 64, 68, 53, 156, 184, 100, 189, 73, 3, 238, 103, 156,
108, 124, 199, 42, 79, 172, 98, 81, 177, 182, 100, 167, 164, 74, 183, 199, 213, 162,
173, 102, 112, 30, 159, 148, 66, 44, 75, 230, 182, 138, 114, 170, 163, 209, 82, 204,
100, 118, 91, 57, 150, 212, 147, 151, 135, 148, 16, 213, 223, 182, 164, 242, 37, 40,
73, 137, 228,
])
.unwrap()
}
#[test]
fn string_conversion_works() {
let mut rng = rand::thread_rng();
@@ -308,4 +356,40 @@ mod tests {
recovered_recipient.gateway.to_bytes()
);
}
// calls `visit_bytes`
#[test]
fn bincode_serialisation_works() {
let recipient = mock_recipient();
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
struct MyStruct {
recipient: Recipient,
}
let a = MyStruct { recipient };
let s = bincode::serialize(&a).unwrap();
let b = bincode::deserialize(&s).unwrap();
assert_eq!(a, b);
}
// calls `visit_seq`
#[test]
fn json_serialisation_works() {
use serde::{Deserialize, Serialize};
let recipient = mock_recipient();
#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)]
struct MyStruct {
recipient: Recipient,
}
let a = MyStruct { recipient };
let s = serde_json::to_string(&a).unwrap();
let b = serde_json::from_str(&s).unwrap();
assert_eq!(a, b);
}
}
+2
View File
@@ -180,6 +180,7 @@ impl NymPacket {
}
#[cfg(feature = "sphinx")]
#[allow(unreachable_patterns)]
pub fn sphinx_packet_ref(&self) -> Option<&SphinxPacket> {
match self {
NymPacket::Sphinx(packet) => Some(packet),
@@ -188,6 +189,7 @@ impl NymPacket {
}
#[cfg(feature = "sphinx")]
#[allow(unreachable_patterns)]
pub fn to_sphinx_packet(self) -> Option<SphinxPacket> {
match self {
NymPacket::Sphinx(packet) => Some(packet),
+2
View File
@@ -7,6 +7,7 @@ homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -36,5 +37,6 @@ url.workspace = true
[build-dependencies]
anyhow = { workspace = true }
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
+14 -4
View File
@@ -1,22 +1,30 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use anyhow::Context;
#[tokio::main]
async fn main() {
async fn main() -> anyhow::Result<()> {
use sqlx::{Connection, SqliteConnection};
use std::env;
let out_dir = env::var("OUT_DIR").unwrap();
let out_dir = env::var("OUT_DIR")?;
let database_path = format!("{out_dir}/scraper-example.sqlite");
// remove the db file if it already existed from previous build
// in case it was from a different branch
if std::fs::exists(&database_path)? {
std::fs::remove_file(&database_path)?;
}
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
.await
.expect("Failed to create SQLx database connection");
.context("Failed to create SQLx database connection")?;
sqlx::migrate!("./sql_migrations")
.run(&mut conn)
.await
.expect("Failed to perform SQLx migrations");
.context("Failed to perform SQLx migrations")?;
#[cfg(target_family = "unix")]
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
@@ -25,4 +33,6 @@ async fn main() {
// for some strange reason we need to add a leading `/` to the windows path even though it's
// not a valid windows path... but hey, it works...
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
Ok(())
}
+30
View File
@@ -0,0 +1,30 @@
[package]
name = "nym-upgrade-mode-check"
version = "0.1.0"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
readme.workspace = true
[dependencies]
jwt-simple = { workspace = true }
reqwest = { workspace = true, features = ["rustls-tls"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
time = { workspace = true, features = ["serde"] }
thiserror = { workspace = true }
tracing = { workspace = true }
nym-http-api-client = { path = "../http-api-client", default-features = false }
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde", "naive_jwt"] }
[dev-dependencies]
anyhow = { workspace = true }
time = { workspace = true, features = ["macros"] }
[lints]
workspace = true
@@ -0,0 +1,123 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::UpgradeModeCheckError;
use nym_crypto::asymmetric::ed25519;
use nym_http_api_client::generate_user_agent;
use serde::{Deserialize, Serialize};
use std::time::Duration;
use time::OffsetDateTime;
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
pub struct UpgradeModeAttestation {
#[serde(flatten)]
pub content: UpgradeModeAttestationContent,
#[serde(with = "ed25519::bs58_ed25519_signature")]
pub signature: ed25519::Signature,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone, Copy)]
#[serde(tag = "type")]
#[serde(rename = "upgrade_mode")]
pub struct UpgradeModeAttestationContent {
#[serde(with = "time::serde::timestamp")]
pub starting_time: OffsetDateTime,
#[serde(with = "ed25519::bs58_ed25519_pubkey")]
pub attester_public_key: ed25519::PublicKey,
}
impl UpgradeModeAttestation {
pub fn verify(&self) -> bool {
self.content
.attester_public_key
.verify(self.content.as_json(), &self.signature)
.is_ok()
}
}
impl UpgradeModeAttestationContent {
pub fn as_json(&self) -> String {
// SAFETY: Serialize impl is valid and we have no non-string map keys
#[allow(clippy::unwrap_used)]
serde_json::to_string(&self).unwrap()
}
}
pub fn generate_new_attestation(key: &ed25519::PrivateKey) -> UpgradeModeAttestation {
generate_new_attestation_with_starting_time(key, OffsetDateTime::now_utc())
}
pub fn generate_new_attestation_with_starting_time(
key: &ed25519::PrivateKey,
starting_time: OffsetDateTime,
) -> UpgradeModeAttestation {
let content = UpgradeModeAttestationContent {
starting_time,
attester_public_key: key.into(),
};
UpgradeModeAttestation {
signature: key.sign(content.as_json()),
content,
}
}
pub async fn attempt_retrieve(
url: &str,
) -> Result<Option<UpgradeModeAttestation>, UpgradeModeCheckError> {
let retrieval_failure = |source| UpgradeModeCheckError::AttestationRetrievalFailure {
url: url.to_string(),
source,
};
let attestation = reqwest::ClientBuilder::new()
.user_agent(generate_user_agent!())
.timeout(Duration::from_secs(5))
.build()
.map_err(retrieval_failure)?
.get(url)
.send()
.await
.map_err(retrieval_failure)?
.json::<Option<UpgradeModeAttestation>>()
.await
.map_err(retrieval_failure)?;
Ok(attestation)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn upgrade_mode_attestation_serde_json() -> anyhow::Result<()> {
// unix timestamp: 1629720000
let starting_time = time::macros::datetime!(2021-08-23 12:00 UTC);
let key = ed25519::PrivateKey::from_bytes(&[
108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248,
163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161,
])?;
let attestation = generate_new_attestation_with_starting_time(&key, starting_time);
let attestation_json = serde_json::to_string(&attestation)?;
let attestation_content_json = attestation.content.as_json();
let expected_attestation = r#"{"type":"upgrade_mode","starting_time":1629720000,"attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt","signature":"5rWUr2ypaDTtrMKegMP3tQkkZGFAuhNTnEVCVe5Azv6QqvLzoGdQiMkFmeyhDd1XSfoXpL9fFM58rsdA1kf4GYMM"}"#;
let expected_content = r#"{"type":"upgrade_mode","starting_time":1629720000,"attester_public_key":"3pkFcBXCEmbmXBT2G8CkFMuKisJcH54mbBGvncHaDibt"}"#;
assert_eq!(attestation_content_json, expected_content);
assert_eq!(attestation_json, expected_attestation);
let recovered_attestation = serde_json::from_str(&attestation_json)?;
assert_eq!(attestation, recovered_attestation);
let recovered_content = serde_json::from_str(&attestation_content_json)?;
assert_eq!(attestation.content, recovered_content);
Ok(())
}
}
+23
View File
@@ -0,0 +1,23 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_crypto::asymmetric::ed25519::Ed25519RecoveryError;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum UpgradeModeCheckError {
#[error("failed to decode jwt metadata")]
TokenMetadataDecodeFailure { source: jwt_simple::Error },
#[error("the jwt metadata didn't contain explicit public key")]
MissingTokenPublicKey,
#[error("the attached public key was not valid ed25519 public key")]
MalformedEd25519PublicKey { source: Ed25519RecoveryError },
#[error("failed to verify the jwt: {source}")]
JwtVerificationFailure { source: jwt_simple::Error },
#[error("failed to retrieve attestation from {url}:{source}")]
AttestationRetrievalFailure { url: String, source: reqwest::Error },
}
+119
View File
@@ -0,0 +1,119 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{UpgradeModeAttestation, UpgradeModeCheckError};
use jwt_simple::claims::Claims;
use jwt_simple::common::{KeyMetadata, VerificationOptions};
use jwt_simple::prelude::{EdDSAKeyPairLike, EdDSAPublicKeyLike};
use jwt_simple::token::Token;
use nym_crypto::asymmetric::ed25519;
use std::collections::HashSet;
use std::time::Duration;
// for now use static issuer such as "nym-credential-proxy"
pub fn generate_jwt_for_upgrade_mode_attestation(
attestation: UpgradeModeAttestation,
validity: Duration,
keys: &ed25519::KeyPair,
issuer: Option<&'static str>,
) -> String {
let claim = Claims::with_custom_claims(attestation, validity.into());
let mut claim = if let Some(issuer) = issuer {
claim.with_issuer(issuer)
} else {
claim
};
claim.create_nonce();
let md = KeyMetadata::default().with_public_key(keys.public_key().to_base58_string());
let mut jwt_keys = keys.to_jwt_compatible_keys();
// SAFETY: trait impl for EdDSA is infallible
#[allow(clippy::unwrap_used)]
jwt_keys.attach_metadata(md).unwrap();
// SAFETY: our construction of the jwt is valid
#[allow(clippy::unwrap_used)]
jwt_keys.sign(claim).unwrap()
}
pub fn validate_upgrade_mode_jwt(
token: &str,
expected_issuer: Option<&'static str>,
) -> Result<UpgradeModeAttestation, UpgradeModeCheckError> {
// for now, we completely ignore the validity of the pubkey (I know, I know).
// that will be changed later on
// so as a bypass we have to extract the claimed issuer from the jwt to verify against it
let metadata = Token::decode_metadata(token)
.map_err(|source| UpgradeModeCheckError::TokenMetadataDecodeFailure { source })?;
let pub_key = metadata
.public_key()
.ok_or(UpgradeModeCheckError::MissingTokenPublicKey)?;
let ed25519_pub_key = ed25519::PublicKey::from_base58_string(pub_key)
.map_err(|source| UpgradeModeCheckError::MalformedEd25519PublicKey { source })?;
let mut opts = VerificationOptions::default();
if let Some(issuer) = expected_issuer {
opts.allowed_issuers = Some(HashSet::from_iter(vec![issuer.to_string()]));
}
let attestation = ed25519_pub_key
.to_jwt_compatible_key()
.verify_token::<UpgradeModeAttestation>(token, Some(opts))
.map_err(|source| UpgradeModeCheckError::JwtVerificationFailure { source })?
.custom;
Ok(attestation)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::generate_new_attestation;
use nym_crypto::asymmetric::ed25519;
#[test]
fn generate_and_validate_jwt() {
let attestation_key = ed25519::PrivateKey::from_bytes(&[
108, 49, 193, 21, 126, 161, 249, 85, 242, 207, 74, 195, 238, 6, 64, 149, 201, 140, 248,
163, 122, 170, 79, 198, 87, 85, 36, 29, 243, 92, 64, 161,
])
.unwrap();
let jwt_key = ed25519::PrivateKey::from_bytes(&[
152, 17, 144, 255, 213, 219, 246, 208, 109, 33, 100, 73, 1, 141, 32, 63, 141, 89, 167,
2, 52, 215, 241, 219, 200, 18, 159, 241, 76, 111, 42, 32,
])
.unwrap();
let keys = ed25519::KeyPair::from(jwt_key);
let attestation = generate_new_attestation(&attestation_key);
let jwt_issuer = generate_jwt_for_upgrade_mode_attestation(
attestation,
Duration::from_secs(60 * 60),
&keys,
Some("nym-credential-proxy"),
);
// we expect 'nym-credential-proxy' issuer
assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("nym-credential-proxy")).is_ok());
// we don't care about issuer
assert!(validate_upgrade_mode_jwt(&jwt_issuer, None).is_ok());
// we expect another-issuer
assert!(validate_upgrade_mode_jwt(&jwt_issuer, Some("another-issuer")).is_err());
let jwt_no_issuer = generate_jwt_for_upgrade_mode_attestation(
attestation,
Duration::from_secs(60 * 60),
&keys,
None,
);
// we expect 'nym-credential-proxy' issuer
assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, Some("nym-credential-proxy")).is_err());
// we don't care about issuer
assert!(validate_upgrade_mode_jwt(&jwt_no_issuer, None).is_ok());
}
}
+13
View File
@@ -0,0 +1,13 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub(crate) mod attestation;
pub(crate) mod error;
pub(crate) mod jwt;
pub use attestation::{
attempt_retrieve, generate_new_attestation, generate_new_attestation_with_starting_time,
UpgradeModeAttestation,
};
pub use error::UpgradeModeCheckError;
pub use jwt::{generate_jwt_for_upgrade_mode_attestation, validate_upgrade_mode_jwt};
+1 -6
View File
@@ -130,12 +130,7 @@ impl VerlocMeasurement {
let variance_micros = data
.iter()
.map(|&value| {
// make sure we don't underflow
let diff = if mean > value {
mean - value
} else {
value - mean
};
let diff = mean.abs_diff(value);
// we don't need nanos precision
let diff_micros = diff.as_micros();
diff_micros * diff_micros
+15 -1
View File
@@ -25,7 +25,7 @@
//! ```
use crate::error::ZulipClientError;
use crate::message::{SendMessageResponse, SendableMessage};
use crate::message::{DirectMessage, SendMessageResponse, SendableMessage, StreamMessage};
use nym_bin_common::bin_info;
use nym_http_api_client::UserAgent;
use reqwest::{header, Method, RequestBuilder};
@@ -92,6 +92,20 @@ impl Client {
.map_err(|source| ZulipClientError::RequestDecodeFailure { source })
}
pub async fn send_direct_message(
&self,
msg: impl Into<DirectMessage>,
) -> Result<SendMessageResponse, ZulipClientError> {
self.send_message(msg.into()).await
}
pub async fn send_channel_message(
&self,
msg: impl Into<StreamMessage>,
) -> Result<SendMessageResponse, ZulipClientError> {
self.send_message(msg.into()).await
}
fn build_request(&self, method: Method, endpoint: &'static str) -> RequestBuilder {
let url = format!("{}{endpoint}", self.server_url);
trace!("posting to {url}");
+62 -10
View File
@@ -22,7 +22,7 @@ pub enum SendMessageResponse {
},
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
#[serde(tag = "type")]
pub enum SendableMessageContent {
@@ -40,7 +40,7 @@ pub enum SendableMessageContent {
},
}
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "snake_case")]
pub struct SendableMessage {
#[serde(flatten)]
@@ -117,17 +117,17 @@ impl StreamMessage {
pub fn new(
to: impl Into<ToChannel>,
content: impl Into<String>,
topic: Option<String>,
topic: impl IntoMaybeTopic,
) -> Self {
StreamMessage {
to: to.into().to_string(),
topic,
topic: topic.into_maybe_topic(),
content: content.into(),
}
}
pub fn no_topic(to: impl Into<ToChannel>, content: impl Into<String>) -> Self {
Self::new(to, content, None)
Self::new(to, content, None::<String>)
}
#[must_use]
@@ -194,22 +194,74 @@ impl From<StreamMessage> for SendableMessageContent {
}
}
impl<T, S> From<(T, S, Option<S>)> for StreamMessage
impl<T, S, U> From<(T, S, U)> for StreamMessage
where
T: Into<ToChannel>,
S: Into<String>,
U: IntoMaybeTopic,
{
fn from((to, content, topic): (T, S, Option<S>)) -> Self {
StreamMessage::new(to, content, topic.map(Into::into))
fn from((to, content, topic): (T, S, U)) -> Self {
StreamMessage::new(to, content, topic)
}
}
impl<T, S> From<(T, S, Option<S>)> for SendableMessage
impl<T, S> From<(T, S)> for StreamMessage
where
T: Into<ToChannel>,
S: Into<String>,
{
fn from(inner: (T, S, Option<S>)) -> Self {
fn from((to, content): (T, S)) -> Self {
StreamMessage::no_topic(to, content)
}
}
impl<T, S, U> From<(T, S, U)> for SendableMessage
where
T: Into<ToChannel>,
S: Into<String>,
U: IntoMaybeTopic,
{
fn from(inner: (T, S, U)) -> Self {
StreamMessage::from(inner).into()
}
}
pub trait IntoMaybeTopic {
fn into_maybe_topic(self) -> Option<String>;
}
impl<S> IntoMaybeTopic for &Option<S>
where
S: Into<String> + Clone,
{
fn into_maybe_topic(self) -> Option<String> {
self.clone().map(|s| s.into())
}
}
impl<S> IntoMaybeTopic for Option<S>
where
S: Into<String>,
{
fn into_maybe_topic(self) -> Option<String> {
self.map(Into::into)
}
}
impl IntoMaybeTopic for String {
fn into_maybe_topic(self) -> Option<String> {
Some(self)
}
}
impl IntoMaybeTopic for &String {
fn into_maybe_topic(self) -> Option<String> {
Some(self.clone())
}
}
impl IntoMaybeTopic for &str {
fn into_maybe_topic(self) -> Option<String> {
Some(self.to_string())
}
}
@@ -5,7 +5,7 @@
},
"mixmining_reserve": {
"denom": "unym",
"amount": "184339094131786"
"amount": "182883243257647"
},
"vesting_tokens": {
"denom": "unym",
@@ -13,6 +13,6 @@
},
"circulating_supply": {
"denom": "unym",
"amount": "815660905868214"
"amount": "817116756742353"
}
}
@@ -1 +1 @@
815_660_905
817_116_756
@@ -1 +1 @@
249_796_152
60_000_000
@@ -1 +1 @@
249_796_152
60_000_000
@@ -1,7 +1,7 @@
| **Item** | **Description** | **Amount in NYM** |
|:-------------------|:------------------------------------------------------|--------------------:|
| Total Supply | Maximum amount of NYM token in existence | 1_000_000_000 |
| Mixmining Reserve | Tokens releasing for operators rewards | 184_339_094 |
| Mixmining Reserve | Tokens releasing for operators rewards | 182_883_243 |
| Vesting Tokens | Tokens locked outside of cicrulation for future claim | 0 |
| Circulating Supply | Amount of unlocked tokens | 815_660_905 |
| Stake Saturation | Optimal size of node self-bond + delegation | 1_040_817 |
| Circulating Supply | Amount of unlocked tokens | 817_116_756 |
| Stake Saturation | Optimal size of node self-bond + delegation | 250_000 |
@@ -1,18 +1,18 @@
{
"interval": {
"reward_pool": "184339094131786.145886263466367605",
"staking_supply": "249796152422140.492822331813424919",
"staking_supply_scale_factor": "0.30625",
"epoch_reward_budget": "5120530392.54961516350731851",
"stake_saturation_point": "1040817301758.918720093049222603",
"reward_pool": "182883243257647.891553460395608456",
"staking_supply": "60000000000000",
"staking_supply_scale_factor": "0.07342892",
"epoch_reward_budget": "5080090090.490219209818344322",
"stake_saturation_point": "250000000000",
"sybil_resistance": "0.3",
"active_set_work_factor": "10",
"interval_pool_emission": "0.02"
},
"rewarded_set": {
"entry_gateways": 50,
"exit_gateways": 70,
"mixnodes": 120,
"entry_gateways": 80,
"exit_gateways": 100,
"mixnodes": 60,
"standby": 0
}
}
@@ -1 +1 @@
Friday, August 22nd 2025, 10:15:08 UTC
Thursday, September 4th 2025, 09:12:25 UTC
@@ -58,8 +58,8 @@ Options:
Specifies whether the wireguard service is enabled on this node [env: NYMNODE_WG_ENABLED=] [possible values: true, false]
--wireguard-bind-address <WIREGUARD_BIND_ADDRESS>
Socket address this node will use for binding its wireguard interface. default: `[::]:51822` [env: NYMNODE_WG_BIND_ADDRESS=]
--wireguard-tunnel-announced-port <WIREGUARD_TUNNEL_ANNOUNCED_PORT>
Tunnel port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
--wireguard-announced-port <WIREGUARD_ANNOUNCED_PORT>
Port announced to external clients wishing to connect to the wireguard interface. Useful in the instances where the node is behind a proxy [env: NYMNODE_WG_ANNOUNCED_PORT=]
--wireguard-private-network-prefix <WIREGUARD_PRIVATE_NETWORK_PREFIX>
The prefix denoting the maximum number of the clients that can be connected via Wireguard. The maximum value for IPv4 is 32 and for IPv6 is 128 [env: NYMNODE_WG_PRIVATE_NETWORK_PREFIX=]
--verloc-bind-address <VERLOC_BIND_ADDRESS>
@@ -4,7 +4,7 @@
| [AmeriNoc](https://www.amerinoc.com) | USA | Yes | nan | nan | 07/2025 |
| [BitLaunch](https://bitlaunch.io) | Canada, USA, UK | No | Yes | Expensive. Digial Ocean through BitLanch has IPv6 | 05/2024 |
| [Cherry Servers](https://www.cherryservers.com) | Lithuania, Netherlands, USA, Singapore | No | Yes | Issued IP doesnt match the location offered by the provider. | 05/2024 |
| [Colocall](https://www.colocall.net/) | Ukraine | Yes | nan | 07/2025 | nan |
| [Colocall](https://www.colocall.net/) | Ukraine | Yes | nan | nan | 07/2025 |
| [DataPacket](https://www.datapacket.com/pricing) | NL, GR, SK, BE, RO, HU, DK, IE, DE, UA, PT, GB, ES, FR, IT, NO, CZ, BG, SE, AT, PL, HR, CH, USA, CO, AR, PE, MX, CL, TR, ZA, NG, IL, HK, AU, SG, JP | Yes | nan | nan | 07/2025 |
| [Dataclub](https://www.dataclub.eu/) | Latvia, Sweden, Netherlands | Yes | nan | nan | 07/2027 |
| [Flokinet](https://flokinet.is) | Netherlands, Iceland, Romania,France | Yes, needs a ticket and custom setup | yes, including XMR | Very slow customer support | 05/2024 |
@@ -13,6 +13,7 @@
| [HostSailor](https://hostsailor.com) | USA | Yes, based on ticket | Yes | The IPv6 setup needs custom research and is not documented | 05/2024 |
| [Hostiko](https://hostiko.com.ua) | Ukraine, Germany | Yes, on by default | Yes | Ukrainian provider. They allow Exit nodes on Germany boxes but limit the bandwidth, you also have to restrict certain ports like 25 and 587. Make sure you open a ticket. | 07/2024 |
| [Hostinger](https://hostinger.com) | France, Lithuania, India, USA, Brazil | Yes, out of the box | Yes | Not fast enough, Crypto payments must be done per each server monthly or annually. | 07/2025 |
| [Hostraha](https://hostraha.com) | Kenya and other African countries | No, but advertised otherwise | Yes, USDT TRC20 | Don't recommend. Unresponsive technical and billing support, never provided IPv6 even though advertised and paid for. When VPS cancelled, company still tried to bill the credit card on file multiple times. | 08/2025 |
| [Hostroyale](https://hostroyale.com/hosting/dedicated-server/) | Various countries with different pricing | nan | Yes | nan | 07/2025 |
| [Hostslick](https://hostslick.com) | Netherlands, Germany | Yes, on by default | Yes | Good amount of bandwidth for the price. Make sure you open the ticket if you want to run Exit node | 07/2024 |
| [Incognet](https://incognet.io) | Netherlands and USA | Yes, on by default | Yes | They allow Tor exit nodes but you must adhere to their rules https://incognet.io/tor-exits | 07/2024 |
+2 -2
View File
@@ -32,7 +32,7 @@
[Zenlayer](https://www.zenlayer.com/bare-metal/), [advertised over 50 locations](50+ https://www.zenlayer.com/global-network),,,,07/2025
[PrivateLayer](https://privatelayer.com),Swiss,Yes,Yes,Slow customer response,07/2025
[AmeriNoc](https://www.amerinoc.com),USA,Yes,,,07/2025
[Colocall](https://www.colocall.net/),Ukraine,Yes,,07/2025,
[Colocall](https://www.colocall.net/),Ukraine,Yes,,,07/2025
[Incognet](https://incognet.io/kansas-city-dedicated-servers),"USA, Netherlands",Yes,,,07/2025
[FranTech](https://my.frantech.ca),USA,Yes,,,07/2025
[Psychz](https://www.psychz.net),"US, UK, Brazil, Japan, Russia, South Africa and many more",Yes,,,07/2025
@@ -41,4 +41,4 @@
[Dataclub](https://www.dataclub.eu/),"Latvia, Sweden, Netherlands",Yes,,,07/2027
[Privex](https://www.privex.io/tor-exit-policy/),"USA, Germany, Sweden",Yes,Yes,,07/2025
[Svea](https://svea.net/vps),Sweden,Yes,,,07/2025
[Hostraha](https://hostraha.com),"Kenya and other African countries", "No, but advertised otherwise", "Yes, USDT TRC20", "Don't recommend. Unresponsive technical and billing support, never provided IPv6 even though advertised and paid for. When VPS cancelled, company still tried to bill the credit card on file multiple times.", 08/2025
[Hostraha](https://hostraha.com),Kenya and other African countries,"No, but advertised otherwise","Yes, USDT TRC20","Don't recommend. Unresponsive technical and billing support, never provided IPv6 even though advertised and paid for. When VPS cancelled, company still tried to bill the credit card on file multiple times.",08/2025
1 **ISP** **Locations** **Public IPv6** **Crypto Payments** **Comments** **Last Updated**
32 [Zenlayer](https://www.zenlayer.com/bare-metal/) [advertised over 50 locations](50+ https://www.zenlayer.com/global-network) 07/2025
33 [PrivateLayer](https://privatelayer.com) Swiss Yes Yes Slow customer response 07/2025
34 [AmeriNoc](https://www.amerinoc.com) USA Yes 07/2025
35 [Colocall](https://www.colocall.net/) Ukraine Yes 07/2025 07/2025
36 [Incognet](https://incognet.io/kansas-city-dedicated-servers) USA, Netherlands Yes 07/2025
37 [FranTech](https://my.frantech.ca) USA Yes 07/2025
38 [Psychz](https://www.psychz.net) US, UK, Brazil, Japan, Russia, South Africa and many more Yes 07/2025
41 [Dataclub](https://www.dataclub.eu/) Latvia, Sweden, Netherlands Yes 07/2027
42 [Privex](https://www.privex.io/tor-exit-policy/) USA, Germany, Sweden Yes Yes 07/2025
43 [Svea](https://svea.net/vps) Sweden Yes 07/2025
44 [Hostraha](https://hostraha.com) Kenya and other African countries No, but advertised otherwise Yes, USDT TRC20 Don't recommend. Unresponsive technical and billing support, never provided IPv6 even though advertised and paid for. When VPS cancelled, company still tried to bill the credit card on file multiple times. 08/2025
+1 -1
View File
@@ -86,5 +86,5 @@ defguard_wireguard_rs = { workspace = true }
[dev-dependencies]
nym-gateway-storage = { path = "../common/gateway-storage", features = ["mock"] }
nym-wireguard = { path = "../common/wireguard", features = ["mock"] }
mock_instant = "0.5.3"
mock_instant = "0.6.0"
time = { workspace = true }
+1 -1
View File
@@ -4,7 +4,7 @@
[package]
name = "nym-api"
license = "GPL-3.0"
version = "1.1.65"
version = "1.1.64"
authors.workspace = true
edition = "2021"
rust-version.workspace = true
+15 -10
View File
@@ -1,16 +1,20 @@
use anyhow::Context;
use sqlx::{Connection, FromRow, SqliteConnection};
use std::env;
const SQLITE_DB_FILENAME: &str = "nym-api-example.sqlite";
// it's fine if compilation fails
#[allow(clippy::unwrap_used)]
#[allow(clippy::expect_used)]
#[tokio::main]
async fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
async fn main() -> anyhow::Result<()> {
let out_dir = env::var("OUT_DIR")?;
let database_path = format!("{out_dir}/{SQLITE_DB_FILENAME}");
// remove the db file if it already existed from previous build
// in case it was from a different branch
if std::fs::exists(&database_path)? {
std::fs::remove_file(&database_path)?;
}
#[cfg(target_family = "unix")]
write_db_path_to_file(&out_dir, SQLITE_DB_FILENAME)
.await
@@ -18,12 +22,12 @@ async fn main() {
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
.await
.expect("Failed to create SQLx database connection");
.context("Failed to create SQLx database connection")?;
sqlx::migrate!("./migrations")
.run(&mut conn)
.await
.expect("Failed to perform SQLx migrations");
.context("Failed to perform SQLx migrations")?;
#[derive(FromRow)]
struct Exists {
@@ -33,8 +37,7 @@ async fn main() {
// check if it was already run
let res: Exists = sqlx::query_as("SELECT EXISTS (SELECT 1 FROM v3_migration_info) AS 'exists'")
.fetch_one(&mut conn)
.await
.unwrap();
.await?;
let already_run = res.exists;
@@ -58,7 +61,7 @@ async fn main() {
)
.execute(&mut conn)
.await
.expect("failed to update post v3 migration tables");
.context("failed to update post v3 migration tables")?;
}
#[cfg(target_family = "unix")]
@@ -68,6 +71,8 @@ async fn main() {
// for some strange reason we need to add a leading `/` to the windows path even though it's
// not a valid windows path... but hey, it works...
println!("cargo:rustc-env=DATABASE_URL=sqlite:///{}", &database_path);
Ok(())
}
/// use `./enter_db.sh` to inspect DB
@@ -31,6 +31,7 @@ pub use schema_helpers::*;
pub use nym_mixnet_contract_common::{EpochId, KeyRotationId, KeyRotationState};
pub use nym_node_requests::api::v1::node::models::BinaryBuildInformationOwned;
pub use nym_noise_keys::VersionedNoiseKey;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
pub struct RequestError {
+2 -2
View File
@@ -50,7 +50,7 @@ impl CredentialRequest for BlindSignRequestBody {
}
fn ecash_pubkey(&self) -> PublicKeyUser {
self.ecash_pubkey.clone()
self.ecash_pubkey
}
}
@@ -60,7 +60,7 @@ pub(crate) fn blind_sign<C: CredentialRequest>(
) -> Result<BlindedSignature, EcashError> {
Ok(nym_compact_ecash::scheme::withdrawal::issue(
signing_key,
request.ecash_pubkey().clone(),
request.ecash_pubkey(),
request.withdrawal_request(),
request.expiration_date_timestamp(),
request.ticketbook_type(),
@@ -1,12 +1,13 @@
[package]
name = "nym-credential-proxy"
version = "0.2.0"
version = "0.1.8"
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -50,14 +51,19 @@ nym-validator-client = { path = "../../common/client-libs/validator-client" }
nym-network-defaults = { path = "../../common/network-defaults" }
nym-credential-proxy-requests = { path = "../nym-credential-proxy-requests", features = ["openapi"] }
nym-ecash-signer-check = { path = "../../common/ecash-signer-check" }
[dev-dependencies]
tempfile = { workspace = true }
[build-dependencies]
anyhow = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
[features]
default = ["cors"]
cors = ["tower-http"]
[lints]
workspace = true
@@ -1,22 +1,31 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use anyhow::Context;
#[tokio::main]
async fn main() {
async fn main() -> anyhow::Result<()> {
use sqlx::{Connection, SqliteConnection};
use std::env;
let out_dir = env::var("OUT_DIR").unwrap();
let out_dir = env::var("OUT_DIR")?;
let database_path = format!("{out_dir}/nym-credential-proxy-example.sqlite");
// remove the db file if it already existed from previous build
// in case it was from a different branch
if std::fs::exists(&database_path)? {
std::fs::remove_file(&database_path)?;
}
let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc"))
.await
.expect("Failed to create SQLx database connection");
.context("Failed to create SQLx database connection")?;
sqlx::migrate!("./migrations")
.run(&mut conn)
.await
.expect("Failed to perform SQLx migrations");
.context("Failed to perform SQLx migrations")?;
println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path);
Ok(())
}
@@ -0,0 +1,81 @@
/*
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: GPL-3.0-only
*/
CREATE TABLE ecash_deposit
(
-- id assigned [by the contract] to the deposit
deposit_id INTEGER PRIMARY KEY NOT NULL,
-- associated tx hash
deposit_tx_hash TEXT NOT NULL,
-- indication of when the deposit request has been created
-- (so that based on block timestamp we could potentially determine latency)
requested_on TIMESTAMP WITHOUT TIME ZONE NOT NULL,
-- the amount put in the deposit (informative, as we expect this to change in the future)
deposit_amount TEXT NOT NULL,
-- the private key generated for the purposes of the deposit (the public component has been put in the transaction)
ed25519_deposit_private_key BLOB NOT NULL
);
INSERT INTO ecash_deposit(deposit_id, deposit_tx_hash, requested_on, deposit_amount, ed25519_deposit_private_key)
SELECT deposit_id, deposit_tx_hash, requested_on, deposit_amount, ed25519_deposit_private_key
FROM ticketbook_deposit;
CREATE TABLE ecash_deposit_usage
(
deposit_id INTEGER PRIMARY KEY REFERENCES ecash_deposit (deposit_id),
ticketbooks_requested_on TIMESTAMP WITHOUT TIME ZONE NOT NULL,
client_pubkey BLOB NOT NULL,
request_uuid TEXT UNIQUE NOT NULL,
-- this has to be improved later on to resume issuance or something, but for now it's fine
ticketbook_request_error TEXT
);
INSERT INTO ecash_deposit_usage(deposit_id, ticketbooks_requested_on, client_pubkey, request_uuid)
SELECT deposit_id, 0, client_pubkey, request_uuid
FROM ticketbook_deposit;
CREATE TABLE partial_blinded_wallet_new
(
corresponding_deposit INTEGER NOT NULL REFERENCES ecash_deposit_usage (deposit_id),
epoch_id INTEGER NOT NULL,
expiration_date DATE NOT NULL,
node_id INTEGER NOT NULL,
created TIMESTAMP WITHOUT TIME ZONE NOT NULL,
blinded_signature BLOB NOT NULL
);
CREATE TABLE partial_blinded_wallet_failure_new
(
corresponding_deposit INTEGER NOT NULL REFERENCES ecash_deposit_usage (deposit_id),
epoch_id INTEGER NOT NULL,
expiration_date DATE NOT NULL,
node_id INTEGER NOT NULL,
created TIMESTAMP WITHOUT TIME ZONE NOT NULL,
failure_message TEXT NOT NULL
);
INSERT INTO partial_blinded_wallet_new
SELECT *
FROM partial_blinded_wallet;
INSERT INTO partial_blinded_wallet_failure_new
SELECT *
FROM partial_blinded_wallet_failure;
DROP TABLE partial_blinded_wallet;
DROP TABLE partial_blinded_wallet_failure;
DROP TABLE ticketbook_deposit;
ALTER TABLE partial_blinded_wallet_new
RENAME TO partial_blinded_wallet;
ALTER TABLE partial_blinded_wallet_failure_new
RENAME TO partial_blinded_wallet_failure;
@@ -10,6 +10,7 @@ use std::fs::create_dir_all;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;
use std::sync::OnceLock;
use std::time::Duration;
use tracing::info;
fn pretty_build_info_static() -> &'static str {
@@ -64,6 +65,23 @@ pub struct Cli {
)]
pub(crate) max_concurrent_deposits: usize,
/// Specify the size of the deposits buffer the credential proxy should have available at any time
/// (default: 256)
#[clap(
long,
env = "NYM_CREDENTIAL_PROXY_DEPOSITS_BUFFER",
default_value_t = 256
)]
pub(crate) deposits_buffer_size: usize,
#[clap(
long,
env = "NYM_CREDENTIAL_PROXY_QUORUM_CHECK_INTERVAL",
default_value = "5m",
value_parser = humantime::parse_duration
)]
pub(crate) quorum_check_interval: Duration,
#[clap(long, env = "NYM_CREDENTIAL_PROXY_PERSISTENT_STORAGE_STORAGE")]
pub(crate) persistent_storage_path: Option<PathBuf>,
}
@@ -90,10 +108,7 @@ impl Cli {
create_dir_all(parent).unwrap();
}
info!(
"setting the storage path path to {}",
default_path.display()
);
info!("setting the storage path to {}", default_path.display());
default_path
})
@@ -1,8 +1,7 @@
// Copyright 2024 Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::deposit_maker::{DepositRequest, DepositResponse};
use crate::error::VpnApiError;
use crate::error::CredentialProxyError;
use crate::http::state::ApiState;
use crate::storage::models::BlindedShares;
use futures::{stream, StreamExt};
@@ -11,52 +10,20 @@ use nym_credential_proxy_requests::api::v1::ticketbook::models::{
TicketbookWalletSharesResponse, WalletShare, WebhookTicketbookWalletShares,
WebhookTicketbookWalletSharesRequest,
};
use nym_credentials::IssuanceTicketBook;
use nym_credentials_interface::Base58;
use nym_crypto::asymmetric::ed25519;
use nym_validator_client::ecash::BlindSignRequestBody;
use nym_validator_client::nyxd::Coin;
use rand::rngs::OsRng;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime;
use tokio::sync::{oneshot, Mutex};
use tokio::time::{timeout, Instant};
use tracing::{debug, error, info, instrument, warn};
use tokio::sync::Mutex;
use tokio::time::timeout;
use tracing::{debug, error, info, instrument};
use uuid::Uuid;
// use the same type alias as our contract without importing the whole thing just for this single line
pub type NodeId = u64;
#[instrument(skip(state), ret, err(Display))]
async fn make_deposit(
state: &ApiState,
pub_key: ed25519::PublicKey,
deposit_amount: &Coin,
) -> Result<DepositResponse, VpnApiError> {
let start = Instant::now();
let (on_done_tx, on_done_rx) = oneshot::channel();
let request = DepositRequest::new(pub_key, deposit_amount, on_done_tx);
state.request_deposit(request).await;
let time_taken = start.elapsed();
let formatted = humantime::format_duration(time_taken);
let Ok(deposit_response) = on_done_rx.await else {
error!("failed to receive deposit response: the corresponding sender channel got dropped by the DepositMaker!");
return Err(VpnApiError::DepositFailure);
};
if time_taken > Duration::from_secs(20) {
warn!("attempting to resolve deposit request took {formatted}. perhaps the buffer is too small or the process/chain is overloaded?")
} else {
debug!("attempting to resolve deposit request took {formatted}")
}
deposit_response.ok_or(VpnApiError::DepositFailure)
}
#[instrument(
skip(state, request_data, request, requested_on),
fields(
@@ -69,13 +36,13 @@ pub(crate) async fn try_obtain_wallet_shares(
request: Uuid,
requested_on: OffsetDateTime,
request_data: TicketbookRequest,
) -> Result<Vec<WalletShare>, VpnApiError> {
let mut rng = OsRng;
let ed25519_keypair = ed25519::KeyPair::new(&mut rng);
) -> Result<Vec<WalletShare>, CredentialProxyError> {
// don't proceed if we don't have quorum available as the request will definitely fail
if !state.quorum_available() {
return Err(CredentialProxyError::UnavailableSigningQuorum);
}
let epoch = state.current_epoch_id().await?;
let deposit_amount = state.deposit_amount().await?;
let threshold = state.ecash_threshold(epoch).await?;
let expiration_date = request_data.expiration_date;
@@ -87,30 +54,11 @@ pub(crate) async fn try_obtain_wallet_shares(
.await?;
let ecash_api_clients = state.ecash_clients(epoch).await?.clone();
let DepositResponse {
deposit_id,
tx_hash,
} = make_deposit(state, *ed25519_keypair.public_key(), &deposit_amount).await?;
info!(deposit_id = %deposit_id, "deposit finished");
// store the deposit information so if we fail, we could perhaps still reuse it for another issuance
state
.storage()
.insert_deposit_data(
deposit_id,
tx_hash,
requested_on,
request,
deposit_amount,
&request_data.ecash_pubkey,
&ed25519_keypair,
)
let deposit_data = state
.get_deposit(request, requested_on, request_data.ecash_pubkey)
.await?;
let plaintext =
IssuanceTicketBook::request_plaintext(&request_data.withdrawal_request, deposit_id);
let signature = ed25519_keypair.private_key().sign(plaintext);
let deposit_id = deposit_data.deposit_id;
let signature = deposit_data.sign_ticketbook_plaintext(&request_data.withdrawal_request);
let credential_request = BlindSignRequestBody::new(
request_data.withdrawal_request.into(),
@@ -135,7 +83,7 @@ pub(crate) async fn try_obtain_wallet_shares(
client.api_client.blind_sign(&credential_request),
)
.await
.map_err(|_| VpnApiError::EcashApiRequestTimeout {
.map_err(|_| CredentialProxyError::EcashApiRequestTimeout {
client_repr: client.to_string(),
})
.and_then(|res| res.map_err(Into::into));
@@ -176,10 +124,14 @@ pub(crate) async fn try_obtain_wallet_shares(
let shares = wallet_shares.len();
if shares < threshold as usize {
return Err(VpnApiError::InsufficientNumberOfCredentials {
let err = CredentialProxyError::InsufficientNumberOfCredentials {
available: shares,
threshold,
});
};
state
.insert_deposit_usage_error(deposit_id, err.to_string())
.await;
return Err(err);
}
Ok(wallet_shares
@@ -199,12 +151,14 @@ async fn try_obtain_wallet_shares_async(
request_data: TicketbookRequest,
device_id: &str,
credential_id: &str,
) -> Result<Vec<WalletShare>, VpnApiError> {
) -> Result<Vec<WalletShare>, CredentialProxyError> {
let shares = match try_obtain_wallet_shares(state, request, requested_on, request_data).await {
Ok(shares) => shares,
Err(err) => {
let obtained = match err {
VpnApiError::InsufficientNumberOfCredentials { available, .. } => available,
CredentialProxyError::InsufficientNumberOfCredentials { available, .. } => {
available
}
_ => 0,
};
@@ -235,7 +189,7 @@ async fn try_obtain_blinded_ticketbook_async_inner(
request_data: TicketbookAsyncRequest,
params: TicketbookObtainQueryParams,
pending: &BlindedShares,
) -> Result<(), VpnApiError> {
) -> Result<(), CredentialProxyError> {
let epoch_id = state.current_epoch_id().await?;
let device_id = &request_data.device_id;
@@ -318,7 +272,7 @@ async fn try_trigger_webhook_request_for_error(
request_data: TicketbookAsyncRequest,
pending: &BlindedShares,
error_message: String,
) -> Result<(), VpnApiError> {
) -> Result<(), CredentialProxyError> {
let device_id = &request_data.device_id;
let credential_id = &request_data.credential_id;
let secret = request_data.secret.clone();
@@ -1,205 +0,0 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::VpnApiError;
use crate::http::state::ChainClient;
use nym_crypto::asymmetric::ed25519;
use nym_ecash_contract_common::deposit::DepositId;
use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData;
use nym_validator_client::nyxd::{Coin, Hash};
use tokio::sync::{mpsc, oneshot};
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, warn};
#[derive(Debug)]
pub(crate) struct DepositResponse {
pub tx_hash: Hash,
pub deposit_id: DepositId,
}
pub(crate) struct DepositRequest {
pubkey: ed25519::PublicKey,
deposit_amount: Coin,
on_done: oneshot::Sender<Option<DepositResponse>>,
}
impl DepositRequest {
pub(crate) fn new(
pubkey: ed25519::PublicKey,
deposit_amount: &Coin,
on_done: oneshot::Sender<Option<DepositResponse>>,
) -> Self {
DepositRequest {
pubkey,
deposit_amount: deposit_amount.clone(),
on_done,
}
}
}
pub(crate) type DepositRequestReceiver = mpsc::Receiver<DepositRequest>;
pub(crate) fn new_control_channels(
max_concurrent_deposits: usize,
) -> (DepositRequestSender, DepositRequestReceiver) {
let (tx, rx) = mpsc::channel(max_concurrent_deposits);
(tx.into(), rx)
}
#[derive(Debug, Clone)]
pub struct DepositRequestSender(mpsc::Sender<DepositRequest>);
impl From<mpsc::Sender<DepositRequest>> for DepositRequestSender {
fn from(inner: mpsc::Sender<DepositRequest>) -> Self {
DepositRequestSender(inner)
}
}
impl DepositRequestSender {
pub(crate) async fn request_deposit(&self, request: DepositRequest) {
if self.0.send(request).await.is_err() {
error!("failed to request deposit: the DepositMaker must have died!")
}
}
}
pub(crate) struct DepositMaker {
client: ChainClient,
max_concurrent_deposits: usize,
deposit_request_sender: DepositRequestSender,
deposit_request_receiver: DepositRequestReceiver,
short_sha: &'static str,
cancellation_token: CancellationToken,
}
impl DepositMaker {
pub(crate) fn new(
short_sha: &'static str,
client: ChainClient,
max_concurrent_deposits: usize,
cancellation_token: CancellationToken,
) -> Self {
let (deposit_request_sender, deposit_request_receiver) =
new_control_channels(max_concurrent_deposits);
DepositMaker {
client,
max_concurrent_deposits,
deposit_request_sender,
deposit_request_receiver,
short_sha,
cancellation_token,
}
}
pub(crate) fn deposit_request_sender(&self) -> DepositRequestSender {
self.deposit_request_sender.clone()
}
pub(crate) async fn process_deposit_requests(
&mut self,
requests: Vec<DepositRequest>,
) -> Result<(), VpnApiError> {
let chain_write_permit = self.client.start_chain_tx().await;
info!("starting deposits");
let mut contents = Vec::new();
let mut replies = Vec::new();
for request in requests {
// check if the channel is still open in case the receiver client has cancelled the request
if request.on_done.is_closed() {
warn!(
"the request for deposit from {} got cancelled",
request.pubkey
);
continue;
}
contents.push((request.pubkey.to_base58_string(), request.deposit_amount));
replies.push(request.on_done);
}
let deposits_res = chain_write_permit
.make_deposits(self.short_sha, contents)
.await;
let execute_res = match deposits_res {
Ok(res) => res,
Err(err) => {
// we have to let requesters know the deposit(s) failed
for reply in replies {
if reply.send(None).is_err() {
warn!("one of the deposit requesters has been terminated")
}
}
return Err(err);
}
};
let tx_hash = execute_res.transaction_hash;
info!("{} deposits made in transaction: {tx_hash}", replies.len());
let contract_data = match execute_res.to_contract_data() {
Ok(contract_data) => contract_data,
Err(err) => {
// that one is tricky. deposits technically got made, but we somehow failed to parse response,
// in this case terminate the proxy with 0 exit code so it wouldn't get automatically restarted
// because it requires some serious MANUAL intervention
error!("CRITICAL FAILURE: failed to parse out deposit information from the contract transaction. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually. error was: {err}");
self.cancellation_token.cancel();
return Err(VpnApiError::DepositFailure);
}
};
if contract_data.len() != replies.len() {
// another critical failure, that one should be quite impossible and thus has to be manually inspected
error!("CRITICAL FAILURE: failed to parse out all deposit information from the contract transaction. got {} responses while we sent {} deposits! either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually", contract_data.len(), replies.len());
self.cancellation_token.cancel();
return Err(VpnApiError::DepositFailure);
}
for (reply_channel, response) in replies.into_iter().zip(contract_data) {
let response_index = response.message_index;
let deposit_id = match response.parse_singleton_u32_contract_data() {
Ok(deposit_id) => deposit_id,
Err(err) => {
// another impossibility
error!("CRITICAL FAILURE: failed to parse out deposit id out of the response at index {response_index}: {err}. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually");
self.cancellation_token.cancel();
return Err(VpnApiError::DepositFailure);
}
};
if reply_channel
.send(Some(DepositResponse {
deposit_id,
tx_hash,
}))
.is_err()
{
warn!("one of the deposit requesters has been terminated. deposit {deposit_id} will remain unclaimed!");
// this shouldn't happen as the requester task shouldn't be killed, but it's not a critical failure
// we just lost some tokens, but it's not an undefined on-chain behaviour
}
}
Ok(())
}
pub async fn run_forever(mut self) {
info!("starting the deposit maker task");
loop {
let mut receive_buffer = Vec::with_capacity(self.max_concurrent_deposits);
tokio::select! {
_ = self.cancellation_token.cancelled() => {
break
}
received = self.deposit_request_receiver.recv_many(&mut receive_buffer, self.max_concurrent_deposits) => {
debug!("received {received} deposit requests");
if let Err(err) = self.process_deposit_requests(receive_buffer).await {
error!("failed to process received deposit requests: {err}")
}
}
}
}
}
}
@@ -0,0 +1,101 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::CredentialProxyError;
use crate::storage::models::StorableEcashDeposit;
use nym_compact_ecash::WithdrawalRequest;
use nym_credentials::IssuanceTicketBook;
use nym_crypto::asymmetric::ed25519;
use nym_validator_client::nyxd::{Coin, Hash};
use time::OffsetDateTime;
use zeroize::Zeroizing;
pub(crate) struct BufferedDeposit {
pub(crate) deposit_id: u32,
// note: this type implements `ZeroizeOnDrop`
pub(crate) ed25519_private_key: ed25519::PrivateKey,
}
impl TryFrom<StorableEcashDeposit> for BufferedDeposit {
type Error = CredentialProxyError;
fn try_from(deposit: StorableEcashDeposit) -> Result<Self, Self::Error> {
let ed25519_private_key = ed25519::PrivateKey::from_bytes(
deposit.ed25519_deposit_private_key.as_ref(),
)
.map_err(|err| CredentialProxyError::DatabaseInconsistency {
reason: format!("one of the stored deposit ed25519 private keys is malformed: {err}"),
})?;
Ok(BufferedDeposit {
deposit_id: deposit.deposit_id,
ed25519_private_key,
})
}
}
impl BufferedDeposit {
pub(crate) fn new(deposit_id: u32, ed25519_private_key: ed25519::PrivateKey) -> Self {
BufferedDeposit {
deposit_id,
ed25519_private_key,
}
}
pub(crate) fn sign_ticketbook_plaintext(
&self,
withdrawal_request: &WithdrawalRequest,
) -> ed25519::Signature {
let plaintext = IssuanceTicketBook::request_plaintext(withdrawal_request, self.deposit_id);
self.ed25519_private_key.sign(plaintext)
}
}
pub(crate) struct PerformedDeposits {
pub(crate) deposits_data: Vec<BufferedDeposit>,
// shared by all performed deposits as they were included in the same tx
pub(crate) tx_hash: Hash,
pub(crate) requested_on: OffsetDateTime,
pub(crate) deposit_amount: Coin,
}
impl PerformedDeposits {
pub(crate) fn to_storable(&self) -> Vec<StorableEcashDeposit> {
self.deposits_data
.iter()
.map(|d| StorableEcashDeposit {
deposit_id: d.deposit_id,
deposit_tx_hash: self.tx_hash.to_string(),
requested_on: self.requested_on,
deposit_amount: self.deposit_amount.to_string(),
ed25519_deposit_private_key: Zeroizing::new(d.ed25519_private_key.to_bytes()),
})
.collect()
}
}
pub(super) fn request_sizes(total: usize, max_request_size: usize) -> impl Iterator<Item = usize> {
(0..total)
.step_by(max_request_size)
.map(move |start| std::cmp::min(max_request_size, total - start))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_sizes_test() {
assert_eq!(
request_sizes(100, 32).collect::<Vec<_>>(),
vec![32, 32, 32, 4]
);
assert_eq!(request_sizes(10, 32).collect::<Vec<_>>(), vec![10]);
assert_eq!(request_sizes(32, 32).collect::<Vec<_>>(), vec![32]);
assert_eq!(request_sizes(33, 32).collect::<Vec<_>>(), vec![32, 1]);
assert_eq!(request_sizes(1, 32).collect::<Vec<_>>(), vec![1]);
}
}
@@ -0,0 +1,306 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::deposits_buffer::helpers::{request_sizes, BufferedDeposit, PerformedDeposits};
use crate::deposits_buffer::refill_task::RefillTask;
use crate::error::CredentialProxyError;
use crate::http::state::required_deposit_cache::RequiredDepositCache;
use crate::http::state::ChainClient;
use crate::storage::CredentialProxyStorage;
use nym_compact_ecash::PublicKeyUser;
use nym_crypto::asymmetric::ed25519;
use nym_ecash_contract_common::deposit::DepositId;
use nym_validator_client::nyxd::cosmwasm_client::ContractResponseData;
use nym_validator_client::nyxd::Coin;
use rand::rngs::OsRng;
use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime;
use tokio::sync::Mutex as AsyncMutex;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, instrument, warn};
use uuid::Uuid;
pub(crate) mod helpers;
mod refill_task;
// TODO: I guess make it configurable
const DEPOSITS_THRESHOLD_P: f32 = 0.1;
struct DepositsBufferInner {
client: ChainClient,
required_deposit_cache: RequiredDepositCache,
storage: CredentialProxyStorage,
target_amount: usize,
max_concurrent_deposits: usize,
unused_deposits: AsyncMutex<Vec<BufferedDeposit>>,
deposits_refill_task: RefillTask,
short_sha: &'static str,
cancellation_token: CancellationToken,
}
#[derive(Clone)]
pub(crate) struct DepositsBuffer {
inner: Arc<DepositsBufferInner>,
}
impl DepositsBuffer {
pub(crate) async fn new(
storage: CredentialProxyStorage,
client: ChainClient,
required_deposit_cache: RequiredDepositCache,
short_sha: &'static str,
target_amount: usize,
max_concurrent_deposits: usize,
cancellation_token: CancellationToken,
) -> Result<Self, CredentialProxyError> {
let unused_deposits = storage.load_unused_deposits().await?;
info!("managed to load {} deposits", unused_deposits.len());
Ok(DepositsBuffer {
inner: Arc::new(DepositsBufferInner {
client,
required_deposit_cache,
storage,
target_amount,
max_concurrent_deposits,
unused_deposits: AsyncMutex::new(unused_deposits),
deposits_refill_task: RefillTask::default(),
short_sha,
cancellation_token,
}),
})
}
async fn deposit_amount(&self) -> Result<Coin, CredentialProxyError> {
self.inner
.required_deposit_cache
.get_or_update(&self.inner.client)
.await
}
#[instrument(skip(self), err(Display))]
async fn make_deposits_request(
&self,
amount: usize,
) -> Result<PerformedDeposits, CredentialProxyError> {
let requested_on = OffsetDateTime::now_utc();
let chain_write_permit = self.inner.client.start_chain_tx().await;
let mut rng = OsRng;
let deposit_amount = self.deposit_amount().await?;
let keys = (0..amount)
.map(|_| ed25519::PrivateKey::new(&mut rng))
.collect::<Vec<_>>();
info!("starting {amount} deposits");
let mut contents = Vec::new();
for key in &keys {
let public_key: ed25519::PublicKey = key.into();
contents.push((public_key.to_base58_string(), deposit_amount.clone()));
}
let execute_res = chain_write_permit
.make_deposits(self.inner.short_sha, contents)
.await?;
let tx_hash = execute_res.transaction_hash;
info!("{amount} deposits made in transaction: {tx_hash}");
let contract_data = match execute_res.to_contract_data() {
Ok(contract_data) => contract_data,
Err(err) => {
// that one is tricky. deposits technically got made, but we somehow failed to parse response,
// in this case terminate the proxy with 0 exit code so it wouldn't get automatically restarted
// because it requires some serious MANUAL intervention
error!("CRITICAL FAILURE: failed to parse out deposit information from the contract transaction. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually. error was: {err}");
self.inner.cancellation_token.cancel();
return Err(CredentialProxyError::DepositFailure);
}
};
if contract_data.len() != amount {
// another critical failure, that one should be quite impossible and thus has to be manually inspected
error!("CRITICAL FAILURE: failed to parse out all deposit information from the contract transaction. got {} responses while we sent {amount} deposits! either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually", contract_data.len());
self.inner.cancellation_token.cancel();
return Err(CredentialProxyError::DepositFailure);
}
let mut deposits_data = Vec::new();
for (key, response) in keys.into_iter().zip(contract_data) {
let response_index = response.message_index;
let deposit_id = match response.parse_singleton_u32_contract_data() {
Ok(deposit_id) => deposit_id,
Err(err) => {
// another impossibility
error!("CRITICAL FAILURE: failed to parse out deposit id out of the response at index {response_index}: {err}. either the chain got upgraded and the schema changed or the ecash contract got changed! terminating the process. it has to be inspected manually");
self.inner.cancellation_token.cancel();
return Err(CredentialProxyError::DepositFailure);
}
};
deposits_data.push(BufferedDeposit::new(deposit_id, key));
}
Ok(PerformedDeposits {
deposits_data,
tx_hash,
requested_on,
deposit_amount,
})
}
async fn insert_new_deposits(
&self,
mut deposits: PerformedDeposits,
) -> Result<(), CredentialProxyError> {
// 1. insert into the db
self.inner.storage.insert_new_deposits(&deposits).await?;
// 2. update the buffer
self.inner
.unused_deposits
.lock()
.await
.append(&mut deposits.deposits_data);
Ok(())
}
/// Start refilling our deposit buffer.
/// It chunks the amount required based on the configured maximum request size
/// and updates global state after each successful transaction.
async fn refill_deposits(&self) -> Result<(), CredentialProxyError> {
let available = self.inner.unused_deposits.lock().await.len();
let target = self.deposits_upper_threshold();
let to_request = target - available;
for request_chunk in request_sizes(to_request, self.inner.max_concurrent_deposits) {
// note: we check for cancellation between individual requests
// as opposed to wrapping that in tokio::select! so that we would never abandon chain operations
// as we wouldn't want to lose funds
if self.inner.cancellation_token.is_cancelled() {
info!("received cancellation during deposits refilling");
return Ok(());
}
// make sure to insert deposits into db/vec as we get them so on initial run,
// we'd start trickling down data as soon as possible
let deposits = self.make_deposits_request(request_chunk).await?;
self.insert_new_deposits(deposits).await?;
}
Ok(())
}
// if we're here, we know we're below the threshold
fn maybe_refill_deposits(&self) {
if let Some(mut guard) = self.inner.deposits_refill_task.try_get_new_task_guard() {
let this = self.clone();
*guard = Some(tokio::spawn(async move { this.refill_deposits().await }));
}
}
fn deposits_lower_threshold(&self) -> usize {
self.inner.target_amount - (self.inner.target_amount as f32 * DEPOSITS_THRESHOLD_P) as usize
}
fn deposits_upper_threshold(&self) -> usize {
self.inner.target_amount + (self.inner.target_amount as f32 * DEPOSITS_THRESHOLD_P) as usize
}
async fn mark_deposit_as_used(
&self,
deposit_id: DepositId,
requested_on: OffsetDateTime,
client_pubkey: PublicKeyUser,
request_uuid: Uuid,
) -> Result<(), CredentialProxyError> {
self.inner
.storage
.insert_deposit_usage(deposit_id, requested_on, client_pubkey, request_uuid)
.await
}
async fn wait_for_deposit(
&self,
request_uuid: Uuid,
requested_on: OffsetDateTime,
client_pubkey: PublicKeyUser,
) -> Result<BufferedDeposit, CredentialProxyError> {
loop {
tokio::time::sleep(Duration::from_millis(500)).await;
if let Some(buffered_deposit) = self.inner.unused_deposits.lock().await.pop() {
// if the db call fails, we technically don't lose the deposit (we'll 'recover' it on restart)
self.mark_deposit_as_used(
buffered_deposit.deposit_id,
requested_on,
client_pubkey,
request_uuid,
)
.await?;
return Ok(buffered_deposit);
} else {
// make sure there's always a task working in the background in case deposits get used up too quickly
self.maybe_refill_deposits()
}
}
}
pub(crate) async fn get_valid_deposit(
&self,
request_uuid: Uuid,
requested_on: OffsetDateTime,
client_pubkey: PublicKeyUser,
) -> Result<BufferedDeposit, CredentialProxyError> {
let mut deposits_guard = self.inner.unused_deposits.lock().await;
let deposits_available = deposits_guard.len();
debug!("we have {deposits_available} unused deposits available");
let maybe_deposit = deposits_guard.pop();
drop(deposits_guard);
if deposits_available < self.deposits_lower_threshold() {
// if we're below threshold, start refill task
self.maybe_refill_deposits()
}
match maybe_deposit {
None => {
warn!("we currently don't have any usable deposits! are we using them up faster than we request them?");
// we have to wait until refill task has completed (either initiated by this or another fn call)
self.wait_for_deposit(request_uuid, requested_on, client_pubkey)
.await
}
Some(buffered_deposit) => {
self.mark_deposit_as_used(
buffered_deposit.deposit_id,
requested_on,
client_pubkey,
request_uuid,
)
.await?;
Ok(buffered_deposit)
}
}
}
pub(crate) async fn wait_for_shutdown(&self) {
let task_handle = self.inner.deposits_refill_task.take_task_join_handle();
if let Some(task_handle) = task_handle {
if !task_handle.is_finished() {
info!("the deposit refill task is currently in progress - waiting for the current transaction to finish before concluding shutdown");
let _ = task_handle.await;
}
}
}
}
impl DepositsBufferInner {
//
}
@@ -0,0 +1,56 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::CredentialProxyError;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Mutex as StdMutex, MutexGuard};
use tokio::task::JoinHandle;
use tracing::{debug, error};
pub(super) type RefillTaskResult = Result<(), CredentialProxyError>;
#[derive(Default)]
pub(super) struct RefillTask {
// note that we can only have a single transaction in progress (or it'd mess up with our sequence numbers)
// if we find that we're using up deposits more quickly than we're refilling them,
// we'll have to increase the number of deposits per transaction
join_handle: StdMutex<Option<JoinHandle<RefillTaskResult>>>,
in_progress: AtomicBool,
}
impl RefillTask {
/// Attempt to set the `in_progress` value to `true` if it's not already `true`.
/// Returns boolean indicating whether it was successful
fn try_set_in_progress(&self) -> bool {
self.in_progress
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
}
pub(super) fn try_get_new_task_guard(
&self,
) -> Option<MutexGuard<'_, Option<JoinHandle<RefillTaskResult>>>> {
// sanity check for concurrent request
if !self.try_set_in_progress() {
debug!("another task has already started deposit refill request");
return None;
}
#[allow(clippy::expect_used)]
let guard = self.join_handle.lock().expect("mutex got poisoned");
if let Some(existing_handle) = guard.as_ref() {
if !existing_handle.is_finished() {
error!("CRITICAL BUG: there was already a deposit refill task spawned that hasn't yet finished")
}
}
Some(guard)
}
pub(super) fn take_task_join_handle(&self) -> Option<JoinHandle<RefillTaskResult>> {
#[allow(clippy::expect_used)]
self.join_handle.lock().expect("mutex got poisoned").take()
}
}
@@ -1,6 +1,7 @@
// Copyright 2024 Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_ecash_signer_check::SignerCheckError;
use nym_validator_client::coconut::EcashApiError;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::error::NyxdError;
@@ -10,7 +11,7 @@ use thiserror::Error;
use time::OffsetDateTime;
#[derive(Debug, Error)]
pub enum VpnApiError {
pub enum CredentialProxyError {
#[error("encountered an internal io error: {source}")]
IoError {
#[from]
@@ -118,11 +119,20 @@ pub enum VpnApiError {
#[error("failed to create deposit")]
DepositFailure,
#[error("can't obtain sufficient number of credential shares due to unavailable quorum")]
UnavailableSigningQuorum,
#[error("failed to perform quorum check: {source}")]
QuorumCheckFailure {
#[from]
source: SignerCheckError,
},
}
impl VpnApiError {
pub fn database_inconsistency<S: Into<String>>(reason: S) -> VpnApiError {
VpnApiError::DatabaseInconsistency {
impl CredentialProxyError {
pub fn database_inconsistency<S: Into<String>>(reason: S) -> CredentialProxyError {
CredentialProxyError::DatabaseInconsistency {
reason: reason.into(),
}
}
@@ -6,15 +6,17 @@ use time::OffsetDateTime;
use tokio_util::sync::CancellationToken;
use tracing::{debug, error, info, warn};
use crate::deposits_buffer::DepositsBuffer;
use crate::http::state::required_deposit_cache::RequiredDepositCache;
use crate::quorum_checker::QuorumStateChecker;
use crate::{
cli::Cli,
deposit_maker::DepositMaker,
error::VpnApiError,
error::CredentialProxyError,
http::{
state::{ApiState, ChainClient},
HttpServer,
},
storage::VpnApiStorage,
storage::CredentialProxyStorage,
tasks::StoragePruner,
};
@@ -77,6 +79,7 @@ pub async fn wait_for_signal() {
}
}
#[allow(clippy::panic)]
fn build_sha_short() -> &'static str {
let bin_info = bin_info!();
if bin_info.commit_sha.len() < 7 {
@@ -91,30 +94,46 @@ fn build_sha_short() -> &'static str {
&bin_info.commit_sha[..7]
}
pub(crate) async fn run_api(cli: Cli) -> Result<(), VpnApiError> {
pub(crate) async fn run_api(cli: Cli) -> Result<(), CredentialProxyError> {
// create the tasks
let bind_address = cli.bind_address();
let storage = VpnApiStorage::init(cli.persistent_storage_path()).await?;
let storage = CredentialProxyStorage::init(cli.persistent_storage_path()).await?;
let mnemonic = cli.mnemonic;
let auth_token = cli.http_auth_token;
let webhook_cfg = cli.webhook;
let chain_client = ChainClient::new(mnemonic)?;
let cancellation_token = CancellationToken::new();
let deposit_maker = DepositMaker::new(
build_sha_short(),
let required_deposit_cache = RequiredDepositCache::default();
let quorum_state_checker = QuorumStateChecker::new(
chain_client.clone(),
cli.quorum_check_interval,
cancellation_token.clone(),
)
.await?;
let quorum_state = quorum_state_checker.quorum_state_ref();
let deposits_buffer = DepositsBuffer::new(
storage.clone(),
chain_client.clone(),
required_deposit_cache.clone(),
build_sha_short(),
cli.deposits_buffer_size,
cli.max_concurrent_deposits,
cancellation_token.clone(),
);
)
.await?;
let deposit_request_sender = deposit_maker.deposit_request_sender();
// let deposit_request_sender = deposit_maker.deposit_request_sender();
let api_state = ApiState::new(
storage.clone(),
quorum_state,
webhook_cfg,
chain_client,
deposit_request_sender,
deposits_buffer,
required_deposit_cache,
cancellation_token.clone(),
)
.await?;
@@ -129,7 +148,7 @@ pub(crate) async fn run_api(cli: Cli) -> Result<(), VpnApiError> {
// spawn all the tasks
api_state.try_spawn(http_server.run_forever());
api_state.try_spawn(storage_pruner.run_forever());
api_state.try_spawn(deposit_maker.run_forever());
api_state.try_spawn(quorum_state_checker.run_forever());
// wait for cancel signal (SIGINT, SIGTERM or SIGQUIT)
wait_for_signal().await;
@@ -1,7 +1,7 @@
// Copyright 2024 Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::VpnApiError;
use crate::error::CredentialProxyError;
use crate::http::types::RequestError;
use axum::http::StatusCode;
use rand::rngs::OsRng;
@@ -16,7 +16,7 @@ pub fn random_uuid() -> Uuid {
Uuid::from_bytes(bytes)
}
pub fn db_failure<T>(err: VpnApiError, uuid: Uuid) -> Result<T, RequestError> {
pub fn db_failure<T>(err: CredentialProxyError, uuid: Uuid) -> Result<T, RequestError> {
warn!("db failure: {err}");
Err(RequestError::new_with_uuid(
format!("oh no, something went wrong {err}"),
@@ -1,7 +1,7 @@
// Copyright 2024 Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::VpnApiError;
use crate::error::CredentialProxyError;
use crate::http::router::build_router;
use crate::http::state::ApiState;
use axum::Router;
@@ -34,13 +34,13 @@ impl HttpServer {
}
}
pub async fn run_forever(self) -> Result<(), VpnApiError> {
pub async fn run_forever(self) -> Result<(), CredentialProxyError> {
let address = self.bind_address;
info!("starting the http server on http://{address}");
let listener = tokio::net::TcpListener::bind(address)
.await
.map_err(|source| VpnApiError::SocketBindFailure { address, source })?;
.map_err(|source| CredentialProxyError::SocketBindFailure { address, source })?;
let cancellation = self.cancellation;
@@ -51,6 +51,6 @@ impl HttpServer {
)
.with_graceful_shutdown(async move { cancellation.cancelled().await })
.await
.map_err(|source| VpnApiError::HttpServerFailure { source })
.map_err(|source| CredentialProxyError::HttpServerFailure { source })
}
}
@@ -1,7 +1,7 @@
// Copyright 2024 Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::VpnApiError;
use crate::error::CredentialProxyError;
use crate::http::helpers::{db_failure, random_uuid};
use crate::http::router::api::v1::ticketbook::FormattedTicketbookWalletSharesResponse;
use crate::http::state::ApiState;
@@ -35,7 +35,7 @@ async fn shares_to_response(
let threshold = state.response_ecash_threshold(uuid, epoch_id).await?;
if shares.len() < threshold as usize {
return Err(RequestError::new_server_error(
VpnApiError::InsufficientNumberOfCredentials {
CredentialProxyError::InsufficientNumberOfCredentials {
available: shares.len(),
threshold,
},
@@ -1,15 +1,18 @@
// Copyright 2024 Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::deposit_maker::{DepositRequest, DepositRequestSender};
use crate::error::VpnApiError;
use crate::deposits_buffer::helpers::BufferedDeposit;
use crate::deposits_buffer::DepositsBuffer;
use crate::error::CredentialProxyError;
use crate::helpers::LockTimer;
use crate::http::state::required_deposit_cache::RequiredDepositCache;
use crate::http::types::RequestError;
use crate::nym_api_helpers::{
ensure_sane_expiration_date, query_all_threshold_apis, CachedEpoch, CachedImmutableEpochItem,
CachedImmutableItems,
};
use crate::storage::VpnApiStorage;
use crate::quorum_checker::QuorumState;
use crate::storage::CredentialProxyStorage;
use crate::webhook::ZkNymWebHookConfig;
use axum::http::StatusCode;
use bip39::Mnemonic;
@@ -19,7 +22,7 @@ use nym_compact_ecash::scheme::coin_indices_signatures::{
use nym_compact_ecash::scheme::expiration_date_signatures::{
aggregate_annotated_expiration_signatures, ExpirationDateSignatureShare,
};
use nym_compact_ecash::Base58;
use nym_compact_ecash::{Base58, PublicKeyUser};
use nym_credential_proxy_requests::api::v1::ticketbook::models::{
AggregatedCoinIndicesSignaturesResponse, AggregatedExpirationDateSignaturesResponse,
MasterVerificationKeyResponse,
@@ -29,12 +32,13 @@ use nym_credentials::{
AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey,
};
use nym_credentials_interface::VerificationKeyAuth;
use nym_ecash_contract_common::deposit::DepositId;
use nym_ecash_contract_common::msg::ExecuteMsg;
use nym_validator_client::coconut::EcashApiError;
use nym_validator_client::nym_api::EpochId;
use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch;
use nym_validator_client::nyxd::contract_traits::{
DkgQueryClient, EcashQueryClient, NymContractsProvider, PagedDkgQueryClient,
DkgQueryClient, NymContractsProvider, PagedDkgQueryClient,
};
use nym_validator_client::nyxd::cosmwasm_client::types::ExecuteResult;
use nym_validator_client::nyxd::{Coin, CosmWasmClient, NyxdClient};
@@ -49,33 +53,46 @@ use tokio::task::JoinHandle;
use tokio::time::Instant;
use tokio_util::sync::CancellationToken;
use tokio_util::task::TaskTracker;
use tracing::{debug, info, warn};
use tracing::{debug, error, info, instrument, warn};
use uuid::Uuid;
pub(crate) mod required_deposit_cache;
// currently we need to hold our keypair so that we could request a freepass credential
#[derive(Clone)]
pub struct ApiState {
inner: Arc<ApiStateInner>,
inner: Arc<CredentialProxyStateInner>,
}
// a lot of functionalities, mostly to do with caching and storage is just copy-pasted from nym-api,
// since we have to do more or less the same work
impl ApiState {
pub async fn new(
storage: VpnApiStorage,
pub(crate) async fn new(
storage: CredentialProxyStorage,
quorum_state: QuorumState,
zk_nym_web_hook_config: ZkNymWebHookConfig,
client: ChainClient,
deposit_requester: DepositRequestSender,
deposits_buffer: DepositsBuffer,
required_deposit_cache: RequiredDepositCache,
cancellation_token: CancellationToken,
) -> Result<Self, VpnApiError> {
) -> Result<Self, CredentialProxyError> {
let state = ApiState {
inner: Arc::new(ApiStateInner {
inner: Arc::new(CredentialProxyStateInner {
storage,
client,
ecash_state: EcashState::default(),
ecash_state: EcashState {
required_deposit_cache,
quorum_state,
cached_epoch: Default::default(),
master_verification_key: Default::default(),
threshold_values: Default::default(),
epoch_clients: Default::default(),
coin_index_signatures: Default::default(),
expiration_date_signatures: Default::default(),
},
zk_nym_web_hook_config,
task_tracker: TaskTracker::new(),
deposit_requester,
deposits_buffer,
cancellation_token,
}),
};
@@ -88,7 +105,7 @@ impl ApiState {
Ok(state)
}
async fn build_initial_cache(&self) -> Result<(), VpnApiError> {
async fn build_initial_cache(&self) -> Result<(), CredentialProxyError> {
let today = ecash_today().date();
let epoch_id = self.current_epoch_id().await?;
@@ -123,6 +140,7 @@ impl ApiState {
pub(crate) async fn cancel_and_wait(&self) {
self.inner.cancellation_token.cancel();
self.inner.deposits_buffer.wait_for_shutdown().await;
self.inner.task_tracker.wait().await
}
@@ -130,7 +148,11 @@ impl ApiState {
&self.inner.zk_nym_web_hook_config
}
async fn ensure_credentials_issuable(&self) -> Result<(), VpnApiError> {
pub(crate) fn quorum_available(&self) -> bool {
self.inner.ecash_state.quorum_state.available()
}
async fn ensure_credentials_issuable(&self) -> Result<(), CredentialProxyError> {
let epoch = self.current_epoch().await?;
if epoch.state.is_final() {
@@ -141,41 +163,29 @@ impl ApiState {
#[allow(clippy::unwrap_used)]
let finish_dt = OffsetDateTime::from_unix_timestamp(final_timestamp as i64).unwrap();
Err(VpnApiError::CredentialsNotYetIssuable {
Err(CredentialProxyError::CredentialsNotYetIssuable {
availability: finish_dt,
})
} else if epoch.state.is_waiting_initialisation() {
Err(VpnApiError::UninitialisedDkg)
Err(CredentialProxyError::UninitialisedDkg)
} else {
Err(VpnApiError::UnknownEcashFailure)
Err(CredentialProxyError::UnknownEcashFailure)
}
}
pub(crate) fn storage(&self) -> &VpnApiStorage {
pub(crate) fn storage(&self) -> &CredentialProxyStorage {
&self.inner.storage
}
pub async fn deposit_amount(&self) -> Result<Coin, VpnApiError> {
let read_guard = self.inner.ecash_state.required_deposit_cache.read().await;
if read_guard.is_valid() {
return Ok(read_guard.required_amount.clone());
}
// update cache
drop(read_guard);
let mut write_guard = self.inner.ecash_state.required_deposit_cache.write().await;
let deposit_amount = self
.query_chain()
pub async fn deposit_amount(&self) -> Result<Coin, CredentialProxyError> {
self.inner
.ecash_state
.required_deposit_cache
.get_or_update(&self.inner.client)
.await
.get_required_deposit_amount()
.await?;
write_guard.update(deposit_amount.clone().into());
Ok(deposit_amount.into())
}
async fn current_epoch(&self) -> Result<Epoch, VpnApiError> {
async fn current_epoch(&self) -> Result<Epoch, CredentialProxyError> {
let read_guard = self.inner.ecash_state.cached_epoch.read().await;
if read_guard.is_valid() {
return Ok(read_guard.current_epoch);
@@ -190,7 +200,7 @@ impl ApiState {
Ok(epoch)
}
pub async fn current_epoch_id(&self) -> Result<EpochId, VpnApiError> {
pub async fn current_epoch_id(&self) -> Result<EpochId, CredentialProxyError> {
let read_guard = self.inner.ecash_state.cached_epoch.read().await;
if read_guard.is_valid() {
return Ok(read_guard.current_epoch.epoch_id);
@@ -209,16 +219,38 @@ impl ApiState {
self.inner.client.query_chain().await
}
pub(crate) async fn request_deposit(&self, request: DepositRequest) {
pub(crate) async fn get_deposit(
&self,
request_uuid: Uuid,
requested_on: OffsetDateTime,
client_pubkey: PublicKeyUser,
) -> Result<BufferedDeposit, CredentialProxyError> {
let start = Instant::now();
self.inner.deposit_requester.request_deposit(request).await;
let deposit = self
.inner
.deposits_buffer
.get_valid_deposit(request_uuid, requested_on, client_pubkey)
.await;
let time_taken = start.elapsed();
let formatted = humantime::format_duration(time_taken);
if time_taken > Duration::from_secs(10) {
warn!("attempting to push new deposit request onto the queue took {formatted}. perhaps the buffer is too small or the process/chain is overloaded?")
warn!("attempting to get buffered deposit took {formatted}. perhaps the buffer is too small or the process/chain is overloaded?")
} else {
debug!("attempting to push new deposit request onto the queue took {formatted}")
debug!("attempting to get buffered deposit took {formatted}")
};
deposit
}
pub(crate) async fn insert_deposit_usage_error(&self, deposit_id: DepositId, error: String) {
if let Err(err) = self
.inner
.storage
.insert_deposit_usage_error(deposit_id, error)
.await
{
error!("failed to insert information about deposit (id: {deposit_id}) usage failure: {err}")
}
}
@@ -235,7 +267,7 @@ impl ApiState {
Option<AggregatedExpirationDateSignaturesResponse>,
Option<AggregatedCoinIndicesSignaturesResponse>,
),
VpnApiError,
CredentialProxyError,
> {
let master_verification_key = if include_master_verification_key {
debug!("including master verification key in the response");
@@ -338,7 +370,7 @@ impl ApiState {
pub(crate) async fn ecash_clients(
&self,
epoch_id: EpochId,
) -> Result<RwLockReadGuard<'_, Vec<EcashApiClient>>, VpnApiError> {
) -> Result<RwLockReadGuard<'_, Vec<EcashApiClient>>, CredentialProxyError> {
self.inner
.ecash_state
.epoch_clients
@@ -355,7 +387,10 @@ impl ApiState {
.await
}
pub(crate) async fn ecash_threshold(&self, epoch_id: EpochId) -> Result<u64, VpnApiError> {
pub(crate) async fn ecash_threshold(
&self,
epoch_id: EpochId,
) -> Result<u64, CredentialProxyError> {
self.inner
.ecash_state
.threshold_values
@@ -368,7 +403,7 @@ impl ApiState {
{
Ok(threshold)
} else {
Err(VpnApiError::UnavailableThreshold { epoch_id })
Err(CredentialProxyError::UnavailableThreshold { epoch_id })
}
})
.await
@@ -388,7 +423,7 @@ impl ApiState {
pub(crate) async fn master_verification_key(
&self,
epoch_id: Option<EpochId>,
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, VpnApiError> {
) -> Result<RwLockReadGuard<'_, VerificationKeyAuth>, CredentialProxyError> {
let epoch_id = match epoch_id {
Some(id) => id,
None => self.current_epoch_id().await?,
@@ -415,7 +450,7 @@ impl ApiState {
let threshold = self.ecash_threshold(epoch_id).await?;
if all_apis.len() < threshold as usize {
return Err(VpnApiError::InsufficientNumberOfSigners {
return Err(CredentialProxyError::InsufficientNumberOfSigners {
threshold,
available: all_apis.len(),
});
@@ -442,7 +477,7 @@ impl ApiState {
pub(crate) async fn master_coin_index_signatures(
&self,
epoch_id: Option<EpochId>,
) -> Result<RwLockReadGuard<'_, AggregatedCoinIndicesSignatures>, VpnApiError> {
) -> Result<RwLockReadGuard<'_, AggregatedCoinIndicesSignatures>, CredentialProxyError> {
let epoch_id = match epoch_id {
Some(id) => id,
None => self.current_epoch_id().await?,
@@ -519,7 +554,7 @@ impl ApiState {
&self,
epoch_id: EpochId,
expiration_date: Date,
) -> Result<RwLockReadGuard<'_, AggregatedExpirationDateSignatures>, VpnApiError> {
) -> Result<RwLockReadGuard<'_, AggregatedExpirationDateSignatures>, CredentialProxyError> {
self.inner
.ecash_state
.expiration_date_signatures
@@ -598,25 +633,25 @@ impl ApiState {
pub struct ChainClient(Arc<RwLock<DirectSigningHttpRpcNyxdClient>>);
impl ChainClient {
pub fn new(mnemonic: Mnemonic) -> Result<Self, VpnApiError> {
pub fn new(mnemonic: Mnemonic) -> Result<Self, CredentialProxyError> {
let network_details = nym_network_defaults::NymNetworkDetails::new_from_env();
let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?;
let nyxd_url = network_details
.endpoints
.first()
.ok_or_else(|| VpnApiError::NoNyxEndpointsAvailable)?
.ok_or_else(|| CredentialProxyError::NoNyxEndpointsAvailable)?
.nyxd_url
.as_str();
let client = NyxdClient::connect_with_mnemonic(client_config, nyxd_url, mnemonic)?;
if client.ecash_contract_address().is_none() {
return Err(VpnApiError::UnavailableEcashContract);
return Err(CredentialProxyError::UnavailableEcashContract);
}
if client.dkg_contract_address().is_none() {
return Err(VpnApiError::UnavailableDKGContract);
return Err(CredentialProxyError::UnavailableDKGContract);
}
Ok(ChainClient(Arc::new(RwLock::new(client))))
@@ -637,14 +672,12 @@ impl ChainClient {
}
}
//
struct ApiStateInner {
storage: VpnApiStorage,
struct CredentialProxyStateInner {
storage: CredentialProxyStorage,
client: ChainClient,
deposit_requester: DepositRequestSender,
deposits_buffer: DepositsBuffer,
zk_nym_web_hook_config: ZkNymWebHookConfig,
@@ -655,39 +688,10 @@ struct ApiStateInner {
cancellation_token: CancellationToken,
}
pub(crate) struct CachedDeposit {
valid_until: OffsetDateTime,
required_amount: Coin,
}
impl CachedDeposit {
const MAX_VALIDITY: time::Duration = time::Duration::MINUTE;
fn is_valid(&self) -> bool {
self.valid_until > OffsetDateTime::now_utc()
}
fn update(&mut self, required_amount: Coin) {
self.valid_until = OffsetDateTime::now_utc() + Self::MAX_VALIDITY;
self.required_amount = required_amount;
}
}
impl Default for CachedDeposit {
fn default() -> Self {
CachedDeposit {
valid_until: OffsetDateTime::UNIX_EPOCH,
required_amount: Coin {
amount: u128::MAX,
denom: "unym".to_string(),
},
}
}
}
#[derive(Default)]
pub(crate) struct EcashState {
pub(crate) required_deposit_cache: RwLock<CachedDeposit>,
pub(crate) required_deposit_cache: RequiredDepositCache,
pub(crate) quorum_state: QuorumState,
pub(crate) cached_epoch: RwLock<CachedEpoch>,
@@ -714,11 +718,12 @@ pub(crate) struct ChainWritePermit<'a> {
}
impl ChainWritePermit<'_> {
#[instrument(skip(self, short_sha, info), err(Display))]
pub(crate) async fn make_deposits(
self,
short_sha: &'static str,
info: Vec<(String, Coin)>,
) -> Result<ExecuteResult, VpnApiError> {
) -> Result<ExecuteResult, CredentialProxyError> {
let address = self.inner.address();
let starting_sequence = self.inner.get_sequence(&address).await?.sequence;
@@ -727,7 +732,7 @@ impl ChainWritePermit<'_> {
let ecash_contract = self
.inner
.ecash_contract_address()
.ok_or(VpnApiError::UnavailableEcashContract)?;
.ok_or(CredentialProxyError::UnavailableEcashContract)?;
let deposit_messages = info
.into_iter()
.map(|(identity_key, amount)| {
@@ -0,0 +1,71 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::CredentialProxyError;
use crate::http::state::ChainClient;
use nym_validator_client::nyxd::contract_traits::EcashQueryClient;
use nym_validator_client::nyxd::Coin;
use std::sync::Arc;
use time::OffsetDateTime;
use tokio::sync::RwLock;
pub(crate) struct CachedDeposit {
valid_until: OffsetDateTime,
required_amount: Coin,
}
impl CachedDeposit {
const MAX_VALIDITY: time::Duration = time::Duration::MINUTE;
fn is_valid(&self) -> bool {
self.valid_until > OffsetDateTime::now_utc()
}
fn update(&mut self, required_amount: Coin) {
self.valid_until = OffsetDateTime::now_utc() + Self::MAX_VALIDITY;
self.required_amount = required_amount;
}
}
impl Default for CachedDeposit {
fn default() -> Self {
CachedDeposit {
valid_until: OffsetDateTime::UNIX_EPOCH,
required_amount: Coin {
amount: u128::MAX,
denom: "unym".to_string(),
},
}
}
}
#[derive(Clone, Default)]
pub(crate) struct RequiredDepositCache {
inner: Arc<RwLock<CachedDeposit>>,
}
impl RequiredDepositCache {
pub(crate) async fn get_or_update(
&self,
chain_client: &ChainClient,
) -> Result<Coin, CredentialProxyError> {
let read_guard = self.inner.read().await;
if read_guard.is_valid() {
return Ok(read_guard.required_amount.clone());
}
// update cache
drop(read_guard);
let mut write_guard = self.inner.write().await;
let deposit_amount = chain_client
.query_chain()
.await
.get_required_deposit_amount()
.await?;
let nym_coin: Coin = deposit_amount.into();
write_guard.update(nym_coin.clone());
Ok(nym_coin)
}
}
@@ -1,7 +1,7 @@
// Copyright 2024 Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use crate::error::VpnApiError;
use crate::error::CredentialProxyError;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
@@ -35,7 +35,7 @@ impl RequestError {
}
}
pub fn new_server_error(err: VpnApiError, uuid: Uuid) -> Self {
pub fn new_server_error(err: CredentialProxyError, uuid: Uuid) -> Self {
RequestError::new_with_uuid(err.to_string(), uuid, StatusCode::INTERNAL_SERVER_ERROR)
}
@@ -1,11 +1,6 @@
// Copyright 2024 Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
#![warn(clippy::expect_used)]
#![warn(clippy::unwrap_used)]
#![warn(clippy::todo)]
#![warn(clippy::dbg_macro)]
cfg_if::cfg_if! {
if #[cfg(unix)] {
use crate::cli::Cli;
@@ -18,7 +13,6 @@ cfg_if::cfg_if! {
pub mod cli;
pub mod config;
pub mod credentials;
mod deposit_maker;
pub mod error;
pub mod helpers;
pub mod http;
@@ -26,6 +20,8 @@ cfg_if::cfg_if! {
pub mod storage;
pub mod tasks;
mod webhook;
mod deposits_buffer;
mod quorum_checker;
}
}
@@ -55,5 +51,6 @@ async fn main() -> anyhow::Result<()> {
#[tokio::main]
async fn main() -> anyhow::Result<()> {
eprintln!("This tool is only supported on Unix systems");
#[allow(clippy::exit)]
std::process::exit(1)
}
@@ -4,7 +4,7 @@
// TODO: this was just copied from nym-api;
// it should have been therefore extracted to a common crate instead and imported as dependency
use crate::error::VpnApiError;
use crate::error::CredentialProxyError;
use futures::{stream, StreamExt};
use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime};
use nym_validator_client::nym_api::EpochId;
@@ -129,16 +129,18 @@ where
}
}
pub(crate) fn ensure_sane_expiration_date(expiration_date: Date) -> Result<(), VpnApiError> {
pub(crate) fn ensure_sane_expiration_date(
expiration_date: Date,
) -> Result<(), CredentialProxyError> {
let today = ecash_today();
if expiration_date < today.date() {
// what's the point of signatures with expiration in the past?
return Err(VpnApiError::ExpirationDateTooEarly);
return Err(CredentialProxyError::ExpirationDateTooEarly);
}
if expiration_date > cred_exp_date().ecash_date() {
return Err(VpnApiError::ExpirationDateTooLate);
return Err(CredentialProxyError::ExpirationDateTooLate);
}
Ok(())
@@ -148,10 +150,10 @@ pub(crate) async fn query_all_threshold_apis<F, T, U>(
all_apis: Vec<EcashApiClient>,
threshold: u64,
f: F,
) -> Result<Vec<T>, VpnApiError>
) -> Result<Vec<T>, CredentialProxyError>
where
F: Fn(EcashApiClient) -> U,
U: Future<Output = Result<T, VpnApiError>>,
U: Future<Output = Result<T, CredentialProxyError>>,
{
let shares = Mutex::new(Vec::with_capacity(all_apis.len()));
@@ -172,7 +174,7 @@ where
let shares = shares.into_inner();
if shares.len() < threshold as usize {
return Err(VpnApiError::InsufficientNumberOfSigners {
return Err(CredentialProxyError::InsufficientNumberOfSigners {
threshold,
available: shares.len(),
});

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