Compare commits

..

51 Commits

Author SHA1 Message Date
Jędrzej Stuczyński d465d77af2 allow networking failures in certain tests 2025-06-03 10:56:22 +01:00
Jędrzej Stuczyński d60e640f9e ignore tests relying on networking behaviour 2025-06-03 10:28:33 +01:00
Jędrzej Stuczyński ce0d4da0ab attempt to retrieve key rotation id before doing any config migration work 2025-06-03 09:46:13 +01:00
Jędrzej Stuczyński 0ef67b5d01 post rebasing fixes 2025-06-03 09:44:57 +01:00
Jędrzej Stuczyński a607bf5c3c fix: incorrect method signature 2025-06-03 09:34:57 +01:00
Jędrzej Stuczyński 9925e67499 remove usage of deprecated methods in sdk example 2025-06-03 09:34:57 +01:00
Jędrzej Stuczyński 93ac879c82 missing MixnetQueryClient variants 2025-06-03 09:34:56 +01:00
Jędrzej Stuczyński e8e75c6b03 post rebasing fixes 2025-06-03 09:34:56 +01:00
Jędrzej Stuczyński 08ffc9f86b remove dead test 2025-06-03 09:34:56 +01:00
Jędrzej Stuczyński abd2a095ad passing shutdown to nym apis client 2025-06-03 09:34:56 +01:00
Jędrzej Stuczyński ffe8cb31e4 additional bugfixes and debugging nym-api deadlock 2025-06-03 09:34:56 +01:00
Jędrzej Stuczyński 89a3480c2a instantiate mixnet contract with custom key rotation validity 2025-06-03 09:34:56 +01:00
Jędrzej Stuczyński 93d2e9509f added helper method to reset node's sphinx key 2025-06-03 09:34:55 +01:00
Jędrzej Stuczyński 1f3278a0b6 more guards against stuck epochs 2025-06-03 09:34:55 +01:00
Jędrzej Stuczyński 729f455e80 clippy 2025-06-03 09:34:53 +01:00
Jędrzej Stuczyński 4bff5631b6 post rebase fixes 2025-06-03 09:34:26 +01:00
Jędrzej Stuczyński 753acdc149 fixed panic during first key rotation 2025-06-03 09:34:26 +01:00
Jędrzej Stuczyński 98d7237eeb fixed host information deserialisation 2025-06-03 09:34:26 +01:00
Jędrzej Stuczyński d6b2b3f92b testnet manager: set first nym-api as the rewarder 2025-06-03 09:34:26 +01:00
Jędrzej Stuczyński 2d5b614777 additional bugfixes and guards against stuck epoch 2025-06-03 09:34:25 +01:00
Jędrzej Stuczyński 6720f9db74 bug fixes... 2025-06-03 09:34:25 +01:00
Jędrzej Stuczyński e3c1e0c485 v2 nym-api endpoints to retrieve nodes with additional metadata information 2025-06-03 09:34:25 +01:00
Jędrzej Stuczyński f1aecd0f80 split nym-nodes http handlers 2025-06-03 09:34:25 +01:00
Jędrzej Stuczyński 0c653cef51 retrieving rotation id when pulling topology 2025-06-03 09:34:25 +01:00
Jędrzej Stuczyński 58087a029b controlling announced sphinx keys within nym-api 2025-06-03 09:34:25 +01:00
Jędrzej Stuczyński 80a94db2b9 added a dedicated CacheRefresher listener to perform full refresh outside the set interval 2025-06-03 09:34:25 +01:00
Jędrzej Stuczyński 42b037c6b4 split up node describe cache 2025-06-03 09:34:25 +01:00
Jędrzej Stuczyński c0435c9a3d fixed backwards compatible deserialisation of host information 2025-06-03 09:34:25 +01:00
Jędrzej Stuczyński ba79b1e4c6 dont use deprecated fields 2025-06-03 09:34:25 +01:00
Jędrzej Stuczyński 78e0111d8c split http state.rs file 2025-06-03 09:34:25 +01:00
Jędrzej Stuczyński 3eabd2aa3a fixes due to backwards compatible hostkeys 2025-06-03 09:34:25 +01:00
Jędrzej Stuczyński 1364b4d134 post rebase fixes 2025-06-03 09:34:24 +01:00
Jędrzej Stuczyński 61dd59ff9d most of nym-node changes 2025-06-03 09:34:24 +01:00
Jędrzej Stuczyński 8bcbb69889 flushing bloomfilters to disk and loading 2025-06-03 09:34:24 +01:00
Jędrzej Stuczyński 8b0d9a00ec wired up KeyRotationController 2025-06-03 09:34:24 +01:00
Jędrzej Stuczyński 5ba44b5bdf rotating bloomfilters 2025-06-03 09:34:24 +01:00
Jędrzej Stuczyński 90d6dddd32 fixed sphinx key loading 2025-06-03 09:34:24 +01:00
Jędrzej Stuczyński 8544c2eb2e processing loop of KeyRotationController 2025-06-03 09:34:24 +01:00
Jędrzej Stuczyński 3a01d6bf2d wip: putting new sphinx keys to self described endpoints 2025-06-03 09:34:24 +01:00
Jędrzej Stuczyński 440a0dc6a4 logic for migrating config file 2025-06-03 09:34:24 +01:00
Jędrzej Stuczyński 90b48b4294 rotating sphinx key files 2025-06-03 09:34:24 +01:00
Jędrzej Stuczyński 874d4c70d6 multi api client + retrieving rotation id 2025-06-03 09:34:23 +01:00
Jędrzej Stuczyński 73860e253a finish packet decoding 2025-06-03 09:33:43 +01:00
Jędrzej Stuczyński f06ad1d51c unified nym-api contract cache refreshing 2025-06-03 09:33:43 +01:00
Jędrzej Stuczyński 0177f6f2f7 wip: introducing cached queries for key rotation info from nym api 2025-06-03 09:33:43 +01:00
Jędrzej Stuczyński 8eb45c2f7a added basic key rotation information to mixnet contract 2025-06-03 09:33:43 +01:00
Jędrzej Stuczyński a0c6ab8f83 attaching key rotation information to reply surbs 2025-06-03 09:33:42 +01:00
Jędrzej Stuczyński b95afd11af further propagation of key rotation information 2025-06-03 09:33:42 +01:00
Jędrzej Stuczyński 85b51f21e5 wip: choosing correct key for packet processing 2025-06-03 09:33:42 +01:00
Jędrzej Stuczyński 30163a0174 wip: wrap node's sphinx key with a manager 2025-06-03 09:33:42 +01:00
Jędrzej Stuczyński 4139a6a675 wip 2025-06-03 09:33:42 +01:00
732 changed files with 112211 additions and 16575 deletions
+3 -3
View File
@@ -415,9 +415,9 @@
}
},
"node_modules/undici": {
"version": "5.29.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz",
"integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==",
"version": "5.28.5",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz",
"integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==",
"license": "MIT",
"dependencies": {
"@fastify/busboy": "^2.0.0"
+2 -1
View File
@@ -5,6 +5,7 @@ on:
paths:
- 'clients/**'
- 'common/**'
- 'explorer-api/**'
- 'gateway/**'
- 'integrations/**'
- 'nym-api/**'
@@ -38,7 +39,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ arc-ubuntu-22.04, custom-windows-11, custom-macos-15 ]
os: [ arc-ubuntu-22.04, custom-windows-11, custom-runner-mac-m1 ]
runs-on: ${{ matrix.os }}
env:
CARGO_TERM_COLOR: always
@@ -44,10 +44,8 @@ jobs:
echo "Tag is empty"
exit 1
fi
# first, list all tags for logging purposes
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
# check if there's a matching tag
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' )
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
if [[ $exists = "true" ]]; then
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
exit 1
@@ -55,5 +53,5 @@ jobs:
echo "Version '$TAG' doesn't exist on the remote"
else
echo "Unknown output '$exists'"
exit 2
exit 1
fi
@@ -1,59 +0,0 @@
name: ci-check-nym-stats-api-version
on:
pull_request:
paths:
- "nym-statistics-api/**"
env:
WORKING_DIRECTORY: "nym-statistics-api"
jobs:
check-if-tag-exists:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Checkout repo
uses: actions/checkout@v4
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
- name: Check if git tag exists
run: |
TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
if [[ -z "$TAG" ]]; then
echo "Tag is empty"
exit 1
fi
git ls-remote --tags origin | awk '{print $2}'
if git ls-remote --tags origin | awk '{print $2}' | grep -q "refs/tags/$TAG$" ; then
echo "Tag '$TAG' ALREADY EXISTS on the remote"
exit 1
else
echo "Tag '$TAG' does not exist on the remote"
fi
- name: Check if harbor tag exists
run: |
TAG=${{ steps.get_version.outputs.result }}
registry=https://harbor.nymte.ch
repo_name=nym/nym-statistics-api
if [[ -z $TAG ]]; then
echo "Tag is empty"
exit 1
fi
# first, list all tags for logging purposes
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
# check if there's a matching tag
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' )
if [[ $exists = "true" ]]; then
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
exit 1
elif [[ $exists = "false" ]]; then
echo "Version '$TAG' doesn't exist on the remote"
else
echo "Unknown output '$exists'"
exit 2
fi
@@ -57,7 +57,6 @@ jobs:
cp contracts/target/wasm32-unknown-unknown/release/cw4_group.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_ecash.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_pool_contract.wasm $OUTPUT_DIR
cp contracts/target/wasm32-unknown-unknown/release/nym_performance_contract.wasm $OUTPUT_DIR
- name: Deploy branch to CI www
continue-on-error: true
@@ -0,0 +1,75 @@
name: ci-nym-wallet-storybook
on:
pull_request:
paths:
- 'nym-wallet/**'
- '.github/workflows/ci-nym-wallet-storybook.yml'
jobs:
build:
runs-on: custom-linux
steps:
- uses: actions/checkout@v4
- name: Install rsync
run: sudo apt-get install rsync
continue-on-error: true
- uses: rlespinasse/github-slug-action@v3.x
- uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup yarn
run: npm install -g yarn
- name: Install Rust stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Install wasm-pack
run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Build dependencies
run: yarn && yarn build
- name: Build storybook
run: yarn storybook:build
working-directory: ./nym-wallet
- name: Deploy branch to CI www (storybook)
continue-on-error: true
uses: easingthemes/ssh-deploy@main
env:
SSH_PRIVATE_KEY: ${{ secrets.CI_WWW_SSH_PRIVATE_KEY }}
ARGS: "-rltgoDzvO --delete"
SOURCE: "nym-wallet/storybook-static/"
REMOTE_HOST: ${{ secrets.CI_WWW_REMOTE_HOST }}
REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }}
TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/wallet-${{ env.GITHUB_REF_SLUG }}
EXCLUDE: "/dist/, /node_modules/"
- name: Matrix - Node Install
run: npm install
working-directory: .github/workflows/support-files
- name: Matrix - Send Notification
env:
NYM_NOTIFICATION_KIND: nym-wallet
NYM_PROJECT_NAME: "nym-wallet"
NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}"
NYM_CI_WWW_LOCATION: "wallet-${{ env.GITHUB_REF_SLUG }}"
GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}"
GIT_BRANCH: "${GITHUB_REF##*/}"
IS_SUCCESS: "${{ job.status == 'success' }}"
MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}"
MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}"
MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}"
MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}"
MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}"
uses: docker://keybaseio/client:stable-node
with:
args: .github/workflows/support-files/notifications/entry_point.sh
+5 -7
View File
@@ -19,11 +19,7 @@ jobs:
if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
strategy:
fail-fast: false
matrix:
include:
- os: arc-ubuntu-22.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.os }}
runs-on: arc-ubuntu-22.04
outputs:
release_id: ${{ steps.create-release.outputs.id }}
@@ -70,6 +66,7 @@ jobs:
with:
name: my-artifact
path: |
target/release/explorer-api
target/release/nym-client
target/release/nym-socks5-client
target/release/nym-api
@@ -78,13 +75,14 @@ jobs:
target/release/nymvisor
target/release/nym-node
retention-days: 30
- id: create-release
name: Upload to release based on tag name
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
uses: softprops/action-gh-release@v2
if: github.event_name == 'release'
with:
files: |
target/release/explorer-api
target/release/nym-client
target/release/nym-socks5-client
target/release/nym-api
@@ -1,42 +0,0 @@
name: Build and upload Nym Statistics API container to harbor.nymte.ch
on:
workflow_dispatch:
env:
WORKING_DIRECTORY: "nym-statistics-api"
CONTAINER_NAME: "nym-statistics-api"
jobs:
build-container:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
- name: Checkout repo
uses: actions/checkout@v4
- name: Configure git identity
run: |
git config --global user.email "lawrence@nymtech.net"
git config --global user.name "Lawrence Stalder"
- name: Get version from cargo.toml
uses: mikefarah/yq@v4.45.4
id: get_version
with:
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
- name: Create tag
run: |
git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}"
git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
- name: BuildAndPushImageOnHarbor
run: |
docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
@@ -5,6 +5,8 @@
>
> ➡️➡️➡️➡️➡️ **View output:**
>
> `storybook`: https://{{ env.NYM_CI_WWW_LOCATION }}.{{ env.NYM_CI_WWW_BASE }}
>
> `branch` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/tree/{{ env.GIT_BRANCH_NAME }}
>
> `commit` {{ env.GITHUB_SERVER_URL }}/{{ env.GITHUB_REPOSITORY }}/commit/{{ env.GITHUB_SHA }}
+1
View File
@@ -40,6 +40,7 @@ validator-config
validator-api-config.toml
dist
storybook-static
envs/qwerty.env
.parcel-cache
**/.DS_Store
cpu-cycles/libcpucycles/build
-24
View File
@@ -4,30 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2025.11-cheddar] (2025-06-10)
- No autoremoval of peers ([#5831])
- Set cached storage counters to 0 ([#5812])
- hack: temporarily use next.config.js instead of next.config.ts ([#5805])
- chore: resolve 1.87 clippy warnings ([#5802])
- Nym Statistics API ([#5800])
- QoL: RequestPath trait for http-api-client ([#5788])
- Fix contains ticketbook function that always returned true ([#5787])
- swap a decode into a fromrow to please future postgres feature ([#5785])
- Make address cache configurable ([#5784])
- Track wireguard credential retries ([#5783])
[#5831]: https://github.com/nymtech/nym/pull/5831
[#5812]: https://github.com/nymtech/nym/pull/5812
[#5805]: https://github.com/nymtech/nym/pull/5805
[#5802]: https://github.com/nymtech/nym/pull/5802
[#5800]: https://github.com/nymtech/nym/pull/5800
[#5788]: https://github.com/nymtech/nym/pull/5788
[#5787]: https://github.com/nymtech/nym/pull/5787
[#5785]: https://github.com/nymtech/nym/pull/5785
[#5784]: https://github.com/nymtech/nym/pull/5784
[#5783]: https://github.com/nymtech/nym/pull/5783
## [2025.10-brie] (2025-05-27)
- Backport PR 5779 ([#5801])
Generated
+529 -1448
View File
File diff suppressed because it is too large Load Diff
+9 -11
View File
@@ -34,12 +34,11 @@ members = [
"common/config",
"common/cosmwasm-smart-contracts/coconut-dkg",
"common/cosmwasm-smart-contracts/contracts-common",
"common/cosmwasm-smart-contracts/contracts-common-testing",
"common/cosmwasm-smart-contracts/easy_addr",
"common/cosmwasm-smart-contracts/ecash-contract",
"common/cosmwasm-smart-contracts/group-contract",
"common/cosmwasm-smart-contracts/mixnet-contract",
"common/cosmwasm-smart-contracts/multisig-contract", "common/cosmwasm-smart-contracts/nym-performance-contract",
"common/cosmwasm-smart-contracts/multisig-contract",
"common/cosmwasm-smart-contracts/nym-pool-contract",
"common/cosmwasm-smart-contracts/vesting-contract",
"common/credential-storage",
@@ -67,8 +66,6 @@ members = [
"common/nym-id",
"common/nym-metrics",
"common/nym_offline_compact_ecash",
"common/nymnoise",
"common/nymnoise/keys",
"common/nymsphinx",
"common/nymsphinx/acknowledgements",
"common/nymsphinx/addressing",
@@ -102,6 +99,7 @@ members = [
"common/wireguard-types",
"documentation/autodoc",
"gateway",
"integrations/bity",
"nym-api",
"nym-api/nym-api-requests",
"nym-browser-extension/storage",
@@ -127,7 +125,6 @@ members = [
"service-providers/common",
"service-providers/ip-packet-router",
"service-providers/network-requester",
"sqlx-pool-guard",
"tools/echo-server",
"tools/internal/contract-state-importer/importer-cli",
"tools/internal/contract-state-importer/importer-contract",
@@ -137,6 +134,7 @@ members = [
"tools/internal/testnet-manager",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
"tools/internal/testnet-manager/dkg-bypass-contract",
"tools/internal/validator-status-check",
"tools/nym-cli",
"tools/nym-id-cli",
@@ -205,7 +203,7 @@ bloomfilter = "3.0.1"
bs58 = "0.5.1"
bytecodec = "0.4.15"
bytes = "1.10.1"
cargo_metadata = "0.19.2"
cargo_metadata = "0.18.1"
celes = "2.6.0"
cfg-if = "1.0.0"
chacha20 = "0.9.0"
@@ -288,7 +286,6 @@ petgraph = "0.6.5"
pin-project = "1.1"
pin-project-lite = "0.2.16"
publicsuffix = "2.3.0"
proc_pidinfo = "0.1.3"
quote = "1"
rand = "0.8.5"
rand_chacha = "0.3"
@@ -313,7 +310,6 @@ serde_with = "3.9.0"
serde_yaml = "0.9.25"
sha2 = "0.10.9"
si-scale = "0.2.3"
snow = "0.9.6"
sphinx-packet = "=0.6.0"
sqlx = "0.8.6"
strum = "0.26"
@@ -323,10 +319,10 @@ syn = "1"
sysinfo = "0.33.0"
tap = "1.0.1"
tar = "0.4.44"
tempfile = "3.20"
tempfile = "3.19"
thiserror = "2.0"
time = "0.3.41"
tokio = "1.45"
tokio = "1.44"
tokio-postgres = "0.7"
tokio-stream = "0.1.17"
tokio-test = "0.4.4"
@@ -370,6 +366,9 @@ subtle = "2.5.0"
# cosmwasm-related
cosmwasm-schema = "=2.2.2"
cosmwasm-std = "=2.2.2"
# use 1.0.1 as that's the version used by cosmwasm-std 2.2.1
# (and ideally we don't want to pull the same dependency twice)
serde-json-wasm = "=1.0.1"
# same version as used by cosmwasm
cw-utils = "=2.0.0"
cw-storage-plus = "=2.0.0"
@@ -377,7 +376,6 @@ cw2 = { version = "=2.0.0" }
cw3 = { version = "=2.0.0" }
cw4 = { version = "=2.0.0" }
cw-controllers = { version = "=2.0.0" }
cw-multi-test = "=2.3.2"
# cosmrs-related
bip32 = { version = "0.5.3", default-features = false }
+1 -1
View File
@@ -133,7 +133,7 @@ clippy: sdk-wasm-lint
# Build contracts ready for deploy
# -----------------------------------------------------------------------------
CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg nym_pool_contract nym_performance_contract
CONTRACTS=vesting_contract mixnet_contract nym_ecash cw3_flex_multisig cw4_group nym_coconut_dkg nym_pool_contract
CONTRACTS_WASM=$(addsuffix .wasm, $(CONTRACTS))
CONTRACTS_OUT_DIR=contracts/target/wasm32-unknown-unknown/release
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.57"
version = "1.1.56"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
@@ -2048,11 +2048,10 @@
}
},
"node_modules/http-proxy-middleware": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz",
"integrity": "sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/http-proxy": "^1.17.8",
"http-proxy": "^1.18.1",
@@ -6096,9 +6095,9 @@
}
},
"http-proxy-middleware": {
"version": "2.0.9",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
"integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.4.tgz",
"integrity": "sha512-m/4FxX17SUvz4lJ5WPXOHDUuCwIqXLfLHs1s0uZ3oYjhoXlx9csYxaOa0ElDEJ+h8Q4iJ1s+lTMbiCa4EXIJqg==",
"dev": true,
"requires": {
"@types/http-proxy": "^1.17.8",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.57"
version = "1.1.56"
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"
@@ -108,7 +108,7 @@ impl GatewayClient {
#[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
@@ -117,7 +117,7 @@ impl GatewayClient {
#[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
@@ -117,7 +117,7 @@ impl GatewayClient {
#[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
@@ -169,7 +169,7 @@ impl GatewayClient {
#[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
@@ -169,7 +169,7 @@ impl GatewayClient {
#[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// use gateways key as a ref to an x25519_dalek key
let dh = gateway_key.inner().diffie_hellman(&self.pub_key);
let dh = (gateway_key.as_ref()).diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
+1 -3
View File
@@ -44,6 +44,7 @@ nym-sphinx = { path = "../nymsphinx" }
nym-statistics-common = { path = "../statistics" }
nym-pemstore = { path = "../pemstore" }
nym-topology = { path = "../topology", features = ["persistence"] }
nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false }
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
nym-task = { path = "../task" }
nym-credentials-interface = { path = "../credentials-interface" }
@@ -56,9 +57,6 @@ nym-client-core-surb-storage = { path = "./surb-storage" }
nym-client-core-gateways-storage = { path = "./gateways-storage" }
nym-ecash-time = { path = "../ecash-time" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies]
nym-mixnet-client = { path = "../client-libs/mixnet-client", default-features = false }
### For serving prometheus metrics
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.hyper]
workspace = true
@@ -2,7 +2,8 @@
// SPDX-License-Identifier: Apache-2.0
use crate::BadGateway;
use std::{io, path::PathBuf};
use std::io;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -18,6 +19,7 @@ pub enum StorageError {
#[error("failed to perform sqlx migration: {source}")]
MigrationError {
#[source]
#[from]
source: sqlx::migrate::MigrateError,
},
@@ -30,6 +32,7 @@ pub enum StorageError {
#[error("failed to run the SQL query: {source}")]
QueryError {
#[source]
#[from]
source: sqlx::error::Error,
},
@@ -1,18 +1,20 @@
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{
client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend},
config,
config::Config,
error::ClientCoreError,
use crate::client::replies::reply_storage::{
fs_backend, CombinedReplyStorage, ReplyStorageBackend,
};
use crate::config;
use crate::config::Config;
use crate::error::ClientCoreError;
use log::{error, info, trace};
use nym_bandwidth_controller::BandwidthController;
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient};
use std::{io, path::Path};
use nym_validator_client::nyxd;
use nym_validator_client::QueryHttpRpcNyxdClient;
use std::path::Path;
use std::{fs, io};
use time::OffsetDateTime;
use url::Url;
@@ -20,11 +22,11 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
db_path: P,
surb_config: &config::ReplySurbs,
) -> Result<fs_backend::Backend, ClientCoreError> {
info!("Creating fresh surb database");
info!("creating fresh surb database");
let mut storage_backend = match fs_backend::Backend::init(db_path).await {
Ok(backend) => backend,
Err(err) => {
error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}");
error!("failed to setup persistent storage backend for our reply needs: {err}");
return Err(ClientCoreError::SurbStorageError {
source: Box::new(err),
});
@@ -38,15 +40,14 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
surb_config.minimum_reply_surb_storage_threshold,
surb_config.maximum_reply_surb_storage_threshold,
);
match storage_backend.init_fresh(&mem_store).await {
Ok(()) => Ok(storage_backend),
Err(err) => {
storage_backend.shutdown().await;
Err(ClientCoreError::SurbStorageError {
source: Box::new(err),
})
}
}
storage_backend
.init_fresh(&mem_store)
.await
.map_err(|err| ClientCoreError::SurbStorageError {
source: Box::new(err),
})?;
Ok(storage_backend)
}
// fn setup_inactive_backend(surb_config: &config::ReplySurbs) -> fs_backend::Backend {
@@ -57,11 +58,12 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
// )
// }
async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
let db_path = db_path.as_ref();
debug_assert!(db_path.exists());
let now = OffsetDateTime::now_utc().unix_timestamp();
let suffix = format!("_{now}.corrupted");
let new_extension =
@@ -70,15 +72,11 @@ async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()
} else {
suffix
};
let renamed = db_path.with_extension(new_extension);
tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| {
error!(
"Failed to rename corrupt database file: {} to {}",
db_path.display(),
renamed.display()
);
})
let mut renamed = db_path.to_owned();
renamed.set_extension(new_extension);
fs::rename(db_path, renamed)
}
pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
@@ -89,12 +87,13 @@ pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
// the existing one
let db_path = db_path.as_ref();
if db_path.exists() {
info!("Loading existing surb database");
info!("loading existing surb database");
match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await {
Ok(backend) => Ok(backend),
Err(err) => {
error!("setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
archive_corrupted_database(db_path).await?;
error!("failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
archive_corrupted_database(db_path)?;
setup_fresh_backend(db_path, surb_config).await
}
}
+1 -12
View File
@@ -17,26 +17,15 @@ nym-crypto = { path = "../../crypto", optional = true, default-features = false
nym-sphinx = { path = "../../nymsphinx" }
nym-task = { path = "../../task" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
workspace = true
features = ["fs"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
workspace = true
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
optional = true
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
path = "../../../sqlx-pool-guard"
[build-dependencies]
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"sqlite",
"macros",
"migrate",
] }
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
[features]
fs-surb-storage = ["sqlx", "nym-crypto", "nym-crypto/hashing"]
@@ -1,7 +1,8 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::{io, path::PathBuf};
use std::io;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -29,6 +30,7 @@ pub enum StorageError {
#[error("failed to perform sqlx migration: {source}")]
MigrationError {
#[source]
#[from]
source: sqlx::migrate::MigrateError,
},
@@ -41,6 +43,7 @@ pub enum StorageError {
#[error("failed to run the SQL query: {source}")]
QueryError {
#[source]
#[from]
source: sqlx::error::Error,
},
@@ -15,11 +15,9 @@ use sqlx::{
};
use std::path::Path;
use sqlx_pool_guard::SqlitePoolGuard;
#[derive(Debug, Clone)]
pub struct StorageManager {
connection_pool: SqlitePoolGuard,
pub connection_pool: sqlx::SqlitePool,
}
// all SQL goes here
@@ -39,7 +37,7 @@ impl StorageManager {
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.synchronous(SqliteSynchronous::Normal)
.auto_vacuum(SqliteAutoVacuum::Incremental)
.filename(&database_path)
.filename(database_path)
.create_if_missing(fresh)
.disable_statement_logging();
@@ -51,15 +49,11 @@ impl StorageManager {
}
};
let connection_pool =
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
if let Err(err) = sqlx::migrate!("./fs_surbs_migrations")
.run(&*connection_pool)
.run(&connection_pool)
.await
{
error!("Failed to initialize SQLx database: {err}");
connection_pool.close().await;
return Err(err.into());
}
@@ -67,43 +61,38 @@ impl StorageManager {
Ok(StorageManager { connection_pool })
}
/// Close connection pool waiting for all connections to be closed.
pub async fn close_pool(&self) {
self.connection_pool.close().await;
}
#[allow(dead_code)]
pub async fn status_table_exists(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'")
.fetch_optional(&*self.connection_pool)
.fetch_optional(&self.connection_pool)
.await
.map(|r| r.is_some())
}
pub async fn create_status_table(&self) -> Result<(), sqlx::Error> {
sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)")
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn get_flush_status(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT flush_in_progress FROM status;")
.fetch_one(&*self.connection_pool)
.fetch_one(&self.connection_pool)
.await
.map(|r| r.flush_in_progress > 0)
}
pub async fn set_previous_flush_timestamp(&self, timestamp: i64) -> Result<(), sqlx::Error> {
sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn get_previous_flush_timestamp(&self) -> Result<i64, sqlx::Error> {
sqlx::query!("SELECT previous_flush_timestamp FROM status;")
.fetch_one(&*self.connection_pool)
.fetch_one(&self.connection_pool)
.await
.map(|r| r.previous_flush_timestamp)
}
@@ -111,14 +100,14 @@ impl StorageManager {
pub async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> {
let in_progress_int = i64::from(in_progress);
sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn get_client_in_use_status(&self) -> Result<bool, sqlx::Error> {
sqlx::query!("SELECT client_in_use FROM status;")
.fetch_one(&*self.connection_pool)
.fetch_one(&self.connection_pool)
.await
.map(|r| r.client_in_use > 0)
}
@@ -126,21 +115,21 @@ impl StorageManager {
pub async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> {
let in_use_int = i64::from(in_use);
sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn delete_all_tags(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM sender_tag;")
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn get_tags(&self) -> Result<Vec<StoredSenderTag>, sqlx::Error> {
sqlx::query_as!(StoredSenderTag, "SELECT * FROM sender_tag;",)
.fetch_all(&*self.connection_pool)
.fetch_all(&self.connection_pool)
.await
}
@@ -152,21 +141,21 @@ impl StorageManager {
stored_tag.recipient,
stored_tag.tag
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM reply_key;")
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn get_reply_keys(&self) -> Result<Vec<StoredReplyKey>, sqlx::Error> {
sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",)
.fetch_all(&*self.connection_pool)
.fetch_all(&self.connection_pool)
.await
}
@@ -182,14 +171,14 @@ impl StorageManager {
stored_reply_key.reply_key,
stored_reply_key.sent_at_timestamp
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub async fn get_surb_senders(&self) -> Result<Vec<StoredSurbSender>, sqlx::Error> {
sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",)
.fetch_all(&*self.connection_pool)
.fetch_all(&self.connection_pool)
.await
}
@@ -204,7 +193,7 @@ impl StorageManager {
stored_surb_sender.tag,
stored_surb_sender.last_sent_timestamp
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?
.last_insert_rowid();
Ok(id)
@@ -222,17 +211,17 @@ impl StorageManager {
"#,
sender_id
)
.fetch_all(&*self.connection_pool)
.fetch_all(&self.connection_pool)
.await
}
pub async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> {
sqlx::query!("DELETE FROM reply_surb;")
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
sqlx::query!("DELETE FROM reply_surb_sender;")
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
@@ -250,7 +239,7 @@ impl StorageManager {
stored_reply_surb.reply_surb,
stored_reply_surb.encoded_key_rotation
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
@@ -264,7 +253,7 @@ impl StorageManager {
SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata;
"#,
)
.fetch_one(&*self.connection_pool)
.fetch_one(&self.connection_pool)
.await
}
@@ -278,7 +267,7 @@ impl StorageManager {
"#,
metadata.min_reply_surb_threshold,
metadata.max_reply_surb_threshold,
).execute(&*self.connection_pool).await?;
).execute(&self.connection_pool).await?;
Ok(())
}
}
@@ -1,21 +1,18 @@
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::backend::fs_backend::manager::StorageManager;
use crate::backend::fs_backend::models::{
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender,
};
use crate::surb_storage::ReceivedReplySurbs;
use crate::{
backend::fs_backend::{
manager::StorageManager,
models::{
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag,
StoredSurbSender,
},
},
surb_storage::ReceivedReplySurbs,
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys,
UsedSenderTags,
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags,
};
use async_trait::async_trait;
use log::{debug, error, info, warn};
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
use std::fs;
use std::path::{Path, PathBuf};
use time::OffsetDateTime;
@@ -44,17 +41,15 @@ impl Backend {
}
let manager = StorageManager::init(database_path, true).await?;
match manager.create_status_table().await {
Ok(()) => Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
}),
Err(err) => {
manager.close_pool().await;
Err(err.into())
}
}
manager.create_status_table().await?;
let backend = Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
};
Ok(backend)
}
pub async fn try_load<P: AsRef<Path>>(
@@ -69,28 +64,7 @@ impl Backend {
}
let manager = StorageManager::init(database_path, false).await?;
match Self::try_load_inner(&manager, fresh_sender_tags).await {
Ok(()) => Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
manager,
}),
Err(e) => {
manager.close_pool().await;
Err(e)
}
}
}
/// Gracefully close sqlite connection pool and drop backend.
pub async fn shutdown(self) {
self.manager.close_pool().await
}
async fn try_load_inner(
manager: &StorageManager,
fresh_sender_tags: bool,
) -> Result<(), StorageError> {
// the database flush wasn't fully finished and thus the data is in inconsistent state
// (we don't really know what's properly saved or what's not)
if manager.get_flush_status().await? {
@@ -152,11 +126,20 @@ impl Backend {
manager.delete_all_tags().await?;
}
Ok(())
Ok(Backend {
temporary_old_path: None,
database_path: owned_path,
// manager: StorageManagerState::Storage(manager),
manager,
})
}
async fn close_pool(&mut self) {
self.manager.connection_pool.close().await;
}
async fn rotate(&mut self) -> Result<(), StorageError> {
self.manager.close_pool().await;
self.close_pool().await;
let new_extension = if let Some(existing_extension) =
self.database_path.extension().and_then(|ext| ext.to_str())
@@ -169,8 +152,7 @@ impl Backend {
let mut temp_old = self.database_path.clone();
temp_old.set_extension(new_extension);
tokio::fs::rename(&self.database_path, &temp_old)
.await
fs::rename(&self.database_path, &temp_old)
.map_err(|err| StorageError::DatabaseRenameError { source: err })?;
self.manager = StorageManager::init(&self.database_path, true).await?;
self.manager.create_status_table().await?;
@@ -179,10 +161,9 @@ impl Backend {
Ok(())
}
async fn remove_old(&mut self) -> Result<(), StorageError> {
fn remove_old(&mut self) -> Result<(), StorageError> {
if let Some(old_path) = self.temporary_old_path.take() {
tokio::fs::remove_file(old_path)
.await
fs::remove_file(old_path)
.map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err })
} else {
warn!("the old database file doesn't seem to exist!");
@@ -354,7 +335,7 @@ impl ReplyStorageBackend for Backend {
self.dump_reply_surb_storage_metadata(surbs_ref).await?;
self.dump_reply_surbs(surbs_ref).await?;
self.remove_old().await?;
self.remove_old()?;
self.end_storage_flush().await
}
@@ -33,6 +33,7 @@ where
self.backend.load_surb_storage().await
}
// this will have to get enabled after merging develop
pub async fn flush_on_shutdown(
mut self,
mem_state: CombinedReplyStorage,
@@ -49,6 +50,7 @@ where
shutdown.recv().await;
info!("PersistentReplyStorage is flushing all reply-related data to underlying storage");
info!("you MUST NOT forcefully shutdown now or you risk data corruption!");
if let Err(err) = self.backend.flush_surb_storage(&mem_state).await {
error!("failed to flush our reply-related data to the persistent storage: {err}")
} else {
+1 -6
View File
@@ -16,14 +16,9 @@ tokio-util = { workspace = true, features = ["codec"], optional = true }
tokio-stream = { workspace = true }
# internal
nym-noise = { path = "../../nymnoise" }
nym-sphinx = { path = "../../nymsphinx" }
nym-task = { path = "../../task", optional = true }
[features]
default = ["client"]
client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
[dev-dependencies]
nym-crypto = { path = "../../crypto" }
rand = { workspace = true }
client = ["tokio-util", "nym-task", "tokio/net", "tokio/rt"]
+3 -37
View File
@@ -3,8 +3,6 @@
use dashmap::DashMap;
use futures::StreamExt;
use nym_noise::config::NoiseConfig;
use nym_noise::upgrade_noise_initiator;
use nym_sphinx::forwarding::packet::MixPacket;
use nym_sphinx::framing::codec::NymCodec;
use nym_sphinx::framing::packet::FramedNymPacket;
@@ -54,7 +52,6 @@ pub trait SendWithoutResponse {
pub struct Client {
active_connections: ActiveConnections,
noise_config: NoiseConfig,
connections_count: Arc<AtomicUsize>,
config: Config,
}
@@ -100,7 +97,6 @@ impl ConnectionSender {
struct ManagedConnection {
address: SocketAddr,
noise_config: NoiseConfig,
message_receiver: ReceiverStream<FramedNymPacket>,
connection_timeout: Duration,
current_reconnection: Arc<AtomicU32>,
@@ -109,14 +105,12 @@ struct ManagedConnection {
impl ManagedConnection {
fn new(
address: SocketAddr,
noise_config: NoiseConfig,
message_receiver: mpsc::Receiver<FramedNymPacket>,
connection_timeout: Duration,
current_reconnection: Arc<AtomicU32>,
) -> Self {
ManagedConnection {
address,
noise_config,
message_receiver: ReceiverStream::new(message_receiver),
connection_timeout,
current_reconnection,
@@ -131,21 +125,9 @@ impl ManagedConnection {
Ok(stream_res) => match stream_res {
Ok(stream) => {
debug!("Managed to establish connection to {}", self.address);
let noise_stream =
match upgrade_noise_initiator(stream, &self.noise_config).await {
Ok(noise_stream) => noise_stream,
Err(err) => {
error!("Failed to perform Noise handshake with {address} - {err}");
// we failed to finish the noise handshake - increase reconnection attempt
self.current_reconnection.fetch_add(1, Ordering::SeqCst);
return;
}
};
// if we managed to connect AND do the noise handshake, reset the reconnection count (whatever it might have been)
// if we managed to connect, reset the reconnection count (whatever it might have been)
self.current_reconnection.store(0, Ordering::Release);
debug!("Noise initiator handshake completed for {:?}", address);
Framed::new(noise_stream, NymCodec)
Framed::new(stream, NymCodec)
}
Err(err) => {
debug!("failed to establish connection to {address} (err: {err})",);
@@ -178,14 +160,9 @@ impl ManagedConnection {
}
impl Client {
pub fn new(
config: Config,
noise_config: NoiseConfig,
connections_count: Arc<AtomicUsize>,
) -> Client {
pub fn new(config: Config, connections_count: Arc<AtomicUsize>) -> Client {
Client {
active_connections: Default::default(),
noise_config,
connections_count,
config,
}
@@ -240,7 +217,6 @@ impl Client {
let initial_connection_timeout = self.config.initial_connection_timeout;
let connections_count = self.connections_count.clone();
let noise_config = self.noise_config.clone();
tokio::spawn(async move {
// before executing the manager, wait for what was specified, if anything
if let Some(backoff) = backoff {
@@ -251,7 +227,6 @@ impl Client {
connections_count.fetch_add(1, Ordering::SeqCst);
ManagedConnection::new(
address,
noise_config,
receiver,
initial_connection_timeout,
current_reconnection_attempt,
@@ -316,12 +291,8 @@ impl SendWithoutResponse for Client {
#[cfg(test)]
mod tests {
use super::*;
use nym_crypto::asymmetric::x25519;
use nym_noise::config::NoiseNetworkView;
use rand::rngs::OsRng;
fn dummy_client() -> Client {
let mut rng = OsRng; //for test only, so we don't care if rng source isn't crypto grade
Client::new(
Config {
initial_reconnection_backoff: Duration::from_millis(10_000),
@@ -329,11 +300,6 @@ mod tests {
initial_connection_timeout: Duration::from_millis(1_500),
maximum_connection_buffer_size: 128,
},
NoiseConfig::new(
Arc::new(x25519::KeyPair::new(&mut rng)),
NoiseNetworkView::new_empty(),
Duration::from_millis(1_500),
),
Default::default(),
)
}
@@ -19,7 +19,6 @@ nym-vesting-contract-common = { path = "../../cosmwasm-smart-contracts/vesting-c
nym-ecash-contract-common = { path = "../../cosmwasm-smart-contracts/ecash-contract" }
nym-multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" }
nym-group-contract-common = { path = "../../cosmwasm-smart-contracts/group-contract" }
nym-performance-contract-common = { path = "../../cosmwasm-smart-contracts/nym-performance-contract" }
nym-serde-helpers = { path = "../../serde-helpers", features = ["hex", "base64"] }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
@@ -16,8 +16,8 @@ async fn main() {
let prefix = "n";
let denom: Denom = "unym".parse().unwrap();
let signer_mnemonic: bip39::Mnemonic = "<MNEMONIC WITH FUNDS HERE>".parse().unwrap();
let validator = "https://rpc.sandbox.nymtech.net";
let to_address: AccountId = "n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa".parse().unwrap();
let validator = "https://qwerty-validator.qa.nymte.ch";
let to_address: AccountId = "n19kdst4srf76xgwe55jg32mpcpcyf6aqgp6qrdk".parse().unwrap();
let signer = DirectSecp256k1HdWallet::from_mnemonic(prefix, signer_mnemonic);
let signer_address = signer.try_derive_accounts().unwrap()[0].address().clone();
@@ -26,7 +26,7 @@ use nym_api_requests::models::{
};
use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated};
use nym_api_requests::nym_nodes::{
NodesByAddressesResponse, SemiSkimmedNodesWithMetadata, SkimmedNode, SkimmedNodesWithMetadata,
NodesByAddressesResponse, SkimmedNode, SkimmedNodesWithMetadata,
};
use nym_coconut_dkg_common::types::EpochId;
use nym_http_api_client::UserAgent;
@@ -530,43 +530,6 @@ impl NymApiClient {
collect_paged_skimmed_v2!(self, get_basic_nodes_v2)
}
/// retrieve expanded information for all bonded nodes on the network
pub async fn get_all_expanded_nodes(
&self,
) -> Result<SemiSkimmedNodesWithMetadata, ValidatorClientError> {
// Unroll the first iteration to get the metadata
let mut page = 0;
let res = self
.nym_api
.get_expanded_nodes(false, Some(page), None)
.await?;
let mut nodes = res.nodes.data;
let metadata = res.metadata;
if res.nodes.pagination.total == nodes.len() {
return Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata));
}
page += 1;
loop {
let mut res = self
.nym_api
.get_expanded_nodes(false, Some(page), None)
.await?;
nodes.append(&mut res.nodes.data);
if nodes.len() < res.nodes.pagination.total {
page += 1
} else {
break;
}
}
Ok(SemiSkimmedNodesWithMetadata::new(nodes, metadata))
}
pub async fn health(&self) -> Result<ApiHealthResponse, ValidatorClientError> {
Ok(self.nym_api.health().await?)
}
@@ -35,7 +35,7 @@ pub use nym_api_requests::{
MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse,
StakeSaturationResponse, UptimeResponse,
},
nym_nodes::{CachedNodesResponse, SemiSkimmedNode, SkimmedNode},
nym_nodes::{CachedNodesResponse, SkimmedNode},
NymNetworkDetailsResponse,
};
use nym_contracts_common::IdentityKey;
@@ -643,39 +643,6 @@ pub trait NymApiClientExt: ApiClient {
.await
}
#[instrument(level = "debug", skip(self))]
async fn get_expanded_nodes(
&self,
no_legacy: bool,
page: Option<u32>,
per_page: Option<u32>,
) -> Result<PaginatedCachedNodesResponseV2<SemiSkimmedNode>, NymAPIError> {
let mut params = Vec::new();
if no_legacy {
params.push(("no_legacy", "true".to_string()))
}
if let Some(page) = page {
params.push(("page", page.to_string()))
}
if let Some(per_page) = per_page {
params.push(("per_page", per_page.to_string()))
}
self.get_json(
&[
routes::V2_API_VERSION,
"unstable",
routes::NYM_NODES_ROUTES,
"semi-skimmed",
],
&params,
)
.await
}
#[deprecated]
#[instrument(level = "debug", skip(self))]
async fn get_active_mixnodes(&self) -> Result<Vec<MixNodeDetails>, NymAPIError> {
@@ -13,7 +13,6 @@ pub mod ecash_query_client;
pub mod group_query_client;
pub mod mixnet_query_client;
pub mod multisig_query_client;
pub mod performance_query_client;
pub mod vesting_query_client;
// signing clients
@@ -22,7 +21,6 @@ pub mod ecash_signing_client;
pub mod group_signing_client;
pub mod mixnet_signing_client;
pub mod multisig_signing_client;
pub mod performance_signing_client;
pub mod vesting_signing_client;
// re-export query traits
@@ -31,7 +29,6 @@ pub use ecash_query_client::{EcashQueryClient, PagedEcashQueryClient};
pub use group_query_client::{GroupQueryClient, PagedGroupQueryClient};
pub use mixnet_query_client::{MixnetQueryClient, PagedMixnetQueryClient};
pub use multisig_query_client::{MultisigQueryClient, PagedMultisigQueryClient};
pub use performance_query_client::{PagedPerformanceQueryClient, PerformanceQueryClient};
pub use vesting_query_client::{PagedVestingQueryClient, VestingQueryClient};
// re-export signing traits
@@ -40,7 +37,6 @@ pub use ecash_signing_client::EcashSigningClient;
pub use group_signing_client::GroupSigningClient;
pub use mixnet_signing_client::MixnetSigningClient;
pub use multisig_signing_client::MultisigSigningClient;
pub use performance_signing_client::PerformanceSigningClient;
pub use vesting_signing_client::VestingSigningClient;
// helper for providing blanket implementation for query clients
@@ -48,7 +44,6 @@ pub trait NymContractsProvider {
// main
fn mixnet_contract_address(&self) -> Option<&AccountId>;
fn vesting_contract_address(&self) -> Option<&AccountId>;
fn performance_contract_address(&self) -> Option<&AccountId>;
// coconut-related
fn ecash_contract_address(&self) -> Option<&AccountId>;
@@ -61,7 +56,6 @@ pub trait NymContractsProvider {
pub struct TypedNymContracts {
pub mixnet_contract_address: Option<AccountId>,
pub vesting_contract_address: Option<AccountId>,
pub performance_contract_address: Option<AccountId>,
pub ecash_contract_address: Option<AccountId>,
pub group_contract_address: Option<AccountId>,
@@ -82,10 +76,6 @@ impl TryFrom<NymContracts> for TypedNymContracts {
.vesting_contract_address
.map(|addr| addr.parse())
.transpose()?,
performance_contract_address: value
.performance_contract_address
.map(|addr| addr.parse())
.transpose()?,
ecash_contract_address: value
.ecash_contract_address
.map(|addr| addr.parse())
@@ -1,265 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::collect_paged;
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::error::NyxdError;
use crate::nyxd::CosmWasmClient;
use async_trait::async_trait;
use cosmrs::AccountId;
pub use nym_performance_contract_common::{
msg::QueryMsg as PerformanceQueryMsg, types::NetworkMonitorResponse,
};
use nym_performance_contract_common::{
EpochId, EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse,
FullHistoricalPerformancePagedResponse, HistoricalPerformance, NetworkMonitorInformation,
NetworkMonitorsPagedResponse, NodeId, NodeMeasurement, NodeMeasurementsResponse,
NodePerformance, NodePerformancePagedResponse, NodePerformanceResponse, RetiredNetworkMonitor,
RetiredNetworkMonitorsPagedResponse,
};
use serde::Deserialize;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait PerformanceQueryClient {
async fn query_performance_contract<T>(
&self,
query: PerformanceQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>;
async fn admin(&self) -> Result<cw_controllers::AdminResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::Admin {})
.await
}
async fn get_node_performance(
&self,
epoch_id: EpochId,
node_id: NodeId,
) -> Result<NodePerformanceResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::NodePerformance { epoch_id, node_id })
.await
}
async fn get_node_performance_paged(
&self,
node_id: NodeId,
start_after: Option<EpochId>,
limit: Option<u32>,
) -> Result<NodePerformancePagedResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::NodePerformancePaged {
node_id,
start_after,
limit,
})
.await
}
async fn get_node_measurements(
&self,
epoch_id: EpochId,
node_id: NodeId,
) -> Result<NodeMeasurementsResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::NodeMeasurements { epoch_id, node_id })
.await
}
async fn get_epoch_measurements_paged(
&self,
epoch_id: EpochId,
start_after: Option<NodeId>,
limit: Option<u32>,
) -> Result<EpochMeasurementsPagedResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::EpochMeasurementsPaged {
epoch_id,
start_after,
limit,
})
.await
}
async fn get_epoch_performance_paged(
&self,
epoch_id: EpochId,
start_after: Option<NodeId>,
limit: Option<u32>,
) -> Result<EpochPerformancePagedResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::EpochPerformancePaged {
epoch_id,
start_after,
limit,
})
.await
}
async fn get_full_historical_performance_paged(
&self,
start_after: Option<(EpochId, NodeId)>,
limit: Option<u32>,
) -> Result<FullHistoricalPerformancePagedResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::FullHistoricalPerformancePaged {
start_after,
limit,
})
.await
}
async fn get_network_monitor(
&self,
address: &AccountId,
) -> Result<NetworkMonitorResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::NetworkMonitor {
address: address.to_string(),
})
.await
}
async fn get_network_monitors_paged(
&self,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<NetworkMonitorsPagedResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::NetworkMonitorsPaged {
start_after,
limit,
})
.await
}
async fn get_retired_network_monitors_paged(
&self,
start_after: Option<String>,
limit: Option<u32>,
) -> Result<RetiredNetworkMonitorsPagedResponse, NyxdError> {
self.query_performance_contract(PerformanceQueryMsg::RetiredNetworkMonitorsPaged {
start_after,
limit,
})
.await
}
}
// extension trait to the query client to deal with the paged queries
// (it didn't feel appropriate to combine it with the existing trait
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait PagedPerformanceQueryClient: PerformanceQueryClient {
async fn get_all_node_performance(
&self,
node_id: NodeId,
) -> Result<Vec<EpochNodePerformance>, NyxdError> {
collect_paged!(self, get_node_performance_paged, performance, node_id)
}
async fn get_all_epoch_measurements(
&self,
node_id: NodeId,
) -> Result<Vec<NodeMeasurement>, NyxdError> {
collect_paged!(self, get_epoch_measurements_paged, measurements, node_id)
}
async fn get_all_epoch_performance(
&self,
epoch_id: EpochId,
) -> Result<Vec<NodePerformance>, NyxdError> {
collect_paged!(self, get_epoch_performance_paged, performance, epoch_id)
}
async fn get_all_full_historical_performance(
&self,
) -> Result<Vec<HistoricalPerformance>, NyxdError> {
collect_paged!(self, get_full_historical_performance_paged, performance)
}
async fn get_all_network_monitors(&self) -> Result<Vec<NetworkMonitorInformation>, NyxdError> {
collect_paged!(self, get_network_monitors_paged, info)
}
async fn get_all_retired_network_monitors(
&self,
) -> Result<Vec<RetiredNetworkMonitor>, NyxdError> {
collect_paged!(self, get_retired_network_monitors_paged, info)
}
}
#[async_trait]
impl<T> PagedPerformanceQueryClient for T where T: PerformanceQueryClient {}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> PerformanceQueryClient for C
where
C: CosmWasmClient + NymContractsProvider + Send + Sync,
{
async fn query_performance_contract<T>(
&self,
query: PerformanceQueryMsg,
) -> Result<T, NyxdError>
where
for<'a> T: Deserialize<'a>,
{
let performance_contract_address = &self
.performance_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("performance contract"))?;
self.query_contract_smart(performance_contract_address, &query)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_query_variants_are_covered<C: PerformanceQueryClient + Send + Sync>(
client: C,
msg: PerformanceQueryMsg,
) {
match msg {
PerformanceQueryMsg::Admin {} => client.admin().ignore(),
PerformanceQueryMsg::NodePerformance { epoch_id, node_id } => {
client.get_node_performance(epoch_id, node_id).ignore()
}
PerformanceQueryMsg::NodePerformancePaged {
node_id,
start_after,
limit,
} => client
.get_node_performance_paged(node_id, start_after, limit)
.ignore(),
PerformanceQueryMsg::NodeMeasurements { epoch_id, node_id } => {
client.get_node_measurements(epoch_id, node_id).ignore()
}
PerformanceQueryMsg::EpochMeasurementsPaged {
epoch_id,
start_after,
limit,
} => client
.get_epoch_measurements_paged(epoch_id, start_after, limit)
.ignore(),
PerformanceQueryMsg::EpochPerformancePaged {
epoch_id,
start_after,
limit,
} => client
.get_epoch_performance_paged(epoch_id, start_after, limit)
.ignore(),
PerformanceQueryMsg::FullHistoricalPerformancePaged { start_after, limit } => client
.get_full_historical_performance_paged(start_after, limit)
.ignore(),
PerformanceQueryMsg::NetworkMonitor { address } => client
.get_network_monitor(&address.parse().unwrap())
.ignore(),
PerformanceQueryMsg::NetworkMonitorsPaged { start_after, limit } => client
.get_network_monitors_paged(start_after, limit)
.ignore(),
PerformanceQueryMsg::RetiredNetworkMonitorsPaged { start_after, limit } => client
.get_retired_network_monitors_paged(start_after, limit)
.ignore(),
};
}
}
@@ -1,217 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::nyxd::coin::Coin;
use crate::nyxd::contract_traits::NymContractsProvider;
use crate::nyxd::cosmwasm_client::types::ExecuteResult;
use crate::nyxd::cosmwasm_client::ContractResponseData;
use crate::nyxd::error::NyxdError;
use crate::nyxd::{Fee, SigningCosmWasmClient};
use crate::signing::signer::OfflineSigner;
use async_trait::async_trait;
use nym_performance_contract_common::{
EpochId, ExecuteMsg as PerformanceExecuteMsg, NodeId, NodePerformance,
RemoveEpochMeasurementsResponse,
};
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
pub trait PerformanceSigningClient {
async fn execute_performance_contract(
&self,
fee: Option<Fee>,
msg: PerformanceExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError>;
async fn update_admin(
&self,
admin: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_performance_contract(
fee,
PerformanceExecuteMsg::UpdateAdmin { admin },
"PerformanceContract::UpdateAdmin".to_string(),
vec![],
)
.await
}
async fn submit_performance(
&self,
epoch: EpochId,
data: NodePerformance,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_performance_contract(
fee,
PerformanceExecuteMsg::Submit { epoch, data },
"PerformanceContract::Submit".to_string(),
vec![],
)
.await
}
async fn batch_submit_performance(
&self,
epoch: EpochId,
data: Vec<NodePerformance>,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_performance_contract(
fee,
PerformanceExecuteMsg::BatchSubmit { epoch, data },
"PerformanceContract::BatchSubmit".to_string(),
vec![],
)
.await
}
async fn authorise_network_monitor(
&self,
address: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_performance_contract(
fee,
PerformanceExecuteMsg::AuthoriseNetworkMonitor { address },
"PerformanceContract::AuthoriseNetworkMonitor".to_string(),
vec![],
)
.await
}
async fn retire_network_monitor(
&self,
address: String,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_performance_contract(
fee,
PerformanceExecuteMsg::RetireNetworkMonitor { address },
"PerformanceContract::RetireNetworkMonitor".to_string(),
vec![],
)
.await
}
async fn remove_node_measurements(
&self,
epoch_id: EpochId,
node_id: NodeId,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_performance_contract(
fee,
PerformanceExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id },
"PerformanceContract::RemoveNodeMeasurements".to_string(),
vec![],
)
.await
}
async fn partial_remove_epoch_measurements(
&self,
epoch_id: EpochId,
fee: Option<Fee>,
) -> Result<ExecuteResult, NyxdError> {
self.execute_performance_contract(
fee,
PerformanceExecuteMsg::RemoveEpochMeasurements { epoch_id },
"PerformanceContract::RemoveEpochMeasurements".to_string(),
vec![],
)
.await
}
async fn remove_epoch_measurements(
&self,
epoch_id: EpochId,
fee: Option<Fee>,
) -> Result<(), NyxdError> {
loop {
let execute_res = self
.partial_remove_epoch_measurements(epoch_id, fee.clone())
.await?;
let response = execute_res
.parse_singleton_json_contract_response::<RemoveEpochMeasurementsResponse>()?;
if !response.additional_entries_to_remove_remaining {
break;
}
}
Ok(())
}
}
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl<C> PerformanceSigningClient for C
where
C: SigningCosmWasmClient + NymContractsProvider + Sync,
NyxdError: From<<Self as OfflineSigner>::Error>,
{
async fn execute_performance_contract(
&self,
fee: Option<Fee>,
msg: PerformanceExecuteMsg,
memo: String,
funds: Vec<Coin>,
) -> Result<ExecuteResult, NyxdError> {
let performance_contract_address = &self
.performance_contract_address()
.ok_or_else(|| NyxdError::unavailable_contract_address("performance contract"))?;
let fee = fee.unwrap_or(Fee::Auto(Some(self.simulated_gas_multiplier())));
let signer_address = &self.signer_addresses()?[0];
self.execute(
signer_address,
performance_contract_address,
&msg,
fee,
memo,
funds,
)
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::nyxd::contract_traits::tests::IgnoreValue;
use nym_performance_contract_common::ExecuteMsg;
// it's enough that this compiles and clippy is happy about it
#[allow(dead_code)]
fn all_execute_variants_are_covered<C: PerformanceSigningClient + Send + Sync>(
client: C,
msg: PerformanceExecuteMsg,
) {
match msg {
PerformanceExecuteMsg::UpdateAdmin { admin } => {
client.update_admin(admin, None).ignore()
}
PerformanceExecuteMsg::Submit { epoch, data } => {
client.submit_performance(epoch, data, None).ignore()
}
PerformanceExecuteMsg::BatchSubmit { epoch, data } => {
client.batch_submit_performance(epoch, data, None).ignore()
}
PerformanceExecuteMsg::AuthoriseNetworkMonitor { address } => {
client.authorise_network_monitor(address, None).ignore()
}
PerformanceExecuteMsg::RetireNetworkMonitor { address } => {
client.retire_network_monitor(address, None).ignore()
}
ExecuteMsg::RemoveNodeMeasurements { epoch_id, node_id } => client
.remove_node_measurements(epoch_id, node_id, None)
.ignore(),
ExecuteMsg::RemoveEpochMeasurements { epoch_id } => client
.partial_remove_epoch_measurements(epoch_id, None)
.ignore(),
};
}
}
@@ -12,8 +12,6 @@ use tendermint_rpc::endpoint::broadcast;
use tracing::error;
pub use cosmrs::abci::MsgResponse;
use cosmwasm_std::from_json;
use serde::de::DeserializeOwned;
pub fn parse_singleton_u32_from_contract_response(b: Vec<u8>) -> Result<u32, NyxdError> {
if b.len() != 4 {
@@ -75,11 +73,6 @@ pub fn parse_msg_responses(data: Bytes) -> Vec<MsgResponse> {
// requires there's a single response message
pub trait ContractResponseData: Sized {
fn parse_singleton_json_contract_response<T: DeserializeOwned>(&self) -> Result<T, NyxdError> {
let b = self.to_singleton_contract_data()?;
from_json(&b).map_err(|err| err.into())
}
fn parse_singleton_u32_contract_data(&self) -> Result<u32, NyxdError> {
let b = self.to_singleton_contract_data()?;
parse_singleton_u32_from_contract_response(b)
@@ -276,10 +276,6 @@ impl<C, S> NymContractsProvider for NyxdClient<C, S> {
self.config.contracts.vesting_contract_address.as_ref()
}
fn performance_contract_address(&self) -> Option<&AccountId> {
self.config.contracts.performance_contract_address.as_ref()
}
fn ecash_contract_address(&self) -> Option<&AccountId> {
self.config.contracts.ecash_contract_address.as_ref()
}
@@ -60,7 +60,7 @@ pub async fn send(args: Args, client: &SigningClient) {
"Nodesguru: https://nym.explorers.guru/transaction/{}",
&res.hash
);
println!("Mintscan: https://ping.pub/nyx/tx/{}", &res.hash);
println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash);
println!("Transaction result code: {}", &res.tx_result.code.value());
println!("Transaction hash: {}", &res.hash);
}
@@ -95,7 +95,7 @@ pub async fn send_multiple(args: Args, client: &SigningClient) {
"Nodesguru: https://nym.explorers.guru/transaction/{}",
&res.hash
);
println!("Mintscan: https://ping.pub/nyx/tx/{}", &res.hash);
println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash);
println!("Transaction result code: {}", &res.tx_result.code.value());
println!("Transaction hash: {}", &res.hash);
@@ -76,7 +76,7 @@ pub async fn execute(args: Args, client: SigningClient) {
&res.transaction_hash
);
println!(
"Mintscan: https://ping.pub/nyx/tx/{}",
"Mintscan: https://www.mintscan.io/nyx/txs/{}",
&res.transaction_hash
);
println!("Transaction hash: {}", &res.transaction_hash);
@@ -1,24 +0,0 @@
[package]
name = "nym-contracts-common-testing"
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]
anyhow = { workspace = true }
cosmwasm-std = { workspace = true }
cw-storage-plus = { workspace = true }
serde = { workspace = true }
rand_chacha = { workspace = true }
rand = { workspace = true }
cw-multi-test = { workspace = true }
[lints]
workspace = true
@@ -1,127 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::testing::{message_info, MockApi, MockQuerier, MockStorage};
use cosmwasm_std::{
coins, Addr, BankMsg, CosmosMsg, Empty, Env, MemoryStorage, MessageInfo, Order, OwnedDeps,
Response, StdResult, Storage,
};
use cw_storage_plus::{KeyDeserialize, Map, Prefix, PrimaryKey};
use rand::{RngCore, SeedableRng};
use rand_chacha::ChaCha20Rng;
use serde::de::DeserializeOwned;
use serde::Serialize;
pub const TEST_DENOM: &str = "unym";
pub const TEST_PREFIX: &str = "n";
pub fn mock_api() -> MockApi {
MockApi::default().with_prefix(TEST_PREFIX)
}
pub fn mock_dependencies() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
OwnedDeps {
storage: MockStorage::default(),
api: mock_api(),
querier: MockQuerier::default(),
custom_query_type: Default::default(),
}
}
pub fn test_rng() -> ChaCha20Rng {
let dummy_seed = [42u8; 32];
rand_chacha::ChaCha20Rng::from_seed(dummy_seed)
}
pub fn deps_with_balance(env: &Env) -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
let mut deps = mock_dependencies();
deps.querier = MockQuerier::<Empty>::new(&[(
env.contract.address.as_str(),
coins(100000000000, TEST_DENOM).as_slice(),
)]);
deps
}
pub fn generate_sorted_addresses(n: usize) -> Vec<Addr> {
let mut rng = test_rng();
let mut addrs = Vec::with_capacity(n);
for i in 0..n {
addrs.push(mock_api().addr_make(&format!("addr{i}{}", rng.next_u64())));
}
addrs.sort();
addrs
}
pub fn addr<S: AsRef<str>>(raw: S) -> Addr {
mock_api().addr_make(raw.as_ref())
}
pub fn sender<S: AsRef<str>>(raw: S) -> MessageInfo {
message_info(&addr(raw), &[])
}
pub trait ExtractBankMsg {
fn unwrap_bank_msg(self) -> Option<BankMsg>;
}
impl ExtractBankMsg for Response {
fn unwrap_bank_msg(self) -> Option<BankMsg> {
for msg in self.messages {
match msg.msg {
CosmosMsg::Bank(bank_msg) => return Some(bank_msg),
_ => continue,
}
}
None
}
}
pub trait FullReader<'a> {
type Key;
type Value: Serialize + DeserializeOwned;
fn all_values(&self, store: &dyn Storage) -> StdResult<Vec<Self::Value>>;
fn all_key_values(&self, store: &dyn Storage) -> StdResult<Vec<(Self::Key, Self::Value)>>;
}
impl<'a, K, T> FullReader<'a> for Map<K, T>
where
T: Serialize + DeserializeOwned,
K: PrimaryKey<'a> + KeyDeserialize,
K::Output: 'static,
{
type Key = K::Output;
type Value = T;
fn all_values(&self, store: &dyn Storage) -> StdResult<Vec<Self::Value>> {
self.range(store, None, None, Order::Ascending)
.map(|record| record.map(|r| r.1))
.collect()
}
fn all_key_values(&self, store: &dyn Storage) -> StdResult<Vec<(Self::Key, Self::Value)>> {
self.range(store, None, None, Order::Ascending).collect()
}
}
impl<'a, K, T, B> FullReader<'a> for Prefix<K, T, B>
where
K: KeyDeserialize + 'static,
T: Serialize + DeserializeOwned,
B: PrimaryKey<'a>,
{
type Key = K::Output;
type Value = T;
fn all_values(&self, store: &dyn Storage) -> StdResult<Vec<Self::Value>> {
self.range(store, None, None, Order::Ascending)
.map(|record| record.map(|r| r.1))
.collect()
}
fn all_key_values(&self, store: &dyn Storage) -> StdResult<Vec<(Self::Key, Self::Value)>> {
self.range(store, None, None, Order::Ascending).collect()
}
}
@@ -1,13 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
// those are all used exclusively for testing thus unwraps, et al. are allowed
#![allow(clippy::unwrap_used)]
#![allow(clippy::expect_used)]
#![allow(clippy::panic)]
pub mod helpers;
pub mod tester;
pub use helpers::*;
pub use tester::*;
@@ -1,239 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{ContractTester, TestableNymContract};
use cosmwasm_std::testing::{message_info, mock_env};
use cosmwasm_std::{
from_json, Addr, Coin, ContractInfo, Deps, DepsMut, Env, MessageInfo, Response, StdResult,
Storage, Timestamp,
};
use cw_multi_test::{next_block, AppResponse, Executor};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::any::type_name;
use std::fmt::Debug;
pub trait ContractOpts {
type ExecuteMsg;
type QueryMsg;
type ContractError;
fn deps(&self) -> Deps<'_>;
fn deps_mut(&mut self) -> DepsMut<'_>;
fn env(&self) -> Env;
fn addr_make(&self, input: &str) -> Addr;
fn deps_mut_env(&mut self) -> (DepsMut<'_>, Env) {
let env = self.env().clone();
(self.deps_mut(), env)
}
fn storage(&self) -> &dyn Storage;
fn storage_mut(&mut self) -> &mut dyn Storage;
fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T>;
fn set_contract_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>);
fn unchecked_read_from_contract_storage<T: DeserializeOwned>(
&self,
key: impl AsRef<[u8]>,
) -> T {
let typ = type_name::<T>();
self.read_from_contract_storage(key)
.unwrap_or_else(|| panic!("value of type {typ} not present in the storage"))
}
fn execute_raw(
&mut self,
sender: Addr,
message: Self::ExecuteMsg,
) -> Result<Response, Self::ContractError> {
self.execute_raw_with_balance(sender, &[], message)
}
fn execute_raw_with_balance(
&mut self,
sender: Addr,
coins: &[Coin],
message: Self::ExecuteMsg,
) -> Result<Response, Self::ContractError>;
}
impl<C> ContractOpts for ContractTester<C>
where
C: TestableNymContract,
{
type ExecuteMsg = C::ExecuteMsg;
type QueryMsg = C::QueryMsg;
type ContractError = C::ContractError;
fn deps(&self) -> Deps<'_> {
Deps {
storage: &self.storage,
api: self.app.api(),
querier: self.app.wrap(),
}
}
fn deps_mut(&mut self) -> DepsMut<'_> {
DepsMut {
storage: &mut self.storage,
api: self.app.api(),
querier: self.app.wrap(),
}
}
fn env(&self) -> Env {
Env {
block: self.app.block_info(),
contract: ContractInfo {
address: self.contract_address.clone(),
},
..mock_env()
}
}
fn addr_make(&self, input: &str) -> Addr {
self.app.api().addr_make(input)
}
fn storage(&self) -> &dyn Storage {
&self.storage
}
fn storage_mut(&mut self) -> &mut dyn Storage {
&mut self.storage
}
fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T> {
let raw = self.deps().storage.get(key.as_ref())?;
from_json(&raw).ok()
}
fn set_contract_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) {
self.deps_mut().storage.set(key.as_ref(), value.as_ref());
}
fn execute_raw_with_balance(
&mut self,
sender: Addr,
coins: &[Coin],
message: C::ExecuteMsg,
) -> Result<Response, C::ContractError> {
let env = self.env();
let info = message_info(&sender, coins);
C::execute()(self.deps_mut(), env, info, message)
}
}
pub trait ChainOpts: ContractOpts {
fn set_contract_balance(&mut self, balance: Coin);
fn next_block(&mut self);
fn set_block_time(&mut self, time: Timestamp);
fn execute_msg(
&mut self,
sender: Addr,
message: &Self::ExecuteMsg,
) -> anyhow::Result<AppResponse> {
self.execute_msg_with_balance(sender, &[], message)
}
fn execute_msg_with_balance(
&mut self,
sender: Addr,
coins: &[Coin],
message: &Self::ExecuteMsg,
) -> anyhow::Result<AppResponse>;
fn execute_arbitrary_contract<T: Serialize + Debug>(
&mut self,
contract: Addr,
sender: MessageInfo,
message: &T,
) -> anyhow::Result<AppResponse>;
fn query_arbitrary_contract<Q: Serialize + Debug, T: DeserializeOwned>(
&self,
contract: Addr,
message: &Q,
) -> StdResult<T>;
fn query<T: DeserializeOwned>(&self, message: &Self::QueryMsg) -> StdResult<T>;
}
impl<C> ChainOpts for ContractTester<C>
where
C: TestableNymContract,
{
fn set_contract_balance(&mut self, balance: Coin) {
let contract_address = &self.contract_address;
self.app
.router()
.bank
.init_balance(
&mut self.storage.inner_storage(),
contract_address,
vec![balance],
)
.unwrap();
}
fn next_block(&mut self) {
self.app.update_block(next_block)
}
fn set_block_time(&mut self, time: Timestamp) {
self.app.update_block(|b| b.time = time)
}
fn execute_msg(
&mut self,
sender: Addr,
message: &C::ExecuteMsg,
) -> anyhow::Result<AppResponse> {
self.execute_msg_with_balance(sender, &[], message)
}
fn execute_msg_with_balance(
&mut self,
sender: Addr,
coins: &[Coin],
message: &C::ExecuteMsg,
) -> anyhow::Result<AppResponse> {
self.app
.execute_contract(sender, self.contract_address.clone(), message, coins)
}
fn execute_arbitrary_contract<T: Serialize + Debug>(
&mut self,
contract: Addr,
sender: MessageInfo,
message: &T,
) -> anyhow::Result<AppResponse> {
let coins = &sender.funds;
let sender = sender.sender;
self.app.execute_contract(sender, contract, message, coins)
}
fn query_arbitrary_contract<Q: Serialize + Debug, T: DeserializeOwned>(
&self,
contract: Addr,
message: &Q,
) -> StdResult<T> {
self.app.wrap().query_wasm_smart(contract, message)
}
fn query<T: DeserializeOwned>(&self, message: &C::QueryMsg) -> StdResult<T> {
self.app
.wrap()
.query_wasm_smart(self.contract_address.as_str(), message)
}
}
@@ -1,305 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{
CommonStorageKeys, ContractOpts, ContractTester, StorageWrapper, TestableNymContract,
TEST_DENOM,
};
use cosmwasm_std::testing::message_info;
use cosmwasm_std::{
coin, coins, from_json, to_json_vec, Addr, Coin, MessageInfo, StdError, StdResult, Storage,
};
use cw_multi_test::Executor;
use cw_storage_plus::{Key, Path, PrimaryKey};
use rand::RngCore;
use rand_chacha::ChaCha20Rng;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::any::type_name;
use std::ops::Deref;
pub trait StorageReader {
fn common_key(&self, key: CommonStorageKeys) -> Option<&[u8]>;
fn read_common_value<T: DeserializeOwned>(&self, key: CommonStorageKeys) -> Option<T> {
self.read_from_contract_storage(self.common_key(key)?)
}
fn unchecked_read_common_value<T: DeserializeOwned>(&self, key: CommonStorageKeys) -> T {
self.unchecked_read_from_contract_storage(
self.common_key(key)
.unwrap_or_else(|| panic!("no key set for {key:?}")),
)
}
fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T>;
fn unchecked_read_from_contract_storage<T: DeserializeOwned>(
&self,
key: impl AsRef<[u8]>,
) -> T {
let typ = type_name::<T>();
self.read_from_contract_storage(key)
.unwrap_or_else(|| panic!("value of type {typ} not present in the storage"))
}
}
// technically it shouldn't rely on `StorageReader` and `common_key` should be extracted
// but this makes it a tad easier and it's only testing code so it's fine
pub trait StorageWriter: StorageReader {
fn set_common_value<T: Serialize>(
&mut self,
key: CommonStorageKeys,
value: &T,
) -> StdResult<()> {
let key = self
.common_key(key)
.ok_or(StdError::not_found("key not found"))?
.to_vec();
self.set_storage_value(key, value)
}
fn set_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>);
fn set_storage_value<T: Serialize>(
&mut self,
key: impl AsRef<[u8]>,
value: &T,
) -> StdResult<()> {
self.set_storage(key, &to_json_vec(value)?);
Ok(())
}
}
pub trait ArbitraryContractStorageReader {
fn may_read_from_contract_storage(
&self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
) -> Option<Vec<u8>>;
fn must_read_from_contract_storage(
&self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
) -> StdResult<Vec<u8>> {
let key = key.as_ref();
self.may_read_from_contract_storage(address, key)
.ok_or(StdError::not_found(format!("no data under {key:?}")))
}
fn may_read_value_from_contract_storage<T: DeserializeOwned>(
&self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
) -> StdResult<Option<T>> {
let Some(bytes) = self.may_read_from_contract_storage(address, key) else {
return Ok(None);
};
from_json(&bytes).map(Some)
}
fn must_read_value_from_contract_storage<T: DeserializeOwned>(
&self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
) -> StdResult<T> {
let bytes = self.must_read_from_contract_storage(address, key)?;
from_json(&bytes)
}
}
pub trait ArbitraryContractStorageWriter {
fn set_contract_storage(
&mut self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
value: impl AsRef<[u8]>,
);
fn set_contract_storage_value<T: Serialize>(
&mut self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
value: &T,
) -> StdResult<()> {
self.set_contract_storage(address, key, &to_json_vec(value)?);
Ok(())
}
// attempts to write to an arbitrary contract `cw_storage_plus::Map`
fn set_contract_map_value<'a, K, T>(
&mut self,
address: impl Into<String>,
namespace: impl AsRef<[u8]>,
key: K,
value: &T,
) -> StdResult<()>
where
K: PrimaryKey<'a>,
T: Serialize + DeserializeOwned,
{
let key_path: Path<T> = Path::new(
namespace.as_ref(),
&key.key().iter().map(Key::as_ref).collect::<Vec<_>>(),
);
let storage_key = key_path.deref();
self.set_contract_storage_value(address, storage_key, value)
}
}
// contract that has an admin
pub trait AdminExt: StorageReader + StorageWriter {
fn admin(&self) -> Option<Addr> {
self.read_common_value(CommonStorageKeys::Admin)
}
fn update_admin(&mut self, admin: &Option<Addr>) -> StdResult<()> {
self.set_common_value(CommonStorageKeys::Admin, admin)
}
fn admin_unchecked(&self) -> Addr {
self.admin().expect("no admin set")
}
fn admin_msg(&self) -> MessageInfo {
message_info(&self.admin_unchecked(), &[])
}
}
// contract that operates on some specific coin denom
pub trait DenomExt: StorageReader {
fn denom(&self) -> String {
self.unchecked_read_common_value(CommonStorageKeys::Denom)
}
fn coin(&self, amount: u128) -> Coin {
coin(amount, self.denom())
}
fn coins(&self, amount: u128) -> Vec<Coin> {
coins(amount, self.denom())
}
}
pub trait RandExt {
fn raw_rng(&mut self) -> &mut ChaCha20Rng;
fn generate_account(&mut self) -> Addr;
fn generate_account_with_balance(&mut self) -> Addr
where
Self: BankExt;
}
pub trait BankExt {
fn send_tokens(&mut self, to: Addr, amount: Coin) -> anyhow::Result<()>;
}
impl<T> AdminExt for T where T: StorageReader + StorageWriter {}
impl<T> DenomExt for T where T: StorageReader {}
impl<C: TestableNymContract> StorageReader for ContractTester<C> {
fn common_key(&self, key: CommonStorageKeys) -> Option<&[u8]> {
self.common_storage_keys.get(&key).map(|v| &**v)
}
fn read_from_contract_storage<T: DeserializeOwned>(&self, key: impl AsRef<[u8]>) -> Option<T> {
<Self as ContractOpts>::read_from_contract_storage(self, key)
}
}
impl<C: TestableNymContract> StorageWriter for ContractTester<C> {
fn set_storage(&mut self, key: impl AsRef<[u8]>, value: impl AsRef<[u8]>) {
<Self as ContractOpts>::set_contract_storage(self, key, value)
}
}
impl<C: TestableNymContract> BankExt for ContractTester<C> {
fn send_tokens(&mut self, to: Addr, amount: Coin) -> anyhow::Result<()> {
self.app
.send_tokens(self.master_address.clone(), to, &[amount])?;
Ok(())
}
}
impl<C: TestableNymContract> RandExt for ContractTester<C> {
fn raw_rng(&mut self) -> &mut ChaCha20Rng {
&mut self.rng
}
fn generate_account(&mut self) -> Addr {
self.app
.api()
.addr_make(&format!("foomp{}", self.rng.next_u64()))
}
fn generate_account_with_balance(&mut self) -> Addr
where
Self: BankExt,
{
let addr = self.generate_account();
let million = 1_000_000_000_000;
self.send_tokens(addr.clone(), coin(million, TEST_DENOM))
.unwrap();
addr
}
}
impl ArbitraryContractStorageReader for StorageWrapper {
fn may_read_from_contract_storage(
&self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
) -> Option<Vec<u8>> {
self.contract_storage_wrapper(&Addr::unchecked(address))
.get(key.as_ref())
}
}
impl ArbitraryContractStorageWriter for StorageWrapper {
fn set_contract_storage(
&mut self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
value: impl AsRef<[u8]>,
) {
// yeah, we're unnecessarily cloning a Rc pointer, but this is a test code, so this inefficiency is fine
let mut wrapped_storage = self
.clone()
.contract_storage_wrapper(&Addr::unchecked(address));
wrapped_storage.set(key.as_ref(), value.as_ref());
}
}
impl<C> ArbitraryContractStorageReader for ContractTester<C>
where
C: TestableNymContract,
{
fn may_read_from_contract_storage(
&self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
) -> Option<Vec<u8>> {
self.storage
.as_inner_storage()
.may_read_from_contract_storage(address, key)
}
}
impl<C> ArbitraryContractStorageWriter for ContractTester<C>
where
C: TestableNymContract,
{
fn set_contract_storage(
&mut self,
address: impl Into<String>,
key: impl AsRef<[u8]>,
value: impl AsRef<[u8]>,
) {
self.storage
.as_inner_storage_mut()
.set_contract_storage(address, key, value);
}
}
@@ -1,276 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{mock_api, test_rng, TEST_DENOM};
use cosmwasm_std::testing::MockApi;
use cosmwasm_std::{
coin, coins, Addr, Binary, Deps, DepsMut, Empty, Env, MessageInfo, Order, QuerierWrapper,
Record, Response, Storage,
};
use cw_multi_test::{App, AppBuilder, BankKeeper, Contract, ContractWrapper, Executor};
use rand_chacha::ChaCha20Rng;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::collections::HashMap;
use std::fmt::{Debug, Display};
use std::marker::PhantomData;
pub use basic_traits::*;
pub use extensions::*;
pub use crate::tester::storage_wrapper::{ContractStorageWrapper, StorageWrapper};
mod basic_traits;
mod extensions;
mod storage_wrapper;
// copied from cw-multi-test (but removed generics for custom messages and querier for we don't need them for now)
pub type ContractFn<T, E> =
fn(deps: DepsMut, env: Env, info: MessageInfo, msg: T) -> Result<Response, E>;
pub type QueryFn<T, E> = fn(deps: Deps, env: Env, msg: T) -> Result<Binary, E>;
pub type PermissionedFn<T, E> = fn(deps: DepsMut, env: Env, msg: T) -> Result<Response, E>;
pub type ContractClosure<T, E> = Box<dyn Fn(DepsMut, Env, MessageInfo, T) -> Result<Response, E>>;
pub type QueryClosure<T, E> = Box<dyn Fn(Deps, Env, T) -> Result<Binary, E>>;
pub trait TestableNymContract {
const NAME: &'static str;
type InitMsg: DeserializeOwned + Serialize + Debug + 'static;
type ExecuteMsg: DeserializeOwned + Serialize + Debug + 'static;
type QueryMsg: DeserializeOwned + Serialize + Debug + 'static;
type MigrateMsg: DeserializeOwned + Serialize + Debug + 'static;
type ContractError: Display + Debug + Send + Sync + 'static;
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError>;
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError>;
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError>;
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError>;
fn base_init_msg() -> Self::InitMsg;
// // for now we don't care about custom queriers
// fn contract_wrapper() -> ContractWrapper<
// Self::ExecuteMsg,
// Self::InitMsg,
// Self::QueryMsg,
// Self::ContractError,
// anyhow::Error,
// anyhow::Error,
// Empty,
// Empty,
// Empty,
// Self::ContractError,
// Self::ContractError,
// Self::MigrateMsg,
// Self::ContractError,
// > {
// ContractWrapper::new(Self::execute(), Self::instantiate(), Self::query())
// .with_migrate(Self::migrate())
// }
fn dyn_contract() -> Box<dyn Contract<Empty>> {
Box::new(
ContractWrapper::new(Self::execute(), Self::instantiate(), Self::query())
.with_migrate(Self::migrate()),
)
}
fn init() -> ContractTester<Self>
where
Self: Sized,
{
ContractTesterBuilder::new()
.instantiate::<Self>(None)
.build()
}
}
pub struct ContractTesterBuilder<C> {
contract: PhantomData<C>,
master_address: Addr,
app: App<BankKeeper, MockApi, StorageWrapper>,
storage: StorageWrapper,
pub well_known_contracts: HashMap<&'static str, Addr>,
}
impl<C> ContractTesterBuilder<C> {
#[allow(clippy::new_without_default)]
pub fn new() -> Self
where
C: TestableNymContract,
{
let storage = StorageWrapper::new();
let api = mock_api();
let master_address = api.addr_make("master-owner");
let app = AppBuilder::new()
.with_api(api)
.with_storage(storage.clone())
.build(|router, _api, storage| {
router
.bank
.init_balance(
storage,
&master_address,
coins(1000000000000000, TEST_DENOM),
)
.unwrap()
});
ContractTesterBuilder {
contract: Default::default(),
master_address,
app,
storage,
well_known_contracts: Default::default(),
}
}
pub fn instantiate<D: TestableNymContract>(
mut self,
custom_init_msg: Option<D::InitMsg>,
) -> ContractTesterBuilder<C> {
let code_id = self.app.store_code(D::dyn_contract());
let contract_address = self
.app
.instantiate_contract(
code_id,
self.master_address.clone(),
&custom_init_msg.unwrap_or(D::base_init_msg()),
&[],
D::NAME,
Some(self.master_address.to_string()),
)
.unwrap();
// send some tokens to the contract
self.app
.send_tokens(
self.master_address.clone(),
contract_address.clone(),
&[coin(100000000, TEST_DENOM)],
)
.unwrap();
self.well_known_contracts.insert(D::NAME, contract_address);
self
}
pub fn build(self) -> ContractTester<C>
where
C: TestableNymContract,
{
if !self.well_known_contracts.contains_key(C::NAME) {
panic!("{} contract has not been instantiated", C::NAME);
}
let contract_address = self.well_known_contracts[C::NAME].clone();
ContractTester {
contract: self.contract,
app: self.app,
rng: test_rng(),
master_address: self.master_address,
storage: self.storage.contract_storage_wrapper(&contract_address),
contract_address,
common_storage_keys: Default::default(),
well_known_contracts: self.well_known_contracts,
}
}
pub fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper {
self.storage.contract_storage_wrapper(contract)
}
pub fn api(&self) -> MockApi {
*self.app.api()
}
pub fn querier(&self) -> QuerierWrapper {
self.app.wrap()
}
}
#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)]
pub enum CommonStorageKeys {
Admin,
Denom,
}
pub struct ContractTester<C: TestableNymContract> {
contract: PhantomData<C>,
pub app: App<BankKeeper, MockApi, StorageWrapper>,
pub rng: ChaCha20Rng,
pub contract_address: Addr,
pub master_address: Addr,
pub(crate) storage: ContractStorageWrapper,
pub common_storage_keys: HashMap<CommonStorageKeys, Vec<u8>>,
// TODO: limitation: doesn't allow multiple contracts of the same type (but that's fine for the time being)
pub well_known_contracts: HashMap<&'static str, Addr>,
}
impl<C> ContractTester<C>
where
C: TestableNymContract,
{
pub fn insert_common_storage_key(&mut self, key: CommonStorageKeys, value: impl AsRef<[u8]>) {
self.common_storage_keys
.insert(key, value.as_ref().to_vec());
}
pub fn with_common_storage_key(
mut self,
key: CommonStorageKeys,
value: impl AsRef<[u8]>,
) -> Self {
self.insert_common_storage_key(key, value);
self
}
}
impl<C> Storage for ContractTester<C>
where
C: TestableNymContract,
{
fn get(&self, key: &[u8]) -> Option<Vec<u8>> {
self.storage.get(key)
}
fn range<'a>(
&'a self,
start: Option<&[u8]>,
end: Option<&[u8]>,
order: Order,
) -> Box<dyn Iterator<Item = Record> + 'a> {
self.storage.range(start, end, order)
}
fn range_keys<'a>(
&'a self,
start: Option<&[u8]>,
end: Option<&[u8]>,
order: Order,
) -> Box<dyn Iterator<Item = Vec<u8>> + 'a> {
self.storage.range_keys(start, end, order)
}
fn range_values<'a>(
&'a self,
start: Option<&[u8]>,
end: Option<&[u8]>,
order: Order,
) -> Box<dyn Iterator<Item = Vec<u8>> + 'a> {
self.storage.range_values(start, end, order)
}
fn set(&mut self, key: &[u8], value: &[u8]) {
self.storage.set(key, value)
}
fn remove(&mut self, key: &[u8]) {
self.storage.remove(key)
}
}
@@ -18,7 +18,6 @@ serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true }
serde_json = { workspace = true }
[build-dependencies]
@@ -35,7 +35,7 @@ pub enum ContractsCommonError {
/// Percent represents a value between 0 and 100%
/// (i.e. between 0.0 and 1.0)
#[cw_serde]
#[derive(Copy, Default, PartialOrd, Ord, Eq)]
#[derive(Copy, Default, PartialOrd)]
pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal);
impl Percent {
@@ -80,44 +80,6 @@ impl Percent {
pub fn checked_pow(&self, exp: u32) -> Result<Self, OverflowError> {
self.0.checked_pow(exp).map(Percent)
}
// truncate provided percent to only have 2 decimal places,
// e.g. convert "0.1234567" into "0.12"
// the purpose of it is to reduce storage space, in particular for the performance contract
// since that extra precision gains us nothing
#[must_use = "this returns the result of the operation, without modifying the original"]
pub fn round_to_two_decimal_places(&self) -> Self {
let raw = self.0;
const DECIMAL_FRACTIONAL: Uint128 = Uint128::new(1_000_000_000_000_000_000u128); // 1*10**18
const THRESHOLD: Decimal = Decimal::permille(5); // 0.005
// in case it ever changes since it's not exposed in the public API
debug_assert_eq!(
DECIMAL_FRACTIONAL,
Uint128::new(10).pow(Decimal::DECIMAL_PLACES)
);
let int = (raw.atomics() * Uint128::new(100)) / DECIMAL_FRACTIONAL;
#[allow(clippy::unwrap_used)]
let floored = Decimal::from_atomics(int, 2).unwrap();
let diff = raw - floored;
let rounded = if diff >= THRESHOLD {
// ceil
floored + Decimal::percent(1)
} else {
floored
};
Percent(rounded)
}
#[must_use = "this returns the result of the operation, without modifying the original"]
pub fn average(&self, other: &Self) -> Self {
let sum = self.0 + other.0;
let inner = Decimal::from_ratio(sum.numerator(), sum.denominator() * Uint128::new(2));
Percent(inner)
}
}
impl Display for Percent {
@@ -372,7 +334,6 @@ mod tests {
}
#[test]
#[cfg(feature = "naive_float")]
fn naive_float_conversion() {
// around 15 decimal places is the maximum precision we can handle
// which is still way more than enough for what we use it for
@@ -386,41 +347,4 @@ mod tests {
assert!(converted.0 - converted.0 < epsilon);
}
#[test]
fn rounding_percent() {
let test_cases = vec![
("0", "0"),
("0.1", "0.1"),
("0.12", "0.12"),
("0.12", "0.123"),
("0.12", "0.123456789"),
("0.13", "0.125"),
("0.13", "0.126"),
("0.13", "0.126436545676"),
("0.99", "0.99"),
("0.99", "0.994"),
("1", "0.999"),
("1", "0.995"),
];
for (expected, input) in test_cases {
let expected: Percent = expected.parse().unwrap();
let pre_truncated: Percent = input.parse().unwrap();
assert_eq!(expected, pre_truncated.round_to_two_decimal_places())
}
}
#[test]
fn calculating_average() -> anyhow::Result<()> {
fn p(raw: &str) -> Percent {
raw.parse().unwrap()
}
assert_eq!(p("0.1").average(&p("0.1")), p("0.1"));
assert_eq!(p("0.1").average(&p("0.2")), p("0.15"));
assert_eq!(p("1").average(&p("0")), p("0.5"));
assert_eq!(p("0.123").average(&p("0.456")), p("0.2895"));
Ok(())
}
}
@@ -23,6 +23,7 @@ semver = { workspace = true, features = ["serde"] }
schemars = { workspace = true }
thiserror = { workspace = true }
contracts-common = { path = "../contracts-common", package = "nym-contracts-common", version = "0.5.0" }
serde-json-wasm = { workspace = true }
humantime-serde = { workspace = true }
utoipa = { workspace = true, optional = true }
@@ -3,7 +3,7 @@
use crate::{IdentityKey, NodeId, SphinxKey};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{to_json_string, Addr, Coin};
use cosmwasm_std::{Addr, Coin};
use std::cmp::Ordering;
use std::fmt::Display;
@@ -154,7 +154,7 @@ pub struct GatewayConfigUpdate {
impl GatewayConfigUpdate {
pub fn to_inline_json(&self) -> String {
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
}
}
@@ -16,7 +16,7 @@ use crate::{
Percent, ProfitMarginRange, SphinxKey,
};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{to_json_string, Addr, Coin, Decimal, StdResult, Uint128};
use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
@@ -604,7 +604,7 @@ pub struct NodeCostParams {
impl NodeCostParams {
pub fn to_inline_json(&self) -> String {
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
}
}
@@ -773,7 +773,7 @@ pub struct MixNodeConfigUpdate {
impl MixNodeConfigUpdate {
pub fn to_inline_json(&self) -> String {
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
}
}
@@ -5,7 +5,7 @@ use crate::helpers::IntoBaseDecimal;
use crate::nym_node::Role;
use crate::{error::MixnetContractError, Percent};
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{to_json_string, Decimal};
use cosmwasm_std::Decimal;
pub type Performance = Percent;
pub type WorkFactor = Decimal;
@@ -84,7 +84,7 @@ pub struct IntervalRewardParams {
impl IntervalRewardParams {
pub fn to_inline_json(&self) -> String {
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
}
}
@@ -410,7 +410,7 @@ impl IntervalRewardingParamsUpdate {
}
pub fn to_inline_json(&self) -> String {
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
}
}
@@ -1,29 +0,0 @@
[package]
name = "nym-performance-contract-common"
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]
thiserror = { workspace = true }
serde = { workspace = true }
schemars = { workspace = true }
cosmwasm-std = { workspace = true }
cosmwasm-schema = { workspace = true }
cw-controllers = { workspace = true }
nym-contracts-common = { path = "../contracts-common" }
[features]
schema = []
[lints]
workspace = true
@@ -1,13 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod storage_keys {
pub const CONTRACT_ADMIN: &str = "contract-admin";
pub const INITIAL_EPOCH_ID: &str = "initial-epoch-id";
pub const MIXNET_CONTRACT: &str = "mixnet-contract";
pub const AUTHORISED_COUNT: &str = "authorised-count";
pub const AUTHORISED: &str = "authorised";
pub const RETIRED: &str = "retired";
pub const PERFORMANCE_RESULTS: &str = "performance-results";
pub const SUBMISSION_METADATA: &str = "submission-metadata";
}
@@ -1,39 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{EpochId, NodeId};
use cosmwasm_std::Addr;
use cw_controllers::AdminError;
use thiserror::Error;
#[derive(Error, Debug, PartialEq)]
pub enum NymPerformanceContractError {
#[error("could not perform contract migration: {comment}")]
FailedMigration { comment: String },
#[error(transparent)]
Admin(#[from] AdminError),
#[error(transparent)]
StdErr(#[from] cosmwasm_std::StdError),
#[error("{address} is already an authorised network monitor")]
AlreadyAuthorised { address: Addr },
#[error("{address} is not an authorised network monitor")]
NotAuthorised { address: Addr },
#[error("attempted to submit performance data for epoch {epoch_id} and node {node_id} whilst last submitted was {last_epoch_id} for node {last_node_id}")]
StalePerformanceSubmission {
epoch_id: EpochId,
node_id: NodeId,
last_epoch_id: EpochId,
last_node_id: NodeId,
},
#[error("the batch performance data has not been sorted")]
UnsortedBatchSubmission,
#[error("node {node_id} does not appear to be bonded")]
NodeNotBonded { node_id: NodeId },
}
@@ -1,2 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
@@ -1,12 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod constants;
pub mod error;
pub mod helpers;
pub mod msg;
pub mod types;
pub use error::*;
pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg};
pub use types::*;
@@ -1,121 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{EpochId, NodeId, NodePerformance};
use cosmwasm_schema::cw_serde;
#[cfg(feature = "schema")]
use crate::types::{
EpochMeasurementsPagedResponse, EpochPerformancePagedResponse,
FullHistoricalPerformancePagedResponse, NetworkMonitorResponse, NetworkMonitorsPagedResponse,
NodeMeasurementsResponse, NodePerformancePagedResponse, NodePerformanceResponse,
RetiredNetworkMonitorsPagedResponse,
};
#[cw_serde]
pub struct InstantiateMsg {
pub mixnet_contract_address: String,
pub authorised_network_monitors: Vec<String>,
}
#[cw_serde]
pub enum ExecuteMsg {
/// Change the admin
UpdateAdmin { admin: String },
/// Attempt to submit performance data of a particular node for given epoch
Submit {
epoch: EpochId,
data: NodePerformance,
},
/// Attempt to submit performance data of a batch of nodes for given epoch
BatchSubmit {
epoch: EpochId,
data: Vec<NodePerformance>,
},
/// Attempt to authorise new network monitor for submitting performance data
AuthoriseNetworkMonitor { address: String },
/// Attempt to retire an existing network monitor and forbid it from submitting any future performance data
RetireNetworkMonitor { address: String },
/// An admin method to remove submitted node measurements. Used as an escape hatch should
/// the data stored get too unwieldy.
RemoveNodeMeasurements { epoch_id: EpochId, node_id: NodeId },
/// An admin method to remove submitted nodes measurements. Used as an escape hatch should
/// the data stored get too unwieldy. Note: it is expected to get called multiple times
/// until the response indicates all the epoch data has been removed.
RemoveEpochMeasurements { epoch_id: EpochId },
}
#[cw_serde]
#[cfg_attr(feature = "schema", derive(cosmwasm_schema::QueryResponses))]
pub enum QueryMsg {
#[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))]
Admin {},
/// Returns performance of particular node for the provided epoch
#[cfg_attr(feature = "schema", returns(NodePerformanceResponse))]
NodePerformance { epoch_id: EpochId, node_id: NodeId },
/// Returns historical performance for particular node
#[cfg_attr(feature = "schema", returns(NodePerformancePagedResponse))]
NodePerformancePaged {
node_id: NodeId,
start_after: Option<EpochId>,
limit: Option<u32>,
},
/// Returns all submitted measurements for the particular node
#[cfg_attr(feature = "schema", returns(NodeMeasurementsResponse))]
NodeMeasurements { epoch_id: EpochId, node_id: NodeId },
/// Returns (paged) measurements for particular epoch
#[cfg_attr(feature = "schema", returns(EpochMeasurementsPagedResponse))]
EpochMeasurementsPaged {
epoch_id: EpochId,
start_after: Option<NodeId>,
limit: Option<u32>,
},
/// Returns (paged) performance for particular epoch
#[cfg_attr(feature = "schema", returns(EpochPerformancePagedResponse))]
EpochPerformancePaged {
epoch_id: EpochId,
start_after: Option<NodeId>,
limit: Option<u32>,
},
/// Returns full (paged) historical performance of the whole network
#[cfg_attr(feature = "schema", returns(FullHistoricalPerformancePagedResponse))]
FullHistoricalPerformancePaged {
start_after: Option<(EpochId, NodeId)>,
limit: Option<u32>,
},
/// Returns information about particular network monitor
#[cfg_attr(feature = "schema", returns(NetworkMonitorResponse))]
NetworkMonitor { address: String },
/// Returns information about all network monitors
#[cfg_attr(feature = "schema", returns(NetworkMonitorsPagedResponse))]
NetworkMonitorsPaged {
start_after: Option<String>,
limit: Option<u32>,
},
/// Returns information about all retired network monitors
#[cfg_attr(feature = "schema", returns(RetiredNetworkMonitorsPagedResponse))]
RetiredNetworkMonitorsPaged {
start_after: Option<String>,
limit: Option<u32>,
},
}
#[cw_serde]
pub struct MigrateMsg {
//
}
@@ -1,242 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_schema::cw_serde;
use cosmwasm_std::{Addr, Env};
use nym_contracts_common::Percent;
pub type EpochId = u32;
pub type NodeId = u32;
#[cw_serde]
pub struct NetworkMonitorDetails {
pub address: Addr,
pub authorised_by: Addr,
pub authorised_at_height: u64,
}
impl NetworkMonitorDetails {
pub fn retire(self, env: &Env, sender: &Addr) -> RetiredNetworkMonitor {
RetiredNetworkMonitor {
details: self,
retired_by: sender.clone(),
retired_at_height: env.block.height,
}
}
}
#[cw_serde]
pub struct RetiredNetworkMonitor {
pub details: NetworkMonitorDetails,
pub retired_by: Addr,
pub retired_at_height: u64,
}
#[cw_serde]
#[derive(Copy)]
pub struct NodePerformance {
#[serde(rename = "n")]
pub node_id: NodeId,
// note: value is rounded to 2 decimal places.
#[serde(rename = "p")]
pub performance: Percent,
}
#[cw_serde]
pub struct NetworkMonitorSubmissionMetadata {
pub last_submitted_epoch_id: EpochId,
pub last_submitted_node_id: NodeId,
}
// the internal values are always sorted
#[cw_serde]
pub struct NodeResults(Vec<Percent>);
impl NodeResults {
pub fn new(initial: Percent) -> NodeResults {
NodeResults(vec![initial.round_to_two_decimal_places()])
}
// ASSUMPTION: number of NM will be relatively small, so loading the whole vector of values
// to insert new one and resave is cheap
pub fn insert_new(&mut self, result: Percent) {
let result = result.round_to_two_decimal_places();
let pos = self.0.binary_search(&result).unwrap_or_else(|e| e);
self.0.insert(pos, result);
}
// SAFETY: there are no codepaths that allow constructing empty struct
pub fn median(&self) -> Percent {
let len = self.0.len();
if len % 2 == 1 {
// odd number of elements: return the middle one
self.0[len / 2]
} else {
// even number: average the two middle elements
let mid1 = self.0[len / 2 - 1];
let mid2 = self.0[len / 2];
mid1.average(&mid2).round_to_two_decimal_places()
}
}
pub fn inner(&self) -> &[Percent] {
&self.0
}
}
#[cw_serde]
pub struct NodePerformanceResponse {
pub performance: Option<Percent>,
}
#[cw_serde]
pub struct NodeMeasurementsResponse {
pub measurements: Option<NodeResults>,
}
#[cw_serde]
#[derive(Copy)]
pub struct EpochNodePerformance {
pub epoch: EpochId,
pub performance: Option<Percent>,
}
#[cw_serde]
pub struct NodePerformancePagedResponse {
pub node_id: NodeId,
pub performance: Vec<EpochNodePerformance>,
pub start_next_after: Option<EpochId>,
}
#[cw_serde]
pub struct EpochPerformancePagedResponse {
pub epoch_id: EpochId,
pub performance: Vec<NodePerformance>,
pub start_next_after: Option<NodeId>,
}
#[cw_serde]
pub struct NodeMeasurement {
pub node_id: NodeId,
pub measurements: NodeResults,
}
#[cw_serde]
pub struct EpochMeasurementsPagedResponse {
pub epoch_id: EpochId,
pub measurements: Vec<NodeMeasurement>,
pub start_next_after: Option<NodeId>,
}
#[cw_serde]
#[derive(Copy)]
pub struct HistoricalPerformance {
pub epoch_id: EpochId,
pub node_id: NodeId,
pub performance: Percent,
}
#[cw_serde]
pub struct FullHistoricalPerformancePagedResponse {
pub performance: Vec<HistoricalPerformance>,
pub start_next_after: Option<(EpochId, NodeId)>,
}
#[cw_serde]
pub struct NetworkMonitorInformation {
pub details: NetworkMonitorDetails,
pub current_submission_metadata: NetworkMonitorSubmissionMetadata,
}
#[cw_serde]
pub struct NetworkMonitorResponse {
pub info: Option<NetworkMonitorInformation>,
}
#[cw_serde]
pub struct NetworkMonitorsPagedResponse {
pub info: Vec<NetworkMonitorInformation>,
pub start_next_after: Option<String>,
}
#[cw_serde]
pub struct RetiredNetworkMonitorsPagedResponse {
pub info: Vec<RetiredNetworkMonitor>,
pub start_next_after: Option<String>,
}
#[cw_serde]
pub struct RemoveEpochMeasurementsResponse {
pub additional_entries_to_remove_remaining: bool,
}
#[cw_serde]
#[derive(Default)]
pub struct BatchSubmissionResult {
pub accepted_scores: u64,
pub non_existent_nodes: Vec<NodeId>,
}
#[cfg(test)]
mod tests {
use super::*;
fn p(raw: impl AsRef<str>) -> Percent {
raw.as_ref().parse().unwrap()
}
fn ps(raw: &[&str]) -> Vec<Percent> {
raw.iter().map(p).collect()
}
#[test]
fn node_results_insertion() {
let initial = NodeResults::new(p("0.5"));
let mut smaller = initial.clone();
let mut greater = initial.clone();
smaller.insert_new(p("0.4"));
greater.insert_new(p("0.6"));
assert_eq!(smaller.0, ps(&["0.4", "0.5"]));
assert_eq!(greater.0, ps(&["0.5", "0.6"]));
let mut another = NodeResults(ps(&["0.1", "0.4", "0.5", "0.6", "0.6", "1.0"]));
another.insert_new(p("0.6"));
another.insert_new(p("0.2"));
another.insert_new(p("0.7"));
another.insert_new(p("0.3"));
another.insert_new(p("0.3"));
another.insert_new(p("0.55"));
assert_eq!(
another.0,
ps(&[
"0.1", "0.2", "0.3", "0.3", "0.4", "0.5", "0.55", "0.6", "0.6", "0.6", "0.7", "1.0"
])
);
}
#[test]
fn node_results_median() {
let results = NodeResults(ps(&["0.1"]));
assert_eq!(results.median(), p("0.1"));
let results = NodeResults(ps(&["0.1", "0.2"]));
assert_eq!(results.median(), p("0.15"));
let results = NodeResults(ps(&["0.1", "0.2", "0.3"]));
assert_eq!(results.median(), p("0.2"));
let results = NodeResults(ps(&["0.1", "0.2", "0.3", "0.4"]));
assert_eq!(results.median(), p("0.25"));
let results = NodeResults(ps(&["0.1", "0.2", "0.3", "0.4", "0.5"]));
assert_eq!(results.median(), p("0.3"));
let results = NodeResults(ps(&["0", "0", "1", "1", "1", "1", "1"]));
assert_eq!(results.median(), p("1"));
}
}
+2 -9
View File
@@ -20,8 +20,6 @@ nym-credentials = { path = "../credentials" }
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
nym-ecash-time = { path = "../ecash-time" }
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
path = "../../sqlx-pool-guard"
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
workspace = true
@@ -33,13 +31,8 @@ features = ["rt-multi-thread", "net", "signal", "fs"]
[build-dependencies]
sqlx = { workspace = true, features = [
"runtime-tokio-rustls",
"sqlite",
"macros",
"migrate",
] }
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
[features]
persistent-storage = ["bincode", "serde"]
persistent-storage = ["bincode", "serde"]
@@ -7,11 +7,10 @@ use crate::models::{
};
use nym_ecash_time::Date;
use sqlx::{Executor, Sqlite, Transaction};
use sqlx_pool_guard::SqlitePoolGuard;
#[derive(Clone)]
pub struct SqliteEcashTicketbookManager {
connection_pool: SqlitePoolGuard,
connection_pool: sqlx::SqlitePool,
}
impl SqliteEcashTicketbookManager {
@@ -20,7 +19,7 @@ impl SqliteEcashTicketbookManager {
/// # Arguments
///
/// * `connection_pool`: database connection pool to use.
pub fn new(connection_pool: SqlitePoolGuard) -> Self {
pub fn new(connection_pool: sqlx::SqlitePool) -> Self {
SqliteEcashTicketbookManager { connection_pool }
}
@@ -34,7 +33,7 @@ impl SqliteEcashTicketbookManager {
"DELETE FROM ecash_ticketbook WHERE expiration_date <= ?",
deadline
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
@@ -61,7 +60,7 @@ impl SqliteEcashTicketbookManager {
data,
expiration_date,
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
@@ -91,7 +90,7 @@ impl SqliteEcashTicketbookManager {
epoch_id,
total_tickets,
used_tickets,
).execute(&*self.connection_pool).await?;
).execute(&self.connection_pool).await?;
Ok(())
}
@@ -106,7 +105,7 @@ impl SqliteEcashTicketbookManager {
"#,
)
.bind(data)
.fetch_optional(&*self.connection_pool)
.fetch_optional(&self.connection_pool)
.await?
.is_some();
@@ -122,7 +121,7 @@ impl SqliteEcashTicketbookManager {
FROM ecash_ticketbook
"#,
)
.fetch_all(&*self.connection_pool)
.fetch_all(&self.connection_pool)
.await
}
@@ -144,7 +143,7 @@ impl SqliteEcashTicketbookManager {
ticketbook_id,
expected_current_total_spent
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?
.rows_affected();
Ok(affected > 0)
@@ -154,7 +153,7 @@ impl SqliteEcashTicketbookManager {
&self,
) -> Result<Vec<StoredPendingTicketbook>, sqlx::Error> {
sqlx::query_as("SELECT * FROM pending_issuance")
.fetch_all(&*self.connection_pool)
.fetch_all(&self.connection_pool)
.await
}
@@ -166,7 +165,7 @@ impl SqliteEcashTicketbookManager {
"DELETE FROM pending_issuance WHERE deposit_id = ?",
pending_id
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
@@ -183,7 +182,7 @@ impl SqliteEcashTicketbookManager {
"#,
epoch_id
)
.fetch_optional(&*self.connection_pool)
.fetch_optional(&self.connection_pool)
.await
}
@@ -209,7 +208,7 @@ impl SqliteEcashTicketbookManager {
serialisation_revision,
epoch_id
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
@@ -226,7 +225,7 @@ impl SqliteEcashTicketbookManager {
"#,
epoch_id
)
.fetch_optional(&*self.connection_pool)
.fetch_optional(&self.connection_pool)
.await
}
@@ -252,7 +251,7 @@ impl SqliteEcashTicketbookManager {
serialisation_revision,
epoch_id,
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
@@ -270,7 +269,7 @@ impl SqliteEcashTicketbookManager {
"#,
expiration_date
)
.fetch_optional(&*self.connection_pool)
.fetch_optional(&self.connection_pool)
.await
}
@@ -299,7 +298,7 @@ impl SqliteEcashTicketbookManager {
serialisation_revision,
expiration_date
)
.execute(&*self.connection_pool)
.execute(&self.connection_pool)
.await?;
Ok(())
}
@@ -37,7 +37,6 @@ use sqlx::{
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
ConnectOptions,
};
use sqlx_pool_guard::SqlitePoolGuard;
use std::path::Path;
use zeroize::Zeroizing;
@@ -55,15 +54,15 @@ impl PersistentStorage {
/// * `database_path`: path to the database.
pub async fn init<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> {
debug!(
"Attempting to connect to database {}",
database_path.as_ref().display()
"Attempting to connect to database {:?}",
database_path.as_ref().as_os_str()
);
let opts = sqlx::sqlite::SqliteConnectOptions::new()
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.synchronous(SqliteSynchronous::Normal)
.auto_vacuum(SqliteAutoVacuum::Incremental)
.filename(&database_path)
.filename(database_path)
.create_if_missing(true)
.disable_statement_logging();
@@ -75,17 +74,13 @@ impl PersistentStorage {
}
};
let connection_pool =
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
if let Err(err) = sqlx::migrate!("./migrations").run(&*connection_pool).await {
if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await {
error!("Failed to perform migration on the SQLx database: {err}");
connection_pool.close().await;
return Err(err.into());
}
Ok(PersistentStorage {
storage_manager: SqliteEcashTicketbookManager::new(connection_pool),
storage_manager: SqliteEcashTicketbookManager::new(connection_pool.clone()),
})
}
}
@@ -40,10 +40,6 @@ impl BandwidthStorageManager {
}
}
pub fn client_bandwidth(&self) -> ClientBandwidth {
self.client_bandwidth.clone()
}
pub async fn available_bandwidth(&self) -> i64 {
self.client_bandwidth.available().await
}
@@ -88,8 +84,7 @@ impl BandwidthStorageManager {
debug!(available = available_bi2, required = required_bi2);
self.consume_bandwidth(required_bandwidth).await?;
let remaining_bandwidth = self.client_bandwidth.available().await;
Ok(remaining_bandwidth)
Ok(available_bandwidth)
}
async fn expire_bandwidth(&mut self) -> Result<()> {
@@ -73,7 +73,7 @@ impl ClientBandwidth {
false
}
pub async fn available(&self) -> i64 {
pub(crate) async fn available(&self) -> i64 {
self.inner.read().await.bandwidth.bytes
}
@@ -218,12 +218,6 @@ impl From<PublicKey> for x25519_dalek::PublicKey {
}
}
impl AsRef<[u8]> for PublicKey {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
#[derive(Zeroize, ZeroizeOnDrop)]
pub struct PrivateKey(x25519_dalek::StaticSecret);
@@ -254,10 +248,6 @@ impl PrivateKey {
PrivateKey(x25519_secret)
}
pub fn inner(&self) -> &x25519_dalek::StaticSecret {
&self.0
}
pub fn public_key(&self) -> PublicKey {
self.into()
}
@@ -266,10 +256,6 @@ impl PrivateKey {
self.0.to_bytes()
}
pub fn as_bytes(&self) -> &[u8; PRIVATE_KEY_SIZE] {
self.0.as_bytes()
}
pub fn from_bytes(b: &[u8]) -> Result<Self, KeyRecoveryError> {
if b.len() != PRIVATE_KEY_SIZE {
return Err(KeyRecoveryError::InvalidSizePrivateKey {
@@ -349,12 +335,6 @@ impl AsRef<x25519_dalek::StaticSecret> for PrivateKey {
}
}
impl AsRef<[u8]> for PrivateKey {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
#[cfg(test)]
mod tests {
use super::*;
+7 -14
View File
@@ -14,7 +14,6 @@ use nym_dkg::bte::{
};
use nym_dkg::interpolation::polynomial::Polynomial;
use nym_dkg::{combine_shares, Dealing, NodeIndex, Share, Threshold};
use rand::CryptoRng;
use rand_core::{RngCore, SeedableRng};
use std::collections::BTreeMap;
@@ -32,7 +31,7 @@ pub fn precomputing_g2_generator_for_miller_loop(c: &mut Criterion) {
}
fn prepare_keys(
mut rng: impl RngCore + CryptoRng,
mut rng: impl RngCore,
nodes: usize,
) -> (BTreeMap<NodeIndex, PublicKey>, Vec<DecryptionKey>) {
let params = setup();
@@ -51,7 +50,7 @@ fn prepare_keys(
}
fn prepare_resharing(
mut rng: impl RngCore + CryptoRng,
mut rng: impl RngCore,
params: &Params,
nodes: usize,
threshold: Threshold,
@@ -69,7 +68,7 @@ fn prepare_resharing(
for (i, ref mut dk) in dks.iter_mut().enumerate() {
let shares = first_dealings
.iter()
.map(|dealing| decrypt_share(params, dk, i, &dealing.ciphertexts, None).unwrap())
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
let recovered_secret =
@@ -155,9 +154,7 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri
|b| {
b.iter(|| {
assert!(dealing.verify(&params, threshold, &receivers, None).is_ok());
black_box(
decrypt_share(&params, first_key, 0, &dealing.ciphertexts, None).unwrap(),
);
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
})
},
);
@@ -240,9 +237,7 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter
|b| {
b.iter(|| {
assert!(dealing.verify(&params, threshold, &receivers, None).is_ok());
black_box(
decrypt_share(&params, first_key, 0, &dealing.ciphertexts, None).unwrap(),
);
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
})
},
);
@@ -325,9 +320,7 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite
|b| {
b.iter(|| {
assert!(dealing.verify(&params, threshold, &receivers, None).is_ok());
black_box(
decrypt_share(&params, first_key, 0, &dealing.ciphertexts, None).unwrap(),
);
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
})
},
);
@@ -554,7 +547,7 @@ pub fn share_decryption(c: &mut Criterion) {
let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], &params, &mut rng);
c.bench_function("single share decryption", |b| {
b.iter(|| black_box(decrypt_share(&params, &dk, 0, &ciphertexts, None)))
b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, None)))
});
}
+10 -43
View File
@@ -9,7 +9,6 @@ use crate::{Chunk, ChunkedShare, Share};
use bls12_381::{G1Affine, G1Projective, G2Prepared, G2Projective, Gt, Scalar};
use ff::Field;
use group::{Curve, Group, GroupEncoding};
use rand::CryptoRng;
use rand_core::RngCore;
use std::collections::HashMap;
use std::ops::Neg;
@@ -192,7 +191,7 @@ impl HazmatRandomness {
pub fn encrypt_shares(
shares: &[(&Share, &PublicKey)],
params: &Params,
mut rng: impl RngCore + CryptoRng,
mut rng: impl RngCore,
) -> (Ciphertexts, HazmatRandomness) {
let g1 = G1Projective::generator();
@@ -263,7 +262,6 @@ pub fn encrypt_shares(
}
pub fn decrypt_share(
params: &Params,
dk: &DecryptionKey,
// in the case of multiple receivers, specifies which index of ciphertext chunks should be used
i: usize,
@@ -272,10 +270,6 @@ pub fn decrypt_share(
) -> Result<Share, DkgError> {
let mut plaintext = ChunkedShare::default();
if !ciphertext.verify_integrity(params) {
return Err(DkgError::FailedCiphertextIntegrityCheck);
}
if i >= ciphertext.ciphertext_chunks.len() {
return Err(DkgError::UnavailableCiphertext(i));
}
@@ -467,22 +461,10 @@ mod tests {
let (ciphertext, hazmat) = encrypt_shares(shares, &params, &mut rng);
verify_hazmat_rand(&ciphertext, &hazmat);
let recovered1 = decrypt_share(
&params,
&decryption_key1,
0,
&ciphertext,
Some(lookup_table),
)
.unwrap();
let recovered2 = decrypt_share(
&params,
&decryption_key2,
1,
&ciphertext,
Some(lookup_table),
)
.unwrap();
let recovered1 =
decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap();
let recovered2 =
decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap();
assert_eq!(m1, recovered1);
assert_eq!(m2, recovered2);
}
@@ -508,22 +490,10 @@ mod tests {
let (ciphertext, hazmat) = encrypt_shares(shares, &params, &mut rng);
verify_hazmat_rand(&ciphertext, &hazmat);
let recovered1 = decrypt_share(
&params,
&decryption_key1,
0,
&ciphertext,
Some(lookup_table),
)
.unwrap();
let recovered2 = decrypt_share(
&params,
&decryption_key2,
1,
&ciphertext,
Some(lookup_table),
)
.unwrap();
let recovered1 =
decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap();
let recovered2 =
decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap();
assert_eq!(m1, recovered1);
assert_eq!(m2, recovered2);
}
@@ -604,10 +574,7 @@ mod tests {
#[test]
fn ciphertexts_roundtrip() {
fn random_ciphertexts(
mut rng: impl RngCore + CryptoRng,
num_receivers: usize,
) -> Ciphertexts {
fn random_ciphertexts(mut rng: impl RngCore, num_receivers: usize) -> Ciphertexts {
Ciphertexts {
rr: (0..NUM_CHUNKS)
.map(|_| G1Projective::random(&mut rng))
+2 -6
View File
@@ -9,15 +9,11 @@ use bls12_381::{G1Projective, G2Projective, Scalar};
use ff::Field;
use group::GroupEncoding;
use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair};
use rand::CryptoRng;
use rand_core::RngCore;
use zeroize::Zeroize;
// produces public key and a decryption key for the root of the tree
pub fn keygen(
params: &Params,
mut rng: impl RngCore + CryptoRng,
) -> (DecryptionKey, PublicKeyWithProof) {
pub fn keygen(params: &Params, mut rng: impl RngCore) -> (DecryptionKey, PublicKeyWithProof) {
let g1 = G1Projective::generator();
let g2 = G2Projective::generator();
@@ -248,7 +244,7 @@ pub struct KeyPair {
}
impl KeyPair {
pub fn new(params: &Params, rng: impl RngCore + CryptoRng) -> Self {
pub fn new(params: &Params, rng: impl RngCore) -> Self {
let (dk, pk) = keygen(params, rng);
Self {
private_key: dk,
+20 -90
View File
@@ -10,7 +10,7 @@ use crate::utils::{deserialize_scalar, RandomOracleBuilder};
use bls12_381::{G1Projective, Scalar};
use ff::Field;
use group::{Group, GroupEncoding};
use rand::{CryptoRng, Rng};
use rand::Rng;
use rand_core::{RngCore, SeedableRng};
const CHUNKING_ORACLE_DOMAIN: &[u8] =
@@ -28,7 +28,6 @@ const SECURITY_PARAMETER: usize = 256;
/// ceil(SECURITY_PARAMETER / PARALLEL_RUNS) in the paper
const NUM_CHALLENGE_BITS: usize = SECURITY_PARAMETER.div_ceil(PARALLEL_RUNS);
const EE: usize = 1 << NUM_CHALLENGE_BITS;
// type alias for ease of use
type FirstChallenge = Vec<Vec<Vec<u64>>>;
@@ -95,7 +94,7 @@ impl ProofOfChunking {
// Scalar(-1) would in reality be Scalar(q - 1), which is greater than Scalar(1) and opposite to
// what we wanted.
pub fn construct(
mut rng: impl RngCore + CryptoRng,
mut rng: impl RngCore,
instance: Instance,
witness_r: &[Scalar; NUM_CHUNKS],
witnesses_s: &[Share],
@@ -111,20 +110,21 @@ impl ProofOfChunking {
// define bounds for the blinding factors
let n = instance.public_keys.len();
let m = NUM_CHUNKS;
let ee = 1 << NUM_CHALLENGE_BITS;
// ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1))
// Z = 2 * l * S
let (ss, zz): (u64, u64) = compute_ss_zz(n, m)?;
// CHUNK_MAX corresponds to paper's B
let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64;
let zz = (2 * (PARALLEL_RUNS as u64))
.checked_mul(ss)
.expect("overflow in Z = 2 * l * S");
let ss_scalar = Scalar::from(ss);
// rather than generating blinding factors in [-S, Z-1] directly,
// do it via [0, Z - 1 + S + 1] and deal with the shift later.
// combined_upper_range = Z - 1 + S + 1
let combined_upper_range = zz.checked_add(ss).ok_or(DkgError::ArithmeticOverflow {
info: "ProofOfChunking::construct | Z - 1 + S + 1",
})?;
let combined_upper_range = (zz - 1)
.checked_add(ss + 1)
.expect("overflow in Z - 1 + S + 1");
let mut betas = Vec::with_capacity(PARALLEL_RUNS);
let mut bs = Vec::with_capacity(PARALLEL_RUNS);
@@ -178,23 +178,12 @@ impl ProofOfChunking {
// I think this part is more readable with a range loop
#[allow(clippy::needless_range_loop)]
for l in 0..PARALLEL_RUNS {
let mut sum: u64 = 0;
let mut sum = 0;
for (i, witness_i) in witnesses_s.iter().enumerate() {
for (j, witness_ij) in witness_i.to_chunks().chunks.iter().enumerate() {
debug_assert!(std::mem::size_of::<Chunk>() <= std::mem::size_of::<u64>());
// sum += first_challenge[i][j][l] * (*witness_ij as u64)
sum = sum
.checked_add(
first_challenge[i][j][l]
.checked_mul(*witness_ij as u64)
.ok_or(DkgError::ArithmeticOverflow {
info: "ProofOfChunking::construct | first_challenge[i][j][l] * witness_ij",
})?,
)
.ok_or(DkgError::ArithmeticOverflow {
info: "ProofOfChunking::construct | sum + (first_challenge[i][j][l] * witness_ij)",
})?;
sum += first_challenge[i][j][l] * (*witness_ij as u64)
}
}
@@ -202,18 +191,7 @@ impl ProofOfChunking {
continue 'retry_loop;
}
// shifted_blinding_factors[l] - ss restores it to "proper" [-S, Z - 1] range
// let response = sum + shifted_blinding_factors[l] - ss;
let response = sum
.checked_add(shifted_blinding_factors[l])
.ok_or(DkgError::ArithmeticOverflow {
info:
"ProofOfChunking::construct | sum + (shifted_blinding_factors[l] - ss)",
})?
.checked_sub(ss)
.ok_or(DkgError::ArithmeticUnderflow {
info: "ProofOfChunking::construct | shifted_blinding_factors[l] - ss",
})?;
let response = sum + shifted_blinding_factors[l] - ss;
if response < zz {
responses_chunks.push(response)
} else {
@@ -298,13 +276,11 @@ impl ProofOfChunking {
ensure_len!(&self.responses_r, n);
ensure_len!(&self.responses_chunks, PARALLEL_RUNS);
// ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1))
// Z = 2 * l * S
let ee = 1 << NUM_CHALLENGE_BITS;
let zz: u64 = match compute_ss_zz(n, m) {
Ok((_, zz_res)) => zz_res,
_ => return false,
};
// CHUNK_MAX corresponds to paper's B
let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64;
let zz = 2 * (PARALLEL_RUNS as u64) * ss;
for response_chunk in &self.responses_chunks {
if response_chunk >= &zz {
@@ -435,7 +411,7 @@ impl ProofOfChunking {
random_oracle_builder.update(lambda_e.to_be_bytes());
let mut oracle = rand_chacha::ChaCha20Rng::from_seed(random_oracle_builder.finalize());
let range_max_excl = EE as u64;
let range_max_excl = 1 << NUM_CHALLENGE_BITS;
(0..n)
.map(|_| {
@@ -661,50 +637,6 @@ impl ProofOfChunking {
}
}
fn compute_ss_zz(n: usize, m: usize) -> Result<(u64, u64), DkgError> {
// let ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1)) as u64;
// CHUNK_MAX corresponds to paper's B
let ee = EE;
let ss = n
.checked_mul(m)
.ok_or(DkgError::ArithmeticOverflow {
info: "ProofOfChunking::compute_ss_zz | n * m",
})?
.checked_mul(
CHUNK_SIZE
.checked_sub(1)
.ok_or(DkgError::ArithmeticUnderflow {
info: "ProofOfChunking::compute_ss_zz | (CHUNK_SIZE - 1)",
})?
.checked_mul(ee.checked_sub(1).ok_or(DkgError::ArithmeticUnderflow {
info: "ProofOfChunking::compute_ss_zz | (ee - 1)",
})?)
.ok_or(DkgError::ArithmeticOverflow {
info: "ProofOfChunking::compute_ss_zz | (CHUNK_SIZE - 1) * (ee - 1)",
})?,
)
.ok_or(DkgError::ArithmeticOverflow {
info: "ProofOfChunking::compute_ss_zz | ss_lhs * ss_rhs",
})? as u64;
// let zz = 2 * PARALLEL_RUNS as u64 * ss;
// Z = 2 * l * S
let zz = 2u64
.checked_mul(PARALLEL_RUNS as u64)
.ok_or(DkgError::ArithmeticOverflow {
info: "ProofOfChunking::compute_ss_zz | 2 * l",
})?
.checked_mul(ss)
.ok_or(DkgError::ArithmeticOverflow {
info: "ProofOfChunking::compute_ss_zz | (2 * l) * S",
})?;
Ok((ss, zz))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -720,9 +652,7 @@ mod tests {
ciphertext_chunks: Vec<[G1Projective; NUM_CHUNKS]>,
}
fn setup(
mut rng: impl RngCore + CryptoRng,
) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec<Share>) {
fn setup(mut rng: impl RngCore) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec<Share>) {
let g1 = G1Projective::generator();
let mut pks = Vec::with_capacity(NODES);
+1 -6
View File
@@ -5,7 +5,6 @@ use crate::utils::hash_to_scalar;
use bls12_381::{G1Projective, Scalar};
use ff::Field;
use group::GroupEncoding;
use rand::CryptoRng;
use rand_core::RngCore;
use zeroize::Zeroize;
@@ -21,11 +20,7 @@ pub struct ProofOfDiscreteLog {
}
impl ProofOfDiscreteLog {
pub fn construct(
mut rng: impl RngCore + CryptoRng,
public: &G1Projective,
witness: &Scalar,
) -> Self {
pub fn construct(mut rng: impl RngCore, public: &G1Projective, witness: &Scalar) -> Self {
let mut rand_x = Scalar::random(&mut rng);
let rand_commitment = G1Projective::generator() * rand_x;
let challenge = Self::compute_challenge(public, &rand_commitment);
+2 -4
View File
@@ -9,7 +9,6 @@ use crate::{NodeIndex, Share};
use bls12_381::{G1Projective, G2Projective, Scalar};
use ff::Field;
use group::GroupEncoding;
use rand::CryptoRng;
use rand_core::RngCore;
use std::collections::BTreeMap;
@@ -88,7 +87,7 @@ pub struct ProofOfSecretSharing {
impl ProofOfSecretSharing {
pub fn construct(
mut rng: impl RngCore + CryptoRng,
mut rng: impl RngCore,
instance: Instance,
witness_r: &Scalar,
witnesses_s: &[Share],
@@ -310,14 +309,13 @@ mod tests {
use super::*;
use crate::interpolation::polynomial::Polynomial;
use group::Group;
use rand::CryptoRng;
use rand_core::SeedableRng;
const NODES: u64 = 50;
const THRESHOLD: u64 = 40;
fn setup(
mut rng: impl RngCore + CryptoRng,
mut rng: impl RngCore,
) -> (
BTreeMap<NodeIndex, PublicKey>,
PublicCoefficients,
+3 -4
View File
@@ -13,7 +13,6 @@ use crate::utils::deserialize_g2;
use crate::{NodeIndex, Share, Threshold};
use bls12_381::{G2Projective, Scalar};
use group::GroupEncoding;
use rand::CryptoRng;
use rand_core::RngCore;
use std::collections::BTreeMap;
use zeroize::Zeroize;
@@ -95,7 +94,7 @@ impl Dealing {
// I'm not a big fan of this function signature, but I'm not clear on how to improve it while
// allowing the dealer to skip decryption of its own share if it was also one of the receivers
pub fn create(
mut rng: impl RngCore + CryptoRng + CryptoRng,
mut rng: impl RngCore,
params: &Params,
dealer_index: NodeIndex,
threshold: Threshold,
@@ -485,7 +484,7 @@ mod tests {
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
let shares = dealings
.values()
.map(|dealing| decrypt_share(&params, dk, i, &dealing.ciphertexts, None).unwrap())
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
derived_secrets.push(
combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap(),
@@ -594,7 +593,7 @@ mod tests {
for (i, (dk, _)) in full_keys.iter().enumerate() {
let shares = dealings
.values()
.map(|dealing| decrypt_share(&params, dk, i, &dealing.ciphertexts, None).unwrap())
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
let recovered_secret = combine_shares(shares, &dealer_indices).unwrap();
-6
View File
@@ -99,12 +99,6 @@ pub enum DkgError {
"The reshared dealing has different public constant coefficient than its prior variant"
)]
InvalidResharing,
#[error("Arithmetic Overflow: {info}")]
ArithmeticOverflow { info: &'static str },
#[error("Arithmetic Underflow: {info}")]
ArithmeticUnderflow { info: &'static str },
}
impl DkgError {
+1 -2
View File
@@ -6,7 +6,6 @@ use crate::utils::deserialize_g2;
use bls12_381::{G2Projective, Scalar};
use ff::Field;
use group::GroupEncoding;
use rand::CryptoRng;
use rand_core::RngCore;
use std::ops::{Add, Index, IndexMut};
use zeroize::Zeroize;
@@ -121,7 +120,7 @@ impl Polynomial {
// for polynomial of degree n, we generate n+1 values
// (for example for degree 1, like y = x + 2, we need [2,1])
/// Creates new pseudorandom polynomial of specified degree.
pub fn new_random(mut rng: impl RngCore + CryptoRng + CryptoRng, degree: u64) -> Self {
pub fn new_random(mut rng: impl RngCore, degree: u64) -> Self {
Polynomial {
coefficients: (0..=degree).map(|_| Scalar::random(&mut rng)).collect(),
}
+7 -8
View File
@@ -53,12 +53,11 @@ fn single_sender() {
// make sure each share is actually decryptable (even though proofs say they must be, perform this sanity check)
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
let _recovered = decrypt_share(&params, dk, i, &dealing.ciphertexts, None).unwrap();
let _recovered = decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap();
}
// and for good measure, check that the dealer's share matches decryption result
let recovered_dealer =
decrypt_share(&params, &full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap();
let recovered_dealer = decrypt_share(&full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap();
assert_eq!(recovered_dealer, dealer_share.unwrap());
}
@@ -116,7 +115,7 @@ fn full_threshold_secret_sharing() {
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
let shares = dealings
.values()
.map(|dealing| decrypt_share(&params, dk, i, &dealing.ciphertexts, None).unwrap())
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
// we know dealer_share matches, but it would be inconvenient to try to put them in here,
@@ -190,7 +189,7 @@ fn full_threshold_secret_resharing() {
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
let shares = first_dealings
.values()
.map(|dealing| decrypt_share(&params, dk, i, &dealing.ciphertexts, None).unwrap())
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
let recovered_secret =
@@ -241,7 +240,7 @@ fn full_threshold_secret_resharing() {
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
let shares = resharing_dealings
.values()
.map(|dealing| decrypt_share(&params, dk, i, &dealing.ciphertexts, None).unwrap())
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
let recovered_secret =
@@ -306,7 +305,7 @@ fn full_threshold_secret_resharing_left_party() {
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
let shares = first_dealings
.values()
.map(|dealing| decrypt_share(&params, dk, i, &dealing.ciphertexts, None).unwrap())
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
let recovered_secret =
@@ -370,7 +369,7 @@ fn full_threshold_secret_resharing_left_party() {
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
let shares = resharing_dealings
.values()
.map(|dealing| decrypt_share(&params, dk, i, &dealing.ciphertexts, None).unwrap())
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
.collect();
let recovered_secret = combine_shares(shares, &node_indices).unwrap();
+2 -2
View File
@@ -33,8 +33,8 @@ impl PersistentStatsStorage {
/// * `database_path`: path to the database.
pub async fn init<P: AsRef<Path> + Send>(database_path: P) -> Result<Self, StatsStorageError> {
debug!(
"Attempting to connect to database {}",
database_path.as_ref().display()
"Attempting to connect to database {:?}",
database_path.as_ref().as_os_str()
);
// TODO: we can inject here more stuff based on our gateway global config
+2 -2
View File
@@ -82,8 +82,8 @@ impl GatewayStorage {
message_retrieval_limit: i64,
) -> Result<Self, GatewayStorageError> {
debug!(
"Attempting to connect to database {}",
database_path.as_ref().display()
"Attempting to connect to database {:?}",
database_path.as_ref().as_os_str()
);
// TODO: we can inject here more stuff based on our gateway global config
+1
View File
@@ -23,6 +23,7 @@ fn main() {
"REWARDING_VALIDATOR_ADDRESS",
"NYM_API",
"NYXD_WS",
"EXPLORER_API",
"NYM_VPN_API",
];
+5 -35
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
#[cfg(feature = "network")]
use crate::{ApiUrlConst, DenomDetails, ValidatorDetails};
use crate::{DenomDetails, ValidatorDetails};
pub const NETWORK_NAME: &str = "mainnet";
@@ -17,11 +17,6 @@ pub const MIXNET_CONTRACT_ADDRESS: &str =
"n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr";
pub const VESTING_CONTRACT_ADDRESS: &str =
"n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw";
// \/ TODO: this has to be updated once the contract is deployed
pub const PERFORMANCE_CONTRACT_ADDRESS: &str = "";
// /\ TODO: this has to be updated once the contract is deployed
pub const ECASH_CONTRACT_ADDRESS: &str =
"n1r7s6aksyc6pqardx88k3rkgfagwvj4z4zum9mmz2sfk3zm2mha0sd4dnun";
pub const GROUP_CONTRACT_ADDRESS: &str =
@@ -34,37 +29,10 @@ pub const COCONUT_DKG_CONTRACT_ADDRESS: &str =
pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy";
pub const NYXD_URL: &str = "https://rpc.nymtech.net";
pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket";
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
#[cfg(feature = "network")]
pub const NYM_APIS: &[ApiUrlConst] = &[
ApiUrlConst {
url: NYM_API,
front_hosts: None,
},
ApiUrlConst {
url: "https://nym-fronntdoor.vercel.app/api/",
front_hosts: Some(&["vercel.app", "vercel.com"]),
},
ApiUrlConst {
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
},
];
pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket";
pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/";
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
#[cfg(feature = "network")]
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
ApiUrlConst {
url: NYM_VPN_API,
front_hosts: Some(&["vercel.app", "vercel.com"]),
},
ApiUrlConst {
url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/",
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
},
];
// I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging
pub const EXIT_POLICY_URL: &str =
@@ -155,6 +123,7 @@ pub fn export_to_env() {
set_var_to_default(var_names::NYXD, NYXD_URL);
set_var_to_default(var_names::NYM_API, NYM_API);
set_var_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS);
set_var_to_default(var_names::EXPLORER_API, EXPLORER_API);
set_var_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL);
set_var_to_default(var_names::NYM_VPN_API, NYM_VPN_API);
}
@@ -196,5 +165,6 @@ pub fn export_to_env_if_not_set() {
set_var_conditionally_to_default(var_names::NYXD, NYXD_URL);
set_var_conditionally_to_default(var_names::NYM_API, NYM_API);
set_var_conditionally_to_default(var_names::NYXD_WEBSOCKET, NYXD_WS);
set_var_conditionally_to_default(var_names::EXPLORER_API, EXPLORER_API);
set_var_conditionally_to_default(var_names::EXIT_POLICY_URL, EXIT_POLICY_URL);
}
+11 -40
View File
@@ -20,8 +20,6 @@ pub struct ChainDetails {
pub struct NymContracts {
pub mixnet_contract_address: Option<String>,
pub vesting_contract_address: Option<String>,
#[serde(default)]
pub performance_contract_address: Option<String>,
pub ecash_contract_address: Option<String>,
pub group_contract_address: Option<String>,
pub multisig_contract_address: Option<String>,
@@ -37,38 +35,8 @@ pub struct NymNetworkDetails {
pub chain_details: ChainDetails,
pub endpoints: Vec<ValidatorDetails>,
pub contracts: NymContracts,
pub explorer_api: Option<String>,
pub nym_vpn_api_url: Option<String>,
pub nym_api_urls: Option<Vec<ApiUrl>>,
pub nym_vpn_api_urls: Option<Vec<ApiUrl>>,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
pub struct ApiUrl {
/// Expects a string formatted Url
///
/// see https://docs.rs/url/latest/url/struct.Url.html
pub url: String,
/// Optional alternative equivalent hostnames. Each entry must parse as valid Host
///
/// see https://docs.rs/url/latest/url/enum.Host.html
pub front_hosts: Option<Vec<String>>,
}
pub struct ApiUrlConst<'a> {
pub url: &'a str,
pub front_hosts: Option<&'a [&'a str]>,
}
impl From<ApiUrlConst<'_>> for ApiUrl {
fn from(value: ApiUrlConst) -> Self {
ApiUrl {
url: value.url.to_string(),
front_hosts: value
.front_hosts
.map(|slice| slice.iter().map(|s| s.to_string()).collect()),
}
}
}
// by default we assume the same defaults as mainnet, i.e. same prefixes and denoms
@@ -97,9 +65,8 @@ impl NymNetworkDetails {
},
endpoints: Default::default(),
contracts: Default::default(),
explorer_api: Default::default(),
nym_vpn_api_url: Default::default(),
nym_api_urls: Default::default(),
nym_vpn_api_urls: Default::default(),
}
}
@@ -157,6 +124,7 @@ impl NymNetworkDetails {
.with_group_contract(get_optional_env(var_names::GROUP_CONTRACT_ADDRESS))
.with_multisig_contract(get_optional_env(var_names::MULTISIG_CONTRACT_ADDRESS))
.with_coconut_dkg_contract(get_optional_env(var_names::COCONUT_DKG_CONTRACT_ADDRESS))
.with_explorer_api(get_optional_env(var_names::EXPLORER_API))
.with_nym_vpn_api_url(get_optional_env(var_names::NYM_VPN_API))
}
@@ -177,9 +145,6 @@ impl NymNetworkDetails {
contracts: NymContracts {
mixnet_contract_address: parse_optional_str(mainnet::MIXNET_CONTRACT_ADDRESS),
vesting_contract_address: parse_optional_str(mainnet::VESTING_CONTRACT_ADDRESS),
performance_contract_address: parse_optional_str(
mainnet::PERFORMANCE_CONTRACT_ADDRESS,
),
ecash_contract_address: parse_optional_str(mainnet::ECASH_CONTRACT_ADDRESS),
group_contract_address: parse_optional_str(mainnet::GROUP_CONTRACT_ADDRESS),
multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS),
@@ -187,9 +152,8 @@ impl NymNetworkDetails {
mainnet::COCONUT_DKG_CONTRACT_ADDRESS,
),
},
explorer_api: parse_optional_str(mainnet::EXPLORER_API),
nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API),
nym_api_urls: None,
nym_vpn_api_urls: None,
}
}
@@ -229,6 +193,7 @@ impl NymNetworkDetails {
set_optional_var(var_names::MULTISIG_CONTRACT_ADDRESS, self.contracts.multisig_contract_address);
set_optional_var(var_names::COCONUT_DKG_CONTRACT_ADDRESS, self.contracts.coconut_dkg_contract_address);
set_optional_var(var_names::EXPLORER_API, self.explorer_api);
set_optional_var(var_names::NYM_VPN_API, self.nym_vpn_api_url);
}
@@ -332,6 +297,12 @@ impl NymNetworkDetails {
self
}
#[must_use]
pub fn with_explorer_api<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
self.explorer_api = endpoint.map(Into::into);
self
}
#[must_use]
pub fn with_nym_vpn_api_url<S: Into<String>>(mut self, endpoint: Option<S>) -> Self {
self.nym_vpn_api_url = endpoint.map(Into::into);
+1
View File
@@ -22,6 +22,7 @@ pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS";
pub const NYXD: &str = "NYXD";
pub const NYM_API: &str = "NYM_API";
pub const NYXD_WEBSOCKET: &str = "NYXD_WS";
pub const EXPLORER_API: &str = "EXPLORER_API";
pub const EXIT_POLICY_URL: &str = "EXIT_POLICY";
pub const NYM_VPN_API: &str = "NYM_VPN_API";
pub const CLIENT_STATS_COLLECTION_PROVIDER: &str = "CLIENT_STATS_COLLECTION_PROVIDER";
-33
View File
@@ -1,33 +0,0 @@
[package]
name = "nym-noise"
version = "0.1.0"
authors = ["Simon Wicky <simon@nymtech.net>"]
edition = "2021"
license.workspace = true
[dependencies]
arc-swap = { workspace = true }
bytes = { workspace = true }
futures = { workspace = true }
tracing = { workspace = true }
pin-project = { workspace = true }
sha2 = { workspace = true }
snow = { workspace = true }
strum = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["net", "io-util", "time"] }
tokio-util = { workspace = true, features = ["codec"] }
# internal
nym-crypto = { path = "../crypto" }
nym-noise-keys = { path = "keys" }
[dev-dependencies]
anyhow = { workspace = true }
tokio = { workspace = true, features = ["full"] }
rand_chacha = { workspace = true }
nym-crypto = { path = "../crypto", features = ["rand"] }
[lints]
workspace = true
-17
View File
@@ -1,17 +0,0 @@
[package]
name = "nym-noise-keys"
version = "0.1.0"
authors = ["Simon Wicky <simon@nymtech.net>"]
edition = "2021"
license.workspace = true
[dependencies]
schemars = { workspace = true, features = ["preserve_order"] }
serde = { workspace = true, features = ["derive"] }
utoipa = { workspace = true }
# internal
nym-crypto = { path = "../../crypto", features = ["asymmetric", "serde"] }
[lints]
workspace = true
-43
View File
@@ -1,43 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_crypto::asymmetric::x25519;
use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey;
use serde::{Deserialize, Serialize};
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)]
#[serde(from = "u8", into = "u8")]
pub enum NoiseVersion {
V1,
Unknown(u8), //Implies a newer version we don't know
}
impl From<u8> for NoiseVersion {
fn from(value: u8) -> Self {
match value {
1 => NoiseVersion::V1,
other => NoiseVersion::Unknown(other),
}
}
}
impl From<NoiseVersion> for u8 {
fn from(version: NoiseVersion) -> Self {
match version {
NoiseVersion::V1 => 1,
NoiseVersion::Unknown(other) => other,
}
}
}
#[derive(Copy, Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)]
pub struct VersionedNoiseKey {
#[schemars(with = "u8")]
#[schema(value_type = u8)]
pub supported_version: NoiseVersion,
#[schemars(with = "String")]
#[serde(with = "bs58_x25519_pubkey")]
#[schema(value_type = String)]
pub x25519_pubkey: x25519::PublicKey,
}
-171
View File
@@ -1,171 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::{
collections::HashMap,
net::{IpAddr, SocketAddr},
sync::Arc,
time::Duration,
};
use arc_swap::ArcSwap;
use nym_crypto::asymmetric::x25519;
use nym_noise_keys::{NoiseVersion, VersionedNoiseKey};
use snow::params::NoiseParams;
use strum::{EnumIter, FromRepr};
#[derive(Default, Debug, Clone, Copy, EnumIter, FromRepr, Eq, PartialEq)]
#[repr(u8)]
#[non_exhaustive]
pub enum NoisePattern {
#[default]
XKpsk3 = 1,
IKpsk2 = 2,
}
impl NoisePattern {
pub(crate) const fn as_str(&self) -> &'static str {
match self {
Self::XKpsk3 => "Noise_XKpsk3_25519_AESGCM_SHA256",
Self::IKpsk2 => "Noise_IKpsk2_25519_ChaChaPoly_BLAKE2s", //Wireguard handshake (not exactly though)
}
}
// SAFETY: we have tests to ensure that hardcoded pattern are correct
#[allow(clippy::unwrap_used)]
pub(crate) fn psk_position(&self) -> u8 {
//automatic parsing, works for correct pattern, more convenient
match self.as_str().find("psk") {
Some(n) => {
let psk_index = n + 3;
let psk_char = self.as_str().chars().nth(psk_index).unwrap();
psk_char.to_string().parse().unwrap()
}
None => 0,
}
}
// SAFETY : we have tests to ensure that hardcoded pattern are correct
#[allow(clippy::unwrap_used)]
pub(crate) fn as_noise_params(&self) -> NoiseParams {
self.as_str().parse().unwrap()
}
}
#[derive(Debug, Default)]
struct SocketAddrToKey {
inner: ArcSwap<HashMap<SocketAddr, VersionedNoiseKey>>,
}
// SW NOTE : Only for phased upgrade. To remove once we decide all nodes have to support Noise
#[derive(Debug, Default)]
struct IpAddrToVersion {
inner: ArcSwap<HashMap<IpAddr, NoiseVersion>>,
}
#[derive(Debug, Clone, Default)]
pub struct NoiseNetworkView {
keys: Arc<SocketAddrToKey>,
support: Arc<IpAddrToVersion>,
}
impl NoiseNetworkView {
pub fn new_empty() -> Self {
NoiseNetworkView {
keys: Default::default(),
support: Default::default(),
}
}
pub fn swap_view(&self, new: HashMap<SocketAddr, VersionedNoiseKey>) {
let noise_support = new
.iter()
.map(|(s_addr, key)| (s_addr.ip(), key.supported_version))
.collect::<HashMap<_, _>>();
self.keys.inner.store(Arc::new(new));
self.support.inner.store(Arc::new(noise_support));
}
}
#[derive(Clone)]
pub struct NoiseConfig {
network: NoiseNetworkView,
pub(crate) local_key: Arc<x25519::KeyPair>,
pub(crate) pattern: NoisePattern,
pub(crate) timeout: Duration,
pub(crate) unsafe_disabled: bool, // allows for nodes to not attempt to do a noise handshake, VERY UNSAFE, FOR DEBUG PURPOSE ONLY
}
impl NoiseConfig {
pub fn new(
noise_key: Arc<x25519::KeyPair>,
network: NoiseNetworkView,
timeout: Duration,
) -> Self {
NoiseConfig {
network,
local_key: noise_key,
pattern: Default::default(),
timeout,
unsafe_disabled: false,
}
}
#[must_use]
pub fn with_noise_pattern(mut self, pattern: NoisePattern) -> Self {
self.pattern = pattern;
self
}
#[must_use]
pub fn with_unsafe_disabled(mut self, disabled: bool) -> Self {
self.unsafe_disabled = disabled;
self
}
pub(crate) fn get_noise_key(&self, s_address: &SocketAddr) -> Option<VersionedNoiseKey> {
self.network.keys.inner.load().get(s_address).copied()
}
// Only for phased update
//SW This can lead to some troubles if two nodes shares the same IP and one support Noise but not the other. This in only for the progressive update though and there is no workaround
pub(crate) fn get_noise_support(&self, ip_addr: IpAddr) -> Option<NoiseVersion> {
let plain_ip_support = self.network.support.inner.load().get(&ip_addr).copied();
// SW default bind address being [::]:1789, it can happen that a responder sees the ipv6-mapped address of the initiator, this check for that
let canonical_ip = &ip_addr.to_canonical();
let canonical_ip_support = self.network.support.inner.load().get(canonical_ip).copied();
plain_ip_support.or(canonical_ip_support)
}
}
#[cfg(test)]
mod tests {
use snow::params::NoiseParams;
use super::NoisePattern;
use std::str::FromStr;
use strum::IntoEnumIterator;
// The goal of these is to make sure every NoisePatterns are correct and unwrap can be used on them
#[test]
fn noise_patterns_are_valid() {
for pattern in NoisePattern::iter() {
assert!(NoiseParams::from_str(pattern.as_str()).is_ok())
}
}
#[test]
fn noise_patterns_psk_position_is_valid() {
for pattern in NoisePattern::iter() {
match pattern {
NoisePattern::XKpsk3 => assert_eq!(pattern.psk_position(), 3),
NoisePattern::IKpsk2 => assert_eq!(pattern.psk_position(), 2),
}
}
}
}
-67
View File
@@ -1,67 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::io;
use pin_project::pin_project;
use tokio::io::{AsyncRead, AsyncWrite};
use crate::stream::NoiseStream;
//SW once plain TCP support is dropped, this whole enum can be dropped, and we can only propagate NoiseStream
#[pin_project(project = ConnectionProj)]
pub enum Connection<C> {
Raw(#[pin] C),
Noise(#[pin] Box<NoiseStream<C>>),
}
impl<C> AsyncRead for Connection<C>
where
C: AsyncRead + AsyncWrite + Unpin,
{
fn poll_read(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut tokio::io::ReadBuf<'_>,
) -> std::task::Poll<io::Result<()>> {
match self.project() {
ConnectionProj::Noise(stream) => stream.poll_read(cx, buf),
ConnectionProj::Raw(stream) => stream.poll_read(cx, buf),
}
}
}
impl<C> AsyncWrite for Connection<C>
where
C: AsyncWrite + AsyncRead + Unpin,
{
fn poll_write(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> std::task::Poll<Result<usize, io::Error>> {
match self.project() {
ConnectionProj::Noise(stream) => stream.poll_write(cx, buf),
ConnectionProj::Raw(stream) => stream.poll_write(cx, buf),
}
}
fn poll_flush(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), io::Error>> {
match self.project() {
ConnectionProj::Noise(stream) => stream.poll_flush(cx),
ConnectionProj::Raw(stream) => stream.poll_flush(cx),
}
}
fn poll_shutdown(
self: std::pin::Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), io::Error>> {
match self.project() {
ConnectionProj::Noise(stream) => stream.poll_shutdown(cx),
ConnectionProj::Raw(stream) => stream.poll_shutdown(cx),
}
}
}
-91
View File
@@ -1,91 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_noise_keys::NoiseVersion;
use snow::Error;
use std::io;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum NoiseError {
#[error("encountered a Noise decryption error")]
DecryptionError,
#[error("encountered a Noise Protocol error: {0}")]
ProtocolError(Error),
#[error("encountered an IO error: {0}")]
IoError(#[from] io::Error),
#[error("Incorrect state")]
IncorrectStateError,
#[error("Handshake did not complete")]
HandshakeError,
#[error("unknown noise version (encoded value: {encoded})")]
UnknownVersion { encoded: u8 },
#[error("unknown noise pattern (encoded value: {encoded})")]
UnknownPattern { encoded: u8 },
#[error("unknown noise message type (encoded value: {encoded})")]
UnknownMessageType { encoded: u8 },
#[error("failed to generate psk for requested version {noise_version}")]
PskGenerationFailure { noise_version: u8 },
#[error("noise initiator attempted to use version v{noise_version} of the protocol - we don't know how to handle it")]
UnknownVersionHandshake { noise_version: u8 },
#[error("noise initiator attempted to use an unexpected noise pattern. we're configured for {configured} while it requested {received}")]
UnexpectedNoisePattern {
configured: &'static str,
received: &'static str,
},
#[error("handshake version has unexpectedly changed. initial was {initial:?} and received {received:?}")]
UnexpectedHandshakeVersion {
initial: NoiseVersion,
received: NoiseVersion,
},
#[error("data packet version has unexpectedly changed. initial was {initial:?} and received {received:?}")]
UnexpectedDataVersion {
initial: NoiseVersion,
received: NoiseVersion,
},
#[error("received a non-handshake message during noise handshake")]
NonHandshakeMessageReceived,
#[error("received a non-data message post noise handshake")]
NonDataMessageReceived,
#[error("handshake message exceeded maximum size (got {size} bytes)")]
HandshakeTooBig { size: usize },
#[error("noise message exceeded maximum size (got {size} bytes)")]
DataTooBig { size: usize },
#[error("Handshake timeout")]
HandshakeTimeout(#[from] tokio::time::error::Elapsed),
}
impl NoiseError {
pub(crate) fn naive_to_io_error(self) -> std::io::Error {
match self {
NoiseError::IoError(err) => err,
other => std::io::Error::other(other),
}
}
}
impl From<Error> for NoiseError {
fn from(err: Error) -> Self {
match err {
Error::Decrypt => NoiseError::DecryptionError,
err => NoiseError::ProtocolError(err),
}
}
}
-118
View File
@@ -1,118 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_noise_keys::NoiseVersion;
use snow::error::Prerequisite;
use snow::Error;
use tokio::net::TcpStream;
use tracing::{error, warn};
pub mod config;
pub mod connection;
pub mod error;
pub mod stream;
use crate::config::NoiseConfig;
use crate::connection::Connection;
use crate::error::NoiseError;
use crate::stream::NoiseStreamBuilder;
const NOISE_PSK_PREFIX: &[u8] = b"NYMTECH_NOISE_dQw4w9WgXcQ";
pub const LATEST_NOISE_VERSION: NoiseVersion = NoiseVersion::V1;
// TODO: this should be behind some trait because presumably, depending on the version,
// other arguments would be needed
mod psk_gen {
use crate::error::NoiseError;
use crate::stream::Psk;
use crate::NOISE_PSK_PREFIX;
use nym_crypto::asymmetric::x25519;
use nym_noise_keys::NoiseVersion;
use sha2::{Digest, Sha256};
pub(crate) fn generate_psk(
responder_pub_key: x25519::PublicKey,
version: NoiseVersion,
) -> Result<Psk, NoiseError> {
match version {
NoiseVersion::V1 => Ok(generate_psk_v1(responder_pub_key)),
NoiseVersion::Unknown(noise_version) => {
Err(NoiseError::PskGenerationFailure { noise_version })
}
}
}
fn generate_psk_v1(responder_pub_key: x25519::PublicKey) -> [u8; 32] {
let mut hasher = Sha256::new();
hasher.update(NOISE_PSK_PREFIX);
hasher.update(responder_pub_key.to_bytes());
hasher.finalize().into()
}
}
pub async fn upgrade_noise_initiator(
conn: TcpStream,
config: &NoiseConfig,
) -> Result<Connection<TcpStream>, NoiseError> {
if config.unsafe_disabled {
warn!("Noise is disabled in the config. Not attempting any handshake");
return Ok(Connection::Raw(conn));
}
//Get init material
let responder_addr = conn.peer_addr().map_err(|err| {
error!("Unable to extract peer address from connection - {err}");
Error::Prereq(Prerequisite::RemotePublicKey)
})?;
let Some(key) = config.get_noise_key(&responder_addr) else {
warn!("{responder_addr} can't speak Noise yet, falling back to TCP");
return Ok(Connection::Raw(conn));
};
let handshake_version = match key.supported_version {
NoiseVersion::V1 => NoiseVersion::V1,
// We're talking to a more recent node, but we can't adapt. Let's try to do our best and if it fails, it fails.
// If that node sees we're older, it will try to adapt too.
NoiseVersion::Unknown(version) => {
warn!("{responder_addr} is announcing an v{version} version of Noise that we don't know how to parse, we will attempt to downgrade to our current highest supported version");
LATEST_NOISE_VERSION
}
};
NoiseStreamBuilder::new(conn)
.perform_initiator_handshake(config, handshake_version, key.x25519_pubkey)
.await
.map(|stream| Connection::Noise(Box::new(stream)))
}
pub async fn upgrade_noise_responder(
conn: TcpStream,
config: &NoiseConfig,
) -> Result<Connection<TcpStream>, NoiseError> {
if config.unsafe_disabled {
warn!("Noise is disabled in the config. Not attempting any handshake");
return Ok(Connection::Raw(conn));
}
//Get init material
let initiator_addr = match conn.peer_addr() {
Ok(addr) => addr,
Err(err) => {
error!("Unable to extract peer address from connection - {err}");
return Err(Error::Prereq(Prerequisite::RemotePublicKey).into());
}
};
// if responder doesn't announce noise support, we fallback to tcp
if config.get_noise_support(initiator_addr.ip()).is_none() {
warn!("{initiator_addr} can't speak Noise yet, falling back to TCP",);
return Ok(Connection::Raw(conn));
};
NoiseStreamBuilder::new(conn)
.perform_responder_handshake(config)
.await
.map(|stream| Connection::Noise(Box::new(stream)))
}
-92
View File
@@ -1,92 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::NoiseError;
use crate::stream::framing::{NymNoiseFrame, NymNoiseHeader};
use bytes::{BufMut, BytesMut};
use tokio_util::codec::{Decoder, Encoder};
#[derive(Debug, Clone, Copy)]
enum DecodeState {
Header,
Payload(NymNoiseHeader),
}
pub struct NymNoiseCodec {
state: DecodeState,
}
impl NymNoiseCodec {
pub fn new() -> Self {
NymNoiseCodec {
state: DecodeState::Header,
}
}
fn decode_header(&self, src: &mut BytesMut) -> Result<Option<NymNoiseHeader>, NoiseError> {
if src.len() < NymNoiseHeader::SIZE {
// Not enough data
return Ok(None);
}
// note: successful call to 'decode' advances the buffer by NymNoiseHeader::SIZE
let Some(header) = NymNoiseHeader::decode(src)? else {
return Ok(None);
};
Ok(Some(header))
}
fn decode_data(&self, n: usize, src: &mut BytesMut) -> Option<BytesMut> {
// At this point, the buffer has already had the required capacity
// reserved. All there is to do is read.
if src.len() < n {
return None;
}
Some(src.split_to(n))
}
}
impl Decoder for NymNoiseCodec {
type Item = NymNoiseFrame;
type Error = NoiseError;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
let header = match self.state {
DecodeState::Header => match self.decode_header(src)? {
None => return Ok(None),
Some(header) => {
self.state = DecodeState::Payload(header);
header
}
},
DecodeState::Payload(header) => header,
};
let Some(data) = self.decode_data(header.data_len as usize, src) else {
return Ok(None);
};
// Update the decode state
self.state = DecodeState::Header;
// make sure the buffer has enough space to read the next header
src.reserve(NymNoiseHeader::SIZE);
Ok(Some(NymNoiseFrame {
header,
data: data.freeze(),
}))
}
}
impl Encoder<NymNoiseFrame> for NymNoiseCodec {
type Error = NoiseError;
fn encode(&mut self, frame: NymNoiseFrame, dst: &mut BytesMut) -> Result<(), Self::Error> {
frame.header.encode(dst);
dst.put_slice(frame.data.as_ref());
Ok(())
}
}
-161
View File
@@ -1,161 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::config::NoisePattern;
use crate::error::NoiseError;
use bytes::{Buf, BufMut, Bytes, BytesMut};
use nym_noise_keys::NoiseVersion;
use strum::FromRepr;
#[derive(Debug)]
pub struct NymNoiseFrame {
pub header: NymNoiseHeader,
pub data: Bytes,
}
impl NymNoiseFrame {
pub fn new_handshake_frame(
data: Bytes,
version: NoiseVersion,
pattern: NoisePattern,
) -> Result<Self, NoiseError> {
if data.len() > u16::MAX as usize {
return Err(NoiseError::HandshakeTooBig { size: data.len() });
}
Ok(NymNoiseFrame {
header: NymNoiseHeader {
version,
noise_pattern: pattern,
message_type: NymNoiseMessageType::Handshake,
data_len: data.len() as u16,
},
data,
})
}
pub fn new_data_frame(
data: Bytes,
version: NoiseVersion,
pattern: NoisePattern,
) -> Result<Self, NoiseError> {
if data.len() > u16::MAX as usize {
return Err(NoiseError::HandshakeTooBig { size: data.len() });
}
Ok(NymNoiseFrame {
header: NymNoiseHeader {
version,
noise_pattern: pattern,
message_type: NymNoiseMessageType::Data,
data_len: data.len() as u16,
},
data,
})
}
pub fn version(&self) -> NoiseVersion {
self.header.version
}
pub fn is_handshake_message(&self) -> bool {
self.header.is_handshake_message()
}
pub fn is_data_message(&self) -> bool {
self.header.is_data_message()
}
pub fn noise_pattern(&self) -> NoisePattern {
self.header.noise_pattern
}
}
#[derive(Debug, Copy, Clone, FromRepr)]
#[repr(u8)]
#[non_exhaustive]
pub enum NymNoiseMessageType {
Handshake = 0,
Data = 1,
}
#[derive(Debug, Clone, Copy)]
pub struct NymNoiseHeader {
pub version: NoiseVersion,
pub noise_pattern: NoisePattern,
pub message_type: NymNoiseMessageType,
pub data_len: u16,
}
impl NymNoiseHeader {
pub(crate) const SIZE: usize = 8;
pub fn is_handshake_message(&self) -> bool {
matches!(self.message_type, NymNoiseMessageType::Handshake)
}
pub fn is_data_message(&self) -> bool {
matches!(self.message_type, NymNoiseMessageType::Data)
}
// 0 1 2 3 4 5 6 7 8
// +-+-+-+-+-+-+-+-+
// |V|P|T|Len| Res.|
// +-+-+-+-+-+-+-+-+
pub(crate) fn encode(&self, dst: &mut BytesMut) {
dst.reserve(Self::SIZE);
// byte 0
dst.put_u8(self.version.into());
// byte 1
dst.put_u8(self.noise_pattern as u8);
// byte 2
dst.put_u8(self.message_type as u8);
// byte 3-4
dst.put_u16(self.data_len);
// byte 5-7 (RESERVED):
dst.extend_from_slice(&[0u8; 3])
}
pub(crate) fn decode(src: &mut BytesMut) -> Result<Option<Self>, NoiseError> {
if src.len() < Self::SIZE {
// can't do anything if we don't have enough bytes - but reserve enough for the next call
src.reserve(Self::SIZE);
return Ok(None);
}
let version = src.get_u8();
let pattern = src.get_u8();
let message_type = src.get_u8();
let data_len = src.get_u16();
// reserved
src.advance(3);
let version = NoiseVersion::from(version);
// here, based on versions, we could do vary the further parsing
// match version {
// NoiseVersion::V1 => {}
// NoiseVersion::Unknown(_) => {}
// }
let noise_pattern = NoisePattern::from_repr(pattern)
.ok_or(NoiseError::UnknownPattern { encoded: pattern })?;
let message_type =
NymNoiseMessageType::from_repr(message_type).ok_or(NoiseError::UnknownMessageType {
encoded: message_type,
})?;
Ok(Some(NymNoiseHeader {
version,
noise_pattern,
message_type,
data_len,
}))
}
}
-583
View File
@@ -1,583 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::config::{NoiseConfig, NoisePattern};
use crate::error::NoiseError;
use crate::psk_gen::generate_psk;
use crate::stream::codec::NymNoiseCodec;
use crate::stream::framing::NymNoiseFrame;
use bytes::{Bytes, BytesMut};
use futures::{Sink, SinkExt, Stream, StreamExt};
use nym_crypto::asymmetric::x25519;
use nym_noise_keys::NoiseVersion;
use snow::{Builder, HandshakeState, TransportState};
use std::io;
use std::pin::Pin;
use std::task::Poll;
use std::{cmp::min, task::ready};
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
use tokio_util::codec::Framed;
mod codec;
mod framing;
const TAGLEN: usize = 16;
const HANDSHAKE_MAX_LEN: usize = 1024; // using this constant to limit the handshake's buffer size
pub(crate) type Psk = [u8; 32];
pub(crate) struct NoiseStreamBuilder<C> {
inner_stream: Framed<C, NymNoiseCodec>,
}
impl<C> NoiseStreamBuilder<C> {
pub(crate) fn new(inner_stream: C) -> Self
where
C: AsyncRead + AsyncWrite,
{
NoiseStreamBuilder {
inner_stream: Framed::new(inner_stream, NymNoiseCodec::new()),
}
}
async fn perform_initiator_handshake_inner(
self,
pattern: NoisePattern,
local_private_key: impl AsRef<[u8]>,
remote_pub_key: impl AsRef<[u8]>,
psk: Psk,
version: NoiseVersion,
) -> Result<NoiseStream<C>, NoiseError>
where
C: AsyncRead + AsyncWrite + Unpin,
{
let handshake = Builder::new(pattern.as_noise_params())
.local_private_key(local_private_key.as_ref())
.remote_public_key(remote_pub_key.as_ref())
.psk(pattern.psk_position(), &psk)
.build_initiator()?;
self.perform_handshake(handshake, version, pattern).await
}
pub(crate) async fn perform_initiator_handshake(
self,
config: &NoiseConfig,
version: NoiseVersion,
remote_pub_key: x25519::PublicKey,
) -> Result<NoiseStream<C>, NoiseError>
where
C: AsyncRead + AsyncWrite + Unpin,
{
let psk = generate_psk(remote_pub_key, version)?;
let timeout = config.timeout;
tokio::time::timeout(
timeout,
self.perform_initiator_handshake_inner(
config.pattern,
config.local_key.private_key(),
remote_pub_key,
psk,
version,
),
)
.await?
}
async fn perform_responder_handshake_inner(
mut self,
noise_pattern: NoisePattern,
local_private_key: impl AsRef<[u8]>,
local_pub_key: x25519::PublicKey,
) -> Result<NoiseStream<C>, NoiseError>
where
C: AsyncRead + AsyncWrite + Unpin,
{
// 1. we read the first message from the initiator to establish noise version and pattern
// and determine if we can continue with the handshake
let initial_frame = self
.inner_stream
.next()
.await
.ok_or(NoiseError::IoError(io::ErrorKind::BrokenPipe.into()))??;
if !initial_frame.is_handshake_message() {
return Err(NoiseError::NonHandshakeMessageReceived);
}
let pattern = initial_frame.noise_pattern();
// I can imagine we should be able to handle multiple patterns here, but I guess there's a reason a value is set in the config
// but refactoring this shouldn't be too difficult
if pattern != noise_pattern {
return Err(NoiseError::UnexpectedNoisePattern {
configured: noise_pattern.as_str(),
received: pattern.as_str(),
});
}
// 2. generate psk and handshake state
let psk = generate_psk(local_pub_key, initial_frame.header.version)?;
let mut handshake = Builder::new(pattern.as_noise_params())
.local_private_key(local_private_key.as_ref())
.psk(pattern.psk_position(), &psk)
.build_responder()?;
// update handshake state with initial frame
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN);
handshake.read_message(&initial_frame.data, &mut buf)?;
// 3. run handshake to completion
self.perform_handshake(handshake, initial_frame.version(), pattern)
.await
}
pub(crate) async fn perform_responder_handshake(
self,
config: &NoiseConfig,
) -> Result<NoiseStream<C>, NoiseError>
where
C: AsyncRead + AsyncWrite + Unpin,
{
let timeout = config.timeout;
tokio::time::timeout(
timeout,
self.perform_responder_handshake_inner(
config.pattern,
config.local_key.private_key(),
*config.local_key.public_key(),
),
)
.await?
}
async fn send_handshake_msg(
&mut self,
handshake: &mut HandshakeState,
version: NoiseVersion,
pattern: NoisePattern,
) -> Result<(), NoiseError>
where
C: AsyncRead + AsyncWrite + Unpin,
{
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer
let len = handshake.write_message(&[], &mut buf)?;
buf.truncate(len);
let frame = NymNoiseFrame::new_handshake_frame(buf.freeze(), version, pattern)?;
self.inner_stream.send(frame).await?;
Ok(())
}
async fn recv_handshake_msg(
&mut self,
handshake: &mut HandshakeState,
version: NoiseVersion,
pattern: NoisePattern,
) -> Result<(), NoiseError>
where
C: AsyncRead + AsyncWrite + Unpin,
{
match self.inner_stream.next().await {
Some(Ok(frame)) => {
// validate the frame
if !frame.is_handshake_message() {
return Err(NoiseError::NonHandshakeMessageReceived);
}
if frame.version() != version {
return Err(NoiseError::UnexpectedHandshakeVersion {
initial: version,
received: frame.version(),
});
}
if frame.noise_pattern() != pattern {
return Err(NoiseError::UnexpectedNoisePattern {
configured: pattern.as_str(),
received: frame.noise_pattern().as_str(),
});
}
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer
handshake.read_message(&frame.data, &mut buf)?;
Ok(())
}
Some(Err(err)) => Err(err),
None => Err(NoiseError::HandshakeError),
}
}
async fn perform_handshake(
mut self,
mut handshake_state: HandshakeState,
version: NoiseVersion,
pattern: NoisePattern,
) -> Result<NoiseStream<C>, NoiseError>
where
C: AsyncRead + AsyncWrite + Unpin,
{
while !handshake_state.is_handshake_finished() {
if handshake_state.is_my_turn() {
self.send_handshake_msg(&mut handshake_state, version, pattern)
.await?;
} else {
self.recv_handshake_msg(&mut handshake_state, version, pattern)
.await?;
}
}
let transport = handshake_state.into_transport_mode()?;
Ok(NoiseStream {
inner_stream: self.inner_stream,
negotiated_pattern: pattern,
negotiated_version: version,
transport,
dec_buffer: Default::default(),
})
}
}
/// Wrapper around a TcpStream
pub struct NoiseStream<C> {
inner_stream: Framed<C, NymNoiseCodec>,
negotiated_pattern: NoisePattern,
negotiated_version: NoiseVersion,
transport: TransportState,
dec_buffer: BytesMut,
}
impl<C> NoiseStream<C> {
fn validate_data_frame(&self, frame: NymNoiseFrame) -> Result<Bytes, NoiseError> {
if !frame.is_data_message() {
return Err(NoiseError::NonDataMessageReceived);
}
// validate the frame
if !frame.is_data_message() {
return Err(NoiseError::NonDataMessageReceived);
}
if frame.version() != self.negotiated_version {
return Err(NoiseError::UnexpectedDataVersion {
initial: self.negotiated_version,
received: frame.version(),
});
}
if frame.noise_pattern() != self.negotiated_pattern {
return Err(NoiseError::UnexpectedNoisePattern {
configured: self.negotiated_pattern.as_str(),
received: frame.noise_pattern().as_str(),
});
};
Ok(frame.data)
}
fn poll_data_frame(
&mut self,
cx: &mut std::task::Context<'_>,
) -> Poll<Option<io::Result<Bytes>>>
where
C: AsyncRead + AsyncWrite + Unpin,
{
match ready!(Pin::new(&mut self.inner_stream).poll_next(cx)) {
None => Poll::Ready(None),
Some(Err(err)) => Poll::Ready(Some(Err(err.naive_to_io_error()))),
Some(Ok(frame)) => match self.validate_data_frame(frame) {
Err(err) => Poll::Ready(Some(Err(err.naive_to_io_error()))),
Ok(data) => Poll::Ready(Some(Ok(data))),
},
}
}
}
impl<C> AsyncRead for NoiseStream<C>
where
C: AsyncRead + AsyncWrite + Unpin,
{
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<std::io::Result<()>> {
let pending = match self.poll_data_frame(cx) {
Poll::Pending => {
//no new data, a return value of Poll::Pending means the waking is already scheduled
//Nothing new to decrypt, only check if we can return something from dec_storage, happens after
true
}
Poll::Ready(Some(Ok(noise_msg))) => {
// We have a new noise msg
let mut dec_msg = BytesMut::zeroed(noise_msg.len() - TAGLEN);
let len = match self.transport.read_message(&noise_msg, &mut dec_msg) {
Ok(len) => len,
Err(_) => return Poll::Ready(Err(io::ErrorKind::InvalidInput.into())),
};
self.dec_buffer.extend(&dec_msg[..len]);
false
}
Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err)),
Poll::Ready(None) => {
//Stream is done, we might still have data in the buffer though, happens afterward
false
}
};
// Checking if there is something to return from the buffer
let read_len = min(buf.remaining(), self.dec_buffer.len());
if read_len > 0 {
buf.put_slice(&self.dec_buffer.split_to(read_len));
return Poll::Ready(Ok(()));
}
// buf.remaining == 0 or nothing in the buffer, we must return the value we had from the inner_stream
if pending {
//If we end up here, it means the previous poll_next was pending as well, hence waking is already scheduled
Poll::Pending
} else {
Poll::Ready(Ok(()))
}
}
}
impl<C> AsyncWrite for NoiseStream<C>
where
C: AsyncWrite + Unpin,
{
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
// returns on Poll::Pending and Poll:Ready(Err)
ready!(Pin::new(&mut self.inner_stream).poll_ready(cx))
.map_err(|err| err.naive_to_io_error())?;
// we can send at most u16::MAX bytes in a frame, but we also have to include the tag when encoding
let msg_len = min(u16::MAX as usize - TAGLEN, buf.len());
// Ready to send, encrypting message
let mut noise_buf = BytesMut::zeroed(msg_len + TAGLEN);
let Ok(len) = self
.transport
.write_message(&buf[..msg_len], &mut noise_buf)
else {
return Poll::Ready(Err(io::ErrorKind::InvalidInput.into()));
};
noise_buf.truncate(len);
let frame = NymNoiseFrame::new_data_frame(
noise_buf.freeze(),
self.negotiated_version,
self.negotiated_pattern,
)
.map_err(|err| err.naive_to_io_error())?;
// Tokio uses the same `start_send ` in their SinkWriter implementation. https://docs.rs/tokio-util/latest/src/tokio_util/io/sink_writer.rs.html#104
match Pin::new(&mut self.inner_stream).start_send(frame) {
Ok(()) => Poll::Ready(Ok(msg_len)),
Err(e) => Poll::Ready(Err(e.naive_to_io_error())),
}
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
Pin::new(&mut self.inner_stream)
.poll_flush(cx)
.map_err(|err| err.naive_to_io_error())
}
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut std::task::Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
Pin::new(&mut self.inner_stream)
.poll_close(cx)
.map_err(|err| err.naive_to_io_error())
}
}
#[cfg(test)]
mod tests {
use super::*;
use nym_crypto::asymmetric::x25519;
use rand_chacha::rand_core::SeedableRng;
use std::io::Error;
use std::mem;
use std::sync::Arc;
use std::task::{Context, Waker};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::join;
use tokio::sync::Mutex;
use tokio::time::timeout;
fn mock_streams() -> (MockStream, MockStream) {
let ch1 = Arc::new(Mutex::new(Default::default()));
let ch2 = Arc::new(Mutex::new(Default::default()));
(
MockStream {
inner: MockStreamInner {
tx: ch1.clone(),
rx: ch2.clone(),
},
},
MockStream {
inner: MockStreamInner { tx: ch2, rx: ch1 },
},
)
}
struct MockStream {
inner: MockStreamInner,
}
#[allow(dead_code)]
impl MockStream {
fn unchecked_tx_data(&self) -> Vec<u8> {
self.inner.tx.try_lock().unwrap().data.clone()
}
fn unchecked_rx_data(&self) -> Vec<u8> {
self.inner.rx.try_lock().unwrap().data.clone()
}
}
struct MockStreamInner {
tx: Arc<Mutex<DataWrapper>>,
rx: Arc<Mutex<DataWrapper>>,
}
#[derive(Default)]
struct DataWrapper {
data: Vec<u8>,
waker: Option<Waker>,
}
impl AsyncRead for MockStream {
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let mut inner = self.inner.rx.try_lock().unwrap();
let data = mem::take(&mut inner.data);
if data.is_empty() {
inner.waker = Some(cx.waker().clone());
return Poll::Pending;
}
if let Some(waker) = inner.waker.take() {
waker.wake();
}
buf.put_slice(&data);
Poll::Ready(Ok(()))
}
}
impl AsyncWrite for MockStream {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, Error>> {
let mut inner = self.inner.tx.try_lock().unwrap();
let len = buf.len();
if !inner.data.is_empty() {
assert!(inner.waker.is_none());
inner.waker = Some(cx.waker().clone());
return Poll::Pending;
}
inner.data.extend_from_slice(buf);
if let Some(waker) = inner.waker.take() {
waker.wake();
}
Poll::Ready(Ok(len))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
Poll::Ready(Ok(()))
}
}
#[tokio::test]
async fn noise_handshake() -> anyhow::Result<()> {
let dummy_seed = [42u8; 32];
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
let initiator_keys = Arc::new(x25519::KeyPair::new(&mut rng));
let responder_keys = Arc::new(x25519::KeyPair::new(&mut rng));
let (initiator_stream, responder_stream) = mock_streams();
let psk = generate_psk(*responder_keys.public_key(), NoiseVersion::V1)?;
let pattern = NoisePattern::default();
let stream_initiator = NoiseStreamBuilder::new(initiator_stream)
.perform_initiator_handshake_inner(
pattern,
initiator_keys.private_key().to_bytes(),
responder_keys.public_key().to_bytes(),
psk,
NoiseVersion::V1,
);
let stream_responder = NoiseStreamBuilder::new(responder_stream)
.perform_responder_handshake_inner(
pattern,
responder_keys.private_key().to_bytes(),
*responder_keys.public_key(),
);
let initiator_fut =
tokio::spawn(
async move { timeout(Duration::from_millis(200), stream_initiator).await },
);
let responder_fut =
tokio::spawn(
async move { timeout(Duration::from_millis(200), stream_responder).await },
);
let (initiator, responder) = join!(initiator_fut, responder_fut);
let mut initiator = initiator???;
let mut responder = responder???;
let msg = b"hello there";
// if noise was successful we should be able to write a proper message across
timeout(Duration::from_millis(200), initiator.write_all(msg)).await??;
initiator.inner_stream.flush().await?;
let inner_buf = initiator.inner_stream.get_ref().unchecked_tx_data();
let mut buf = [0u8; 11];
timeout(Duration::from_millis(200), responder.read(&mut buf)).await??;
assert_eq!(&buf[..], msg);
// the inner content is different from the actual msg since it was encrypted
assert_ne!(inner_buf, buf);
assert_ne!(inner_buf.len(), msg.len());
Ok(())
}
}
-20
View File
@@ -45,10 +45,6 @@ pub enum PeerControlRequest {
key: Key,
response_tx: oneshot::Sender<QueryBandwidthControlResponse>,
},
GetClientBandwidth {
key: Key,
response_tx: oneshot::Sender<GetClientBandwidthControlResponse>,
},
}
pub struct AddPeerControlResponse {
@@ -69,10 +65,6 @@ pub struct QueryBandwidthControlResponse {
pub bandwidth_data: Option<RemainingBandwidthData>,
}
pub struct GetClientBandwidthControlResponse {
pub client_bandwidth: Option<ClientBandwidth>,
}
pub struct PeerController {
storage: GatewayStorage,
@@ -275,14 +267,6 @@ impl PeerController {
}))
}
async fn handle_get_client_bandwidth(&self, key: &Key) -> Option<ClientBandwidth> {
if let Some(Some(bandwidth_storage_manager)) = self.bw_storage_managers.get(key) {
Some(bandwidth_storage_manager.read().await.client_bandwidth())
} else {
None
}
}
async fn update_metrics(&self, new_host: &Host) {
let now = SystemTime::now();
const ACTIVITY_THRESHOLD: Duration = Duration::from_secs(180);
@@ -404,10 +388,6 @@ impl PeerController {
response_tx.send(QueryBandwidthControlResponse { success: false, bandwidth_data: None }).ok();
}
}
Some(PeerControlRequest::GetClientBandwidth { key, response_tx }) => {
let client_bandwidth = self.handle_get_client_bandwidth(&key).await;
response_tx.send(GetClientBandwidthControlResponse { client_bandwidth }).ok();
}
None => {
log::trace!("PeerController [main loop]: stopping since channel closed");
break;
+40 -46
View File
@@ -13,10 +13,12 @@ use nym_gateway_storage::models::WireguardPeer;
use nym_task::TaskClient;
use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK;
use std::sync::Arc;
use std::time::{Duration, SystemTime};
use tokio::sync::{mpsc, RwLock};
use tokio_stream::{wrappers::IntervalStream, StreamExt};
pub(crate) type SharedBandwidthStorageManager = Arc<RwLock<BandwidthStorageManager>>;
const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60); // 1 hour
pub struct PeerHandle {
public_key: Key,
@@ -26,6 +28,7 @@ pub struct PeerHandle {
request_tx: mpsc::Sender<PeerControlRequest>,
timeout_check_interval: IntervalStream,
task_client: TaskClient,
startup_timestamp: SystemTime,
}
impl PeerHandle {
@@ -50,6 +53,7 @@ impl PeerHandle {
request_tx,
timeout_check_interval,
task_client,
startup_timestamp: SystemTime::now(),
}
}
@@ -69,49 +73,26 @@ impl PeerHandle {
Ok(success)
}
fn compute_spent_bandwidth(kernel_peer: &Peer, storage_peer: &WireguardPeer) -> Option<u64> {
let storage_peer_rx_bytes = u64::try_from(storage_peer.rx_bytes)
.inspect_err(|e| tracing::error!("Storage rx bytes could not be converted: {e}"))
.ok()?;
let storage_peer_tx_bytes = u64::try_from(storage_peer.tx_bytes)
.inspect_err(|e| tracing::error!("Storage tx bytes could not be converted: {e}"))
.ok()?;
let kernel_total = kernel_peer
.rx_bytes
.checked_add(kernel_peer.tx_bytes)
.or_else(|| {
tracing::error!(
"Overflow on kernel adding bytes: {} + {}",
kernel_peer.rx_bytes,
kernel_peer.tx_bytes
);
None
})?;
let storage_total = storage_peer_rx_bytes
.checked_add(storage_peer_tx_bytes)
.or_else(|| {
tracing::error!("Overflow on storage adding bytes: {storage_peer_rx_bytes} + {storage_peer_tx_bytes}");
None
})?;
kernel_total.checked_sub(storage_total).or_else(|| {
tracing::error!("Overflow on spent bandwidth subtraction: kernel - storage = {kernel_total} - {storage_total}");
None
})
}
async fn active_peer(&mut self, kernel_peer: &Peer) -> Result<bool, Error> {
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
log::debug!(
"Peer {:?} not in storage anymore, shutting down handle",
self.public_key
);
return Ok(false);
};
async fn active_peer(
&mut self,
storage_peer: &WireguardPeer,
kernel_peer: &Peer,
) -> Result<bool, Error> {
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
let spent_bandwidth = Self::compute_spent_bandwidth(kernel_peer, &storage_peer)
if kernel_peer.last_handshake.is_none()
&& SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER
{
let success = self.remove_peer().await?;
self.peer_storage_manager.remove_peer();
tracing::debug!(
"Peer {} has not been active for more then {} seconds, removing it",
kernel_peer.public_key.to_string(),
AUTO_REMOVE_AFTER.as_secs()
);
return Ok(!success);
}
let spent_bandwidth = (kernel_peer.rx_bytes + kernel_peer.tx_bytes)
.checked_sub(storage_peer.rx_bytes as u64 + storage_peer.tx_bytes as u64)
.unwrap_or_else(|| {
// if gateway restarted, the kernel values restart from 0
// and we should restart from 0 in storage as well
@@ -119,10 +100,8 @@ impl PeerHandle {
self.peer_storage_manager.peer_information.as_mut()
{
peer_information.force_sync = true;
peer_information.peer.rx_bytes = kernel_peer.rx_bytes;
peer_information.peer.tx_bytes = kernel_peer.tx_bytes;
}
0
kernel_peer.rx_bytes + kernel_peer.tx_bytes
})
.try_into()
.map_err(|_| Error::InconsistentConsumedBytes)?;
@@ -145,6 +124,14 @@ impl PeerHandle {
}
}
} else {
if SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER {
log::debug!(
"Peer {} has been present for 30 days, removing it",
self.public_key
);
let success = self.remove_peer().await?;
return Ok(!success);
}
let spent_bandwidth = kernel_peer.rx_bytes + kernel_peer.tx_bytes;
if spent_bandwidth >= BANDWIDTH_CAP_PER_DAY {
log::debug!(
@@ -171,7 +158,14 @@ impl PeerHandle {
// the host information hasn't beed updated yet
return Ok(true);
};
if !self.active_peer(&kernel_peer).await? {
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
log::debug!(
"Peer {:?} not in storage anymore, shutting down handle",
self.public_key
);
return Ok(false);
};
if !self.active_peer(&storage_peer, &kernel_peer).await? {
log::debug!(
"Peer {:?} is not active anymore, shutting down handle",
self.public_key
+9 -66
View File
@@ -31,9 +31,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anyhow"
version = "1.0.98"
version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
[[package]]
name = "ark-bls12-381"
@@ -266,20 +266,6 @@ dependencies = [
"thiserror 1.0.64",
]
[[package]]
name = "cargo_metadata"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
dependencies = [
"camino",
"cargo-platform",
"semver",
"serde",
"serde_json",
"thiserror 2.0.12",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
@@ -1133,19 +1119,6 @@ dependencies = [
"vergen",
]
[[package]]
name = "nym-contracts-common-testing"
version = "0.1.0"
dependencies = [
"anyhow",
"cosmwasm-std",
"cw-multi-test",
"cw-storage-plus",
"rand",
"rand_chacha",
"serde",
]
[[package]]
name = "nym-crypto"
version = "0.4.0"
@@ -1227,9 +1200,7 @@ dependencies = [
"cw2",
"easy-addr",
"nym-contracts-common",
"nym-contracts-common-testing",
"nym-crypto",
"nym-mixnet-contract",
"nym-mixnet-contract-common",
"nym-vesting-contract-common",
"rand",
@@ -1253,6 +1224,7 @@ dependencies = [
"schemars",
"semver",
"serde",
"serde-json-wasm",
"serde_repr",
"thiserror 2.0.12",
"time",
@@ -1277,7 +1249,7 @@ dependencies = [
name = "nym-network-defaults"
version = "0.1.0"
dependencies = [
"cargo_metadata 0.19.2",
"cargo_metadata",
"regex",
]
@@ -1290,38 +1262,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "nym-performance-contract"
version = "0.1.0"
dependencies = [
"anyhow",
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"cw-storage-plus",
"cw2",
"nym-contracts-common",
"nym-contracts-common-testing",
"nym-crypto",
"nym-mixnet-contract",
"nym-mixnet-contract-common",
"nym-performance-contract-common",
"serde",
]
[[package]]
name = "nym-performance-contract-common"
version = "0.1.0"
dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"nym-contracts-common",
"schemars",
"serde",
"thiserror 2.0.12",
]
[[package]]
name = "nym-pool-contract"
version = "0.1.0"
@@ -1330,11 +1270,14 @@ dependencies = [
"cosmwasm-schema",
"cosmwasm-std",
"cw-controllers",
"cw-multi-test",
"cw-storage-plus",
"cw2",
"nym-contracts-common",
"nym-contracts-common-testing",
"nym-pool-contract-common",
"rand",
"rand_chacha",
"serde",
]
[[package]]
@@ -2076,7 +2019,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e27d6bdd219887a9eadd19e1c34f32e47fa332301184935c6d9bca26f3cca525"
dependencies = [
"anyhow",
"cargo_metadata 0.18.1",
"cargo_metadata",
"cfg-if",
"regex",
"rustc_version",
+1 -2
View File
@@ -9,7 +9,6 @@ members = [
"multisig/cw3-flex-multisig",
"multisig/cw4-group",
"vesting",
"performance",
]
[workspace.package]
@@ -65,4 +64,4 @@ dbg_macro = "deny"
exit = "deny"
panic = "deny"
unimplemented = "deny"
unreachable = "deny"
unreachable = "deny"
+6 -12
View File
@@ -26,13 +26,13 @@ name = "mixnet_contract"
crate-type = ["cdylib", "rlib"]
[dependencies]
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common" }
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common" }
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing", optional = true }
mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract", package = "nym-mixnet-contract-common", version = "0.6.0" }
vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vesting-contract", package = "nym-vesting-contract-common", version = "0.7.0" }
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common", version = "0.5.0" }
cosmwasm-schema = { workspace = true, optional = true }
cosmwasm-std = { workspace = true }
cw-controllers = { workspace = true }
cw2 = { workspace = true }
cw-storage-plus = { workspace = true }
@@ -41,22 +41,16 @@ bs58 = { workspace = true }
serde = { workspace = true, default-features = false, features = ["derive"] }
semver = { workspace = true }
[dev-dependencies]
anyhow.workspace = true
rand_chacha = { workspace = true }
rand = { workspace = true }
rand_chacha = "0.3"
rand = "0.8.5"
nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] }
easy-addr = { path = "../../common/cosmwasm-smart-contracts/easy_addr" }
# activate the `testable-mixnet-contract` in tests (weird workaround, but it does the trick)
nym-mixnet-contract = { path = ".", features = ["testable-mixnet-contract"] }
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" }
[features]
default = []
contract-testing = ["mixnet-contract-common/contract-testing"]
testable-mixnet-contract = ["nym-contracts-common-testing"]
schema-gen = ["mixnet-contract-common/schema", "cosmwasm-schema"]
[lints]

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