Compare commits
74 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 120220517d | |||
| 7677c02494 | |||
| c20c952633 | |||
| 578c9b0567 | |||
| 802d9b69ca | |||
| 329ad83fc0 | |||
| ec53b570dc | |||
| ebcc658f98 | |||
| 6a155721c6 | |||
| 1bb8b3a3ec | |||
| 8d1a16eb02 | |||
| 8d10cf70e9 | |||
| e32df10b4d | |||
| d1660c01e6 | |||
| 14378b1db9 | |||
| 35bbf5fd84 | |||
| c374a4935a | |||
| 513f4f652d | |||
| 82b9425ca6 | |||
| 615e98b166 | |||
| b11f6c6c70 | |||
| 2f5e8e0bcd | |||
| 812a8782b4 | |||
| a7b57d7e58 | |||
| 84e10a654c | |||
| d724f94319 | |||
| d0692a567a | |||
| 2ae38b9e49 | |||
| ef5990658a | |||
| 658dec8299 | |||
| 447352b8d6 | |||
| eb59615c56 | |||
| 07c908c497 | |||
| 6de0c4ce92 | |||
| 05d8b31e51 | |||
| 692fbf1392 | |||
| 0de4aea77b | |||
| a7cd8efc04 | |||
| 56aad75220 | |||
| e2f2ab89ec | |||
| 4d09b6f2e5 | |||
| b9339b8f0c | |||
| 43a7360399 | |||
| 5f9f7f0fac | |||
| df0e2fe489 | |||
| bc33cc4c8d | |||
| a31597aca9 | |||
| 378229b04e | |||
| fec196c097 | |||
| 1d7ffc1bb6 | |||
| 0caa627960 | |||
| d6b3d7fc0a | |||
| ac273480f8 | |||
| 3ac58e0c49 | |||
| e52bd918fb | |||
| 9d82d6d111 | |||
| 3593631e4a | |||
| f5846d5bc2 | |||
| d7779df1b7 | |||
| 7fcc188041 | |||
| b8c8d33c94 | |||
| 02909c03dd | |||
| 11262836d2 | |||
| f26fd5384d | |||
| 085103b333 | |||
| 574f7f1abd | |||
| 31e161604a | |||
| e4e349bea8 | |||
| 6391b7ed3a | |||
| c225511f95 | |||
| 4eedbb235a | |||
| 548b8717b2 | |||
| a215b3d0bf | |||
| 03d5a133eb |
@@ -44,8 +44,10 @@ 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
|
||||
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
|
||||
# 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
|
||||
@@ -53,5 +55,5 @@ jobs:
|
||||
echo "Version '$TAG' doesn't exist on the remote"
|
||||
else
|
||||
echo "Unknown output '$exists'"
|
||||
exit 1
|
||||
exit 2
|
||||
fi
|
||||
|
||||
@@ -44,8 +44,10 @@ 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
|
||||
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
|
||||
# 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
|
||||
@@ -53,5 +55,5 @@ jobs:
|
||||
echo "Version '$TAG' doesn't exist on the remote"
|
||||
else
|
||||
echo "Unknown output '$exists'"
|
||||
exit 1
|
||||
exit 2
|
||||
fi
|
||||
|
||||
@@ -31,32 +31,26 @@ jobs:
|
||||
- name: Install Rust stable
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: wasm32-unknown-unknown
|
||||
override: true
|
||||
|
||||
- name: Install wasm-opt
|
||||
uses: ./.github/actions/install-wasm-opt
|
||||
with:
|
||||
version: '114'
|
||||
|
||||
- name: Install cosmwasm-check
|
||||
run: cargo install cosmwasm-check
|
||||
|
||||
- name: Build release contracts
|
||||
run: make contracts
|
||||
run: make publish-contracts
|
||||
|
||||
- name: Prepare build output
|
||||
shell: bash
|
||||
env:
|
||||
OUTPUT_DIR: ci-contract-builds/${{ github.ref_name }}
|
||||
run: |
|
||||
cp contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/nym_coconut_dkg.wasm $OUTPUT_DIR
|
||||
cp contracts/target/wasm32-unknown-unknown/release/cw3_flex_multisig.wasm $OUTPUT_DIR
|
||||
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
|
||||
find contracts/artifacts -maxdepth 1 -type f -name '*.wasm' -exec cp {} $OUTPUT_DIR \;
|
||||
# Also include the optimizer-generated checksums if present
|
||||
if [ -f contracts/artifacts/checksums.txt ]; then
|
||||
cp contracts/artifacts/checksums.txt $OUTPUT_DIR
|
||||
fi
|
||||
|
||||
- name: Deploy branch to CI www
|
||||
continue-on-error: true
|
||||
|
||||
@@ -19,7 +19,11 @@ jobs:
|
||||
if: ${{ (startsWith(github.ref, 'refs/tags/nym-binaries-') && github.event_name == 'release') || github.event_name == 'workflow_dispatch' }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
runs-on: arc-ubuntu-22.04
|
||||
matrix:
|
||||
include:
|
||||
- os: arc-ubuntu-22.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
outputs:
|
||||
release_id: ${{ steps.create-release.outputs.id }}
|
||||
|
||||
@@ -5,8 +5,15 @@ on:
|
||||
inputs:
|
||||
gateway_probe_git_ref:
|
||||
type: string
|
||||
default: nym-vpn-core-v1.4.0
|
||||
required: true
|
||||
description: Which gateway probe git ref to build the image with
|
||||
|
||||
release_image:
|
||||
description: 'Tag image as a release'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-agent"
|
||||
CONTAINER_NAME: "node-status-agent"
|
||||
@@ -43,19 +50,32 @@ jobs:
|
||||
GIT_REF_SLUG="${GATEWAY_PROBE_GIT_REF//\//-}"
|
||||
echo "git_ref=${GIT_REF_SLUG}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Remove existing tag if exists
|
||||
run: |
|
||||
if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} >/dev/null 2>&1; then
|
||||
git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}
|
||||
git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}
|
||||
fi
|
||||
- name: Set GIT_TAG variable
|
||||
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Create tag
|
||||
run: |
|
||||
git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }} -m "Version ${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}"
|
||||
git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}
|
||||
- name: Set RELEASE_TAG variable
|
||||
if: github.event.inputs.release_image == 'true'
|
||||
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
|
||||
|
||||
- name: Set IMAGE_NAME_AND_TAGS variable
|
||||
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}" >> $GITHUB_ENV
|
||||
|
||||
- name: New env vars
|
||||
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
|
||||
|
||||
# - name: Remove existing tag if exists
|
||||
# run: |
|
||||
# if git rev-parse $${{ env.GIT_TAG }} >/dev/null 2>&1; then
|
||||
# git push --delete origin $${{ env.GIT_TAG }}
|
||||
# git tag -d $${{ env.GIT_TAG }}
|
||||
# fi
|
||||
|
||||
# - name: Create tag
|
||||
# run: |
|
||||
# git tag -a $${{ env.GIT_TAG }} -m "Version ${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}"
|
||||
# git push origin $${{ env.GIT_TAG }}
|
||||
|
||||
- name: BuildAndPushImageOnHarbor
|
||||
run: |
|
||||
docker build --build-arg GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }} -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }}-${{ steps.cleanup_gateway_probe_ref.outputs.git_ref }}
|
||||
docker build --build-arg GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }} -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }}
|
||||
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
name: Build and upload Node Status API container to harbor.nymte.ch
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
inputs:
|
||||
release_image:
|
||||
description: 'Tag image as a release'
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
env:
|
||||
WORKING_DIRECTORY: "nym-node-status-api/nym-node-status-api"
|
||||
CONTAINER_NAME: "node-status-api"
|
||||
@@ -31,25 +37,35 @@ jobs:
|
||||
with:
|
||||
cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml
|
||||
|
||||
- name: Check if tag exists
|
||||
run: |
|
||||
if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then
|
||||
echo "Tag ${{ steps.get_version.outputs.result }} already exists"
|
||||
fi
|
||||
- name: Set GIT_TAG variable
|
||||
run: echo "GIT_TAG=${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Remove existing tag if exists
|
||||
run: |
|
||||
if git rev-parse ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} >/dev/null 2>&1; then
|
||||
git push --delete origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
|
||||
git tag -d ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }}
|
||||
fi
|
||||
- name: Set RELEASE_TAG variable
|
||||
if: github.event.inputs.release_image == 'true'
|
||||
run: echo "RELEASE_TAG=golden-" >> $GITHUB_ENV
|
||||
|
||||
- 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: Set IMAGE_NAME_AND_TAGS variable
|
||||
run: echo "IMAGE_NAME_AND_TAGS=${{ env.CONTAINER_NAME }}:${{ env.RELEASE_TAG }}${{ steps.get_version.outputs.result }}" >> $GITHUB_ENV
|
||||
|
||||
- name: New env vars
|
||||
run: echo "RELEASE_TAG='$RELEASE_TAG' GIT_TAG='$GIT_TAG' IMAGE_NAME_AND_TAGS='$IMAGE_NAME_AND_TAGS'"
|
||||
|
||||
# - name: Remove existing tag if exists, then create
|
||||
# run: |
|
||||
# if git rev-parse "$GIT_TAG" >/dev/null 2>&1; then
|
||||
# echo "Tag '$GIT_TAG' already exists, deleting"
|
||||
# git push --delete origin "$GIT_TAG"
|
||||
# git tag -d "$GIT_TAG"
|
||||
# echo "Tag '$GIT_TAG' deleted"
|
||||
# else
|
||||
# echo "Tag '$GIT_TAG' does not exist, creating it"
|
||||
# git tag -a $GIT_TAG -m "Version ${{ steps.get_version.outputs.result }}"
|
||||
# git push origin $GIT_TAG
|
||||
# echo "Tag '$GIT_TAG' created"
|
||||
# fi
|
||||
|
||||
- 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 build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.IMAGE_NAME_AND_TAGS }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
|
||||
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
|
||||
|
||||
|
||||
@@ -35,6 +35,8 @@ validator-api/keypair
|
||||
contracts/mixnet/code_id
|
||||
contracts/mixnet/Justfile
|
||||
contracts/mixnet/Makefile
|
||||
artifacts
|
||||
contracts/artifacts
|
||||
validator-config
|
||||
*.patch
|
||||
validator-api-config.toml
|
||||
|
||||
@@ -4,6 +4,54 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2025.13-emmental] (2025-07-22)
|
||||
|
||||
- fix: don't allow mixnode running in exit mode ([#5898])
|
||||
- fix contract build process in Makefile ([#5892])
|
||||
- bugfix: ignore 'Send' responses when claiming bandwidth ([#5884])
|
||||
- Update push-node-status-agent.yaml ([#5882])
|
||||
- listen for shutdown signals during nym-node startup ([#5879])
|
||||
- feat: forbid running mixnode + entry on the same node ([#5878])
|
||||
- chore: 1.88 clippy ([#5877])
|
||||
- Batch SQL writes for packet stats ([#5874])
|
||||
- fix the broken link ([#5873])
|
||||
- Set busy_timeout in sqlx ([#5872])
|
||||
- feat: basic performance contract integration [within Nym API] ([#5871])
|
||||
- scraper bugfix: ignore precommits from missing validators ([#5867])
|
||||
- Return true remaining ([#5866])
|
||||
- Make Mix hops optional for Mixnet Client SURBs ([#5861])
|
||||
- Check gateway supported versions ([#5860])
|
||||
- Add build info endpoints ([#5857])
|
||||
- Clear out screaming logs ([#5856])
|
||||
- fix removal of qa env ([#5855])
|
||||
- Use display when printing paths ([#5853])
|
||||
- feat: initial performance contract ([#5833])
|
||||
- Security patches for the `dkg` crate ([#5828])
|
||||
- HTTP Discovery objects & network defaults ([#5814])
|
||||
|
||||
[#5898]: https://github.com/nymtech/nym/pull/5898
|
||||
[#5892]: https://github.com/nymtech/nym/pull/5892
|
||||
[#5884]: https://github.com/nymtech/nym/pull/5884
|
||||
[#5882]: https://github.com/nymtech/nym/pull/5882
|
||||
[#5879]: https://github.com/nymtech/nym/pull/5879
|
||||
[#5878]: https://github.com/nymtech/nym/pull/5878
|
||||
[#5877]: https://github.com/nymtech/nym/pull/5877
|
||||
[#5874]: https://github.com/nymtech/nym/pull/5874
|
||||
[#5873]: https://github.com/nymtech/nym/pull/5873
|
||||
[#5872]: https://github.com/nymtech/nym/pull/5872
|
||||
[#5871]: https://github.com/nymtech/nym/pull/5871
|
||||
[#5867]: https://github.com/nymtech/nym/pull/5867
|
||||
[#5866]: https://github.com/nymtech/nym/pull/5866
|
||||
[#5861]: https://github.com/nymtech/nym/pull/5861
|
||||
[#5860]: https://github.com/nymtech/nym/pull/5860
|
||||
[#5857]: https://github.com/nymtech/nym/pull/5857
|
||||
[#5856]: https://github.com/nymtech/nym/pull/5856
|
||||
[#5855]: https://github.com/nymtech/nym/pull/5855
|
||||
[#5853]: https://github.com/nymtech/nym/pull/5853
|
||||
[#5833]: https://github.com/nymtech/nym/pull/5833
|
||||
[#5828]: https://github.com/nymtech/nym/pull/5828
|
||||
[#5814]: https://github.com/nymtech/nym/pull/5814
|
||||
|
||||
## [2025.12-dolcelatte] (2025-07-07)
|
||||
|
||||
- bugfix: key-rotation + reply SURBs ([#5876])
|
||||
|
||||
Generated
+184
-16
@@ -1923,6 +1923,26 @@ dependencies = [
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cw-multi-test"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "533b31c94b9e10e77e2468a2b1559aa506505d18c4e52eb64cbfc624ca876ad2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bech32",
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-storage-plus",
|
||||
"cw-utils",
|
||||
"itertools 0.14.0",
|
||||
"prost 0.13.5",
|
||||
"schemars",
|
||||
"serde",
|
||||
"sha2 0.10.9",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cw-storage-plus"
|
||||
version = "2.0.0"
|
||||
@@ -4795,7 +4815,7 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
|
||||
|
||||
[[package]]
|
||||
name = "nym-api"
|
||||
version = "1.1.61"
|
||||
version = "1.1.62"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
@@ -5084,7 +5104,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-cli"
|
||||
version = "1.1.58"
|
||||
version = "1.1.59"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
@@ -5166,7 +5186,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-client"
|
||||
version = "1.1.58"
|
||||
version = "1.1.59"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -5302,6 +5322,7 @@ dependencies = [
|
||||
"nym-sphinx 0.1.0",
|
||||
"nym-task 0.1.0",
|
||||
"sqlx",
|
||||
"sqlx-pool-guard",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
"tokio",
|
||||
@@ -5447,6 +5468,7 @@ dependencies = [
|
||||
name = "nym-contracts-common"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bs58",
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
@@ -5474,6 +5496,19 @@ dependencies = [
|
||||
"vergen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-contracts-common-testing"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cosmwasm-std",
|
||||
"cw-multi-test",
|
||||
"cw-storage-plus",
|
||||
"rand 0.8.5",
|
||||
"rand_chacha 0.3.1",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-cpp-ffi"
|
||||
version = "0.1.2"
|
||||
@@ -5570,6 +5605,7 @@ dependencies = [
|
||||
"nym-ecash-time 0.1.0",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"sqlx-pool-guard",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"zeroize",
|
||||
@@ -6284,7 +6320,6 @@ dependencies = [
|
||||
"schemars",
|
||||
"semver 1.0.26",
|
||||
"serde",
|
||||
"serde-json-wasm",
|
||||
"serde_repr",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
@@ -6436,7 +6471,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-network-requester"
|
||||
version = "1.1.59"
|
||||
version = "1.1.60"
|
||||
dependencies = [
|
||||
"addr",
|
||||
"anyhow",
|
||||
@@ -6486,7 +6521,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node"
|
||||
version = "1.14.0"
|
||||
version = "1.15.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arc-swap",
|
||||
@@ -6647,7 +6682,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-node-status-api"
|
||||
version = "3.0.0"
|
||||
version = "3.1.2"
|
||||
dependencies = [
|
||||
"ammonia",
|
||||
"anyhow",
|
||||
@@ -6889,6 +6924,19 @@ dependencies = [
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-performance-contract-common"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cosmwasm-schema",
|
||||
"cosmwasm-std",
|
||||
"cw-controllers",
|
||||
"nym-contracts-common 0.5.0",
|
||||
"schemars",
|
||||
"serde",
|
||||
"thiserror 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nym-pool-contract-common"
|
||||
version = "0.1.0"
|
||||
@@ -7011,7 +7059,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.58"
|
||||
version = "1.1.59"
|
||||
dependencies = [
|
||||
"bs58",
|
||||
"clap",
|
||||
@@ -7436,7 +7484,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nym-statistics-api"
|
||||
version = "0.1.3"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum 0.7.9",
|
||||
@@ -7708,6 +7756,7 @@ dependencies = [
|
||||
"nym-mixnet-contract-common 0.6.0",
|
||||
"nym-multisig-contract-common 0.1.0",
|
||||
"nym-network-defaults 0.1.0",
|
||||
"nym-performance-contract-common",
|
||||
"nym-serde-helpers 0.1.0",
|
||||
"nym-vesting-contract-common 0.7.0",
|
||||
"prost 0.13.5",
|
||||
@@ -7966,7 +8015,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "nymvisor"
|
||||
version = "0.1.23"
|
||||
version = "0.1.24"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -8689,6 +8738,15 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc_pidinfo"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af53dad2390f8df98dda1e4188322bdf2f91c86cf6001f51d10d64451edf463a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometheus"
|
||||
version = "0.14.0"
|
||||
@@ -10276,6 +10334,19 @@ dependencies = [
|
||||
"whoami",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-pool-guard"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"proc_pidinfo",
|
||||
"sqlx",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"windows 0.61.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-postgres"
|
||||
version = "0.8.6"
|
||||
@@ -10732,6 +10803,7 @@ dependencies = [
|
||||
"nym-mixnet-contract-common 0.6.0",
|
||||
"nym-multisig-contract-common 0.1.0",
|
||||
"nym-pemstore 0.3.0",
|
||||
"nym-performance-contract-common",
|
||||
"nym-validator-client 0.1.0",
|
||||
"nym-vesting-contract-common 0.7.0",
|
||||
"rand 0.8.5",
|
||||
@@ -12291,6 +12363,28 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.61.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c5ee8f3d025738cb02bad7868bbb5f8a6327501e870bf51f1b455b0a2454a419"
|
||||
dependencies = [
|
||||
"windows-collections",
|
||||
"windows-core 0.61.2",
|
||||
"windows-future",
|
||||
"windows-link",
|
||||
"windows-numerics",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-collections"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
@@ -12325,6 +12419,30 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
|
||||
dependencies = [
|
||||
"windows-implement 0.60.0",
|
||||
"windows-interface 0.59.1",
|
||||
"windows-link",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.4.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-future"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link",
|
||||
"windows-threading",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.57.0"
|
||||
@@ -12347,6 +12465,17 @@ dependencies = [
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-implement"
|
||||
version = "0.60.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-interface"
|
||||
version = "0.57.0"
|
||||
@@ -12370,10 +12499,31 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.0"
|
||||
name = "windows-interface"
|
||||
version = "0.59.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
|
||||
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.98",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38"
|
||||
|
||||
[[package]]
|
||||
name = "windows-numerics"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1"
|
||||
dependencies = [
|
||||
"windows-core 0.61.2",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-registry"
|
||||
@@ -12381,7 +12531,7 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3"
|
||||
dependencies = [
|
||||
"windows-result 0.3.1",
|
||||
"windows-result 0.3.4",
|
||||
"windows-strings 0.3.1",
|
||||
"windows-targets 0.53.0",
|
||||
]
|
||||
@@ -12406,9 +12556,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows-result"
|
||||
version = "0.3.1"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06374efe858fab7e4f881500e6e86ec8bc28f9462c47e5a9941a0142ad86b189"
|
||||
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
@@ -12432,6 +12582,15 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-strings"
|
||||
version = "0.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
@@ -12530,6 +12689,15 @@ dependencies = [
|
||||
"windows_x86_64_msvc 0.53.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
|
||||
+5
-5
@@ -34,11 +34,12 @@ 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/multisig-contract", "common/cosmwasm-smart-contracts/nym-performance-contract",
|
||||
"common/cosmwasm-smart-contracts/nym-pool-contract",
|
||||
"common/cosmwasm-smart-contracts/vesting-contract",
|
||||
"common/credential-storage",
|
||||
@@ -126,6 +127,7 @@ 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",
|
||||
@@ -135,7 +137,6 @@ 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",
|
||||
@@ -287,6 +288,7 @@ 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"
|
||||
@@ -368,9 +370,6 @@ 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"
|
||||
@@ -378,6 +377,7 @@ 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 }
|
||||
|
||||
@@ -12,7 +12,11 @@ help:
|
||||
@echo " clippy: run clippy for all workspaces"
|
||||
@echo " test: run clippy, unit tests, and formatting."
|
||||
@echo " test-all: like test, but also includes the expensive tests"
|
||||
@echo " deb: build debian packages
|
||||
@echo " deb: build debian packages"
|
||||
@echo ""
|
||||
@echo "Contract building targets:"
|
||||
@echo " contracts: build contracts for development (includes wasm-opt)"
|
||||
@echo " publish-contracts: build contracts using Docker optimizer (deterministic)"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Meta targets
|
||||
@@ -130,25 +134,69 @@ cargo-test: sdk-wasm-test
|
||||
clippy: sdk-wasm-lint
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Build contracts ready for deploy
|
||||
# Build CosmWasm contracts (deterministic docker build)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
|
||||
contracts: build-release-contracts wasm-opt-contracts cosmwasm-check-contracts
|
||||
WASM_CONTRACT_DIR := contracts/target/wasm32-unknown-unknown/release
|
||||
# Find every direct contract folder that contains a Cargo.toml
|
||||
CONTRACT_DIRS := $(shell find contracts -type f -name Cargo.toml \( ! -path "contracts/Cargo.toml" \) | grep -v integration-tests | xargs -n1 dirname | sort -u)
|
||||
|
||||
CONTRACTS_OUT_DIR = contracts/artifacts
|
||||
|
||||
# Build all contracts via the official CosmWasm optimizer image (one invocation per contract)
|
||||
# See : https://github.com/CosmWasm/optimizer?tab=readme-ov-file#contracts-excluded-from-workspace
|
||||
# The optimizer ships separate multi-arch images. ARM builds are *not* bit-for-bit identical to the
|
||||
# canonical x86_64 build (see README notice in CosmWasm/optimizer). For reproducible artefacts we
|
||||
# therefore always run the amd64 variant by default.
|
||||
# Override with :
|
||||
# $ COSMWASM_OPTIMIZER_IMAGE=cosmwasm/optimizer-arm64:0.17.0 make contracts-publish
|
||||
#
|
||||
COSMWASM_OPTIMIZER_IMAGE ?= cosmwasm/optimizer:0.17.0
|
||||
COSMWASM_OPTIMIZER_PLATFORM ?= linux/amd64
|
||||
|
||||
# Ensure clean build environment and run the optimizer
|
||||
optimize-contracts:
|
||||
@rm -rf artifacts 2>/dev/null || true
|
||||
@echo "=== Ensuring clean build environment"
|
||||
docker volume rm nym_contracts_cache 2>/dev/null || true
|
||||
docker volume rm registry_cache 2>/dev/null || true
|
||||
@for DIR in $(CONTRACT_DIRS); do \
|
||||
echo "=== Optimizing $${DIR}"; \
|
||||
docker run --rm --platform $(COSMWASM_OPTIMIZER_PLATFORM) \
|
||||
-v $(CURDIR):/code \
|
||||
--mount type=volume,source=nym_contracts_cache,target=/target \
|
||||
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
|
||||
-e CARGO_BUILD_INCREMENTAL=false \
|
||||
-e RUSTFLAGS="-C target-cpu=generic -C debuginfo=0" \
|
||||
-e SOURCE_DATE_EPOCH=1 \
|
||||
$(COSMWASM_OPTIMIZER_IMAGE) $${DIR}; \
|
||||
done
|
||||
@mkdir -p $(CONTRACTS_OUT_DIR)
|
||||
@cp artifacts/*.wasm $(CONTRACTS_OUT_DIR)/ 2>/dev/null || true
|
||||
|
||||
@cd $(CONTRACTS_OUT_DIR) && sha256sum *.wasm > checksums.txt
|
||||
# Cleanup temporary artefacts directory
|
||||
@rm -rf artifacts 2>/dev/null || true
|
||||
|
||||
wasm-opt-contracts:
|
||||
for contract in $(CONTRACTS_WASM); do \
|
||||
wasm-opt --signext-lowering -Os $(CONTRACTS_OUT_DIR)/$$contract -o $(CONTRACTS_OUT_DIR)/$$contract; \
|
||||
@for WASM in $(WASM_CONTRACT_DIR)/*.wasm; do \
|
||||
echo "Running wasm-opt on $$WASM"; \
|
||||
wasm-opt --signext-lowering -Os $$WASM -o $$WASM ; \
|
||||
done
|
||||
|
||||
cosmwasm-check-contracts:
|
||||
for contract in $(CONTRACTS_WASM); do \
|
||||
cosmwasm-check $(CONTRACTS_OUT_DIR)/$$contract; \
|
||||
@for WASM in $(WASM_CONTRACT_DIR)/*.wasm; do \
|
||||
echo "Checking $$WASM"; \
|
||||
cosmwasm-check $$WASM ; \
|
||||
done
|
||||
|
||||
# Default development build
|
||||
contracts: build-release-contracts wasm-opt-contracts cosmwasm-check-contracts
|
||||
|
||||
# Publishing build used by CI – deterministic Docker optimiser
|
||||
publish-contracts: optimize-contracts cosmwasm-check-contracts
|
||||
|
||||
# Consider adding 's' to make plural consistent (beware: used in github workflow)
|
||||
contract-schema:
|
||||
$(MAKE) -C contracts schema
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.58"
|
||||
version = "1.1.59"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.58"
|
||||
version = "1.1.59"
|
||||
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"
|
||||
|
||||
@@ -416,6 +416,9 @@ pub struct Traffic {
|
||||
/// will be routed as usual, to the entry gateway, through three mix nodes, egressing
|
||||
/// through the exit gateway. If mix hops are disabled, traffic will be routed directly
|
||||
/// from the entry gateway to the exit gateway, bypassing the mix nodes.
|
||||
///
|
||||
/// This overrides the `use_legacy_sphinx_format` setting as reduced mix hops
|
||||
/// requires use of the updated SURB packet format.
|
||||
pub disable_mix_hops: bool,
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::BadGateway;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::{io, path::PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -19,7 +18,6 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to perform sqlx migration: {source}")]
|
||||
MigrationError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::migrate::MigrateError,
|
||||
},
|
||||
@@ -32,7 +30,6 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to run the SQL query: {source}")]
|
||||
QueryError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::error::Error,
|
||||
},
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
// 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,
|
||||
use crate::{
|
||||
client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend},
|
||||
config,
|
||||
config::Config,
|
||||
error::ClientCoreError,
|
||||
};
|
||||
use crate::config;
|
||||
use crate::config::Config;
|
||||
use crate::error::ClientCoreError;
|
||||
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;
|
||||
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||
use std::path::Path;
|
||||
use std::{fs, io};
|
||||
use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient};
|
||||
use std::{io, path::Path};
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{error, info, trace};
|
||||
use url::Url;
|
||||
@@ -22,11 +20,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!("failed to setup persistent storage backend for our reply needs: {err}");
|
||||
error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}");
|
||||
return Err(ClientCoreError::SurbStorageError {
|
||||
source: Box::new(err),
|
||||
});
|
||||
@@ -40,14 +38,15 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
|
||||
surb_config.minimum_reply_surb_storage_threshold,
|
||||
surb_config.maximum_reply_surb_storage_threshold,
|
||||
);
|
||||
storage_backend
|
||||
.init_fresh(&mem_store)
|
||||
.await
|
||||
.map_err(|err| ClientCoreError::SurbStorageError {
|
||||
source: Box::new(err),
|
||||
})?;
|
||||
|
||||
Ok(storage_backend)
|
||||
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),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fn setup_inactive_backend(surb_config: &config::ReplySurbs) -> fs_backend::Backend {
|
||||
@@ -58,12 +57,11 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
|
||||
// )
|
||||
// }
|
||||
|
||||
fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
|
||||
async 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 =
|
||||
@@ -72,11 +70,15 @@ fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
|
||||
} else {
|
||||
suffix
|
||||
};
|
||||
let renamed = db_path.with_extension(new_extension);
|
||||
|
||||
let mut renamed = db_path.to_owned();
|
||||
renamed.set_extension(new_extension);
|
||||
|
||||
fs::rename(db_path, renamed)
|
||||
tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| {
|
||||
error!(
|
||||
"Failed to rename corrupt database file: {} to {}",
|
||||
db_path.display(),
|
||||
renamed.display()
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
|
||||
@@ -87,13 +89,12 @@ 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).await {
|
||||
Ok(backend) => Ok(backend),
|
||||
Err(err) => {
|
||||
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)?;
|
||||
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?;
|
||||
setup_fresh_backend(db_path, surb_config).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,24 +96,46 @@ impl MixTrafficController {
|
||||
mut mix_packets: Vec<MixPacket>,
|
||||
) -> Result<(), ErasedGatewayError> {
|
||||
debug_assert!(!mix_packets.is_empty());
|
||||
|
||||
let result = if mix_packets.len() == 1 {
|
||||
let send_future = if mix_packets.len() == 1 {
|
||||
// SAFETY: we just checked we have one packet
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let mix_packet = mix_packets.pop().unwrap();
|
||||
self.gateway_transceiver.send_mix_packet(mix_packet).await
|
||||
self.gateway_transceiver.send_mix_packet(mix_packet)
|
||||
} else {
|
||||
self.gateway_transceiver
|
||||
.batch_send_mix_packets(mix_packets)
|
||||
.await
|
||||
self.gateway_transceiver.batch_send_mix_packets(mix_packets)
|
||||
};
|
||||
|
||||
if result.is_err() {
|
||||
self.consecutive_gateway_failure_count += 1;
|
||||
} else {
|
||||
trace!("We *might* have managed to forward sphinx packet(s) to the gateway!");
|
||||
self.consecutive_gateway_failure_count = 0;
|
||||
}
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = self.task_client.recv() => {
|
||||
trace!("received shutdown while handling messages");
|
||||
Ok(())
|
||||
}
|
||||
result = send_future => {
|
||||
if result.is_err() {
|
||||
self.consecutive_gateway_failure_count += 1;
|
||||
} else {
|
||||
trace!("We *might* have managed to forward sphinx packet(s) to the gateway!");
|
||||
self.consecutive_gateway_failure_count = 0;
|
||||
}
|
||||
|
||||
result
|
||||
result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_client_request(&mut self, client_request: ClientRequest) {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = self.task_client.recv() => {
|
||||
trace!("received shutdown while handling client request");
|
||||
}
|
||||
result = self.gateway_transceiver.send_client_request(client_request) => {
|
||||
if let Err(err) = result {
|
||||
error!("Failed to send client request: {err}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(mut self) {
|
||||
@@ -122,7 +144,11 @@ impl MixTrafficController {
|
||||
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
mix_packets = self.mix_rx.recv() => match mix_packets {
|
||||
biased;
|
||||
_ = self.task_client.recv() => {
|
||||
tracing::trace!("MixTrafficController: Received shutdown");
|
||||
break;
|
||||
}mix_packets = self.mix_rx.recv() => match mix_packets {
|
||||
Some(mix_packets) => {
|
||||
if let Err(err) = self.on_messages(mix_packets).await {
|
||||
error!("Failed to send sphinx packet(s) to the gateway: {err}");
|
||||
@@ -144,19 +170,14 @@ impl MixTrafficController {
|
||||
},
|
||||
client_request = self.client_rx.recv() => match client_request {
|
||||
Some(client_request) => {
|
||||
match self.gateway_transceiver.send_client_request(client_request).await {
|
||||
Ok(_) => (),
|
||||
Err(e) => error!("Failed to send client request: {}", e),
|
||||
};
|
||||
self.on_client_request(client_request).await;
|
||||
|
||||
},
|
||||
None => {
|
||||
tracing::trace!("MixTrafficController, client request channel closed");
|
||||
}
|
||||
},
|
||||
_ = self.task_client.recv() => {
|
||||
tracing::trace!("MixTrafficController: Received shutdown");
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
self.task_client.recv_timeout().await;
|
||||
|
||||
+1
-1
@@ -65,7 +65,7 @@ impl AcknowledgementListener {
|
||||
return;
|
||||
}
|
||||
|
||||
trace!("Received {} from the mix network", frag_id);
|
||||
trace!("Received {frag_id} from the mix network");
|
||||
self.stats_tx
|
||||
.report(PacketStatisticsEvent::RealAckReceived(ack_content.len()).into());
|
||||
if let Err(err) = self
|
||||
|
||||
+7
-19
@@ -126,7 +126,7 @@ impl ActionController {
|
||||
fn handle_insert(&mut self, pending_acks: Vec<PendingAcknowledgement>) {
|
||||
for pending_ack in pending_acks {
|
||||
let frag_id = pending_ack.message_chunk.fragment_identifier();
|
||||
trace!("{} is inserted", frag_id);
|
||||
trace!("{frag_id} is inserted");
|
||||
|
||||
if self
|
||||
.pending_acks_data
|
||||
@@ -161,22 +161,16 @@ impl ActionController {
|
||||
let new_queue_key = self.pending_acks_timers.insert(frag_id, timeout);
|
||||
*queue_key = Some(new_queue_key)
|
||||
} else {
|
||||
debug!(
|
||||
"Tried to START TIMER on pending ack that is already gone! - {}",
|
||||
frag_id
|
||||
);
|
||||
debug!("Tried to START TIMER on pending ack that is already gone! - {frag_id}");
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_remove(&mut self, frag_id: FragmentIdentifier) {
|
||||
trace!("{} is getting removed", frag_id);
|
||||
trace!("{frag_id} is getting removed");
|
||||
|
||||
match self.pending_acks_data.remove(&frag_id) {
|
||||
None => {
|
||||
debug!(
|
||||
"Tried to REMOVE pending ack that is already gone! - {}",
|
||||
frag_id
|
||||
);
|
||||
debug!("Tried to REMOVE pending ack that is already gone! - {frag_id}");
|
||||
}
|
||||
Some((_, queue_key)) => {
|
||||
if let Some(queue_key) = queue_key {
|
||||
@@ -188,10 +182,7 @@ impl ActionController {
|
||||
} else {
|
||||
// I'm not 100% sure if having a `None` key is even possible here
|
||||
// (REMOVE would have to be called before START TIMER),
|
||||
debug!(
|
||||
"Tried to REMOVE pending ack without TIMER active - {}",
|
||||
frag_id
|
||||
);
|
||||
debug!("Tried to REMOVE pending ack without TIMER active - {frag_id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,7 +191,7 @@ impl ActionController {
|
||||
// initiated basically as a first step of retransmission. At first data has its delay updated
|
||||
// (as new sphinx packet was created with new expected delivery time)
|
||||
fn handle_update_pending_ack(&mut self, frag_id: FragmentIdentifier, delay: SphinxDelay) {
|
||||
trace!("{} is updating its delay", frag_id);
|
||||
trace!("{frag_id} is updating its delay");
|
||||
// TODO: is it possible to solve this without either locking or temporarily removing the value?
|
||||
if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.remove(&frag_id) {
|
||||
// this Action is triggered by `RetransmissionRequestListener` (for 'normal' packets)
|
||||
@@ -213,10 +204,7 @@ impl ActionController {
|
||||
self.pending_acks_data
|
||||
.insert(frag_id, (Arc::new(inner_data), queue_key));
|
||||
} else {
|
||||
debug!(
|
||||
"Tried to UPDATE TIMER on pending ack that is already gone! - {}",
|
||||
frag_id
|
||||
);
|
||||
debug!("Tried to UPDATE TIMER on pending ack that is already gone! - {frag_id}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -223,6 +223,11 @@ where
|
||||
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = self.task_client.recv() => {
|
||||
tracing::trace!("InputMessageListener: Received shutdown");
|
||||
break;
|
||||
}
|
||||
input_msg = self.input_receiver.recv() => match input_msg {
|
||||
Some(input_msg) => {
|
||||
self.on_input_message(input_msg).await;
|
||||
@@ -232,9 +237,7 @@ where
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = self.task_client.recv() => {
|
||||
tracing::trace!("InputMessageListener: Received shutdown");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
self.task_client.recv_timeout().await;
|
||||
|
||||
+6
-3
@@ -179,6 +179,11 @@ where
|
||||
|
||||
while !self.task_client.is_shutdown() {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = self.task_client.recv() => {
|
||||
tracing::trace!("RetransmissionRequestListener: Received shutdown");
|
||||
break;
|
||||
}
|
||||
timed_out_ack = self.request_receiver.next() => match timed_out_ack {
|
||||
Some(timed_out_ack) => self.on_retransmission_request(timed_out_ack, packet_type).await,
|
||||
None => {
|
||||
@@ -186,9 +191,7 @@ where
|
||||
break;
|
||||
}
|
||||
},
|
||||
_ = self.task_client.recv() => {
|
||||
tracing::trace!("RetransmissionRequestListener: Received shutdown");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
self.task_client.recv_timeout().await;
|
||||
|
||||
@@ -103,6 +103,9 @@ pub(crate) struct Config {
|
||||
/// will be routed as usual, to the entry gateway, through three mix nodes, egressing
|
||||
/// through the exit gateway. If mix hops are disabled, traffic will be routed directly
|
||||
/// from the entry gateway to the exit gateway, bypassing the mix nodes.
|
||||
///
|
||||
/// This overrides the `use_legacy_sphinx_format` setting as reduced mix hops
|
||||
/// requires use of the updated SURB packet format.
|
||||
disable_mix_hops: bool,
|
||||
|
||||
/// Average delay a data packet is going to get delay at a single mixnode.
|
||||
@@ -157,8 +160,12 @@ impl Config {
|
||||
}
|
||||
|
||||
/// Configure whether messages senders using this config should use mix hops or not when sending messages.
|
||||
///
|
||||
/// This overrides the `use_legacy_sphinx_format` setting as disabled mix hops
|
||||
/// requires use of the updated SURB packet format.
|
||||
pub fn disable_mix_hops(mut self, disable_mix_hops: bool) -> Self {
|
||||
self.disable_mix_hops = disable_mix_hops;
|
||||
self.use_legacy_sphinx_format = false;
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -528,6 +535,7 @@ where
|
||||
pending_acks.push(pending_ack);
|
||||
}
|
||||
|
||||
drop(topology_permit);
|
||||
self.insert_pending_acks(pending_acks);
|
||||
self.forward_messages(real_messages, lane).await;
|
||||
|
||||
@@ -709,17 +717,21 @@ where
|
||||
|
||||
// tells real message sender (with the poisson timer) to send this to the mix network
|
||||
pub(crate) async fn forward_messages(
|
||||
&self,
|
||||
&mut self,
|
||||
messages: Vec<RealMessage>,
|
||||
transmission_lane: TransmissionLane,
|
||||
) {
|
||||
if let Err(err) = self
|
||||
.real_message_sender
|
||||
.send((messages, transmission_lane))
|
||||
.await
|
||||
{
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
error!("Failed to forward messages to the real message sender: {err}");
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = self.task_client.recv() => {
|
||||
trace!("received shutdown while attempting to forward mixnet messages");
|
||||
}
|
||||
sending_res = self.real_message_sender.send((messages, transmission_lane)) => {
|
||||
if sending_res.is_err() {
|
||||
error!(
|
||||
"failed to forward mixnet messages due to closed channel (outside of shutdown!)"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,7 +202,7 @@ where
|
||||
// well technically the message was not sent just yet, but now it's up to internal
|
||||
// queues and client load rather than the required delay. So realistically we can treat
|
||||
// whatever is about to happen as negligible additional delay.
|
||||
trace!("{} is about to get sent to the mixnet", frag_id);
|
||||
trace!("{frag_id} is about to get sent to the mixnet");
|
||||
if let Err(err) = self.sent_notifier.unbounded_send(frag_id) {
|
||||
error!("Failed to notify about sent message: {err}");
|
||||
}
|
||||
@@ -278,17 +278,33 @@ where
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = self.mix_tx.send(vec![next_message]).await {
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
tracing::error!("Failed to send: {err}");
|
||||
let sending_res = tokio::select! {
|
||||
biased;
|
||||
_ = self.task_client.recv() => {
|
||||
trace!("received shutdown signal while attempting to send mix message");
|
||||
return
|
||||
}
|
||||
sending_res = self.mix_tx.send(vec![next_message]) => {
|
||||
sending_res
|
||||
}
|
||||
};
|
||||
|
||||
match sending_res {
|
||||
Err(_) => {
|
||||
if !self.task_client.is_shutdown_poll() {
|
||||
tracing::error!(
|
||||
"failed to send mixnet packet due to closed channel (outside of shutdown!)"
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(_) => {
|
||||
let event = if fragment_id.is_some() {
|
||||
PacketStatisticsEvent::RealPacketSent(packet_size)
|
||||
} else {
|
||||
PacketStatisticsEvent::CoverPacketSent(packet_size)
|
||||
};
|
||||
self.stats_tx.report(event.into());
|
||||
}
|
||||
} else {
|
||||
let event = if fragment_id.is_some() {
|
||||
PacketStatisticsEvent::RealPacketSent(packet_size)
|
||||
} else {
|
||||
PacketStatisticsEvent::CoverPacketSent(packet_size)
|
||||
};
|
||||
self.stats_tx.report(event.into());
|
||||
}
|
||||
|
||||
// notify ack controller about sending our message only after we actually managed to push it
|
||||
|
||||
+3
-3
@@ -164,11 +164,11 @@ impl SendingDelayController {
|
||||
self.current_multiplier()
|
||||
);
|
||||
if self.current_multiplier() > 0 {
|
||||
tracing::debug!("{}", status_str);
|
||||
tracing::debug!("{status_str}");
|
||||
} else if self.current_multiplier() > 1 {
|
||||
tracing::info!("{}", status_str);
|
||||
tracing::info!("{status_str}");
|
||||
} else if self.current_multiplier() > 2 {
|
||||
tracing::warn!("{}", status_str);
|
||||
tracing::warn!("{status_str}");
|
||||
}
|
||||
self.time_when_logged_about_elevated_multiplier = now;
|
||||
}
|
||||
|
||||
@@ -221,10 +221,7 @@ impl<R: MessageReceiver> ReceivedMessagesBuffer<R> {
|
||||
let stored_messages = std::mem::take(&mut guard.messages);
|
||||
if !stored_messages.is_empty() {
|
||||
if let Err(err) = sender.unbounded_send(stored_messages) {
|
||||
error!(
|
||||
"The sender channel we just received is already invalidated - {:?}",
|
||||
err
|
||||
);
|
||||
error!("The sender channel we just received is already invalidated - {err:?}");
|
||||
// put the values back to the buffer
|
||||
// the returned error has two fields: err: SendError and val: T,
|
||||
// where val is the value that was failed to get sent;
|
||||
|
||||
@@ -93,7 +93,7 @@ impl StatisticsControl {
|
||||
None,
|
||||
);
|
||||
if let Err(err) = self.report_tx.send(report_message).await {
|
||||
tracing::error!("Failed to report client stats: {:?}", err);
|
||||
tracing::error!("Failed to report client stats: {err:?}");
|
||||
} else {
|
||||
self.stats.reset();
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ impl<T> TransmissionBuffer<T> {
|
||||
};
|
||||
|
||||
let msg = self.pop_front_from_lane(&lane)?;
|
||||
tracing::trace!("picking to send from lane: {:?}", lane);
|
||||
tracing::trace!("picking to send from lane: {lane:?}");
|
||||
Some((lane, msg))
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ pub async fn gateways_for_init<R: Rng>(
|
||||
.nodes;
|
||||
info!("nym api reports {} gateways", gateways.len());
|
||||
|
||||
tracing::trace!("Gateways: {:#?}", gateways);
|
||||
tracing::trace!("Gateways: {gateways:#?}");
|
||||
|
||||
// filter out gateways below minimum performance and ones that could operate as a mixnode
|
||||
// (we don't want instability)
|
||||
@@ -124,7 +124,7 @@ pub async fn gateways_for_init<R: Rng>(
|
||||
.filter_map(|gateway| gateway.try_into().ok())
|
||||
.collect::<Vec<_>>();
|
||||
tracing::debug!("After checking validity: {}", valid_gateways.len());
|
||||
tracing::trace!("Valid gateways: {:#?}", valid_gateways);
|
||||
tracing::trace!("Valid gateways: {valid_gateways:#?}");
|
||||
|
||||
tracing::info!(
|
||||
"and {} after validity and performance filtering",
|
||||
@@ -289,7 +289,7 @@ pub(super) fn get_specified_gateway(
|
||||
gateways: &[RoutingNode],
|
||||
must_use_tls: bool,
|
||||
) -> Result<RoutingNode, ClientCoreError> {
|
||||
tracing::debug!("Requesting specified gateway: {}", gateway_identity);
|
||||
tracing::debug!("Requesting specified gateway: {gateway_identity}");
|
||||
let user_gateway = ed25519::PublicKey::from_base58_string(gateway_identity)
|
||||
.map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?;
|
||||
|
||||
|
||||
@@ -17,15 +17,26 @@ 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", "time"]
|
||||
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,8 +1,7 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::{io, path::PathBuf};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -30,7 +29,6 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to perform sqlx migration: {source}")]
|
||||
MigrationError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::migrate::MigrateError,
|
||||
},
|
||||
@@ -43,7 +41,6 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to run the SQL query: {source}")]
|
||||
QueryError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::error::Error,
|
||||
},
|
||||
|
||||
@@ -13,9 +13,11 @@ use std::path::Path;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{error, info};
|
||||
|
||||
use sqlx_pool_guard::SqlitePoolGuard;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StorageManager {
|
||||
pub connection_pool: sqlx::SqlitePool,
|
||||
connection_pool: SqlitePoolGuard,
|
||||
}
|
||||
|
||||
// all SQL goes here
|
||||
@@ -35,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();
|
||||
|
||||
@@ -47,11 +49,15 @@ 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());
|
||||
}
|
||||
|
||||
@@ -59,10 +65,15 @@ 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())
|
||||
}
|
||||
@@ -71,28 +82,28 @@ impl StorageManager {
|
||||
sqlx::query!(
|
||||
"INSERT INTO status(flush_in_progress, previous_flush, 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(&self, timestamp: OffsetDateTime) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("UPDATE status SET previous_flush = ?", timestamp)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_previous_flush_time(&self) -> Result<OffsetDateTime, sqlx::Error> {
|
||||
sqlx::query!(r#"SELECT previous_flush AS "previous_flush: OffsetDateTime" FROM status"#)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.previous_flush)
|
||||
}
|
||||
@@ -100,14 +111,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)
|
||||
}
|
||||
@@ -115,21 +126,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_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("SELECT * FROM reply_key;")
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -145,14 +156,14 @@ impl StorageManager {
|
||||
stored_reply_key.reply_key,
|
||||
stored_reply_key.sent_at
|
||||
)
|
||||
.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("SELECT * FROM reply_surb_sender;")
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -167,7 +178,7 @@ impl StorageManager {
|
||||
stored_surb_sender.tag,
|
||||
stored_surb_sender.last_sent
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?
|
||||
.last_insert_rowid();
|
||||
Ok(id)
|
||||
@@ -185,17 +196,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(())
|
||||
@@ -213,7 +224,7 @@ impl StorageManager {
|
||||
stored_reply_surb.reply_surb,
|
||||
stored_reply_surb.encoded_key_rotation
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -227,7 +238,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
|
||||
}
|
||||
|
||||
@@ -241,7 +252,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,15 +1,16 @@
|
||||
// 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, StoredSurbSender,
|
||||
use crate::{
|
||||
backend::fs_backend::{
|
||||
manager::StorageManager,
|
||||
models::{ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSurbSender},
|
||||
},
|
||||
surb_storage::ReceivedReplySurbs,
|
||||
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys,
|
||||
};
|
||||
use crate::surb_storage::ReceivedReplySurbs;
|
||||
use crate::{CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys};
|
||||
use async_trait::async_trait;
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use time::OffsetDateTime;
|
||||
use tracing::{error, info, warn};
|
||||
@@ -39,15 +40,17 @@ impl Backend {
|
||||
}
|
||||
|
||||
let manager = StorageManager::init(database_path, true).await?;
|
||||
manager.create_status_table().await?;
|
||||
|
||||
let backend = Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
manager,
|
||||
};
|
||||
|
||||
Ok(backend)
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn try_load<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> {
|
||||
@@ -59,7 +62,25 @@ impl Backend {
|
||||
}
|
||||
|
||||
let manager = StorageManager::init(database_path, false).await?;
|
||||
match Self::try_load_inner(&manager).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) -> 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? {
|
||||
@@ -104,20 +125,11 @@ impl Backend {
|
||||
manager.delete_all_reply_keys().await?;
|
||||
}
|
||||
|
||||
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;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rotate(&mut self) -> Result<(), StorageError> {
|
||||
self.close_pool().await;
|
||||
self.manager.close_pool().await;
|
||||
|
||||
let new_extension = if let Some(existing_extension) =
|
||||
self.database_path.extension().and_then(|ext| ext.to_str())
|
||||
@@ -130,7 +142,8 @@ impl Backend {
|
||||
let mut temp_old = self.database_path.clone();
|
||||
temp_old.set_extension(new_extension);
|
||||
|
||||
fs::rename(&self.database_path, &temp_old)
|
||||
tokio::fs::rename(&self.database_path, &temp_old)
|
||||
.await
|
||||
.map_err(|err| StorageError::DatabaseRenameError { source: err })?;
|
||||
self.manager = StorageManager::init(&self.database_path, true).await?;
|
||||
self.manager.create_status_table().await?;
|
||||
@@ -139,9 +152,10 @@ impl Backend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_old(&mut self) -> Result<(), StorageError> {
|
||||
async fn remove_old(&mut self) -> Result<(), StorageError> {
|
||||
if let Some(old_path) = self.temporary_old_path.take() {
|
||||
fs::remove_file(old_path)
|
||||
tokio::fs::remove_file(old_path)
|
||||
.await
|
||||
.map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err })
|
||||
} else {
|
||||
warn!("the old database file doesn't seem to exist!");
|
||||
@@ -302,7 +316,7 @@ impl ReplyStorageBackend for Backend {
|
||||
self.dump_reply_surb_storage_metadata(surbs_ref).await?;
|
||||
self.dump_reply_surbs(surbs_ref).await?;
|
||||
|
||||
self.remove_old()?;
|
||||
self.remove_old().await?;
|
||||
self.end_storage_flush().await
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,6 @@ where
|
||||
self.backend.load_surb_storage(surb_freshness_cutoff).await
|
||||
}
|
||||
|
||||
// this will have to get enabled after merging develop
|
||||
pub async fn flush_on_shutdown(
|
||||
mut self,
|
||||
mem_state: CombinedReplyStorage,
|
||||
@@ -54,7 +53,6 @@ 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 {
|
||||
|
||||
@@ -201,7 +201,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> {
|
||||
debug!(
|
||||
"Attemting to establish connection to gateway at: {}",
|
||||
"Attempting to establish connection to gateway at: {}",
|
||||
self.gateway_address
|
||||
);
|
||||
let (ws_stream, _) = connect_async(
|
||||
@@ -272,7 +272,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
) -> Result<(), GatewayClientError> {
|
||||
if let Some(shared_key) = self.shared_key() {
|
||||
let encrypted = message.encrypt(&*shared_key)?;
|
||||
Box::pin(self.send_websocket_message(encrypted)).await?;
|
||||
Box::pin(self.send_websocket_message_without_response(encrypted)).await?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(GatewayClientError::ConnectionInInvalidState)
|
||||
@@ -330,9 +330,80 @@ impl<C, St> GatewayClient<C, St> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to send a websocket message to the gateway without waiting for any response
|
||||
async fn send_websocket_message_without_response(
|
||||
&mut self,
|
||||
msg: impl Into<Message>,
|
||||
) -> Result<(), GatewayClientError> {
|
||||
match self.connection {
|
||||
SocketState::Available(ref mut conn) => Ok(conn.send(msg.into()).await?),
|
||||
SocketState::PartiallyDelegated(ref mut partially_delegated) => {
|
||||
if let Err(err) = partially_delegated.send_without_response(msg.into()).await {
|
||||
error!("failed to send message without response - {err}...");
|
||||
// we must ensure we do not leave the task still active
|
||||
if let Err(err) = self.recover_socket_connection().await {
|
||||
error!("... and the delegated stream has also errored out - {err}")
|
||||
}
|
||||
Err(err)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
SocketState::NotConnected => Err(GatewayClientError::ConnectionNotEstablished),
|
||||
_ => Err(GatewayClientError::ConnectionInInvalidState),
|
||||
}
|
||||
}
|
||||
|
||||
// A very nasty hack due to lack of id tags on messages - send a non-sphinx packet websocket
|
||||
// message and wait until first non 'Send' response within timeout
|
||||
pub async fn send_websocket_message_with_non_send_response(
|
||||
&mut self,
|
||||
msg: impl Into<Message>,
|
||||
) -> Result<ServerResponse, GatewayClientError> {
|
||||
let should_restart_mixnet_listener = if self.connection.is_partially_delegated() {
|
||||
self.recover_socket_connection().await?;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let conn = match self.connection {
|
||||
SocketState::Available(ref mut conn) => conn,
|
||||
SocketState::NotConnected => return Err(GatewayClientError::ConnectionNotEstablished),
|
||||
_ => return Err(GatewayClientError::ConnectionInInvalidState),
|
||||
};
|
||||
conn.send(msg.into()).await?;
|
||||
|
||||
let timeout = sleep(self.cfg.connection.response_timeout_duration);
|
||||
tokio::pin!(timeout);
|
||||
|
||||
let response = loop {
|
||||
tokio::select! {
|
||||
_ = &mut timeout => {
|
||||
break Err(GatewayClientError::Timeout);
|
||||
}
|
||||
// note: the below will also listen for shutdown signals
|
||||
msg = self.read_control_response() => {
|
||||
match msg {
|
||||
Ok(res) => if !res.is_send() {
|
||||
break Ok(res);
|
||||
},
|
||||
Err(err) => break Err(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if should_restart_mixnet_listener {
|
||||
self.start_listening_for_mixnet_messages()?;
|
||||
}
|
||||
response
|
||||
}
|
||||
|
||||
/// Attempt to send a websocket message to the gateway and wait until we receive a response.
|
||||
// If we want to send a message (with response), we need to have a full control over the socket,
|
||||
// as we need to be able to write the request and read the subsequent response
|
||||
pub async fn send_websocket_message(
|
||||
pub async fn send_websocket_message_with_response(
|
||||
&mut self,
|
||||
msg: impl Into<Message>,
|
||||
) -> Result<ServerResponse, GatewayClientError> {
|
||||
@@ -387,29 +458,6 @@ impl<C, St> GatewayClient<C, St> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn send_websocket_message_without_response(
|
||||
&mut self,
|
||||
msg: Message,
|
||||
) -> Result<(), GatewayClientError> {
|
||||
match self.connection {
|
||||
SocketState::Available(ref mut conn) => Ok(conn.send(msg).await?),
|
||||
SocketState::PartiallyDelegated(ref mut partially_delegated) => {
|
||||
if let Err(err) = partially_delegated.send_without_response(msg).await {
|
||||
error!("failed to send message without response - {err}...");
|
||||
// we must ensure we do not leave the task still active
|
||||
if let Err(err) = self.recover_socket_connection().await {
|
||||
error!("... and the delegated stream has also errored out - {err}")
|
||||
}
|
||||
Err(err)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
SocketState::NotConnected => Err(GatewayClientError::ConnectionNotEstablished),
|
||||
_ => Err(GatewayClientError::ConnectionInInvalidState),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_gateway_protocol(
|
||||
&self,
|
||||
gateway_protocol: Option<u8>,
|
||||
@@ -535,7 +583,10 @@ impl<C, St> GatewayClient<C, St> {
|
||||
.encrypt(legacy_key)?;
|
||||
|
||||
info!("sending upgrade request and awaiting the acknowledgement back");
|
||||
let (ciphertext, nonce) = match self.send_websocket_message(upgrade_request).await? {
|
||||
let (ciphertext, nonce) = match self
|
||||
.send_websocket_message_with_response(upgrade_request)
|
||||
.await?
|
||||
{
|
||||
ServerResponse::EncryptedResponse { ciphertext, nonce } => (ciphertext, nonce),
|
||||
ServerResponse::Error { message } => {
|
||||
return Err(GatewayClientError::GatewayError(message))
|
||||
@@ -567,7 +618,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
&mut self,
|
||||
msg: ClientControlRequest,
|
||||
) -> Result<(), GatewayClientError> {
|
||||
match self.send_websocket_message(msg).await? {
|
||||
match self.send_websocket_message_with_response(msg).await? {
|
||||
ServerResponse::Authenticate {
|
||||
protocol_version,
|
||||
status,
|
||||
@@ -717,13 +768,16 @@ impl<C, St> GatewayClient<C, St> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempt to retrieve the currently supported gateway protocol version of the remote.
|
||||
pub async fn get_gateway_protocol(&mut self) -> Result<u8, GatewayClientError> {
|
||||
if !self.connection.is_established() {
|
||||
return Err(GatewayClientError::ConnectionNotEstablished);
|
||||
}
|
||||
|
||||
match self
|
||||
.send_websocket_message(ClientControlRequest::SupportedProtocol {})
|
||||
.send_websocket_message_with_non_send_response(
|
||||
ClientControlRequest::SupportedProtocol {},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
ServerResponse::SupportedProtocol { version } => Ok(version),
|
||||
@@ -740,7 +794,10 @@ impl<C, St> GatewayClient<C, St> {
|
||||
credential,
|
||||
self.shared_key.as_ref().unwrap(),
|
||||
)?;
|
||||
let bandwidth_remaining = match self.send_websocket_message(msg).await? {
|
||||
let bandwidth_remaining = match self
|
||||
.send_websocket_message_with_non_send_response(msg)
|
||||
.await?
|
||||
{
|
||||
ServerResponse::Bandwidth { available_total } => Ok(available_total),
|
||||
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
|
||||
ServerResponse::TypedError { error } => {
|
||||
@@ -758,7 +815,10 @@ impl<C, St> GatewayClient<C, St> {
|
||||
|
||||
async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> {
|
||||
let msg = ClientControlRequest::ClaimFreeTestnetBandwidth;
|
||||
let bandwidth_remaining = match self.send_websocket_message(msg).await? {
|
||||
let bandwidth_remaining = match self
|
||||
.send_websocket_message_with_non_send_response(msg)
|
||||
.await?
|
||||
{
|
||||
ServerResponse::Bandwidth { available_total } => Ok(available_total),
|
||||
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
|
||||
other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
|
||||
|
||||
@@ -337,7 +337,7 @@ impl PartiallyDelegatedHandle {
|
||||
// check if the split stream didn't error out
|
||||
let receive_res = stream_receiver
|
||||
.try_recv()
|
||||
.expect("stream sender was somehow dropped without sending anything!");
|
||||
.map_err(|_| GatewayClientError::ConnectionAbruptlyClosed)?;
|
||||
|
||||
if let Some(res) = receive_res {
|
||||
let _res = res?;
|
||||
|
||||
@@ -19,6 +19,7 @@ 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 }
|
||||
|
||||
@@ -13,6 +13,7 @@ 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
|
||||
@@ -21,6 +22,7 @@ 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
|
||||
@@ -29,6 +31,7 @@ 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
|
||||
@@ -37,6 +40,7 @@ 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
|
||||
@@ -44,6 +48,7 @@ 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>;
|
||||
@@ -56,6 +61,7 @@ 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>,
|
||||
@@ -76,6 +82,10 @@ 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())
|
||||
|
||||
+271
@@ -0,0 +1,271 @@
|
||||
// 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;
|
||||
use serde::Deserialize;
|
||||
|
||||
pub use nym_performance_contract_common::{
|
||||
msg::QueryMsg as PerformanceQueryMsg, types::NetworkMonitorResponse, EpochId,
|
||||
EpochMeasurementsPagedResponse, EpochNodePerformance, EpochPerformancePagedResponse,
|
||||
FullHistoricalPerformancePagedResponse, HistoricalPerformance, LastSubmission,
|
||||
NetworkMonitorInformation, NetworkMonitorsPagedResponse, NodeId, NodeMeasurement,
|
||||
NodeMeasurementsResponse, NodePerformance, NodePerformancePagedResponse,
|
||||
NodePerformanceResponse, RetiredNetworkMonitor, RetiredNetworkMonitorsPagedResponse,
|
||||
};
|
||||
|
||||
#[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
|
||||
}
|
||||
|
||||
async fn get_last_submission(&self) -> Result<LastSubmission, NyxdError> {
|
||||
self.query_performance_contract(PerformanceQueryMsg::LastSubmittedMeasurement {})
|
||||
.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;
|
||||
use nym_performance_contract_common::QueryMsg;
|
||||
|
||||
// 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(),
|
||||
QueryMsg::LastSubmittedMeasurement {} => client.get_last_submission().ignore(),
|
||||
};
|
||||
}
|
||||
}
|
||||
+217
@@ -0,0 +1,217 @@
|
||||
// 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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
+30
-2
@@ -665,8 +665,7 @@ where
|
||||
CosmWasmClient::broadcast_tx_commit(self, tx_bytes).await
|
||||
}
|
||||
|
||||
/// Broadcast a transaction to the network and monitors its inclusion in a block.
|
||||
async fn sign_and_broadcast(
|
||||
async fn sign_and_broadcast_inner(
|
||||
&self,
|
||||
signer_address: &AccountId,
|
||||
messages: Vec<Any>,
|
||||
@@ -686,6 +685,35 @@ where
|
||||
self.broadcast_tx(tx_bytes, None, None).await
|
||||
}
|
||||
|
||||
/// Broadcast a transaction to the network and monitors its inclusion in a block.
|
||||
async fn sign_and_broadcast(
|
||||
&self,
|
||||
signer_address: &AccountId,
|
||||
messages: Vec<Any>,
|
||||
fee: Fee,
|
||||
memo: impl Into<String> + Send + 'static,
|
||||
) -> Result<TxResponse, NyxdError> {
|
||||
let memo = memo.into();
|
||||
loop {
|
||||
match self
|
||||
.sign_and_broadcast_inner(
|
||||
signer_address,
|
||||
messages.clone(),
|
||||
fee.clone(),
|
||||
memo.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) => return Ok(res),
|
||||
Err(err) => {
|
||||
if !err.to_string().contains("sequence") {
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn sign(
|
||||
&self,
|
||||
signer_address: &AccountId,
|
||||
|
||||
@@ -12,6 +12,8 @@ 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 {
|
||||
@@ -73,6 +75,11 @@ 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,6 +276,10 @@ 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()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
[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
|
||||
@@ -0,0 +1,127 @@
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// 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::*;
|
||||
@@ -0,0 +1,239 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,305 @@
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
// 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
-2
@@ -11,7 +11,7 @@ use std::rc::Rc;
|
||||
pub struct StorageWrapper(Rc<RefCell<MemoryStorage>>);
|
||||
|
||||
impl StorageWrapper {
|
||||
pub(super) fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper {
|
||||
pub fn contract_storage_wrapper(&self, contract: &Addr) -> ContractStorageWrapper {
|
||||
ContractStorageWrapper {
|
||||
address: contract.clone(),
|
||||
inner: self.clone(),
|
||||
@@ -24,7 +24,7 @@ impl StorageWrapper {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ContractStorageWrapper {
|
||||
pub struct ContractStorageWrapper {
|
||||
address: Addr,
|
||||
inner: StorageWrapper,
|
||||
}
|
||||
@@ -33,6 +33,22 @@ impl ContractStorageWrapper {
|
||||
pub fn inner_storage(&self) -> StorageWrapper {
|
||||
self.inner.clone()
|
||||
}
|
||||
|
||||
pub fn as_inner_storage(&self) -> &StorageWrapper {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
pub fn as_inner_storage_mut(&mut self) -> &mut StorageWrapper {
|
||||
&mut self.inner
|
||||
}
|
||||
|
||||
#[must_use = "this returns the result of the operation, without modifying the original"]
|
||||
pub fn change_contract(&self, contract: &Addr) -> Self {
|
||||
ContractStorageWrapper {
|
||||
address: contract.clone(),
|
||||
inner: self.inner.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Storage for StorageWrapper {
|
||||
@@ -18,6 +18,7 @@ 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)]
|
||||
#[derive(Copy, Default, PartialOrd, Ord, Eq)]
|
||||
pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal);
|
||||
|
||||
impl Percent {
|
||||
@@ -80,6 +80,44 @@ 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 {
|
||||
@@ -334,6 +372,7 @@ 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
|
||||
@@ -347,4 +386,41 @@ 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,7 +23,6 @@ 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::{Addr, Coin};
|
||||
use cosmwasm_std::{to_json_string, 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 {
|
||||
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
to_json_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::{Addr, Coin, Decimal, StdResult, Uint128};
|
||||
use cosmwasm_std::{to_json_string, 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 {
|
||||
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -773,7 +773,7 @@ pub struct MixNodeConfigUpdate {
|
||||
|
||||
impl MixNodeConfigUpdate {
|
||||
pub fn to_inline_json(&self) -> String {
|
||||
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
to_json_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::Decimal;
|
||||
use cosmwasm_std::{to_json_string, 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 {
|
||||
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,7 +410,7 @@ impl IntervalRewardingParamsUpdate {
|
||||
}
|
||||
|
||||
pub fn to_inline_json(&self) -> String {
|
||||
serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,17 @@ pub struct RewardEstimate {
|
||||
pub operating_cost: Decimal,
|
||||
}
|
||||
|
||||
impl RewardEstimate {
|
||||
pub const fn zero() -> RewardEstimate {
|
||||
RewardEstimate {
|
||||
total_node_reward: Decimal::zero(),
|
||||
operator: Decimal::zero(),
|
||||
delegates: Decimal::zero(),
|
||||
operating_cost: Decimal::zero(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
#[derive(Copy, Default)]
|
||||
pub struct RewardDistribution {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
[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
|
||||
@@ -0,0 +1,14 @@
|
||||
// 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 LAST_SUBMISSION: &str = "last-submission";
|
||||
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";
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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 },
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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::*;
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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, LastSubmission, 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>,
|
||||
},
|
||||
|
||||
/// Returns information regarding the latest submitted performance data
|
||||
#[cfg_attr(feature = "schema", returns(LastSubmission))]
|
||||
LastSubmittedMeasurement {},
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct MigrateMsg {
|
||||
//
|
||||
}
|
||||
@@ -0,0 +1,258 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_schema::cw_serde;
|
||||
use cosmwasm_std::{Addr, Env, Timestamp};
|
||||
use nym_contracts_common::Percent;
|
||||
|
||||
pub type EpochId = u32;
|
||||
pub type NodeId = u32;
|
||||
|
||||
#[cw_serde]
|
||||
pub struct LastSubmission {
|
||||
pub block_height: u64,
|
||||
pub block_time: Timestamp,
|
||||
|
||||
// not as relevant, but might as well store it
|
||||
pub data: Option<LastSubmittedData>,
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
pub struct LastSubmittedData {
|
||||
pub sender: Addr,
|
||||
pub epoch_id: EpochId,
|
||||
pub data: NodePerformance,
|
||||
}
|
||||
|
||||
#[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"));
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ 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
|
||||
@@ -31,8 +33,13 @@ 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,10 +7,11 @@ 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: sqlx::SqlitePool,
|
||||
connection_pool: SqlitePoolGuard,
|
||||
}
|
||||
|
||||
impl SqliteEcashTicketbookManager {
|
||||
@@ -19,7 +20,7 @@ impl SqliteEcashTicketbookManager {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `connection_pool`: database connection pool to use.
|
||||
pub fn new(connection_pool: sqlx::SqlitePool) -> Self {
|
||||
pub fn new(connection_pool: SqlitePoolGuard) -> Self {
|
||||
SqliteEcashTicketbookManager { connection_pool }
|
||||
}
|
||||
|
||||
@@ -33,7 +34,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"DELETE FROM ecash_ticketbook WHERE expiration_date <= ?",
|
||||
deadline
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -60,7 +61,7 @@ impl SqliteEcashTicketbookManager {
|
||||
data,
|
||||
expiration_date,
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@@ -90,7 +91,7 @@ impl SqliteEcashTicketbookManager {
|
||||
epoch_id,
|
||||
total_tickets,
|
||||
used_tickets,
|
||||
).execute(&self.connection_pool).await?;
|
||||
).execute(&*self.connection_pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -105,7 +106,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
)
|
||||
.bind(data)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await?
|
||||
.is_some();
|
||||
|
||||
@@ -121,7 +122,7 @@ impl SqliteEcashTicketbookManager {
|
||||
FROM ecash_ticketbook
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -143,7 +144,7 @@ impl SqliteEcashTicketbookManager {
|
||||
ticketbook_id,
|
||||
expected_current_total_spent
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?
|
||||
.rows_affected();
|
||||
Ok(affected > 0)
|
||||
@@ -153,7 +154,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
|
||||
}
|
||||
|
||||
@@ -165,7 +166,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"DELETE FROM pending_issuance WHERE deposit_id = ?",
|
||||
pending_id
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -182,7 +183,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
epoch_id
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -208,7 +209,7 @@ impl SqliteEcashTicketbookManager {
|
||||
serialisation_revision,
|
||||
epoch_id
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -225,7 +226,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
epoch_id
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -251,7 +252,7 @@ impl SqliteEcashTicketbookManager {
|
||||
serialisation_revision,
|
||||
epoch_id,
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -269,7 +270,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
expiration_date
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -298,7 +299,7 @@ impl SqliteEcashTicketbookManager {
|
||||
serialisation_revision,
|
||||
expiration_date
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.execute(&*self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ use sqlx::{
|
||||
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
|
||||
ConnectOptions,
|
||||
};
|
||||
use sqlx_pool_guard::SqlitePoolGuard;
|
||||
use std::path::Path;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
@@ -54,15 +55,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().as_os_str()
|
||||
"Attempting to connect to database {}",
|
||||
database_path.as_ref().display()
|
||||
);
|
||||
|
||||
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();
|
||||
|
||||
@@ -74,13 +75,17 @@ impl PersistentStorage {
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await {
|
||||
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 {
|
||||
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.clone()),
|
||||
storage_manager: SqliteEcashTicketbookManager::new(connection_pool),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,8 @@ impl BandwidthStorageManager {
|
||||
debug!(available = available_bi2, required = required_bi2);
|
||||
|
||||
self.consume_bandwidth(required_bandwidth).await?;
|
||||
Ok(available_bandwidth)
|
||||
let remaining_bandwidth = self.client_bandwidth.available().await;
|
||||
Ok(remaining_bandwidth)
|
||||
}
|
||||
|
||||
async fn expire_bandwidth(&mut self) -> Result<()> {
|
||||
|
||||
@@ -14,6 +14,7 @@ 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;
|
||||
|
||||
@@ -31,7 +32,7 @@ pub fn precomputing_g2_generator_for_miller_loop(c: &mut Criterion) {
|
||||
}
|
||||
|
||||
fn prepare_keys(
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
nodes: usize,
|
||||
) -> (BTreeMap<NodeIndex, PublicKey>, Vec<DecryptionKey>) {
|
||||
let params = setup();
|
||||
@@ -50,7 +51,7 @@ fn prepare_keys(
|
||||
}
|
||||
|
||||
fn prepare_resharing(
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
params: &Params,
|
||||
nodes: usize,
|
||||
threshold: Threshold,
|
||||
@@ -68,7 +69,7 @@ fn prepare_resharing(
|
||||
for (i, ref mut dk) in dks.iter_mut().enumerate() {
|
||||
let shares = first_dealings
|
||||
.iter()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(params, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret =
|
||||
@@ -154,7 +155,9 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
|
||||
black_box(
|
||||
decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(),
|
||||
);
|
||||
})
|
||||
},
|
||||
);
|
||||
@@ -237,7 +240,9 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
|
||||
black_box(
|
||||
decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(),
|
||||
);
|
||||
})
|
||||
},
|
||||
);
|
||||
@@ -320,7 +325,9 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok());
|
||||
black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap());
|
||||
black_box(
|
||||
decrypt_share(¶ms, first_key, 0, &dealing.ciphertexts, None).unwrap(),
|
||||
);
|
||||
})
|
||||
},
|
||||
);
|
||||
@@ -547,7 +554,7 @@ pub fn share_decryption(c: &mut Criterion) {
|
||||
let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], ¶ms, &mut rng);
|
||||
|
||||
c.bench_function("single share decryption", |b| {
|
||||
b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, None)))
|
||||
b.iter(|| black_box(decrypt_share(¶ms, &dk, 0, &ciphertexts, None)))
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ 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;
|
||||
@@ -191,7 +192,7 @@ impl HazmatRandomness {
|
||||
pub fn encrypt_shares(
|
||||
shares: &[(&Share, &PublicKey)],
|
||||
params: &Params,
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
) -> (Ciphertexts, HazmatRandomness) {
|
||||
let g1 = G1Projective::generator();
|
||||
|
||||
@@ -262,6 +263,7 @@ 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,
|
||||
@@ -270,6 +272,10 @@ 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));
|
||||
}
|
||||
@@ -461,10 +467,22 @@ mod tests {
|
||||
let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng);
|
||||
verify_hazmat_rand(&ciphertext, &hazmat);
|
||||
|
||||
let recovered1 =
|
||||
decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap();
|
||||
let recovered2 =
|
||||
decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap();
|
||||
let recovered1 = decrypt_share(
|
||||
¶ms,
|
||||
&decryption_key1,
|
||||
0,
|
||||
&ciphertext,
|
||||
Some(lookup_table),
|
||||
)
|
||||
.unwrap();
|
||||
let recovered2 = decrypt_share(
|
||||
¶ms,
|
||||
&decryption_key2,
|
||||
1,
|
||||
&ciphertext,
|
||||
Some(lookup_table),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(m1, recovered1);
|
||||
assert_eq!(m2, recovered2);
|
||||
}
|
||||
@@ -490,10 +508,22 @@ mod tests {
|
||||
let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng);
|
||||
verify_hazmat_rand(&ciphertext, &hazmat);
|
||||
|
||||
let recovered1 =
|
||||
decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap();
|
||||
let recovered2 =
|
||||
decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap();
|
||||
let recovered1 = decrypt_share(
|
||||
¶ms,
|
||||
&decryption_key1,
|
||||
0,
|
||||
&ciphertext,
|
||||
Some(lookup_table),
|
||||
)
|
||||
.unwrap();
|
||||
let recovered2 = decrypt_share(
|
||||
¶ms,
|
||||
&decryption_key2,
|
||||
1,
|
||||
&ciphertext,
|
||||
Some(lookup_table),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(m1, recovered1);
|
||||
assert_eq!(m2, recovered2);
|
||||
}
|
||||
@@ -574,7 +604,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn ciphertexts_roundtrip() {
|
||||
fn random_ciphertexts(mut rng: impl RngCore, num_receivers: usize) -> Ciphertexts {
|
||||
fn random_ciphertexts(
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
num_receivers: usize,
|
||||
) -> Ciphertexts {
|
||||
Ciphertexts {
|
||||
rr: (0..NUM_CHUNKS)
|
||||
.map(|_| G1Projective::random(&mut rng))
|
||||
|
||||
@@ -9,11 +9,15 @@ 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) -> (DecryptionKey, PublicKeyWithProof) {
|
||||
pub fn keygen(
|
||||
params: &Params,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
) -> (DecryptionKey, PublicKeyWithProof) {
|
||||
let g1 = G1Projective::generator();
|
||||
let g2 = G2Projective::generator();
|
||||
|
||||
@@ -244,7 +248,7 @@ pub struct KeyPair {
|
||||
}
|
||||
|
||||
impl KeyPair {
|
||||
pub fn new(params: &Params, rng: impl RngCore) -> Self {
|
||||
pub fn new(params: &Params, rng: impl RngCore + CryptoRng) -> Self {
|
||||
let (dk, pk) = keygen(params, rng);
|
||||
Self {
|
||||
private_key: dk,
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::utils::{deserialize_scalar, RandomOracleBuilder};
|
||||
use bls12_381::{G1Projective, Scalar};
|
||||
use ff::Field;
|
||||
use group::{Group, GroupEncoding};
|
||||
use rand::Rng;
|
||||
use rand::{CryptoRng, Rng};
|
||||
use rand_core::{RngCore, SeedableRng};
|
||||
|
||||
const CHUNKING_ORACLE_DOMAIN: &[u8] =
|
||||
@@ -28,6 +28,7 @@ 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>>>;
|
||||
@@ -94,7 +95,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,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
instance: Instance,
|
||||
witness_r: &[Scalar; NUM_CHUNKS],
|
||||
witnesses_s: &[Share],
|
||||
@@ -110,21 +111,20 @@ impl ProofOfChunking {
|
||||
// define bounds for the blinding factors
|
||||
let n = instance.public_keys.len();
|
||||
let m = NUM_CHUNKS;
|
||||
let ee = 1 << NUM_CHALLENGE_BITS;
|
||||
|
||||
// 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");
|
||||
// ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1))
|
||||
// Z = 2 * l * S
|
||||
let (ss, zz): (u64, u64) = compute_ss_zz(n, m)?;
|
||||
|
||||
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.
|
||||
let combined_upper_range = (zz - 1)
|
||||
.checked_add(ss + 1)
|
||||
.expect("overflow in Z - 1 + S + 1");
|
||||
// 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 mut betas = Vec::with_capacity(PARALLEL_RUNS);
|
||||
let mut bs = Vec::with_capacity(PARALLEL_RUNS);
|
||||
@@ -178,12 +178,23 @@ 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 = 0;
|
||||
let mut sum: u64 = 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 += 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)",
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -191,7 +202,18 @@ 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 + 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",
|
||||
})?;
|
||||
|
||||
if response < zz {
|
||||
responses_chunks.push(response)
|
||||
} else {
|
||||
@@ -276,11 +298,13 @@ impl ProofOfChunking {
|
||||
ensure_len!(&self.responses_r, n);
|
||||
ensure_len!(&self.responses_chunks, PARALLEL_RUNS);
|
||||
|
||||
let ee = 1 << NUM_CHALLENGE_BITS;
|
||||
// ss = (n * m * (CHUNK_SIZE - 1) * (ee - 1))
|
||||
// Z = 2 * l * S
|
||||
|
||||
// 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;
|
||||
let zz: u64 = match compute_ss_zz(n, m) {
|
||||
Ok((_, zz_res)) => zz_res,
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
for response_chunk in &self.responses_chunks {
|
||||
if response_chunk >= &zz {
|
||||
@@ -411,7 +435,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 = 1 << NUM_CHALLENGE_BITS;
|
||||
let range_max_excl = EE as u64;
|
||||
|
||||
(0..n)
|
||||
.map(|_| {
|
||||
@@ -637,6 +661,50 @@ 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::*;
|
||||
@@ -652,7 +720,9 @@ mod tests {
|
||||
ciphertext_chunks: Vec<[G1Projective; NUM_CHUNKS]>,
|
||||
}
|
||||
|
||||
fn setup(mut rng: impl RngCore) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec<Share>) {
|
||||
fn setup(
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
) -> (OwnedInstance, [Scalar; NUM_CHUNKS], Vec<Share>) {
|
||||
let g1 = G1Projective::generator();
|
||||
|
||||
let mut pks = Vec::with_capacity(NODES);
|
||||
|
||||
@@ -5,6 +5,7 @@ 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;
|
||||
|
||||
@@ -20,7 +21,11 @@ pub struct ProofOfDiscreteLog {
|
||||
}
|
||||
|
||||
impl ProofOfDiscreteLog {
|
||||
pub fn construct(mut rng: impl RngCore, public: &G1Projective, witness: &Scalar) -> Self {
|
||||
pub fn construct(
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
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);
|
||||
|
||||
@@ -9,6 +9,7 @@ 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;
|
||||
|
||||
@@ -87,7 +88,7 @@ pub struct ProofOfSecretSharing {
|
||||
|
||||
impl ProofOfSecretSharing {
|
||||
pub fn construct(
|
||||
mut rng: impl RngCore,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
instance: Instance,
|
||||
witness_r: &Scalar,
|
||||
witnesses_s: &[Share],
|
||||
@@ -309,13 +310,14 @@ 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,
|
||||
mut rng: impl RngCore + CryptoRng,
|
||||
) -> (
|
||||
BTreeMap<NodeIndex, PublicKey>,
|
||||
PublicCoefficients,
|
||||
|
||||
@@ -13,6 +13,7 @@ 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;
|
||||
@@ -94,7 +95,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,
|
||||
mut rng: impl RngCore + CryptoRng + CryptoRng,
|
||||
params: &Params,
|
||||
dealer_index: NodeIndex,
|
||||
threshold: Threshold,
|
||||
@@ -484,7 +485,7 @@ mod tests {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
derived_secrets.push(
|
||||
combine_shares(shares, &receivers.keys().copied().collect::<Vec<_>>()).unwrap(),
|
||||
@@ -593,7 +594,7 @@ mod tests {
|
||||
for (i, (dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret = combine_shares(shares, &dealer_indices).unwrap();
|
||||
|
||||
@@ -99,6 +99,12 @@ 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 {
|
||||
|
||||
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -120,7 +121,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, degree: u64) -> Self {
|
||||
pub fn new_random(mut rng: impl RngCore + CryptoRng + CryptoRng, degree: u64) -> Self {
|
||||
Polynomial {
|
||||
coefficients: (0..=degree).map(|_| Scalar::random(&mut rng)).collect(),
|
||||
}
|
||||
|
||||
@@ -53,11 +53,12 @@ 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(dk, i, &dealing.ciphertexts, None).unwrap();
|
||||
let _recovered = decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap();
|
||||
}
|
||||
|
||||
// and for good measure, check that the dealer's share matches decryption result
|
||||
let recovered_dealer = decrypt_share(&full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap();
|
||||
let recovered_dealer =
|
||||
decrypt_share(¶ms, &full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap();
|
||||
assert_eq!(recovered_dealer, dealer_share.unwrap());
|
||||
}
|
||||
|
||||
@@ -115,7 +116,7 @@ fn full_threshold_secret_sharing() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
// we know dealer_share matches, but it would be inconvenient to try to put them in here,
|
||||
@@ -189,7 +190,7 @@ fn full_threshold_secret_resharing() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = first_dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret =
|
||||
@@ -240,7 +241,7 @@ fn full_threshold_secret_resharing() {
|
||||
for (i, (ref dk, _)) in full_keys.iter().enumerate() {
|
||||
let shares = resharing_dealings
|
||||
.values()
|
||||
.map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret =
|
||||
@@ -305,7 +306,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(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret =
|
||||
@@ -369,7 +370,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(dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.map(|dealing| decrypt_share(¶ms, dk, i, &dealing.ciphertexts, None).unwrap())
|
||||
.collect();
|
||||
|
||||
let recovered_secret = combine_shares(shares, &node_indices).unwrap();
|
||||
|
||||
@@ -112,6 +112,10 @@ impl ServerResponse {
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_send(&self) -> bool {
|
||||
matches!(self, ServerResponse::Send { .. })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ServerResponse> for Message {
|
||||
|
||||
@@ -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().as_os_str()
|
||||
"Attempting to connect to database {}",
|
||||
database_path.as_ref().display()
|
||||
);
|
||||
|
||||
// TODO: we can inject here more stuff based on our gateway global config
|
||||
|
||||
@@ -82,8 +82,8 @@ impl GatewayStorage {
|
||||
message_retrieval_limit: i64,
|
||||
) -> Result<Self, GatewayStorageError> {
|
||||
debug!(
|
||||
"Attempting to connect to database {:?}",
|
||||
database_path.as_ref().as_os_str()
|
||||
"Attempting to connect to database {}",
|
||||
database_path.as_ref().display()
|
||||
);
|
||||
|
||||
// TODO: we can inject here more stuff based on our gateway global config
|
||||
|
||||
@@ -23,7 +23,6 @@ fn main() {
|
||||
"REWARDING_VALIDATOR_ADDRESS",
|
||||
"NYM_API",
|
||||
"NYXD_WS",
|
||||
"EXPLORER_API",
|
||||
"NYM_VPN_API",
|
||||
];
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#[cfg(feature = "network")]
|
||||
use crate::{DenomDetails, ValidatorDetails};
|
||||
use crate::{ApiUrlConst, DenomDetails, ValidatorDetails};
|
||||
|
||||
pub const NETWORK_NAME: &str = "mainnet";
|
||||
|
||||
@@ -17,6 +17,11 @@ 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 =
|
||||
@@ -29,10 +34,37 @@ 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 NYM_API: &str = "https://validator.nymtech.net/api/";
|
||||
pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket";
|
||||
pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/";
|
||||
|
||||
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 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 =
|
||||
@@ -123,7 +155,6 @@ 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);
|
||||
}
|
||||
@@ -165,6 +196,5 @@ 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);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ 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>,
|
||||
@@ -35,8 +37,38 @@ 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
|
||||
@@ -65,8 +97,9 @@ 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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +157,6 @@ 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))
|
||||
}
|
||||
|
||||
@@ -145,6 +177,9 @@ 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),
|
||||
@@ -152,8 +187,9 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -193,7 +229,6 @@ 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);
|
||||
}
|
||||
|
||||
@@ -297,12 +332,6 @@ 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);
|
||||
|
||||
@@ -22,7 +22,6 @@ 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";
|
||||
|
||||
@@ -56,6 +56,13 @@ impl ReplySurb {
|
||||
packet_size.plaintext_size() - ack_overhead - ReplySurbKeyDigestAlgorithm::output_size() - 1
|
||||
}
|
||||
|
||||
/// Construct a ResplySurb object. Selects mix hops for the surb unique to this
|
||||
/// individual construction.
|
||||
///
|
||||
/// If mix hops are disabled, the route will consistency of the recipient
|
||||
/// (i.e. the ingress hop) only. When `disable_mix_hops` is enabled
|
||||
/// `use_legacy_surb_format` is ignored as disabled mix hops requires use of
|
||||
/// the updated SURB format.
|
||||
// TODO: should this return `ReplySURBError` for consistency sake
|
||||
// or keep `NymTopologyError` because it's the only error it can actually return?
|
||||
pub fn construct<R>(
|
||||
@@ -64,17 +71,21 @@ impl ReplySurb {
|
||||
average_delay: Duration,
|
||||
use_legacy_surb_format: bool,
|
||||
topology: &NymRouteProvider,
|
||||
_disable_mix_hops: bool, // TODO: support SURBs with no mix hops after changes to surb format / construction
|
||||
disable_mix_hops: bool,
|
||||
) -> Result<Self, NymTopologyError>
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let route = topology.random_route_to_egress(rng, recipient.gateway())?;
|
||||
let route = if disable_mix_hops {
|
||||
topology.empty_route_to_egress(recipient.gateway())?
|
||||
} else {
|
||||
topology.random_route_to_egress(rng, recipient.gateway())?
|
||||
};
|
||||
let delays = nym_sphinx_routing::generate_hop_delays(average_delay, route.len());
|
||||
let destination = recipient.as_sphinx_destination();
|
||||
|
||||
let mut surb_material = SURBMaterial::new(route, delays, destination);
|
||||
if use_legacy_surb_format {
|
||||
if use_legacy_surb_format && !disable_mix_hops {
|
||||
surb_material = surb_material.with_version(X25519_WITH_EXPLICIT_PAYLOAD_KEYS_VERSION)
|
||||
}
|
||||
|
||||
|
||||
@@ -335,6 +335,9 @@ pub struct MessagePreparer<R> {
|
||||
/// will be routed as usual, to the entry gateway, through three mix nodes, egressing
|
||||
/// through the exit gateway. If mix hops are disabled, traffic will be routed directly
|
||||
/// from the entry gateway to the exit gateway, bypassing the mix nodes.
|
||||
///
|
||||
/// This overrides the `use_legacy_sphinx_format` setting as reduced/disabled mix hops
|
||||
/// requires use of the updated SURB packet format.
|
||||
pub disable_mix_hops: bool,
|
||||
}
|
||||
|
||||
@@ -388,7 +391,7 @@ where
|
||||
self.average_packet_delay,
|
||||
use_legacy_reply_surb_format,
|
||||
topology,
|
||||
disabled_mix_hops, // TODO: support SURBs with no mix hops after changes to surb format / construction
|
||||
disabled_mix_hops,
|
||||
)?
|
||||
.with_key_rotation(key_rotation);
|
||||
reply_surbs.push(reply_surb)
|
||||
|
||||
@@ -324,7 +324,13 @@ async fn persist_commits(
|
||||
} => (validator_address, timestamp, signature),
|
||||
};
|
||||
|
||||
let validator = crate::helpers::validator_info(*validator_id, validators)?;
|
||||
let validator = match crate::helpers::validator_info(*validator_id, validators) {
|
||||
Ok(validator_info) => validator_info,
|
||||
Err(err) => {
|
||||
error!("{err}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let validator_address = crate::helpers::validator_consensus_address(*validator_id)?;
|
||||
|
||||
if signature.is_none() {
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::{TaskClient, TaskManager};
|
||||
use futures::stream::FuturesUnordered;
|
||||
use futures::StreamExt;
|
||||
use std::future::Future;
|
||||
use std::mem;
|
||||
use std::ops::Deref;
|
||||
use std::pin::Pin;
|
||||
use std::time::Duration;
|
||||
@@ -185,12 +186,21 @@ impl ShutdownDropGuard {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ShutdownSignals(JoinSet<()>);
|
||||
|
||||
impl ShutdownSignals {
|
||||
pub async fn wait_for_signal(&mut self) {
|
||||
self.0.join_next().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ShutdownManager {
|
||||
pub root_token: ShutdownToken,
|
||||
|
||||
legacy_task_manager: Option<TaskManager>,
|
||||
|
||||
shutdown_signals: JoinSet<()>,
|
||||
shutdown_signals: ShutdownSignals,
|
||||
|
||||
// the reason I'm not using a `JoinSet` is because it forces us to use futures with the same `::Output` type
|
||||
tracker: TaskTracker,
|
||||
@@ -261,7 +271,7 @@ impl ShutdownManager {
|
||||
F: Send + 'static,
|
||||
{
|
||||
let shutdown_token = self.root_token.clone();
|
||||
self.shutdown_signals.spawn(async move {
|
||||
self.shutdown_signals.0.spawn(async move {
|
||||
shutdown.await;
|
||||
|
||||
info!("sending cancellation after receiving shutdown signal");
|
||||
@@ -356,9 +366,20 @@ impl ShutdownManager {
|
||||
wait_futures.next().await;
|
||||
}
|
||||
|
||||
pub async fn wait_for_shutdown_signal(mut self) {
|
||||
self.shutdown_signals.join_next().await;
|
||||
pub fn detach_shutdown_signals(&mut self) -> ShutdownSignals {
|
||||
mem::take(&mut self.shutdown_signals)
|
||||
}
|
||||
|
||||
pub fn replace_shutdown_signals(&mut self, signals: ShutdownSignals) {
|
||||
self.shutdown_signals = signals;
|
||||
}
|
||||
|
||||
// cancellation safe
|
||||
pub async fn wait_for_shutdown_signal(&mut self) {
|
||||
self.shutdown_signals.0.join_next().await;
|
||||
}
|
||||
|
||||
pub async fn perform_shutdown(mut self) {
|
||||
if let Some(legacy_manager) = self.legacy_task_manager.as_mut() {
|
||||
info!("attempting to shutdown legacy tasks");
|
||||
let _ = legacy_manager.signal_shutdown();
|
||||
@@ -367,4 +388,10 @@ impl ShutdownManager {
|
||||
info!("waiting for tasks to finish... (press ctrl-c to force)");
|
||||
self.finish_shutdown().await;
|
||||
}
|
||||
|
||||
pub async fn run_until_shutdown(mut self) {
|
||||
self.wait_for_shutdown_signal().await;
|
||||
|
||||
self.perform_shutdown().await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ where
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[track_caller]
|
||||
pub fn spawn<F>(future: F)
|
||||
where
|
||||
F: Future + Send + 'static,
|
||||
@@ -18,6 +19,7 @@ where
|
||||
tokio::spawn(future);
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn spawn_with_report_error<F, T, E>(future: F, mut shutdown: TaskClient)
|
||||
where
|
||||
F: Future<Output = Result<T, E>> + Send + 'static,
|
||||
|
||||
@@ -201,6 +201,9 @@ pub struct TrafficWasm {
|
||||
/// will be routed as usual, to the entry gateway, through three mix nodes, egressing
|
||||
/// through the exit gateway. If mix hops are disabled, traffic will be routed directly
|
||||
/// from the entry gateway to the exit gateway, bypassing the mix nodes.
|
||||
///
|
||||
/// This overrides the `use_legacy_sphinx_format` setting as reduced/disabeld mix hops
|
||||
/// requires use of the updated SURB packet format.
|
||||
pub disable_mix_hops: bool,
|
||||
}
|
||||
|
||||
|
||||
Generated
+50
-7
@@ -31,9 +31,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.97"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "ark-bls12-381"
|
||||
@@ -1133,6 +1133,19 @@ 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"
|
||||
@@ -1214,7 +1227,9 @@ 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",
|
||||
@@ -1238,7 +1253,6 @@ dependencies = [
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde-json-wasm",
|
||||
"serde_repr",
|
||||
"thiserror 2.0.12",
|
||||
"time",
|
||||
@@ -1276,6 +1290,38 @@ 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"
|
||||
@@ -1284,14 +1330,11 @@ 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]]
|
||||
|
||||
@@ -9,6 +9,7 @@ members = [
|
||||
"multisig/cw3-flex-multisig",
|
||||
"multisig/cw4-group",
|
||||
"vesting",
|
||||
"performance",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
@@ -64,4 +65,4 @@ dbg_macro = "deny"
|
||||
exit = "deny"
|
||||
panic = "deny"
|
||||
unimplemented = "deny"
|
||||
unreachable = "deny"
|
||||
unreachable = "deny"
|
||||
|
||||
@@ -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", 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" }
|
||||
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 }
|
||||
|
||||
cosmwasm-schema = { workspace = true, optional = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
|
||||
cw-controllers = { workspace = true }
|
||||
cw2 = { workspace = true }
|
||||
cw-storage-plus = { workspace = true }
|
||||
@@ -41,16 +41,22 @@ bs58 = { workspace = true }
|
||||
serde = { workspace = true, default-features = false, features = ["derive"] }
|
||||
semver = { workspace = true }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow.workspace = true
|
||||
rand_chacha = "0.3"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
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]
|
||||
|
||||
@@ -649,7 +649,7 @@ pub fn migrate(
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::rewards::storage as rewards_storage;
|
||||
use cosmwasm_std::testing::{message_info, mock_dependencies, mock_env};
|
||||
use cosmwasm_std::testing::{message_info, mock_env};
|
||||
use cosmwasm_std::{Decimal, Uint128};
|
||||
use mixnet_contract_common::reward_params::{
|
||||
IntervalRewardParams, RewardedSetParams, RewardingParams,
|
||||
@@ -657,6 +657,7 @@ mod tests {
|
||||
use mixnet_contract_common::{
|
||||
InitialRewardingParams, OperatingCostRange, Percent, ProfitMarginRange,
|
||||
};
|
||||
use nym_contracts_common_testing::mock_dependencies;
|
||||
use std::time::Duration;
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -22,3 +22,6 @@ mod support;
|
||||
#[cfg(feature = "contract-testing")]
|
||||
mod testing;
|
||||
mod vesting_migration;
|
||||
|
||||
#[cfg(feature = "testable-mixnet-contract")]
|
||||
pub mod testable_mixnet_contract;
|
||||
|
||||
@@ -130,20 +130,22 @@ pub mod tests {
|
||||
use crate::mixnet_contract_settings::queries::query_rewarding_validator_address;
|
||||
use crate::mixnet_contract_settings::storage::rewarding_denom;
|
||||
use crate::support::tests::test_helpers;
|
||||
use cosmwasm_std::testing::{message_info, MockApi};
|
||||
use cosmwasm_std::testing::message_info;
|
||||
use cosmwasm_std::{Coin, Uint128};
|
||||
use cw_controllers::AdminError::NotAdmin;
|
||||
use mixnet_contract_common::OperatorsParamsUpdate;
|
||||
use nym_contracts_common_testing::mock_api;
|
||||
|
||||
#[test]
|
||||
fn update_contract_rewarding_validator_address() {
|
||||
let mut deps = test_helpers::init_contract();
|
||||
let mock_api = mock_api();
|
||||
|
||||
let info = message_info(&deps.api.addr_make("not-the-creator"), &[]);
|
||||
let res = try_update_rewarding_validator_address(
|
||||
deps.as_mut(),
|
||||
info,
|
||||
MockApi::default().addr_make("not-the-creator").to_string(),
|
||||
mock_api.addr_make("not-the-creator").to_string(),
|
||||
);
|
||||
assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {})));
|
||||
|
||||
@@ -151,14 +153,14 @@ pub mod tests {
|
||||
let res = try_update_rewarding_validator_address(
|
||||
deps.as_mut(),
|
||||
info,
|
||||
MockApi::default().addr_make("new-good-address").to_string(),
|
||||
mock_api.addr_make("new-good-address").to_string(),
|
||||
);
|
||||
assert_eq!(
|
||||
res,
|
||||
Ok(
|
||||
Response::default().add_event(new_rewarding_validator_address_update_event(
|
||||
MockApi::default().addr_make("rewarder"),
|
||||
MockApi::default().addr_make("new-good-address")
|
||||
mock_api.addr_make("rewarder"),
|
||||
mock_api.addr_make("new-good-address")
|
||||
))
|
||||
)
|
||||
);
|
||||
@@ -166,7 +168,7 @@ pub mod tests {
|
||||
let state = storage::CONTRACT_STATE.load(&deps.storage).unwrap();
|
||||
assert_eq!(
|
||||
state.rewarding_validator_address,
|
||||
MockApi::default().addr_make("new-good-address")
|
||||
mock_api.addr_make("new-good-address")
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -51,11 +51,11 @@ pub mod test_helpers {
|
||||
use crate::support::helpers::ensure_no_existing_bond;
|
||||
use crate::support::tests;
|
||||
use crate::support::tests::fixtures::{
|
||||
good_gateway_pledge, good_mixnode_pledge, good_node_plegge, TEST_COIN_DENOM,
|
||||
good_gateway_pledge, good_mixnode_pledge, good_node_plegge,
|
||||
};
|
||||
use crate::support::tests::{legacy, test_helpers};
|
||||
use crate::testable_mixnet_contract::MixnetContract;
|
||||
use cosmwasm_std::testing::message_info;
|
||||
use cosmwasm_std::testing::mock_dependencies;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use cosmwasm_std::testing::MockApi;
|
||||
use cosmwasm_std::testing::MockQuerier;
|
||||
@@ -74,22 +74,24 @@ pub mod test_helpers {
|
||||
use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role};
|
||||
use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData};
|
||||
use mixnet_contract_common::reward_params::{
|
||||
NodeRewardingParameters, Performance, RewardedSetParams, RewardingParams, WorkFactor,
|
||||
NodeRewardingParameters, Performance, RewardingParams, WorkFactor,
|
||||
};
|
||||
use mixnet_contract_common::rewarding::simulator::simulated_node::SimulatedNode;
|
||||
use mixnet_contract_common::rewarding::simulator::Simulator;
|
||||
use mixnet_contract_common::rewarding::RewardDistribution;
|
||||
use mixnet_contract_common::{
|
||||
ContractStateParamsUpdate, Delegation, EpochEventId, EpochState, EpochStatus, ExecuteMsg,
|
||||
Gateway, GatewayBondingPayload, IdentityKey, InitialRewardingParams, InstantiateMsg,
|
||||
Interval, MixNode, MixNodeBond, MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode,
|
||||
NymNodeBond, NymNodeBondingPayload, NymNodeDetails, OperatingCostRange,
|
||||
OperatorsParamsUpdate, Percent, ProfitMarginRange, RoleAssignment,
|
||||
SignableGatewayBondingMsg, SignableMixNodeBondingMsg, SignableNymNodeBondingMsg,
|
||||
Gateway, GatewayBondingPayload, IdentityKey, Interval, MixNode, MixNodeBond,
|
||||
MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode, NymNodeBond, NymNodeBondingPayload,
|
||||
NymNodeDetails, OperatingCostRange, OperatorsParamsUpdate, ProfitMarginRange,
|
||||
RoleAssignment, SignableGatewayBondingMsg, SignableMixNodeBondingMsg,
|
||||
SignableNymNodeBondingMsg,
|
||||
};
|
||||
use nym_contracts_common::signing::{
|
||||
ContractMessageContent, MessageSignature, SignableMessage, SigningAlgorithm, SigningPurpose,
|
||||
};
|
||||
use nym_contracts_common_testing::TestableNymContract;
|
||||
use nym_contracts_common_testing::{mock_api, mock_dependencies};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_crypto::asymmetric::ed25519::KeyPair;
|
||||
use rand::distributions::WeightedIndex;
|
||||
@@ -100,13 +102,12 @@ pub mod test_helpers {
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Debug;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) fn 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(MockApi::default().addr_make(&format!("addr{i}{}", rng.next_u64())));
|
||||
addrs.push(mock_api().addr_make(&format!("addr{i}{}", rng.next_u64())));
|
||||
}
|
||||
addrs.sort();
|
||||
addrs
|
||||
@@ -1820,46 +1821,9 @@ pub mod test_helpers {
|
||||
SignableGatewayBondingMsg::new(nonce, content)
|
||||
}
|
||||
|
||||
fn intial_rewarded_set_params() -> RewardedSetParams {
|
||||
RewardedSetParams {
|
||||
entry_gateways: 50,
|
||||
exit_gateways: 70,
|
||||
mixnodes: 120,
|
||||
standby: 50,
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_rewarding_params() -> InitialRewardingParams {
|
||||
let reward_pool = 250_000_000_000_000u128;
|
||||
let staking_supply = 100_000_000_000_000u128;
|
||||
|
||||
InitialRewardingParams {
|
||||
initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens)
|
||||
initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
|
||||
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
|
||||
interval_pool_emission: Percent::from_percentage_value(2).unwrap(),
|
||||
rewarded_set_params: intial_rewarded_set_params(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init_contract() -> OwnedDeps<MemoryStorage, MockApi, MockQuerier<Empty>> {
|
||||
let mut deps = mock_dependencies();
|
||||
let msg = InstantiateMsg {
|
||||
rewarding_validator_address: deps.api.addr_make("rewarder").to_string(),
|
||||
vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(),
|
||||
rewarding_denom: TEST_COIN_DENOM.to_string(),
|
||||
epochs_in_interval: 720,
|
||||
epoch_duration: Duration::from_secs(60 * 60),
|
||||
initial_rewarding_params: initial_rewarding_params(),
|
||||
current_nym_node_version: "1.1.10".to_string(),
|
||||
version_score_weights: Default::default(),
|
||||
version_score_params: Default::default(),
|
||||
profit_margin: Default::default(),
|
||||
interval_operating_cost: Default::default(),
|
||||
key_validity_in_epochs: None,
|
||||
};
|
||||
let msg = MixnetContract::base_init_msg();
|
||||
let env = mock_env();
|
||||
let info = sender("creator");
|
||||
instantiate(deps.as_mut(), env, info, msg).unwrap();
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// fine in test code
|
||||
#![allow(clippy::unwrap_used)]
|
||||
|
||||
use crate::contract::{execute, instantiate, migrate, query};
|
||||
use cosmwasm_std::Decimal;
|
||||
use mixnet_contract_common::error::MixnetContractError;
|
||||
use mixnet_contract_common::reward_params::RewardedSetParams;
|
||||
use mixnet_contract_common::{
|
||||
ExecuteMsg, InitialRewardingParams, InstantiateMsg, MigrateMsg, QueryMsg,
|
||||
};
|
||||
use nym_contracts_common::Percent;
|
||||
use nym_contracts_common_testing::{
|
||||
mock_dependencies, ContractFn, PermissionedFn, QueryFn, TEST_DENOM,
|
||||
};
|
||||
use std::time::Duration;
|
||||
|
||||
pub use nym_contracts_common_testing::TestableNymContract;
|
||||
|
||||
pub struct MixnetContract;
|
||||
|
||||
fn initial_rewarded_set_params() -> RewardedSetParams {
|
||||
RewardedSetParams {
|
||||
entry_gateways: 50,
|
||||
exit_gateways: 70,
|
||||
mixnodes: 120,
|
||||
standby: 50,
|
||||
}
|
||||
}
|
||||
|
||||
fn initial_rewarding_params() -> InitialRewardingParams {
|
||||
let reward_pool = 250_000_000_000_000u128;
|
||||
let staking_supply = 100_000_000_000_000u128;
|
||||
|
||||
InitialRewardingParams {
|
||||
initial_reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens)
|
||||
initial_staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
|
||||
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
|
||||
interval_pool_emission: Percent::from_percentage_value(2).unwrap(),
|
||||
rewarded_set_params: initial_rewarded_set_params(),
|
||||
}
|
||||
}
|
||||
|
||||
impl TestableNymContract for MixnetContract {
|
||||
const NAME: &'static str = "mixnet-contract";
|
||||
type InitMsg = InstantiateMsg;
|
||||
type ExecuteMsg = ExecuteMsg;
|
||||
type QueryMsg = QueryMsg;
|
||||
type MigrateMsg = MigrateMsg;
|
||||
type ContractError = MixnetContractError;
|
||||
|
||||
fn instantiate() -> ContractFn<Self::InitMsg, Self::ContractError> {
|
||||
instantiate
|
||||
}
|
||||
|
||||
fn execute() -> ContractFn<Self::ExecuteMsg, Self::ContractError> {
|
||||
execute
|
||||
}
|
||||
|
||||
fn query() -> QueryFn<Self::QueryMsg, Self::ContractError> {
|
||||
query
|
||||
}
|
||||
|
||||
fn migrate() -> PermissionedFn<Self::MigrateMsg, Self::ContractError> {
|
||||
migrate
|
||||
}
|
||||
|
||||
fn base_init_msg() -> Self::InitMsg {
|
||||
let deps = mock_dependencies();
|
||||
InstantiateMsg {
|
||||
rewarding_validator_address: deps.api.addr_make("rewarder").to_string(),
|
||||
vesting_contract_address: deps.api.addr_make("vesting-contract").to_string(),
|
||||
rewarding_denom: TEST_DENOM.to_string(),
|
||||
epochs_in_interval: 720,
|
||||
epoch_duration: Duration::from_secs(60 * 60),
|
||||
initial_rewarding_params: initial_rewarding_params(),
|
||||
current_nym_node_version: "1.1.10".to_string(),
|
||||
version_score_weights: Default::default(),
|
||||
version_score_params: Default::default(),
|
||||
profit_margin: Default::default(),
|
||||
interval_operating_cost: Default::default(),
|
||||
key_validity_in_epochs: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,13 +25,9 @@ cosmwasm-schema = { workspace = true, optional = true }
|
||||
nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" }
|
||||
nym-pool-contract-common = { path = "../../common/cosmwasm-smart-contracts/nym-pool-contract" }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
rand_chacha = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
cw-multi-test = { workspace = true }
|
||||
nym-contracts-common-testing = { path = "../../common/cosmwasm-smart-contracts/contracts-common-testing" }
|
||||
|
||||
[features]
|
||||
schema-gen = ["nym-pool-contract-common/schema", "cosmwasm-schema"]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user