diff --git a/.github/workflows/cd-docs.yml b/.github/workflows/cd-docs.yml index c3b4f6a7b2..97150e29c8 100644 --- a/.github/workflows/cd-docs.yml +++ b/.github/workflows/cd-docs.yml @@ -5,7 +5,7 @@ on: jobs: build: - runs-on: ubuntu-20.04-16-core + runs-on: arc-ubuntu-20.04 steps: - uses: actions/checkout@v4 - name: Install Dependencies (Linux) @@ -99,24 +99,3 @@ jobs: run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} working-directory: dist/docs continue-on-error: false - - - name: Matrix - Node Install - run: npm install - working-directory: .github/workflows/support-files - - name: Matrix - Send Notification - env: - NYM_NOTIFICATION_KIND: cd-docs - NYM_PROJECT_NAME: "Docs CD" - NYM_CI_WWW_BASE: "${{ secrets.NYM_CD_WWW_BASE }}" - NYM_CI_WWW_LOCATION: "${{ env.GITHUB_REF_SLUG }}" - GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" - GIT_BRANCH: "${GITHUB_REF##*/}" - MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}" - MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_DOCS }}" - MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}" - MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}" - MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}" - IS_SUCCESS: "${{ job.status == 'success' }}" - uses: docker://keybaseio/client:stable-node - with: - args: .github/workflows/support-files/notifications/entry_point.sh diff --git a/.github/workflows/ci-build-ts.yml b/.github/workflows/ci-build-ts.yml index 4c8dbe6b7c..934f855c5f 100644 --- a/.github/workflows/ci-build-ts.yml +++ b/.github/workflows/ci-build-ts.yml @@ -1,6 +1,7 @@ name: ci-build-ts on: + workflow_dispatch: pull_request: paths: - "ts-packages/**" @@ -9,7 +10,7 @@ on: jobs: build: - runs-on: ubuntu-20.04-16-core + runs-on: arc-ubuntu-20.04 steps: - uses: actions/checkout@v4 - name: Install rsync @@ -45,23 +46,3 @@ jobs: REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }} TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/ts-${{ env.GITHUB_REF_SLUG }}-example EXCLUDE: "/dist/, /node_modules/" - - name: Matrix - Node Install - run: npm install - working-directory: .github/workflows/support-files - - name: Matrix - Send Notification - env: - NYM_NOTIFICATION_KIND: ts-packages - NYM_PROJECT_NAME: "ts-packages" - NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}" - NYM_CI_WWW_LOCATION: "ts-${{ env.GITHUB_REF_SLUG }}" - GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" - GIT_BRANCH: "${GITHUB_REF##*/}" - IS_SUCCESS: "${{ job.status == 'success' }}" - MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}" - MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}" - MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}" - MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}" - MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}" - uses: docker://keybaseio/client:stable-node - with: - args: .github/workflows/support-files/notifications/entry_point.sh diff --git a/.github/workflows/ci-build-vpn-api-wasm.yml b/.github/workflows/ci-build-vpn-api-wasm.yml new file mode 100644 index 0000000000..a4abd8fa3e --- /dev/null +++ b/.github/workflows/ci-build-vpn-api-wasm.yml @@ -0,0 +1,41 @@ +name: ci-build-vpn-api-wasm + +on: + pull_request: + paths: + - 'common/**' + - 'nym-credential-proxy/**' + - '.github/workflows/ci-build-vpn-api-wasm.yml' + +jobs: + wasm: + runs-on: arc-ubuntu-22.04 + env: + CARGO_TERM_COLOR: always + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + target: wasm32-unknown-unknown + override: true + components: rustfmt, clippy + + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + - name: Install wasm-opt + uses: ./.github/actions/install-wasm-opt + with: + version: '116' + + - name: Install wasm-bindgen-cli + run: cargo install wasm-bindgen-cli + + - name: "Build" + run: make + working-directory: nym-credential-proxy/vpn-api-lib-wasm diff --git a/.github/workflows/ci-build.yml b/.github/workflows/ci-build.yml index a68cc72562..d7a76f2673 100644 --- a/.github/workflows/ci-build.yml +++ b/.github/workflows/ci-build.yml @@ -30,7 +30,7 @@ jobs: strategy: fail-fast: false matrix: - os: [arc-ubuntu-20.04, custom-runner-mac-m1] + os: [ arc-ubuntu-20.04, custom-runner-mac-m1 ] runs-on: ${{ matrix.os }} env: CARGO_TERM_COLOR: always @@ -57,18 +57,16 @@ jobs: command: fmt args: --all -- --check - - name: Build all binaries + - name: Clippy uses: actions-rs/cargo@v1 with: - command: build + command: clippy + args: --workspace --all-targets -- -D warnings - # while disabled by default, this build ensures nothing is broken within - # `axum` feature - - name: Build with `axum` feature + - name: Build all binaries uses: actions-rs/cargo@v1 with: command: build - args: --features axum - name: Build all examples if: contains(matrix.os, 'ubuntu') @@ -90,9 +88,3 @@ jobs: with: command: test args: --workspace -- --ignored - - - name: Clippy - uses: actions-rs/cargo@v1 - with: - command: clippy - args: --workspace --all-targets --features axum -- -D warnings diff --git a/.github/workflows/ci-contracts-upload-binaries.yml b/.github/workflows/ci-contracts-upload-binaries.yml index 4873d46885..9cb7116dca 100644 --- a/.github/workflows/ci-contracts-upload-binaries.yml +++ b/.github/workflows/ci-contracts-upload-binaries.yml @@ -16,7 +16,7 @@ jobs: strategy: fail-fast: false matrix: - platform: arc-ubuntu-20.04 + platform: [ arc-ubuntu-20.04 ] runs-on: ${{ matrix.platform }} env: diff --git a/.github/workflows/ci-docs.yml b/.github/workflows/ci-docs.yml index 74b599d26d..602df75b44 100644 --- a/.github/workflows/ci-docs.yml +++ b/.github/workflows/ci-docs.yml @@ -10,7 +10,7 @@ on: jobs: build: - runs-on: ubuntu-20.04-16-core + runs-on: arc-ubuntu-20.04 steps: - uses: actions/checkout@v4 - name: Install Dependencies (Linux) @@ -70,24 +70,3 @@ jobs: REMOTE_USER: ${{ secrets.CI_WWW_REMOTE_USER }} TARGET: ${{ secrets.CI_WWW_REMOTE_TARGET }}/docs-${{ env.GITHUB_REF_SLUG }} EXCLUDE: "/node_modules/" - - - name: Matrix - Node Install - run: npm install - working-directory: .github/workflows/support-files - - name: Matrix - Send Notification - env: - NYM_NOTIFICATION_KIND: ci-docs - NYM_PROJECT_NAME: "Docs CI" - NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}" - NYM_CI_WWW_LOCATION: "docs-${{ env.GITHUB_REF_SLUG }}" - GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" - GIT_BRANCH: "${GITHUB_REF##*/}" - MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}" - MATRIX_ROOM: "${{ secrets.MATRIX_ROOM_DOCS }}" - MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}" - MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}" - MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}" - IS_SUCCESS: "${{ job.status == 'success' }}" - uses: docker://keybaseio/client:stable-node - with: - args: .github/workflows/support-files/notifications/entry_point.sh diff --git a/.github/workflows/ci-lint-typescript.yml b/.github/workflows/ci-lint-typescript.yml index 165446bbd8..0f3aa78fa0 100644 --- a/.github/workflows/ci-lint-typescript.yml +++ b/.github/workflows/ci-lint-typescript.yml @@ -1,6 +1,7 @@ name: ci-lint-typescript on: + workflow_dispatch: pull_request: paths: - "ts-packages/**" @@ -14,7 +15,7 @@ on: jobs: build: - runs-on: ubuntu-20.04-16-core + runs-on: arc-ubuntu-20.04 steps: - uses: actions/checkout@v4 - uses: rlespinasse/github-slug-action@v3.x @@ -53,24 +54,3 @@ jobs: run: yarn lint - name: Typecheck with tsc run: yarn tsc - - - name: Matrix - Node Install - run: npm install - working-directory: .github/workflows/support-files - - name: Matrix - Send Notification - env: - NYM_NOTIFICATION_KIND: ts-packages - NYM_PROJECT_NAME: "ts-packages" - NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}" - NYM_CI_WWW_LOCATION: "ts-${{ env.GITHUB_REF_SLUG }}" - GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" - GIT_BRANCH: "${GITHUB_REF##*/}" - IS_SUCCESS: "${{ job.status == 'success' }}" - MATRIX_SERVER: "${{ secrets.MATRIX_SERVER }}" - MATRIX_ROOM: "${{ secrets.MATRIX_ROOM }}" - MATRIX_USER_ID: "${{ secrets.MATRIX_USER_ID }}" - MATRIX_TOKEN: "${{ secrets.MATRIX_TOKEN }}" - MATRIX_DEVICE_ID: "${{ secrets.MATRIX_DEVICE_ID }}" - uses: docker://keybaseio/client:stable-node - with: - args: .github/workflows/support-files/notifications/entry_point.sh diff --git a/.github/workflows/ci-nym-credential-proxy.yml b/.github/workflows/ci-nym-credential-proxy.yml new file mode 100644 index 0000000000..8b70a1a73c --- /dev/null +++ b/.github/workflows/ci-nym-credential-proxy.yml @@ -0,0 +1,45 @@ +name: ci-nym-credential-proxy + +on: + pull_request: + paths: + - 'common/**' + - 'nym-credential-proxy/**' + - '.github/workspace/ci-nym-credential-proxy.yml' + workflow_dispatch: + +jobs: + build: + runs-on: arc-ubuntu-22.04 + env: + CARGO_TERM_COLOR: always + MANIFEST_PATH: "--manifest-path nym-credential-proxy/Cargo.toml" + steps: + - name: Check out repository code + uses: actions/checkout@v4 + + - name: Install rust toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + components: rustfmt, clippy + + - name: Check formatting + uses: actions-rs/cargo@v1 + with: + command: fmt + args: ${{ env.MANIFEST_PATH }} --all -- --check + + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + args: ${{ env.MANIFEST_PATH }} --workspace --all-targets + + - name: Clippy + uses: actions-rs/cargo@v1 + with: + command: clippy + args: ${{ env.MANIFEST_PATH }} --workspace --all-targets -- -D warnings diff --git a/.github/workflows/hello-world.yaml b/.github/workflows/hello-world.yaml new file mode 100644 index 0000000000..68a8ca8752 --- /dev/null +++ b/.github/workflows/hello-world.yaml @@ -0,0 +1,11 @@ +name: Hello world + +on: + workflow_dispatch: + +jobs: + my-job: + runs-on: arc-ubuntu-22.04 + steps: + - name: my-step + run: echo "Hello World!" diff --git a/.github/workflows/publish-nym-binaries.yml b/.github/workflows/publish-nym-binaries.yml index e64825e425..b28f118ec7 100644 --- a/.github/workflows/publish-nym-binaries.yml +++ b/.github/workflows/publish-nym-binaries.yml @@ -55,6 +55,7 @@ jobs: uses: actions-rs/toolchain@v1 with: toolchain: stable + override: true - name: Build all binaries uses: actions-rs/cargo@v1 diff --git a/.github/workflows/publish-nym-contracts.yml b/.github/workflows/publish-nym-contracts.yml index 85f1363b9b..ed1141ddf1 100644 --- a/.github/workflows/publish-nym-contracts.yml +++ b/.github/workflows/publish-nym-contracts.yml @@ -14,13 +14,14 @@ jobs: - name: Install Rust stable uses: actions-rs/toolchain@v1 with: - toolchain: stable + toolchain: 1.77 target: wasm32-unknown-unknown override: true - components: rustfmt, clippy - name: Install wasm-opt - run: cargo install --version 0.114.0 wasm-opt + uses: ./.github/actions/install-wasm-opt + with: + version: '114' - name: Build release contracts run: make contracts diff --git a/.github/workflows/publish-sdk-npm.yml b/.github/workflows/publish-sdk-npm.yml index 583006b638..405cbcd022 100644 --- a/.github/workflows/publish-sdk-npm.yml +++ b/.github/workflows/publish-sdk-npm.yml @@ -4,7 +4,7 @@ on: jobs: publish: - runs-on: ubuntu-20.04-16-core + runs-on: arc-ubuntu-20.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/push-credential-proxy.yaml b/.github/workflows/push-credential-proxy.yaml new file mode 100644 index 0000000000..9576dc61d3 --- /dev/null +++ b/.github/workflows/push-credential-proxy.yaml @@ -0,0 +1,55 @@ +name: Build and upload Credential Proxy container to harbor.nymte.ch +on: + workflow_dispatch: + +env: + WORKING_DIRECTORY: "nym-credential-proxy" + CONTAINER_NAME: "credential-proxy" + +jobs: + build-container: + runs-on: arc-ubuntu-22.04-dind + steps: + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: harbor.nymte.ch + username: ${{ secrets.HARBOR_ROBOT_USERNAME }} + password: ${{ secrets.HARBOR_ROBOT_SECRET }} + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure git identity + run: | + git config --global user.email "lawrence@nymtech.net" + git config --global user.name "Lawrence Stalder" + + - name: Get version from cargo.toml + uses: mikefarah/yq@v4.44.3 + id: get_version + with: + cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Cargo.toml + + - name: Check if tag exists + run: | + if git rev-parse ${{ steps.get_version.outputs.value }} >/dev/null 2>&1; then + echo "Tag ${{ steps.get_version.outputs.value }} already exists" + fi + + - 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: Create tag + run: | + git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}" + git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + + - name: BuildAndPushImageOnHarbor + run: | + docker build -f ${{ env.WORKING_DIRECTORY }}/nym-credential-proxy/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest + docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags diff --git a/.github/workflows/push-node-status-agent.yaml b/.github/workflows/push-node-status-agent.yaml new file mode 100644 index 0000000000..fb25f9dac0 --- /dev/null +++ b/.github/workflows/push-node-status-agent.yaml @@ -0,0 +1,61 @@ +name: Build and upload Node Status agent container to harbor.nymte.ch + +on: + workflow_dispatch: + inputs: + gateway_probe_git_ref: + type: string + description: Which gateway probe git ref to build the image with + +env: + WORKING_DIRECTORY: "nym-node-status-agent" + CONTAINER_NAME: "node-status-agent" + +jobs: + build-container: + runs-on: arc-ubuntu-22.04-dind + steps: + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: harbor.nymte.ch + username: ${{ secrets.HARBOR_ROBOT_USERNAME }} + password: ${{ secrets.HARBOR_ROBOT_SECRET }} + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure git identity + run: | + git config --global user.email "lawrence@nymtech.net" + git config --global user.name "Lawrence Stalder" + + - name: Get version from cargo.toml + uses: mikefarah/yq@v4.44.3 + id: get_version + with: + cmd: yq -oy '.package.version' ${{ env.WORKING_DIRECTORY }}/Cargo.toml + + - name: cleanup-gateway-probe-ref + id: cleanup_gateway_probe_ref + run: | + GATEWAY_PROBE_GIT_REF=${{ github.event.inputs.gateway_probe_git_ref }} + 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: 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: 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 push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags \ No newline at end of file diff --git a/.github/workflows/push-node-status-api.yaml b/.github/workflows/push-node-status-api.yaml new file mode 100644 index 0000000000..941a8619f0 --- /dev/null +++ b/.github/workflows/push-node-status-api.yaml @@ -0,0 +1,55 @@ +name: Build and upload Node Status API container to harbor.nymte.ch +on: + workflow_dispatch: + +env: + WORKING_DIRECTORY: "nym-node-status-api" + CONTAINER_NAME: "node-status-api" + +jobs: + build-container: + runs-on: arc-ubuntu-22.04-dind + steps: + - name: Login to Harbor + uses: docker/login-action@v3 + with: + registry: harbor.nymte.ch + username: ${{ secrets.HARBOR_ROBOT_USERNAME }} + password: ${{ secrets.HARBOR_ROBOT_SECRET }} + + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Configure git identity + run: | + git config --global user.email "lawrence@nymtech.net" + git config --global user.name "Lawrence Stalder" + + - name: Get version from cargo.toml + uses: mikefarah/yq@v4.44.3 + id: get_version + 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: 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: Create tag + run: | + git tag -a ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} -m "Version ${{ steps.get_version.outputs.result }}" + git push origin ${{ env.WORKING_DIRECTORY }}-${{ steps.get_version.outputs.result }} + + - name: BuildAndPushImageOnHarbor + run: | + docker build -f ${{ env.WORKING_DIRECTORY }}/Dockerfile . -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.result }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest + docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fdec67361..46cb35531e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,204 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ## [Unreleased] +## [2024.13-magura-patched] (2024-11-22) + +- [experimental] allow clients to change between deterministic route selection based on packet headers and a pseudorandom distribution +- Introduced a configurable limit on retransmission frequency of packets if ACKs are not received +- Filtered out invalid IP addresses on nym-api + +## [2024.13-magura] (2024-11-18) + +- Limit race probability ([#5145]) +- bugifx: assign 'node_id' when converting from 'GatewayDetails' to 'TestNode' ([#5143]) +- bugfix: make sure to assign correct node_id and identity during 'gateway_details' table migration ([#5142]) +- Respond to auth messages with same version ([#5140]) +- Pain/polyfill deprecated endpoints ([#5131]) +- change: dont select mixnodes bonded with vested tokens into the rewarded set ([#5129]) +- nym-credential-proxy-requests: reqwest use rustls-tls ([#5116]) +- bugfix: preserve as much as possible of the rewarded set during migration ([#5103]) +- Feature/force refresh node ([#5101]) +- Add NYM_VPN_API to env files ([#5099]) +- bugfix: fixed historical uptimes for nodes ([#5097]) +- Remove old use of 1GB constant ([#5096]) +- Graceful agent 1.1.5 ([#5093]) +- Add more translations from v2 to v3 authenticator ([#5091]) +- Nym node - Fix claim delegator rewards ([#5090]) +- Make 250 GB/30 days for free ride mode ([#5083]) +- Don't increase bandwidth two times ([#5081]) +- Fix expiration date as today + 7 days ([#5076]) +- Fix gateway decreasing bandwidth ([#5075]) +- Allow custom http port to be reset ([#5073]) +- bugfix: additional checks inside credential proxy ([#5072]) +- chore: deprecated old nym-api client methods and replaced them when possible ([#5069]) +- NS API with directory v2 (#5058) ([#5068]) +- bugfix: credential-proxy obtain-async ([#5067]) +- Allow nym node config updates ([#5066]) +- bugfix: use corrext axum extractors for ecash route arguments ([#5065]) +- Merge2/release/2024.13 magura ([#5063]) +- bugfix/feature: added NymApiClient method to get all skimmed nodes ([#5062]) +- Merge1/release/2024.13 magura ([#5061]) +- added hacky routes to return nymnodes alongside legacy nodes ([#5051]) +- bugfix: mark migrated gateways as rewarded in the previous epoch in case theyre in the rewarded set ([#5049]) +- bugfix: adjust runtime storage migration ([#5047]) +- bugfix: supersede 'cb13be27f8f61d9ae74d924e85d2e6787895eb14' by using… ([#5046]) +- bugfix: restore default http port for nym-api ([#5045]) +- bugfix: fix ecash handlers routes ([#5043]) +- bugfix: don't assign exit gateways to standby set ([#5041]) +- bugfix: make sure nym-nodes are also tested by network monitor ([#5040]) +- bugfix: use bonded nym-nodes for determining initial network monitor … ([#5039]) +- bugfix: make gateways insert themselves into [local] topology ([#5038]) +- Pass poisson flag ([#5037]) +- bugfix: use human readable roles for annotations ([#5036]) +- bugfix: use old name for 'epoch_role' in SkimmedNode ([#5034]) +- bugfix: make sure to use correct highest node id when assigning role ([#5032]) +- feature: use axum_client_ip for attempting to extract source ip ([#5031]) +- bugfix: fixed backwards incompatibility for /gateways/described endpoint ([#5030]) +- bugfix: verifying signed information of legacy nodes ([#5029]) +- bugfix: introduce 'LegacyPendingMixNodeChanges' that does not contain 'cost_params_change' ([#5028]) +- bugfix: missing #[serde(default)] for announce port ([#5024]) +- bugfix: directory v2.1 `get_all_avg_gateway_reliability_in_interval` query ([#5023]) +- added 'get_all_described_nodes' to NymApiClient and adjusted return t… ([#5016]) +- Reapply fixes to new branch ([#5014]) +- Consume only positive bandwidth ([#5013]) +- feature: adjusted ticket sizes to the agreed amounts ([#5009]) +- Push private ip before inserting ([#5008]) +- chore: update itertools in compact ecash ([#4994]) +- feature: make accepting t&c a hard requirement for rewarded set selection ([#4993]) +- Fix rustfmt in nym-credential-proxy ([#4992]) +- bugfix: client memory leak ([#4991]) +- Eliminate 0 bandwidth race check ([#4988]) +- [DOCs;/operators]: Release notes for v2024.12 aero ([#4984]) +- Add topup req constructor ([#4983]) +- Fix critical issues SI86 and SI87 from Cure53 ([#4982]) +- Rename nym-vpn-api to nym-credential-proxy ([#4981]) +- enable global ecash routes even if api is not a signer ([#4980]) +- resolve beta clippy issues in contracts ([#4978]) +- Re-enable vested delegation migration ([#4977]) +- feature: require reporting using nym-node binary for rewarded set selection ([#4976]) +- Top up bandwidth ([#4975]) +- [Product Data] Add session type based on ecash ticket received ([#4974]) +- Bugfix/additional directory fixes ([#4973]) +- feat: add Dockerfile for nym node ([#4972]) +- chore: remove unused rocket code ([#4968]) +- Import nym-vpn-api crates ([#4967]) +- feature: importer-cli to correctly handle mixnet/vesting import ([#4966]) +- bugfix: fix expected return type on /v1/gateways endpoint ([#4965]) +- [Product Data] First step in gateway usage data collection ([#4963]) +- Bump sqlx to 0.7.4 ([#4959]) +- Add env feature to clap and make clap parameters available as env variables ([#4957]) +- Feature/contract state tools ([#4954]) +- expose authenticator address along other address in node-details ([#4953]) +- Extract packet processing from mixnode-common ([#4949]) +- nym-api container ([#4948]) +- Ticket type storage ([#4947]) +- Add "utoipa" feature to nym-node ([#4945]) +- build(deps): bump the patch-updates group across 1 directory with 9 updates ([#4944]) +- V2 performance monitoring feature flag ([#4943]) +- Bugfix/rewarder post pruning adjustments ([#4942]) +- Switch over the last set of jobs to arc runners ([#4938]) +- Fix broken build after merge ([#4937]) +- bugfix: correctly paginate through 'search_tx' endpoint ([#4936]) +- Add more conversions for responses of authenticator messages ([#4929]) +- Directory Sevices v2.1 ([#4903]) +- Migrate Legacy Node (Frontend) ([#4826]) +- Fix critical issues SI84 and SI85 from Cure53 ([#4758]) + +[#5145]: https://github.com/nymtech/nym/pull/5145 +[#5143]: https://github.com/nymtech/nym/pull/5143 +[#5142]: https://github.com/nymtech/nym/pull/5142 +[#5140]: https://github.com/nymtech/nym/pull/5140 +[#5131]: https://github.com/nymtech/nym/pull/5131 +[#5129]: https://github.com/nymtech/nym/pull/5129 +[#5116]: https://github.com/nymtech/nym/pull/5116 +[#5103]: https://github.com/nymtech/nym/pull/5103 +[#5101]: https://github.com/nymtech/nym/pull/5101 +[#5099]: https://github.com/nymtech/nym/pull/5099 +[#5097]: https://github.com/nymtech/nym/pull/5097 +[#5096]: https://github.com/nymtech/nym/pull/5096 +[#5093]: https://github.com/nymtech/nym/pull/5093 +[#5091]: https://github.com/nymtech/nym/pull/5091 +[#5090]: https://github.com/nymtech/nym/pull/5090 +[#5083]: https://github.com/nymtech/nym/pull/5083 +[#5081]: https://github.com/nymtech/nym/pull/5081 +[#5076]: https://github.com/nymtech/nym/pull/5076 +[#5075]: https://github.com/nymtech/nym/pull/5075 +[#5073]: https://github.com/nymtech/nym/pull/5073 +[#5072]: https://github.com/nymtech/nym/pull/5072 +[#5069]: https://github.com/nymtech/nym/pull/5069 +[#5068]: https://github.com/nymtech/nym/pull/5068 +[#5067]: https://github.com/nymtech/nym/pull/5067 +[#5066]: https://github.com/nymtech/nym/pull/5066 +[#5065]: https://github.com/nymtech/nym/pull/5065 +[#5063]: https://github.com/nymtech/nym/pull/5063 +[#5062]: https://github.com/nymtech/nym/pull/5062 +[#5061]: https://github.com/nymtech/nym/pull/5061 +[#5051]: https://github.com/nymtech/nym/pull/5051 +[#5049]: https://github.com/nymtech/nym/pull/5049 +[#5047]: https://github.com/nymtech/nym/pull/5047 +[#5046]: https://github.com/nymtech/nym/pull/5046 +[#5045]: https://github.com/nymtech/nym/pull/5045 +[#5043]: https://github.com/nymtech/nym/pull/5043 +[#5041]: https://github.com/nymtech/nym/pull/5041 +[#5040]: https://github.com/nymtech/nym/pull/5040 +[#5039]: https://github.com/nymtech/nym/pull/5039 +[#5038]: https://github.com/nymtech/nym/pull/5038 +[#5037]: https://github.com/nymtech/nym/pull/5037 +[#5036]: https://github.com/nymtech/nym/pull/5036 +[#5034]: https://github.com/nymtech/nym/pull/5034 +[#5032]: https://github.com/nymtech/nym/pull/5032 +[#5031]: https://github.com/nymtech/nym/pull/5031 +[#5030]: https://github.com/nymtech/nym/pull/5030 +[#5029]: https://github.com/nymtech/nym/pull/5029 +[#5028]: https://github.com/nymtech/nym/pull/5028 +[#5024]: https://github.com/nymtech/nym/pull/5024 +[#5023]: https://github.com/nymtech/nym/pull/5023 +[#5016]: https://github.com/nymtech/nym/pull/5016 +[#5014]: https://github.com/nymtech/nym/pull/5014 +[#5013]: https://github.com/nymtech/nym/pull/5013 +[#5009]: https://github.com/nymtech/nym/pull/5009 +[#5008]: https://github.com/nymtech/nym/pull/5008 +[#4994]: https://github.com/nymtech/nym/pull/4994 +[#4993]: https://github.com/nymtech/nym/pull/4993 +[#4992]: https://github.com/nymtech/nym/pull/4992 +[#4991]: https://github.com/nymtech/nym/pull/4991 +[#4988]: https://github.com/nymtech/nym/pull/4988 +[#4984]: https://github.com/nymtech/nym/pull/4984 +[#4983]: https://github.com/nymtech/nym/pull/4983 +[#4982]: https://github.com/nymtech/nym/pull/4982 +[#4981]: https://github.com/nymtech/nym/pull/4981 +[#4980]: https://github.com/nymtech/nym/pull/4980 +[#4978]: https://github.com/nymtech/nym/pull/4978 +[#4977]: https://github.com/nymtech/nym/pull/4977 +[#4976]: https://github.com/nymtech/nym/pull/4976 +[#4975]: https://github.com/nymtech/nym/pull/4975 +[#4974]: https://github.com/nymtech/nym/pull/4974 +[#4973]: https://github.com/nymtech/nym/pull/4973 +[#4972]: https://github.com/nymtech/nym/pull/4972 +[#4968]: https://github.com/nymtech/nym/pull/4968 +[#4967]: https://github.com/nymtech/nym/pull/4967 +[#4966]: https://github.com/nymtech/nym/pull/4966 +[#4965]: https://github.com/nymtech/nym/pull/4965 +[#4963]: https://github.com/nymtech/nym/pull/4963 +[#4959]: https://github.com/nymtech/nym/pull/4959 +[#4957]: https://github.com/nymtech/nym/pull/4957 +[#4954]: https://github.com/nymtech/nym/pull/4954 +[#4953]: https://github.com/nymtech/nym/pull/4953 +[#4949]: https://github.com/nymtech/nym/pull/4949 +[#4948]: https://github.com/nymtech/nym/pull/4948 +[#4947]: https://github.com/nymtech/nym/pull/4947 +[#4945]: https://github.com/nymtech/nym/pull/4945 +[#4944]: https://github.com/nymtech/nym/pull/4944 +[#4943]: https://github.com/nymtech/nym/pull/4943 +[#4942]: https://github.com/nymtech/nym/pull/4942 +[#4938]: https://github.com/nymtech/nym/pull/4938 +[#4937]: https://github.com/nymtech/nym/pull/4937 +[#4936]: https://github.com/nymtech/nym/pull/4936 +[#4929]: https://github.com/nymtech/nym/pull/4929 +[#4903]: https://github.com/nymtech/nym/pull/4903 +[#4826]: https://github.com/nymtech/nym/pull/4826 +[#4758]: https://github.com/nymtech/nym/pull/4758 + ## [2024.12-aero] (2024-10-17) - nym-node: don't use bloomfilters for double spending checks ([#4960]) diff --git a/Cargo.lock b/Cargo.lock index 135ecf5136..3be4b4751b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" - [[package]] name = "accessory" version = "1.3.1" @@ -119,6 +113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", + "getrandom", "once_cell", "version_check", "zerocopy", @@ -306,6 +301,16 @@ dependencies = [ "nom", ] +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-channel" version = "1.9.0" @@ -313,10 +318,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" dependencies = [ "concurrent-queue", - "event-listener", + "event-listener 2.5.3", "futures-core", ] +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-stream" version = "0.3.5" @@ -341,9 +357,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -369,9 +385,9 @@ dependencies = [ [[package]] name = "atoi" -version = "1.0.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7c57d12312ff59c811c0643f4d80830505833c9ffaebd193d819392b265be8e" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" dependencies = [ "num-traits", ] @@ -402,6 +418,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "auto-future" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c1e7e457ea78e524f48639f551fd79703ac3f2237f5ecccdf4708f8a75ad373" + [[package]] name = "autocfg" version = "1.3.0" @@ -431,19 +453,20 @@ dependencies = [ "rustversion", "serde", "sync_wrapper 0.1.2", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", ] [[package]] name = "axum" -version = "0.7.5" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" dependencies = [ "async-trait", - "axum-core 0.4.3", + "axum-core 0.4.5", + "axum-macros", "bytes", "futures-util", "http 1.1.0", @@ -464,12 +487,23 @@ dependencies = [ "serde_urlencoded", "sync_wrapper 1.0.1", "tokio", - "tower", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", ] +[[package]] +name = "axum-client-ip" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eefda7e2b27e1bda4d6fa8a06b50803b8793769045918bc37ad062d48a6efac" +dependencies = [ + "axum 0.7.7", + "forwarded-header-value", + "serde", +] + [[package]] name = "axum-core" version = "0.3.4" @@ -489,9 +523,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" dependencies = [ "async-trait", "bytes", @@ -502,7 +536,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "tower-layer", "tower-service", "tracing", @@ -510,12 +544,12 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" +checksum = "73c3220b188aea709cf1b6c5f9b01c3bd936bb08bd2b5184a12b35ac8131b1f9" dependencies = [ - "axum 0.7.5", - "axum-core 0.4.3", + "axum 0.7.7", + "axum-core 0.4.5", "bytes", "futures-util", "headers", @@ -525,12 +559,52 @@ dependencies = [ "mime", "pin-project-lite", "serde", - "tower", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", ] +[[package]] +name = "axum-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + +[[package]] +name = "axum-test" +version = "16.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3254184de359bbae2a8ca10b050870a7a4f1c8332a2c27d53f360b9835bb3911" +dependencies = [ + "anyhow", + "assert-json-diff", + "auto-future", + "axum 0.7.7", + "bytes", + "cookie", + "http 1.1.0", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "mime", + "pretty_assertions", + "reserve-port", + "rust-multipart-rfc7578_2", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tokio", + "tower 0.5.1", + "url", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -576,6 +650,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "base85rs" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87678d33a2af71f019ed11f52db246ca6c5557edee2cccbe689676d1ad9c6b5a" + [[package]] name = "basic-toml" version = "0.1.9" @@ -653,6 +733,9 @@ name = "bitflags" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +dependencies = [ + "serde", +] [[package]] name = "bitvec" @@ -807,9 +890,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" dependencies = [ "serde", ] @@ -1009,9 +1092,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac" +checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3" dependencies = [ "clap_builder", "clap_derive", @@ -1019,9 +1102,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.17" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73" +checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b" dependencies = [ "anstream", "anstyle", @@ -1031,11 +1114,11 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.28" +version = "4.5.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b378c786d3bde9442d2c6dd7e6080b2a818db2b96e30d6e7f1b6d224eb617d3" +checksum = "8937760c3f4c60871870b8c3ee5f9b30771f792a7045c48bcbba999d7d6b3b8e" dependencies = [ - "clap 4.5.17", + "clap 4.5.18", ] [[package]] @@ -1044,15 +1127,15 @@ version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d494102c8ff3951810c72baf96910b980fb065ca5d3101243e6a8dc19747c86b" dependencies = [ - "clap 4.5.17", + "clap 4.5.18", "clap_complete", ] [[package]] name = "clap_derive" -version = "4.5.13" +version = "4.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -1452,7 +1535,7 @@ dependencies = [ "anes", "cast", "ciborium", - "clap 4.5.17", + "clap 4.5.18", "criterion-plot", "is-terminal", "itertools 0.10.5", @@ -1532,7 +1615,7 @@ dependencies = [ "crossterm_winapi", "libc", "mio 0.8.11", - "parking_lot 0.12.3", + "parking_lot", "signal-hook", "signal-hook-mio", "winapi", @@ -1547,7 +1630,7 @@ dependencies = [ "bitflags 2.5.0", "crossterm_winapi", "libc", - "parking_lot 0.12.3", + "parking_lot", "winapi", ] @@ -1900,7 +1983,7 @@ dependencies = [ "hashbrown 0.14.5", "lock_api", "once_cell", - "parking_lot_core 0.9.10", + "parking_lot_core", "serde", ] @@ -1948,6 +2031,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" dependencies = [ "const-oid", + "pem-rfc7468", "zeroize", ] @@ -2016,6 +2100,12 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.8.1" @@ -2046,33 +2136,13 @@ dependencies = [ "subtle 2.5.0", ] -[[package]] -name = "dirs" -version = "4.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" -dependencies = [ - "dirs-sys 0.3.7", -] - [[package]] name = "dirs" version = "5.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" dependencies = [ - "dirs-sys 0.4.1", -] - -[[package]] -name = "dirs-sys" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" -dependencies = [ - "libc", - "redox_users", - "winapi", + "dirs-sys", ] [[package]] @@ -2152,7 +2222,7 @@ dependencies = [ "bytecodec", "bytes", "dashmap", - "dirs 5.0.1", + "dirs", "nym-sdk", "serde", "tokio", @@ -2284,6 +2354,15 @@ dependencies = [ "termcolor", ] +[[package]] +name = "envy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" +dependencies = [ + "serde", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -2300,6 +2379,17 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + [[package]] name = "etherparse" version = "0.13.0" @@ -2315,12 +2405,33 @@ version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + [[package]] name = "explorer-api" -version = "1.1.41" +version = "1.1.42" dependencies = [ "chrono", - "clap 4.5.17", + "clap 4.5.18", "dotenvy", "humantime-serde", "isocountry", @@ -2488,9 +2599,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide 0.8.0", @@ -2508,13 +2619,12 @@ dependencies = [ [[package]] name = "flume" -version = "0.10.14" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1657b4441c3403d9f7b3409e47575237dac27b1b5726df654a6ecbf92f0f7577" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" dependencies = [ "futures-core", "futures-sink", - "pin-project", "spin 0.9.8", ] @@ -2539,6 +2649,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" +[[package]] +name = "forwarded-header-value" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" +dependencies = [ + "nonempty", + "thiserror", +] + [[package]] name = "fs-err" version = "2.11.0" @@ -2607,13 +2727,13 @@ dependencies = [ [[package]] name = "futures-intrusive" -version = "0.4.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a604f7a68fbf8103337523b1fadc8ade7361ee3f112f7c680ad179651616aed5" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" dependencies = [ "futures-core", "lock_api", - "parking_lot 0.11.2", + "parking_lot", ] [[package]] @@ -3035,6 +3155,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + [[package]] name = "http" version = "0.2.12" @@ -3233,9 +3362,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.5" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b875924a60b96e5d7b9ae7b066540b1dd1cbd90d1828f54c92e02a283351c56" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" dependencies = [ "bytes", "futures-channel", @@ -3246,7 +3375,6 @@ dependencies = [ "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] @@ -3300,6 +3428,36 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "importer-cli" +version = "0.1.0" +dependencies = [ + "anyhow", + "bip39", + "clap 4.5.18", + "dirs", + "importer-contract", + "nym-bin-common", + "nym-mixnet-contract-common", + "nym-network-defaults", + "nym-validator-client", + "nym-vesting-contract-common", + "serde", + "serde_json", + "tokio", + "tracing", +] + +[[package]] +name = "importer-contract" +version = "0.1.0" +dependencies = [ + "base85rs", + "cosmwasm-schema", + "cosmwasm-std", + "cosmwasm-storage", +] + [[package]] name = "indenter" version = "0.3.3" @@ -3488,7 +3646,7 @@ dependencies = [ "crossbeam-utils", "curl", "curl-sys", - "event-listener", + "event-listener 2.5.3", "futures-lite", "http 0.2.12", "log", @@ -3621,6 +3779,9 @@ name = "lazy_static" version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] [[package]] name = "ledger-apdu" @@ -3683,9 +3844,9 @@ dependencies = [ [[package]] name = "libsqlite3-sys" -version = "0.24.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" dependencies = [ "cc", "pkg-config", @@ -3967,6 +4128,30 @@ dependencies = [ "wasm-utils", ] +[[package]] +name = "moka" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cf62eb4dd975d2dde76432fb1075c49e3ee2331cf36f1f8fd4b66550d32b6f" +dependencies = [ + "async-lock", + "async-trait", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "event-listener 5.3.1", + "futures-util", + "once_cell", + "parking_lot", + "quanta", + "rustc_version 0.4.0", + "smallvec", + "tagptr", + "thiserror", + "triomphe", + "uuid", +] + [[package]] name = "multer" version = "2.1.0" @@ -4111,6 +4296,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonempty" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + [[package]] name = "notify" version = "5.2.0" @@ -4148,6 +4339,23 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + [[package]] name = "num-conv" version = "0.1.0" @@ -4165,6 +4373,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -4223,31 +4451,32 @@ checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" [[package]] name = "nym-api" -version = "1.1.45" +version = "1.1.46" dependencies = [ "anyhow", "async-trait", - "axum 0.7.5", + "axum 0.7.7", "axum-extra", + "axum-test", "bincode", "bip39", "bloomfilter", "bs58", "cfg-if", - "clap 4.5.17", + "clap 4.5.18", "console-subscriber", "cosmwasm-std", "cw-utils", "cw2", "cw3", "cw4", - "dirs 5.0.1", + "dashmap", + "dirs", "futures", "getset", "humantime-serde", "itertools 0.13.0", "k256", - "log", "nym-api-requests", "nym-bandwidth-controller", "nym-bin-common", @@ -4272,26 +4501,22 @@ dependencies = [ "nym-node-requests", "nym-node-tester-utils", "nym-pemstore", + "nym-serde-helpers", "nym-sphinx", "nym-task", "nym-topology", "nym-types", "nym-validator-client", "nym-vesting-contract-common", - "okapi", "pin-project", "rand", "rand_chacha", "reqwest 0.12.4", - "rocket", - "rocket_cors", - "rocket_okapi", "schemars", "serde", "serde_json", "sha2 0.9.9", "sqlx", - "tap", "tempfile", "thiserror", "time", @@ -4300,7 +4525,6 @@ dependencies = [ "tokio-util", "tower-http", "tracing", - "tracing-subscriber", "ts-rs", "url", "utoipa", @@ -4323,9 +4547,9 @@ dependencies = [ "nym-crypto", "nym-ecash-time", "nym-mixnet-contract-common", + "nym-network-defaults", "nym-node-requests", "nym-serde-helpers", - "rocket", "schemars", "serde", "serde_json", @@ -4355,7 +4579,7 @@ dependencies = [ "bincode", "bs58", "bytes", - "clap 4.5.17", + "clap 4.5.18", "defguard_wireguard_rs", "fastrand 2.1.1", "futures", @@ -4433,7 +4657,7 @@ dependencies = [ name = "nym-bin-common" version = "0.6.0" dependencies = [ - "clap 4.5.17", + "clap 4.5.18", "clap_complete", "clap_complete_fig", "const-str", @@ -4469,13 +4693,13 @@ dependencies = [ [[package]] name = "nym-cli" -version = "1.1.43" +version = "1.1.44" dependencies = [ "anyhow", "base64 0.22.1", "bip39", "bs58", - "clap 4.5.17", + "clap 4.5.18", "clap_complete", "clap_complete_fig", "dotenvy", @@ -4501,7 +4725,7 @@ dependencies = [ "bip39", "bs58", "cfg-if", - "clap 4.5.17", + "clap 4.5.18", "colored", "comfy-table", "cosmrs 0.17.0-pre", @@ -4550,11 +4774,11 @@ dependencies = [ [[package]] name = "nym-client" -version = "1.1.42" +version = "1.1.44" dependencies = [ "bs58", - "clap 4.5.17", - "dirs 5.0.1", + "clap 4.5.18", + "dirs", "futures", "log", "nym-bandwidth-controller", @@ -4593,7 +4817,7 @@ dependencies = [ "base64 0.22.1", "bs58", "cfg-if", - "clap 4.5.17", + "clap 4.5.18", "comfy-table", "futures", "gloo-timers", @@ -4774,6 +4998,13 @@ dependencies = [ "nym-multisig-contract-common", ] +[[package]] +name = "nym-common-models" +version = "0.1.0" +dependencies = [ + "serde", +] + [[package]] name = "nym-compact-ecash" version = "0.1.0" @@ -4786,13 +5017,14 @@ dependencies = [ "digest 0.9.0", "ff", "group", - "itertools 0.12.1", + "itertools 0.13.0", "nym-network-defaults", "nym-pemstore", "rand", "rayon", "serde", "sha2 0.9.9", + "subtle 2.5.0", "thiserror", "zeroize", ] @@ -4801,7 +5033,7 @@ dependencies = [ name = "nym-config" version = "0.1.0" dependencies = [ - "dirs 5.0.1", + "dirs", "handlebars", "log", "nym-network-defaults", @@ -4978,8 +5210,9 @@ name = "nym-data-observatory" version = "0.1.0" dependencies = [ "anyhow", - "axum 0.7.5", + "axum 0.7.7", "chrono", + "clap 4.5.18", "nym-bin-common", "nym-network-defaults", "nym-node-requests", @@ -5087,12 +5320,12 @@ dependencies = [ name = "nym-explorer-client" version = "0.1.0" dependencies = [ - "log", "nym-explorer-api-requests", "reqwest 0.12.4", "serde", "thiserror", "tokio", + "tracing", "url", ] @@ -5119,11 +5352,11 @@ dependencies = [ "async-trait", "bip39", "bs58", - "clap 4.5.17", + "clap 4.5.18", "colored", "dashmap", "defguard_wireguard_rs", - "dirs 5.0.1", + "dirs", "dotenvy", "futures", "humantime-serde", @@ -5145,8 +5378,11 @@ dependencies = [ "nym-network-requester", "nym-node-http-api", "nym-pemstore", + "nym-sdk", "nym-sphinx", + "nym-statistics-common", "nym-task", + "nym-topology", "nym-types", "nym-validator-client", "nym-wireguard", @@ -5159,6 +5395,7 @@ dependencies = [ "sqlx", "subtle-encoding", "thiserror", + "time", "tokio", "tokio-stream", "tokio-tungstenite", @@ -5178,6 +5415,7 @@ dependencies = [ "nym-bandwidth-controller", "nym-credential-storage", "nym-credentials", + "nym-credentials-interface", "nym-crypto", "nym-gateway-requests", "nym-network-defaults", @@ -5294,7 +5532,8 @@ dependencies = [ name = "nym-http-api-common" version = "0.1.0" dependencies = [ - "axum 0.7.5", + "axum 0.7.7", + "axum-client-ip", "bytes", "colored", "mime", @@ -5323,7 +5562,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bs58", - "clap 4.5.17", + "clap 4.5.18", "nym-bin-common", "nym-credential-storage", "nym-id", @@ -5365,7 +5604,7 @@ dependencies = [ "bincode", "bs58", "bytes", - "clap 4.5.17", + "clap 4.5.18", "etherparse", "futures", "log", @@ -5440,6 +5679,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-controllers", + "cw-storage-plus", "cw2", "humantime-serde", "log", @@ -5460,12 +5700,12 @@ name = "nym-mixnode" version = "1.1.37" dependencies = [ "anyhow", - "axum 0.7.5", + "axum 0.7.7", "bs58", - "clap 4.5.17", + "clap 4.5.18", "colored", "cupid", - "dirs 5.0.1", + "dirs", "futures", "humantime-serde", "lazy_static", @@ -5562,8 +5802,8 @@ name = "nym-network-monitor" version = "0.1.0" dependencies = [ "anyhow", - "axum 0.7.5", - "clap 4.5.17", + "axum 0.7.7", + "clap 4.5.18", "dashmap", "futures", "log", @@ -5589,14 +5829,14 @@ dependencies = [ [[package]] name = "nym-network-requester" -version = "1.1.43" +version = "1.1.45" dependencies = [ "addr", "anyhow", "async-trait", "bs58", - "clap 4.5.17", - "dirs 5.0.1", + "clap 4.5.18", + "dirs", "futures", "humantime-serde", "ipnetwork 0.20.0", @@ -5640,14 +5880,14 @@ dependencies = [ [[package]] name = "nym-node" -version = "1.1.9" +version = "1.1.11" dependencies = [ "anyhow", "bip39", "bs58", "cargo_metadata 0.18.1", "celes", - "clap 4.5.17", + "clap 4.5.18", "colored", "cupid", "humantime-serde", @@ -5667,6 +5907,7 @@ dependencies = [ "nym-sphinx-addressing", "nym-task", "nym-types", + "nym-validator-client", "nym-wireguard", "nym-wireguard-types", "rand", @@ -5686,7 +5927,7 @@ dependencies = [ name = "nym-node-http-api" version = "0.1.0" dependencies = [ - "axum 0.7.5", + "axum 0.7.7", "axum-extra", "base64 0.22.1", "colored", @@ -5707,7 +5948,7 @@ dependencies = [ "thiserror", "time", "tokio", - "tower", + "tower 0.4.13", "tower-http", "tracing", "utoipa", @@ -5740,13 +5981,68 @@ dependencies = [ ] [[package]] -name = "nym-node-tester-utils" -version = "0.1.0" +name = "nym-node-status-agent" +version = "0.1.6" dependencies = [ - "futures", - "log", - "nym-crypto", - "nym-sphinx", + "anyhow", + "clap 4.5.18", + "nym-bin-common", + "nym-common-models", + "reqwest 0.12.4", + "serde_json", + "tokio", + "tokio-util", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "nym-node-status-api" +version = "0.1.6" +dependencies = [ + "anyhow", + "axum 0.7.7", + "chrono", + "clap 4.5.18", + "cosmwasm-std", + "envy", + "futures-util", + "moka", + "nym-bin-common", + "nym-common-models", + "nym-explorer-client", + "nym-network-defaults", + "nym-node-requests", + "nym-task", + "nym-validator-client", + "regex", + "reqwest 0.12.4", + "serde", + "serde_json", + "serde_json_path", + "sqlx", + "strum 0.26.3", + "strum_macros 0.26.4", + "thiserror", + "tokio", + "tokio-util", + "tower-http", + "tracing", + "tracing-log 0.2.0", + "tracing-subscriber", + "utoipa", + "utoipa-swagger-ui", + "utoipauto", +] + +[[package]] +name = "nym-node-tester-utils" +version = "0.1.0" +dependencies = [ + "futures", + "log", + "nym-crypto", + "nym-sphinx", "nym-sphinx-params", "nym-task", "nym-topology", @@ -5794,7 +6090,7 @@ name = "nym-nr-query" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.5.17", + "clap 4.5.18", "log", "nym-bin-common", "nym-network-defaults", @@ -5850,7 +6146,7 @@ dependencies = [ "bytecodec", "bytes", "dashmap", - "dirs 5.0.1", + "dirs", "dotenvy", "futures", "hex", @@ -5875,7 +6171,7 @@ dependencies = [ "nym-task", "nym-topology", "nym-validator-client", - "parking_lot 0.12.3", + "parking_lot", "pretty_env_logger", "rand", "reqwest 0.12.4", @@ -5899,6 +6195,7 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "bs58", + "hex", "serde", "time", ] @@ -5929,10 +6226,10 @@ dependencies = [ [[package]] name = "nym-socks5-client" -version = "1.1.42" +version = "1.1.44" dependencies = [ "bs58", - "clap 4.5.17", + "clap 4.5.18", "log", "nym-bin-common", "nym-client-core", @@ -5965,7 +6262,7 @@ name = "nym-socks5-client-core" version = "0.1.0" dependencies = [ "anyhow", - "dirs 5.0.1", + "dirs", "futures", "log", "nym-bandwidth-controller", @@ -6167,9 +6464,15 @@ name = "nym-sphinx-framing" version = "0.1.0" dependencies = [ "bytes", + "log", + "nym-metrics", + "nym-sphinx-acknowledgements", + "nym-sphinx-addressing", + "nym-sphinx-forwarding", "nym-sphinx-params", "nym-sphinx-types", "thiserror", + "tokio", "tokio-util", ] @@ -6201,6 +6504,16 @@ dependencies = [ "thiserror", ] +[[package]] +name = "nym-statistics-common" +version = "0.1.0" +dependencies = [ + "futures", + "nym-credentials-interface", + "nym-sphinx", + "time", +] + [[package]] name = "nym-store-cipher" version = "0.1.0" @@ -6316,7 +6629,6 @@ dependencies = [ "flate2", "futures", "itertools 0.13.0", - "log", "nym-api-requests", "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", @@ -6329,6 +6641,7 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", + "nym-serde-helpers", "nym-vesting-contract-common", "prost 0.12.6", "reqwest 0.12.4", @@ -6339,6 +6652,7 @@ dependencies = [ "thiserror", "time", "tokio", + "tracing", "ts-rs", "url", "wasmtimer", @@ -6351,7 +6665,7 @@ version = "0.1.0" dependencies = [ "anyhow", "bip39", - "clap 4.5.17", + "clap 4.5.18", "cosmwasm-std", "futures", "humantime 2.1.0", @@ -6457,11 +6771,11 @@ dependencies = [ [[package]] name = "nymvisor" -version = "0.1.8" +version = "0.1.9" dependencies = [ "anyhow", "bytes", - "clap 4.5.17", + "clap 4.5.18", "dotenvy", "flate2", "futures", @@ -6710,17 +7024,6 @@ version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - [[package]] name = "parking_lot" version = "0.12.3" @@ -6728,21 +7031,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", - "parking_lot_core 0.9.10", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall 0.2.16", - "smallvec", - "winapi", + "parking_lot_core", ] [[package]] @@ -6836,6 +7125,15 @@ dependencies = [ "regex", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -6929,6 +7227,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -7036,6 +7345,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "pretty_env_logger" version = "0.4.0" @@ -7143,7 +7462,7 @@ dependencies = [ "fnv", "lazy_static", "memchr", - "parking_lot 0.12.3", + "parking_lot", "protobuf", "thiserror", ] @@ -7243,6 +7562,21 @@ dependencies = [ "psl-types", ] +[[package]] +name = "quanta" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -7334,6 +7668,15 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "raw-cpuid" +version = "11.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ab240315c661615f2ee9f0f2cd32d5a7343a84d5ebcccb99d46e6637565e7b0" +dependencies = [ + "bitflags 2.5.0", +] + [[package]] name = "rayon" version = "1.10.0" @@ -7354,15 +7697,6 @@ dependencies = [ "crossbeam-utils", ] -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.4.1" @@ -7543,28 +7877,23 @@ dependencies = [ ] [[package]] -name = "rfc6979" -version = "0.4.0" +name = "reserve-port" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +checksum = "9838134a2bfaa8e1f40738fcc972ac799de6e0e06b5157acb95fc2b05a0ea283" dependencies = [ - "hmac", - "subtle 2.5.0", + "lazy_static", + "thiserror", ] [[package]] -name = "ring" -version = "0.16.20" +name = "rfc6979" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "cc", - "libc", - "once_cell", - "spin 0.5.2", - "untrusted 0.7.1", - "web-sys", - "winapi", + "hmac", + "subtle 2.5.0", ] [[package]] @@ -7578,7 +7907,7 @@ dependencies = [ "getrandom", "libc", "spin 0.9.8", - "untrusted 0.9.0", + "untrusted", "windows-sys 0.52.0", ] @@ -7610,7 +7939,7 @@ dependencies = [ "memchr", "multer", "num_cpus", - "parking_lot 0.12.3", + "parking_lot", "pin-project-lite", "rand", "ref-cast", @@ -7718,6 +8047,26 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle 2.5.0", + "zeroize", +] + [[package]] name = "rust-embed" version = "8.4.0" @@ -7752,6 +8101,22 @@ dependencies = [ "walkdir", ] +[[package]] +name = "rust-multipart-rfc7578_2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03b748410c0afdef2ebbe3685a6a862e2ee937127cdaae623336a459451c8d57" +dependencies = [ + "bytes", + "futures-core", + "futures-util", + "http 0.2.12", + "mime", + "mime_guess", + "rand", + "thiserror", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -7789,18 +8154,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustls" -version = "0.20.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b80e3dec595989ea8510028f30c408a4630db12c9cbb8de34203b89d6577e99" -dependencies = [ - "log", - "ring 0.16.20", - "sct", - "webpki", -] - [[package]] name = "rustls" version = "0.21.12" @@ -7808,7 +8161,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" dependencies = [ "log", - "ring 0.17.8", + "ring", "rustls-webpki 0.101.7", "sct", ] @@ -7820,7 +8173,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" dependencies = [ "log", - "ring 0.17.8", + "ring", "rustls-pki-types", "rustls-webpki 0.102.4", "subtle 2.5.0", @@ -7883,8 +8236,8 @@ version = "0.101.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", + "ring", + "untrusted", ] [[package]] @@ -7893,9 +8246,9 @@ version = "0.102.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" dependencies = [ - "ring 0.17.8", + "ring", "rustls-pki-types", - "untrusted 0.9.0", + "untrusted", ] [[package]] @@ -8023,8 +8376,8 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", + "ring", + "untrusted", ] [[package]] @@ -8173,9 +8526,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa", "memchr", @@ -8183,6 +8536,59 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_json_path" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bc0207b6351893eafa1e39aa9aea452abb6425ca7b02dd64faf29109e7a33ba" +dependencies = [ + "inventory", + "nom", + "once_cell", + "regex", + "serde", + "serde_json", + "serde_json_path_core", + "serde_json_path_macros", + "thiserror", +] + +[[package]] +name = "serde_json_path_core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3d64fe53ce1aaa31bea2b2b46d3b6ab6a37e61854bedcbd9f174e188f3f7d79" +dependencies = [ + "inventory", + "once_cell", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "serde_json_path_macros" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a31e8177a443fd3e94917f12946ae7891dfb656e6d4c5e79b8c5d202fbcb723" +dependencies = [ + "inventory", + "once_cell", + "serde_json_path_core", + "serde_json_path_macros_internal", +] + +[[package]] +name = "serde_json_path_macros_internal" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75dde5a1d2ed78dfc411fc45592f72d3694436524d3353683ecb3d22009731dc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "serde_path_to_error" version = "0.1.16" @@ -8522,77 +8928,79 @@ dependencies = [ [[package]] name = "sqlx" -version = "0.6.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8de3b03a925878ed54a954f621e64bf55a3c1bd29652d0d1a17830405350188" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" dependencies = [ "sqlx-core", "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", ] [[package]] name = "sqlx-core" -version = "0.6.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa8241483a83a3f33aa5fff7e7d9def398ff9990b2752b6c6112b83c6d246029" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" dependencies = [ - "ahash 0.7.8", + "ahash 0.8.11", "atoi", - "base64 0.13.1", - "bitflags 1.3.2", "byteorder", "bytes", "chrono", "crc", "crossbeam-queue", - "dirs 4.0.0", - "dotenvy", "either", - "event-listener", - "flume", + "event-listener 2.5.3", "futures-channel", "futures-core", - "futures-executor", "futures-intrusive", + "futures-io", "futures-util", "hashlink", "hex", - "hkdf", - "hmac", - "indexmap 1.9.3", - "itoa", - "libc", - "libsqlite3-sys", + "indexmap 2.2.6", "log", - "md-5", "memchr", "once_cell", "paste", "percent-encoding", - "rand", - "rustls 0.20.9", + "rustls 0.21.12", "rustls-pemfile 1.0.4", "serde", "serde_json", - "sha1", "sha2 0.10.8", "smallvec", "sqlformat", - "sqlx-rt", - "stringprep", "thiserror", "time", + "tokio", "tokio-stream", + "tracing", "url", - "webpki-roots 0.22.6", - "whoami", + "webpki-roots 0.25.4", ] [[package]] name = "sqlx-macros" -version = "0.6.3" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9966e64ae989e7e575b19d7265cb79d7fc3cbbdf179835cb0d716f294c2049c9" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" dependencies = [ "dotenvy", "either", @@ -8605,20 +9013,122 @@ dependencies = [ "serde_json", "sha2 0.10.8", "sqlx-core", - "sqlx-rt", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", "syn 1.0.109", + "tempfile", + "tokio", "url", ] [[package]] -name = "sqlx-rt" -version = "0.6.3" +name = "sqlx-mysql" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.5.0", + "byteorder", + "bytes", + "chrono", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array 0.14.7", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", "once_cell", - "tokio", - "tokio-rustls 0.23.4", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2 0.10.8", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.5.0", + "byteorder", + "chrono", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2 0.10.8", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +dependencies = [ + "atoi", + "chrono", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "time", + "tracing", + "url", + "urlencoding", ] [[package]] @@ -8626,7 +9136,7 @@ name = "ssl-inject" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.5.17", + "clap 4.5.18", "hex", "tokio", ] @@ -8853,6 +9363,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + [[package]] name = "tap" version = "1.0.1" @@ -8861,9 +9377,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tar" -version = "0.4.41" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb797dad5fb5b76fcf519e702f4a589483b5ef06567f160c392832c1f5e44909" +checksum = "4ff6c40d3aedb5e06b57c6f669ad17ab063dd1e63d977c6a88e7f4dfa4f04020" dependencies = [ "filetime", "libc", @@ -9042,7 +9558,7 @@ dependencies = [ "anyhow", "bip39", "bs58", - "clap 4.5.17", + "clap 4.5.18", "console", "cw-utils", "dkg-bypass-contract", @@ -9082,18 +9598,18 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", @@ -9201,7 +9717,7 @@ dependencies = [ "bytes", "libc", "mio 1.0.1", - "parking_lot 0.12.3", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", @@ -9231,17 +9747,6 @@ dependencies = [ "syn 2.0.66", ] -[[package]] -name = "tokio-rustls" -version = "0.23.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" -dependencies = [ - "rustls 0.20.9", - "tokio", - "webpki", -] - [[package]] name = "tokio-rustls" version = "0.24.1" @@ -9419,7 +9924,7 @@ dependencies = [ "prost 0.11.9", "tokio", "tokio-stream", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -9445,6 +9950,22 @@ dependencies = [ "tracing", ] +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower-http" version = "0.5.2" @@ -9472,15 +9993,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -9620,6 +10141,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + [[package]] name = "try-lock" version = "0.2.5" @@ -9628,10 +10155,11 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "ts-rs" -version = "7.1.1" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2cae1fc5d05d47aa24b64f9a4f7cba24cdc9187a2084dd97ac57bef5eccae6" +checksum = "3a2f31991cee3dce1ca4f929a8a04fdd11fd8801aac0f2030b0fa8a0a3fef6b9" dependencies = [ + "lazy_static", "thiserror", "ts-rs-macros", ] @@ -9653,11 +10181,10 @@ dependencies = [ [[package]] name = "ts-rs-macros" -version = "7.1.1" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f7f9b821696963053a89a7bd8b292dc34420aea8294d7b225274d488f3ec92" +checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e" dependencies = [ - "Inflector", "proc-macro2", "quote", "syn 2.0.66", @@ -9841,7 +10368,7 @@ checksum = "21345172d31092fd48c47fd56c53d4ae9e41c4b1f559fb8c38c1ab1685fd919f" dependencies = [ "anyhow", "camino", - "clap 4.5.17", + "clap 4.5.18", "uniffi_bindgen", "uniffi_build", "uniffi_core", @@ -9858,7 +10385,7 @@ dependencies = [ "askama", "camino", "cargo_metadata 0.15.4", - "clap 4.5.17", + "clap 4.5.18", "fs-err", "glob", "goblin", @@ -9990,12 +10517,6 @@ version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - [[package]] name = "untrusted" version = "0.9.0" @@ -10069,7 +10590,7 @@ version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "943e0ff606c6d57d410fd5663a4d7c074ab2c5f14ab903b9514565e59fa1189e" dependencies = [ - "axum 0.7.5", + "axum 0.7.7", "mime_guess", "regex", "reqwest 0.12.4", @@ -10372,7 +10893,7 @@ checksum = "5f656cd8858a5164932d8a90f936700860976ec21eb00e0fe2aa8cab13f6b4cf" dependencies = [ "futures", "js-sys", - "parking_lot 0.12.3", + "parking_lot", "pin-utils", "slab", "wasm-bindgen", @@ -10388,25 +10909,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed63aea5ce73d0ff405984102c42de94fc55a6b75765d621c65262469b3c9b53" -dependencies = [ - "ring 0.17.8", - "untrusted 0.9.0", -] - -[[package]] -name = "webpki-roots" -version = "0.22.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c71e40d7d2c34a5106301fb632274ca37242cd0c9d3e64dbece371a40a2d87" -dependencies = [ - "webpki", -] - [[package]] name = "webpki-roots" version = "0.24.0" @@ -10448,7 +10950,6 @@ checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ "redox_syscall 0.5.1", "wasite", - "web-sys", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 7c0102e3c4..1dc988d673 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,6 +60,7 @@ members = [ "common/ip-packet-requests", "common/ledger", "common/mixnode-common", + "common/models", "common/network-defaults", "common/node-tester-utils", "common/nonexhaustive-delayqueue", @@ -85,6 +86,7 @@ members = [ "common/socks5-client-core", "common/socks5/proxy-helpers", "common/socks5/requests", + "common/statistics", "common/store-cipher", "common/task", "common/topology", @@ -118,6 +120,8 @@ members = [ "nym-node", "nym-node/nym-node-http-api", "nym-node/nym-node-requests", + "nym-node-status-api", + "nym-node-status-agent", "nym-outfox", "nym-validator-rewarder", "tools/echo-server", @@ -138,18 +142,23 @@ members = [ "tools/internal/testnet-manager", "tools/internal/testnet-manager/dkg-bypass-contract", "tools/echo-server", + "tools/internal/contract-state-importer/importer-cli", + "tools/internal/contract-state-importer/importer-contract", ] default-members = [ "clients/native", "clients/socks5", + "common/models", "explorer-api", "gateway", "mixnode", "nym-api", "nym-data-observatory", "nym-node", + "nym-node-status-api", "nym-validator-rewarder", + "nym-node-status-api", "service-providers/authenticator", "service-providers/ip-packet-router", "service-providers/network-requester", @@ -182,9 +191,10 @@ aes-gcm-siv = "0.11.1" aead = "0.5.2" anyhow = "1.0.89" argon2 = "0.5.0" -async-trait = "0.1.82" +async-trait = "0.1.83" +axum-client-ip = "0.6.1" axum = "0.7.5" -axum-extra = "0.9.3" +axum-extra = "0.9.4" base64 = "0.22.1" bincode = "1.3.3" bip39 = { version = "2.0.0", features = ["zeroize"] } @@ -197,7 +207,7 @@ blake3 = "1.5.4" bloomfilter = "1.0.14" bs58 = "0.5.1" bytecodec = "0.4.15" -bytes = "1.7.1" +bytes = "1.7.2" cargo_metadata = "0.18.1" celes = "2.4.0" cfg-if = "1.0.0" @@ -205,7 +215,7 @@ chacha20 = "0.9.0" chacha20poly1305 = "0.10.1" chrono = "0.4.31" cipher = "0.4.3" -clap = "4.5.17" +clap = "4.5.18" clap_complete = "4.5" clap_complete_fig = "4.5" colored = "2.0" @@ -230,10 +240,12 @@ dotenvy = "0.15.6" ecdsa = "0.16" ed25519-dalek = "2.1" etherparse = "0.13.0" +envy = "0.4" eyre = "0.6.9" fastrand = "2.1.1" -flate2 = "1.0.33" +flate2 = "1.0.34" futures = "0.3.28" +futures-util = "0.3" generic-array = "0.14.7" getrandom = "0.2.10" getset = "0.1.3" @@ -263,6 +275,7 @@ ledger-transport-hid = "0.10.0" log = "0.4" maxminddb = "0.23.0" mime = "0.3.17" +moka = { version = "0.12", features = ["future"] } nix = "0.27.1" notify = "5.1.0" okapi = "0.7.0" @@ -273,6 +286,7 @@ parking_lot = "0.12.3" pem = "0.8" petgraph = "0.6.5" pin-project = "1.0" +pin-project-lite = "0.2.14" pretty_env_logger = "0.4.0" publicsuffix = "2.2.3" quote = "1" @@ -294,22 +308,24 @@ semver = "1.0.23" serde = "1.0.210" serde_bytes = "0.11.15" serde_derive = "1.0" -serde_json = "1.0.128" +serde_json = "1.0.132" +serde_json_path = "0.6.7" serde_repr = "0.1" serde_with = "3.9.0" serde_yaml = "0.9.25" sha2 = "0.10.8" si-scale = "0.2.3" sphinx-packet = "0.1.1" -sqlx = "0.6.3" +sqlx = "0.7.4" strum = "0.26" +strum_macros = "0.26" subtle-encoding = "0.5" syn = "1" sysinfo = "0.30.13" tap = "1.0.1" -tar = "0.4.41" +tar = "0.4.42" tempfile = "3.5.0" -thiserror = "1.0.63" +thiserror = "1.0.64" time = "0.3.30" tokio = "1.39" tokio-stream = "0.1.16" @@ -324,7 +340,8 @@ tracing = "0.1.37" tracing-opentelemetry = "0.19.0" tracing-subscriber = "0.3.16" tracing-tree = "0.2.2" -ts-rs = "7.0.0" +tracing-log = "0.2" +ts-rs = "10.0.0" tungstenite = { version = "0.20.1", default-features = false } url = "2.5" utoipa = "4.2" @@ -346,6 +363,7 @@ prometheus = { version = "0.13.0" } bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", default-features = false, branch = "temp/experimental-serdect" } group = { version = "0.13.0", default-features = false } ff = { version = "0.13.0", default-features = false } +subtle = "2.5.0" # cosmwasm-related cosmwasm-schema = "=1.4.3" @@ -392,6 +410,12 @@ web-sys = "0.3.70" # Profile settings for individual crates +# Compile-time verified queries do quite a bit of work at compile time. Incremental +# actions like cargo check and cargo build can be significantly faster when +# using an optimized build +[profile.dev.package.sqlx-macros] +opt-level = 3 + [profile.release.package.nym-socks5-listener] strip = true codegen-units = 1 diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index c9317bb5f3..c5679d02de 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-client" -version = "1.1.42" +version = "1.1.44" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index 9a22fd4355..f5d3cd8967 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym-socks5-client" -version = "1.1.42" +version = "1.1.44" authors = ["Dave Hrycyszyn "] description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address" edition = "2021" diff --git a/common/authenticator-requests/src/error.rs b/common/authenticator-requests/src/error.rs index d07ad7c1e7..1c43da3cad 100644 --- a/common/authenticator-requests/src/error.rs +++ b/common/authenticator-requests/src/error.rs @@ -19,4 +19,10 @@ pub enum Error { #[source] source: hmac::digest::MacError, }, + + #[error("conversion: {0}")] + Conversion(String), + + #[error("failed to serialize response packet: {source}")] + FailedToSerializeResponsePacket { source: Box }, } diff --git a/common/authenticator-requests/src/lib.rs b/common/authenticator-requests/src/lib.rs index 80112f061c..f2b2fb55ce 100644 --- a/common/authenticator-requests/src/lib.rs +++ b/common/authenticator-requests/src/lib.rs @@ -1,15 +1,17 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +pub mod traits; pub mod v1; pub mod v2; +pub mod v3; mod error; pub use error::Error; -pub use v2 as latest; +pub use v3 as latest; -pub const CURRENT_VERSION: u8 = 2; +pub const CURRENT_VERSION: u8 = 3; fn make_bincode_serializer() -> impl bincode::Options { use bincode::Options; diff --git a/common/authenticator-requests/src/traits.rs b/common/authenticator-requests/src/traits.rs new file mode 100644 index 0000000000..cb7cf37ee1 --- /dev/null +++ b/common/authenticator-requests/src/traits.rs @@ -0,0 +1,269 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::net::IpAddr; + +use nym_credentials_interface::CredentialSpendingData; +use nym_crypto::asymmetric::x25519::PrivateKey; +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; +use nym_sphinx::addressing::clients::Recipient; +use nym_wireguard_types::PeerPublicKey; + +use crate::{v1, v2, v3, Error}; + +#[derive(Copy, Clone, Debug)] +pub enum AuthenticatorVersion { + V1, + V2, + V3, + UNKNOWN, +} + +impl From for AuthenticatorVersion { + fn from(value: Protocol) -> Self { + if value.service_provider_type != ServiceProviderType::Authenticator { + AuthenticatorVersion::UNKNOWN + } else if value.version == v1::VERSION { + AuthenticatorVersion::V1 + } else if value.version == v2::VERSION { + AuthenticatorVersion::V2 + } else if value.version == v3::VERSION { + AuthenticatorVersion::V3 + } else { + AuthenticatorVersion::UNKNOWN + } + } +} + +pub trait InitMessage { + fn pub_key(&self) -> PeerPublicKey; +} + +impl InitMessage for v1::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +impl InitMessage for v2::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +impl InitMessage for v3::registration::InitMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +pub trait FinalMessage { + fn pub_key(&self) -> PeerPublicKey; + fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error>; + fn private_ip(&self) -> IpAddr; + fn credential(&self) -> Option; +} + +impl FinalMessage for v1::GatewayClient { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + self.verify(private_key, nonce) + } + + fn private_ip(&self) -> IpAddr { + self.private_ip + } + + fn credential(&self) -> Option { + None + } +} + +impl FinalMessage for v2::registration::FinalMessage { + fn pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + self.gateway_client.verify(private_key, nonce) + } + + fn private_ip(&self) -> IpAddr { + self.gateway_client.private_ip + } + + fn credential(&self) -> Option { + self.credential.clone() + } +} + +impl FinalMessage for v3::registration::FinalMessage { + fn pub_key(&self) -> PeerPublicKey { + self.gateway_client.pub_key + } + + fn verify(&self, private_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + self.gateway_client.verify(private_key, nonce) + } + + fn private_ip(&self) -> IpAddr { + self.gateway_client.private_ip + } + + fn credential(&self) -> Option { + self.credential.clone() + } +} + +pub trait QueryBandwidthMessage { + fn pub_key(&self) -> PeerPublicKey; +} + +impl QueryBandwidthMessage for PeerPublicKey { + fn pub_key(&self) -> PeerPublicKey { + *self + } +} + +pub trait TopUpMessage { + fn pub_key(&self) -> PeerPublicKey; + fn credential(&self) -> CredentialSpendingData; +} + +impl TopUpMessage for v3::topup::TopUpMessage { + fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } + + fn credential(&self) -> CredentialSpendingData { + self.credential.clone() + } +} + +pub enum AuthenticatorRequest { + Initial { + msg: Box, + protocol: Protocol, + reply_to: Recipient, + request_id: u64, + }, + Final { + msg: Box, + protocol: Protocol, + reply_to: Recipient, + request_id: u64, + }, + QueryBandwidth { + msg: Box, + protocol: Protocol, + reply_to: Recipient, + request_id: u64, + }, + TopUpBandwidth { + msg: Box, + protocol: Protocol, + reply_to: Recipient, + request_id: u64, + }, +} + +impl From for AuthenticatorRequest { + fn from(value: v1::request::AuthenticatorRequest) -> Self { + match value.data { + v1::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: Protocol { + version: value.version, + service_provider_type: ServiceProviderType::Authenticator, + }, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v1::request::AuthenticatorRequestData::Final(gateway_client) => Self::Final { + msg: Box::new(gateway_client), + protocol: Protocol { + version: value.version, + service_provider_type: ServiceProviderType::Authenticator, + }, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v1::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: Protocol { + version: value.version, + service_provider_type: ServiceProviderType::Authenticator, + }, + reply_to: value.reply_to, + request_id: value.request_id, + } + } + } + } +} + +impl From for AuthenticatorRequest { + fn from(value: v2::request::AuthenticatorRequest) -> Self { + match value.data { + v2::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v2::request::AuthenticatorRequestData::Final(final_message) => Self::Final { + msg: final_message, + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v2::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + } + } + } + } +} + +impl From for AuthenticatorRequest { + fn from(value: v3::request::AuthenticatorRequest) -> Self { + match value.data { + v3::request::AuthenticatorRequestData::Initial(init_message) => Self::Initial { + msg: Box::new(init_message), + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v3::request::AuthenticatorRequestData::Final(final_message) => Self::Final { + msg: final_message, + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + }, + v3::request::AuthenticatorRequestData::QueryBandwidth(peer_public_key) => { + Self::QueryBandwidth { + msg: Box::new(peer_public_key), + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + } + } + v3::request::AuthenticatorRequestData::TopUpBandwidth(top_up_message) => { + Self::TopUpBandwidth { + msg: top_up_message, + protocol: value.protocol, + reply_to: value.reply_to, + request_id: value.request_id, + } + } + } + } +} diff --git a/common/authenticator-requests/src/v1/mod.rs b/common/authenticator-requests/src/v1/mod.rs index 90363b4f89..2644abee60 100644 --- a/common/authenticator-requests/src/v1/mod.rs +++ b/common/authenticator-requests/src/v1/mod.rs @@ -10,4 +10,4 @@ pub use registration::{ClientMac, GatewayClient, InitMessage, Nonce}; #[cfg(feature = "verify")] pub use registration::HmacSha256; -const VERSION: u8 = 1; +pub const VERSION: u8 = 1; diff --git a/common/authenticator-requests/src/v2/conversion.rs b/common/authenticator-requests/src/v2/conversion.rs index 1c40a4e357..b8e16ac4ba 100644 --- a/common/authenticator-requests/src/v2/conversion.rs +++ b/common/authenticator-requests/src/v2/conversion.rs @@ -62,8 +62,113 @@ impl From for v2::registration::GatewayClient { } } +impl From for v1::registration::GatewayClient { + fn from(gw_client: v2::registration::GatewayClient) -> Self { + Self { + pub_key: gw_client.pub_key, + private_ip: gw_client.private_ip, + mac: gw_client.mac.into(), + } + } +} + impl From for v2::registration::ClientMac { fn from(mac: v1::registration::ClientMac) -> Self { Self::new(mac.to_vec()) } } + +impl From for v1::registration::ClientMac { + fn from(mac: v2::registration::ClientMac) -> Self { + Self::new(mac.to_vec()) + } +} + +impl From for v1::response::AuthenticatorResponse { + fn from(authenticator_response: v2::response::AuthenticatorResponse) -> Self { + Self { + version: authenticator_response.protocol.version, + data: authenticator_response.data.into(), + reply_to: authenticator_response.reply_to, + } + } +} + +impl From for v1::response::AuthenticatorResponseData { + fn from(authenticator_response_data: v2::response::AuthenticatorResponseData) -> Self { + match authenticator_response_data { + v2::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => v1::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response.into(), + ), + v2::response::AuthenticatorResponseData::Registered(registered_response) => { + v1::response::AuthenticatorResponseData::Registered(registered_response.into()) + } + v2::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => v1::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response.into(), + ), + } + } +} + +impl From for v1::response::PendingRegistrationResponse { + fn from(value: v2::response::PendingRegistrationResponse) -> Self { + Self { + request_id: value.request_id, + reply_to: value.reply_to, + reply: value.reply.into(), + } + } +} + +impl From for v1::response::RegisteredResponse { + fn from(value: v2::response::RegisteredResponse) -> Self { + Self { + request_id: value.request_id, + reply_to: value.reply_to, + reply: value.reply.into(), + } + } +} + +impl From for v1::response::RemainingBandwidthResponse { + fn from(value: v2::response::RemainingBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply_to: value.reply_to, + reply: value.reply.map(Into::into), + } + } +} + +impl From for v1::registration::RegistrationData { + fn from(value: v2::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v1::registration::RegistredData { + fn from(value: v2::registration::RegistredData) -> Self { + Self { + pub_key: value.pub_key, + private_ip: value.private_ip, + wg_port: value.wg_port, + } + } +} + +impl From for v1::registration::RemainingBandwidthData { + fn from(value: v2::registration::RemainingBandwidthData) -> Self { + Self { + available_bandwidth: value.available_bandwidth as u64, + suspended: false, + } + } +} diff --git a/common/authenticator-requests/src/v2/mod.rs b/common/authenticator-requests/src/v2/mod.rs index 29be89580d..3dd3373553 100644 --- a/common/authenticator-requests/src/v2/mod.rs +++ b/common/authenticator-requests/src/v2/mod.rs @@ -6,4 +6,4 @@ pub mod registration; pub mod request; pub mod response; -const VERSION: u8 = 2; +pub const VERSION: u8 = 2; diff --git a/common/authenticator-requests/src/v3/conversion.rs b/common/authenticator-requests/src/v3/conversion.rs new file mode 100644 index 0000000000..2ceaeb0301 --- /dev/null +++ b/common/authenticator-requests/src/v3/conversion.rs @@ -0,0 +1,272 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; + +use crate::{v2, v3}; + +impl From for v3::request::AuthenticatorRequest { + fn from(authenticator_request: v2::request::AuthenticatorRequest) -> Self { + Self { + protocol: Protocol { + version: 2, + service_provider_type: ServiceProviderType::Authenticator, + }, + data: authenticator_request.data.into(), + reply_to: authenticator_request.reply_to, + request_id: authenticator_request.request_id, + } + } +} + +impl From for v3::request::AuthenticatorRequestData { + fn from(authenticator_request_data: v2::request::AuthenticatorRequestData) -> Self { + match authenticator_request_data { + v2::request::AuthenticatorRequestData::Initial(init_msg) => { + v3::request::AuthenticatorRequestData::Initial(init_msg.into()) + } + v2::request::AuthenticatorRequestData::Final(gw_client) => { + v3::request::AuthenticatorRequestData::Final(gw_client.into()) + } + v2::request::AuthenticatorRequestData::QueryBandwidth(pub_key) => { + v3::request::AuthenticatorRequestData::QueryBandwidth(pub_key) + } + } + } +} + +impl From for v3::registration::InitMessage { + fn from(init_msg: v2::registration::InitMessage) -> Self { + Self { + pub_key: init_msg.pub_key, + } + } +} + +impl From> for Box { + fn from(gw_client: Box) -> Self { + Box::new(v3::registration::FinalMessage { + gateway_client: gw_client.gateway_client.into(), + credential: gw_client.credential, + }) + } +} + +impl From for v3::registration::GatewayClient { + fn from(gw_client: v2::registration::GatewayClient) -> Self { + Self { + pub_key: gw_client.pub_key, + private_ip: gw_client.private_ip, + mac: gw_client.mac.into(), + } + } +} + +impl From for v2::registration::GatewayClient { + fn from(gw_client: v3::registration::GatewayClient) -> Self { + Self { + pub_key: gw_client.pub_key, + private_ip: gw_client.private_ip, + mac: gw_client.mac.into(), + } + } +} + +impl From for v3::registration::ClientMac { + fn from(mac: v2::registration::ClientMac) -> Self { + Self::new(mac.to_vec()) + } +} + +impl From for v2::registration::ClientMac { + fn from(mac: v3::registration::ClientMac) -> Self { + Self::new(mac.to_vec()) + } +} + +impl TryFrom for v2::response::AuthenticatorResponse { + type Error = crate::Error; + + fn try_from( + authenticator_response: v3::response::AuthenticatorResponse, + ) -> Result { + Ok(Self { + data: authenticator_response.data.try_into()?, + reply_to: authenticator_response.reply_to, + protocol: authenticator_response.protocol, + }) + } +} + +impl From for v3::response::AuthenticatorResponse { + fn from(value: v2::response::AuthenticatorResponse) -> Self { + Self { + protocol: value.protocol, + data: value.data.into(), + reply_to: value.reply_to, + } + } +} + +impl TryFrom for v2::response::AuthenticatorResponseData { + type Error = crate::Error; + + fn try_from( + authenticator_response_data: v3::response::AuthenticatorResponseData, + ) -> Result { + match authenticator_response_data { + v3::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Ok( + v2::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response.into(), + ), + ), + v3::response::AuthenticatorResponseData::Registered(registered_response) => Ok( + v2::response::AuthenticatorResponseData::Registered(registered_response.into()), + ), + v3::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Ok(v2::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response.into(), + )), + v3::response::AuthenticatorResponseData::TopUpBandwidth(_) => { + Err(Self::Error::Conversion( + "a v2 request couldn't produce a v3 only type of response".to_string(), + )) + } + } + } +} + +impl From for v3::response::AuthenticatorResponseData { + fn from(value: v2::response::AuthenticatorResponseData) -> Self { + match value { + v2::response::AuthenticatorResponseData::PendingRegistration( + pending_registration_response, + ) => Self::PendingRegistration(pending_registration_response.into()), + v2::response::AuthenticatorResponseData::Registered(registered_response) => { + Self::Registered(registered_response.into()) + } + v2::response::AuthenticatorResponseData::RemainingBandwidth( + remaining_bandwidth_response, + ) => Self::RemainingBandwidth(remaining_bandwidth_response.into()), + } + } +} + +impl From for v2::response::PendingRegistrationResponse { + fn from(value: v3::response::PendingRegistrationResponse) -> Self { + Self { + request_id: value.request_id, + reply_to: value.reply_to, + reply: value.reply.into(), + } + } +} + +impl From for v3::response::PendingRegistrationResponse { + fn from(value: v2::response::PendingRegistrationResponse) -> Self { + Self { + request_id: value.request_id, + reply_to: value.reply_to, + reply: value.reply.into(), + } + } +} + +impl From for v2::response::RegisteredResponse { + fn from(value: v3::response::RegisteredResponse) -> Self { + Self { + request_id: value.request_id, + reply_to: value.reply_to, + reply: value.reply.into(), + } + } +} + +impl From for v3::response::RegisteredResponse { + fn from(value: v2::response::RegisteredResponse) -> Self { + Self { + request_id: value.request_id, + reply_to: value.reply_to, + reply: value.reply.into(), + } + } +} + +impl From for v2::response::RemainingBandwidthResponse { + fn from(value: v3::response::RemainingBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply_to: value.reply_to, + reply: value.reply.map(Into::into), + } + } +} + +impl From for v3::response::RemainingBandwidthResponse { + fn from(value: v2::response::RemainingBandwidthResponse) -> Self { + Self { + request_id: value.request_id, + reply_to: value.reply_to, + reply: value.reply.map(Into::into), + } + } +} + +impl From for v2::registration::RegistrationData { + fn from(value: v3::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v3::registration::RegistrationData { + fn from(value: v2::registration::RegistrationData) -> Self { + Self { + nonce: value.nonce, + gateway_data: value.gateway_data.into(), + wg_port: value.wg_port, + } + } +} + +impl From for v2::registration::RegistredData { + fn from(value: v3::registration::RegistredData) -> Self { + Self { + pub_key: value.pub_key, + private_ip: value.private_ip, + wg_port: value.wg_port, + } + } +} + +impl From for v3::registration::RegistredData { + fn from(value: v2::registration::RegistredData) -> Self { + Self { + pub_key: value.pub_key, + private_ip: value.private_ip, + wg_port: value.wg_port, + } + } +} + +impl From for v2::registration::RemainingBandwidthData { + fn from(value: v3::registration::RemainingBandwidthData) -> Self { + Self { + available_bandwidth: value.available_bandwidth, + } + } +} + +impl From for v3::registration::RemainingBandwidthData { + fn from(value: v2::registration::RemainingBandwidthData) -> Self { + Self { + available_bandwidth: value.available_bandwidth, + } + } +} diff --git a/common/authenticator-requests/src/v3/mod.rs b/common/authenticator-requests/src/v3/mod.rs new file mode 100644 index 0000000000..1a060367d7 --- /dev/null +++ b/common/authenticator-requests/src/v3/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod conversion; +pub mod registration; +pub mod request; +pub mod response; +pub mod topup; + +pub const VERSION: u8 = 3; diff --git a/common/authenticator-requests/src/v3/registration.rs b/common/authenticator-requests/src/v3/registration.rs new file mode 100644 index 0000000000..37234f7e1f --- /dev/null +++ b/common/authenticator-requests/src/v3/registration.rs @@ -0,0 +1,227 @@ +// -2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::Error; +use base64::{engine::general_purpose, Engine}; +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::net::IpAddr; +use std::time::SystemTime; +use std::{fmt, ops::Deref, str::FromStr}; + +#[cfg(feature = "verify")] +use hmac::{Hmac, Mac}; +#[cfg(feature = "verify")] +use nym_crypto::asymmetric::encryption::PrivateKey; +#[cfg(feature = "verify")] +use sha2::Sha256; + +pub type PendingRegistrations = HashMap; +pub type PrivateIPs = HashMap; + +#[cfg(feature = "verify")] +pub type HmacSha256 = Hmac; + +pub type Nonce = u64; +pub type Taken = Option; + +pub const BANDWIDTH_CAP_PER_DAY: u64 = 250 * 1024 * 1024 * 1024; // 250 GB + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct InitMessage { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, +} + +impl InitMessage { + pub fn new(pub_key: PeerPublicKey) -> Self { + InitMessage { pub_key } + } +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct FinalMessage { + /// Gateway client data + pub gateway_client: GatewayClient, + + /// Ecash credential + pub credential: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RegistrationData { + pub nonce: u64, + pub gateway_data: GatewayClient, + pub wg_port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RegistredData { + pub pub_key: PeerPublicKey, + pub private_ip: IpAddr, + pub wg_port: u16, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RemainingBandwidthData { + pub available_bandwidth: i64, +} + +/// Client that wants to register sends its PublicKey bytes mac digest encrypted with a DH shared secret. +/// Gateway/Nym node can then verify pub_key payload using the same process +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct GatewayClient { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, + + /// Assigned private IP + pub private_ip: IpAddr, + + /// Sha256 hmac on the data (alongside the prior nonce) + pub mac: ClientMac, +} + +impl GatewayClient { + #[cfg(feature = "verify")] + pub fn new( + local_secret: &PrivateKey, + remote_public: x25519_dalek::PublicKey, + private_ip: IpAddr, + nonce: u64, + ) -> Self { + // convert from 1.0 x25519-dalek private key into 2.0 x25519-dalek + #[allow(clippy::expect_used)] + let static_secret = x25519_dalek::StaticSecret::from(local_secret.to_bytes()); + let local_public: x25519_dalek::PublicKey = (&static_secret).into(); + + let dh = static_secret.diffie_hellman(&remote_public); + + // TODO: change that to use our nym_crypto::hmac module instead + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(dh.as_bytes()) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(local_public.as_bytes()); + mac.update(private_ip.to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + GatewayClient { + pub_key: PeerPublicKey::new(local_public), + private_ip, + mac: ClientMac(mac.finalize().into_bytes().to_vec()), + } + } + + // Reusable secret should be gateways Wireguard PK + // Client should perform this step when generating its payload, using its own WG PK + #[cfg(feature = "verify")] + pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> { + // convert from 1.0 x25519-dalek private key into 2.0 x25519-dalek + #[allow(clippy::expect_used)] + let static_secret = x25519_dalek::StaticSecret::from(gateway_key.to_bytes()); + + let dh = static_secret.diffie_hellman(&self.pub_key); + + // TODO: change that to use our nym_crypto::hmac module instead + #[allow(clippy::expect_used)] + let mut mac = HmacSha256::new_from_slice(dh.as_bytes()) + .expect("x25519 shared secret is always 32 bytes long"); + + mac.update(self.pub_key.as_bytes()); + mac.update(self.private_ip.to_string().as_bytes()); + mac.update(&nonce.to_le_bytes()); + + mac.verify_slice(&self.mac) + .map_err(|source| Error::FailedClientMacVerification { + client: self.pub_key.to_string(), + source, + }) + } + + pub fn pub_key(&self) -> PeerPublicKey { + self.pub_key + } +} + +// TODO: change the inner type into generic array of size HmacSha256::OutputSize +// TODO2: rely on our internal crypto/hmac +#[derive(Debug, Clone)] +pub struct ClientMac(Vec); + +impl fmt::Display for ClientMac { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", general_purpose::STANDARD.encode(&self.0)) + } +} + +impl ClientMac { + #[allow(dead_code)] + pub fn new(mac: Vec) -> Self { + ClientMac(mac) + } +} + +impl Deref for ClientMac { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl FromStr for ClientMac { + type Err = Error; + + fn from_str(s: &str) -> Result { + let mac_bytes: Vec = + general_purpose::STANDARD + .decode(s) + .map_err(|source| Error::MalformedClientMac { + mac: s.to_string(), + source, + })?; + + Ok(ClientMac(mac_bytes)) + } +} + +impl Serialize for ClientMac { + fn serialize(&self, serializer: S) -> Result { + let encoded_key = general_purpose::STANDARD.encode(self.0.clone()); + serializer.serialize_str(&encoded_key) + } +} + +impl<'de> Deserialize<'de> for ClientMac { + fn deserialize>(deserializer: D) -> Result { + let encoded_key = String::deserialize(deserializer)?; + ClientMac::from_str(&encoded_key).map_err(serde::de::Error::custom) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use nym_crypto::asymmetric::encryption; + + #[test] + #[cfg(feature = "verify")] + fn client_request_roundtrip() { + let mut rng = rand::thread_rng(); + + let gateway_key_pair = encryption::KeyPair::new(&mut rng); + let client_key_pair = encryption::KeyPair::new(&mut rng); + + let nonce = 1234567890; + + let client = GatewayClient::new( + client_key_pair.private_key(), + x25519_dalek::PublicKey::from(gateway_key_pair.public_key().to_bytes()), + "10.0.0.42".parse().unwrap(), + nonce, + ); + assert!(client.verify(gateway_key_pair.private_key(), nonce).is_ok()) + } +} diff --git a/common/authenticator-requests/src/v3/request.rs b/common/authenticator-requests/src/v3/request.rs new file mode 100644 index 0000000000..32db17aed2 --- /dev/null +++ b/common/authenticator-requests/src/v3/request.rs @@ -0,0 +1,136 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::{ + registration::{FinalMessage, InitMessage}, + topup::TopUpMessage, +}; +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; +use nym_sphinx::addressing::Recipient; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; + +use crate::make_bincode_serializer; + +use super::VERSION; + +fn generate_random() -> u64 { + use rand::RngCore; + let mut rng = rand::rngs::OsRng; + rng.next_u64() +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuthenticatorRequest { + pub protocol: Protocol, + pub data: AuthenticatorRequestData, + pub reply_to: Recipient, + pub request_id: u64, +} + +impl AuthenticatorRequest { + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } + + pub fn new_initial_request(init_message: InitMessage, reply_to: Recipient) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorRequestData::Initial(init_message), + reply_to, + request_id, + }, + request_id, + ) + } + + pub fn new_final_request(final_message: FinalMessage, reply_to: Recipient) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorRequestData::Final(Box::new(final_message)), + reply_to, + request_id, + }, + request_id, + ) + } + + pub fn new_query_request(peer_public_key: PeerPublicKey, reply_to: Recipient) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorRequestData::QueryBandwidth(peer_public_key), + reply_to, + request_id, + }, + request_id, + ) + } + + pub fn new_topup_request(top_up_message: TopUpMessage, reply_to: Recipient) -> (Self, u64) { + let request_id = generate_random(); + ( + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorRequestData::TopUpBandwidth(Box::new(top_up_message)), + reply_to, + request_id, + }, + request_id, + ) + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum AuthenticatorRequestData { + Initial(InitMessage), + Final(Box), + QueryBandwidth(PeerPublicKey), + TopUpBandwidth(Box), +} + +#[cfg(test)] +mod tests { + use super::*; + use std::str::FromStr; + + #[test] + fn check_first_bytes_protocol() { + let version = 2; + let data = AuthenticatorRequest { + protocol: Protocol { version, service_provider_type: ServiceProviderType::Authenticator }, + data: AuthenticatorRequestData::Initial(InitMessage::new( + PeerPublicKey::from_str("yvNUDpT5l7W/xDhiu6HkqTHDQwbs/B3J5UrLmORl1EQ=").unwrap(), + )), + reply_to: Recipient::try_from_base58_string("D1rrpsysCGCYXy9saP8y3kmNpGtJZUXN9SvFoUcqAsM9.9Ssso1ea5NfkbMASdiseDSjTN1fSWda5SgEVjdSN4CvV@GJqd3ZxpXWSNxTfx7B1pPtswpetH4LnJdFeLeuY5KUuN").unwrap(), + request_id: 1, + }; + let bytes = *data.to_bytes().unwrap().first_chunk::<2>().unwrap(); + assert_eq!(bytes, [version, ServiceProviderType::Authenticator as u8]); + } +} diff --git a/common/authenticator-requests/src/v3/response.rs b/common/authenticator-requests/src/v3/response.rs new file mode 100644 index 0000000000..370fc64671 --- /dev/null +++ b/common/authenticator-requests/src/v3/response.rs @@ -0,0 +1,157 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData}; +use nym_service_provider_requests_common::{Protocol, ServiceProviderType}; +use nym_sphinx::addressing::Recipient; +use serde::{Deserialize, Serialize}; + +use crate::make_bincode_serializer; + +use super::VERSION; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct AuthenticatorResponse { + pub protocol: Protocol, + pub data: AuthenticatorResponseData, + pub reply_to: Recipient, +} + +impl AuthenticatorResponse { + pub fn new_pending_registration_success( + registration_data: RegistrationData, + request_id: u64, + reply_to: Recipient, + ) -> Self { + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorResponseData::PendingRegistration(PendingRegistrationResponse { + reply: registration_data, + reply_to, + request_id, + }), + reply_to, + } + } + + pub fn new_registered( + registred_data: RegistredData, + reply_to: Recipient, + request_id: u64, + ) -> Self { + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorResponseData::Registered(RegisteredResponse { + reply: registred_data, + reply_to, + request_id, + }), + reply_to, + } + } + + pub fn new_remaining_bandwidth( + remaining_bandwidth_data: Option, + reply_to: Recipient, + request_id: u64, + ) -> Self { + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorResponseData::RemainingBandwidth(RemainingBandwidthResponse { + reply: remaining_bandwidth_data, + reply_to, + request_id, + }), + reply_to, + } + } + + pub fn new_topup_bandwidth( + remaining_bandwidth_data: RemainingBandwidthData, + reply_to: Recipient, + request_id: u64, + ) -> Self { + Self { + protocol: Protocol { + service_provider_type: ServiceProviderType::Authenticator, + version: VERSION, + }, + data: AuthenticatorResponseData::TopUpBandwidth(TopUpBandwidthResponse { + reply: remaining_bandwidth_data, + reply_to, + request_id, + }), + reply_to, + } + } + + pub fn recipient(&self) -> Recipient { + self.reply_to + } + + pub fn to_bytes(&self) -> Result, bincode::Error> { + use bincode::Options; + make_bincode_serializer().serialize(self) + } + + pub fn from_reconstructed_message( + message: &nym_sphinx::receiver::ReconstructedMessage, + ) -> Result { + use bincode::Options; + make_bincode_serializer().deserialize(&message.message) + } + + pub fn id(&self) -> Option { + match &self.data { + AuthenticatorResponseData::PendingRegistration(response) => Some(response.request_id), + AuthenticatorResponseData::Registered(response) => Some(response.request_id), + AuthenticatorResponseData::RemainingBandwidth(response) => Some(response.request_id), + AuthenticatorResponseData::TopUpBandwidth(response) => Some(response.request_id), + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum AuthenticatorResponseData { + PendingRegistration(PendingRegistrationResponse), + Registered(RegisteredResponse), + RemainingBandwidth(RemainingBandwidthResponse), + TopUpBandwidth(TopUpBandwidthResponse), +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct PendingRegistrationResponse { + pub request_id: u64, + pub reply_to: Recipient, + pub reply: RegistrationData, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RegisteredResponse { + pub request_id: u64, + pub reply_to: Recipient, + pub reply: RegistredData, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RemainingBandwidthResponse { + pub request_id: u64, + pub reply_to: Recipient, + pub reply: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct TopUpBandwidthResponse { + pub request_id: u64, + pub reply_to: Recipient, + pub reply: RemainingBandwidthData, +} diff --git a/common/authenticator-requests/src/v3/topup.rs b/common/authenticator-requests/src/v3/topup.rs new file mode 100644 index 0000000000..31a61a0659 --- /dev/null +++ b/common/authenticator-requests/src/v3/topup.rs @@ -0,0 +1,15 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_credentials_interface::CredentialSpendingData; +use nym_wireguard_types::PeerPublicKey; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TopUpMessage { + /// Base64 encoded x25519 public key + pub pub_key: PeerPublicKey, + + /// Ecash credential + pub credential: CredentialSpendingData, +} diff --git a/common/bandwidth-controller/src/lib.rs b/common/bandwidth-controller/src/lib.rs index a164d3df60..a9b15db1fb 100644 --- a/common/bandwidth-controller/src/lib.rs +++ b/common/bandwidth-controller/src/lib.rs @@ -16,7 +16,7 @@ use nym_credential_storage::models::RetrievedTicketbook; use nym_credential_storage::storage::Storage; use nym_credentials::ecash::bandwidth::CredentialSpendingData; use nym_credentials_interface::{ - AnnotatedCoinIndexSignature, AnnotatedExpirationDateSignature, VerificationKeyAuth, + AnnotatedCoinIndexSignature, AnnotatedExpirationDateSignature, TicketType, VerificationKeyAuth, }; use nym_ecash_time::Date; use nym_validator_client::nym_api::EpochId; @@ -64,9 +64,10 @@ impl BandwidthController { BandwidthController { storage, client } } - /// Tries to retrieve one of the stored, unused credentials that hasn't yet expired. + /// Tries to retrieve one of the stored, unused credentials for the given type that hasn't yet expired. pub async fn get_next_usable_ticketbook( &self, + ticketbook_type: TicketType, tickets: u32, ) -> Result where @@ -74,7 +75,7 @@ impl BandwidthController { { let Some(ticketbook) = self .storage - .get_next_unspent_usable_ticketbook(tickets) + .get_next_unspent_usable_ticketbook(ticketbook_type.to_string(), tickets) .await .map_err(BandwidthControllerError::credential_storage_error)? else { @@ -181,6 +182,7 @@ impl BandwidthController { pub async fn prepare_ecash_ticket( &self, + ticketbook_type: TicketType, provider_pk: [u8; 32], tickets_to_spend: u32, ) -> Result @@ -188,7 +190,9 @@ impl BandwidthController { C: DkgQueryClient + Sync + Send, ::StorageError: Send + Sync + 'static, { - let retrieved_ticketbook = self.get_next_usable_ticketbook(tickets_to_spend).await?; + let retrieved_ticketbook = self + .get_next_usable_ticketbook(ticketbook_type, tickets_to_spend) + .await?; let ticketbook_id = retrieved_ticketbook.ticketbook_id; let epoch_id = retrieved_ticketbook.ticketbook.epoch_id(); diff --git a/common/bin-common/Cargo.toml b/common/bin-common/Cargo.toml index b63631ddaa..11a2c76f2b 100644 --- a/common/bin-common/Cargo.toml +++ b/common/bin-common/Cargo.toml @@ -45,3 +45,4 @@ tracing = [ "opentelemetry", ] clap = [ "dep:clap", "dep:clap_complete", "dep:clap_complete_fig" ] +models = [] diff --git a/common/bin-common/src/logging/mod.rs b/common/bin-common/src/logging/mod.rs index 250fef793f..bd9a76374b 100644 --- a/common/bin-common/src/logging/mod.rs +++ b/common/bin-common/src/logging/mod.rs @@ -47,6 +47,7 @@ pub fn setup_logging() { #[cfg(feature = "basic_tracing")] pub fn setup_tracing_logger() { let log_builder = tracing_subscriber::fmt() + .with_writer(std::io::stderr) // Use a more compact, abbreviated log format .compact() // Display source code file paths diff --git a/common/client-core/config-types/src/lib.rs b/common/client-core/config-types/src/lib.rs index 4eaba838df..508bc2fca0 100644 --- a/common/client-core/config-types/src/lib.rs +++ b/common/client-core/config-types/src/lib.rs @@ -381,13 +381,20 @@ pub struct Traffic { /// poisson distribution. pub disable_main_poisson_packet_distribution: bool, + /// Specify whether route selection should be determined by the packet header. + pub deterministic_route_selection: bool, + + /// Specify how many times particular packet can be retransmitted + /// None - no limit + pub maximum_number_of_retransmissions: Option, + /// Specifies the packet size used for sent messages. /// Do not override it unless you understand the consequences of that change. pub primary_packet_size: PacketSize, /// Specifies the optional auxiliary packet size for optimizing message streams. /// Note that its use decreases overall anonymity. - /// Do not set it it unless you understand the consequences of that change. + /// Do not set it unless you understand the consequences of that change. pub secondary_packet_size: Option, pub packet_type: PacketType, @@ -412,6 +419,8 @@ impl Default for Traffic { average_packet_delay: DEFAULT_AVERAGE_PACKET_DELAY, message_sending_average_delay: DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY, disable_main_poisson_packet_distribution: false, + deterministic_route_selection: false, + maximum_number_of_retransmissions: None, primary_packet_size: PacketSize::RegularPacket, secondary_packet_size: None, packet_type: PacketType::Mix, diff --git a/common/client-core/config-types/src/old/v5.rs b/common/client-core/config-types/src/old/v5.rs index 0b4dab44c7..e6d4fa26ea 100644 --- a/common/client-core/config-types/src/old/v5.rs +++ b/common/client-core/config-types/src/old/v5.rs @@ -111,6 +111,7 @@ impl From for Config { primary_packet_size: value.debug.traffic.primary_packet_size, secondary_packet_size: value.debug.traffic.secondary_packet_size, packet_type: value.debug.traffic.packet_type, + ..Default::default() }, cover_traffic: CoverTraffic { loop_cover_traffic_average_delay: value diff --git a/common/client-core/gateways-storage/.sqlx/query-06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32.json b/common/client-core/gateways-storage/.sqlx/query-06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32.json new file mode 100644 index 0000000000..627c54341c --- /dev/null +++ b/common/client-core/gateways-storage/.sqlx/query-06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS (SELECT 1 FROM registered_gateway WHERE gateway_id_bs58 = ?) AS 'exists'", + "describe": { + "columns": [ + { + "name": "exists", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + null + ] + }, + "hash": "06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32" +} diff --git a/common/client-core/gateways-storage/.sqlx/query-0e85ec18da67cf4e3df04ad80136571f6e920eb2290f20b1b8c5b0ab4b489985.json b/common/client-core/gateways-storage/.sqlx/query-0e85ec18da67cf4e3df04ad80136571f6e920eb2290f20b1b8c5b0ab4b489985.json new file mode 100644 index 0000000000..61aa248946 --- /dev/null +++ b/common/client-core/gateways-storage/.sqlx/query-0e85ec18da67cf4e3df04ad80136571f6e920eb2290f20b1b8c5b0ab4b489985.json @@ -0,0 +1,44 @@ +{ + "db_name": "SQLite", + "query": "SELECT * FROM remote_gateway_details WHERE gateway_id_bs58 = ?", + "describe": { + "columns": [ + { + "name": "gateway_id_bs58", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "gateway_owner_address", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "gateway_listener", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "derived_aes128_ctr_blake3_hmac_keys_bs58", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "derived_aes256_gcm_siv_key", + "ordinal": 4, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + true, + false, + true, + true + ] + }, + "hash": "0e85ec18da67cf4e3df04ad80136571f6e920eb2290f20b1b8c5b0ab4b489985" +} diff --git a/common/client-core/gateways-storage/.sqlx/query-0f1dfb89f1eb39f4a58787af0f53a7a93afb7e4d2e54e2d38fd79d31c8575a54.json b/common/client-core/gateways-storage/.sqlx/query-0f1dfb89f1eb39f4a58787af0f53a7a93afb7e4d2e54e2d38fd79d31c8575a54.json new file mode 100644 index 0000000000..7e5c8bf6fe --- /dev/null +++ b/common/client-core/gateways-storage/.sqlx/query-0f1dfb89f1eb39f4a58787af0f53a7a93afb7e4d2e54e2d38fd79d31c8575a54.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE remote_gateway_details\n SET\n derived_aes128_ctr_blake3_hmac_keys_bs58 = ?,\n derived_aes256_gcm_siv_key = ?\n WHERE gateway_id_bs58 = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "0f1dfb89f1eb39f4a58787af0f53a7a93afb7e4d2e54e2d38fd79d31c8575a54" +} diff --git a/common/client-core/gateways-storage/.sqlx/query-1da6904e72b5abb9abf75affb13af7974d7795b4cbdba234273345fe161df233.json b/common/client-core/gateways-storage/.sqlx/query-1da6904e72b5abb9abf75affb13af7974d7795b4cbdba234273345fe161df233.json new file mode 100644 index 0000000000..9d4281137b --- /dev/null +++ b/common/client-core/gateways-storage/.sqlx/query-1da6904e72b5abb9abf75affb13af7974d7795b4cbdba234273345fe161df233.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM remote_gateway_details WHERE gateway_id_bs58 = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "1da6904e72b5abb9abf75affb13af7974d7795b4cbdba234273345fe161df233" +} diff --git a/common/client-core/gateways-storage/.sqlx/query-4f78619aca933484cd67cb89a376b2a5bec1c191993ff58f0c71c03e3ef6d92d.json b/common/client-core/gateways-storage/.sqlx/query-4f78619aca933484cd67cb89a376b2a5bec1c191993ff58f0c71c03e3ef6d92d.json new file mode 100644 index 0000000000..eb2dc28e12 --- /dev/null +++ b/common/client-core/gateways-storage/.sqlx/query-4f78619aca933484cd67cb89a376b2a5bec1c191993ff58f0c71c03e3ef6d92d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM custom_gateway_details WHERE gateway_id_bs58 = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "4f78619aca933484cd67cb89a376b2a5bec1c191993ff58f0c71c03e3ef6d92d" +} diff --git a/common/client-core/gateways-storage/.sqlx/query-54f552a9dbe95236f946ac2b6615e03504afa58e345ae16a128629d8e76f0a11.json b/common/client-core/gateways-storage/.sqlx/query-54f552a9dbe95236f946ac2b6615e03504afa58e345ae16a128629d8e76f0a11.json new file mode 100644 index 0000000000..b1d46fd96b --- /dev/null +++ b/common/client-core/gateways-storage/.sqlx/query-54f552a9dbe95236f946ac2b6615e03504afa58e345ae16a128629d8e76f0a11.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT * FROM custom_gateway_details WHERE gateway_id_bs58 = ?", + "describe": { + "columns": [ + { + "name": "gateway_id_bs58", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "data", + "ordinal": 1, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + true + ] + }, + "hash": "54f552a9dbe95236f946ac2b6615e03504afa58e345ae16a128629d8e76f0a11" +} diff --git a/common/client-core/gateways-storage/.sqlx/query-5661cf1ad8bd5ca062e855e1971a8787133ee41814bd3efdd501f9ee0c050f2b.json b/common/client-core/gateways-storage/.sqlx/query-5661cf1ad8bd5ca062e855e1971a8787133ee41814bd3efdd501f9ee0c050f2b.json new file mode 100644 index 0000000000..ad91647c9a --- /dev/null +++ b/common/client-core/gateways-storage/.sqlx/query-5661cf1ad8bd5ca062e855e1971a8787133ee41814bd3efdd501f9ee0c050f2b.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT gateway_id_bs58 FROM registered_gateway", + "describe": { + "columns": [ + { + "name": "gateway_id_bs58", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "5661cf1ad8bd5ca062e855e1971a8787133ee41814bd3efdd501f9ee0c050f2b" +} diff --git a/common/client-core/gateways-storage/.sqlx/query-80476cf2906eb0ecf7f66c16bc5682169b87f488b6927fa67fade6bf5abf7582.json b/common/client-core/gateways-storage/.sqlx/query-80476cf2906eb0ecf7f66c16bc5682169b87f488b6927fa67fade6bf5abf7582.json new file mode 100644 index 0000000000..02b9ef7426 --- /dev/null +++ b/common/client-core/gateways-storage/.sqlx/query-80476cf2906eb0ecf7f66c16bc5682169b87f488b6927fa67fade6bf5abf7582.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE active_gateway SET active_gateway_id_bs58 = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "80476cf2906eb0ecf7f66c16bc5682169b87f488b6927fa67fade6bf5abf7582" +} diff --git a/common/client-core/gateways-storage/.sqlx/query-8909fd329e7e5fb16c4989b15b3d3a12bba1569520e01f6f074178e23d6ee89e.json b/common/client-core/gateways-storage/.sqlx/query-8909fd329e7e5fb16c4989b15b3d3a12bba1569520e01f6f074178e23d6ee89e.json new file mode 100644 index 0000000000..eabf0e5fb9 --- /dev/null +++ b/common/client-core/gateways-storage/.sqlx/query-8909fd329e7e5fb16c4989b15b3d3a12bba1569520e01f6f074178e23d6ee89e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO registered_gateway(gateway_id_bs58, registration_timestamp, gateway_type) \n VALUES (?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "8909fd329e7e5fb16c4989b15b3d3a12bba1569520e01f6f074178e23d6ee89e" +} diff --git a/common/client-core/gateways-storage/.sqlx/query-a6939bea03b10cde810a9a099bd597b4f51092e30a41c4085a8f8668f039f7c0.json b/common/client-core/gateways-storage/.sqlx/query-a6939bea03b10cde810a9a099bd597b4f51092e30a41c4085a8f8668f039f7c0.json new file mode 100644 index 0000000000..b2e77df70a --- /dev/null +++ b/common/client-core/gateways-storage/.sqlx/query-a6939bea03b10cde810a9a099bd597b4f51092e30a41c4085a8f8668f039f7c0.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO remote_gateway_details(gateway_id_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key, gateway_owner_address, gateway_listener)\n VALUES (?, ?, ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "a6939bea03b10cde810a9a099bd597b4f51092e30a41c4085a8f8668f039f7c0" +} diff --git a/common/client-core/gateways-storage/.sqlx/query-b059bc3688b6b7f83f47048db9897720fd4e6f3211bf74030a9638f7bf6738e4.json b/common/client-core/gateways-storage/.sqlx/query-b059bc3688b6b7f83f47048db9897720fd4e6f3211bf74030a9638f7bf6738e4.json new file mode 100644 index 0000000000..646b32adb8 --- /dev/null +++ b/common/client-core/gateways-storage/.sqlx/query-b059bc3688b6b7f83f47048db9897720fd4e6f3211bf74030a9638f7bf6738e4.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO custom_gateway_details(gateway_id_bs58, data) \n VALUES (?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "b059bc3688b6b7f83f47048db9897720fd4e6f3211bf74030a9638f7bf6738e4" +} diff --git a/common/client-core/gateways-storage/.sqlx/query-bf249752f08c283bf5942b6ff48125c24750b523cfcad1e5e9069dbf7050e2a1.json b/common/client-core/gateways-storage/.sqlx/query-bf249752f08c283bf5942b6ff48125c24750b523cfcad1e5e9069dbf7050e2a1.json new file mode 100644 index 0000000000..a1ae46b595 --- /dev/null +++ b/common/client-core/gateways-storage/.sqlx/query-bf249752f08c283bf5942b6ff48125c24750b523cfcad1e5e9069dbf7050e2a1.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT active_gateway_id_bs58 FROM active_gateway", + "describe": { + "columns": [ + { + "name": "active_gateway_id_bs58", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + true + ] + }, + "hash": "bf249752f08c283bf5942b6ff48125c24750b523cfcad1e5e9069dbf7050e2a1" +} diff --git a/common/client-core/gateways-storage/.sqlx/query-f3ebe259e26c05ecdd33bd9085dbb91cd5046a8c9d4434cf085a4fa2ebf03e93.json b/common/client-core/gateways-storage/.sqlx/query-f3ebe259e26c05ecdd33bd9085dbb91cd5046a8c9d4434cf085a4fa2ebf03e93.json new file mode 100644 index 0000000000..e2cfa29a82 --- /dev/null +++ b/common/client-core/gateways-storage/.sqlx/query-f3ebe259e26c05ecdd33bd9085dbb91cd5046a8c9d4434cf085a4fa2ebf03e93.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM registered_gateway WHERE gateway_id_bs58 = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "f3ebe259e26c05ecdd33bd9085dbb91cd5046a8c9d4434cf085a4fa2ebf03e93" +} diff --git a/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs b/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs index 1bf78408f7..4b32c60936 100644 --- a/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs +++ b/common/client-core/gateways-storage/src/backend/fs_backend/manager.rs @@ -29,11 +29,10 @@ impl StorageManager { })?; } - let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + let opts = sqlx::sqlite::SqliteConnectOptions::new() .filename(database_path) - .create_if_missing(true); - - opts.disable_statement_logging(); + .create_if_missing(true) + .disable_statement_logging(); let connection_pool = sqlx::SqlitePool::connect_with(opts) .await @@ -82,7 +81,7 @@ impl StorageManager { sqlx::query!("SELECT EXISTS (SELECT 1 FROM registered_gateway WHERE gateway_id_bs58 = ?) AS 'exists'", gateway_id) .fetch_one(&self.connection_pool) .await - .map(|result| result.exists == 1) + .map(|result| result.exists == Some(1)) } pub(crate) async fn maybe_get_registered_gateway( diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs index cb50d4ba09..6f6753c35f 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs @@ -30,7 +30,8 @@ pub(crate) enum Action { InsertPending(Vec), /// Removes given `PendingAcknowledgement` from the 'shared' state. Also cancels the retransmission timer. - /// Initiated by `AcknowledgementListener` + /// Initiated by `AcknowledgementListener` upon receiving the acknowledgement. Also by `RetransmissionRequestListener` + /// upon deciding to abandon the data. RemovePending(FragmentIdentifier), /// Starts the retransmission timer on given `PendingAcknowledgement` with the `Duration` based on @@ -41,7 +42,7 @@ pub(crate) enum Action { /// Updates the expected delay of given `PendingAcknowledgement` with the new provided `SphinxDelay`. /// Initiated by `RetransmissionRequestListener` - UpdateDelay(FragmentIdentifier, SphinxDelay), + UpdatePendingAck(FragmentIdentifier, SphinxDelay), } impl Action { @@ -57,8 +58,8 @@ impl Action { Action::StartTimer(frag_id) } - pub(crate) fn new_update_delay(frag_id: FragmentIdentifier, delay: SphinxDelay) -> Self { - Action::UpdateDelay(frag_id, delay) + pub(crate) fn new_update_pending_ack(frag_id: FragmentIdentifier, delay: SphinxDelay) -> Self { + Action::UpdatePendingAck(frag_id, delay) } } @@ -135,7 +136,7 @@ impl ActionController { } fn handle_start_timer(&mut self, frag_id: FragmentIdentifier) { - trace!("{} is starting its timer", frag_id); + trace!("{frag_id} is starting its timer"); if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.get_mut(&frag_id) { // the fact that this branch is now POSSIBLE is a sign of a need to refactor this whole @@ -193,7 +194,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_delay(&mut self, frag_id: FragmentIdentifier, delay: SphinxDelay) { + fn handle_update_pending_ack(&mut self, frag_id: FragmentIdentifier, delay: SphinxDelay) { trace!("{} is updating its delay", frag_id); // 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) { @@ -202,7 +203,7 @@ impl ActionController { // reference to this Arc. HOWEVER, before the Action was pushed onto the queue, the reference // was dropped hence this unwrap is safe. let mut inner_data = Arc::try_unwrap(pending_ack_data).unwrap(); - inner_data.update_delay(delay); + inner_data.update_retransmitted(delay); self.pending_acks_data .insert(frag_id, (Arc::new(inner_data), queue_key)); @@ -225,7 +226,7 @@ impl ActionController { // about it. Perhaps just reschedule it at later point? let frag_id = expired_ack.into_inner(); - trace!("{} has expired", frag_id); + trace!("{frag_id} has expired"); if let Some((pending_ack_data, queue_key)) = self.pending_acks_data.get_mut(&frag_id) { if queue_key.is_none() { @@ -258,7 +259,9 @@ impl ActionController { Action::InsertPending(pending_acks) => self.handle_insert(pending_acks), Action::RemovePending(frag_id) => self.handle_remove(frag_id), Action::StartTimer(frag_id) => self.handle_start_timer(frag_id), - Action::UpdateDelay(frag_id, delay) => self.handle_update_delay(frag_id, delay), + Action::UpdatePendingAck(frag_id, delay) => { + self.handle_update_pending_ack(frag_id, delay) + } } } diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs index ec35429b9f..e3810a91ab 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/mod.rs @@ -71,6 +71,7 @@ pub(crate) struct PendingAcknowledgement { delay: SphinxDelay, destination: PacketDestination, mix_hops: Option, + retransmissions: u32, } impl PendingAcknowledgement { @@ -86,6 +87,7 @@ impl PendingAcknowledgement { delay, destination: PacketDestination::KnownRecipient(recipient.into()), mix_hops, + retransmissions: 0, } } @@ -105,6 +107,7 @@ impl PendingAcknowledgement { // Messages sent using SURBs are using the number of mix hops set by the recipient when // they provided the SURBs, so it doesn't make sense to include it here. mix_hops: None, + retransmissions: 0, } } @@ -116,8 +119,9 @@ impl PendingAcknowledgement { self.message_chunk.clone() } - fn update_delay(&mut self, new_delay: SphinxDelay) { + fn update_retransmitted(&mut self, new_delay: SphinxDelay) { self.delay = new_delay; + self.retransmissions += 1; } } @@ -163,6 +167,9 @@ impl AcknowledgementControllerConnectors { /// Configurable parameters of the `AcknowledgementController` pub(super) struct Config { + /// Specify how many times particular packet can be retransmitted + maximum_retransmissions: Option, + /// Given ack timeout in the form a * BASE_DELAY + b, it specifies the additive part `b` ack_wait_addition: Duration, @@ -174,8 +181,13 @@ pub(super) struct Config { } impl Config { - pub(super) fn new(ack_wait_addition: Duration, ack_wait_multiplier: f64) -> Self { + pub(super) fn new( + maximum_retransmissions: Option, + ack_wait_addition: Duration, + ack_wait_multiplier: f64, + ) -> Self { Config { + maximum_retransmissions, ack_wait_addition, ack_wait_multiplier, packet_size: Default::default(), @@ -238,6 +250,7 @@ where // will listen for any ack timeouts and trigger retransmission let retransmission_request_listener = RetransmissionRequestListener::new( + config.maximum_retransmissions, connectors.ack_action_sender.clone(), message_handler, retransmission_rx, diff --git a/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs b/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs index 1d305ff5ff..6eda1c8adc 100644 --- a/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs +++ b/common/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs @@ -20,6 +20,7 @@ use std::sync::{Arc, Weak}; // responsible for packet retransmission upon fired timer pub(super) struct RetransmissionRequestListener { + maximum_retransmissions: Option, action_sender: AckActionSender, message_handler: MessageHandler, request_receiver: RetransmissionRequestReceiver, @@ -31,12 +32,14 @@ where R: CryptoRng + Rng, { pub(super) fn new( + maximum_retransmissions: Option, action_sender: AckActionSender, message_handler: MessageHandler, request_receiver: RetransmissionRequestReceiver, reply_controller_sender: ReplyControllerSender, ) -> Self { RetransmissionRequestListener { + maximum_retransmissions, action_sender, message_handler, request_receiver, @@ -77,6 +80,18 @@ where } }; + let frag_id = timed_out_ack.message_chunk.fragment_identifier(); + + if let Some(limit) = self.maximum_retransmissions { + if timed_out_ack.retransmissions >= limit { + warn!("reached maximum number of allowed retransmissions for the packet"); + self.action_sender + .unbounded_send(Action::new_remove(frag_id)) + .unwrap(); + return; + } + } + let maybe_prepared_fragment = match &timed_out_ack.destination { PacketDestination::Anonymous { recipient_tag, @@ -101,8 +116,6 @@ where } }; - let frag_id = timed_out_ack.message_chunk.fragment_identifier(); - let prepared_fragment = match maybe_prepared_fragment { Ok(prepared_fragment) => prepared_fragment, Err(err) => { @@ -136,7 +149,7 @@ where // with the additional poisson delay. // And since Actions are executed in order `UpdateTimer` will HAVE TO be executed before `StartTimer` self.action_sender - .unbounded_send(Action::new_update_delay(frag_id, new_delay)) + .unbounded_send(Action::new_update_pending_ack(frag_id, new_delay)) .unwrap(); // send to `OutQueueControl` to eventually send to the mix network diff --git a/common/client-core/src/client/real_messages_control/message_handler.rs b/common/client-core/src/client/real_messages_control/message_handler.rs index 8262e5b996..75cca06f76 100644 --- a/common/client-core/src/client/real_messages_control/message_handler.rs +++ b/common/client-core/src/client/real_messages_control/message_handler.rs @@ -91,6 +91,9 @@ pub(crate) struct Config { /// and surb-based are going to be sent. sender_address: Recipient, + /// Specify whether route selection should be determined by the packet header. + deterministic_route_selection: bool, + /// Average delay a data packet is going to get delay at a single mixnode. average_packet_delay: Duration, @@ -114,10 +117,12 @@ impl Config { sender_address: Recipient, average_packet_delay: Duration, average_ack_delay: Duration, + deterministic_route_selection: bool, ) -> Self { Config { ack_key, sender_address, + deterministic_route_selection, average_packet_delay, average_ack_delay, num_mix_hops: DEFAULT_NUM_MIX_HOPS, @@ -176,6 +181,7 @@ where { let message_preparer = MessagePreparer::new( rng, + config.deterministic_route_selection, config.sender_address, config.average_packet_delay, config.average_ack_delay, @@ -634,7 +640,7 @@ where pub(crate) fn update_ack_delay(&self, id: FragmentIdentifier, new_delay: Delay) { self.action_sender - .unbounded_send(Action::UpdateDelay(id, new_delay)) + .unbounded_send(Action::UpdatePendingAck(id, new_delay)) .expect("action control task has died") } diff --git a/common/client-core/src/client/real_messages_control/mod.rs b/common/client-core/src/client/real_messages_control/mod.rs index d4914f1440..fa7da0f99b 100644 --- a/common/client-core/src/client/real_messages_control/mod.rs +++ b/common/client-core/src/client/real_messages_control/mod.rs @@ -65,6 +65,7 @@ pub struct Config { impl<'a> From<&'a Config> for acknowledgement_control::Config { fn from(cfg: &'a Config) -> Self { acknowledgement_control::Config::new( + cfg.traffic.maximum_number_of_retransmissions, cfg.acks.ack_wait_addition, cfg.acks.ack_wait_multiplier, ) @@ -97,6 +98,7 @@ impl<'a> From<&'a Config> for message_handler::Config { cfg.self_recipient, cfg.traffic.average_packet_delay, cfg.acks.average_ack_delay, + cfg.traffic.deterministic_route_selection, ) .with_custom_primary_packet_size(cfg.traffic.primary_packet_size) .with_custom_secondary_packet_size(cfg.traffic.secondary_packet_size) diff --git a/common/client-core/src/client/topology_control/geo_aware_provider.rs b/common/client-core/src/client/topology_control/geo_aware_provider.rs index 96d52fd8d5..7e961bb8d2 100644 --- a/common/client-core/src/client/topology_control/geo_aware_provider.rs +++ b/common/client-core/src/client/topology_control/geo_aware_provider.rs @@ -3,11 +3,11 @@ use log::{debug, error}; use nym_explorer_client::{ExplorerClient, PrettyDetailedMixNodeBond}; use nym_network_defaults::var_names::EXPLORER_API; use nym_topology::{ - nym_topology_from_detailed, + nym_topology_from_basic_info, provider_trait::{async_trait, TopologyProvider}, NymTopology, }; -use nym_validator_client::client::MixId; +use nym_validator_client::client::NodeId; use rand::{prelude::SliceRandom, thread_rng}; use std::collections::HashMap; use tap::TapOptional; @@ -39,10 +39,10 @@ fn create_explorer_client() -> Option { fn group_mixnodes_by_country_code( mixnodes: Vec, -) -> HashMap> { +) -> HashMap> { mixnodes .into_iter() - .fold(HashMap::>::new(), |mut acc, m| { + .fold(HashMap::>::new(), |mut acc, m| { if let Some(ref location) = m.location { let country_code = location.two_letter_iso_country_code.clone(); let group_code = CountryGroup::new(country_code.as_str()); @@ -53,7 +53,7 @@ fn group_mixnodes_by_country_code( }) } -fn log_mixnode_distribution(mixnodes: &HashMap>) { +fn log_mixnode_distribution(mixnodes: &HashMap>) { let mixnode_distribution = mixnodes .iter() .map(|(k, v)| format!("{}: {}", k, v.len())) @@ -110,7 +110,11 @@ impl GeoAwareTopologyProvider { } async fn get_topology(&self) -> Option { - let mixnodes = match self.validator_client.get_cached_active_mixnodes().await { + let mixnodes = match self + .validator_client + .get_all_basic_active_mixing_assigned_nodes(Some(self.client_version.clone())) + .await + { Err(err) => { error!("failed to get network mixnodes - {err}"); return None; @@ -118,7 +122,11 @@ impl GeoAwareTopologyProvider { Ok(mixes) => mixes, }; - let gateways = match self.validator_client.get_cached_gateways().await { + let gateways = match self + .validator_client + .get_all_basic_entry_assigned_nodes(Some(self.client_version.clone())) + .await + { Err(err) => { error!("failed to get network gateways - {err}"); return None; @@ -182,11 +190,10 @@ impl GeoAwareTopologyProvider { let mixnodes = mixnodes .into_iter() - .filter(|m| filtered_mixnode_ids.contains(&m.mix_id())) + .filter(|m| filtered_mixnode_ids.contains(&m.node_id)) .collect::>(); - let topology = nym_topology_from_detailed(mixnodes, gateways) - .filter_system_version(&self.client_version); + let topology = nym_topology_from_basic_info(&mixnodes, &gateways); // TODO: return real error type check_layer_integrity(topology.clone()).ok()?; diff --git a/common/client-core/src/client/topology_control/mod.rs b/common/client-core/src/client/topology_control/mod.rs index bbf32f377c..4e60278a22 100644 --- a/common/client-core/src/client/topology_control/mod.rs +++ b/common/client-core/src/client/topology_control/mod.rs @@ -6,7 +6,6 @@ pub(crate) use accessor::{TopologyAccessor, TopologyReadPermit}; use futures::StreamExt; use log::*; use nym_sphinx::addressing::nodes::NodeIdentity; -use nym_topology::provider_trait::TopologyProvider; use nym_topology::NymTopologyError; use std::time::Duration; @@ -18,7 +17,11 @@ use wasmtimer::tokio::sleep; mod accessor; pub mod geo_aware_provider; -pub(crate) mod nym_api_provider; +pub mod nym_api_provider; + +pub use geo_aware_provider::GeoAwareTopologyProvider; +pub use nym_api_provider::{Config as NymApiTopologyProviderConfig, NymApiTopologyProvider}; +pub use nym_topology::provider_trait::TopologyProvider; // TODO: move it to config later const MAX_FAILURE_COUNT: usize = 10; diff --git a/common/client-core/src/client/topology_control/nym_api_provider.rs b/common/client-core/src/client/topology_control/nym_api_provider.rs index 3a00f462bd..7734ea7461 100644 --- a/common/client-core/src/client/topology_control/nym_api_provider.rs +++ b/common/client-core/src/client/topology_control/nym_api_provider.rs @@ -14,9 +14,10 @@ use url::Url; pub const DEFAULT_MIN_MIXNODE_PERFORMANCE: u8 = 50; pub const DEFAULT_MIN_GATEWAY_PERFORMANCE: u8 = 50; -pub(crate) struct Config { - pub(crate) min_mixnode_performance: u8, - pub(crate) min_gateway_performance: u8, +#[derive(Debug)] +pub struct Config { + pub min_mixnode_performance: u8, + pub min_gateway_performance: u8, } impl Default for Config { @@ -29,7 +30,7 @@ impl Default for Config { } } -pub(crate) struct NymApiTopologyProvider { +pub struct NymApiTopologyProvider { config: Config, validator_client: nym_validator_client::client::NymApiClient, @@ -40,7 +41,7 @@ pub(crate) struct NymApiTopologyProvider { } impl NymApiTopologyProvider { - pub(crate) fn new( + pub fn new( config: Config, mut nym_api_urls: Vec, client_version: String, @@ -98,7 +99,7 @@ impl NymApiTopologyProvider { async fn get_current_compatible_topology(&mut self) -> Option { let mixnodes = match self .validator_client - .get_basic_mixnodes(Some(self.client_version.clone())) + .get_all_basic_active_mixing_assigned_nodes(Some(self.client_version.clone())) .await { Err(err) => { @@ -110,7 +111,7 @@ impl NymApiTopologyProvider { let gateways = match self .validator_client - .get_basic_gateways(Some(self.client_version.clone())) + .get_all_basic_entry_assigned_nodes(Some(self.client_version.clone())) .await { Err(err) => { @@ -134,7 +135,6 @@ impl NymApiTopologyProvider { g.performance.round_to_integer() >= self.config.min_gateway_performance }), ); - if let Err(err) = self.check_layer_distribution(&topology) { warn!("The current filtered active topology has extremely skewed layer distribution. It cannot be used: {err}"); self.use_next_nym_api(); diff --git a/common/client-core/src/error.rs b/common/client-core/src/error.rs index 653256df1f..4d412902e0 100644 --- a/common/client-core/src/error.rs +++ b/common/client-core/src/error.rs @@ -187,16 +187,6 @@ pub enum ClientCoreError { source: Ed25519RecoveryError, }, - #[error("the account owner of gateway {gateway_id} ({raw_owner}) is malformed: {err}")] - MalformedGatewayOwnerAccountAddress { - gateway_id: String, - - raw_owner: String, - - // just use the string formatting as opposed to underlying type to avoid having to import cosmrs - err: String, - }, - #[error( "the listening address of gateway {gateway_id} ({raw_listener}) is malformed: {source}" )] diff --git a/common/client-core/src/init/helpers.rs b/common/client-core/src/init/helpers.rs index c6d17fc16d..3f6a390bd0 100644 --- a/common/client-core/src/init/helpers.rs +++ b/common/client-core/src/init/helpers.rs @@ -7,7 +7,7 @@ use futures::{SinkExt, StreamExt}; use log::{debug, info, trace, warn}; use nym_crypto::asymmetric::identity; use nym_gateway_client::GatewayClient; -use nym_topology::{filter::VersionFilterable, gateway, mix}; +use nym_topology::{gateway, mix}; use nym_validator_client::client::IdentityKeyRef; use nym_validator_client::UserAgent; use rand::{seq::SliceRandom, Rng}; @@ -53,7 +53,7 @@ pub trait ConnectableGateway { fn is_wss(&self) -> bool; } -impl ConnectableGateway for gateway::Node { +impl ConnectableGateway for gateway::LegacyNode { fn identity(&self) -> &identity::PublicKey { self.identity() } @@ -82,7 +82,7 @@ pub async fn current_gateways( rng: &mut R, nym_apis: &[Url], user_agent: Option, -) -> Result, ClientCoreError> { +) -> Result, ClientCoreError> { let nym_api = nym_apis .choose(rng) .ok_or(ClientCoreError::ListOfNymApisIsEmpty)?; @@ -94,31 +94,26 @@ pub async fn current_gateways( log::debug!("Fetching list of gateways from: {nym_api}"); - let gateways = client.get_cached_described_gateways().await?; + let gateways = client.get_all_basic_entry_assigned_nodes(None).await?; log::debug!("Found {} gateways", gateways.len()); log::trace!("Gateways: {:#?}", gateways); let valid_gateways = gateways - .into_iter() + .iter() .filter_map(|gateway| gateway.try_into().ok()) - .collect::>(); - log::debug!("Ater checking validity: {}", valid_gateways.len()); + .collect::>(); + log::debug!("After checking validity: {}", valid_gateways.len()); log::trace!("Valid gateways: {:#?}", valid_gateways); - // we were always filtering by version so I'm not removing that 'feature' - let filtered_gateways = valid_gateways.filter_by_version(env!("CARGO_PKG_VERSION")); - log::debug!("After filtering for version: {}", filtered_gateways.len()); - log::trace!("Filtered gateways: {:#?}", filtered_gateways); - - log::info!("nym-api reports {} valid gateways", filtered_gateways.len()); + log::info!("nym-api reports {} valid gateways", valid_gateways.len()); - Ok(filtered_gateways) + Ok(valid_gateways) } pub async fn current_mixnodes( rng: &mut R, nym_apis: &[Url], -) -> Result, ClientCoreError> { +) -> Result, ClientCoreError> { let nym_api = nym_apis .choose(rng) .ok_or(ClientCoreError::ListOfNymApisIsEmpty)?; @@ -126,15 +121,15 @@ pub async fn current_mixnodes( log::trace!("Fetching list of mixnodes from: {nym_api}"); - let mixnodes = client.get_cached_mixnodes().await?; + let mixnodes = client + .get_all_basic_active_mixing_assigned_nodes(None) + .await?; let valid_mixnodes = mixnodes - .into_iter() - .filter_map(|mixnode| (&mixnode.bond_information).try_into().ok()) - .collect::>(); + .iter() + .filter_map(|mixnode| mixnode.try_into().ok()) + .collect::>(); - // we were always filtering by version so I'm not removing that 'feature' - let filtered_mixnodes = valid_mixnodes.filter_by_version(env!("CARGO_PKG_VERSION")); - Ok(filtered_mixnodes) + Ok(valid_mixnodes) } #[cfg(not(target_arch = "wasm32"))] @@ -273,9 +268,9 @@ fn filter_by_tls( pub(super) fn uniformly_random_gateway( rng: &mut R, - gateways: &[gateway::Node], + gateways: &[gateway::LegacyNode], must_use_tls: bool, -) -> Result { +) -> Result { filter_by_tls(gateways, must_use_tls)? .choose(rng) .ok_or(ClientCoreError::NoGatewaysOnNetwork) @@ -284,9 +279,9 @@ pub(super) fn uniformly_random_gateway( pub(super) fn get_specified_gateway( gateway_identity: IdentityKeyRef, - gateways: &[gateway::Node], + gateways: &[gateway::LegacyNode], must_use_tls: bool, -) -> Result { +) -> Result { log::debug!("Requesting specified gateway: {}", gateway_identity); let user_gateway = identity::PublicKey::from_base58_string(gateway_identity) .map_err(ClientCoreError::UnableToCreatePublicKeyFromGatewayId)?; diff --git a/common/client-core/src/init/mod.rs b/common/client-core/src/init/mod.rs index 479d73be1a..8e93babbf2 100644 --- a/common/client-core/src/init/mod.rs +++ b/common/client-core/src/init/mod.rs @@ -50,7 +50,7 @@ async fn setup_new_gateway( key_store: &K, details_store: &D, selection_specification: GatewaySelectionSpecification, - available_gateways: Vec, + available_gateways: Vec, ) -> Result where K: KeyStore, diff --git a/common/client-core/src/init/types.rs b/common/client-core/src/init/types.rs index 2ba78dd6d1..1aa4a0d24b 100644 --- a/common/client-core/src/init/types.rs +++ b/common/client-core/src/init/types.rs @@ -18,7 +18,6 @@ use nym_validator_client::client::IdentityKey; use nym_validator_client::nyxd::AccountId; use serde::Serialize; use std::fmt::Display; -use std::str::FromStr; use std::sync::Arc; use time::OffsetDateTime; use url::Url; @@ -39,7 +38,7 @@ pub enum SelectedGateway { impl SelectedGateway { pub fn from_topology_node( - node: gateway::Node, + node: gateway::LegacyNode, must_use_tls: bool, ) -> Result { let gateway_listener = if must_use_tls { @@ -51,20 +50,6 @@ impl SelectedGateway { node.clients_address() }; - let gateway_owner_address = node - .owner - .as_ref() - .map(|raw_owner| { - AccountId::from_str(raw_owner).map_err(|source| { - ClientCoreError::MalformedGatewayOwnerAccountAddress { - gateway_id: node.identity_key.to_base58_string(), - raw_owner: raw_owner.clone(), - err: source.to_string(), - } - }) - }) - .transpose()?; - let gateway_listener = Url::parse(&gateway_listener).map_err(|source| ClientCoreError::MalformedListener { gateway_id: node.identity_key.to_base58_string(), @@ -74,7 +59,7 @@ impl SelectedGateway { Ok(SelectedGateway::Remote { gateway_id: node.identity_key, - gateway_owner_address, + gateway_owner_address: None, gateway_listener, }) } @@ -215,7 +200,7 @@ pub enum GatewaySetup { specification: GatewaySelectionSpecification, // TODO: seems to be a bit inefficient to pass them by value - available_gateways: Vec, + available_gateways: Vec, }, ReuseConnection { diff --git a/common/client-core/surb-storage/src/backend/fs_backend/manager.rs b/common/client-core/surb-storage/src/backend/fs_backend/manager.rs index 8b663b5c9a..b6adadd3d5 100644 --- a/common/client-core/surb-storage/src/backend/fs_backend/manager.rs +++ b/common/client-core/surb-storage/src/backend/fs_backend/manager.rs @@ -1,9 +1,12 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::backend::fs_backend::error::StorageError; -use crate::backend::fs_backend::models::{ - ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender, +use crate::backend::fs_backend::{ + error::StorageError, + models::{ + ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, + StoredSurbSender, + }, }; use log::{error, info}; use sqlx::ConnectOptions; @@ -27,11 +30,10 @@ impl StorageManager { })?; } - let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + let opts = sqlx::sqlite::SqliteConnectOptions::new() .filename(database_path) - .create_if_missing(fresh); - - opts.disable_statement_logging(); + .create_if_missing(fresh) + .disable_statement_logging(); let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { Ok(pool) => pool, diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index 418debb4a9..81b9b4d7f4 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -24,6 +24,7 @@ zeroize.workspace = true nym-bandwidth-controller = { path = "../../bandwidth-controller" } nym-credentials = { path = "../../credentials" } nym-credential-storage = { path = "../../credential-storage" } +nym-credentials-interface = { path = "../../credentials-interface" } nym-crypto = { path = "../../crypto" } nym-gateway-requests = { path = "../../gateway-requests" } nym-network-defaults = { path = "../../network-defaults" } diff --git a/common/client-libs/gateway-client/src/client/mod.rs b/common/client-libs/gateway-client/src/client/mod.rs index 2278c6fa47..58e061887e 100644 --- a/common/client-libs/gateway-client/src/client/mod.rs +++ b/common/client-libs/gateway-client/src/client/mod.rs @@ -16,6 +16,7 @@ use nym_bandwidth_controller::{BandwidthController, BandwidthStatusMessage}; use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage; use nym_credential_storage::storage::Storage as CredentialStorage; use nym_credentials::CredentialSpendingData; +use nym_credentials_interface::TicketType; use nym_crypto::asymmetric::identity; use nym_gateway_requests::registration::handshake::client_handshake; use nym_gateway_requests::{ @@ -748,7 +749,11 @@ impl GatewayClient { } let prepared_credential = self .unchecked_bandwidth_controller() - .prepare_ecash_ticket(self.gateway_identity.to_bytes(), TICKETS_TO_SPEND) + .prepare_ecash_ticket( + TicketType::V1MixnetEntry, + self.gateway_identity.to_bytes(), + TICKETS_TO_SPEND, + ) .await?; match self.claim_ecash_bandwidth(prepared_credential.data).await { diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 6e8dd55772..5cd86e2dba 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -20,11 +20,12 @@ nym-coconut-bandwidth-contract-common = { path = "../../cosmwasm-smart-contracts 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-serde-helpers = { path = "../../serde-helpers", features = ["hex", "base64"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } nym-http-api-client = { path = "../../../common/http-api-client" } thiserror = { workspace = true } -log = { workspace = true } +tracing = { workspace = true } url = { workspace = true, features = ["serde"] } tokio = { workspace = true, features = ["sync", "time"] } time = { workspace = true, features = ["formatting"] } diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index 32cfd9ae2f..d0d64f4b93 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -17,23 +17,23 @@ use nym_api_requests::ecash::{ BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, VerificationKeyResponse, }; -use nym_api_requests::models::{DescribedGateway, MixNodeBondAnnotated}; use nym_api_requests::models::{ - GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, - RewardEstimationResponse, StakeSaturationResponse, + ApiHealthResponse, GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, + NymNodeDescription, RewardEstimationResponse, StakeSaturationResponse, }; +use nym_api_requests::models::{LegacyDescribedGateway, MixNodeBondAnnotated}; use nym_api_requests::nym_nodes::SkimmedNode; use nym_coconut_dkg_common::types::EpochId; use nym_http_api_client::UserAgent; +use nym_mixnet_contract_common::NymNodeDetails; use nym_network_defaults::NymNetworkDetails; use time::Date; use url::Url; pub use crate::nym_api::NymApiClientExt; pub use nym_mixnet_contract_common::{ - mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, MixId, + mixnode::MixNodeDetails, GatewayBond, IdentityKey, IdentityKeyRef, NodeId, }; - // re-export the type to not break existing imports pub use crate::coconut::EcashApiClient; @@ -106,7 +106,9 @@ impl Config { pub struct Client { // ideally they would have been read-only, but unfortunately rust doesn't have such features + // #[deprecated(note = "please use `nym_api_client` instead")] pub nym_api: nym_api::Client, + // pub nym_api_client: NymApiClient, pub nyxd: NyxdClient, } @@ -190,6 +192,8 @@ impl Client { } // validator-api wrappers +// we have to allow the use of deprecated method here as they're calling the deprecated trait methods +#[allow(deprecated)] impl Client { pub fn api_url(&self) -> &Url { self.nym_api.current_url() @@ -199,50 +203,102 @@ impl Client { self.nym_api.change_base_url(new_endpoint) } + #[deprecated] pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { Ok(self.nym_api.get_mixnodes().await?) } + #[deprecated] pub async fn get_cached_mixnodes_detailed( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_mixnodes_detailed().await?) } + #[deprecated] pub async fn get_cached_mixnodes_detailed_unfiltered( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_mixnodes_detailed_unfiltered().await?) } + #[deprecated] pub async fn get_cached_rewarded_mixnodes( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_rewarded_mixnodes().await?) } + #[deprecated] pub async fn get_cached_rewarded_mixnodes_detailed( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_rewarded_mixnodes_detailed().await?) } + #[deprecated] pub async fn get_cached_active_mixnodes( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_active_mixnodes().await?) } + #[deprecated] pub async fn get_cached_active_mixnodes_detailed( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_active_mixnodes_detailed().await?) } + #[deprecated] pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { Ok(self.nym_api.get_gateways().await?) } + // TODO: combine with NymApiClient... + pub async fn get_all_cached_described_nodes( + &self, + ) -> Result, ValidatorClientError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut descriptions = Vec::new(); + + loop { + let mut res = self.nym_api.get_nodes_described(Some(page), None).await?; + + descriptions.append(&mut res.data); + if descriptions.len() < res.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(descriptions) + } + + // TODO: combine with NymApiClient... + pub async fn get_all_cached_bonded_nym_nodes( + &self, + ) -> Result, ValidatorClientError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut bonds = Vec::new(); + + loop { + let mut res = self.nym_api.get_nym_nodes(Some(page), None).await?; + + bonds.append(&mut res.data); + if bonds.len() < res.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(bonds) + } + pub async fn blind_sign( &self, request_body: &BlindSignRequestBody, @@ -258,6 +314,8 @@ pub struct NymApiClient { // we could re-implement the communication with the REST API on port 1317 } +// we have to allow the use of deprecated method here as they're calling the deprecated trait methods +#[allow(deprecated)] impl NymApiClient { pub fn new(api_url: Url) -> Self { let nym_api = nym_api::Client::new(api_url, None); @@ -265,10 +323,17 @@ impl NymApiClient { NymApiClient { nym_api } } - pub fn new_with_user_agent(api_url: Url, user_agent: UserAgent) -> Self { + #[cfg(not(target_arch = "wasm32"))] + pub fn new_with_timeout(api_url: Url, timeout: std::time::Duration) -> Self { + let nym_api = nym_api::Client::new(api_url, Some(timeout)); + + NymApiClient { nym_api } + } + + pub fn new_with_user_agent(api_url: Url, user_agent: impl Into) -> Self { let nym_api = nym_api::Client::builder::<_, ValidatorClientError>(api_url) .expect("invalid api url") - .with_user_agent(user_agent) + .with_user_agent(user_agent.into()) .build::() .expect("failed to build nym api client"); @@ -283,6 +348,7 @@ impl NymApiClient { self.nym_api.change_base_url(new_endpoint); } + #[deprecated(note = "use get_all_basic_active_mixing_assigned_nodes instead")] pub async fn get_basic_mixnodes( &self, semver_compatibility: Option, @@ -294,6 +360,7 @@ impl NymApiClient { .nodes) } + #[deprecated(note = "use get_all_basic_entry_assigned_nodes instead")] pub async fn get_basic_gateways( &self, semver_compatibility: Option, @@ -305,32 +372,206 @@ impl NymApiClient { .nodes) } + /// retrieve basic information for nodes are capable of operating as an entry gateway + /// this includes legacy gateways and nym-nodes + pub async fn get_all_basic_entry_assigned_nodes( + &self, + semver_compatibility: Option, + ) -> Result, ValidatorClientError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut nodes = Vec::new(); + + loop { + let mut res = self + .nym_api + .get_basic_entry_assigned_nodes( + semver_compatibility.clone(), + false, + Some(page), + None, + ) + .await?; + + nodes.append(&mut res.nodes.data); + if nodes.len() < res.nodes.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(nodes) + } + + /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch + /// this includes legacy mixnodes and nym-nodes + pub async fn get_all_basic_active_mixing_assigned_nodes( + &self, + semver_compatibility: Option, + ) -> Result, ValidatorClientError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut nodes = Vec::new(); + + loop { + let mut res = self + .nym_api + .get_basic_active_mixing_assigned_nodes( + semver_compatibility.clone(), + false, + Some(page), + None, + ) + .await?; + + nodes.append(&mut res.nodes.data); + if nodes.len() < res.nodes.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(nodes) + } + + /// retrieve basic information for nodes are capable of operating as a mixnode + /// this includes legacy mixnodes and nym-nodes + pub async fn get_all_basic_mixing_capable_nodes( + &self, + semver_compatibility: Option, + ) -> Result, ValidatorClientError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut nodes = Vec::new(); + + loop { + let mut res = self + .nym_api + .get_basic_mixing_capable_nodes( + semver_compatibility.clone(), + false, + Some(page), + None, + ) + .await?; + + nodes.append(&mut res.nodes.data); + if nodes.len() < res.nodes.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(nodes) + } + + /// retrieve basic information for all bonded nodes on the network + pub async fn get_all_basic_nodes( + &self, + semver_compatibility: Option, + ) -> Result, ValidatorClientError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut nodes = Vec::new(); + + loop { + let mut res = self + .nym_api + .get_basic_nodes(semver_compatibility.clone(), false, Some(page), None) + .await?; + + nodes.append(&mut res.nodes.data); + if nodes.len() < res.nodes.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(nodes) + } + + pub async fn health(&self) -> Result { + Ok(self.nym_api.health().await?) + } + + #[deprecated] pub async fn get_cached_active_mixnodes( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_active_mixnodes().await?) } + #[deprecated] pub async fn get_cached_rewarded_mixnodes( &self, ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_rewarded_mixnodes().await?) } + #[deprecated] pub async fn get_cached_mixnodes(&self) -> Result, ValidatorClientError> { Ok(self.nym_api.get_mixnodes().await?) } + #[deprecated] pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { Ok(self.nym_api.get_gateways().await?) } + #[deprecated] pub async fn get_cached_described_gateways( &self, - ) -> Result, ValidatorClientError> { + ) -> Result, ValidatorClientError> { Ok(self.nym_api.get_gateways_described().await?) } + pub async fn get_all_described_nodes( + &self, + ) -> Result, ValidatorClientError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut descriptions = Vec::new(); + + loop { + let mut res = self.nym_api.get_nodes_described(Some(page), None).await?; + + descriptions.append(&mut res.data); + if descriptions.len() < res.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(descriptions) + } + + pub async fn get_all_bonded_nym_nodes( + &self, + ) -> Result, ValidatorClientError> { + // TODO: deal with paging in macro or some helper function or something, because it's the same pattern everywhere + let mut page = 0; + let mut bonds = Vec::new(); + + loop { + let mut res = self.nym_api.get_nym_nodes(Some(page), None).await?; + + bonds.append(&mut res.data); + if bonds.len() < res.pagination.total { + page += 1 + } else { + break; + } + } + + Ok(bonds) + } + + #[deprecated] pub async fn get_gateway_core_status_count( &self, identity: IdentityKeyRef<'_>, @@ -342,9 +583,10 @@ impl NymApiClient { .await?) } + #[deprecated] pub async fn get_mixnode_core_status_count( &self, - mix_id: MixId, + mix_id: NodeId, since: Option, ) -> Result { Ok(self @@ -353,23 +595,26 @@ impl NymApiClient { .await?) } + #[deprecated] pub async fn get_mixnode_status( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { Ok(self.nym_api.get_mixnode_status(mix_id).await?) } + #[deprecated] pub async fn get_mixnode_reward_estimation( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { Ok(self.nym_api.get_mixnode_reward_estimation(mix_id).await?) } + #[deprecated] pub async fn get_mixnode_stake_saturation( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { Ok(self.nym_api.get_mixnode_stake_saturation(mix_id).await?) } @@ -398,6 +643,7 @@ impl NymApiClient { .await?) } + #[deprecated] pub async fn spent_credentials_filter( &self, ) -> Result { diff --git a/common/client-libs/validator-client/src/connection_tester.rs b/common/client-libs/validator-client/src/connection_tester.rs index 15d2efe250..3e71a3c000 100644 --- a/common/client-libs/validator-client/src/connection_tester.rs +++ b/common/client-libs/validator-client/src/connection_tester.rs @@ -121,36 +121,36 @@ async fn test_nyxd_connection( { Ok(Err(NyxdError::TendermintErrorRpc(e))) => { // If we get a tendermint-rpc error, we classify the node as not contactable - log::warn!("Checking: nyxd url: {url}: {}: {}", "failed".red(), e); + tracing::warn!("Checking: nyxd url: {url}: {}: {}", "failed".red(), e); false } Ok(Err(NyxdError::AbciError { code, log, .. })) => { // We accept the mixnet contract not found as ok from a connection standpoint. This happens // for example on a pre-launch network. - log::debug!( + tracing::debug!( "Checking: nyxd url: {url}: {}, but with abci error: {code}: {log}", "success".green() ); code == 18 } Ok(Err(error @ NyxdError::NoContractAddressAvailable(_))) => { - log::warn!("Checking: nyxd url: {url}: {}: {error}", "failed".red()); + tracing::warn!("Checking: nyxd url: {url}: {}: {error}", "failed".red()); false } Ok(Err(e)) => { // For any other error, we're optimistic and just try anyway. - log::warn!( + tracing::warn!( "Checking: nyxd_url: {url}: {}, but with error: {e}", "success".green() ); true } Ok(Ok(_)) => { - log::debug!("Checking: nyxd_url: {url}: {}", "success".green()); + tracing::debug!("Checking: nyxd_url: {url}: {}", "success".green()); true } Err(e) => { - log::warn!("Checking: nyxd_url: {url}: {}: {e}", "failed".red()); + tracing::warn!("Checking: nyxd_url: {url}: {}: {e}", "failed".red()); false } }; @@ -164,20 +164,20 @@ async fn test_nym_api_connection( ) -> ConnectionResult { let result = match timeout( Duration::from_secs(CONNECTION_TEST_TIMEOUT_SEC), - client.get_cached_mixnodes(), + client.health(), ) .await { Ok(Ok(_)) => { - log::debug!("Checking: api_url: {url}: {}", "success".green()); + tracing::debug!("Checking: api_url: {url}: {}", "success".green()); true } Ok(Err(e)) => { - log::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); + tracing::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); false } Err(e) => { - log::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); + tracing::debug!("Checking: api_url: {url}: {}: {e}", "failed".red()); false } }; diff --git a/common/client-libs/validator-client/src/nym_api/mod.rs b/common/client-libs/validator-client/src/nym_api/mod.rs index 1ebfea7284..a3ea3aea8a 100644 --- a/common/client-libs/validator-client/src/nym_api/mod.rs +++ b/common/client-libs/validator-client/src/nym_api/mod.rs @@ -10,8 +10,12 @@ use nym_api_requests::ecash::models::{ VerifyEcashTicketBody, }; use nym_api_requests::ecash::VerificationKeyResponse; -use nym_api_requests::models::DescribedMixNode; -use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; +use nym_api_requests::models::{ + AnnotationResponse, ApiHealthResponse, LegacyDescribedMixNode, NodePerformanceResponse, + NodeRefreshBody, NymNodeDescription, +}; +use nym_api_requests::nym_nodes::PaginatedCachedNodesResponse; +use nym_api_requests::pagination::PaginatedResponse; pub use nym_api_requests::{ ecash::{ models::{ @@ -23,21 +27,23 @@ pub use nym_api_requests::{ VerifyEcashCredentialBody, }, models::{ - ComputeRewardEstParam, DescribedGateway, GatewayBondAnnotated, GatewayCoreStatusResponse, + ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, InclusionProbabilityResponse, - MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse, - MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, - StakeSaturationResponse, UptimeResponse, + LegacyDescribedGateway, MixNodeBondAnnotated, MixnodeCoreStatusResponse, + MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, + RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }, + nym_nodes::{CachedNodesResponse, SkimmedNode}, }; pub use nym_coconut_dkg_common::types::EpochId; use nym_contracts_common::IdentityKey; pub use nym_http_api_client::Client; use nym_http_api_client::{ApiClient, NO_PARAMS}; use nym_mixnet_contract_common::mixnode::MixNodeDetails; -use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; +use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId, NymNodeDetails}; use time::format_description::BorrowedFormatItem; use time::Date; +use tracing::instrument; pub mod error; pub mod routes; @@ -49,11 +55,27 @@ pub fn rfc_3339_date() -> Vec> { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait NymApiClientExt: ApiClient { + async fn health(&self) -> Result { + self.get_json( + &[ + routes::API_VERSION, + routes::API_STATUS_ROUTES, + routes::HEALTH, + ], + NO_PARAMS, + ) + .await + } + + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnodes(&self) -> Result, NymAPIError> { self.get_json(&[routes::API_VERSION, routes::MIXNODES], NO_PARAMS) .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnodes_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ @@ -67,6 +89,8 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_gateways_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ @@ -80,6 +104,8 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnodes_detailed_unfiltered( &self, ) -> Result, NymAPIError> { @@ -95,12 +121,16 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_gateways(&self) -> Result, NymAPIError> { self.get_json(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) .await } - async fn get_gateways_described(&self) -> Result, NymAPIError> { + #[deprecated] + #[instrument(level = "debug", skip(self))] + async fn get_gateways_described(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::GATEWAYS, routes::DESCRIBED], NO_PARAMS, @@ -108,7 +138,9 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn get_mixnodes_described(&self) -> Result, NymAPIError> { + #[deprecated] + #[instrument(level = "debug", skip(self))] + async fn get_mixnodes_described(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::DESCRIBED], NO_PARAMS, @@ -116,6 +148,48 @@ pub trait NymApiClientExt: ApiClient { .await } + #[tracing::instrument(level = "debug", skip_all)] + async fn get_nodes_described( + &self, + page: Option, + per_page: Option, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + self.get_json(&[routes::API_VERSION, "nym-nodes", "described"], ¶ms) + .await + } + + #[tracing::instrument(level = "debug", skip_all)] + async fn get_nym_nodes( + &self, + page: Option, + per_page: Option, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + self.get_json(&[routes::API_VERSION, "nym-nodes", "bonded"], ¶ms) + .await + } + + #[deprecated] + #[tracing::instrument(level = "debug", skip_all)] async fn get_basic_mixnodes( &self, semver_compatibility: Option, @@ -139,6 +213,8 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_basic_gateways( &self, semver_compatibility: Option, @@ -162,6 +238,167 @@ pub trait NymApiClientExt: ApiClient { .await } + /// retrieve basic information for nodes are capable of operating as an entry gateway + /// this includes legacy gateways and nym-nodes + #[instrument(level = "debug", skip(self))] + async fn get_basic_entry_assigned_nodes( + &self, + semver_compatibility: Option, + no_legacy: bool, + page: Option, + per_page: Option, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if let Some(arg) = &semver_compatibility { + params.push(("semver_compatibility", arg.clone())) + } + + if no_legacy { + params.push(("no_legacy", "true".to_string())) + } + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + self.get_json( + &[ + routes::API_VERSION, + "unstable", + "nym-nodes", + "skimmed", + "entry-gateways", + "all", + ], + ¶ms, + ) + .await + } + + /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch + /// this includes legacy mixnodes and nym-nodes + #[instrument(level = "debug", skip(self))] + async fn get_basic_active_mixing_assigned_nodes( + &self, + semver_compatibility: Option, + no_legacy: bool, + page: Option, + per_page: Option, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if let Some(arg) = &semver_compatibility { + params.push(("semver_compatibility", arg.clone())) + } + + if no_legacy { + params.push(("no_legacy", "true".to_string())) + } + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + self.get_json( + &[ + routes::API_VERSION, + "unstable", + "nym-nodes", + "skimmed", + "mixnodes", + "active", + ], + ¶ms, + ) + .await + } + + /// retrieve basic information for nodes that got assigned 'mixing' node in this epoch + /// this includes legacy mixnodes and nym-nodes + #[instrument(level = "debug", skip(self))] + async fn get_basic_mixing_capable_nodes( + &self, + semver_compatibility: Option, + no_legacy: bool, + page: Option, + per_page: Option, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if let Some(arg) = &semver_compatibility { + params.push(("semver_compatibility", arg.clone())) + } + + if no_legacy { + params.push(("no_legacy", "true".to_string())) + } + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + self.get_json( + &[ + routes::API_VERSION, + "unstable", + "nym-nodes", + "skimmed", + "mixnodes", + "all", + ], + ¶ms, + ) + .await + } + #[instrument(level = "debug", skip(self))] + + async fn get_basic_nodes( + &self, + semver_compatibility: Option, + no_legacy: bool, + page: Option, + per_page: Option, + ) -> Result, NymAPIError> { + let mut params = Vec::new(); + + if let Some(arg) = &semver_compatibility { + params.push(("semver_compatibility", arg.clone())) + } + + if no_legacy { + params.push(("no_legacy", "true".to_string())) + } + + if let Some(page) = page { + params.push(("page", page.to_string())) + } + + if let Some(per_page) = per_page { + params.push(("per_page", per_page.to_string())) + } + + self.get_json( + &[routes::API_VERSION, "unstable", "nym-nodes", "skimmed"], + ¶ms, + ) + .await + } + + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_active_mixnodes(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::ACTIVE], @@ -170,6 +407,8 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_active_mixnodes_detailed(&self) -> Result, NymAPIError> { self.get_json( &[ @@ -184,6 +423,8 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_rewarded_mixnodes(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::REWARDED], @@ -192,9 +433,11 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnode_report( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -209,6 +452,8 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_gateway_report( &self, identity: IdentityKeyRef<'_>, @@ -226,9 +471,11 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnode_history( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -243,6 +490,8 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_gateway_history( &self, identity: IdentityKeyRef<'_>, @@ -260,6 +509,8 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_rewarded_mixnodes_detailed( &self, ) -> Result, NymAPIError> { @@ -276,6 +527,8 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_gateway_core_status_count( &self, identity: IdentityKeyRef<'_>, @@ -307,9 +560,11 @@ pub trait NymApiClientExt: ApiClient { } } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnode_core_status_count( &self, - mix_id: MixId, + mix_id: NodeId, since: Option, ) -> Result { if let Some(since) = since { @@ -339,9 +594,11 @@ pub trait NymApiClientExt: ApiClient { } } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnode_status( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -356,9 +613,11 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnode_reward_estimation( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -373,9 +632,11 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn compute_mixnode_reward_estimation( &self, - mix_id: MixId, + mix_id: NodeId, request_body: &ComputeRewardEstParam, ) -> Result { self.post_json( @@ -392,9 +653,11 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnode_stake_saturation( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -409,9 +672,11 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_mixnode_inclusion_probability( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.get_json( &[ @@ -426,7 +691,41 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn get_mixnode_avg_uptime(&self, mix_id: MixId) -> Result { + #[instrument(level = "debug", skip(self))] + async fn get_current_node_performance( + &self, + node_id: NodeId, + ) -> Result { + self.get_json( + &[ + routes::API_VERSION, + "nym-nodes", + "performance", + &node_id.to_string(), + ], + NO_PARAMS, + ) + .await + } + + async fn get_node_annotation( + &self, + node_id: NodeId, + ) -> Result { + self.get_json( + &[ + routes::API_VERSION, + "nym-nodes", + "annotation", + &node_id.to_string(), + ], + NO_PARAMS, + ) + .await + } + + #[deprecated] + async fn get_mixnode_avg_uptime(&self, mix_id: NodeId) -> Result { self.get_json( &[ routes::API_VERSION, @@ -440,7 +739,9 @@ pub trait NymApiClientExt: ApiClient { .await } - async fn get_mixnodes_blacklisted(&self) -> Result, NymAPIError> { + #[deprecated] + #[instrument(level = "debug", skip(self))] + async fn get_mixnodes_blacklisted(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::MIXNODES, routes::BLACKLISTED], NO_PARAMS, @@ -448,6 +749,8 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn get_gateways_blacklisted(&self) -> Result, NymAPIError> { self.get_json( &[routes::API_VERSION, routes::GATEWAYS, routes::BLACKLISTED], @@ -456,6 +759,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self, request_body))] async fn blind_sign( &self, request_body: &BlindSignRequestBody, @@ -472,6 +776,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self, request_body))] async fn verify_ecash_ticket( &self, request_body: &VerifyEcashTicketBody, @@ -488,6 +793,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self, request_body))] async fn batch_redeem_ecash_tickets( &self, request_body: &BatchRedeemTicketsBody, @@ -504,6 +810,8 @@ pub trait NymApiClientExt: ApiClient { .await } + #[deprecated] + #[instrument(level = "debug", skip(self))] async fn double_spending_filter_v1(&self) -> Result { self.get_json( &[ @@ -516,6 +824,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn partial_expiration_date_signatures( &self, expiration_date: Option, @@ -539,6 +848,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn partial_coin_indices_signatures( &self, epoch_id: Option, @@ -559,6 +869,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn global_expiration_date_signatures( &self, expiration_date: Option, @@ -582,6 +893,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn global_coin_indices_signatures( &self, epoch_id: Option, @@ -602,6 +914,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn master_verification_key( &self, epoch_id: Option, @@ -621,6 +934,19 @@ pub trait NymApiClientExt: ApiClient { .await } + async fn force_refresh_describe_cache( + &self, + request: &NodeRefreshBody, + ) -> Result<(), NymAPIError> { + self.post_json( + &[routes::API_VERSION, "nym-nodes", "refresh-described"], + NO_PARAMS, + request, + ) + .await + } + + #[instrument(level = "debug", skip(self))] async fn epoch_credentials( &self, dkg_epoch: EpochId, @@ -637,6 +963,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn issued_credential( &self, credential_id: i64, @@ -653,6 +980,7 @@ pub trait NymApiClientExt: ApiClient { .await } + #[instrument(level = "debug", skip(self))] async fn issued_credentials( &self, credential_ids: Vec, diff --git a/common/client-libs/validator-client/src/nym_api/routes.rs b/common/client-libs/validator-client/src/nym_api/routes.rs index dc87026701..e0325c44cf 100644 --- a/common/client-libs/validator-client/src/nym_api/routes.rs +++ b/common/client-libs/validator-client/src/nym_api/routes.rs @@ -36,8 +36,11 @@ pub mod ecash { } pub const STATUS_ROUTES: &str = "status"; +pub const API_STATUS_ROUTES: &str = "api-status"; +pub const HEALTH: &str = "health"; pub const MIXNODE: &str = "mixnode"; pub const GATEWAY: &str = "gateway"; +pub const NYM_NODES: &str = "nym-nodes"; pub const CORE_STATUS_COUNT: &str = "core-status-count"; pub const SINCE_ARG: &str = "since"; @@ -52,5 +55,6 @@ pub const STAKE_SATURATION: &str = "stake-saturation"; pub const INCLUSION_CHANCE: &str = "inclusion-probability"; pub const SUBMIT_GATEWAY: &str = "submit-gateway-monitoring-results"; pub const SUBMIT_NODE: &str = "submit-node-monitoring-results"; +pub const PERFORMANCE: &str = "performance"; pub const SERVICE_PROVIDERS: &str = "services"; diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs index 4d8fd3237c..8674d7cb0a 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/dkg_query_client.rs @@ -8,9 +8,9 @@ use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cosmrs::AccountId; use cosmwasm_std::Addr; -use log::trace; use nym_coconut_dkg_common::types::{ChunkIndex, NodeIndex, StateAdvanceResponse}; use serde::Deserialize; +use tracing::trace; use nym_coconut_dkg_common::dealer::RegisteredDealerDetails; pub use nym_coconut_dkg_common::{ diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs index 47b67d8f16..b6fdadbce1 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_query_client.rs @@ -8,28 +8,33 @@ use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cosmrs::AccountId; use nym_contracts_common::signing::Nonce; +use nym_mixnet_contract_common::gateway::{PreassignedGatewayIdsResponse, PreassignedId}; +use nym_mixnet_contract_common::nym_node::{ + EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeDetailsResponse, + NodeOwnershipResponse, NodeRewardingDetailsResponse, PagedNymNodeBondsResponse, + PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse, + StakeSaturationResponse, UnbondedNodeResponse, UnbondedNymNode, +}; +use nym_mixnet_contract_common::reward_params::WorkFactor; use nym_mixnet_contract_common::{ delegation, - delegation::{MixNodeDelegationResponse, OwnerProxySubKey}, - families::{Family, FamilyHead}, + delegation::{NodeDelegationResponse, OwnerProxySubKey}, mixnode::{ - MixnodeRewardingDetailsResponse, PagedMixnodesDetailsResponse, - PagedUnbondedMixnodesResponse, StakeSaturationResponse, UnbondedMixnodeResponse, + MixStakeSaturationResponse, MixnodeRewardingDetailsResponse, PagedMixnodesDetailsResponse, + PagedUnbondedMixnodesResponse, UnbondedMixnodeResponse, }, reward_params::{Performance, RewardingParams}, rewarding::{EstimatedCurrentEpochRewardResponse, PendingRewardResponse}, ContractBuildInformation, ContractState, ContractStateParams, CurrentIntervalResponse, - Delegation, EpochEventId, EpochStatus, FamilyByHeadResponse, FamilyByLabelResponse, - FamilyMembersByHeadResponse, FamilyMembersByLabelResponse, GatewayBond, GatewayBondResponse, - GatewayOwnershipResponse, IdentityKey, IdentityKeyRef, IntervalEventId, LayerDistribution, - MixId, MixNodeBond, MixNodeDetails, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, - MixnodeDetailsResponse, NumberOfPendingEventsResponse, PagedAllDelegationsResponse, - PagedDelegatorDelegationsResponse, PagedFamiliesResponse, PagedGatewayResponse, - PagedMembersResponse, PagedMixNodeDelegationsResponse, PagedMixnodeBondsResponse, - PagedRewardedSetResponse, PendingEpochEvent, PendingEpochEventResponse, - PendingEpochEventsResponse, PendingIntervalEvent, PendingIntervalEventResponse, - PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg, RewardedSetNodeStatus, - UnbondedMixnode, + Delegation, EpochEventId, EpochStatus, GatewayBond, GatewayBondResponse, + GatewayOwnershipResponse, IdentityKey, IdentityKeyRef, IntervalEventId, MixNodeBond, + MixNodeDetails, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, + NodeId, NumberOfPendingEventsResponse, NymNodeBond, NymNodeDetails, + PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, PagedGatewayResponse, + PagedMixnodeBondsResponse, PagedNodeDelegationsResponse, PendingEpochEvent, + PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEvent, + PendingIntervalEventResponse, PendingIntervalEventsResponse, QueryMsg as MixnetQueryMsg, + RewardedSet, UnbondedMixnode, }; use serde::Deserialize; @@ -91,56 +96,11 @@ pub trait MixnetQueryClient { .await } - async fn get_rewarded_set_paged( - &self, - start_after: Option, - limit: Option, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetRewardedSet { limit, start_after }) - .await - } - - async fn get_all_node_families_paged( - &self, - start_after: Option, - limit: Option, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetAllFamiliesPaged { limit, start_after }) - .await - } - - async fn get_all_family_members_paged( - &self, - start_after: Option, - limit: Option, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetAllMembersPaged { limit, start_after }) - .await - } - - async fn get_family_members_by_head + Send>( - &self, - head: S, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetFamilyMembersByHead { head: head.into() }) - .await - } - - async fn get_family_members_by_label + Send>( - &self, - label: S, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetFamilyMembersByLabel { - label: label.into(), - }) - .await - } - // mixnode-related: async fn get_mixnode_bonds_paged( &self, - start_after: Option, + start_after: Option, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetMixNodeBonds { limit, start_after }) @@ -149,26 +109,26 @@ pub trait MixnetQueryClient { async fn get_mixnodes_detailed_paged( &self, - start_after: Option, + start_after: Option, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetMixNodesDetailed { limit, start_after }) .await } - async fn get_unbonded_paged( + async fn get_unbonded_mixnodes_paged( &self, - start_after: Option, + start_after: Option, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodes { limit, start_after }) .await } - async fn get_unbonded_by_owner_paged( + async fn get_unbonded_mixnodes_by_owner_paged( &self, owner: &AccountId, - start_after: Option, + start_after: Option, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodesByOwner { @@ -179,10 +139,10 @@ pub trait MixnetQueryClient { .await } - async fn get_unbonded_by_identity_paged( + async fn get_unbonded_mixnodes_by_identity_paged( &self, identity_key: IdentityKeyRef<'_>, - start_after: Option, + start_after: Option, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodesByIdentityKey { @@ -205,7 +165,7 @@ pub trait MixnetQueryClient { async fn get_mixnode_details( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeDetails { mix_id }) .await @@ -223,7 +183,7 @@ pub trait MixnetQueryClient { async fn get_mixnode_rewarding_details( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeRewardingDetails { mix_id }) .await @@ -231,24 +191,24 @@ pub trait MixnetQueryClient { async fn get_mixnode_stake_saturation( &self, - mix_id: MixId, - ) -> Result { + mix_id: NodeId, + ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetStakeSaturation { mix_id }) .await } async fn get_unbonded_mixnode_information( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedMixNodeInformation { mix_id }) .await } - async fn get_layer_distribution(&self) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetLayerDistribution {}) - .await - } + // async fn get_layer_distribution(&self) -> Result { + // self.query_mixnet_contract(MixnetQueryMsg::GetRoleDistribution {}) + // .await + // } // gateway-related: @@ -281,17 +241,139 @@ pub trait MixnetQueryClient { .await } + async fn get_preassigned_gateway_ids_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetPreassignedGatewayIds { start_after, limit }) + .await + } + + // nym-nodes related: + async fn get_nymnode_bonds_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNymNodeBondsPaged { limit, start_after }) + .await + } + + async fn get_nymnodes_detailed_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNymNodesDetailedPaged { limit, start_after }) + .await + } + + async fn get_unbonded_nymnodes_paged( + &self, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedNymNodesPaged { limit, start_after }) + .await + } + + async fn get_unbonded_nymnodes_by_owner_paged( + &self, + owner: &AccountId, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedNymNodesByOwnerPaged { + owner: owner.to_string(), + limit, + start_after, + }) + .await + } + + async fn get_unbonded_nymnodes_by_identity_paged( + &self, + identity_key: IdentityKeyRef<'_>, + start_after: Option, + limit: Option, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedNymNodesByIdentityKeyPaged { + identity_key: identity_key.to_string(), + limit, + start_after, + }) + .await + } + + async fn get_owned_nymnode( + &self, + address: &AccountId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetOwnedNymNode { + address: address.to_string(), + }) + .await + } + + async fn get_nymnode_details(&self, node_id: NodeId) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNymNodeDetails { node_id }) + .await + } + + async fn get_nymnode_details_by_identity( + &self, + node_identity: IdentityKey, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNymNodeDetailsByIdentityKey { node_identity }) + .await + } + + async fn get_nymnode_rewarding_details( + &self, + node_id: NodeId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNodeRewardingDetails { node_id }) + .await + } + + async fn get_node_stake_saturation( + &self, + node_id: NodeId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNodeStakeSaturation { node_id }) + .await + } + + async fn get_unbonded_nymnode_information( + &self, + node_id: NodeId, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetUnbondedNymNode { node_id }) + .await + } + + async fn get_role_assignment(&self, role: Role) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetRoleAssignment { role }) + .await + } + + async fn get_rewarded_set_metadata(&self) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetRewardedSetMetadata {}) + .await + } + // delegation-related: /// Gets list of all delegations towards particular mixnode on particular page. async fn get_mixnode_delegations_paged( &self, - mix_id: MixId, + node_id: NodeId, start_after: Option, limit: Option, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetMixnodeDelegations { - mix_id, + ) -> Result { + self.query_mixnet_contract(MixnetQueryMsg::GetNodeDelegations { + node_id, start_after, limit, }) @@ -302,7 +384,7 @@ pub trait MixnetQueryClient { async fn get_delegator_delegations_paged( &self, delegator: &AccountId, - start_after: Option<(MixId, OwnerProxySubKey)>, + start_after: Option<(NodeId, OwnerProxySubKey)>, limit: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetDelegatorDelegations { @@ -316,12 +398,12 @@ pub trait MixnetQueryClient { /// Checks value of delegation of given client towards particular mixnode. async fn get_delegation_details( &self, - mix_id: MixId, + node_id: NodeId, delegator: &AccountId, proxy: Option, - ) -> Result { + ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetDelegationDetails { - mix_id, + node_id, delegator: delegator.to_string(), proxy, }) @@ -351,21 +433,21 @@ pub trait MixnetQueryClient { async fn get_pending_mixnode_operator_reward( &self, - mix_id: MixId, + node_id: NodeId, ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetPendingMixNodeOperatorReward { mix_id }) + self.query_mixnet_contract(MixnetQueryMsg::GetPendingNodeOperatorReward { node_id }) .await } async fn get_pending_delegator_reward( &self, delegator: &AccountId, - mix_id: MixId, + node_id: NodeId, proxy: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetPendingDelegatorReward { address: delegator.to_string(), - mix_id, + node_id, proxy, }) .await @@ -374,12 +456,14 @@ pub trait MixnetQueryClient { // given the provided performance, estimate the reward at the end of the current epoch async fn get_estimated_current_epoch_operator_reward( &self, - mix_id: MixId, + node_id: NodeId, estimated_performance: Performance, + estimated_work: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetEstimatedCurrentEpochOperatorReward { - mix_id, + node_id, estimated_performance, + estimated_work, }) .await } @@ -388,15 +472,15 @@ pub trait MixnetQueryClient { async fn get_estimated_current_epoch_delegator_reward( &self, delegator: &AccountId, - mix_id: MixId, - proxy: Option, + node_id: NodeId, estimated_performance: Performance, + estimated_work: Option, ) -> Result { self.query_mixnet_contract(MixnetQueryMsg::GetEstimatedCurrentEpochDelegatorReward { address: delegator.to_string(), - mix_id, - proxy, + node_id, estimated_performance, + estimated_work, }) .await } @@ -450,22 +534,6 @@ pub trait MixnetQueryClient { }) .await } - - async fn get_node_family_by_label( - &self, - label: String, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetFamilyByLabel { label }) - .await - } - - async fn get_node_family_by_head( - &self, - head: String, - ) -> Result { - self.query_mixnet_contract(MixnetQueryMsg::GetFamilyByHead { head }) - .await - } } // extension trait to the query client to deal with the paged queries @@ -473,18 +541,35 @@ pub trait MixnetQueryClient { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] pub trait PagedMixnetQueryClient: MixnetQueryClient { - async fn get_all_node_families(&self) -> Result, NyxdError> { - collect_paged!(self, get_all_node_families_paged, families) + async fn get_all_nymnode_bonds(&self) -> Result, NyxdError> { + collect_paged!(self, get_nymnode_bonds_paged, nodes) + } + + async fn get_all_nymnodes_detailed(&self) -> Result, NyxdError> { + collect_paged!(self, get_nymnodes_detailed_paged, nodes) } - async fn get_all_family_members(&self) -> Result, NyxdError> { - collect_paged!(self, get_all_family_members_paged, members) + async fn get_all_unbonded_nymnodes(&self) -> Result, NyxdError> { + collect_paged!(self, get_unbonded_nymnodes_paged, nodes) } - async fn get_all_rewarded_set_mixnodes( + async fn get_all_unbonded_nymnodes_by_owner( &self, - ) -> Result, NyxdError> { - collect_paged!(self, get_rewarded_set_paged, nodes) + owner: &AccountId, + ) -> Result, NyxdError> { + collect_paged!(self, get_unbonded_nymnodes_by_owner_paged, nodes, owner) + } + + async fn get_all_unbonded_nymnodes_by_identity( + &self, + identity_key: IdentityKeyRef<'_>, + ) -> Result, NyxdError> { + collect_paged!( + self, + get_unbonded_nymnodes_by_identity_paged, + nodes, + identity_key + ) } async fn get_all_mixnode_bonds(&self) -> Result, NyxdError> { @@ -495,31 +580,40 @@ pub trait PagedMixnetQueryClient: MixnetQueryClient { collect_paged!(self, get_mixnodes_detailed_paged, nodes) } - async fn get_all_unbonded_mixnodes(&self) -> Result, NyxdError> { - collect_paged!(self, get_unbonded_paged, nodes) + async fn get_all_unbonded_mixnodes(&self) -> Result, NyxdError> { + collect_paged!(self, get_unbonded_mixnodes_paged, nodes) } async fn get_all_unbonded_mixnodes_by_owner( &self, owner: &AccountId, - ) -> Result, NyxdError> { - collect_paged!(self, get_unbonded_by_owner_paged, nodes, owner) + ) -> Result, NyxdError> { + collect_paged!(self, get_unbonded_mixnodes_by_owner_paged, nodes, owner) } async fn get_all_unbonded_mixnodes_by_identity( &self, identity_key: IdentityKeyRef<'_>, - ) -> Result, NyxdError> { - collect_paged!(self, get_unbonded_by_identity_paged, nodes, identity_key) + ) -> Result, NyxdError> { + collect_paged!( + self, + get_unbonded_mixnodes_by_identity_paged, + nodes, + identity_key + ) } async fn get_all_gateways(&self) -> Result, NyxdError> { collect_paged!(self, get_gateways_paged, nodes) } + async fn get_all_preassigned_gateway_ids(&self) -> Result, NyxdError> { + collect_paged!(self, get_preassigned_gateway_ids_paged, ids) + } + async fn get_all_single_mixnode_delegations( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result, NyxdError> { collect_paged!(self, get_mixnode_delegations_paged, delegations, mix_id) } @@ -554,6 +648,65 @@ pub trait PagedMixnetQueryClient: MixnetQueryClient { #[async_trait] impl PagedMixnetQueryClient for T where T: MixnetQueryClient {} +// extension help to provide extra functionalities based on existing queries: +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +pub trait MixnetQueryClientExt: MixnetQueryClient { + async fn get_rewarded_set(&self) -> Result { + let error_response = |message| Err(NyxdError::extension_query_failure("mixnet", message)); + + let metadata = self.get_rewarded_set_metadata().await?; + if !metadata.metadata.fully_assigned { + return error_response("the rewarded set hasn't been fully assigned for this epoch"); + } + let expected_epoch_id = metadata.metadata.epoch_id; + + // if we have to query those things more frequently, we could do it concurrently, + // but as it stands now, it happens so infrequently it might as well be sequential + let entry = self.get_role_assignment(Role::EntryGateway).await?; + if entry.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'entry' returned unexpected epoch_id"); + } + + let exit = self.get_role_assignment(Role::ExitGateway).await?; + if exit.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'exit' returned unexpected epoch_id"); + } + + let layer1 = self.get_role_assignment(Role::Layer1).await?; + if layer1.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'layer1' returned unexpected epoch_id"); + } + + let layer2 = self.get_role_assignment(Role::Layer2).await?; + if layer2.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'layer2' returned unexpected epoch_id"); + } + + let layer3 = self.get_role_assignment(Role::Layer3).await?; + if layer3.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'layer3' returned unexpected epoch_id"); + } + + let standby = self.get_role_assignment(Role::Standby).await?; + if standby.epoch_id != expected_epoch_id { + return error_response("the nodes assigned for 'standby' returned unexpected epoch_id"); + } + + Ok(RewardedSet { + entry_gateways: entry.nodes, + exit_gateways: exit.nodes, + layer1: layer1.nodes, + layer2: layer2.nodes, + layer3: layer3.nodes, + standby: standby.nodes, + }) + } +} + +#[async_trait] +impl MixnetQueryClientExt for T where T: MixnetQueryClient {} + #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] #[cfg_attr(not(target_arch = "wasm32"), async_trait)] impl MixnetQueryClient for C @@ -585,24 +738,6 @@ mod tests { ) -> u32 { match msg { MixnetQueryMsg::Admin {} => client.admin().ignore(), - MixnetQueryMsg::GetAllFamiliesPaged { limit, start_after } => client - .get_all_family_members_paged(start_after, limit) - .ignore(), - MixnetQueryMsg::GetAllMembersPaged { limit, start_after } => client - .get_all_family_members_paged(start_after, limit) - .ignore(), - MixnetQueryMsg::GetFamilyByHead { head } => { - client.get_node_family_by_head(head).ignore() - } - MixnetQueryMsg::GetFamilyByLabel { label } => { - client.get_node_family_by_label(label).ignore() - } - MixnetQueryMsg::GetFamilyMembersByHead { head } => { - client.get_family_members_by_head(head).ignore() - } - MixnetQueryMsg::GetFamilyMembersByLabel { label } => { - client.get_family_members_by_label(label).ignore() - } MixnetQueryMsg::GetContractVersion {} => client.get_mixnet_contract_version().ignore(), MixnetQueryMsg::GetCW2ContractVersion {} => { client.get_mixnet_contract_cw2_version().ignore() @@ -617,31 +752,28 @@ mod tests { MixnetQueryMsg::GetCurrentIntervalDetails {} => { client.get_current_interval_details().ignore() } - MixnetQueryMsg::GetRewardedSet { limit, start_after } => { - client.get_rewarded_set_paged(start_after, limit).ignore() - } MixnetQueryMsg::GetMixNodeBonds { limit, start_after } => { client.get_mixnode_bonds_paged(start_after, limit).ignore() } MixnetQueryMsg::GetMixNodesDetailed { limit, start_after } => client .get_mixnodes_detailed_paged(start_after, limit) .ignore(), - MixnetQueryMsg::GetUnbondedMixNodes { limit, start_after } => { - client.get_unbonded_paged(start_after, limit).ignore() - } + MixnetQueryMsg::GetUnbondedMixNodes { limit, start_after } => client + .get_unbonded_mixnodes_paged(start_after, limit) + .ignore(), MixnetQueryMsg::GetUnbondedMixNodesByOwner { owner, limit, start_after, } => client - .get_unbonded_by_owner_paged(&owner.parse().unwrap(), start_after, limit) + .get_unbonded_mixnodes_by_owner_paged(&owner.parse().unwrap(), start_after, limit) .ignore(), MixnetQueryMsg::GetUnbondedMixNodesByIdentityKey { identity_key, limit, start_after, } => client - .get_unbonded_by_identity_paged(&identity_key, start_after, limit) + .get_unbonded_mixnodes_by_identity_paged(&identity_key, start_after, limit) .ignore(), MixnetQueryMsg::GetOwnedMixnode { address } => { client.get_owned_mixnode(&address.parse().unwrap()).ignore() @@ -661,7 +793,6 @@ mod tests { MixnetQueryMsg::GetBondedMixnodeDetailsByIdentity { mix_identity } => client .get_mixnode_details_by_identity(mix_identity) .ignore(), - MixnetQueryMsg::GetLayerDistribution {} => client.get_layer_distribution().ignore(), MixnetQueryMsg::GetGateways { start_after, limit } => { client.get_gateways_paged(start_after, limit).ignore() } @@ -671,8 +802,8 @@ mod tests { MixnetQueryMsg::GetOwnedGateway { address } => { client.get_owned_gateway(&address.parse().unwrap()).ignore() } - MixnetQueryMsg::GetMixnodeDelegations { - mix_id, + MixnetQueryMsg::GetNodeDelegations { + node_id: mix_id, start_after, limit, } => client @@ -686,7 +817,7 @@ mod tests { .get_delegator_delegations_paged(&delegator.parse().unwrap(), start_after, limit) .ignore(), MixnetQueryMsg::GetDelegationDetails { - mix_id, + node_id: mix_id, delegator, proxy, } => client @@ -698,33 +829,38 @@ mod tests { MixnetQueryMsg::GetPendingOperatorReward { address } => client .get_pending_operator_reward(&address.parse().unwrap()) .ignore(), - MixnetQueryMsg::GetPendingMixNodeOperatorReward { mix_id } => { + MixnetQueryMsg::GetPendingNodeOperatorReward { node_id: mix_id } => { client.get_pending_mixnode_operator_reward(mix_id).ignore() } MixnetQueryMsg::GetPendingDelegatorReward { address, - mix_id, + node_id: mix_id, proxy, } => client .get_pending_delegator_reward(&address.parse().unwrap(), mix_id, proxy) .ignore(), MixnetQueryMsg::GetEstimatedCurrentEpochOperatorReward { - mix_id, + node_id, estimated_performance, + estimated_work, } => client - .get_estimated_current_epoch_operator_reward(mix_id, estimated_performance) + .get_estimated_current_epoch_operator_reward( + node_id, + estimated_performance, + estimated_work, + ) .ignore(), MixnetQueryMsg::GetEstimatedCurrentEpochDelegatorReward { address, - mix_id, - proxy, + node_id, estimated_performance, + estimated_work, } => client .get_estimated_current_epoch_delegator_reward( &address.parse().unwrap(), - mix_id, - proxy, + node_id, estimated_performance, + estimated_work, ) .ignore(), MixnetQueryMsg::GetPendingEpochEvents { limit, start_after } => client @@ -745,6 +881,54 @@ mod tests { MixnetQueryMsg::GetSigningNonce { address } => { client.get_signing_nonce(&address.parse().unwrap()).ignore() } + MixnetQueryMsg::GetPreassignedGatewayIds { start_after, limit } => client + .get_preassigned_gateway_ids_paged(start_after, limit) + .ignore(), + MixnetQueryMsg::GetNymNodeBondsPaged { limit, start_after } => { + client.get_nymnode_bonds_paged(limit, start_after).ignore() + } + MixnetQueryMsg::GetNymNodesDetailedPaged { limit, start_after } => client + .get_nymnodes_detailed_paged(limit, start_after) + .ignore(), + MixnetQueryMsg::GetUnbondedNymNode { node_id } => { + client.get_unbonded_nymnode_information(node_id).ignore() + } + MixnetQueryMsg::GetUnbondedNymNodesPaged { limit, start_after } => client + .get_unbonded_nymnodes_paged(limit, start_after) + .ignore(), + MixnetQueryMsg::GetUnbondedNymNodesByOwnerPaged { + owner, + limit, + start_after, + } => client + .get_unbonded_nymnodes_by_owner_paged(&owner.parse().unwrap(), limit, start_after) + .ignore(), + MixnetQueryMsg::GetUnbondedNymNodesByIdentityKeyPaged { + identity_key, + limit, + start_after, + } => client + .get_unbonded_nymnodes_by_identity_paged(&identity_key, limit, start_after) + .ignore(), + MixnetQueryMsg::GetOwnedNymNode { address } => { + client.get_owned_nymnode(&address.parse().unwrap()).ignore() + } + MixnetQueryMsg::GetNymNodeDetails { node_id } => { + client.get_nymnode_details(node_id).ignore() + } + MixnetQueryMsg::GetNymNodeDetailsByIdentityKey { node_identity } => client + .get_nymnode_details_by_identity(node_identity) + .ignore(), + MixnetQueryMsg::GetNodeRewardingDetails { node_id } => { + client.get_nymnode_rewarding_details(node_id).ignore() + } + MixnetQueryMsg::GetNodeStakeSaturation { node_id } => { + client.get_node_stake_saturation(node_id).ignore() + } + MixnetQueryMsg::GetRoleAssignment { role } => client.get_role_assignment(role).ignore(), + MixnetQueryMsg::GetRewardedSetMetadata {} => { + client.get_rewarded_set_metadata().ignore() + } } } } diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs index f84becb55f..b253c3dc94 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/mixnet_signing_client.rs @@ -10,13 +10,15 @@ use crate::signing::signer::OfflineSigner; use async_trait::async_trait; use cosmrs::AccountId; use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::families::FamilyHead; use nym_mixnet_contract_common::gateway::GatewayConfigUpdate; -use nym_mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; -use nym_mixnet_contract_common::reward_params::{IntervalRewardingParamsUpdate, Performance}; +use nym_mixnet_contract_common::mixnode::{MixNodeConfigUpdate, NodeCostParams}; +use nym_mixnet_contract_common::nym_node::NodeConfigUpdate; +use nym_mixnet_contract_common::reward_params::{ + ActiveSetUpdate, IntervalRewardingParamsUpdate, NodeRewardingParameters, +}; use nym_mixnet_contract_common::{ - ContractStateParams, ExecuteMsg as MixnetExecuteMsg, Gateway, Layer, LayerAssignment, MixId, - MixNode, + ContractStateParams, ExecuteMsg as MixnetExecuteMsg, Gateway, MixNode, NodeId, NymNode, + RoleAssignment, }; #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -70,14 +72,14 @@ pub trait MixnetSigningClient { async fn update_active_set_size( &self, - active_set_size: u32, + update: ActiveSetUpdate, force_immediately: bool, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, - MixnetExecuteMsg::UpdateActiveSetSize { - active_set_size, + MixnetExecuteMsg::UpdateActiveSetDistribution { + update, force_immediately, }, vec![], @@ -126,37 +128,6 @@ pub trait MixnetSigningClient { .await } - async fn advance_current_epoch( - &self, - new_rewarded_set: Vec, - expected_active_set_size: u32, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::AdvanceCurrentEpoch { - new_rewarded_set, - expected_active_set_size, - }, - vec![], - ) - .await - } - - async fn assign_node_layer( - &self, - mix_id: MixId, - layer: Layer, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::AssignNodeLayer { mix_id, layer }, - vec![], - ) - .await - } - async fn reconcile_epoch_events( &self, limit: Option, @@ -170,126 +141,21 @@ pub trait MixnetSigningClient { .await } - // family related - async fn create_family( - &self, - label: String, - fee: Option, - ) -> Result { - self.execute_mixnet_contract(fee, MixnetExecuteMsg::CreateFamily { label }, vec![]) - .await - } - - async fn create_family_on_behalf( - &self, - owner_address: String, - label: String, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::CreateFamilyOnBehalf { - owner_address, - label, - }, - vec![], - ) - .await - } - - async fn join_family( - &self, - join_permit: MessageSignature, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::JoinFamily { - join_permit, - family_head, - }, - vec![], - ) - .await - } - - async fn join_family_on_behalf( - &self, - member_address: String, - join_permit: MessageSignature, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::JoinFamilyOnBehalf { - member_address, - join_permit, - family_head, - }, - vec![], - ) - .await - } - - async fn leave_family( - &self, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_mixnet_contract(fee, MixnetExecuteMsg::LeaveFamily { family_head }, vec![]) - .await - } - - async fn leave_family_on_behalf( - &self, - member_address: String, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::LeaveFamilyOnBehalf { - member_address, - family_head, - }, - vec![], - ) - .await - } - - async fn kick_family_member( + async fn assign_roles( &self, - member: String, + assignment: RoleAssignment, fee: Option, ) -> Result { - self.execute_mixnet_contract(fee, MixnetExecuteMsg::KickFamilyMember { member }, vec![]) + self.execute_mixnet_contract(fee, MixnetExecuteMsg::AssignRoles { assignment }, vec![]) .await } - async fn kick_family_member_on_behalf( - &self, - head_address: String, - member: String, - fee: Option, - ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::KickFamilyMemberOnBehalf { - head_address, - member, - }, - vec![], - ) - .await - } // mixnode-related: async fn bond_mixnode( &self, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, pledge: Coin, fee: Option, @@ -310,7 +176,7 @@ pub trait MixnetSigningClient { &self, owner: AccountId, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, pledge: Coin, fee: Option, @@ -409,14 +275,14 @@ pub trait MixnetSigningClient { .await } - async fn update_mixnode_cost_params( + async fn update_cost_params( &self, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, - MixnetExecuteMsg::UpdateMixnodeCostParams { new_costs }, + MixnetExecuteMsg::UpdateCostParams { new_costs }, vec![], ) .await @@ -425,7 +291,7 @@ pub trait MixnetSigningClient { async fn update_mixnode_cost_params_on_behalf( &self, owner: AccountId, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, fee: Option, ) -> Result { self.execute_mixnet_contract( @@ -559,26 +425,75 @@ pub trait MixnetSigningClient { .await } - // delegation-related: + // nym-node related: + async fn migrate_legacy_mixnode(&self, fee: Option) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::MigrateMixnode {}, vec![]) + .await + } - async fn delegate_to_mixnode( + async fn migrate_legacy_gateway( &self, - mix_id: MixId, - amount: Coin, + cost_params: Option, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, - MixnetExecuteMsg::DelegateToMixnode { mix_id }, - vec![amount], + MixnetExecuteMsg::MigrateGateway { cost_params }, + vec![], + ) + .await + } + + async fn bond_nymnode( + &self, + node: NymNode, + cost_params: NodeCostParams, + owner_signature: MessageSignature, + pledge: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract( + fee, + MixnetExecuteMsg::BondNymNode { + node, + cost_params, + owner_signature, + }, + vec![pledge], ) .await } + async fn unbond_nymnode(&self, fee: Option) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::UnbondNymNode {}, vec![]) + .await + } + + async fn update_nymnode_config( + &self, + update: NodeConfigUpdate, + fee: Option, + ) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::UpdateNodeConfig { update }, vec![]) + .await + } + + // delegation-related: + + async fn delegate( + &self, + node_id: NodeId, + amount: Coin, + fee: Option, + ) -> Result { + self.execute_mixnet_contract(fee, MixnetExecuteMsg::Delegate { node_id }, vec![amount]) + .await + } + async fn delegate_to_mixnode_on_behalf( &self, delegate: AccountId, - mix_id: MixId, + mix_id: NodeId, amount: Coin, fee: Option, ) -> Result { @@ -593,23 +508,19 @@ pub trait MixnetSigningClient { .await } - async fn undelegate_from_mixnode( + async fn undelegate( &self, - mix_id: MixId, + node_id: NodeId, fee: Option, ) -> Result { - self.execute_mixnet_contract( - fee, - MixnetExecuteMsg::UndelegateFromMixnode { mix_id }, - vec![], - ) - .await + self.execute_mixnet_contract(fee, MixnetExecuteMsg::Undelegate { node_id }, vec![]) + .await } async fn undelegate_to_mixnode_on_behalf( &self, delegate: AccountId, - mix_id: MixId, + mix_id: NodeId, fee: Option, ) -> Result { self.execute_mixnet_contract( @@ -625,18 +536,15 @@ pub trait MixnetSigningClient { // reward-related - async fn reward_mixnode( + async fn reward_node( &self, - mix_id: MixId, - performance: Performance, + node_id: NodeId, + params: NodeRewardingParameters, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, - MixnetExecuteMsg::RewardMixnode { - mix_id, - performance, - }, + MixnetExecuteMsg::RewardNode { node_id, params }, vec![], ) .await @@ -664,12 +572,12 @@ pub trait MixnetSigningClient { async fn withdraw_delegator_reward( &self, - mix_id: MixId, + node_id: NodeId, fee: Option, ) -> Result { self.execute_mixnet_contract( fee, - MixnetExecuteMsg::WithdrawDelegatorReward { mix_id }, + MixnetExecuteMsg::WithdrawDelegatorReward { node_id }, vec![], ) .await @@ -678,7 +586,7 @@ pub trait MixnetSigningClient { async fn withdraw_delegator_reward_on_behalf( &self, owner: AccountId, - mix_id: MixId, + mix_id: NodeId, fee: Option, ) -> Result { self.execute_mixnet_contract( @@ -699,7 +607,7 @@ pub trait MixnetSigningClient { async fn migrate_vested_delegation( &self, - mix_id: MixId, + mix_id: NodeId, fee: Option, ) -> Result { self.execute_mixnet_contract( @@ -761,6 +669,7 @@ where mod tests { use super::*; use crate::nyxd::contract_traits::tests::{mock_coin, IgnoreValue}; + use nym_mixnet_contract_common::ExecuteMsg; // it's enough that this compiles and clippy is happy about it #[allow(dead_code)] @@ -770,56 +679,17 @@ mod tests { ) { match msg { MixnetExecuteMsg::UpdateAdmin { admin } => client.update_admin(admin, None).ignore(), - MixnetExecuteMsg::AssignNodeLayer { mix_id, layer } => { - client.assign_node_layer(mix_id, layer, None).ignore() - } - MixnetExecuteMsg::CreateFamily { label } => client.create_family(label, None).ignore(), - MixnetExecuteMsg::JoinFamily { - join_permit, - family_head, - } => client.join_family(join_permit, family_head, None).ignore(), - MixnetExecuteMsg::LeaveFamily { family_head } => { - client.leave_family(family_head, None).ignore() - } - MixnetExecuteMsg::KickFamilyMember { member } => { - client.kick_family_member(member, None).ignore() - } - MixnetExecuteMsg::CreateFamilyOnBehalf { - owner_address, - label, - } => client - .create_family_on_behalf(owner_address, label, None) - .ignore(), - MixnetExecuteMsg::JoinFamilyOnBehalf { - member_address, - join_permit, - family_head, - } => client - .join_family_on_behalf(member_address, join_permit, family_head, None) - .ignore(), - MixnetExecuteMsg::LeaveFamilyOnBehalf { - member_address, - family_head, - } => client - .leave_family_on_behalf(member_address, family_head, None) - .ignore(), - MixnetExecuteMsg::KickFamilyMemberOnBehalf { - head_address, - member, - } => client - .kick_family_member_on_behalf(head_address, member, None) - .ignore(), MixnetExecuteMsg::UpdateRewardingValidatorAddress { address } => client .update_rewarding_validator_address(address.parse().unwrap(), None) .ignore(), MixnetExecuteMsg::UpdateContractStateParams { updated_parameters } => client .update_contract_state_params(updated_parameters, None) .ignore(), - MixnetExecuteMsg::UpdateActiveSetSize { - active_set_size, + MixnetExecuteMsg::UpdateActiveSetDistribution { + update, force_immediately, } => client - .update_active_set_size(active_set_size, force_immediately, None) + .update_active_set_size(update, force_immediately, None) .ignore(), MixnetExecuteMsg::UpdateRewardingParams { updated_params, @@ -842,12 +712,6 @@ mod tests { MixnetExecuteMsg::BeginEpochTransition {} => { client.begin_epoch_transition(None).ignore() } - MixnetExecuteMsg::AdvanceCurrentEpoch { - new_rewarded_set, - expected_active_set_size, - } => client - .advance_current_epoch(new_rewarded_set, expected_active_set_size, None) - .ignore(), MixnetExecuteMsg::ReconcileEpochEvents { limit } => { client.reconcile_epoch_events(limit, None).ignore() } @@ -887,8 +751,8 @@ mod tests { MixnetExecuteMsg::UnbondMixnodeOnBehalf { owner } => client .unbond_mixnode_on_behalf(owner.parse().unwrap(), None) .ignore(), - MixnetExecuteMsg::UpdateMixnodeCostParams { new_costs } => { - client.update_mixnode_cost_params(new_costs, None).ignore() + MixnetExecuteMsg::UpdateCostParams { new_costs } => { + client.update_cost_params(new_costs, None).ignore() } MixnetExecuteMsg::UpdateMixnodeCostParamsOnBehalf { new_costs, owner } => client .update_mixnode_cost_params_on_behalf(owner.parse().unwrap(), new_costs, None) @@ -928,29 +792,28 @@ mod tests { MixnetExecuteMsg::UpdateGatewayConfigOnBehalf { new_config, owner } => client .update_gateway_config_on_behalf(owner.parse().unwrap(), new_config, None) .ignore(), - MixnetExecuteMsg::DelegateToMixnode { mix_id } => client - .delegate_to_mixnode(mix_id, mock_coin(), None) - .ignore(), + MixnetExecuteMsg::Delegate { node_id: mix_id } => { + client.delegate(mix_id, mock_coin(), None).ignore() + } MixnetExecuteMsg::DelegateToMixnodeOnBehalf { mix_id, delegate } => client .delegate_to_mixnode_on_behalf(delegate.parse().unwrap(), mix_id, mock_coin(), None) .ignore(), - MixnetExecuteMsg::UndelegateFromMixnode { mix_id } => { - client.undelegate_from_mixnode(mix_id, None).ignore() + MixnetExecuteMsg::Undelegate { node_id: mix_id } => { + client.undelegate(mix_id, None).ignore() } MixnetExecuteMsg::UndelegateFromMixnodeOnBehalf { mix_id, delegate } => client .undelegate_to_mixnode_on_behalf(delegate.parse().unwrap(), mix_id, None) .ignore(), - MixnetExecuteMsg::RewardMixnode { - mix_id, - performance, - } => client.reward_mixnode(mix_id, performance, None).ignore(), + MixnetExecuteMsg::RewardNode { node_id, params } => { + client.reward_node(node_id, params, None).ignore() + } MixnetExecuteMsg::WithdrawOperatorReward {} => { client.withdraw_operator_reward(None).ignore() } MixnetExecuteMsg::WithdrawOperatorRewardOnBehalf { owner } => client .withdraw_operator_reward_on_behalf(owner.parse().unwrap(), None) .ignore(), - MixnetExecuteMsg::WithdrawDelegatorReward { mix_id } => { + MixnetExecuteMsg::WithdrawDelegatorReward { node_id: mix_id } => { client.withdraw_delegator_reward(mix_id, None).ignore() } MixnetExecuteMsg::WithdrawDelegatorRewardOnBehalf { mix_id, owner } => client @@ -963,6 +826,25 @@ mod tests { client.migrate_vested_delegation(mix_id, None).ignore() } + ExecuteMsg::AssignRoles { assignment } => { + client.assign_roles(assignment, None).ignore() + } + ExecuteMsg::MigrateMixnode {} => client.migrate_legacy_mixnode(None).ignore(), + ExecuteMsg::MigrateGateway { cost_params } => { + client.migrate_legacy_gateway(cost_params, None).ignore() + } + ExecuteMsg::BondNymNode { + node, + cost_params, + owner_signature, + } => client + .bond_nymnode(node, cost_params, owner_signature, mock_coin(), None) + .ignore(), + ExecuteMsg::UnbondNymNode {} => client.unbond_nymnode(None).ignore(), + ExecuteMsg::UpdateNodeConfig { update } => { + client.update_nymnode_config(update, None).ignore() + } + #[cfg(feature = "contract-testing")] MixnetExecuteMsg::TestingResolveAllPendingEvents { .. } => { client.testing_resolve_all_pending_events(None).ignore() diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_query_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_query_client.rs index fa6bd643fa..02eaf8c4b8 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_query_client.rs @@ -9,7 +9,7 @@ use crate::nyxd::CosmWasmClient; use async_trait::async_trait; use cosmwasm_std::{Coin as CosmWasmCoin, Timestamp}; use nym_contracts_common::ContractBuildInformation; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_vesting_contract_common::{ messages::QueryMsg as VestingQueryMsg, Account, AccountVestingCoins, AccountsResponse, AllDelegationsResponse, BaseVestingAccountInfo, DelegationTimesResponse, @@ -238,7 +238,7 @@ pub trait VestingQueryClient { async fn get_vesting_delegation( &self, address: &str, - mix_id: MixId, + mix_id: NodeId, block_timestamp_secs: u64, ) -> Result { self.query_vesting_contract(VestingQueryMsg::GetDelegation { @@ -252,7 +252,7 @@ pub trait VestingQueryClient { async fn get_total_delegation_amount( &self, address: &str, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.query_vesting_contract(VestingQueryMsg::GetTotalDelegationAmount { address: address.to_string(), @@ -264,7 +264,7 @@ pub trait VestingQueryClient { async fn get_delegation_timestamps( &self, address: &str, - mix_id: MixId, + mix_id: NodeId, ) -> Result { self.query_vesting_contract(VestingQueryMsg::GetDelegationTimes { address: address.to_string(), @@ -275,7 +275,7 @@ pub trait VestingQueryClient { async fn get_all_vesting_delegations_paged( &self, - start_after: Option<(u32, MixId, u64)>, + start_after: Option<(u32, NodeId, u64)>, limit: Option, ) -> Result { self.query_vesting_contract(VestingQueryMsg::GetAllDelegations { start_after, limit }) diff --git a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs index 07472c4262..2f50016f91 100644 --- a/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/contract_traits/vesting_signing_client.rs @@ -9,10 +9,9 @@ use crate::signing::signer::OfflineSigner; use async_trait::async_trait; use cosmrs::AccountId; use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::families::FamilyHead; use nym_mixnet_contract_common::gateway::GatewayConfigUpdate; -use nym_mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; -use nym_mixnet_contract_common::{Gateway, MixId, MixNode}; +use nym_mixnet_contract_common::mixnode::{MixNodeConfigUpdate, NodeCostParams}; +use nym_mixnet_contract_common::{Gateway, MixNode, NodeId}; use nym_vesting_contract_common::messages::ExecuteMsg as VestingExecuteMsg; use nym_vesting_contract_common::{PledgeCap, VestingSpecification}; @@ -28,7 +27,7 @@ pub trait VestingSigningClient { async fn vesting_update_mixnode_cost_params( &self, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, fee: Option, ) -> Result { self.execute_vesting_contract( @@ -124,7 +123,7 @@ pub trait VestingSigningClient { async fn vesting_bond_mixnode( &self, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, pledge: Coin, fee: Option, @@ -204,7 +203,7 @@ pub trait VestingSigningClient { async fn vesting_track_undelegation( &self, address: &str, - mix_id: MixId, + mix_id: NodeId, amount: Coin, fee: Option, ) -> Result { @@ -222,7 +221,7 @@ pub trait VestingSigningClient { async fn vesting_delegate_to_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, amount: Coin, on_behalf_of: Option, fee: Option, @@ -241,7 +240,7 @@ pub trait VestingSigningClient { async fn vesting_undelegate_from_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, on_behalf_of: Option, fee: Option, ) -> Result { @@ -301,7 +300,7 @@ pub trait VestingSigningClient { async fn vesting_withdraw_delegator_reward( &self, - mix_id: MixId, + mix_id: NodeId, fee: Option, ) -> Result { self.execute_vesting_contract( @@ -354,50 +353,6 @@ pub trait VestingSigningClient { ) .await } - - async fn vesting_create_family( - &self, - label: String, - fee: Option, - ) -> Result { - self.execute_vesting_contract(fee, VestingExecuteMsg::CreateFamily { label }, vec![]) - .await - } - - async fn vesting_join_family( - &self, - join_permit: MessageSignature, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_vesting_contract( - fee, - VestingExecuteMsg::JoinFamily { - join_permit, - family_head, - }, - vec![], - ) - .await - } - - async fn vesting_leave_family( - &self, - family_head: FamilyHead, - fee: Option, - ) -> Result { - self.execute_vesting_contract(fee, VestingExecuteMsg::LeaveFamily { family_head }, vec![]) - .await - } - - async fn vesting_kick_family_member( - &self, - member: String, - fee: Option, - ) -> Result { - self.execute_vesting_contract(fee, VestingExecuteMsg::KickFamilyMember { member }, vec![]) - .await - } } #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] @@ -446,21 +401,6 @@ mod tests { msg: VestingExecuteMsg, ) { match msg { - VestingExecuteMsg::CreateFamily { label } => { - client.vesting_create_family(label, None).ignore() - } - VestingExecuteMsg::JoinFamily { - join_permit, - family_head, - } => client - .vesting_join_family(join_permit, family_head, None) - .ignore(), - VestingExecuteMsg::LeaveFamily { family_head } => { - client.vesting_leave_family(family_head, None).ignore() - } - VestingExecuteMsg::KickFamilyMember { member } => { - client.vesting_kick_family_member(member, None).ignore() - } VestingExecuteMsg::TrackReward { amount, address } => client .vesting_track_reward(amount.into(), address, None) .ignore(), diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs index f63399ce18..8feceebdbf 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/query_client.rs @@ -5,7 +5,7 @@ use crate::nyxd; use crate::nyxd::coin::Coin; use crate::nyxd::cosmwasm_client::helpers::{create_pagination, next_page_key}; use crate::nyxd::cosmwasm_client::types::{ - Account, CodeDetails, Contract, ContractCodeId, SequenceResponse, SimulateResponse, + Account, CodeDetails, Contract, ContractCodeId, Model, SequenceResponse, SimulateResponse, }; use crate::nyxd::error::NyxdError; use crate::nyxd::Query; @@ -21,15 +21,14 @@ use cosmrs::proto::cosmos::tx::v1beta1::{ SimulateRequest, SimulateResponse as ProtoSimulateResponse, }; use cosmrs::proto::cosmwasm::wasm::v1::{ - QueryCodeRequest, QueryCodeResponse, QueryCodesRequest, QueryCodesResponse, - QueryContractHistoryRequest, QueryContractHistoryResponse, QueryContractInfoRequest, - QueryContractInfoResponse, QueryContractsByCodeRequest, QueryContractsByCodeResponse, - QueryRawContractStateRequest, QueryRawContractStateResponse, QuerySmartContractStateRequest, - QuerySmartContractStateResponse, + QueryAllContractStateRequest, QueryAllContractStateResponse, QueryCodeRequest, + QueryCodeResponse, QueryCodesRequest, QueryCodesResponse, QueryContractHistoryRequest, + QueryContractHistoryResponse, QueryContractInfoRequest, QueryContractInfoResponse, + QueryContractsByCodeRequest, QueryContractsByCodeResponse, QueryRawContractStateRequest, + QueryRawContractStateResponse, QuerySmartContractStateRequest, QuerySmartContractStateResponse, }; use cosmrs::tendermint::{block, chain, Hash}; use cosmrs::{AccountId, Coin as CosmosCoin, Tx}; -use log::trace; use prost::Message; use serde::{Deserialize, Serialize}; @@ -68,7 +67,7 @@ pub trait CosmWasmClient: TendermintRpcClient { Res: Message + Default, { if let Some(ref abci_path) = path { - trace!("performing query on abci path {abci_path}") + tracing::trace!("performing query on abci path {abci_path}") } let mut buf = Vec::with_capacity(req.encoded_len()); req.encode(&mut buf)?; @@ -218,17 +217,19 @@ pub trait CosmWasmClient: TendermintRpcClient { loop { let mut res = self - .tx_search(query.clone(), false, page, 100, Order::Ascending) + .tx_search(query.clone(), false, page, per_page, Order::Ascending) .await?; - results.append(&mut res.txs); // sanity check for if tendermint's maximum per_page was modified - // we don't want to accidentally be stuck in an infinite loop - if res.total_count == 0 || res.txs.is_empty() { + let early_break = res.total_count == 0 || res.txs.is_empty(); + results.append(&mut res.txs); + + if early_break { break; } - if res.total_count >= per_page { + if res.total_count > results.len() as u32 { page += 1 } else { break; @@ -295,7 +296,7 @@ pub trait CosmWasmClient: TendermintRpcClient { let start = Instant::now(); loop { - log::debug!( + tracing::debug!( "Polling for result of including {} in a block...", broadcasted.hash ); @@ -442,6 +443,38 @@ pub trait CosmWasmClient: TendermintRpcClient { .collect::>()?) } + async fn query_all_contract_state(&self, address: &AccountId) -> Result, NyxdError> { + let path = Some("/cosmwasm.wasm.v1.Query/AllContractState".to_owned()); + + let mut models = Vec::new(); + let mut pagination = None; + + loop { + let req = QueryAllContractStateRequest { + address: address.to_string(), + pagination, + }; + + let mut res = self + .make_abci_query::<_, QueryAllContractStateResponse>(path.clone(), req) + .await?; + + let empty_response = res.models.is_empty(); + models.append(&mut res.models); + + if empty_response { + break; + } + if let Some(next_key) = next_page_key(res.pagination) { + pagination = Some(create_pagination(next_key)) + } else { + break; + } + } + + Ok(models.into_iter().map(Into::into).collect()) + } + async fn query_contract_raw( &self, address: &AccountId, @@ -488,7 +521,7 @@ pub trait CosmWasmClient: TendermintRpcClient { .make_abci_query::<_, QuerySmartContractStateResponse>(path, req) .await?; - trace!("raw query response: {}", String::from_utf8_lossy(&res.data)); + tracing::trace!("raw query response: {}", String::from_utf8_lossy(&res.data)); Ok(serde_json::from_slice(&res.data)?) } diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs index 8d0bb0fb4f..cec29e9c50 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/client_traits/signing_client.rs @@ -25,12 +25,12 @@ use cosmrs::proto::cosmos::tx::signing::v1beta1::SignMode; use cosmrs::staking::{MsgDelegate, MsgUndelegate}; use cosmrs::tx::{self, Msg}; use cosmrs::{cosmwasm, AccountId, Any, Tx}; -use log::debug; use serde::Serialize; use sha2::Digest; use sha2::Sha256; use std::time::SystemTime; use tendermint_rpc::endpoint::broadcast; +use tracing::debug; fn empty_fee() -> tx::Fee { tx::Fee { diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs index d6e42daac7..559ad434a2 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/helpers.rs @@ -7,9 +7,9 @@ use base64::Engine; use cosmrs::abci::TxMsgData; use cosmrs::cosmwasm::MsgExecuteContractResponse; use cosmrs::proto::cosmos::base::query::v1beta1::{PageRequest, PageResponse}; -use log::error; use prost::bytes::Bytes; use tendermint_rpc::endpoint::broadcast; +use tracing::error; pub use cosmrs::abci::MsgResponse; diff --git a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs index 564a17441e..26003c1d29 100644 --- a/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs +++ b/common/client-libs/validator-client/src/nyxd/cosmwasm_client/types.rs @@ -27,13 +27,34 @@ use cosmrs::vesting::{ }; use cosmrs::{AccountId, Any, Coin as CosmosCoin}; use prost::Message; -use serde::Serialize; +use serde::{Deserialize, Serialize}; pub use cosmrs::abci::GasInfo; pub use cosmrs::abci::MsgResponse; pub type ContractCodeId = u64; +// yet another thing to put in cosmrs +#[derive(Serialize, Deserialize)] +pub struct Model { + #[serde(with = "nym_serde_helpers::hex")] + pub key: Vec, + + #[serde(with = "nym_serde_helpers::base64")] + pub value: Vec, +} + +// follow the cosmwasm serialisation format, i.e. hex for key and base64 for value + +impl From for Model { + fn from(model: cosmrs::proto::cosmwasm::wasm::v1::Model) -> Self { + Model { + key: model.key, + value: model.value, + } + } +} + #[derive(Serialize)] pub struct EmptyMsg {} diff --git a/common/client-libs/validator-client/src/nyxd/error.rs b/common/client-libs/validator-client/src/nyxd/error.rs index c71ed596ad..e4b2f1b9e5 100644 --- a/common/client-libs/validator-client/src/nyxd/error.rs +++ b/common/client-libs/validator-client/src/nyxd/error.rs @@ -154,6 +154,23 @@ pub enum NyxdError { #[error("the response data has invalid size. got {got} bytes, but expected {expected} bytes instead")] MalformedResponseData { got: usize, expected: usize }, + + #[error( + "one of the extension query for {contract} failed with the following message: {message}" + )] + ExtensionQueryFailure { contract: String, message: String }, +} + +impl NyxdError { + pub fn extension_query_failure( + contract: impl Into, + message: impl Into, + ) -> Self { + NyxdError::ExtensionQueryFailure { + contract: contract.into(), + message: message.into(), + } + } } // The purpose of parsing the abci query result is that we want to generate the `pretty_log` if diff --git a/common/commands/src/ecash/generate_ticket.rs b/common/commands/src/ecash/generate_ticket.rs index 67a85ab093..0da2a878c4 100644 --- a/common/commands/src/ecash/generate_ticket.rs +++ b/common/commands/src/ecash/generate_ticket.rs @@ -9,10 +9,15 @@ use comfy_table::Table; use nym_credential_storage::initialise_persistent_storage; use nym_credential_storage::storage::Storage; use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; +use nym_credentials_interface::TicketType; use std::path::PathBuf; #[derive(Debug, Parser)] pub struct Args { + /// Specify which type of ticketbook + #[clap(long, default_value_t = TicketType::V1MixnetEntry)] + pub(crate) ticketbook_type: TicketType, + /// Specify the index of the ticket to retrieve from the ticketbook. /// By default, the current unspent value is used. #[clap(long, group = "output")] @@ -62,7 +67,7 @@ pub async fn execute(args: Args) -> anyhow::Result<()> { let persistent_storage = initialise_persistent_storage(&credentials_store).await; let Some(mut next_ticketbook) = persistent_storage - .get_next_unspent_usable_ticketbook(0) + .get_next_unspent_usable_ticketbook(args.ticketbook_type.to_string(), 0) .await? else { bail!( diff --git a/common/commands/src/ecash/import_ticket_book.rs b/common/commands/src/ecash/import_ticket_book.rs index 2c5d597659..14ebd4b686 100644 --- a/common/commands/src/ecash/import_ticket_book.rs +++ b/common/commands/src/ecash/import_ticket_book.rs @@ -1,8 +1,6 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::utils::CommonConfigsWrapper; -use anyhow::bail; use clap::ArgGroup; use clap::Parser; use nym_credential_storage::initialise_persistent_storage; @@ -31,7 +29,7 @@ impl FromStr for CredentialDataWrapper { pub struct Args { /// Config file of the client that is supposed to use the credential. #[clap(long)] - pub(crate) client_config: PathBuf, + pub(crate) credentials_store: PathBuf, /// Explicitly provide the encoded credential data (as base58) #[clap(long, group = "cred_data")] @@ -70,21 +68,7 @@ impl Args { } pub async fn execute(args: Args) -> anyhow::Result<()> { - let loaded = CommonConfigsWrapper::try_load(&args.client_config)?; - - if let Ok(id) = loaded.try_get_id() { - println!("loaded config file for client '{id}'"); - } - - let Ok(credentials_store) = loaded.try_get_credentials_store() else { - bail!("the loaded config does not have a credentials store information") - }; - - println!( - "using credentials store at '{}'", - credentials_store.display() - ); - let credentials_store = initialise_persistent_storage(credentials_store).await; + let credentials_store = initialise_persistent_storage(args.credentials_store.clone()).await; let version = args.version; let standalone = args.standalone; diff --git a/common/commands/src/ecash/issue_ticket_book.rs b/common/commands/src/ecash/issue_ticket_book.rs index 28e8c3eafc..0097f4676a 100644 --- a/common/commands/src/ecash/issue_ticket_book.rs +++ b/common/commands/src/ecash/issue_ticket_book.rs @@ -107,7 +107,7 @@ async fn issue_to_file(args: Args, client: SigningClient) -> anyhow::Result<()> utils::issue_credential(&client, &credentials_store, &secret, args.ticketbook_type).await?; let ticketbook = credentials_store - .get_next_unspent_usable_ticketbook(0) + .get_next_unspent_usable_ticketbook(args.ticketbook_type.to_string(), 0) .await? .ok_or(anyhow!("we just issued a ticketbook, it must be present!"))? .ticketbook; diff --git a/common/commands/src/validator/cosmwasm/generators/mixnet.rs b/common/commands/src/validator/cosmwasm/generators/mixnet.rs index afd21fde9a..7f97809e36 100644 --- a/common/commands/src/validator/cosmwasm/generators/mixnet.rs +++ b/common/commands/src/validator/cosmwasm/generators/mixnet.rs @@ -4,6 +4,7 @@ use clap::Parser; use cosmwasm_std::Decimal; use log::{debug, info}; +use nym_mixnet_contract_common::reward_params::RewardedSetParams; use nym_mixnet_contract_common::{ InitialRewardingParams, InstantiateMsg, OperatingCostRange, Percent, ProfitMarginRange, }; @@ -56,11 +57,17 @@ pub struct Args { #[clap(long, default_value_t = 2)] pub interval_pool_emission: u64, - #[clap(long, default_value_t = 240)] - pub rewarded_set_size: u32, + #[clap(long, default_value_t = 50)] + pub(crate) entry_gateways: u32, + + #[clap(long, default_value_t = 70)] + pub(crate) exit_gateways: u32, + + #[clap(long, default_value_t = 120)] + pub(crate) mixnodes: u32, - #[clap(long, default_value_t = 240)] - pub active_set_size: u32, + #[clap(long, default_value_t = 0)] + pub(crate) standby: u32, #[clap(long, default_value_t = Percent::zero())] pub minimum_profit_margin_percent: Percent, @@ -95,8 +102,13 @@ pub async fn generate(args: Args) { .expect("active_set_work_factor can't be converted to Decimal"), interval_pool_emission: Percent::from_percentage_value(args.interval_pool_emission) .expect("interval_pool_emission can't be converted to Percent"), - rewarded_set_size: args.rewarded_set_size, - active_set_size: args.active_set_size, + + rewarded_set_params: RewardedSetParams { + entry_gateways: args.entry_gateways, + exit_gateways: args.exit_gateways, + mixnodes: args.mixnodes, + standby: args.standby, + }, }; debug!("initial_rewarding_params: {:?}", initial_rewarding_params); diff --git a/common/commands/src/validator/cosmwasm/mod.rs b/common/commands/src/validator/cosmwasm/mod.rs index 43dcf08951..2fbd63a535 100644 --- a/common/commands/src/validator/cosmwasm/mod.rs +++ b/common/commands/src/validator/cosmwasm/mod.rs @@ -7,13 +7,14 @@ pub mod execute_contract; pub mod generators; pub mod init_contract; pub mod migrate_contract; +pub mod raw_contract_state; pub mod upload_contract; #[derive(Debug, Args)] #[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] pub struct Cosmwasm { #[clap(subcommand)] - pub command: Option, + pub command: CosmwasmCommands, } #[derive(Debug, Subcommand)] @@ -28,4 +29,6 @@ pub enum CosmwasmCommands { Migrate(crate::validator::cosmwasm::migrate_contract::Args), /// Execute a WASM smart contract method Execute(crate::validator::cosmwasm::execute_contract::Args), + /// Obtain raw contract state of a cosmwasm smart contract + RawContractState(crate::validator::cosmwasm::raw_contract_state::Args), } diff --git a/common/commands/src/validator/cosmwasm/raw_contract_state.rs b/common/commands/src/validator/cosmwasm/raw_contract_state.rs new file mode 100644 index 0000000000..29771b7274 --- /dev/null +++ b/common/commands/src/validator/cosmwasm/raw_contract_state.rs @@ -0,0 +1,39 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::QueryClient; +use clap::Parser; +use cosmrs::AccountId; +use log::trace; +use nym_validator_client::nyxd::CosmWasmClient; +use std::fs; +use std::fs::File; +use std::path::PathBuf; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long, value_parser)] + #[clap(help = "The address of contract to get the state of")] + pub contract: AccountId, + + #[clap(short, long)] + #[clap(help = "Output file for the retrieved contract state")] + pub output: PathBuf, +} + +pub async fn execute(args: Args, client: QueryClient) -> anyhow::Result<()> { + trace!("args: {args:?}"); + + let output = File::create(&args.output)?; + let raw = client.query_all_contract_state(&args.contract).await?; + + serde_json::to_writer(output, &raw)?; + println!( + "wrote {} key-value from {} pairs into '{}'", + raw.len(), + args.contract, + fs::canonicalize(args.output)?.display() + ); + + Ok(()) +} diff --git a/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs b/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs index 54b2516d6e..dc8a2d591f 100644 --- a/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/delegate_to_mixnode.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use nym_mixnet_contract_common::{Coin, MixId}; +use nym_mixnet_contract_common::{Coin, NodeId}; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, @@ -43,7 +43,7 @@ pub async fn delegate_to_mixnode(args: Args, client: SigningClient) { let coin = Coin::new(args.amount, denom); let res = client - .delegate_to_mixnode(mix_id, coin.into(), None) + .delegate(mix_id, coin.into(), None) .await .expect("failed to delegate to mixnode!"); diff --git a/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs b/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs index 787d27a56e..77d59d6bb3 100644 --- a/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs +++ b/common/commands/src/validator/mixnet/delegators/delegate_to_multiple_mixnodes.rs @@ -1,21 +1,18 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::cmp::Ordering; -use std::collections::{HashMap, HashSet}; -use std::fs; -use std::fs::OpenOptions; - use clap::Parser; use comfy_table::Table; use csv::WriterBuilder; use log::info; use nym_mixnet_contract_common::ExecuteMsg; -use nym_mixnet_contract_common::ExecuteMsg::{DelegateToMixnode, UndelegateFromMixnode}; - -use nym_mixnet_contract_common::PendingEpochEventKind::{Delegate, Undelegate}; +use nym_mixnet_contract_common::PendingEpochEventKind; use nym_validator_client::nyxd::contract_traits::{NymContractsProvider, PagedMixnetQueryClient}; use nym_validator_client::nyxd::Coin; +use std::cmp::Ordering; +use std::collections::{HashMap, HashSet}; +use std::fs; +use std::fs::OpenOptions; use crate::context::SigningClient; use crate::utils::pretty_coin; @@ -40,7 +37,7 @@ pub struct Args { #[derive(Debug)] pub struct InputFileRow { - pub mix_id: String, + pub node_id: String, pub amount: Coin, } #[derive(Debug)] @@ -76,7 +73,7 @@ impl InputFileReader { } rows.push(InputFileRow { - mix_id, + node_id: mix_id, amount: Coin { amount: micro_nym_amount, denom: "unym".to_string(), @@ -140,8 +137,10 @@ async fn fetch_delegation_data( let mut pending_delegation_map: HashMap = HashMap::new(); for delegation in delegations { - existing_delegation_map - .insert(delegation.mix_id.to_string(), Coin::from(delegation.amount)); + existing_delegation_map.insert( + delegation.node_id.to_string(), + Coin::from(delegation.amount), + ); } // Look for pending delegate / undelegate events which might be of interest to us @@ -155,27 +154,27 @@ async fn fetch_delegation_data( for event in pending_events { match event.event.kind { // If a pending undelegate tx is found, remove it from delegation map - Undelegate { owner, mix_id, .. } => { + PendingEpochEventKind::Undelegate { owner, node_id, .. } => { if owner == address.as_ref() - && existing_delegation_map.contains_key(&mix_id.to_string()) + && existing_delegation_map.contains_key(&node_id.to_string()) { - existing_delegation_map.remove(&mix_id.to_string()); + existing_delegation_map.remove(&node_id.to_string()); } } // If a pending delegation event is found, gather them to consolidate later - Delegate { + PendingEpochEventKind::Delegate { owner, - mix_id, + node_id, amount, .. } => { if owner == address.as_ref() { let mut amount = Coin::from(amount); - if let Some(pending_record) = pending_delegation_map.get(&mix_id.to_string()) { + if let Some(pending_record) = pending_delegation_map.get(&node_id.to_string()) { amount.amount += pending_record.amount; } - pending_delegation_map.insert(mix_id.to_string(), amount); + pending_delegation_map.insert(node_id.to_string(), amount); } } _ => {} @@ -217,7 +216,7 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) { for row in &records.rows { let input_amount = row.amount.amount; let existing_delegation_amount = existing_delegation_map - .get(&row.mix_id) + .get(&row.node_id) .map_or(0, |coin| coin.amount); match existing_delegation_amount.cmp(&input_amount) { @@ -229,25 +228,26 @@ pub async fn delegate_to_multiple_mixnodes(args: Args, client: SigningClient) { amount: input_amount - existing_delegation_amount, denom: row.amount.denom.clone(), }; - let mix_id = row.mix_id.clone().parse::().unwrap(); - delegation_msgs.push((DelegateToMixnode { mix_id }, vec![difference.clone()])); + let node_id = row.node_id.clone().parse::().unwrap(); + delegation_msgs.push((ExecuteMsg::Delegate { node_id }, vec![difference.clone()])); delegation_table.add_row(&[ - row.mix_id.clone(), + row.node_id.clone(), pretty_coin(&row.amount), pretty_coin(&difference), ]); } Ordering::Greater => { - let mix_id = row.mix_id.clone().parse::().unwrap(); + let node_id = row.node_id.clone().parse::().unwrap(); let coins: Vec = vec![]; - undelegation_msgs.push((UndelegateFromMixnode { mix_id }, coins)); - undelegation_table.add_row(&[row.mix_id.clone()]); + undelegation_msgs.push((ExecuteMsg::Undelegate { node_id }, coins)); + undelegation_table.add_row(&[row.node_id.clone()]); if row.amount.amount > 0 { - delegation_msgs.push((DelegateToMixnode { mix_id }, vec![row.amount.clone()])); + delegation_msgs + .push((ExecuteMsg::Delegate { node_id }, vec![row.amount.clone()])); delegation_table.add_row(&[ - row.mix_id.clone(), + row.node_id.clone(), pretty_coin(&row.amount), pretty_coin(&row.amount), ]); diff --git a/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs b/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs index 8523e63639..8ac1ab9c07 100644 --- a/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs +++ b/common/commands/src/validator/mixnet/delegators/migrate_vested_delegation.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs b/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs index b1a223b603..b05f44b6fb 100644 --- a/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs +++ b/common/commands/src/validator/mixnet/delegators/query_for_delegations.rs @@ -66,7 +66,7 @@ async fn print_delegations(delegations: Vec, client: &SigningClientW for delegation in delegations { table.add_row(vec![ to_iso_timestamp(delegation.height as u32, client).await, - delegation.mix_id.to_string(), + delegation.node_id.to_string(), pretty_cosmwasm_coin(&delegation.amount), delegation .proxy @@ -93,7 +93,7 @@ async fn print_delegation_events(events: Vec, client: &Signin match event.event.kind { PendingEpochEventKind::Delegate { owner, - mix_id, + node_id: mix_id, amount, proxy, .. @@ -110,7 +110,7 @@ async fn print_delegation_events(events: Vec, client: &Signin } PendingEpochEventKind::Undelegate { owner, - mix_id, + node_id: mix_id, proxy, .. } => { diff --git a/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs b/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs index 327a3c0fd3..83426a316e 100644 --- a/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs +++ b/common/commands/src/validator/mixnet/delegators/rewards/claim_delegator_reward.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs b/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs index aa6b8dbe44..d36c4a7b17 100644 --- a/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs +++ b/common/commands/src/validator/mixnet/delegators/rewards/vesting_claim_delegator_reward.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs b/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs index cadb6c272f..19593ed5ce 100644 --- a/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/undelegate_from_mixnode.rs @@ -4,13 +4,13 @@ use crate::context::SigningClient; use clap::Parser; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, @@ -36,7 +36,7 @@ pub async fn undelegate_from_mixnode(args: Args, client: SigningClient) { }; let res = client - .undelegate_from_mixnode(mix_id, None) + .undelegate(mix_id, None) .await .expect("failed to remove stake from mixnode!"); diff --git a/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs b/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs index 45c4ffa5c3..3fa3fd7cee 100644 --- a/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/vesting_delegate_to_mixnode.rs @@ -4,7 +4,7 @@ use clap::Parser; use log::info; -use nym_mixnet_contract_common::{Coin, MixId}; +use nym_mixnet_contract_common::{Coin, NodeId}; use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_validator_client::nyxd::contract_traits::VestingSigningClient; @@ -13,7 +13,7 @@ use crate::context::SigningClient; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs b/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs index 13bae0b7a0..9daf8691d3 100644 --- a/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs +++ b/common/commands/src/validator/mixnet/delegators/vesting_undelegate_from_mixnode.rs @@ -3,7 +3,7 @@ use clap::Parser; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_validator_client::nyxd::contract_traits::VestingSigningClient; @@ -12,7 +12,7 @@ use crate::context::SigningClient; #[derive(Debug, Parser)] pub struct Args { #[clap(long)] - pub mix_id: Option, + pub mix_id: Option, #[clap(long)] pub identity_key: Option, diff --git a/common/commands/src/validator/mixnet/operators/gateway/mod.rs b/common/commands/src/validator/mixnet/operators/gateway/mod.rs index 8c4b8753c3..dcc81efbcc 100644 --- a/common/commands/src/validator/mixnet/operators/gateway/mod.rs +++ b/common/commands/src/validator/mixnet/operators/gateway/mod.rs @@ -5,6 +5,7 @@ use clap::{Args, Subcommand}; pub mod bond_gateway; pub mod gateway_bonding_sign_payload; +pub mod nymnode_migration; pub mod settings; pub mod unbond_gateway; pub mod vesting_bond_gateway; @@ -31,4 +32,6 @@ pub enum MixnetOperatorsGatewayCommands { VestingUnbond(vesting_unbond_gateway::Args), /// Create base58-encoded payload required for producing valid bonding signature. CreateGatewayBondingSignPayload(gateway_bonding_sign_payload::Args), + /// Migrate the gateway into a Nym Node + MigrateToNymnode(nymnode_migration::Args), } diff --git a/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs b/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs new file mode 100644 index 0000000000..a6b22a2d4b --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/gateway/nymnode_migration.rs @@ -0,0 +1,56 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use cosmwasm_std::Uint128; +use log::info; +use nym_contracts_common::Percent; +use nym_mixnet_contract_common::{ + NodeCostParams, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; +use nym_validator_client::nyxd::CosmWasmCoin; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub profit_margin_percent: Option, + + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, +} + +pub async fn migrate_to_nymnode(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + let cost_params = + if args.profit_margin_percent.is_some() || args.interval_operating_cost.is_some() { + Some(NodeCostParams { + profit_margin_percent: Percent::from_percentage_value( + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), + ) + .unwrap(), + interval_operating_cost: CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), + }, + }) + } else { + None + }; + + let res = client + .migrate_legacy_gateway(cost_params, None) + .await + .expect("failed to migrate gateway!"); + + info!("migration result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs index 862a797566..410ce91d28 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/bond_mixnode.rs @@ -1,20 +1,21 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::context::SigningClient; use clap::Parser; use cosmwasm_std::Uint128; use log::{info, warn}; - use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::{Coin, MixNodeCostParams, Percent}; +use nym_mixnet_contract_common::{ + Coin, NodeCostParams, Percent, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, + DEFAULT_PROFIT_MARGIN_PERCENT, +}; use nym_network_defaults::{ DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, }; use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; use nym_validator_client::nyxd::CosmWasmCoin; -use crate::context::SigningClient; - #[derive(Debug, Parser)] pub struct Args { #[clap(long)] @@ -42,7 +43,7 @@ pub struct Args { pub version: String, #[clap(long)] - pub profit_margin_percent: Option, + pub profit_margin_percent: Option, #[clap( long, @@ -85,14 +86,18 @@ pub async fn bond_mixnode(args: Args, client: SigningClient) { let coin = Coin::new(args.amount, denom); - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value( - args.profit_margin_percent.unwrap_or(10) as u64, + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), ) .unwrap(), interval_operating_cost: CosmWasmCoin { denom: denom.into(), - amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), }, }; diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs deleted file mode 100644 index 6e2c664a35..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use clap::Parser; -use log::info; -use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; - -#[derive(Debug, Parser)] -pub struct Args { - /// Label that is going to be used for creating the family - #[arg(long)] - pub family_label: String, -} - -pub async fn create_family(args: Args, client: SigningClient) { - info!("Create family"); - - let res = client - .create_family(args.family_label, None) - .await - .expect("failed to create family"); - - info!("Family creation result: {:?}", res); -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs deleted file mode 100644 index ac397150ea..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/create_family_join_permit_sign_payload.rs +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2021 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::QueryClient; -use crate::utils::DataWrapper; -use clap::Parser; -use cosmrs::AccountId; -use log::info; -use nym_bin_common::output_format::OutputFormat; -use nym_crypto::asymmetric::identity; -use nym_mixnet_contract_common::construct_family_join_permit; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; - -#[derive(Debug, Parser)] -pub struct Args { - /// Account address (i.e. owner of the family head) which will be used for issuing the permit - #[arg(long)] - pub address: AccountId, - - // might as well validate the value when parsing the arguments - /// Identity of the member for whom we're issuing the permit - #[arg(long)] - pub member: identity::PublicKey, - - #[clap(short, long, default_value_t = OutputFormat::default())] - output: OutputFormat, -} - -pub async fn create_family_join_permit_sign_payload(args: Args, client: QueryClient) { - info!("Create family join permit sign payload"); - - // get the address of our mixnode to recover the family head information - let Some(mixnode) = client - .get_owned_mixnode(&args.address) - .await - .unwrap() - .mixnode_details - else { - eprintln!("{} does not seem to even own a mixnode!", args.address); - return; - }; - - // make sure this mixnode is actually a family head - if client - .get_node_family_by_head(mixnode.bond_information.identity().to_string()) - .await - .unwrap() - .family - .is_none() - { - eprintln!("{} does not even seem to own a family!", args.address); - return; - } - - let nonce = match client.get_signing_nonce(&args.address).await { - Ok(nonce) => nonce, - Err(err) => { - eprint!( - "failed to query for the signing nonce of {}: {err}", - args.address - ); - return; - } - }; - - let head = FamilyHead::new(mixnode.bond_information.identity()); - - let payload = construct_family_join_permit(nonce, head, args.member.to_base58_string()); - let wrapper = DataWrapper::new(payload.to_base58_string().unwrap()); - println!("{}", args.output.format(&wrapper)) -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs deleted file mode 100644 index 08b4c471c0..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/join_family.rs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use clap::Parser; -use log::info; -use nym_contracts_common::signing::MessageSignature; -use nym_crypto::asymmetric::identity; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; - -#[derive(Debug, Parser)] -pub struct Args { - /// The head of the family that we intend to join - #[arg(long)] - pub family_head: identity::PublicKey, - - /// Permission, as provided by the family head, for joining the family - #[arg(long)] - pub join_permit: MessageSignature, -} - -pub async fn join_family(args: Args, client: SigningClient) { - info!("Join family"); - - let family_head = FamilyHead::new(args.family_head.to_base58_string()); - - let res = client - .join_family(args.join_permit, family_head, None) - .await - .expect("failed to join family"); - - info!("Family join result: {:?}", res); -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/kick_family_member.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/kick_family_member.rs deleted file mode 100644 index 27b5d71d7d..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/kick_family_member.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use clap::Parser; -use log::info; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; -use nym_validator_client::nyxd::contract_traits::VestingSigningClient; - -#[derive(Debug, Parser)] -pub struct Args { - /// The member of the family that we intend to kick - #[arg(long)] - pub member: identity::PublicKey, - - /// Indicates whether the family was created (and managed) via the vesting contract - #[arg(long)] - pub with_vesting_account: bool, -} - -pub async fn kick_family_member(args: Args, client: SigningClient) { - info!("Leave family"); - - let member = args.member.to_base58_string(); - - let res = if args.with_vesting_account { - client - .vesting_kick_family_member(member, None) - .await - .expect("failed to kick family member with vesting account") - } else { - client - .kick_family_member(member, None) - .await - .expect("failed to kick family member") - }; - - info!("Family leave result: {:?}", res); -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs deleted file mode 100644 index d9c31e3933..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/leave_family.rs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::context::SigningClient; -use clap::Parser; -use log::info; -use nym_crypto::asymmetric::identity; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; - -#[derive(Debug, Parser)] -pub struct Args { - /// The head of the family that we intend to leave - #[arg(long)] - pub family_head: identity::PublicKey, -} - -pub async fn leave_family(args: Args, client: SigningClient) { - info!("Leave family"); - - let family_head = FamilyHead::new(args.family_head.to_base58_string()); - - let res = client - .leave_family(family_head, None) - .await - .expect("failed to leave family"); - - info!("Family leave result: {:?}", res); -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/families/mod.rs b/common/commands/src/validator/mixnet/operators/mixnode/families/mod.rs deleted file mode 100644 index c343ef977f..0000000000 --- a/common/commands/src/validator/mixnet/operators/mixnode/families/mod.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use clap::{Args, Subcommand}; - -pub mod create_family; -pub mod create_family_join_permit_sign_payload; -pub mod join_family; -pub mod kick_family_member; -pub mod leave_family; - -#[derive(Debug, Args)] -#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] -pub struct MixnetOperatorsMixnodeFamilies { - #[clap(subcommand)] - pub command: MixnetOperatorsMixnodeFamiliesCommands, -} - -#[derive(Debug, Subcommand)] -pub enum MixnetOperatorsMixnodeFamiliesCommands { - /// Create family - CreateFamily(create_family::Args), - - /// Join family - JoinFamily(join_family::Args), - - /// Leave family, - LeaveFamily(leave_family::Args), - - /// Kick family member - KickFamilyMember(kick_family_member::Args), - - /// Create a message payload that is required to get signed in order to obtain a permit for joining family - CreateFamilyJoinPermitSignPayload(create_family_join_permit_sign_payload::Args), -} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs b/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs index 3eda67a2e9..7525657547 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/mixnode_bonding_sign_payload.rs @@ -8,7 +8,8 @@ use cosmwasm_std::{Coin, Uint128}; use nym_bin_common::output_format::OutputFormat; use nym_contracts_common::Percent; use nym_mixnet_contract_common::{ - construct_legacy_mixnode_bonding_sign_payload, MixNodeCostParams, + construct_legacy_mixnode_bonding_sign_payload, NodeCostParams, + DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, }; use nym_network_defaults::{ DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, @@ -40,7 +41,7 @@ pub struct Args { pub version: String, #[clap(long)] - pub profit_margin_percent: Option, + pub profit_margin_percent: Option, #[clap( long, @@ -75,14 +76,18 @@ pub async fn create_payload(args: Args, client: SigningClient) { let coin = Coin::new(args.amount, denom); - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value( - args.profit_margin_percent.unwrap_or(10) as u64, + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), ) .unwrap(), interval_operating_cost: CosmWasmCoin { denom: denom.into(), - amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), }, }; diff --git a/common/commands/src/validator/mixnet/operators/mixnode/mod.rs b/common/commands/src/validator/mixnet/operators/mixnode/mod.rs index abb5060e9b..3244a189bc 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/mod.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/mod.rs @@ -5,10 +5,10 @@ use clap::{Args, Subcommand}; pub mod bond_mixnode; pub mod decrease_pledge; -pub mod families; pub mod keys; pub mod migrate_vested_mixnode; pub mod mixnode_bonding_sign_payload; +pub mod nymnode_migration; pub mod pledge_more; pub mod rewards; pub mod settings; @@ -33,8 +33,6 @@ pub enum MixnetOperatorsMixnodeCommands { Rewards(rewards::MixnetOperatorsMixnodeRewards), /// Manage your mixnode settings stored in the directory Settings(settings::MixnetOperatorsMixnodeSettings), - /// Operations for mixnode families - Families(families::MixnetOperatorsMixnodeFamilies), /// Bond to a mixnode Bond(bond_mixnode::Args), /// Unbond from a mixnode @@ -55,4 +53,6 @@ pub enum MixnetOperatorsMixnodeCommands { DecreasePledgeVesting(vesting_decrease_pledge::Args), /// Migrate the mixnode to use liquid tokens MigrateVestedNode(migrate_vested_mixnode::Args), + /// Migrate the mixnode into a Nym Node + MigrateToNymnode(nymnode_migration::Args), } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs b/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs new file mode 100644 index 0000000000..fe46e2c11a --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/mixnode/nymnode_migration.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn migrate_to_nymnode(_args: Args, client: SigningClient) { + let res = client + .migrate_legacy_mixnode(None) + .await + .expect("failed to migrate mixnode!"); + + info!("migration result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs index 7c09ba2946..cd145737ca 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/settings/update_cost_params.rs @@ -2,12 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::context::SigningClient; +use crate::validator::mixnet::operators::nymnode; use clap::Parser; -use cosmwasm_std::Uint128; -use log::info; -use nym_mixnet_contract_common::{MixNodeCostParams, Percent}; -use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; -use nym_validator_client::nyxd::CosmWasmCoin; #[derive(Debug, Parser)] pub struct Args { @@ -24,62 +20,14 @@ pub struct Args { pub interval_operating_cost: Option, } -pub async fn update_cost_params(args: Args, client: SigningClient) { - let denom = client.current_chain_details().mix_denom.base.as_str(); - - fn convert_to_percent(value: u64) -> Percent { - Percent::from_percentage_value(value).expect("Invalid value") - } - - let default_profit_margin: Percent = convert_to_percent(20); - - let mixownership_response = match client.get_owned_mixnode(&client.address()).await { - Ok(response) => response, - Err(_) => { - eprintln!("Failed to obtain owned mixnode"); - return; - } - }; - - let mix_id = match mixownership_response.mixnode_details { - Some(details) => details.bond_information.mix_id, - None => { - eprintln!("Failed to obtain mixnode details"); - return; - } - }; - - let rewarding_response = match client.get_mixnode_rewarding_details(mix_id).await { - Ok(details) => details, - Err(_) => { - eprintln!("Failed to obtain rewarding details"); - return; - } - }; - - let profit_margin_percent = rewarding_response - .rewarding_details - .map(|rd| rd.cost_params.profit_margin_percent) - .unwrap_or(default_profit_margin); - - let profit_margin_value = args - .profit_margin_percent - .map(|pm| convert_to_percent(pm as u64)) - .unwrap_or(profit_margin_percent); - - let cost_params = MixNodeCostParams { - profit_margin_percent: profit_margin_value, - interval_operating_cost: CosmWasmCoin { - denom: denom.into(), - amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), +pub async fn update_cost_params(args: Args, client: SigningClient) -> anyhow::Result<()> { + // the below can handle both, nymnode and legacy mixnode + nymnode::settings::update_cost_params::update_cost_params( + nymnode::settings::update_cost_params::Args { + profit_margin_percent: args.profit_margin_percent, + interval_operating_cost: args.interval_operating_cost, }, - }; - - info!("Starting mixnode params updating!"); - let res = client - .update_mixnode_cost_params(cost_params, None) - .await - .expect("failed to update cost params"); - - info!("Cost params result: {:?}", res) + client, + ) + .await } diff --git a/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs index 415f9fe2b3..d55d6463e7 100644 --- a/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs +++ b/common/commands/src/validator/mixnet/operators/mixnode/vesting_bond_mixnode.rs @@ -6,7 +6,9 @@ use clap::Parser; use cosmwasm_std::Uint128; use log::{info, warn}; use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::{Coin, MixNodeCostParams}; +use nym_mixnet_contract_common::{ + Coin, NodeCostParams, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, +}; use nym_mixnet_contract_common::{MixNode, Percent}; use nym_network_defaults::{ DEFAULT_HTTP_API_LISTENING_PORT, DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT, @@ -40,7 +42,7 @@ pub struct Args { pub version: String, #[clap(long)] - pub profit_margin_percent: Option, + pub profit_margin_percent: Option, #[clap( long, @@ -84,14 +86,18 @@ pub async fn vesting_bond_mixnode(client: SigningClient, args: Args, denom: &str let coin = Coin::new(args.amount, denom); - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value( - args.profit_margin_percent.unwrap_or(10) as u64, + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), ) .unwrap(), interval_operating_cost: CosmWasmCoin { denom: denom.into(), - amount: Uint128::new(args.interval_operating_cost.unwrap_or(40_000_000)), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), }, }; diff --git a/common/commands/src/validator/mixnet/operators/mod.rs b/common/commands/src/validator/mixnet/operators/mod.rs index cde9dc3754..cf6e2d9d5f 100644 --- a/common/commands/src/validator/mixnet/operators/mod.rs +++ b/common/commands/src/validator/mixnet/operators/mod.rs @@ -6,6 +6,7 @@ use clap::{Args, Subcommand}; pub mod gateway; pub mod identity_key; pub mod mixnode; +pub mod nymnode; #[derive(Debug, Args)] #[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] @@ -17,9 +18,11 @@ pub struct MixnetOperators { #[allow(clippy::large_enum_variant)] #[derive(Debug, Subcommand)] pub enum MixnetOperatorsCommands { - /// Manage your mixnode + /// Manage your Nym Node + Nymnode(nymnode::MixnetOperatorsNymNode), + /// Manage your legacy mixnode Mixnode(mixnode::MixnetOperatorsMixnode), - /// Manage your gateway + /// Manage your legacy gateway Gateway(gateway::MixnetOperatorsGateway), /// Sign messages using your private identity key IdentityKey(identity_key::MixnetOperatorsIdentityKey), diff --git a/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs b/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs new file mode 100644 index 0000000000..acde3d39e3 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/bond_nymnode.rs @@ -0,0 +1,89 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use cosmwasm_std::Uint128; +use log::{info, warn}; +use nym_contracts_common::signing::MessageSignature; +use nym_mixnet_contract_common::{ + Coin, NodeCostParams, Percent, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, + DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; +use nym_validator_client::nyxd::CosmWasmCoin; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: String, + + #[clap(long)] + pub signature: MessageSignature, + + #[clap(long)] + pub http_api_port: Option, + + #[clap(long)] + pub identity_key: String, + + #[clap(long)] + pub profit_margin_percent: Option, + + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, + + #[clap( + long, + help = "bonding amount in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub amount: u128, + + #[clap(short, long)] + pub force: bool, +} + +pub async fn bond_nymnode(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + info!("Starting nym node bonding!"); + + // if we're trying to bond less than 1 token + if args.amount < 1_000_000 && !args.force { + warn!("You're trying to bond only {}{} which is less than 1 full token. Are you sure that's what you want? If so, run with `--force` or `-f` flag", args.amount, denom); + return; + } + + let nymnode = nym_mixnet_contract_common::NymNode { + host: args.host, + custom_http_port: args.http_api_port, + identity_key: args.identity_key, + }; + + let coin = Coin::new(args.amount, denom); + + let cost_params = NodeCostParams { + profit_margin_percent: Percent::from_percentage_value( + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), + ) + .unwrap(), + interval_operating_cost: CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), + }, + }; + + let res = client + .bond_nymnode(nymnode, cost_params, args.signature, coin.into(), None) + .await + .expect("failed to bond nymnode!"); + + info!("Bonding result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/keys/decode_node_key.rs b/common/commands/src/validator/mixnet/operators/nymnode/keys/decode_node_key.rs new file mode 100644 index 0000000000..04b6d7c9ea --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/keys/decode_node_key.rs @@ -0,0 +1,20 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use base64::Engine; +use clap::Parser; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(short, long)] + pub key: String, +} + +pub fn decode_node_key(args: Args) { + let b64_decoded = base64::prelude::BASE64_STANDARD + .decode(args.key) + .expect("failed to decode base64 string"); + let b58_encoded = bs58::encode(&b64_decoded).into_string(); + + println!("{b58_encoded}") +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/keys/mod.rs b/common/commands/src/validator/mixnet/operators/nymnode/keys/mod.rs new file mode 100644 index 0000000000..1f0b5d4c21 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/keys/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod decode_node_key; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsNymNodeKeys { + #[clap(subcommand)] + pub command: MixnetOperatorsNymNodeKeysCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsNymNodeKeysCommands { + /// Decode a Nym Node key + DecodeNodeKey(decode_node_key::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/mod.rs b/common/commands/src/validator/mixnet/operators/nymnode/mod.rs new file mode 100644 index 0000000000..e2cd7890a7 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/mod.rs @@ -0,0 +1,43 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod bond_nymnode; +pub mod keys; +pub mod nymnode_bonding_sign_payload; +pub mod pledge; +pub mod rewards; +pub mod settings; +pub mod unbond_nymnode; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsNymNode { + #[clap(subcommand)] + pub command: MixnetOperatorsNymNodeCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsNymNodeCommands { + /// Operations for Nym Node keys + Keys(keys::MixnetOperatorsNymNodeKeys), + + /// Manage your Nym Node operator rewards + Rewards(rewards::MixnetOperatorsNymNodeRewards), + + /// Manage your Nym Node settings stored in the directory + Settings(settings::MixnetOperatorsNymNodeSettings), + + /// Manage your Nym Node pledge + Pledge(pledge::MixnetOperatorsNymNodePledge), + + /// Bond to a Nym Node + Bond(bond_nymnode::Args), + + /// Unbond from a Nym Node + Unbond(unbond_nymnode::Args), + + /// Create base58-encoded payload required for producing valid bonding signature. + CreateNodeBondingSignPayload(nymnode_bonding_sign_payload::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/nymnode_bonding_sign_payload.rs b/common/commands/src/validator/mixnet/operators/nymnode/nymnode_bonding_sign_payload.rs new file mode 100644 index 0000000000..e3b66e65be --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/nymnode_bonding_sign_payload.rs @@ -0,0 +1,90 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use crate::utils::{account_id_to_cw_addr, DataWrapper}; +use clap::Parser; +use cosmwasm_std::{Coin, Uint128}; +use nym_bin_common::output_format::OutputFormat; +use nym_contracts_common::Percent; +use nym_mixnet_contract_common::{ + construct_nym_node_bonding_sign_payload, NodeCostParams, + DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; +use nym_validator_client::nyxd::CosmWasmCoin; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: String, + + #[clap(long)] + pub identity_key: String, + + #[clap(long)] + pub custom_http_api_port: Option, + + #[clap(long)] + pub profit_margin_percent: Option, + + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, + + #[clap( + long, + help = "bonding amount in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub amount: u128, + + #[clap(short, long, default_value_t = OutputFormat::default())] + pub output: OutputFormat, +} + +pub async fn create_payload(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + let mixnode = nym_mixnet_contract_common::NymNode { + host: args.host, + custom_http_port: args.custom_http_api_port, + identity_key: args.identity_key, + }; + + let coin = Coin::new(args.amount, denom); + + let cost_params = NodeCostParams { + profit_margin_percent: Percent::from_percentage_value( + args.profit_margin_percent + .unwrap_or(DEFAULT_PROFIT_MARGIN_PERCENT), + ) + .unwrap(), + interval_operating_cost: CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new( + args.interval_operating_cost + .unwrap_or(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT), + ), + }, + }; + + let nonce = match client.get_signing_nonce(&client.address()).await { + Ok(nonce) => nonce, + Err(err) => { + eprint!( + "failed to query for the signing nonce of {}: {err}", + client.address() + ); + return; + } + }; + + let address = account_id_to_cw_addr(&client.address()); + + let payload = + construct_nym_node_bonding_sign_payload(nonce, address, coin, mixnode, cost_params); + let wrapper = DataWrapper::new(payload.to_base58_string().unwrap()); + println!("{}", args.output.format(&wrapper)) +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs b/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs new file mode 100644 index 0000000000..a299ddbe65 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/pledge/decrease_pledge.rs @@ -0,0 +1,29 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_mixnet_contract_common::Coin; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub decrease_by: u128, +} + +pub async fn decrease_pledge(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + info!("Starting to decrease pledge"); + + let coin = Coin::new(args.decrease_by, denom); + + let res = client + .pledge_more(coin.into(), None) + .await + .expect("failed to decrease pledge!"); + + info!("decreasing pledge: {:?}", res); +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs b/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs new file mode 100644 index 0000000000..09b94bc5d5 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/pledge/increase_pledge.rs @@ -0,0 +1,29 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_mixnet_contract_common::Coin; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub amount: u128, +} + +pub async fn increase_pledge(args: Args, client: SigningClient) { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + info!("Starting to pledge more"); + + let coin = Coin::new(args.amount, denom); + + let res = client + .pledge_more(coin.into(), None) + .await + .expect("failed to pledge more!"); + + info!("pledging more: {:?}", res); +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/pledge/mod.rs b/common/commands/src/validator/mixnet/operators/nymnode/pledge/mod.rs new file mode 100644 index 0000000000..188e024857 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/pledge/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod decrease_pledge; +pub mod increase_pledge; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsNymNodePledge { + #[clap(subcommand)] + pub command: MixnetOperatorsNymNodePledgeCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsNymNodePledgeCommands { + /// Increase current pledge + Increase(increase_pledge::Args), + /// decrease current pledge + Decrease(decrease_pledge::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs b/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs new file mode 100644 index 0000000000..a8f157f661 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/rewards/claim_operator_reward.rs @@ -0,0 +1,21 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn claim_operator_reward(_args: Args, client: SigningClient) { + info!("Claim operator reward"); + + let res = client + .withdraw_operator_reward(None) + .await + .expect("failed to claim operator reward"); + + info!("Claiming operator reward: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/rewards/mod.rs b/common/commands/src/validator/mixnet/operators/nymnode/rewards/mod.rs new file mode 100644 index 0000000000..166124751c --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/rewards/mod.rs @@ -0,0 +1,19 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod claim_operator_reward; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsNymNodeRewards { + #[clap(subcommand)] + pub command: MixnetOperatorsNymNodeRewardsCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsNymNodeRewardsCommands { + /// Claim rewards + Claim(claim_operator_reward::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/settings/mod.rs b/common/commands/src/validator/mixnet/operators/nymnode/settings/mod.rs new file mode 100644 index 0000000000..8c5661188c --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/settings/mod.rs @@ -0,0 +1,22 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::{Args, Subcommand}; + +pub mod update_config; +pub mod update_cost_params; + +#[derive(Debug, Args)] +#[clap(args_conflicts_with_subcommands = true, subcommand_required = true)] +pub struct MixnetOperatorsNymNodeSettings { + #[clap(subcommand)] + pub command: MixnetOperatorsNymNodeSettingsCommands, +} + +#[derive(Debug, Subcommand)] +pub enum MixnetOperatorsNymNodeSettingsCommands { + /// Update Nym Node configuration + UpdateConfig(update_config::Args), + /// Update Nym Node cost parameters + UpdateCostParameters(update_cost_params::Args), +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs new file mode 100644 index 0000000000..5ba5bf52fa --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_config.rs @@ -0,0 +1,50 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use clap::Parser; +use log::info; +use nym_mixnet_contract_common::nym_node::NodeConfigUpdate; +use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap(long)] + pub host: Option, + + // ideally this would have been `Option>`, but not sure if clap would have recognised it + #[clap(long)] + pub custom_http_port: Option, + + // equivalent to setting `custom_http_port` to `None` + #[clap(long)] + pub restore_default_http_port: bool, +} + +pub async fn update_config(args: Args, client: SigningClient) { + info!("Update nym node config!"); + + if client + .get_owned_nymnode(&client.address()) + .await + .expect("failed to query the chain for nym node details") + .details + .is_none() + { + log::warn!("this operator does not own a nym node to update"); + return; + } + + let update = NodeConfigUpdate { + host: args.host, + custom_http_port: args.custom_http_port, + restore_default_http_port: args.restore_default_http_port, + }; + + let res = client + .update_nymnode_config(update, None) + .await + .expect("updating nym node config"); + + info!("nym node config updated: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs new file mode 100644 index 0000000000..5668c92e0d --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/settings/update_cost_params.rs @@ -0,0 +1,73 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::context::SigningClient; +use anyhow::anyhow; +use clap::Parser; +use cosmwasm_std::Uint128; +use log::info; +use nym_mixnet_contract_common::{NodeCostParams, Percent}; +use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; +use nym_validator_client::nyxd::CosmWasmCoin; + +#[derive(Debug, Parser)] +pub struct Args { + #[clap( + long, + help = "input your profit margin as follows; (so it would be 20, rather than 0.2)" + )] + pub profit_margin_percent: Option, + + #[clap( + long, + help = "operating cost in current DENOMINATION (so it would be 'unym', rather than 'nym')" + )] + pub interval_operating_cost: Option, +} + +pub async fn update_cost_params(args: Args, client: SigningClient) -> anyhow::Result<()> { + let denom = client.current_chain_details().mix_denom.base.as_str(); + + let current_parameters = if let Some(client_mixnode) = client + .get_owned_mixnode(&client.address()) + .await? + .mixnode_details + { + client_mixnode.rewarding_details.cost_params + } else { + client + .get_owned_nymnode(&client.address()) + .await? + .details + .ok_or_else(|| anyhow!("the client does not own any nodes"))? + .rewarding_details + .cost_params + }; + + let profit_margin_percent = args + .profit_margin_percent + .map(|pm| Percent::from_percentage_value(pm as u64)) + .unwrap_or(Ok(current_parameters.profit_margin_percent))?; + + let interval_operating_cost = args + .interval_operating_cost + .map(|oc| CosmWasmCoin { + denom: denom.into(), + amount: Uint128::new(oc), + }) + .unwrap_or(current_parameters.interval_operating_cost); + + let cost_params = NodeCostParams { + profit_margin_percent, + interval_operating_cost, + }; + + info!("Starting cost params updating using {cost_params:?} !"); + let res = client + .update_cost_params(cost_params, None) + .await + .expect("failed to update cost params"); + + info!("Cost params result: {:?}", res); + Ok(()) +} diff --git a/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs b/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs new file mode 100644 index 0000000000..fbcef6bfd7 --- /dev/null +++ b/common/commands/src/validator/mixnet/operators/nymnode/unbond_nymnode.rs @@ -0,0 +1,22 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use clap::Parser; +use log::info; +use nym_validator_client::nyxd::contract_traits::MixnetSigningClient; + +use crate::context::SigningClient; + +#[derive(Debug, Parser)] +pub struct Args {} + +pub async fn unbond_nymnode(_args: Args, client: SigningClient) { + info!("Starting Nym Node unbonding!"); + + let res = client + .unbond_nymnode(None) + .await + .expect("failed to unbond Nym Node!"); + + info!("Unbonding result: {:?}", res) +} diff --git a/common/commands/src/validator/mixnet/query/query_all_gateways.rs b/common/commands/src/validator/mixnet/query/query_all_gateways.rs index 16f0e44285..f03fb61d28 100644 --- a/common/commands/src/validator/mixnet/query/query_all_gateways.rs +++ b/common/commands/src/validator/mixnet/query/query_all_gateways.rs @@ -2,10 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::context::QueryClientWithNyxd; -use crate::utils::{pretty_cosmwasm_coin, show_error}; +use crate::utils::show_error; use clap::Parser; use comfy_table::Table; -use nym_validator_client::client::NymApiClientExt; #[derive(Debug, Parser)] pub struct Args { @@ -15,12 +14,11 @@ pub struct Args { } pub async fn query(args: Args, client: &QueryClientWithNyxd) { - match client.nym_api.get_gateways().await { + match client.get_all_cached_described_nodes().await { Ok(res) => match args.identity_key { Some(identity_key) => { let node = res.iter().find(|node| { - node.gateway - .identity_key + node.ed25519_identity_key() .to_string() .eq_ignore_ascii_case(&identity_key) }); @@ -32,14 +30,16 @@ pub async fn query(args: Args, client: &QueryClientWithNyxd) { None => { let mut table = Table::new(); - table.set_header(vec!["Identity Key", "Owner", "Host", "Bond", "Version"]); - for node in res { + table.set_header(vec!["Node Id", "Identity Key", "Version", "Is Legacy"]); + for node in res + .into_iter() + .filter(|node| node.description.declared_role.entry) + { table.add_row(vec![ - node.gateway.identity_key.to_string(), - node.owner.to_string(), - node.gateway.host.to_string(), - pretty_cosmwasm_coin(&node.pledge_amount), - node.gateway.version, + node.node_id.to_string(), + node.ed25519_identity_key().to_base58_string(), + node.description.build_information.build_version, + (!node.contract_node_type.is_nym_node()).to_string(), ]); } diff --git a/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs b/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs index 9bb20cc8d4..ef5b4a9548 100644 --- a/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs +++ b/common/commands/src/validator/mixnet/query/query_all_mixnodes.rs @@ -2,10 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::context::QueryClientWithNyxd; -use crate::utils::{pretty_decimal_with_denom, show_error}; +use crate::utils::show_error; use clap::Parser; use comfy_table::Table; -use nym_validator_client::client::NymApiClientExt; #[derive(Debug, Parser)] pub struct Args { @@ -15,13 +14,11 @@ pub struct Args { } pub async fn query(args: Args, client: &QueryClientWithNyxd) { - match client.nym_api.get_mixnodes().await { + match client.get_all_cached_described_nodes().await { Ok(res) => match args.identity_key { Some(identity_key) => { let node = res.iter().find(|node| { - node.bond_information - .mix_node - .identity_key + node.ed25519_identity_key() .to_string() .eq_ignore_ascii_case(&identity_key) }); @@ -33,25 +30,16 @@ pub async fn query(args: Args, client: &QueryClientWithNyxd) { None => { let mut table = Table::new(); - table.set_header(vec![ - "Mix id", - "Identity Key", - "Owner", - "Host", - "Bond", - "Total Delegations", - "Version", - ]); - for node in res { - let denom = &node.bond_information.original_pledge().denom; + table.set_header(vec!["Node Id", "Identity Key", "Version", "Is Legacy"]); + for node in res + .into_iter() + .filter(|node| node.description.declared_role.mixnode) + { table.add_row(vec![ - node.mix_id().to_string(), - node.bond_information.mix_node.identity_key.clone(), - node.bond_information.owner.clone().into_string(), - node.bond_information.mix_node.host.clone(), - pretty_decimal_with_denom(node.rewarding_details.operator, denom), - pretty_decimal_with_denom(node.rewarding_details.delegates, denom), - node.bond_information.mix_node.version, + node.node_id.to_string(), + node.ed25519_identity_key().to_base58_string(), + node.description.build_information.build_version, + (!node.contract_node_type.is_nym_node()).to_string(), ]); } diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/helpers.rs b/common/cosmwasm-smart-contracts/contracts-common/src/helpers.rs new file mode 100644 index 0000000000..4bcc65b75b --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common/src/helpers.rs @@ -0,0 +1,27 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{BankMsg, Coin, CosmosMsg, Response}; + +pub trait ResponseExt { + fn add_optional_message(self, msg: Option>>) -> Self; + + fn send_tokens(self, to: impl AsRef, amount: Coin) -> Self; +} + +impl ResponseExt for Response { + fn add_optional_message(self, msg: Option>>) -> Self { + if let Some(msg) = msg { + self.add_message(msg) + } else { + self + } + } + + fn send_tokens(self, to: impl AsRef, amount: Coin) -> Self { + self.add_message(BankMsg::Send { + to_address: to.as_ref().to_string(), + amount: vec![amount], + }) + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs index 301e109d80..a81fafdf41 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs @@ -10,4 +10,6 @@ pub mod events; pub mod signing; pub mod types; +pub mod helpers; + pub use types::*; diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs index e14cb3501a..96d329e753 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/types.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/types.rs @@ -8,7 +8,7 @@ use cosmwasm_std::Uint128; use serde::de::Error; use serde::{Deserialize, Deserializer}; use std::fmt::{self, Display, Formatter}; -use std::ops::Mul; +use std::ops::{Deref, Mul}; use std::str::FromStr; use thiserror::Error; @@ -23,7 +23,7 @@ pub fn truncate_decimal(amount: Decimal) -> Uint128 { #[derive(Error, Debug)] pub enum ContractsCommonError { #[error("Provided percent value ({0}) is greater than 100%")] - InvalidPercent(Decimal), + InvalidPercent(String), #[error("{source}")] StdErr { @@ -41,7 +41,7 @@ pub struct Percent(#[serde(deserialize_with = "de_decimal_percent")] Decimal); impl Percent { pub fn new(value: Decimal) -> Result { if value > Decimal::one() { - Err(ContractsCommonError::InvalidPercent(value)) + Err(ContractsCommonError::InvalidPercent(value.to_string())) } else { Ok(Percent(value)) } @@ -51,11 +51,15 @@ impl Percent { self.0 == Decimal::zero() } - pub fn zero() -> Self { + pub fn is_hundred(&self) -> bool { + self == &Self::hundred() + } + + pub const fn zero() -> Self { Self(Decimal::zero()) } - pub fn hundred() -> Self { + pub const fn hundred() -> Self { Self(Decimal::one()) } @@ -117,6 +121,70 @@ impl Mul for Percent { } } +impl Deref for Percent { + type Target = Decimal; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +// this is not implemented via From traits due to its naive nature and loss of precision +#[cfg(not(target_arch = "wasm32"))] +pub trait NaiveFloat { + fn naive_to_f64(&self) -> f64; + + fn naive_try_from_f64(val: f64) -> Result + where + Self: Sized; +} + +#[cfg(not(target_arch = "wasm32"))] +impl NaiveFloat for Percent { + fn naive_to_f64(&self) -> f64 { + use cosmwasm_std::Fraction; + + // note: this conversion loses precision with too many decimal places, + // but for the purposes of displaying basic performance, that's not an issue + self.numerator().u128() as f64 / self.denominator().u128() as f64 + } + + fn naive_try_from_f64(val: f64) -> Result + where + Self: Sized, + { + // we are only interested in positive values between 0 and 1 + if !(0. ..=1.).contains(&val) { + return Err(ContractsCommonError::InvalidPercent(val.to_string())); + } + + fn gcd(mut x: u64, mut y: u64) -> u64 { + while y > 0 { + let rem = x % y; + x = y; + y = rem; + } + + x + } + + fn to_rational(x: f64) -> (u64, u64) { + let log = x.log2().floor(); + if log >= 0.0 { + (x as u64, 1) + } else { + let num: u64 = (x / f64::EPSILON) as _; + let den: u64 = (1.0 / f64::EPSILON) as _; + let gcd = gcd(num, den); + (num / gcd, den / gcd) + } + } + + let (n, d) = to_rational(val); + Percent::new(Decimal::from_ratio(n, d)) + } +} + // implement custom Deserialize because we want to validate Percent has the correct range fn de_decimal_percent<'de, D>(deserializer: D) -> Result where @@ -243,4 +311,19 @@ mod tests { let p = serde_json::from_str::<'_, Percent>("\"1.00\"").unwrap(); assert_eq!(p.round_to_integer(), 100); } + + #[test] + 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 + let float: f64 = "0.546295475423853".parse().unwrap(); + let percent: Percent = "0.546295475423853".parse().unwrap(); + + assert_eq!(float, percent.naive_to_f64()); + + let epsilon = Decimal::from_ratio(1u64, 1000000000000000u64); + let converted = Percent::naive_try_from_f64(float).unwrap(); + + assert!(converted.0 - converted.0 < epsilon); + } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml index 58335f5c5e..ca87cb9b95 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/mixnet-contract/Cargo.toml @@ -12,6 +12,7 @@ repository = { workspace = true } bs58 = { workspace = true } cosmwasm-std = { workspace = true } cosmwasm-schema = { workspace = true } +cw-storage-plus.workspace = true cw-controllers = { workspace = true } cw2 = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs index 2269c20cd7..c68d1fc31f 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/constants.rs @@ -5,6 +5,9 @@ use cosmwasm_std::{Decimal, Uint128}; pub const TOKEN_SUPPLY: Uint128 = Uint128::new(1_000_000_000_000_000); +pub const DEFAULT_INTERVAL_OPERATING_COST_AMOUNT: u128 = 40_000_000; +pub const DEFAULT_PROFIT_MARGIN_PERCENT: u64 = 20; + // I'm still not 100% sure how to feel about existence of this file // This is equivalent of representing our display coin with 6 decimal places. // I'm using this one as opposed to "Decimal::one()", as this provides us with higher accuracy diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs index f30dc63a73..5c7a733728 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/delegation.rs @@ -3,14 +3,14 @@ use crate::constants::TOKEN_SUPPLY; use crate::helpers::IntoBaseDecimal; -use crate::{Addr, MixId}; +use crate::{Addr, NodeId}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, Decimal, StdResult}; // just use a string representation of those so that we wouldn't need to bother with decoding bytes // and trying to figure out whether they're valid, etc pub type OwnerProxySubKey = String; -pub type StorageKey = (MixId, OwnerProxySubKey); +pub type StorageKey = (NodeId, OwnerProxySubKey); // throughout the contract we ensure that our proxy can ONLY ever be the vesting contract // thus this method is equivalent to either using the existing address (for when there's no proxy) @@ -40,8 +40,9 @@ pub struct Delegation { /// Address of the owner of this delegation. pub owner: Addr, - /// Id of the MixNode that this delegation was performed against. - pub mix_id: MixId, + /// Id of the Node that this delegation was performed against. + #[serde(alias = "mix_id")] + pub node_id: NodeId, // Note to UI/UX devs: there's absolutely no point in displaying this value to the users, // it would serve them no purpose. It's only used for calculating rewards @@ -56,12 +57,13 @@ pub struct Delegation { /// Proxy address used to delegate the funds on behalf of another address pub proxy: Option, + // TODO: perhaps add a field to indicate if it was made against old mixnode with #[serde(default)]? } impl Delegation { pub fn new( owner: Addr, - mix_id: MixId, + node_id: NodeId, cumulative_reward_ratio: Decimal, amount: Coin, height: u64, @@ -73,7 +75,7 @@ impl Delegation { Delegation { owner, - mix_id, + node_id, cumulative_reward_ratio, amount, height, @@ -82,7 +84,7 @@ impl Delegation { } pub fn generate_storage_key( - mix_id: MixId, + mix_id: NodeId, owner_address: &Addr, proxy: Option<&Addr>, ) -> StorageKey { @@ -92,7 +94,7 @@ impl Delegation { // this function might seem a bit redundant, but I'd rather explicitly keep it around in case // some types change in the future pub fn generate_storage_key_with_subkey( - mix_id: MixId, + mix_id: NodeId, owner_proxy_subkey: OwnerProxySubKey, ) -> StorageKey { (mix_id, owner_proxy_subkey) @@ -107,13 +109,13 @@ impl Delegation { } pub fn storage_key(&self) -> StorageKey { - Self::generate_storage_key(self.mix_id, &self.owner, self.proxy.as_ref()) + Self::generate_storage_key(self.node_id, &self.owner, self.proxy.as_ref()) } } -/// Response containing paged list of all delegations made towards particular mixnode. +/// Response containing paged list of all delegations made towards particular node. #[cw_serde] -pub struct PagedMixNodeDelegationsResponse { +pub struct PagedNodeDelegationsResponse { /// Each individual delegation made. pub delegations: Vec, @@ -121,9 +123,9 @@ pub struct PagedMixNodeDelegationsResponse { pub start_next_after: Option, } -impl PagedMixNodeDelegationsResponse { +impl PagedNodeDelegationsResponse { pub fn new(delegations: Vec, start_next_after: Option) -> Self { - PagedMixNodeDelegationsResponse { + PagedNodeDelegationsResponse { delegations, start_next_after, } @@ -137,13 +139,13 @@ pub struct PagedDelegatorDelegationsResponse { pub delegations: Vec, /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option<(MixId, OwnerProxySubKey)>, + pub start_next_after: Option<(NodeId, OwnerProxySubKey)>, } impl PagedDelegatorDelegationsResponse { pub fn new( delegations: Vec, - start_next_after: Option<(MixId, OwnerProxySubKey)>, + start_next_after: Option<(NodeId, OwnerProxySubKey)>, ) -> Self { PagedDelegatorDelegationsResponse { delegations, @@ -154,19 +156,24 @@ impl PagedDelegatorDelegationsResponse { /// Response containing delegation details. #[cw_serde] -pub struct MixNodeDelegationResponse { +pub struct NodeDelegationResponse { /// If the delegation exists, this field contains its detailed information. pub delegation: Option, /// Flag indicating whether the node towards which the delegation was made is still bonded in the network. + #[deprecated(note = "this field will be removed. use .node_still_bonded instead")] pub mixnode_still_bonded: bool, + + pub node_still_bonded: bool, } -impl MixNodeDelegationResponse { - pub fn new(delegation: Option, mixnode_still_bonded: bool) -> Self { - MixNodeDelegationResponse { +impl NodeDelegationResponse { + pub fn new(delegation: Option, node_still_bonded: bool) -> Self { + #[allow(deprecated)] + NodeDelegationResponse { delegation, - mixnode_still_bonded, + mixnode_still_bonded: node_still_bonded, + node_still_bonded, } } } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs index 12f2030acc..aa61facb18 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/error.rs @@ -1,7 +1,10 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{EpochEventId, EpochState, IdentityKey, MixId, OperatingCostRange, ProfitMarginRange}; +use crate::nym_node::Role; +use crate::{ + EpochEventId, EpochState, IntervalEventId, NodeId, OperatingCostRange, ProfitMarginRange, +}; use contracts_common::signing::verifier::ApiVerifierError; use contracts_common::Percent; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; @@ -34,6 +37,16 @@ pub enum MixnetContractError { #[error("Not enough funds sent for node pledge. (received {received}, minimum {minimum})")] InsufficientPledge { received: Coin, minimum: Coin }, + #[error( + "the provided value for node host is too long. it must not be longer than 255 characters" + )] + HostTooLong, + + #[error( + "the provided node identity public key is not a correctly encoded base58 slice of 32 bytes" + )] + InvalidPubKey, + #[error("Attempted to reduce node pledge ({current}{denom} - {decrease_by}{denom}) below the minimum amount: {minimum}{denom}")] InvalidPledgeReduction { current: Uint128, @@ -45,11 +58,19 @@ pub enum MixnetContractError { #[error("A pledge change is already pending in this epoch. The event id: {pending_event_id}")] PendingPledgeChange { pending_event_id: EpochEventId }, + #[error( + "A cost params change is already pending in this epoch. The event id: {pending_event_id}" + )] + PendingParamsChange { pending_event_id: IntervalEventId }, + #[error("Not enough funds sent for node delegation. (received {received}, minimum {minimum})")] InsufficientDelegation { received: Coin, minimum: Coin }, + #[error("Node ({node_id}) does not exist")] + NymNodeBondNotFound { node_id: NodeId }, + #[error("Mixnode ({mix_id}) does not exist")] - MixNodeBondNotFound { mix_id: MixId }, + MixNodeBondNotFound { mix_id: NodeId }, #[error("{owner} does not seem to own any mixnodes")] NoAssociatedMixNodeBond { owner: Addr }, @@ -57,12 +78,18 @@ pub enum MixnetContractError { #[error("{owner} does not seem to own any gateways")] NoAssociatedGatewayBond { owner: Addr }, + #[error("{owner} does not seem to own any nodes")] + NoAssociatedNodeBond { owner: Addr }, + #[error("This address has already bonded a mixnode")] AlreadyOwnsMixnode, #[error("This address has already bonded a gateway")] AlreadyOwnsGateway, + #[error("This address has already bonded a nym-node")] + AlreadyOwnsNymNode, + #[error("Gateway with this identity already exists. Its owner is {owner}")] DuplicateGateway { owner: Addr }, @@ -103,32 +130,35 @@ pub enum MixnetContractError { epoch_end: i64, }, - #[error("Mixnode {mix_id} has already been rewarded during the current rewarding epoch ({absolute_epoch_id})")] - MixnodeAlreadyRewarded { - mix_id: MixId, - absolute_epoch_id: u32, - }, + #[error("attempted to reward a gateway node - this has not been fully integrated yet")] + GatewayRewarding, - #[error("Mixnode {mix_id} hasn't been selected to the rewarding set in this epoch ({absolute_epoch_id})")] - MixnodeNotInRewardedSet { - mix_id: MixId, + #[error("node {node_id} has already been rewarded during the current rewarding epoch ({absolute_epoch_id})")] + NodeAlreadyRewarded { + node_id: NodeId, absolute_epoch_id: u32, }, + #[error("node {node_id} hasn't been assigned the role of {role} for this epoch")] + IncorrectEpochRole { node_id: NodeId, role: Role }, + #[error("Mixnode {mix_id} is currently in the process of unbonding")] - MixnodeIsUnbonding { mix_id: MixId }, + MixnodeIsUnbonding { mix_id: NodeId }, + + #[error("Node {node_id} is currently in the process of unbonding")] + NodeIsUnbonding { node_id: NodeId }, #[error("Mixnode {mix_id} has already unbonded")] - MixnodeHasUnbonded { mix_id: MixId }, + MixnodeHasUnbonded { mix_id: NodeId }, #[error("The contract has ended up in a state that was deemed impossible: {comment}")] InconsistentState { comment: String }, #[error( - "Could not find any delegation information associated with mixnode {mix_id} for {address} (proxy: {proxy:?})" + "Could not find any delegation information associated with node {node_id} for {address} (proxy: {proxy:?})" )] - NoMixnodeDelegationFound { - mix_id: MixId, + NodeDelegationNotFound { + node_id: NodeId, address: String, proxy: Option, }, @@ -136,63 +166,18 @@ pub enum MixnetContractError { #[error("Provided message to update rewarding params did not contain any updates")] EmptyParamsChangeMsg, - #[error("Provided active set size is bigger than the rewarded set")] - InvalidActiveSetSize, - - #[error("Provided rewarded set size is smaller than the active set")] - InvalidRewardedSetSize, - - #[error("Provided active set size is zero")] - ZeroActiveSet, - - #[error("Provided rewarded set size is zero")] - ZeroRewardedSet, - - #[error("Received unexpected value for the active set. Got: {received}, expected: {expected}")] - UnexpectedActiveSetSize { received: u32, expected: u32 }, - - #[error("Received unexpected value for the rewarded set. Got: {received}, expected at most: {expected}")] - UnexpectedRewardedSetSize { received: u32, expected: u32 }, + #[error("one of the roles in the new active set is empty")] + EmptyRoleAssignment, - #[error("Mixnode {mix_id} appears multiple times in the provided rewarded set update!")] - DuplicateRewardedSetNode { mix_id: MixId }, + #[error("the number of mixnodes in the rewarded set is not divisible by the number of mix-layers (3)")] + UnevenLayerAssignment, - #[error("Family with head {head} does not exist!")] - FamilyDoesNotExist { head: String }, - - #[error("Family with label {label} does not exist!")] - FamilyLabelDoesNotExist { label: String }, - - #[error("Family with label '{0}' already exists")] - FamilyWithLabelExists(String), + #[error("provided active set is bigger than the rewarded set")] + InvalidActiveSetSize, #[error("Invalid layer expected 1, 2 or 3, got {0}")] InvalidLayer(u8), - #[error("Head already has a family")] - FamilyCanHaveOnlyOne, - - #[error("Already member of family {0}")] - AlreadyMemberOfFamily(String), - - #[error("Can't join own family, family head {head}, member {member}")] - CantJoinOwnFamily { - head: IdentityKey, - member: IdentityKey, - }, - - #[error("Can't leave own family, family head {head}, member {member}")] - CantLeaveOwnFamily { - head: IdentityKey, - member: IdentityKey, - }, - - #[error("{member} is not a member of family {head}")] - NotAMember { - head: IdentityKey, - member: IdentityKey, - }, - #[error("Feature is not yet implemented")] NotImplemented, @@ -219,13 +204,28 @@ pub enum MixnetContractError { #[error("attempted to reward mixnode out of order. Attempted to reward {attempted_to_reward} while last rewarded was {last_rewarded}.")] RewardingOutOfOrder { - last_rewarded: MixId, - attempted_to_reward: MixId, + last_rewarded: NodeId, + attempted_to_reward: NodeId, }, #[error("the epoch is currently not in the 'event reconciliation' state. (the state is {current_state})")] EpochNotInEventReconciliationState { current_state: EpochState }, + #[error( + "the epoch is currently not in the 'role assignment' state. (the state is {current_state})" + )] + EpochNotInRoleAssignmentState { current_state: EpochState }, + + #[error("unexpected role assignment. got: {got} while expected: {expected}")] + UnexpectedRoleAssignment { expected: Role, got: Role }, + + #[error("attempted to assign an invalid number of nodes for a role of {role}. got {assigned}, but the maximum allowed is {allowed}")] + IllegalRoleCount { + role: Role, + assigned: u32, + allowed: u32, + }, + #[error("the epoch is currently not in the 'epoch advancement' state. (the state is {current_state})")] EpochNotInAdvancementState { current_state: EpochState }, @@ -258,6 +258,17 @@ pub enum MixnetContractError { provided: Uint128, range: OperatingCostRange, }, + + #[error( + "currently it's not possible to migrate nodes bonded with vesting tokens into a nym-node. please perform vesting->liquid migration first." + )] + VestingNodeMigration, + + #[error("value {got} does not correspond to any known node role")] + UnknownRoleRepresentation { got: u8 }, + + #[error("the total work for this epoch seems to be bigger than 1.0!")] + TotalWorkAboveOne, } impl MixnetContractError { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs index 7ebb604225..4fccb50295 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/events.rs @@ -1,11 +1,13 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::error::MixnetContractError; use crate::gateway::GatewayConfigUpdate; -use crate::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; -use crate::reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate}; +use crate::mixnode::{MixNodeConfigUpdate, NodeCostParams}; +use crate::nym_node::Role; +use crate::reward_params::{ActiveSetUpdate, IntervalRewardParams, IntervalRewardingParamsUpdate}; use crate::rewarding::RewardDistribution; -use crate::{BlockHeight, ContractStateParams, IdentityKeyRef, Interval, Layer, MixId}; +use crate::{BlockHeight, ContractStateParams, EpochId, IdentityKeyRef, Interval, NodeId}; pub use contracts_common::events::*; use cosmwasm_std::{Addr, Coin, Decimal, Event}; use std::fmt::Display; @@ -14,6 +16,11 @@ pub const EVENT_VERSION_PREFIX: &str = "v2_"; pub enum MixnetEventType { MixnodeBonding, + NymNodeBonding, + NymNodeUnbonding, + PendingNymNodeUnbonding, + GatewayMigration, + MixnodeMigration, PendingPledgeIncrease, PledgeIncrease, PendingPledgeDecrease, @@ -23,9 +30,9 @@ pub enum MixnetEventType { PendingMixnodeUnbonding, MixnodeUnbonding, MixnodeConfigUpdate, - PendingMixnodeCostParamsUpdate, - MixnodeCostParamsUpdate, - MixnodeRewarding, + PendingCostParamsUpdate, + CostParamsUpdate, + NodeRewarding, WithdrawDelegatorReward, WithdrawOperatorReward, PendingActiveSetUpdate, @@ -41,6 +48,7 @@ pub enum MixnetEventType { RewardingValidatorUpdate, BeginEpochTransition, AdvanceEpoch, + RoleAssignment, ExecutePendingEpochEvents, ExecutePendingIntervalEvents, ReconcilePendingEvents, @@ -59,6 +67,11 @@ impl Display for MixnetEventType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let event_name = match self { MixnetEventType::MixnodeBonding => "mixnode_bonding", + MixnetEventType::NymNodeBonding => "nymnode_bonding", + MixnetEventType::NymNodeUnbonding => "nymnode_unbonding", + MixnetEventType::PendingNymNodeUnbonding => "pending_nymnode_unbonding", + MixnetEventType::GatewayMigration => "gateway_migration", + MixnetEventType::MixnodeMigration => "mixnode_migration", MixnetEventType::PendingPledgeIncrease => "pending_pledge_increase", MixnetEventType::PledgeIncrease => "pledge_increase", MixnetEventType::PendingPledgeDecrease => "pending_pledge_decrease", @@ -68,9 +81,9 @@ impl Display for MixnetEventType { MixnetEventType::PendingMixnodeUnbonding => "pending_mixnode_unbonding", MixnetEventType::MixnodeConfigUpdate => "mixnode_config_update", MixnetEventType::MixnodeUnbonding => "mixnode_unbonding", - MixnetEventType::PendingMixnodeCostParamsUpdate => "pending_mixnode_cost_params_update", - MixnetEventType::MixnodeCostParamsUpdate => "mixnode_cost_params_update", - MixnetEventType::MixnodeRewarding => "mix_rewarding", + MixnetEventType::PendingCostParamsUpdate => "pending_cost_params_update", + MixnetEventType::CostParamsUpdate => "cost_params_update", + MixnetEventType::NodeRewarding => "node_rewarding", MixnetEventType::WithdrawDelegatorReward => "withdraw_delegator_reward", MixnetEventType::WithdrawOperatorReward => "withdraw_operator_reward", MixnetEventType::PendingActiveSetUpdate => "pending_active_set_update", @@ -87,6 +100,7 @@ impl Display for MixnetEventType { MixnetEventType::RewardingValidatorUpdate => "rewarding_validator_address_update", MixnetEventType::BeginEpochTransition => "beginning_epoch_transition", MixnetEventType::AdvanceEpoch => "advance_epoch", + MixnetEventType::RoleAssignment => "role_assignment", MixnetEventType::ExecutePendingEpochEvents => "execute_pending_epoch_events", MixnetEventType::ExecutePendingIntervalEvents => "execute_pending_interval_events", MixnetEventType::ReconcilePendingEvents => "reconcile_pending_events", @@ -103,6 +117,7 @@ impl Display for MixnetEventType { // attributes that are used in multiple places pub const OWNER_KEY: &str = "owner"; pub const AMOUNT_KEY: &str = "amount"; +pub const ERROR_MESSAGE_KEY: &str = "error_message"; // event-specific attributes @@ -113,16 +128,14 @@ pub const UNIT_REWARD_KEY: &str = "unit_reward"; // bonding/unbonding pub const MIX_ID_KEY: &str = "mix_id"; +pub const NODE_ID_KEY: &str = "node_id"; pub const NODE_IDENTITY_KEY: &str = "identity"; -pub const ASSIGNED_LAYER_KEY: &str = "assigned_layer"; // settings change -pub const OLD_MINIMUM_MIXNODE_PLEDGE_KEY: &str = "old_minimum_mixnode_pledge"; -pub const OLD_MINIMUM_GATEWAY_PLEDGE_KEY: &str = "old_minimum_gateway_pledge"; +pub const OLD_MINIMUM_PLEDGE_KEY: &str = "old_minimum_pledge"; pub const OLD_MINIMUM_DELEGATION_KEY: &str = "old_minimum_delegation"; -pub const NEW_MINIMUM_MIXNODE_PLEDGE_KEY: &str = "new_minimum_mixnode_pledge"; -pub const NEW_MINIMUM_GATEWAY_PLEDGE_KEY: &str = "new_minimum_gateway_pledge"; +pub const NEW_MINIMUM_PLEDGE_KEY: &str = "new_minimum_pledge"; pub const NEW_MINIMUM_DELEGATION_KEY: &str = "new_minimum_delegation"; pub const OLD_REWARDING_VALIDATOR_ADDRESS_KEY: &str = "old_rewarding_validator_address"; @@ -144,14 +157,19 @@ pub const PRIOR_UNIT_REWARD_KEY: &str = "prior_unit_reward"; pub const NO_REWARD_REASON_KEY: &str = "no_reward_reason"; pub const BOND_NOT_FOUND_VALUE: &str = "bond_not_found"; -pub const ZERO_PERFORMANCE_VALUE: &str = "zero_performance"; +pub const ZERO_PERFORMANCE_OR_WORK_VALUE: &str = "zero_performance_or_work"; // rewarded set update -pub const ACTIVE_SET_SIZE_KEY: &str = "active_set_size"; +pub const NUM_MIXNODES_KEY: &str = "num_mixnodes"; +pub const NUM_ENTRIES_KEY: &str = "num_entry_gateways"; +pub const NUM_EXITS_KEY: &str = "num_exit_gateways"; pub const CURRENT_EPOCH_KEY: &str = "current_epoch"; pub const NEW_CURRENT_EPOCH_KEY: &str = "new_current_epoch"; +pub const ROLE_KEY: &str = "role"; +pub const NODE_COUNT_KEY: &str = "node_count"; + // interval pub const EVENTS_EXECUTED_KEY: &str = "number_of_events_executed"; pub const EVENT_CREATION_HEIGHT_KEY: &str = "created_at"; @@ -163,7 +181,7 @@ pub fn new_delegation_event( created_at: BlockHeight, delegator: &Addr, amount: &Coin, - mix_id: MixId, + mix_id: NodeId, unit_reward: Decimal, ) -> Event { Event::new(MixnetEventType::Delegation) @@ -174,45 +192,57 @@ pub fn new_delegation_event( .add_attribute(UNIT_REWARD_KEY, unit_reward.to_string()) } -pub fn new_delegation_on_unbonded_node_event(delegator: &Addr, mix_id: MixId) -> Event { +pub fn new_delegation_on_unbonded_node_event(delegator: &Addr, mix_id: NodeId) -> Event { Event::new(MixnetEventType::Delegation) .add_attribute(DELEGATOR_KEY, delegator) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } -pub fn new_pending_delegation_event(delegator: &Addr, amount: &Coin, mix_id: MixId) -> Event { +pub fn new_pending_delegation_event(delegator: &Addr, amount: &Coin, mix_id: NodeId) -> Event { Event::new(MixnetEventType::PendingDelegation) .add_attribute(DELEGATOR_KEY, delegator) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } -pub fn new_withdraw_operator_reward_event(owner: &Addr, amount: Coin, mix_id: MixId) -> Event { +pub fn new_withdraw_operator_reward_event(owner: &Addr, amount: Coin, mix_id: NodeId) -> Event { Event::new(MixnetEventType::WithdrawOperatorReward) .add_attribute(OWNER_KEY, owner.as_str()) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(MIX_ID_KEY, mix_id.to_string()) } -pub fn new_withdraw_delegator_reward_event(delegator: &Addr, amount: Coin, mix_id: MixId) -> Event { +pub fn new_withdraw_delegator_reward_event( + delegator: &Addr, + amount: Coin, + mix_id: NodeId, +) -> Event { Event::new(MixnetEventType::WithdrawDelegatorReward) .add_attribute(DELEGATOR_KEY, delegator) .add_attribute(AMOUNT_KEY, amount.to_string()) .add_attribute(DELEGATION_TARGET_KEY, mix_id.to_string()) } -pub fn new_active_set_update_event(created_at: BlockHeight, new_size: u32) -> Event { +pub fn new_active_set_update_failure(err: MixnetContractError) -> Event { + Event::new(MixnetEventType::ActiveSetUpdate).add_attribute(ERROR_MESSAGE_KEY, err.to_string()) +} + +pub fn new_active_set_update_event(created_at: BlockHeight, update: ActiveSetUpdate) -> Event { Event::new(MixnetEventType::ActiveSetUpdate) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) - .add_attribute(ACTIVE_SET_SIZE_KEY, new_size.to_string()) + .add_attribute(NUM_MIXNODES_KEY, update.mixnodes.to_string()) + .add_attribute(NUM_ENTRIES_KEY, update.entry_gateways.to_string()) + .add_attribute(NUM_EXITS_KEY, update.exit_gateways.to_string()) } pub fn new_pending_active_set_update_event( - new_size: u32, + update: ActiveSetUpdate, approximate_time_remaining_secs: i64, ) -> Event { Event::new(MixnetEventType::PendingActiveSetUpdate) - .add_attribute(ACTIVE_SET_SIZE_KEY, new_size.to_string()) + .add_attribute(NUM_MIXNODES_KEY, update.mixnodes.to_string()) + .add_attribute(NUM_ENTRIES_KEY, update.entry_gateways.to_string()) + .add_attribute(NUM_EXITS_KEY, update.exit_gateways.to_string()) .add_attribute( APPROXIMATE_TIME_LEFT_SECS_KEY, approximate_time_remaining_secs.to_string(), @@ -221,7 +251,6 @@ pub fn new_pending_active_set_update_event( pub fn new_rewarding_params_update_event( created_at: BlockHeight, - update: IntervalRewardingParamsUpdate, updated: IntervalRewardParams, ) -> Event { @@ -252,84 +281,109 @@ pub fn new_pending_rewarding_params_update_event( ) } -pub fn new_undelegation_event(created_at: BlockHeight, delegator: &Addr, mix_id: MixId) -> Event { +pub fn new_undelegation_event(created_at: BlockHeight, delegator: &Addr, mix_id: NodeId) -> Event { Event::new(MixnetEventType::Undelegation) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(DELEGATOR_KEY, delegator) .add_attribute(MIX_ID_KEY, mix_id.to_string()) } -pub fn new_pending_undelegation_event(delegator: &Addr, mix_id: MixId) -> Event { +pub fn new_pending_undelegation_event(delegator: &Addr, mix_id: NodeId) -> Event { Event::new(MixnetEventType::PendingUndelegation) .add_attribute(DELEGATOR_KEY, delegator) .add_attribute(MIX_ID_KEY, mix_id.to_string()) } -pub fn new_gateway_bonding_event( +pub fn new_gateway_unbonding_event( owner: &Addr, amount: &Coin, identity: IdentityKeyRef<'_>, ) -> Event { - Event::new(MixnetEventType::GatewayBonding) + Event::new(MixnetEventType::GatewayUnbonding) .add_attribute(OWNER_KEY, owner) .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_gateway_unbonding_event( +pub fn new_nym_node_bonding_event( owner: &Addr, amount: &Coin, identity: IdentityKeyRef<'_>, + node_id: NodeId, ) -> Event { - Event::new(MixnetEventType::GatewayUnbonding) - .add_attribute(OWNER_KEY, owner) + Event::new(MixnetEventType::NymNodeBonding) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(NODE_IDENTITY_KEY, identity) + .add_attribute(OWNER_KEY, owner) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_mixnode_bonding_event( +pub fn new_nym_node_unbonding_event(created_at: BlockHeight, node_id: NodeId) -> Event { + Event::new(MixnetEventType::NymNodeUnbonding) + .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) +} + +pub fn new_pending_nym_node_unbonding_event( owner: &Addr, - amount: &Coin, identity: IdentityKeyRef<'_>, - mix_id: MixId, - assigned_layer: Layer, + node_id: NodeId, ) -> Event { - // coin implements Display trait and we use that implementation here - Event::new(MixnetEventType::MixnodeBonding) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + Event::new(MixnetEventType::PendingNymNodeUnbonding) + .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(NODE_IDENTITY_KEY, identity) + .add_attribute(OWNER_KEY, owner) +} + +pub fn new_migrated_gateway_event( + owner: &Addr, + identity: IdentityKeyRef<'_>, + node_id: NodeId, +) -> Event { + Event::new(MixnetEventType::GatewayMigration) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(NODE_IDENTITY_KEY, identity) .add_attribute(OWNER_KEY, owner) - .add_attribute(ASSIGNED_LAYER_KEY, assigned_layer) - .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_pending_pledge_increase_event(mix_id: MixId, amount: &Coin) -> Event { +pub fn new_migrated_mixnode_event( + owner: &Addr, + identity: IdentityKeyRef<'_>, + node_id: NodeId, +) -> Event { + Event::new(MixnetEventType::MixnodeMigration) + .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(NODE_IDENTITY_KEY, identity) + .add_attribute(OWNER_KEY, owner) +} + +pub fn new_pending_pledge_increase_event(node_id: NodeId, amount: &Coin) -> Event { Event::new(MixnetEventType::PendingPledgeIncrease) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_pledge_increase_event(created_at: BlockHeight, mix_id: MixId, amount: &Coin) -> Event { +pub fn new_pledge_increase_event(created_at: BlockHeight, node_id: NodeId, amount: &Coin) -> Event { Event::new(MixnetEventType::PledgeIncrease) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_pending_pledge_decrease_event(mix_id: MixId, amount: &Coin) -> Event { +pub fn new_pending_pledge_decrease_event(node_id: NodeId, amount: &Coin) -> Event { Event::new(MixnetEventType::PendingPledgeDecrease) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_pledge_decrease_event(created_at: BlockHeight, mix_id: MixId, amount: &Coin) -> Event { +pub fn new_pledge_decrease_event(created_at: BlockHeight, node_id: NodeId, amount: &Coin) -> Event { Event::new(MixnetEventType::PledgeDecrease) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(AMOUNT_KEY, amount.to_string()) } -pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: MixId) -> Event { +pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: NodeId) -> Event { Event::new(MixnetEventType::MixnodeUnbonding) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(MIX_ID_KEY, mix_id.to_string()) @@ -338,7 +392,7 @@ pub fn new_mixnode_unbonding_event(created_at: BlockHeight, mix_id: MixId) -> Ev pub fn new_pending_mixnode_unbonding_event( owner: &Addr, identity: IdentityKeyRef<'_>, - mix_id: MixId, + mix_id: NodeId, ) -> Event { Event::new(MixnetEventType::PendingMixnodeUnbonding) .add_attribute(MIX_ID_KEY, mix_id.to_string()) @@ -347,7 +401,7 @@ pub fn new_pending_mixnode_unbonding_event( } pub fn new_mixnode_config_update_event( - mix_id: MixId, + mix_id: NodeId, owner: &Addr, update: &MixNodeConfigUpdate, ) -> Event { @@ -363,23 +417,18 @@ pub fn new_gateway_config_update_event(owner: &Addr, update: &GatewayConfigUpdat .add_attribute(UPDATED_GATEWAY_CONFIG_KEY, update.to_inline_json()) } -pub fn new_mixnode_pending_cost_params_update_event( - mix_id: MixId, - owner: &Addr, - new_costs: &MixNodeCostParams, -) -> Event { - Event::new(MixnetEventType::PendingMixnodeCostParamsUpdate) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) - .add_attribute(OWNER_KEY, owner) +pub fn new_pending_cost_params_update_event(mix_id: NodeId, new_costs: &NodeCostParams) -> Event { + Event::new(MixnetEventType::PendingCostParamsUpdate) + .add_attribute(NODE_ID_KEY, mix_id.to_string()) .add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json()) } -pub fn new_mixnode_cost_params_update_event( +pub fn new_cost_params_update_event( created_at: BlockHeight, - mix_id: MixId, - new_costs: &MixNodeCostParams, + mix_id: NodeId, + new_costs: &NodeCostParams, ) -> Event { - Event::new(MixnetEventType::MixnodeCostParamsUpdate) + Event::new(MixnetEventType::CostParamsUpdate) .add_attribute(EVENT_CREATION_HEIGHT_KEY, created_at.to_string()) .add_attribute(MIX_ID_KEY, mix_id.to_string()) .add_attribute(UPDATED_MIXNODE_COST_PARAMS_KEY, new_costs.to_inline_json()) @@ -397,37 +446,25 @@ pub fn new_settings_update_event( ) -> Event { let mut event = Event::new(MixnetEventType::ContractSettingsUpdate); - if old_params.minimum_mixnode_pledge != new_params.minimum_mixnode_pledge { + if old_params.minimum_pledge != new_params.minimum_pledge { event = event .add_attribute( - OLD_MINIMUM_MIXNODE_PLEDGE_KEY, - old_params.minimum_mixnode_pledge.to_string(), + OLD_MINIMUM_PLEDGE_KEY, + old_params.minimum_pledge.to_string(), ) .add_attribute( - NEW_MINIMUM_MIXNODE_PLEDGE_KEY, - new_params.minimum_mixnode_pledge.to_string(), + NEW_MINIMUM_PLEDGE_KEY, + new_params.minimum_pledge.to_string(), ) } - if old_params.minimum_gateway_pledge != new_params.minimum_gateway_pledge { - event = event - .add_attribute( - OLD_MINIMUM_GATEWAY_PLEDGE_KEY, - old_params.minimum_gateway_pledge.to_string(), - ) - .add_attribute( - NEW_MINIMUM_GATEWAY_PLEDGE_KEY, - new_params.minimum_gateway_pledge.to_string(), - ) - } - - if old_params.minimum_mixnode_delegation != new_params.minimum_mixnode_delegation { - if let Some(ref old) = old_params.minimum_mixnode_delegation { + if old_params.minimum_delegation != new_params.minimum_delegation { + if let Some(ref old) = old_params.minimum_delegation { event = event.add_attribute(OLD_MINIMUM_DELEGATION_KEY, old.to_string()) } else { event = event.add_attribute(OLD_MINIMUM_DELEGATION_KEY, "None") } - if let Some(ref new) = new_params.minimum_mixnode_delegation { + if let Some(ref new) = new_params.minimum_delegation { event = event.add_attribute(NEW_MINIMUM_DELEGATION_KEY, new.to_string()) } else { event = event.add_attribute(NEW_MINIMUM_DELEGATION_KEY, "None") @@ -437,41 +474,41 @@ pub fn new_settings_update_event( event } -pub fn new_not_found_mix_operator_rewarding_event(interval: Interval, mix_id: MixId) -> Event { - Event::new(MixnetEventType::MixnodeRewarding) +pub fn new_not_found_node_operator_rewarding_event(interval: Interval, node_id: NodeId) -> Event { + Event::new(MixnetEventType::NodeRewarding) .add_attribute( INTERVAL_KEY, interval.current_epoch_absolute_id().to_string(), ) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute(NO_REWARD_REASON_KEY, BOND_NOT_FOUND_VALUE) } -pub fn new_zero_uptime_mix_operator_rewarding_event(interval: Interval, mix_id: MixId) -> Event { - Event::new(MixnetEventType::MixnodeRewarding) +pub fn new_zero_uptime_mix_operator_rewarding_event(interval: Interval, node_id: NodeId) -> Event { + Event::new(MixnetEventType::NodeRewarding) .add_attribute( INTERVAL_KEY, interval.current_epoch_absolute_id().to_string(), ) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) - .add_attribute(NO_REWARD_REASON_KEY, ZERO_PERFORMANCE_VALUE) + .add_attribute(NODE_ID_KEY, node_id.to_string()) + .add_attribute(NO_REWARD_REASON_KEY, ZERO_PERFORMANCE_OR_WORK_VALUE) } pub fn new_mix_rewarding_event( interval: Interval, - mix_id: MixId, + node_id: NodeId, reward_distribution: RewardDistribution, prior_delegates: Decimal, prior_unit_reward: Decimal, ) -> Event { - Event::new(MixnetEventType::MixnodeRewarding) + Event::new(MixnetEventType::NodeRewarding) .add_attribute( INTERVAL_KEY, interval.current_epoch_absolute_id().to_string(), ) .add_attribute(PRIOR_DELEGATES_KEY, prior_delegates.to_string()) .add_attribute(PRIOR_UNIT_REWARD_KEY, prior_unit_reward.to_string()) - .add_attribute(MIX_ID_KEY, mix_id.to_string()) + .add_attribute(NODE_ID_KEY, node_id.to_string()) .add_attribute( OPERATOR_REWARD_KEY, reward_distribution.operator.to_string(), @@ -489,13 +526,15 @@ pub fn new_epoch_transition_start_event(current_interval: Interval) -> Event { ) } -pub fn new_advance_epoch_event(interval: Interval, rewarded_nodes: u32) -> Event { +pub fn new_assigned_role_event(role: Role, nodes: u32) -> Event { + Event::new(MixnetEventType::RoleAssignment) + .add_attribute(ROLE_KEY, role.to_string()) + .add_attribute(NODE_COUNT_KEY, nodes.to_string()) +} + +pub fn new_advance_epoch_event(epoch_id: EpochId) -> Event { Event::new(MixnetEventType::AdvanceEpoch) - .add_attribute( - NEW_CURRENT_EPOCH_KEY, - interval.current_epoch_absolute_id().to_string(), - ) - .add_attribute(REWARDED_SET_NODES_KEY, rewarded_nodes.to_string()) + .add_attribute(NEW_CURRENT_EPOCH_KEY, epoch_id.to_string()) } pub fn new_pending_epoch_events_execution_event(executed: u32) -> Event { diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs deleted file mode 100644 index 58f74f4e08..0000000000 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/families.rs +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::{IdentityKey, IdentityKeyRef}; -use cosmwasm_schema::cw_serde; -use schemars::JsonSchema; -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::fmt::{Display, Formatter}; -use std::str::FromStr; - -/// A group of mixnodes associated with particular staking entity. -/// When defined all nodes belonging to the same family will be prioritised to be put onto the same layer. -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/NodeFamily.ts") -)] -#[cw_serde] -pub struct Family { - /// Owner of this family. - head: FamilyHead, - - /// Optional proxy (i.e. vesting contract address) used when creating the family. - proxy: Option, - - /// Human readable label for this family. - label: String, -} - -/// Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58). -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/NodeFamilyHead.ts") -)] -#[derive(Debug, Clone, Eq, PartialEq, JsonSchema)] -pub struct FamilyHead(IdentityKey); - -impl Serialize for FamilyHead { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - self.0.serialize(serializer) - } -} - -impl<'de> Deserialize<'de> for FamilyHead { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let inner = IdentityKey::deserialize(deserializer)?; - Ok(FamilyHead(inner)) - } -} - -impl FromStr for FamilyHead { - type Err = ::Err; - - fn from_str(s: &str) -> Result { - // theoretically we should be verifying whether it's a valid base58 value - // (or even better, whether it's a valid ed25519 public key), but definition of - // `FamilyHead` might change later - Ok(FamilyHead(IdentityKey::from_str(s)?)) - } -} - -impl Display for FamilyHead { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl FamilyHead { - pub fn new>(identity: S) -> Self { - FamilyHead(identity.into()) - } - - pub fn identity(&self) -> IdentityKeyRef<'_> { - &self.0 - } -} - -impl Family { - pub fn new(head: FamilyHead, label: String) -> Self { - Family { - head, - proxy: None, - label, - } - } - - #[allow(dead_code)] - pub fn head(&self) -> &FamilyHead { - &self.head - } - - pub fn head_identity(&self) -> IdentityKeyRef<'_> { - self.head.identity() - } - - #[allow(dead_code)] - pub fn proxy(&self) -> Option<&String> { - self.proxy.as_ref() - } - - pub fn label(&self) -> &str { - &self.label - } -} - -/// Response containing paged list of all families registered in the contract. -#[cw_serde] -pub struct PagedFamiliesResponse { - /// The families registered in the contract. - pub families: Vec, - - /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, -} - -/// Response containing paged list of all family members (of ALL families) registered in the contract. -#[cw_serde] -pub struct PagedMembersResponse { - /// The members alongside their family heads. - pub members: Vec<(IdentityKey, FamilyHead)>, - - /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, -} - -/// Response containing family information. -#[cw_serde] -pub struct FamilyByHeadResponse { - /// The family head used for the query. - pub head: FamilyHead, - - /// If applicable, the family associated with the provided head. - pub family: Option, -} - -/// Response containing family information. -#[cw_serde] -pub struct FamilyByLabelResponse { - /// The family label used for the query. - pub label: String, - - /// If applicable, the family associated with the provided label. - pub family: Option, -} - -/// Response containing family members information. -#[cw_serde] -pub struct FamilyMembersByHeadResponse { - /// The family head used for the query. - pub head: FamilyHead, - - /// All members belonging to the specified family. - pub members: Vec, -} - -/// Response containing family members information. -#[cw_serde] -pub struct FamilyMembersByLabelResponse { - /// The family label used for the query. - pub label: String, - - /// All members belonging to the specified family. - pub members: Vec, -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn family_head_serde() { - let dummy = FamilyHead::new("foomp"); - - let ser_str = serde_json_wasm::to_string(&dummy).unwrap(); - let de_str: FamilyHead = serde_json_wasm::from_str(&ser_str).unwrap(); - assert_eq!(dummy, de_str); - - let ser_bytes = serde_json_wasm::to_vec(&dummy).unwrap(); - let de_bytes: FamilyHead = serde_json_wasm::from_slice(&ser_bytes).unwrap(); - assert_eq!(dummy, de_bytes); - } -} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs index 9d75b66c22..ed0ee864e8 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/gateway.rs @@ -1,7 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{IdentityKey, SphinxKey}; +use crate::{IdentityKey, NodeId, SphinxKey}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin}; use std::cmp::Ordering; @@ -135,7 +135,10 @@ impl Display for GatewayBond { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/GatewayConfigUpdate.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/GatewayConfigUpdate.ts" + ) )] #[cw_serde] pub struct GatewayConfigUpdate { @@ -200,6 +203,23 @@ pub struct GatewayBondResponse { pub gateway: Option, } +#[cw_serde] +pub struct PreassignedId { + /// The identity key (base58-encoded ed25519 public key) of the gateway. + pub identity: IdentityKey, + + /// The id pre-assigned to this gateway + pub node_id: NodeId, +} + +#[cw_serde] +pub struct PreassignedGatewayIdsResponse { + pub ids: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + #[cfg(test)] mod tests { use super::*; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs index fc0a05892b..092f35e80c 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/helpers.rs @@ -1,14 +1,22 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{Decimal, StdError, StdResult, Uint128}; +use crate::error::MixnetContractError; +use crate::mixnode::PendingMixNodeChanges; +use crate::{ + EpochEventId, IntervalEventId, MixNodeBond, MixNodeDetails, NodeId, NodeRewarding, NymNodeBond, + NymNodeDetails, PendingNodeChanges, +}; +use contracts_common::IdentityKeyRef; +use cosmwasm_std::{Coin, Decimal, StdError, StdResult, Uint128}; +#[track_caller] pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option) { let epsilon = epsilon.unwrap_or_else(|| Decimal::from_ratio(1u128, 100_000_000u128)); if a > b { - assert!(a - b < epsilon, "{a} != {b}") + assert!(a - b < epsilon, "{a} != {b}, delta: {}", a - b) } else { - assert!(b - a < epsilon, "{a} != {b}") + assert!(b - a < epsilon, "{a} != {b}, delta: {}", b - a) } } @@ -31,3 +39,158 @@ where }) } } + +pub trait NodeDetails { + type Bond: NodeBond; + type PendingChanges: PendingChanges; + + fn split(self) -> (Self::Bond, NodeRewarding, Self::PendingChanges); + fn rewarding_info(&self) -> &NodeRewarding; + fn bond_info(&self) -> &Self::Bond; + fn pending_changes(&self) -> &Self::PendingChanges; +} + +pub trait NodeBond { + fn node_id(&self) -> NodeId; + + fn is_unbonding(&self) -> bool; + + fn identity(&self) -> IdentityKeyRef; + + fn original_pledge(&self) -> &Coin; + + fn ensure_bonded(&self) -> Result<(), MixnetContractError> { + if self.is_unbonding() { + return Err(MixnetContractError::NodeIsUnbonding { + node_id: self.node_id(), + }); + } + Ok(()) + } +} + +pub trait PendingChanges { + fn pending_pledge_changes(&self) -> Option; + + fn pending_cost_params_changes(&self) -> Option; + + fn ensure_no_pending_pledge_changes(&self) -> Result<(), MixnetContractError> { + if let Some(pending_event_id) = self.pending_pledge_changes() { + return Err(MixnetContractError::PendingPledgeChange { pending_event_id }); + } + Ok(()) + } + + fn ensure_no_pending_params_changes(&self) -> Result<(), MixnetContractError> { + if let Some(pending_event_id) = self.pending_cost_params_changes() { + return Err(MixnetContractError::PendingParamsChange { pending_event_id }); + } + Ok(()) + } +} + +impl NodeDetails for MixNodeDetails { + type Bond = MixNodeBond; + type PendingChanges = PendingMixNodeChanges; + + fn split(self) -> (Self::Bond, NodeRewarding, Self::PendingChanges) { + ( + self.bond_information, + self.rewarding_details, + self.pending_changes, + ) + } + + fn rewarding_info(&self) -> &NodeRewarding { + &self.rewarding_details + } + + fn bond_info(&self) -> &Self::Bond { + &self.bond_information + } + + fn pending_changes(&self) -> &Self::PendingChanges { + &self.pending_changes + } +} + +impl NodeBond for MixNodeBond { + fn node_id(&self) -> NodeId { + self.mix_id + } + + fn is_unbonding(&self) -> bool { + self.is_unbonding + } + + fn identity(&self) -> IdentityKeyRef { + self.identity() + } + + fn original_pledge(&self) -> &Coin { + self.original_pledge() + } +} + +impl PendingChanges for PendingMixNodeChanges { + fn pending_pledge_changes(&self) -> Option { + self.pledge_change + } + + fn pending_cost_params_changes(&self) -> Option { + self.cost_params_change + } +} + +impl NodeDetails for NymNodeDetails { + type Bond = NymNodeBond; + type PendingChanges = PendingNodeChanges; + + fn split(self) -> (Self::Bond, NodeRewarding, Self::PendingChanges) { + ( + self.bond_information, + self.rewarding_details, + self.pending_changes, + ) + } + + fn rewarding_info(&self) -> &NodeRewarding { + &self.rewarding_details + } + + fn bond_info(&self) -> &Self::Bond { + &self.bond_information + } + + fn pending_changes(&self) -> &Self::PendingChanges { + &self.pending_changes + } +} + +impl NodeBond for NymNodeBond { + fn node_id(&self) -> NodeId { + self.node_id + } + + fn is_unbonding(&self) -> bool { + self.is_unbonding + } + + fn identity(&self) -> IdentityKeyRef { + self.identity() + } + + fn original_pledge(&self) -> &Coin { + &self.original_pledge + } +} + +impl PendingChanges for PendingNodeChanges { + fn pending_pledge_changes(&self) -> Option { + self.pledge_change + } + + fn pending_cost_params_changes(&self) -> Option { + self.cost_params_change + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs index d5382a29e6..e36935d66c 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/interval.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::MixnetContractError; -use crate::MixId; +use crate::nym_node::Role; +use crate::NodeId; use cosmwasm_schema::cw_serde; use cosmwasm_schema::schemars::gen::SchemaGenerator; use cosmwasm_schema::schemars::schema::{InstanceType, Schema, SchemaObject}; @@ -86,7 +87,7 @@ impl EpochStatus { pub fn update_last_rewarded( &mut self, - new_last_rewarded: MixId, + new_last_rewarded: NodeId, ) -> Result { match &mut self.state { EpochState::Rewarding { @@ -109,7 +110,7 @@ impl EpochStatus { } } - pub fn last_rewarded(&self) -> Result { + pub fn last_rewarded(&self) -> Result { match self.state { EpochState::Rewarding { last_rewarded, .. } => Ok(last_rewarded), state => Err(MixnetContractError::UnexpectedNonRewardingEpochState { @@ -127,12 +128,23 @@ impl EpochStatus { Ok(()) } - pub fn ensure_is_in_advancement_state(&self) -> Result<(), MixnetContractError> { - if !matches!(self.state, EpochState::AdvancingEpoch) { - return Err(MixnetContractError::EpochNotInAdvancementState { + pub fn ensure_is_in_expected_role_assignment_state( + &self, + caller: Role, + ) -> Result<(), MixnetContractError> { + let EpochState::RoleAssignment { next } = self.state else { + return Err(MixnetContractError::EpochNotInRoleAssignmentState { current_state: self.state, }); + }; + + if caller != next { + return Err(MixnetContractError::UnexpectedRoleAssignment { + expected: next, + got: caller, + }); } + Ok(()) } @@ -147,10 +159,6 @@ impl EpochStatus { pub fn is_reconciling(&self) -> bool { matches!(self.state, EpochState::ReconcilingEvents) } - - pub fn is_advancing(&self) -> bool { - matches!(self.state, EpochState::AdvancingEpoch) - } } /// The state of the current rewarding epoch. @@ -167,10 +175,10 @@ pub enum EpochState { #[serde(alias = "Rewarding")] Rewarding { /// The id of the last node that has already received its rewards. - last_rewarded: MixId, + last_rewarded: NodeId, /// The id of the last node that's going to be rewarded before progressing into the next state. - final_node_id: MixId, + final_node_id: NodeId, // total_rewarded: u32, }, @@ -179,10 +187,9 @@ pub enum EpochState { #[serde(alias = "ReconcilingEvents")] ReconcilingEvents, - /// Represents the state of an epoch when all mixnodes have already been rewarded for their work in this epoch, - /// all issued actions got resolved and the epoch should now be advanced whilst assigning new rewarded set. - #[serde(alias = "AdvancingEpoch")] - AdvancingEpoch, + /// Represents the state of an epoch when all nodes have already been rewarded for their work in this epoch, + /// all issued actions got resolved and node roles should now be assigned before advancing into the next epoch. + RoleAssignment { next: Role }, } impl Display for EpochState { @@ -197,7 +204,9 @@ impl Display for EpochState { "mix rewarding (last rewarded: {last_rewarded}, final node: {final_node_id})" ), EpochState::ReconcilingEvents => write!(f, "event reconciliation"), - EpochState::AdvancingEpoch => write!(f, "advancing epoch"), + EpochState::RoleAssignment { next } => { + write!(f, "role assignment with next assignment for: {next}") + } } } } @@ -206,7 +215,7 @@ impl Display for EpochState { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Interval.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Interval.ts") )] #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, Serialize)] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] @@ -359,6 +368,17 @@ impl Interval { self.current_epoch_end_unix_timestamp() <= env.block.time.seconds() as i64 } + pub fn ensure_current_epoch_is_over(&self, env: &Env) -> Result<(), MixnetContractError> { + if !self.is_current_epoch_over(env) { + return Err(MixnetContractError::EpochInProgress { + current_block_time: env.block.time.seconds(), + epoch_start: self.current_epoch_start_unix_timestamp(), + epoch_end: self.current_epoch_end_unix_timestamp(), + }); + } + Ok(()) + } + pub fn secs_until_current_epoch_end(&self, env: &Env) -> i64 { if self.is_current_epoch_over(env) { 0 diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs index 50a5e90aec..fca9f6bfcf 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/lib.rs @@ -3,32 +3,30 @@ #![warn(clippy::expect_used)] #![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] -mod constants; +pub mod constants; pub mod delegation; pub mod error; pub mod events; -pub mod families; pub mod gateway; pub mod helpers; pub mod interval; pub mod mixnode; pub mod msg; +pub mod nym_node; pub mod pending_events; pub mod reward_params; pub mod rewarding; pub mod signing_types; pub mod types; +pub use constants::*; pub use contracts_common::types::*; pub use cosmwasm_std::{Addr, Coin, Decimal, Fraction}; pub use delegation::{ Delegation, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, - PagedMixNodeDelegationsResponse, -}; -pub use families::{ - Family, FamilyByHeadResponse, FamilyByLabelResponse, FamilyHead, FamilyMembersByHeadResponse, - FamilyMembersByLabelResponse, PagedFamiliesResponse, PagedMembersResponse, + PagedNodeDelegationsResponse, }; pub use gateway::{ Gateway, GatewayBond, GatewayBondResponse, GatewayConfigUpdate, GatewayOwnershipResponse, @@ -38,11 +36,12 @@ pub use interval::{ CurrentIntervalResponse, EpochId, EpochState, EpochStatus, Interval, IntervalId, }; pub use mixnode::{ - Layer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeCostParams, MixNodeDetails, - MixNodeRewarding, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, - MixnodeDetailsResponse, PagedMixnodeBondsResponse, RewardedSetNodeStatus, UnbondedMixnode, + LegacyMixLayer, MixNode, MixNodeBond, MixNodeConfigUpdate, MixNodeDetails, + MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, NodeCostParams, + NodeRewarding, PagedMixnodeBondsResponse, UnbondedMixnode, }; pub use msg::*; +pub use nym_node::{NymNode, NymNodeBond, NymNodeDetails, PendingNodeChanges}; pub use pending_events::{ EpochEventId, IntervalEventId, NumberOfPendingEventsResponse, PendingEpochEvent, PendingEpochEventData, PendingEpochEventKind, PendingEpochEventResponse, @@ -50,8 +49,6 @@ pub use pending_events::{ PendingIntervalEventKind, PendingIntervalEventResponse, PendingIntervalEventsResponse, }; pub use reward_params::{IntervalRewardParams, IntervalRewardingParamsUpdate, RewardingParams}; -pub use rewarding::{ - EstimatedCurrentEpochRewardResponse, PagedRewardedSetResponse, PendingRewardResponse, -}; +pub use rewarding::{EstimatedCurrentEpochRewardResponse, PendingRewardResponse}; pub use signing_types::*; pub use types::*; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs index 8772763150..fb03a1034b 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/mixnode.rs @@ -7,42 +7,19 @@ use crate::constants::{TOKEN_SUPPLY, UNIT_DELEGATION_BASE}; use crate::error::MixnetContractError; use crate::helpers::IntoBaseDecimal; -use crate::reward_params::{NodeRewardParams, RewardingParams}; +use crate::reward_params::{NodeRewardingParameters, RewardingParams}; use crate::rewarding::helpers::truncate_reward; use crate::rewarding::RewardDistribution; use crate::{ - Delegation, EpochEventId, EpochId, IdentityKey, MixId, OperatingCostRange, Percent, - ProfitMarginRange, SphinxKey, + Delegation, EpochEventId, EpochId, IdentityKey, IntervalEventId, NodeId, OperatingCostRange, + Percent, ProfitMarginRange, SphinxKey, }; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin, Decimal, StdResult, Uint128}; use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; -/// Current state of given node in the rewarded set. -#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] -#[cfg_attr( - feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/RewardedSetNodeStatus.ts") -)] -#[cw_serde] -#[derive(Copy)] -pub enum RewardedSetNodeStatus { - /// Node that is currently active, i.e. is expected to be used by clients for mixing packets. - #[serde(alias = "Active")] - Active, - - /// Node that is currently in standby, i.e. it's present in the rewarded set but is not active. - #[serde(alias = "Standby")] - Standby, -} - -impl RewardedSetNodeStatus { - pub fn is_active(&self) -> bool { - matches!(self, RewardedSetNodeStatus::Active) - } -} - /// Full details associated with given mixnode. #[cw_serde] pub struct MixNodeDetails { @@ -50,7 +27,7 @@ pub struct MixNodeDetails { pub bond_information: MixNodeBond, /// Details used for computation of rewarding related data. - pub rewarding_details: MixNodeRewarding, + pub rewarding_details: NodeRewarding, /// Adjustments to the mixnode that are ought to happen during future epoch transitions. #[serde(default)] @@ -60,7 +37,7 @@ pub struct MixNodeDetails { impl MixNodeDetails { pub fn new( bond_information: MixNodeBond, - rewarding_details: MixNodeRewarding, + rewarding_details: NodeRewarding, pending_changes: PendingMixNodeChanges, ) -> Self { MixNodeDetails { @@ -70,14 +47,10 @@ impl MixNodeDetails { } } - pub fn mix_id(&self) -> MixId { + pub fn mix_id(&self) -> NodeId { self.bond_information.mix_id } - pub fn layer(&self) -> Layer { - self.bond_information.layer - } - pub fn is_unbonding(&self) -> bool { self.bond_information.is_unbonding } @@ -106,10 +79,11 @@ impl MixNodeDetails { } } +// currently this struct is shared between mixnodes and nymnodes #[cw_serde] -pub struct MixNodeRewarding { +pub struct NodeRewarding { /// Information provided by the operator that influence the cost function. - pub cost_params: MixNodeCostParams, + pub cost_params: NodeCostParams, /// Total pledge and compounded reward earned by the node operator. pub operator: Decimal, @@ -120,7 +94,7 @@ pub struct MixNodeRewarding { /// Cumulative reward earned by the "unit delegation" since the block 0. pub total_unit_reward: Decimal, - /// Value of the theoretical "unit delegation" that has delegated to this mixnode at block 0. + /// Value of the theoretical "unit delegation" that has delegated to this node at block 0. pub unit_delegation: Decimal, /// Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt @@ -133,9 +107,9 @@ pub struct MixNodeRewarding { pub unique_delegations: u32, } -impl MixNodeRewarding { +impl NodeRewarding { pub fn initialise_new( - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, initial_pledge: &Coin, current_epoch: EpochId, ) -> Result { @@ -144,7 +118,7 @@ impl MixNodeRewarding { "pledge cannot be larger than the token supply" ); - Ok(MixNodeRewarding { + Ok(NodeRewarding { cost_params, operator: initial_pledge.amount.into_base_decimal()?, delegates: Decimal::zero(), @@ -155,6 +129,15 @@ impl MixNodeRewarding { }) } + pub fn normalise_cost_function( + &mut self, + allowed_profit_margin: ProfitMarginRange, + allowed_operating_cost: OperatingCostRange, + ) { + self.normalise_profit_margin(allowed_profit_margin); + self.normalise_operating_cost(allowed_operating_cost) + } + pub fn normalise_profit_margin(&mut self, allowed_range: ProfitMarginRange) { self.cost_params.profit_margin_percent = allowed_range.normalise(self.cost_params.profit_margin_percent) @@ -257,23 +240,18 @@ impl MixNodeRewarding { pub fn node_reward( &self, - reward_params: &RewardingParams, - node_params: NodeRewardParams, + global_params: &RewardingParams, + node_params: NodeRewardingParameters, ) -> Decimal { - let work = if node_params.in_active_set { - reward_params.active_node_work() - } else { - reward_params.standby_node_work() - }; - - let alpha = reward_params.interval.sybil_resistance; + let work = node_params.work_factor; + let alpha = global_params.interval.sybil_resistance; - reward_params.interval.epoch_reward_budget - * node_params.performance.value() - * self.bond_saturation(reward_params) + global_params.interval.epoch_reward_budget + * node_params.performance + * self.bond_saturation(global_params) * (work - + alpha.value() * self.pledge_saturation(reward_params) - / reward_params.dec_rewarded_set_size()) + + alpha.value() * self.pledge_saturation(global_params) + / global_params.dec_rewarded_set_size()) / (Decimal::one() + alpha.value()) } @@ -285,7 +263,7 @@ impl MixNodeRewarding { epochs_in_interval: u32, ) -> RewardDistribution { let node_cost = - self.cost_params.epoch_operating_cost(epochs_in_interval) * node_performance.value(); + self.cost_params.epoch_operating_cost(epochs_in_interval) * node_performance; // check if profit is positive if node_reward > node_cost { @@ -315,7 +293,7 @@ impl MixNodeRewarding { pub fn calculate_epoch_reward( &self, reward_params: &RewardingParams, - node_params: NodeRewardParams, + node_params: NodeRewardingParameters, epochs_in_interval: u32, ) -> RewardDistribution { let node_reward = self.node_reward(reward_params, node_params); @@ -341,7 +319,7 @@ impl MixNodeRewarding { pub fn epoch_rewarding( &mut self, reward_params: &RewardingParams, - node_params: NodeRewardParams, + node_params: NodeRewardingParameters, epochs_in_interval: u32, absolute_epoch_id: EpochId, ) { @@ -492,13 +470,30 @@ impl MixNodeRewarding { amount / self.delegates } } + + /// Returns a copy of `Self` with zeroed operator value + pub fn clear_operator(&self) -> NodeRewarding { + let mut zeroed = self.clone(); + zeroed.operator = Decimal::zero(); + zeroed + } } /// Basic mixnode information provided by the node operator. -#[cw_serde] +// note: we had to remove `#[cw_serde]` as it enforces `#[serde(deny_unknown_fields)]` which we do not want +// with the removal of explicit .layer field +#[derive( + ::cosmwasm_schema::serde::Serialize, + ::cosmwasm_schema::serde::Deserialize, + ::std::clone::Clone, + ::std::fmt::Debug, + ::std::cmp::PartialEq, + ::cosmwasm_schema::schemars::JsonSchema, +)] +#[schemars(crate = "::cosmwasm_schema::schemars")] pub struct MixNodeBond { /// Unique id assigned to the bonded mixnode. - pub mix_id: MixId, + pub mix_id: NodeId, /// Address of the owner of this mixnode. pub owner: Addr, @@ -506,9 +501,9 @@ pub struct MixNodeBond { /// Original amount pledged by the operator of this node. pub original_pledge: Coin, - /// Layer assigned to this mixnode. - pub layer: Layer, - + // REMOVED (but might be needed due to legacy things, idk yet) + // /// Layer assigned to this mixnode. + // pub layer: Layer, /// Information provided by the operator for the purposes of bonding. pub mix_node: MixNode, @@ -525,26 +520,6 @@ pub struct MixNodeBond { } impl MixNodeBond { - pub fn new( - mix_id: MixId, - owner: Addr, - original_pledge: Coin, - layer: Layer, - mix_node: MixNode, - bonding_height: u64, - ) -> Self { - MixNodeBond { - mix_id, - owner, - original_pledge, - layer, - mix_node, - proxy: None, - bonding_height, - is_unbonding: false, - } - } - pub fn identity(&self) -> &str { &self.mix_node.identity_key } @@ -567,7 +542,7 @@ impl MixNodeBond { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Mixnode.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Mixnode.ts") )] pub struct MixNode { /// Network address of this mixnode, for example 1.1.1.1 or foo.mixnode.com @@ -595,21 +570,21 @@ pub struct MixNode { /// The cost parameters, or the cost function, defined for the particular mixnode that influences /// how the rewards should be split between the node operator and its delegators. #[cw_serde] -pub struct MixNodeCostParams { - /// The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator. +pub struct NodeCostParams { + /// The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator. pub profit_margin_percent: Percent, - /// Operating cost of the associated mixnode per the entire interval. + /// Operating cost of the associated node per the entire interval. pub interval_operating_cost: Coin, } -impl MixNodeCostParams { +impl NodeCostParams { pub fn to_inline_json(&self) -> String { serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } -impl MixNodeCostParams { +impl NodeCostParams { pub fn epoch_operating_cost(&self, epochs_in_interval: u32) -> Decimal { Decimal::from_ratio(self.interval_operating_cost.amount, epochs_in_interval) } @@ -628,38 +603,39 @@ impl MixNodeCostParams { Deserialize_repr, JsonSchema, )] +#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] #[repr(u8)] -pub enum Layer { +pub enum LegacyMixLayer { One = 1, Two = 2, Three = 3, } -impl From for String { - fn from(layer: Layer) -> Self { +impl From for String { + fn from(layer: LegacyMixLayer) -> Self { (layer as u8).to_string() } } -impl TryFrom for Layer { +impl TryFrom for LegacyMixLayer { type Error = MixnetContractError; - fn try_from(i: u8) -> Result { + fn try_from(i: u8) -> Result { match i { - 1 => Ok(Layer::One), - 2 => Ok(Layer::Two), - 3 => Ok(Layer::Three), + 1 => Ok(LegacyMixLayer::One), + 2 => Ok(LegacyMixLayer::Two), + 3 => Ok(LegacyMixLayer::Three), _ => Err(MixnetContractError::InvalidLayer(i)), } } } -impl From for u8 { - fn from(layer: Layer) -> u8 { +impl From for u8 { + fn from(layer: LegacyMixLayer) -> u8 { match layer { - Layer::One => 1, - Layer::Two => 2, - Layer::Three => 3, + LegacyMixLayer::One => 1, + LegacyMixLayer::Two => 2, + LegacyMixLayer::Three => 3, } } } @@ -667,19 +643,49 @@ impl From for u8 { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PendingMixnodeChanges.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/PendingMixnodeChanges.ts" + ) )] -#[cw_serde] -#[derive(Default, Copy)] +// note: we had to remove `#[cw_serde]` as it enforces `#[serde(deny_unknown_fields)]` which we do not want +// with the addition of .cost_params_change field +#[derive( + ::cosmwasm_schema::serde::Serialize, + ::cosmwasm_schema::serde::Deserialize, + ::std::clone::Clone, + ::std::fmt::Debug, + ::std::cmp::PartialEq, + ::cosmwasm_schema::schemars::JsonSchema, + Default, + Copy, +)] +#[schemars(crate = "::cosmwasm_schema::schemars")] pub struct PendingMixNodeChanges { pub pledge_change: Option, - // pub cost_params_change: Option, + + #[serde(default)] + pub cost_params_change: Option, +} + +#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct LegacyPendingMixNodeChanges { + pub pledge_change: Option, +} + +impl From for LegacyPendingMixNodeChanges { + fn from(value: PendingMixNodeChanges) -> Self { + LegacyPendingMixNodeChanges { + pledge_change: value.pledge_change, + } + } } impl PendingMixNodeChanges { pub fn new_empty() -> PendingMixNodeChanges { PendingMixNodeChanges { pledge_change: None, + cost_params_change: None, } } } @@ -688,7 +694,10 @@ impl PendingMixNodeChanges { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/UnbondedMixnode.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/UnbondedMixnode.ts" + ) )] #[cw_serde] pub struct UnbondedMixnode { @@ -712,7 +721,10 @@ pub struct UnbondedMixnode { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixNodeConfigUpdate.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/MixNodeConfigUpdate.ts" + ) )] #[cw_serde] pub struct MixNodeConfigUpdate { @@ -740,11 +752,11 @@ pub struct PagedMixnodeBondsResponse { pub per_page: usize, /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, + pub start_next_after: Option, } impl PagedMixnodeBondsResponse { - pub fn new(nodes: Vec, per_page: usize, start_next_after: Option) -> Self { + pub fn new(nodes: Vec, per_page: usize, start_next_after: Option) -> Self { PagedMixnodeBondsResponse { nodes, per_page, @@ -766,14 +778,14 @@ pub struct PagedMixnodesDetailsResponse { pub per_page: usize, /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, + pub start_next_after: Option, } impl PagedMixnodesDetailsResponse { pub fn new( nodes: Vec, per_page: usize, - start_next_after: Option, + start_next_after: Option, ) -> Self { PagedMixnodesDetailsResponse { nodes, @@ -787,21 +799,21 @@ impl PagedMixnodesDetailsResponse { #[cw_serde] pub struct PagedUnbondedMixnodesResponse { /// The past ids of unbonded mixnodes alongside their basic information such as the owner or the identity key. - pub nodes: Vec<(MixId, UnbondedMixnode)>, + pub nodes: Vec<(NodeId, UnbondedMixnode)>, /// Maximum number of entries that could be included in a response. `per_page <= nodes.len()` // this field is rather redundant and should be deprecated. pub per_page: usize, /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, + pub start_next_after: Option, } impl PagedUnbondedMixnodesResponse { pub fn new( - nodes: Vec<(MixId, UnbondedMixnode)>, + nodes: Vec<(NodeId, UnbondedMixnode)>, per_page: usize, - start_next_after: Option, + start_next_after: Option, ) -> Self { PagedUnbondedMixnodesResponse { nodes, @@ -825,7 +837,7 @@ pub struct MixOwnershipResponse { #[cw_serde] pub struct MixnodeDetailsResponse { /// Id of the requested mixnode. - pub mix_id: MixId, + pub mix_id: NodeId, /// If there exists a mixnode with the provided id, this field contains its detailed information. pub mixnode_details: Option, @@ -845,17 +857,17 @@ pub struct MixnodeDetailsByIdentityResponse { #[cw_serde] pub struct MixnodeRewardingDetailsResponse { /// Id of the requested mixnode. - pub mix_id: MixId, + pub mix_id: NodeId, /// If there exists a mixnode with the provided id, this field contains its rewarding information. - pub rewarding_details: Option, + pub rewarding_details: Option, } /// Response containing basic information of an unbonded mixnode with the provided id. #[cw_serde] pub struct UnbondedMixnodeResponse { /// Id of the requested mixnode. - pub mix_id: MixId, + pub mix_id: NodeId, /// If there existed a mixnode with the provided id, this field contains its basic information. pub unbonded_info: Option, @@ -863,9 +875,9 @@ pub struct UnbondedMixnodeResponse { /// Response containing the current state of the stake saturation of a mixnode with the provided id. #[cw_serde] -pub struct StakeSaturationResponse { +pub struct MixStakeSaturationResponse { /// Id of the requested mixnode. - pub mix_id: MixId, + pub mix_id: NodeId, /// The current stake saturation of this node that is indirectly used in reward calculation formulas. /// Note that it can't be larger than 1. diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index 863fdc0327..0e728aaf63 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -3,15 +3,17 @@ use crate::delegation::{self, OwnerProxySubKey}; use crate::error::MixnetContractError; -use crate::families::FamilyHead; use crate::gateway::{Gateway, GatewayConfigUpdate}; use crate::helpers::IntoBaseDecimal; -use crate::mixnode::{Layer, MixNode, MixNodeConfigUpdate, MixNodeCostParams}; +use crate::mixnode::{MixNode, MixNodeConfigUpdate, NodeCostParams}; +use crate::nym_node::{NodeConfigUpdate, Role}; use crate::pending_events::{EpochEventId, IntervalEventId}; use crate::reward_params::{ - IntervalRewardParams, IntervalRewardingParamsUpdate, Performance, RewardingParams, + ActiveSetUpdate, IntervalRewardParams, IntervalRewardingParamsUpdate, NodeRewardingParameters, + Performance, RewardedSetParams, RewardingParams, WorkFactor, }; -use crate::types::{ContractStateParams, LayerAssignment, MixId}; +use crate::types::{ContractStateParams, NodeId}; +use crate::{NymNode, RoleAssignment}; use crate::{OperatingCostRange, ProfitMarginRange}; use contracts_common::{signing::MessageSignature, IdentityKey, Percent}; use cosmwasm_schema::cw_serde; @@ -21,28 +23,31 @@ use std::time::Duration; #[cfg(feature = "schema")] use crate::{ delegation::{ - MixNodeDelegationResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, - PagedMixNodeDelegationsResponse, + NodeDelegationResponse, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, + PagedNodeDelegationsResponse, }, - families::{ - FamilyByHeadResponse, FamilyByLabelResponse, FamilyMembersByHeadResponse, - FamilyMembersByLabelResponse, PagedFamiliesResponse, PagedMembersResponse, + gateway::{ + GatewayBondResponse, GatewayOwnershipResponse, PagedGatewayResponse, + PreassignedGatewayIdsResponse, }, - gateway::{GatewayBondResponse, GatewayOwnershipResponse, PagedGatewayResponse}, interval::{CurrentIntervalResponse, EpochStatus}, mixnode::{ - MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, - MixnodeRewardingDetailsResponse, PagedMixnodeBondsResponse, PagedMixnodesDetailsResponse, - PagedUnbondedMixnodesResponse, StakeSaturationResponse, UnbondedMixnodeResponse, + MixOwnershipResponse, MixStakeSaturationResponse, MixnodeDetailsByIdentityResponse, + MixnodeDetailsResponse, MixnodeRewardingDetailsResponse, PagedMixnodeBondsResponse, + PagedMixnodesDetailsResponse, PagedUnbondedMixnodesResponse, UnbondedMixnodeResponse, + }, + nym_node::{ + EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeDetailsResponse, + NodeOwnershipResponse, NodeRewardingDetailsResponse, PagedNymNodeBondsResponse, + PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, RolesMetadataResponse, + StakeSaturationResponse, UnbondedNodeResponse, }, pending_events::{ NumberOfPendingEventsResponse, PendingEpochEventResponse, PendingEpochEventsResponse, PendingIntervalEventResponse, PendingIntervalEventsResponse, }, - rewarding::{ - EstimatedCurrentEpochRewardResponse, PagedRewardedSetResponse, PendingRewardResponse, - }, - types::{ContractState, LayerDistribution}, + rewarding::{EstimatedCurrentEpochRewardResponse, PendingRewardResponse}, + types::ContractState, }; #[cfg(feature = "schema")] use contracts_common::{signing::Nonce, ContractBuildInformation}; @@ -76,8 +81,7 @@ pub struct InitialRewardingParams { pub active_set_work_factor: Decimal, pub interval_pool_emission: Percent, - pub rewarded_set_size: u32, - pub active_set_size: u32, + pub rewarded_set_params: RewardedSetParams, } impl InitialRewardingParams { @@ -88,8 +92,11 @@ impl InitialRewardingParams { let epoch_reward_budget = self.initial_reward_pool / epochs_in_interval.into_base_decimal()? * self.interval_pool_emission; - let stake_saturation_point = - self.initial_staking_supply / self.rewarded_set_size.into_base_decimal()?; + let stake_saturation_point = self.initial_staking_supply + / self + .rewarded_set_params + .rewarded_set_size() + .into_base_decimal()?; Ok(RewardingParams { interval: IntervalRewardParams { @@ -102,8 +109,7 @@ impl InitialRewardingParams { active_set_work_factor: self.active_set_work_factor, interval_pool_emission: self.interval_pool_emission, }, - rewarded_set_size: self.rewarded_set_size, - active_set_size: self.active_set_size, + rewarded_set: self.rewarded_set_params, }) } } @@ -115,45 +121,6 @@ pub enum ExecuteMsg { admin: String, }, - AssignNodeLayer { - mix_id: MixId, - layer: Layer, - }, - // Families - /// Only owner of the node can crate the family with node as head - CreateFamily { - label: String, - }, - /// Family head needs to sign the joining node IdentityKey - JoinFamily { - join_permit: MessageSignature, - family_head: FamilyHead, - }, - LeaveFamily { - family_head: FamilyHead, - }, - KickFamilyMember { - member: IdentityKey, - }, - CreateFamilyOnBehalf { - owner_address: String, - label: String, - }, - /// Family head needs to sign the joining node IdentityKey, MixNode needs to provide its signature proving that it wants to join the family - JoinFamilyOnBehalf { - member_address: String, - join_permit: MessageSignature, - family_head: FamilyHead, - }, - LeaveFamilyOnBehalf { - member_address: String, - family_head: FamilyHead, - }, - KickFamilyMemberOnBehalf { - head_address: String, - member: IdentityKey, - }, - // state/sys-params-related UpdateRewardingValidatorAddress { address: String, @@ -161,8 +128,8 @@ pub enum ExecuteMsg { UpdateContractStateParams { updated_parameters: ContractStateParams, }, - UpdateActiveSetSize { - active_set_size: u32, + UpdateActiveSetDistribution { + update: ActiveSetUpdate, force_immediately: bool, }, UpdateRewardingParams { @@ -174,25 +141,24 @@ pub enum ExecuteMsg { epoch_duration_secs: u64, force_immediately: bool, }, + BeginEpochTransition {}, - AdvanceCurrentEpoch { - new_rewarded_set: Vec, - // families_in_layer: HashMap, - expected_active_set_size: u32, - }, ReconcileEpochEvents { limit: Option, }, + AssignRoles { + assignment: RoleAssignment, + }, // mixnode-related: BondMixnode { mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, }, BondMixnodeOnBehalf { mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, owner: String, }, @@ -211,11 +177,15 @@ pub enum ExecuteMsg { UnbondMixnodeOnBehalf { owner: String, }, - UpdateMixnodeCostParams { - new_costs: MixNodeCostParams, + #[serde( + alias = "UpdateMixnodeCostParams", + alias = "update_mixnode_cost_params" + )] + UpdateCostParams { + new_costs: NodeCostParams, }, UpdateMixnodeCostParamsOnBehalf { - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, owner: String, }, UpdateMixnodeConfig { @@ -225,6 +195,7 @@ pub enum ExecuteMsg { new_config: MixNodeConfigUpdate, owner: String, }, + MigrateMixnode {}, // gateway-related: BondGateway { @@ -247,44 +218,64 @@ pub enum ExecuteMsg { new_config: GatewayConfigUpdate, owner: String, }, + MigrateGateway { + cost_params: Option, + }, + + // nym-node related: + BondNymNode { + node: NymNode, + cost_params: NodeCostParams, + owner_signature: MessageSignature, + }, + UnbondNymNode {}, + UpdateNodeConfig { + update: NodeConfigUpdate, + }, // delegation-related: - DelegateToMixnode { - mix_id: MixId, + #[serde(alias = "DelegateToMixnode", alias = "delegate_to_mixnode")] + Delegate { + #[serde(alias = "mix_id")] + node_id: NodeId, }, DelegateToMixnodeOnBehalf { - mix_id: MixId, + mix_id: NodeId, delegate: String, }, - UndelegateFromMixnode { - mix_id: MixId, + #[serde(alias = "UndelegateFromMixnode", alias = "undelegate_from_mixnode")] + Undelegate { + #[serde(alias = "mix_id")] + node_id: NodeId, }, UndelegateFromMixnodeOnBehalf { - mix_id: MixId, + mix_id: NodeId, delegate: String, }, // reward-related - RewardMixnode { - mix_id: MixId, - performance: Performance, + RewardNode { + #[serde(alias = "mix_id")] + node_id: NodeId, + params: NodeRewardingParameters, }, WithdrawOperatorReward {}, WithdrawOperatorRewardOnBehalf { owner: String, }, WithdrawDelegatorReward { - mix_id: MixId, + #[serde(alias = "mix_id")] + node_id: NodeId, }, WithdrawDelegatorRewardOnBehalf { - mix_id: MixId, + mix_id: NodeId, owner: String, }, // vesting migration: MigrateVestedMixNode {}, MigrateVestedDelegation { - mix_id: MixId, + mix_id: NodeId, }, // testing-only @@ -298,41 +289,15 @@ impl ExecuteMsg { pub fn default_memo(&self) -> String { match self { ExecuteMsg::UpdateAdmin { admin } => format!("updating contract admin to {admin}"), - ExecuteMsg::AssignNodeLayer { mix_id, layer } => { - format!("assigning mix {mix_id} for layer {layer:?}") - } - ExecuteMsg::CreateFamily { .. } => "crating node family with".to_string(), - ExecuteMsg::JoinFamily { family_head, .. } => { - format!("joining family {family_head}") - } - ExecuteMsg::LeaveFamily { family_head, .. } => { - format!("leaving family {family_head}") - } - ExecuteMsg::KickFamilyMember { member, .. } => { - format!("kicking {member} from family") - } - ExecuteMsg::CreateFamilyOnBehalf { .. } => "crating node family with".to_string(), - ExecuteMsg::JoinFamilyOnBehalf { family_head, .. } => { - format!("joining family {family_head}") - } - ExecuteMsg::LeaveFamilyOnBehalf { family_head, .. } => { - format!("leaving family {family_head}") - } - ExecuteMsg::KickFamilyMemberOnBehalf { member, .. } => { - format!("kicking {member} from family") - } ExecuteMsg::UpdateRewardingValidatorAddress { address } => { format!("updating rewarding validator to {address}") } ExecuteMsg::UpdateContractStateParams { .. } => { "updating mixnet state parameters".into() } - ExecuteMsg::UpdateActiveSetSize { - active_set_size, - force_immediately, - } => format!( - "updating active set size to {active_set_size}. forced: {force_immediately}" - ), + ExecuteMsg::UpdateActiveSetDistribution { + force_immediately, .. + } => format!("updating active set distribution. forced: {force_immediately}"), ExecuteMsg::UpdateRewardingParams { force_immediately, .. } => format!("updating mixnet rewarding parameters. forced: {force_immediately}"), @@ -340,7 +305,6 @@ impl ExecuteMsg { force_immediately, .. } => format!("updating mixnet interval configuration. forced: {force_immediately}"), ExecuteMsg::BeginEpochTransition {} => "beginning epoch transition".into(), - ExecuteMsg::AdvanceCurrentEpoch { .. } => "advancing current epoch".into(), ExecuteMsg::ReconcileEpochEvents { .. } => "reconciling epoch events".into(), ExecuteMsg::BondMixnode { mix_node, .. } => { format!("bonding mixnode {}", mix_node.identity_key) @@ -356,7 +320,7 @@ impl ExecuteMsg { } ExecuteMsg::UnbondMixnode { .. } => "unbonding mixnode".into(), ExecuteMsg::UnbondMixnodeOnBehalf { .. } => "unbonding mixnode on behalf".into(), - ExecuteMsg::UpdateMixnodeCostParams { .. } => "updating mixnode cost parameters".into(), + ExecuteMsg::UpdateCostParams { .. } => "updating mixnode cost parameters".into(), ExecuteMsg::UpdateMixnodeCostParamsOnBehalf { .. } => { "updating mixnode cost parameters on behalf".into() } @@ -376,25 +340,22 @@ impl ExecuteMsg { ExecuteMsg::UpdateGatewayConfigOnBehalf { .. } => { "updating gateway configuration on behalf".into() } - ExecuteMsg::DelegateToMixnode { mix_id } => format!("delegating to mixnode {mix_id}"), + ExecuteMsg::Delegate { node_id: mix_id } => format!("delegating to mixnode {mix_id}"), ExecuteMsg::DelegateToMixnodeOnBehalf { mix_id, .. } => { format!("delegating to mixnode {mix_id} on behalf") } - ExecuteMsg::UndelegateFromMixnode { mix_id } => { + ExecuteMsg::Undelegate { node_id: mix_id } => { format!("removing delegation from mixnode {mix_id}") } ExecuteMsg::UndelegateFromMixnodeOnBehalf { mix_id, .. } => { format!("removing delegation from mixnode {mix_id} on behalf") } - ExecuteMsg::RewardMixnode { - mix_id, - performance, - } => format!("rewarding mixnode {mix_id} for performance {performance}"), + ExecuteMsg::RewardNode { node_id, .. } => format!("rewarding node {node_id}"), ExecuteMsg::WithdrawOperatorReward { .. } => "withdrawing operator reward".into(), ExecuteMsg::WithdrawOperatorRewardOnBehalf { .. } => { "withdrawing operator reward on behalf".into() } - ExecuteMsg::WithdrawDelegatorReward { mix_id } => { + ExecuteMsg::WithdrawDelegatorReward { node_id: mix_id } => { format!("withdrawing delegator reward from mixnode {mix_id}") } ExecuteMsg::WithdrawDelegatorRewardOnBehalf { mix_id, .. } => { @@ -402,6 +363,12 @@ impl ExecuteMsg { } ExecuteMsg::MigrateVestedMixNode { .. } => "migrate vested mixnode".into(), ExecuteMsg::MigrateVestedDelegation { .. } => "migrate vested delegation".to_string(), + ExecuteMsg::AssignRoles { .. } => "assigning epoch roles".into(), + ExecuteMsg::MigrateMixnode { .. } => "migrating legacy mixnode".into(), + ExecuteMsg::MigrateGateway { .. } => "migrating legacy gateway".into(), + ExecuteMsg::BondNymNode { .. } => "bonding nym-node".into(), + ExecuteMsg::UnbondNymNode { .. } => "unbonding nym-node".into(), + ExecuteMsg::UpdateNodeConfig { .. } => "updating node config".into(), #[cfg(feature = "contract-testing")] ExecuteMsg::TestingResolveAllPendingEvents { .. } => { @@ -417,43 +384,6 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(cw_controllers::AdminResponse))] Admin {}, - // families - /// Gets the list of families registered in this contract. - #[cfg_attr(feature = "schema", returns(PagedFamiliesResponse))] - GetAllFamiliesPaged { - /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. - limit: Option, - - /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, - }, - - /// Gets the list of all family members registered in this contract. - #[cfg_attr(feature = "schema", returns(PagedMembersResponse))] - GetAllMembersPaged { - /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. - limit: Option, - - /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, - }, - - /// Attempts to lookup family information given the family head. - #[cfg_attr(feature = "schema", returns(FamilyByHeadResponse))] - GetFamilyByHead { head: String }, - - /// Attempts to lookup family information given the family label. - #[cfg_attr(feature = "schema", returns(FamilyByLabelResponse))] - GetFamilyByLabel { label: String }, - - /// Attempts to retrieve family members given the family head. - #[cfg_attr(feature = "schema", returns(FamilyMembersByHeadResponse))] - GetFamilyMembersByHead { head: String }, - - /// Attempts to retrieve family members given the family label. - #[cfg_attr(feature = "schema", returns(FamilyMembersByLabelResponse))] - GetFamilyMembersByLabel { label: String }, - // state/sys-params-related /// Gets build information of this contract, such as the commit hash used for the build or rustc version. #[cfg_attr(feature = "schema", returns(ContractBuildInformation))] @@ -488,16 +418,6 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(CurrentIntervalResponse))] GetCurrentIntervalDetails {}, - /// Gets the current list of mixnodes in the rewarded set. - #[cfg_attr(feature = "schema", returns(PagedRewardedSetResponse))] - GetRewardedSet { - /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. - limit: Option, - - /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, - }, - // mixnode-related: /// Gets the basic list of all currently bonded mixnodes. #[cfg_attr(feature = "schema", returns(PagedMixnodeBondsResponse))] @@ -506,7 +426,7 @@ pub enum QueryMsg { limit: Option, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, + start_after: Option, }, /// Gets the detailed list of all currently bonded mixnodes. @@ -516,7 +436,7 @@ pub enum QueryMsg { limit: Option, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, + start_after: Option, }, /// Gets the basic list of all unbonded mixnodes. @@ -526,20 +446,20 @@ pub enum QueryMsg { limit: Option, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, + start_after: Option, }, /// Gets the basic list of all unbonded mixnodes that belonged to a particular owner. #[cfg_attr(feature = "schema", returns(PagedUnbondedMixnodesResponse))] GetUnbondedMixNodesByOwner { - /// The address of the owner of the the mixnodes used for the query. + /// The address of the owner of the mixnodes used for the query. owner: String, /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. limit: Option, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, + start_after: Option, }, /// Gets the basic list of all unbonded mixnodes that used the particular identity key. @@ -552,7 +472,7 @@ pub enum QueryMsg { limit: Option, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option, + start_after: Option, }, /// Gets the detailed mixnode information belonging to the particular owner. @@ -566,28 +486,28 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(MixnodeDetailsResponse))] GetMixnodeDetails { /// Id of the node to query. - mix_id: MixId, + mix_id: NodeId, }, /// Gets the rewarding information of a mixnode with the provided id. #[cfg_attr(feature = "schema", returns(MixnodeRewardingDetailsResponse))] GetMixnodeRewardingDetails { /// Id of the node to query. - mix_id: MixId, + mix_id: NodeId, }, /// Gets the stake saturation of a mixnode with the provided id. - #[cfg_attr(feature = "schema", returns(StakeSaturationResponse))] + #[cfg_attr(feature = "schema", returns(MixStakeSaturationResponse))] GetStakeSaturation { /// Id of the node to query. - mix_id: MixId, + mix_id: NodeId, }, /// Gets the basic information of an unbonded mixnode with the provided id. #[cfg_attr(feature = "schema", returns(UnbondedMixnodeResponse))] GetUnbondedMixNodeInformation { /// Id of the node to query. - mix_id: MixId, + mix_id: NodeId, }, /// Gets the detailed mixnode information of a node given its current identity key. @@ -597,10 +517,6 @@ pub enum QueryMsg { mix_identity: IdentityKey, }, - /// Gets the current layer configuration of the mix network. - #[cfg_attr(feature = "schema", returns(LayerDistribution))] - GetLayerDistribution {}, - // gateway-related: /// Gets the basic list of all currently bonded gateways. #[cfg_attr(feature = "schema", returns(PagedGatewayResponse))] @@ -626,12 +542,129 @@ pub enum QueryMsg { address: String, }, + /// Get the `NodeId`s of all the legacy gateways that they will get assigned once migrated into NymNodes + #[cfg_attr(feature = "schema", returns(PreassignedGatewayIdsResponse))] + GetPreassignedGatewayIds { + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + }, + + // nym-node-related: + /// Gets the basic list of all currently bonded nymnodes. + #[cfg_attr(feature = "schema", returns(PagedNymNodeBondsResponse))] + GetNymNodeBondsPaged { + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + /// Gets the detailed list of all currently bonded nymnodes. + #[cfg_attr(feature = "schema", returns(PagedNymNodeDetailsResponse))] + GetNymNodesDetailedPaged { + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + /// Gets the basic information of an unbonded nym-node with the provided id. + #[cfg_attr(feature = "schema", returns(UnbondedNodeResponse))] + GetUnbondedNymNode { + /// Id of the node to query. + node_id: NodeId, + }, + + /// Gets the basic list of all unbonded nymnodes. + #[cfg_attr(feature = "schema", returns(PagedUnbondedNymNodesResponse))] + GetUnbondedNymNodesPaged { + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + /// Gets the basic list of all unbonded nymnodes that belonged to a particular owner. + #[cfg_attr(feature = "schema", returns(PagedUnbondedNymNodesResponse))] + GetUnbondedNymNodesByOwnerPaged { + /// The address of the owner of the nym-node used for the query + owner: String, + + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + /// Gets the basic list of all unbonded nymnodes that used the particular identity key. + #[cfg_attr(feature = "schema", returns(PagedUnbondedNymNodesResponse))] + GetUnbondedNymNodesByIdentityKeyPaged { + /// The identity key (base58-encoded ed25519 public key) of the node used for the query. + identity_key: IdentityKey, + + /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. + limit: Option, + + /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. + start_after: Option, + }, + + /// Gets the detailed nymnode information belonging to the particular owner. + #[cfg_attr(feature = "schema", returns(NodeOwnershipResponse))] + GetOwnedNymNode { + /// Address of the node owner to use for the query. + address: String, + }, + + /// Gets the detailed nymnode information of a node with the provided id. + #[cfg_attr(feature = "schema", returns(NodeDetailsResponse))] + GetNymNodeDetails { + /// Id of the node to query. + node_id: NodeId, + }, + + /// Gets the detailed nym-node information given its current identity key. + #[cfg_attr(feature = "schema", returns(NodeDetailsByIdentityResponse))] + GetNymNodeDetailsByIdentityKey { + /// The identity key (base58-encoded ed25519 public key) of the nym-node used for the query. + node_identity: IdentityKey, + }, + + /// Gets the rewarding information of a nym-node with the provided id. + #[cfg_attr(feature = "schema", returns(NodeRewardingDetailsResponse))] + GetNodeRewardingDetails { + /// Id of the node to query. + node_id: NodeId, + }, + + /// Gets the stake saturation of a nym-node with the provided id. + #[cfg_attr(feature = "schema", returns(StakeSaturationResponse))] + GetNodeStakeSaturation { + /// Id of the node to query. + node_id: NodeId, + }, + + #[cfg_attr(feature = "schema", returns(EpochAssignmentResponse))] + GetRoleAssignment { role: Role }, + + #[cfg_attr(feature = "schema", returns(RolesMetadataResponse))] + GetRewardedSetMetadata {}, + // delegation-related: - /// Gets all delegations associated with particular mixnode - #[cfg_attr(feature = "schema", returns(PagedMixNodeDelegationsResponse))] - GetMixnodeDelegations { + /// Gets all delegations associated with particular node + #[cfg_attr(feature = "schema", returns(PagedNodeDelegationsResponse))] + #[serde(alias = "GetMixnodeDelegations", alias = "get_mixnode_delegations")] + GetNodeDelegations { /// Id of the node to query. - mix_id: MixId, + #[serde(alias = "mix_id")] + node_id: NodeId, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. start_after: Option, @@ -649,17 +682,18 @@ pub enum QueryMsg { delegator: String, /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option<(MixId, OwnerProxySubKey)>, + start_after: Option<(NodeId, OwnerProxySubKey)>, /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. limit: Option, }, /// Gets delegation information associated with particular mixnode - delegator pair - #[cfg_attr(feature = "schema", returns(MixNodeDelegationResponse))] + #[cfg_attr(feature = "schema", returns(NodeDelegationResponse))] GetDelegationDetails { /// Id of the node to query. - mix_id: MixId, + #[serde(alias = "mix_id")] + node_id: NodeId, /// The address of the owner of the delegation. delegator: String, @@ -689,9 +723,14 @@ pub enum QueryMsg { /// Gets the reward amount accrued by the particular mixnode that has not yet been claimed. #[cfg_attr(feature = "schema", returns(PendingRewardResponse))] - GetPendingMixNodeOperatorReward { + #[serde( + alias = "GetPendingMixNodeOperatorReward", + alias = "get_pending_mix_node_operator_reward" + )] + GetPendingNodeOperatorReward { /// Id of the node to query. - mix_id: MixId, + #[serde(alias = "mix_id")] + node_id: NodeId, }, /// Gets the reward amount accrued by the particular delegator that has not yet been claimed. @@ -701,7 +740,8 @@ pub enum QueryMsg { address: String, /// Id of the node to query. - mix_id: MixId, + #[serde(alias = "mix_id")] + node_id: NodeId, /// Entity who made the delegation on behalf of the owner. /// If present, it's most likely the address of the vesting contract. @@ -712,10 +752,14 @@ pub enum QueryMsg { #[cfg_attr(feature = "schema", returns(EstimatedCurrentEpochRewardResponse))] GetEstimatedCurrentEpochOperatorReward { /// Id of the node to query. - mix_id: MixId, + #[serde(alias = "mix_id")] + node_id: NodeId, /// The estimated performance for the current epoch of the given node. estimated_performance: Performance, + + /// The estimated work for the current epoch of the given node. + estimated_work: Option, }, /// Given the provided node performance, attempt to estimate the delegator reward for the current epoch. @@ -725,14 +769,14 @@ pub enum QueryMsg { address: String, /// Id of the node to query. - mix_id: MixId, - - /// Entity who made the delegation on behalf of the owner. - /// If present, it's most likely the address of the vesting contract. - proxy: Option, + #[serde(alias = "mix_id")] + node_id: NodeId, /// The estimated performance for the current epoch of the given node. estimated_performance: Performance, + + /// The estimated work for the current epoch of the given node. + estimated_work: Option, }, // interval-related @@ -786,4 +830,5 @@ pub enum QueryMsg { #[cw_serde] pub struct MigrateMsg { pub vesting_contract_address: Option, + pub unsafe_skip_state_updates: Option, } diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs new file mode 100644 index 0000000000..375fd34374 --- /dev/null +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/nym_node.rs @@ -0,0 +1,589 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::error::MixnetContractError; +use crate::{EpochEventId, EpochId, Gateway, IntervalEventId, MixNode, NodeId, NodeRewarding}; +use contracts_common::IdentityKey; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Addr, Coin, Decimal, StdError, StdResult}; +use cw_storage_plus::{IntKey, Key, KeyDeserialize, PrimaryKey}; +use std::fmt::{Display, Formatter}; + +#[cw_serde] +#[derive(PartialOrd, Copy, Hash, Eq)] +#[repr(u8)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export, export_to = "ts-packages/types/src/types/rust/Role.ts") +)] +pub enum Role { + #[serde(rename = "eg", alias = "entry", alias = "entry_gateway")] + EntryGateway = 0, + + #[serde(rename = "l1", alias = "layer1")] + Layer1 = 1, + + #[serde(rename = "l2", alias = "layer2")] + Layer2 = 2, + + #[serde(rename = "l3", alias = "layer3")] + Layer3 = 3, + + #[serde(rename = "xg", alias = "exit", alias = "exit_gateway")] + ExitGateway = 4, + + #[serde(rename = "stb", alias = "standby")] + Standby = 128, +} + +impl TryFrom for Role { + type Error = MixnetContractError; + fn try_from(value: u8) -> Result { + match value { + n if n == Role::EntryGateway as u8 => Ok(Role::EntryGateway), + n if n == Role::Layer1 as u8 => Ok(Role::Layer1), + n if n == Role::Layer2 as u8 => Ok(Role::Layer2), + n if n == Role::Layer3 as u8 => Ok(Role::Layer3), + n if n == Role::ExitGateway as u8 => Ok(Role::ExitGateway), + n if n == Role::Standby as u8 => Ok(Role::Standby), + n => Err(MixnetContractError::UnknownRoleRepresentation { got: n }), + } + } +} + +impl<'a> PrimaryKey<'a> for Role { + type Prefix = >::Prefix; + type SubPrefix = >::SubPrefix; + type Suffix = >::Suffix; + type SuperSuffix = >::SuperSuffix; + + fn key(&self) -> Vec { + // I'm not sure why it wasn't possible to delegate the call to + // `(*self as u8).key()` directly... + // I guess because of the `Key::Ref(&'a [u8])` variant? + vec![Key::Val8((*self as u8).to_cw_bytes())] + } + + fn joined_key(&self) -> Vec { + (*self as u8).joined_key() + } + + fn joined_extra_key(&self, key: &[u8]) -> Vec { + (*self as u8).joined_extra_key(key) + } +} + +impl KeyDeserialize for Role { + type Output = Role; + + fn from_vec(value: Vec) -> StdResult { + let u8_key: ::Output = ::from_vec(value)?; + Role::try_from(u8_key).map_err(|err| StdError::generic_err(err.to_string())) + } + + fn from_slice(value: &[u8]) -> StdResult { + let u8_key: ::Output = ::from_slice(value)?; + Role::try_from(u8_key).map_err(|err| StdError::generic_err(err.to_string())) + } +} + +impl Role { + pub fn first() -> Role { + Role::ExitGateway + } + + pub fn next(&self) -> Option { + // roles have to be assigned in the following order: + // exit -> entry -> l1 -> l2 -> l3 -> standby + match self { + Role::ExitGateway => Some(Role::EntryGateway), + Role::EntryGateway => Some(Role::Layer1), + Role::Layer1 => Some(Role::Layer2), + Role::Layer2 => Some(Role::Layer3), + Role::Layer3 => Some(Role::Standby), + Role::Standby => None, + } + } + + pub fn is_first(&self) -> bool { + self == &Role::first() + } + + pub fn is_standby(&self) -> bool { + matches!(self, Role::Standby) + } +} + +impl Display for Role { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Role::Layer1 => write!(f, "mix layer 1"), + Role::Layer2 => write!(f, "mix layer 2"), + Role::Layer3 => write!(f, "mix layer 3"), + Role::EntryGateway => write!(f, "entry gateway"), + Role::ExitGateway => write!(f, "exit gateway"), + Role::Standby => write!(f, "standby"), + } + } +} + +/// Metadata associated with the rewarded set. +#[cw_serde] +#[derive(Default, Copy)] +pub struct RewardedSetMetadata { + /// Epoch that this data corresponds to. + pub epoch_id: EpochId, + + /// Indicates whether all roles got assigned to the set for this epoch. + pub fully_assigned: bool, + + /// Metadata for the 'EntryGateway' role + pub entry_gateway_metadata: RoleMetadata, + + /// Metadata for the 'ExitGateway' role + pub exit_gateway_metadata: RoleMetadata, + + /// Metadata for the 'Layer1' role + pub layer1_metadata: RoleMetadata, + + /// Metadata for the 'Layer2' role + pub layer2_metadata: RoleMetadata, + + /// Metadata for the 'Layer3' role + pub layer3_metadata: RoleMetadata, + + /// Metadata for the 'Standby' role + pub standby_metadata: RoleMetadata, +} + +impl RewardedSetMetadata { + pub fn new(epoch_id: EpochId) -> Self { + RewardedSetMetadata { + epoch_id, + fully_assigned: false, + entry_gateway_metadata: Default::default(), + exit_gateway_metadata: Default::default(), + layer1_metadata: Default::default(), + layer2_metadata: Default::default(), + layer3_metadata: Default::default(), + standby_metadata: Default::default(), + } + } + + pub fn set_role_count(&mut self, role: Role, num_nodes: u32) { + match role { + Role::EntryGateway => self.entry_gateway_metadata.num_nodes = num_nodes, + Role::Layer1 => self.layer1_metadata.num_nodes = num_nodes, + Role::Layer2 => self.layer2_metadata.num_nodes = num_nodes, + Role::Layer3 => self.layer3_metadata.num_nodes = num_nodes, + Role::ExitGateway => self.exit_gateway_metadata.num_nodes = num_nodes, + Role::Standby => self.standby_metadata.num_nodes = num_nodes, + } + } + + pub fn set_highest_id(&mut self, highest_id: NodeId, role: Role) { + match role { + Role::EntryGateway => self.entry_gateway_metadata.highest_id = highest_id, + Role::Layer1 => self.layer1_metadata.highest_id = highest_id, + Role::Layer2 => self.layer2_metadata.highest_id = highest_id, + Role::Layer3 => self.layer3_metadata.highest_id = highest_id, + Role::ExitGateway => self.exit_gateway_metadata.highest_id = highest_id, + Role::Standby => self.standby_metadata.highest_id = highest_id, + } + } + + pub fn highest_rewarded_id(&self) -> NodeId { + let mut highest = 0; + if self.layer1_metadata.highest_id > highest { + highest = self.layer1_metadata.highest_id; + } + if self.layer2_metadata.highest_id > highest { + highest = self.layer2_metadata.highest_id; + } + if self.layer3_metadata.highest_id > highest { + highest = self.layer3_metadata.highest_id; + } + if self.entry_gateway_metadata.highest_id > highest { + highest = self.entry_gateway_metadata.highest_id; + } + if self.exit_gateway_metadata.highest_id > highest { + highest = self.exit_gateway_metadata.highest_id; + } + if self.standby_metadata.highest_id > highest { + highest = self.standby_metadata.highest_id; + } + + highest + } +} + +/// Metadata associated with particular node role. +#[cw_serde] +#[derive(Default, Copy)] +pub struct RoleMetadata { + /// Highest, also latest, node-id of a node assigned this role. + pub highest_id: NodeId, + + /// Number of nodes assigned this particular role. + pub num_nodes: u32, +} + +/// Full details associated with given node. +#[cw_serde] +pub struct NymNodeDetails { + /// Basic bond information of this node, such as owner address, original pledge, etc. + pub bond_information: NymNodeBond, + + /// Details used for computation of rewarding related data. + pub rewarding_details: NodeRewarding, + + /// Adjustments to the node that are scheduled to happen during future epoch/interval transitions. + pub pending_changes: PendingNodeChanges, +} + +impl NymNodeDetails { + pub fn new( + bond_information: NymNodeBond, + rewarding_details: NodeRewarding, + pending_changes: PendingNodeChanges, + ) -> Self { + NymNodeDetails { + bond_information, + rewarding_details, + pending_changes, + } + } + + pub fn node_id(&self) -> NodeId { + self.bond_information.node_id + } + + pub fn is_unbonding(&self) -> bool { + self.bond_information.is_unbonding + } + + pub fn original_pledge(&self) -> &Coin { + &self.bond_information.original_pledge + } + + pub fn pending_operator_reward(&self) -> Coin { + let pledge = self.original_pledge(); + self.rewarding_details.pending_operator_reward(pledge) + } + + pub fn pending_detailed_operator_reward(&self) -> StdResult { + let pledge = self.original_pledge(); + self.rewarding_details + .pending_detailed_operator_reward(pledge) + } + + pub fn total_stake(&self) -> Decimal { + self.rewarding_details.node_bond() + } + + pub fn pending_pledge_change(&self) -> Option { + self.pending_changes.pledge_change + } +} + +#[cw_serde] +pub struct NymNodeBond { + /// Unique id assigned to the bonded node. + pub node_id: NodeId, + + /// Address of the owner of this nym-node. + pub owner: Addr, + + /// Original amount pledged by the operator of this node. + pub original_pledge: Coin, + + /// Block height at which this nym-node has been bonded. + pub bonding_height: u64, + + /// Flag to indicate whether this node is in the process of unbonding, + /// that will conclude upon the epoch finishing. + pub is_unbonding: bool, + + /// Information provided by the operator for the purposes of bonding. + pub node: NymNode, +} + +impl NymNodeBond { + pub fn new( + node_id: NodeId, + owner: Addr, + original_pledge: Coin, + node: impl Into, + bonding_height: u64, + ) -> NymNodeBond { + Self { + node_id, + owner, + original_pledge, + bonding_height, + is_unbonding: false, + node: node.into(), + } + } + + pub fn identity(&self) -> &str { + &self.node.identity_key + } + + pub fn ensure_bonded(&self) -> Result<(), MixnetContractError> { + if self.is_unbonding { + return Err(MixnetContractError::NodeIsUnbonding { + node_id: self.node_id, + }); + } + Ok(()) + } +} + +/// Information provided by the node operator during bonding that are used to allow other entities to use the services of this node. +#[cw_serde] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export, export_to = "ts-packages/types/src/types/rust/NymNode.ts") +)] +pub struct NymNode { + /// Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com + /// that is used to discover other capabilities of this node. + pub host: String, + + /// Allow specifying custom port for accessing the http, and thus self-described, api + /// of this node for the capabilities discovery. + pub custom_http_port: Option, + + /// Base58-encoded ed25519 EdDSA public key. + pub identity_key: IdentityKey, + // TODO: I don't think we want to include sphinx keys here, + // given we want to rotate them and keeping that in sync with contract will be a PITA +} + +impl NymNode { + /// Perform naive validation of the attached identity key - makes sure it's correctly encoded + /// and has 32 bytes (as expected from ed25519). we're not, however, checking if it's a valid curve point + pub fn naive_ensure_valid_pubkey(&self) -> Result<(), MixnetContractError> { + let decoded = bs58::decode(&self.identity_key) + .into_vec() + .map_err(|_| MixnetContractError::InvalidPubKey)?; + if decoded.len() != 32 { + return Err(MixnetContractError::InvalidPubKey); + } + Ok(()) + } + + /// Makes sure the provided host's length is at most 255 characters to prevent abuse. + pub fn ensure_host_in_range(&self) -> Result<(), MixnetContractError> { + if self.host.len() > 255 { + return Err(MixnetContractError::HostTooLong); + } + Ok(()) + } +} + +impl From for NymNode { + fn from(value: MixNode) -> Self { + NymNode { + host: value.host, + custom_http_port: Some(value.http_api_port), + identity_key: value.identity_key, + } + } +} + +impl From for NymNode { + fn from(value: Gateway) -> Self { + NymNode { + host: value.host, + custom_http_port: None, + identity_key: value.identity_key, + } + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/NodeConfigUpdate.ts" + ) +)] +#[cw_serde] +#[derive(Default)] +pub struct NodeConfigUpdate { + pub host: Option, + // ideally this would have been `Option>`, but not sure if json would have recognised it + pub custom_http_port: Option, + + // equivalent to setting `custom_http_port` to `None` + #[serde(default)] + pub restore_default_http_port: bool, +} + +#[cw_serde] +#[derive(Default, Copy)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/PendingNodeChanges.ts" + ) +)] +pub struct PendingNodeChanges { + pub pledge_change: Option, + pub cost_params_change: Option, +} + +impl PendingNodeChanges { + pub fn new_empty() -> PendingNodeChanges { + PendingNodeChanges { + pledge_change: None, + cost_params_change: None, + } + } + + pub fn ensure_no_pending_pledge_changes(&self) -> Result<(), MixnetContractError> { + if let Some(pending_event_id) = self.pledge_change { + return Err(MixnetContractError::PendingPledgeChange { pending_event_id }); + } + Ok(()) + } + + pub fn ensure_no_pending_params_changes(&self) -> Result<(), MixnetContractError> { + if let Some(pending_event_id) = self.cost_params_change { + return Err(MixnetContractError::PendingParamsChange { pending_event_id }); + } + Ok(()) + } +} + +/// Basic information of a node that used to be part of the nym network but has already unbonded. +#[cw_serde] +pub struct UnbondedNymNode { + /// Base58-encoded ed25519 EdDSA public key. + pub identity_key: IdentityKey, + + /// NodeId assigned to this node. + pub node_id: NodeId, + + /// Address of the owner of this nym node. + pub owner: Addr, + + /// Block height at which this nym node has unbonded. + pub unbonding_height: u64, +} + +/// Response containing rewarding information of a node with the provided id. +#[cw_serde] +pub struct NodeRewardingDetailsResponse { + /// Id of the requested node. + pub node_id: NodeId, + + /// If there exists a node with the provided id, this field contains its rewarding information. + pub rewarding_details: Option, +} + +/// Response containing details of a node belonging to the particular owner. +#[cw_serde] +pub struct NodeOwnershipResponse { + /// Validated address of the node owner. + pub address: Addr, + + /// If the provided address owns a nym-node, this field contains its detailed information. + pub details: Option, +} + +/// Response containing details of a node with the provided id. +#[cw_serde] +pub struct NodeDetailsResponse { + /// Id of the requested node. + pub node_id: NodeId, + + /// If there exists a node with the provided id, this field contains its detailed information. + pub details: Option, +} + +/// Response containing details of a bonded node with the provided identity key. +#[cw_serde] +pub struct NodeDetailsByIdentityResponse { + /// The identity key (base58-encoded ed25519 public key) of the node. + pub identity_key: IdentityKey, + + /// If there exists a bonded node with the provided identity key, this field contains its detailed information. + pub details: Option, +} + +/// Response containing the current state of the stake saturation of a node with the provided id. +#[cw_serde] +pub struct StakeSaturationResponse { + /// Id of the requested node. + pub node_id: NodeId, + + /// The current stake saturation of this node that is indirectly used in reward calculation formulas. + /// Note that it can't be larger than 1. + pub current_saturation: Option, + + /// The current, absolute, stake saturation of this node. + /// Note that as the name suggests it can be larger than 1. + /// However, anything beyond that value has no effect on the total node reward. + pub uncapped_saturation: Option, +} + +/// Response containing paged list of all nym-nodes that have ever unbonded. +#[cw_serde] +pub struct PagedUnbondedNymNodesResponse { + /// Basic information of the node such as the owner or the identity key. + pub nodes: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + +/// Response containing basic information of an unbonded nym-node with the provided id. +#[cw_serde] +pub struct UnbondedNodeResponse { + /// Id of the requested nym-node. + pub node_id: NodeId, + + /// If there existed a nym-node with the provided id, this field contains its basic information. + pub details: Option, +} + +#[cw_serde] +pub struct PagedNymNodeBondsResponse { + /// The nym node bond information present in the contract. + pub nodes: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + +#[cw_serde] +pub struct PagedNymNodeDetailsResponse { + /// All nym-node details stored in the contract. + /// Apart from the basic bond information it also contains details required for all future reward calculation + /// as well as any pending changes requested by the operator. + pub nodes: Vec, + + /// Field indicating paging information for the following queries if the caller wishes to get further entries. + pub start_next_after: Option, +} + +#[cw_serde] +pub struct EpochAssignmentResponse { + /// Epoch that this data corresponds to. + pub epoch_id: EpochId, + + pub nodes: Vec, +} + +#[cw_serde] +pub struct RolesMetadataResponse { + pub metadata: RewardedSetMetadata, +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs index 8d97e20494..72266d01d8 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/pending_events.rs @@ -1,9 +1,9 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::mixnode::MixNodeCostParams; -use crate::reward_params::IntervalRewardingParamsUpdate; -use crate::{BlockHeight, MixId}; +use crate::mixnode::NodeCostParams; +use crate::reward_params::{ActiveSetUpdate, IntervalRewardingParamsUpdate}; +use crate::{BlockHeight, NodeId}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin}; @@ -35,7 +35,7 @@ pub struct PendingEpochEventData { pub enum PendingEpochEventKind { // can't just pass the `Delegation` struct here as it's impossible to determine // `cumulative_reward_ratio` ahead of time - /// Request to create a delegation towards particular mixnode. + /// Request to create a delegation towards particular node. /// Note that if such delegation already exists, it will get updated with the provided token amount. #[serde(alias = "Delegate")] #[non_exhaustive] @@ -43,8 +43,9 @@ pub enum PendingEpochEventKind { /// The address of the owner of the delegation. owner: Addr, - /// The id of the mixnode used for the delegation. - mix_id: MixId, + /// The id of the node used for the delegation. + #[serde(alias = "mix_id")] + node_id: NodeId, /// The amount of tokens to use for the delegation. amount: Coin, @@ -54,36 +55,55 @@ pub enum PendingEpochEventKind { proxy: Option, }, - /// Request to remove delegation from particular mixnode. + /// Request to remove delegation from particular node. #[serde(alias = "Undelegate")] #[non_exhaustive] Undelegate { /// The address of the owner of the delegation. owner: Addr, - /// The id of the mixnode used for the delegation. - mix_id: MixId, + /// The id of the node used for the delegation. + #[serde(alias = "mix_id")] + node_id: NodeId, /// Entity who made the delegation on behalf of the owner. /// If present, it's most likely the address of the vesting contract. proxy: Option, }, + /// Request to pledge more tokens (by the node operator) towards its node. + NymNodePledgeMore { + /// The id of the nym node that will have its pledge updated. + node_id: NodeId, + + /// The amount of additional tokens to use in the pledge. + amount: Coin, + }, + /// Request to pledge more tokens (by the node operator) towards its node. #[serde(alias = "PledgeMore")] - PledgeMore { + MixnodePledgeMore { /// The id of the mixnode that will have its pledge updated. - mix_id: MixId, + mix_id: NodeId, - /// The amount of additional tokens to use by the pledge. + /// The amount of additional tokens to use in the pledge. amount: Coin, }, + /// Request to decrease amount of pledged tokens (by the node operator) from its node. + NymNodeDecreasePledge { + /// The id of the nym node that will have its pledge updated. + node_id: NodeId, + + /// The amount of tokens that should be removed from the pledge. + decrease_by: Coin, + }, + /// Request to decrease amount of pledged tokens (by the node operator) from its node. #[serde(alias = "DecreasePledge")] - DecreasePledge { + MixnodeDecreasePledge { /// The id of the mixnode that will have its pledge updated. - mix_id: MixId, + mix_id: NodeId, /// The amount of tokens that should be removed from the pledge. decrease_by: Coin, @@ -93,15 +113,17 @@ pub enum PendingEpochEventKind { #[serde(alias = "UnbondMixnode")] UnbondMixnode { /// The id of the mixnode that will get unbonded. - mix_id: MixId, + mix_id: NodeId, }, - /// Request to update the current size of the active set. - #[serde(alias = "UpdateActiveSetSize")] - UpdateActiveSetSize { - /// The new desired size of the active set. - new_size: u32, + /// Request to unbond a nym node and completely remove it from the network. + UnbondNymNode { + /// The id of the node that will get unbonded. + node_id: NodeId, }, + + /// Request to update the current active set. + UpdateActiveSet { update: ActiveSetUpdate }, } impl PendingEpochEventKind { @@ -112,19 +134,19 @@ impl PendingEpochEventKind { } } - pub fn new_delegate(owner: Addr, mix_id: MixId, amount: Coin) -> Self { + pub fn new_delegate(owner: Addr, node_id: NodeId, amount: Coin) -> Self { PendingEpochEventKind::Delegate { owner, - mix_id, + node_id, amount, proxy: None, } } - pub fn new_undelegate(owner: Addr, mix_id: MixId) -> Self { + pub fn new_undelegate(owner: Addr, node_id: NodeId) -> Self { PendingEpochEventKind::Undelegate { owner, - mix_id, + node_id, proxy: None, } } @@ -166,10 +188,19 @@ pub enum PendingIntervalEventKind { #[serde(alias = "ChangeMixCostParams")] ChangeMixCostParams { /// The id of the mixnode that will have its cost parameters updated. - mix_id: MixId, + mix_id: NodeId, /// The new updated cost function of this mixnode. - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, + }, + + /// Request to update cost parameters of given nym node. + ChangeNymNodeCostParams { + /// The id of the nym node that will have its cost parameters updated. + node_id: NodeId, + + /// The new updated cost function of this nym node. + new_costs: NodeCostParams, }, /// Request to update the underlying rewarding parameters used by the system diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs index 4928bd6a1e..40e34b2888 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/reward_params.rs @@ -2,17 +2,22 @@ // SPDX-License-Identifier: Apache-2.0 use crate::helpers::IntoBaseDecimal; +use crate::nym_node::Role; use crate::{error::MixnetContractError, Percent}; use cosmwasm_schema::cw_serde; use cosmwasm_std::Decimal; pub type Performance = Percent; +pub type WorkFactor = Decimal; /// Parameters required by the mix-mining reward distribution that do not change during an interval. #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/IntervalRewardParams.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/IntervalRewardParams.ts" + ) )] #[cw_serde] #[derive(Copy)] @@ -78,7 +83,10 @@ impl IntervalRewardParams { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/RewardingParams.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/RewardingParams.ts" + ) )] #[cw_serde] #[derive(Copy)] @@ -86,23 +94,15 @@ pub struct RewardingParams { /// Parameters that should remain unchanged throughout an interval. pub interval: IntervalRewardParams, - // while the rewarded set size can change between epochs to accommodate for bandwidth demands, - // the active set size should be unchanged between epochs and should only be adjusted between - // intervals. However, it makes more sense to keep both of those values together as they're - // very strongly related to each other. - /// The expected number of mixnodes in the rewarded set (i.e. active + standby). - pub rewarded_set_size: u32, - - /// The expected number of mixnodes in the active set. - pub active_set_size: u32, + pub rewarded_set: RewardedSetParams, } impl RewardingParams { - pub fn active_node_work(&self) -> Decimal { + pub fn active_node_work(&self) -> WorkFactor { self.interval.active_set_work_factor * self.standby_node_work() } - pub fn standby_node_work(&self) -> Decimal { + pub fn standby_node_work(&self) -> WorkFactor { let f = self.interval.active_set_work_factor; let k = self.dec_rewarded_set_size(); let one = Decimal::one(); @@ -113,27 +113,33 @@ impl RewardingParams { one / (f * k - (f - one) * k_r) } + pub fn rewarded_set_size(&self) -> u32 { + self.rewarded_set.rewarded_set_size() + } + + pub fn active_set_size(&self) -> u32 { + self.rewarded_set.active_set_size() + } + pub fn dec_rewarded_set_size(&self) -> Decimal { // the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal // with 0 decimal places #[allow(clippy::unwrap_used)] - self.rewarded_set_size.into_base_decimal().unwrap() + self.rewarded_set_size().into_base_decimal().unwrap() } pub fn dec_active_set_size(&self) -> Decimal { // the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal // with 0 decimal places #[allow(clippy::unwrap_used)] - self.active_set_size.into_base_decimal().unwrap() + self.active_set_size().into_base_decimal().unwrap() } fn dec_standby_set_size(&self) -> Decimal { // the unwrap here is fine as we're guaranteed an `u32` is going to fit in a Decimal // with 0 decimal places #[allow(clippy::unwrap_used)] - (self.rewarded_set_size - self.active_set_size) - .into_base_decimal() - .unwrap() + self.rewarded_set.standby.into_base_decimal().unwrap() } pub fn apply_epochs_in_interval_change(&mut self, new_epochs_in_interval: u32) { @@ -146,19 +152,35 @@ impl RewardingParams { * self.interval.interval_pool_emission; } - pub fn try_change_active_set_size( - &mut self, - new_active_set_size: u32, + pub fn validate_active_set_update( + &self, + update: ActiveSetUpdate, ) -> Result<(), MixnetContractError> { - if new_active_set_size == 0 { - return Err(MixnetContractError::ZeroActiveSet); - } + update.ensure_non_empty()?; + let active_set_size = update.active_set_size(); - if new_active_set_size > self.rewarded_set_size { + if active_set_size > self.rewarded_set_size() { return Err(MixnetContractError::InvalidActiveSetSize); } - self.active_set_size = new_active_set_size; + Ok(()) + } + + pub fn try_change_active_set( + &mut self, + update: ActiveSetUpdate, + ) -> Result<(), MixnetContractError> { + self.validate_active_set_update(update)?; + let active_set_size = update.active_set_size(); + let rewarded_set_size = self.rewarded_set_size(); + + // safety: due to validation we know that the active_set_size <= rewarded_set_size + let new_standby = rewarded_set_size - active_set_size; + + self.rewarded_set.exit_gateways = update.exit_gateways; + self.rewarded_set.entry_gateways = update.entry_gateways; + self.rewarded_set.mixnodes = update.mixnodes; + self.rewarded_set.standby = new_standby; Ok(()) } @@ -201,16 +223,10 @@ impl RewardingParams { self.interval.interval_pool_emission = interval_pool_emission; } - if let Some(rewarded_set_size) = updates.rewarded_set_size { - if rewarded_set_size == 0 { - return Err(MixnetContractError::ZeroRewardedSet); - } - if rewarded_set_size < self.active_set_size { - return Err(MixnetContractError::InvalidRewardedSetSize); - } - + if let Some(rewarded_set_update) = updates.rewarded_set_params { + rewarded_set_update.ensure_valid()?; recompute_saturation_point = true; - self.rewarded_set_size = rewarded_set_size; + self.rewarded_set = rewarded_set_update; } if recompute_epoch_budget { @@ -221,39 +237,123 @@ impl RewardingParams { if recompute_saturation_point { self.interval.stake_saturation_point = - self.interval.staking_supply / self.rewarded_set_size.into_base_decimal()? + self.interval.staking_supply / self.rewarded_set_size().into_base_decimal()? + } + + Ok(()) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/RewardedSetParams.ts" + ) +)] +#[cw_serde] +#[derive(Copy)] +pub struct RewardedSetParams { + /// The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`]) + pub entry_gateways: u32, + + /// The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`]) + pub exit_gateways: u32, + + /// The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`]. + pub mixnodes: u32, + + /// Number of nodes in the 'standby' set. (i.e. [`Role::Standby`]) + pub standby: u32, +} + +impl RewardedSetParams { + pub fn active_set_size(&self) -> u32 { + self.entry_gateways + self.exit_gateways + self.mixnodes + } + + pub fn rewarded_set_size(&self) -> u32 { + self.active_set_size() + self.standby + } + + pub fn ensure_valid(&self) -> Result<(), MixnetContractError> { + if self.entry_gateways == 0 || self.exit_gateways == 0 || self.mixnodes == 0 { + return Err(MixnetContractError::EmptyRoleAssignment); + } + if self.mixnodes % 3 != 0 { + return Err(MixnetContractError::UnevenLayerAssignment); + } + Ok(()) + } + + pub fn maximum_role_count(&self, role: Role) -> u32 { + match role { + Role::EntryGateway => self.entry_gateways, + Role::Layer1 | Role::Layer2 | Role::Layer3 => self.mixnodes / 3, + Role::ExitGateway => self.exit_gateways, + Role::Standby => self.standby, + } + } + + pub fn ensure_role_count(&self, role: Role, assigned: u32) -> Result<(), MixnetContractError> { + let allowed = self.maximum_role_count(role); + + if assigned > allowed { + return Err(MixnetContractError::IllegalRoleCount { + role, + assigned, + allowed, + }); } Ok(()) } } -// TODO: possibly refactor this -/// Parameters used for rewarding particular mixnode. +/// Parameters used for rewarding particular node. +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/NodeRewardingParameters.ts" + ) +)] #[cw_serde] #[derive(Copy)] -pub struct NodeRewardParams { +pub struct NodeRewardingParameters { /// Performance of the particular node in the current epoch. - pub performance: Percent, + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub performance: Performance, - /// Flag indicating whether the node has been in the active set during the epoch. - pub in_active_set: bool, + /// Amount of work performed by this node in the current epoch + /// also known as 'omega' in the paper + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub work_factor: WorkFactor, } -impl NodeRewardParams { - pub fn new(performance: Percent, in_active_set: bool) -> Self { - NodeRewardParams { +impl NodeRewardingParameters { + pub fn new(performance: Performance, work_factor: WorkFactor) -> Self { + NodeRewardingParameters { performance, - in_active_set, + work_factor, } } + + pub fn is_zero(&self) -> bool { + self.performance.is_zero() || self.work_factor.is_zero() + } } /// Specification on how the rewarding params should be updated. #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/IntervalRewardingParamsUpdate.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/IntervalRewardingParamsUpdate.ts" + ) )] #[cw_serde] #[derive(Copy, Default)] @@ -282,8 +382,8 @@ pub struct IntervalRewardingParamsUpdate { /// Defines the new value of the interval pool emission rate. pub interval_pool_emission: Option, - /// Defines the new size of the rewarded set. - pub rewarded_set_size: Option, + /// Defines the parameters of the rewarded set. + pub rewarded_set_params: Option, } impl IntervalRewardingParamsUpdate { @@ -295,10 +395,45 @@ impl IntervalRewardingParamsUpdate { || self.sybil_resistance_percent.is_some() || self.active_set_work_factor.is_some() || self.interval_pool_emission.is_some() - || self.rewarded_set_size.is_some() + || self.rewarded_set_params.is_some() } pub fn to_inline_json(&self) -> String { serde_json_wasm::to_string(self).unwrap_or_else(|_| "serialisation failure".into()) } } + +/// Specification on how the active set should be updated. +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/ActiveSetUpdate.ts" + ) +)] +#[cw_serde] +#[derive(Copy, Default)] +pub struct ActiveSetUpdate { + /// The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`]) + pub entry_gateways: u32, + + /// The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`]) + pub exit_gateways: u32, + + /// The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`]. + pub mixnodes: u32, +} + +impl ActiveSetUpdate { + pub fn active_set_size(&self) -> u32 { + self.entry_gateways + self.exit_gateways + self.mixnodes + } + + pub fn ensure_non_empty(&self) -> Result<(), MixnetContractError> { + if self.entry_gateways == 0 || self.exit_gateways == 0 || self.mixnodes == 0 { + return Err(MixnetContractError::EmptyRoleAssignment); + } + Ok(()) + } +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs index 4ef9cd433a..e1c320f806 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/mod.rs @@ -1,7 +1,6 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{MixId, RewardedSetNodeStatus}; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, Decimal}; @@ -11,7 +10,10 @@ pub mod simulator; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/RewardEstimate.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/RewardEstimate.ts" + ) )] #[cw_serde] #[derive(Copy, Default)] @@ -63,7 +65,10 @@ pub struct PendingRewardResponse { /// The associated mixnode is still fully bonded, meaning it is neither unbonded /// nor in the process of unbonding that would have finished at the epoch transition. + #[deprecated(note = "this field will be removed. use .node_still_fully_bonded instead")] pub mixnode_still_fully_bonded: bool, + + pub node_still_fully_bonded: bool, } /// Response containing estimation of node rewards for the current epoch. @@ -99,13 +104,3 @@ impl EstimatedCurrentEpochRewardResponse { } } } - -/// Response containing paged list of all mixnodes in the rewarded set. -#[cw_serde] -pub struct PagedRewardedSetResponse { - /// Nodes in the current rewarded set. - pub nodes: Vec<(MixId, RewardedSetNodeStatus)>, - - /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option, -} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs index 0cc4d06dee..46326a1712 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/mod.rs @@ -3,23 +3,21 @@ use crate::error::MixnetContractError; use crate::helpers::IntoBaseDecimal; -use crate::reward_params::NodeRewardParams; +use crate::reward_params::{NodeRewardingParameters, WorkFactor}; use crate::rewarding::simulator::simulated_node::SimulatedNode; use crate::rewarding::RewardDistribution; -use crate::{ - Delegation, Interval, IntervalRewardParams, MixId, MixNodeCostParams, RewardingParams, -}; +use crate::{Delegation, Interval, IntervalRewardParams, NodeCostParams, NodeId, RewardingParams}; use cosmwasm_std::{Coin, Decimal}; use std::collections::BTreeMap; pub mod simulated_node; pub struct Simulator { - pub nodes: BTreeMap, + pub nodes: BTreeMap, pub system_rewarding_params: RewardingParams, pub interval: Interval, - next_mix_id: MixId, + next_mix_id: NodeId, pending_reward_pool_emission: Decimal, } @@ -34,6 +32,14 @@ impl Simulator { } } + pub fn legacy_standby_work_factor(&self) -> WorkFactor { + self.system_rewarding_params.standby_node_work() + } + + pub fn legacy_active_work_factor(&self) -> WorkFactor { + self.system_rewarding_params.active_node_work() + } + fn advance_epoch(&mut self) -> Result<(), MixnetContractError> { let updated = self.interval.advance_epoch(); @@ -53,7 +59,7 @@ impl Simulator { let stake_saturation_point = staking_supply / self .system_rewarding_params - .rewarded_set_size + .rewarded_set_size() .into_base_decimal()?; let updated_params = RewardingParams { @@ -67,8 +73,7 @@ impl Simulator { active_set_work_factor: old.active_set_work_factor, interval_pool_emission: old.interval_pool_emission, }, - rewarded_set_size: self.system_rewarding_params.rewarded_set_size, - active_set_size: self.system_rewarding_params.active_set_size, + rewarded_set: self.system_rewarding_params.rewarded_set, }; self.system_rewarding_params = updated_params; @@ -82,8 +87,8 @@ impl Simulator { pub fn bond( &mut self, pledge: Coin, - cost_params: MixNodeCostParams, - ) -> Result { + cost_params: NodeCostParams, + ) -> Result { let mix_id = self.next_mix_id; self.nodes.insert( @@ -105,7 +110,7 @@ impl Simulator { &mut self, delegator: S, delegation: Coin, - mix_id: MixId, + mix_id: NodeId, ) -> Result<(), MixnetContractError> { let node = self .nodes @@ -119,7 +124,7 @@ impl Simulator { pub fn undelegate>( &mut self, delegator: S, - mix_id: MixId, + mix_id: NodeId, ) -> Result<(Coin, Coin), MixnetContractError> { let node = self .nodes @@ -130,7 +135,7 @@ impl Simulator { pub fn simulate_epoch_single_node( &mut self, - params: NodeRewardParams, + params: NodeRewardingParameters, ) -> Result { assert_eq!(self.nodes.len(), 1); @@ -148,8 +153,8 @@ impl Simulator { pub fn simulate_epoch( &mut self, - node_params: &BTreeMap, - ) -> Result, MixnetContractError> { + node_params: &BTreeMap, + ) -> Result, MixnetContractError> { let mut params_keys = node_params.keys().copied().collect::>(); params_keys.sort_unstable(); let mut node_keys = self.nodes.keys().copied().collect::>(); @@ -185,7 +190,7 @@ impl Simulator { &self, delegation: &Delegation, ) -> Result { - Ok(self.nodes[&delegation.mix_id] + Ok(self.nodes[&delegation.node_id] .rewarding_details .determine_delegation_reward(delegation)?) } @@ -206,7 +211,7 @@ impl Simulator { // assume node state doesn't change in the interval (kinda unrealistic) pub fn simulate_full_interval( &mut self, - node_params: &BTreeMap, + node_params: &BTreeMap, ) -> Result<(), MixnetContractError> { for _ in 0..self.interval.epochs_in_interval() { self.simulate_epoch(node_params)?; @@ -219,6 +224,7 @@ impl Simulator { mod tests { use super::*; use crate::helpers::compare_decimals; + use crate::reward_params::RewardedSetParams; use crate::Percent; use cosmwasm_std::testing::mock_env; use std::time::Duration; @@ -226,6 +232,7 @@ mod tests { #[cfg(test)] mod single_node_case { use super::*; + use crate::reward_params::RewardedSetParams; use crate::rewarding::helpers::truncate_reward_amount; use cosmwasm_std::coin; @@ -237,15 +244,23 @@ mod tests { let profit_margin = Percent::from_percentage_value(10).unwrap(); let interval_operating_cost = Coin::new(40_000_000, "unym"); let epochs_in_interval = 720u32; - let rewarded_set_size = 240; - let active_set_size = 100; let interval_pool_emission = Percent::from_percentage_value(2).unwrap(); + // the import values here are active set being 100 and rewarded set being 240 + // since those are the values we were using in the past + let rewarded_set = RewardedSetParams { + entry_gateways: 20, + exit_gateways: 50, + mixnodes: 30, + standby: 140, + }; + let reward_pool = 250_000_000_000_000u128; let staking_supply = 100_000_000_000_000u128; let epoch_reward_budget = interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval); - let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size); + let stake_saturation_point = + Decimal::from_ratio(staking_supply, rewarded_set.rewarded_set_size()); let rewarding_params = RewardingParams { interval: IntervalRewardParams { @@ -258,8 +273,7 @@ mod tests { active_set_work_factor: Decimal::percent(1000), // value '10' interval_pool_emission, }, - rewarded_set_size, - active_set_size, + rewarded_set, }; let interval = Interval::init_interval( @@ -270,7 +284,7 @@ mod tests { let initial_pledge = Coin::new(initial_pledge, "unym"); let mut simulator = Simulator::new(rewarding_params, interval); - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: profit_margin, interval_operating_cost, }; @@ -315,8 +329,10 @@ mod tests { fn simulator_returns_expected_values_for_base_case() { let mut simulator = base_simulator(10000_000000); - let epoch_params = - NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let epoch_params = NodeRewardingParameters::new( + Percent::from_percentage_value(100).unwrap(), + simulator.legacy_active_work_factor(), + ); let rewards = simulator.simulate_epoch_single_node(epoch_params).unwrap(); assert_eq!(rewards.delegates, Decimal::zero()); @@ -334,8 +350,10 @@ mod tests { .delegate("alice", Coin::new(18000_000000, "unym"), 0) .unwrap(); - let node_params = - NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let node_params = NodeRewardingParameters::new( + Percent::from_percentage_value(100).unwrap(), + simulator.legacy_active_work_factor(), + ); let rewards = simulator.simulate_epoch_single_node(node_params).unwrap(); compare_decimals( @@ -364,8 +382,10 @@ mod tests { #[test] fn delegation_and_undelegation() { let mut simulator = base_simulator(10000_000000); - let node_params = - NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let node_params = NodeRewardingParameters::new( + Percent::from_percentage_value(100).unwrap(), + simulator.legacy_active_work_factor(), + ); let rewards1 = simulator.simulate_epoch_single_node(node_params).unwrap(); let expected_operator1 = "1128452.5416104363".parse().unwrap(); @@ -411,8 +431,10 @@ mod tests { // essentially all delegators' rewards (and the operator itself) are still correctly computed let original_pledge = coin(10000_000000, "unym"); let mut simulator = base_simulator(original_pledge.amount.u128()); - let node_params = - NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let node_params = NodeRewardingParameters::new( + Percent::from_percentage_value(100).unwrap(), + simulator.legacy_active_work_factor(), + ); // add 2 delegations at genesis (because it makes things easier and as shown with previous tests // delegating at different times still work) @@ -454,8 +476,10 @@ mod tests { fn withdrawing_delegator_reward() { // essentially all delegators' rewards (and the operator itself) are still correctly computed let mut simulator = base_simulator(10000_000000); - let node_params = - NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true); + let node_params = NodeRewardingParameters::new( + Percent::from_percentage_value(100).unwrap(), + simulator.legacy_active_work_factor(), + ); // add 2 delegations at genesis (because it makes things easier and as shown with previous tests // delegating at different times still work) @@ -524,7 +548,7 @@ mod tests { fn simulating_multiple_epochs() { let mut simulator = base_simulator(10000_000000); - let mut is_active = true; + let mut work_factor = simulator.legacy_active_work_factor(); let mut performance = Percent::from_percentage_value(100).unwrap(); for epoch in 0..720 { if epoch == 0 { @@ -538,7 +562,7 @@ mod tests { .unwrap() } if epoch == 89 { - is_active = false; + work_factor = simulator.legacy_standby_work_factor(); } if epoch == 123 { simulator @@ -560,7 +584,7 @@ mod tests { // TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be } if epoch == 345 { - is_active = true; + work_factor = simulator.legacy_active_work_factor(); } if epoch == 358 { performance = Percent::from_percentage_value(100).unwrap(); @@ -579,7 +603,7 @@ mod tests { // this has to always hold check_rewarding_invariant(&simulator); - let node_params = NodeRewardParams::new(performance, is_active); + let node_params = NodeRewardingParameters::new(performance, work_factor); simulator.simulate_epoch_single_node(node_params).unwrap(); } @@ -600,15 +624,23 @@ mod tests { // rather than just checking the final results let epochs_in_interval = 1u32; - let rewarded_set_size = 10; - let active_set_size = 6; let interval_pool_emission = Percent::from_percentage_value(2).unwrap(); + // the import values here are active set being 6 and rewarded set being 10 + // since those are the values we were using in the past + let rewarded_set = RewardedSetParams { + entry_gateways: 1, + exit_gateways: 2, + mixnodes: 3, + standby: 4, + }; + let reward_pool = 250_000_000_000_000u128; let staking_supply = 100_000_000_000_000u128; let epoch_reward_budget = interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval); - let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size); + let stake_saturation_point = + Decimal::from_ratio(staking_supply, rewarded_set.rewarded_set_size()); let rewarding_params = RewardingParams { interval: IntervalRewardParams { @@ -621,8 +653,7 @@ mod tests { active_set_work_factor: Decimal::percent(1000), // value '10' interval_pool_emission, }, - rewarded_set_size, - active_set_size, + rewarded_set, }; let interval = Interval::init_interval( @@ -636,7 +667,7 @@ mod tests { let n0 = simulator .bond( Coin::new(11_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -649,7 +680,7 @@ mod tests { let n1 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -662,7 +693,7 @@ mod tests { let n2 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -675,7 +706,7 @@ mod tests { let n3 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(0).unwrap(), interval_operating_cost: Coin::new(500_000_000, "unym"), }, @@ -688,7 +719,7 @@ mod tests { let n4 = simulator .bond( Coin::new(1000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -701,7 +732,7 @@ mod tests { let n5 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -714,7 +745,7 @@ mod tests { let n6 = simulator .bond( Coin::new(11_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -727,7 +758,7 @@ mod tests { let n7 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -740,7 +771,7 @@ mod tests { let n8 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(0).unwrap(), interval_operating_cost: Coin::new(500_000_000, "unym"), }, @@ -753,7 +784,7 @@ mod tests { let n9 = simulator .bond( Coin::new(1_000_000_000000, "unym"), - MixNodeCostParams { + NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin::new(40_000_000, "unym"), }, @@ -767,17 +798,20 @@ mod tests { let uptime_09 = Percent::from_percentage_value(90).unwrap(); let uptime_0 = Percent::from_percentage_value(0).unwrap(); + let active_work = simulator.legacy_active_work_factor(); + let standby_work = simulator.legacy_standby_work_factor(); + let node_params = [ - (n0, NodeRewardParams::new(uptime_1, true)), - (n1, NodeRewardParams::new(uptime_1, true)), - (n2, NodeRewardParams::new(uptime_1, true)), - (n3, NodeRewardParams::new(uptime_09, true)), - (n4, NodeRewardParams::new(uptime_09, true)), - (n5, NodeRewardParams::new(uptime_0, true)), - (n6, NodeRewardParams::new(uptime_1, false)), - (n7, NodeRewardParams::new(uptime_1, false)), - (n8, NodeRewardParams::new(uptime_09, false)), - (n9, NodeRewardParams::new(uptime_0, false)), + (n0, NodeRewardingParameters::new(uptime_1, active_work)), + (n1, NodeRewardingParameters::new(uptime_1, active_work)), + (n2, NodeRewardingParameters::new(uptime_1, active_work)), + (n3, NodeRewardingParameters::new(uptime_09, active_work)), + (n4, NodeRewardingParameters::new(uptime_09, active_work)), + (n5, NodeRewardingParameters::new(uptime_0, active_work)), + (n6, NodeRewardingParameters::new(uptime_1, standby_work)), + (n7, NodeRewardingParameters::new(uptime_1, standby_work)), + (n8, NodeRewardingParameters::new(uptime_09, standby_work)), + (n9, NodeRewardingParameters::new(uptime_0, standby_work)), ] .into_iter() .collect::>(); diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs index 12a9e967bd..bc6d18a16e 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/rewarding/simulator/simulated_node.rs @@ -1,7 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::{Delegation, EpochId, MixId, MixNodeCostParams, MixNodeRewarding}; +use crate::{Delegation, EpochId, NodeCostParams, NodeId, NodeRewarding}; use cosmwasm_std::{Addr, Coin}; use std::collections::HashMap; @@ -9,21 +9,21 @@ use crate::error::MixnetContractError; use crate::rewarding::helpers::truncate_reward; pub struct SimulatedNode { - pub mix_id: MixId, - pub rewarding_details: MixNodeRewarding, + pub mix_id: NodeId, + pub rewarding_details: NodeRewarding, pub delegations: HashMap, } impl SimulatedNode { pub fn new( - mix_id: MixId, - cost_params: MixNodeCostParams, + mix_id: NodeId, + cost_params: NodeCostParams, initial_pledge: &Coin, current_epoch: EpochId, ) -> Result { Ok(SimulatedNode { mix_id, - rewarding_details: MixNodeRewarding::initialise_new( + rewarding_details: NodeRewarding::initialise_new( cost_params, initial_pledge, current_epoch, @@ -59,8 +59,8 @@ impl SimulatedNode { ) -> Result<(Coin, Coin), MixnetContractError> { let delegator = delegator.into(); let delegation = self.delegations.remove(&delegator).ok_or( - MixnetContractError::NoMixnodeDelegationFound { - mix_id: MixId::MAX, + MixnetContractError::NodeDelegationNotFound { + node_id: NodeId::MAX, address: delegator, proxy: None, }, diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs index 261fa99ff0..00130d52d8 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/signing_types.rs @@ -1,8 +1,8 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::families::FamilyHead; -use crate::{Gateway, IdentityKey, MixNode, MixNodeCostParams}; +use crate::nym_node::NymNode; +use crate::{Gateway, MixNode, NodeCostParams}; use contracts_common::signing::{ ContractMessageContent, LegacyContractMessageContent, MessageType, Nonce, SignableMessage, SigningPurpose, @@ -12,20 +12,20 @@ use serde::Serialize; pub type SignableMixNodeBondingMsg = SignableMessage>; pub type SignableGatewayBondingMsg = SignableMessage>; +pub type SignableNymNodeBondingMsg = SignableMessage>; pub type SignableLegacyMixNodeBondingMsg = SignableMessage>; pub type SignableLegacyGatewayBondingMsg = SignableMessage>; -pub type SignableFamilyJoinPermitMsg = SignableMessage; #[derive(Serialize)] pub struct MixnodeBondingPayload { mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, } impl MixnodeBondingPayload { - pub fn new(mix_node: MixNode, cost_params: MixNodeCostParams) -> Self { + pub fn new(mix_node: MixNode, cost_params: NodeCostParams) -> Self { Self { mix_node, cost_params, @@ -44,7 +44,7 @@ pub fn construct_mixnode_bonding_sign_payload( sender: Addr, pledge: Coin, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, ) -> SignableMixNodeBondingMsg { let payload = MixnodeBondingPayload::new(mix_node, cost_params); let content = ContractMessageContent::new(sender, vec![pledge], payload); @@ -57,7 +57,7 @@ pub fn construct_legacy_mixnode_bonding_sign_payload( sender: Addr, pledge: Coin, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, ) -> SignableLegacyMixNodeBondingMsg { let payload = MixnodeBondingPayload::new(mix_node, cost_params); let content: LegacyContractMessageContent<_> = @@ -109,39 +109,48 @@ pub fn construct_legacy_gateway_bonding_sign_payload( } #[derive(Serialize)] -pub struct FamilyJoinPermit { - // the granter of this permit - family_head: FamilyHead, - // the actual member we want to permit to join - member_node: IdentityKey, +pub struct NymNodeBondingPayload { + nym_node: NymNode, + cost_params: NodeCostParams, } -impl FamilyJoinPermit { - pub fn new(family_head: FamilyHead, member_node: IdentityKey) -> Self { - Self { - family_head, - member_node, +impl NymNodeBondingPayload { + pub fn new(nym_node: NymNode, cost_params: NodeCostParams) -> Self { + NymNodeBondingPayload { + nym_node, + cost_params, } } } -impl SigningPurpose for FamilyJoinPermit { +impl SigningPurpose for NymNodeBondingPayload { fn message_type() -> MessageType { - MessageType::new("family-join-permit") + MessageType::new("nym-node-bonding") } } -pub fn construct_family_join_permit( +pub fn construct_nym_node_bonding_sign_payload( nonce: Nonce, - family_head: FamilyHead, - member_node: IdentityKey, -) -> SignableFamilyJoinPermitMsg { - let payload = FamilyJoinPermit::new(family_head, member_node); - - // note: we're NOT wrapping it in `ContractMessageContent` because the family head is not going to be the one - // sending the message to the contract - SignableMessage::new(nonce, payload) + sender: Addr, + pledge: Coin, + nym_node: NymNode, + cost_params: NodeCostParams, +) -> SignableNymNodeBondingMsg { + let payload = NymNodeBondingPayload::new(nym_node, cost_params); + let content = ContractMessageContent::new(sender, vec![pledge], payload); + + SignableMessage::new(nonce, content) } -// TODO: depending on our threat model, we should perhaps extend it to include all _on_behalf methods -// (update: but we trust our vesting contract since its compromise would be even more devastating so there's no need) +pub fn construct_generic_node_bonding_payload( + nonce: Nonce, + sender: Addr, + pledge: Coin, + payload: T, +) -> SignableMessage> +where + T: SigningPurpose, +{ + let content = ContractMessageContent::new(sender, vec![pledge], payload); + SignableMessage::new(nonce, content) +} diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs index f95c15eb34..c2653053da 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs @@ -1,22 +1,75 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::MixnetContractError; -use crate::Layer; +use crate::nym_node::Role; use contracts_common::Percent; use cosmwasm_schema::cw_serde; use cosmwasm_std::Coin; use cosmwasm_std::{Addr, Uint128}; use std::fmt::{Display, Formatter}; -use std::ops::Index; // type aliases for better reasoning about available data pub type SphinxKey = String; pub type SphinxKeyRef<'a> = &'a str; -pub type MixId = u32; +pub type NodeId = u32; pub type BlockHeight = u64; +#[cw_serde] +pub struct RoleAssignment { + pub role: Role, + pub nodes: Vec, +} + +impl RoleAssignment { + pub fn new(role: Role, nodes: Vec) -> RoleAssignment { + RoleAssignment { role, nodes } + } + + pub fn is_final_assignment(&self) -> bool { + self.role.is_standby() + } +} + +#[cw_serde] +#[derive(Default)] +pub struct RewardedSet { + pub entry_gateways: Vec, + + pub exit_gateways: Vec, + + pub layer1: Vec, + + pub layer2: Vec, + + pub layer3: Vec, + + pub standby: Vec, +} + +impl RewardedSet { + pub fn is_empty(&self) -> bool { + self.entry_gateways.is_empty() + && self.exit_gateways.is_empty() + && self.layer1.is_empty() + && self.layer2.is_empty() + && self.layer3.is_empty() + && self.standby.is_empty() + } + + pub fn active_set_size(&self) -> usize { + self.entry_gateways.len() + + self.exit_gateways.len() + + self.layer1.len() + + self.layer2.len() + + self.layer3.len() + } + + pub fn rewarded_set_size(&self) -> usize { + self.active_set_size() + self.standby.len() + } +} + #[cw_serde] pub struct RangedValue { pub minimum: T, @@ -61,6 +114,10 @@ impl RangedValue where T: Copy + PartialOrd + PartialEq, { + pub fn new(minimum: T, maximum: T) -> Self { + RangedValue { minimum, maximum } + } + pub fn normalise(&self, value: T) -> T { if value < self.minimum { self.minimum @@ -76,113 +133,6 @@ where } } -/// Specifies layer assignment for the given mixnode. -#[cw_serde] -pub struct LayerAssignment { - /// The id of the mixnode. - mix_id: MixId, - - /// The layer to which it's going to be assigned - layer: Layer, -} - -impl LayerAssignment { - pub fn new(mix_id: MixId, layer: Layer) -> Self { - LayerAssignment { mix_id, layer } - } - - pub fn mix_id(&self) -> MixId { - self.mix_id - } - - pub fn layer(&self) -> Layer { - self.layer - } -} - -/// The current layer distribution of the mix network. -#[cw_serde] -#[derive(Copy, Default)] -pub struct LayerDistribution { - /// Number of nodes on the first layer. - pub layer1: u64, - - /// Number of nodes on the second layer. - pub layer2: u64, - - /// Number of nodes on the third layer. - pub layer3: u64, -} - -impl LayerDistribution { - pub fn choose_with_fewest(&self) -> Layer { - let layers = [ - (Layer::One, self.layer1), - (Layer::Two, self.layer2), - (Layer::Three, self.layer3), - ]; - - // we explicitly put 3 elements into the iterator, so the iterator is DEFINITELY - // not empty and thus the unwrap cannot fail - #[allow(clippy::unwrap_used)] - layers.iter().min_by_key(|x| x.1).unwrap().0 - } - - pub fn increment_layer_count(&mut self, layer: Layer) { - match layer { - Layer::One => self.layer1 += 1, - Layer::Two => self.layer2 += 1, - Layer::Three => self.layer3 += 1, - } - } - - pub fn decrement_layer_count(&mut self, layer: Layer) -> Result<(), MixnetContractError> { - match layer { - Layer::One => { - self.layer1 = - self.layer1 - .checked_sub(1) - .ok_or(MixnetContractError::OverflowSubtraction { - minuend: self.layer1, - subtrahend: 1, - })? - } - Layer::Two => { - self.layer2 = - self.layer2 - .checked_sub(1) - .ok_or(MixnetContractError::OverflowSubtraction { - minuend: self.layer2, - subtrahend: 1, - })? - } - Layer::Three => { - self.layer3 = - self.layer3 - .checked_sub(1) - .ok_or(MixnetContractError::OverflowSubtraction { - minuend: self.layer3, - subtrahend: 1, - })? - } - } - - Ok(()) - } -} - -impl Index for LayerDistribution { - type Output = u64; - - fn index(&self, index: Layer) -> &Self::Output { - match index { - Layer::One => &self.layer1, - Layer::Two => &self.layer2, - Layer::Three => &self.layer3, - } - } -} - /// The current state of the mixnet contract. #[cw_serde] pub struct ContractState { @@ -212,13 +162,10 @@ pub struct ContractState { #[cw_serde] pub struct ContractStateParams { /// Minimum amount a delegator must stake in orders for his delegation to get accepted. - pub minimum_mixnode_delegation: Option, - - /// Minimum amount a mixnode must pledge to get into the system. - pub minimum_mixnode_pledge: Coin, + pub minimum_delegation: Option, - /// Minimum amount a gateway must pledge to get into the system. - pub minimum_gateway_pledge: Coin, + /// Minimum amount a node must pledge to get into the system. + pub minimum_pledge: Coin, /// Defines the allowed profit margin range of operators. /// default: 0% - 100% diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/error.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/error.rs index 65825c6ad9..da31bdf30a 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/error.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/error.rs @@ -3,7 +3,7 @@ use crate::account::VestingAccountStorageKey; use cosmwasm_std::{Addr, Coin, OverflowError, StdError, Uint128}; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use thiserror::Error; #[derive(Error, Debug, PartialEq)] @@ -50,7 +50,7 @@ pub enum VestingContractError { MultipleDenoms, #[error("VESTING ({}): No delegations found for account {0}, mix_identity {1}", line!())] - NoSuchDelegation(Addr, MixId), + NoSuchDelegation(Addr, NodeId), #[error("VESTING ({}): Only mixnet contract can perform this operation, got {0}", line!())] NotMixnetContract(Addr), @@ -95,7 +95,7 @@ pub enum VestingContractError { TooManyDelegations { address: Addr, acc_id: VestingAccountStorageKey, - mix_id: MixId, + mix_id: NodeId, num: u32, cap: u32, }, diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs index 31fab7a7ef..741fde4f7b 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/lib.rs @@ -6,7 +6,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::{Addr, Coin}; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; pub mod account; pub mod error; @@ -64,7 +64,7 @@ pub struct DelegationTimesResponse { pub account_id: u32, /// Id of the mixnode towards which the delegation was made - pub mix_id: MixId, + pub mix_id: NodeId, /// All timestamps where a delegation was made pub delegation_timestamps: Vec, @@ -77,7 +77,7 @@ pub struct AllDelegationsResponse { pub delegations: Vec, /// Field indicating paging information for the following queries if the caller wishes to get further entries. - pub start_next_after: Option<(u32, MixId, u64)>, + pub start_next_after: Option<(u32, NodeId, u64)>, } /// Basic information regarding particular vesting account alongside the amount of vesting coins. diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs index eefe07e9f1..303fe1e89f 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/messages.rs @@ -5,11 +5,10 @@ use crate::{PledgeCap, VestingSpecification}; use contracts_common::signing::MessageSignature; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, Timestamp}; -use mixnet_contract_common::families::FamilyHead; use mixnet_contract_common::{ gateway::GatewayConfigUpdate, - mixnode::{MixNodeConfigUpdate, MixNodeCostParams}, - Gateway, IdentityKey, MixId, MixNode, + mixnode::{MixNodeConfigUpdate, NodeCostParams}, + Gateway, MixNode, NodeId, }; #[cfg(feature = "schema")] @@ -36,32 +35,16 @@ pub struct MigrateMsg {} #[cw_serde] pub enum ExecuteMsg { - // Families - /// Only owner of the node can crate the family with node as head - CreateFamily { - label: String, - }, - /// Family head needs to sign the joining node IdentityKey, the Node provides its signature signaling consent to join the family - JoinFamily { - join_permit: MessageSignature, - family_head: FamilyHead, - }, - LeaveFamily { - family_head: FamilyHead, - }, - KickFamilyMember { - member: IdentityKey, - }, TrackReward { amount: Coin, address: String, }, ClaimOperatorReward {}, ClaimDelegatorReward { - mix_id: MixId, + mix_id: NodeId, }, UpdateMixnodeCostParams { - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, }, UpdateMixnodeConfig { new_config: MixNodeConfigUpdate, @@ -70,12 +53,12 @@ pub enum ExecuteMsg { address: String, }, DelegateToMixnode { - mix_id: MixId, + mix_id: NodeId, amount: Coin, on_behalf_of: Option, }, UndelegateFromMixnode { - mix_id: MixId, + mix_id: NodeId, on_behalf_of: Option, }, CreateAccount { @@ -89,12 +72,12 @@ pub enum ExecuteMsg { }, TrackUndelegation { owner: String, - mix_id: MixId, + mix_id: NodeId, amount: Coin, }, BondMixnode { mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, amount: Coin, }, @@ -142,17 +125,13 @@ pub enum ExecuteMsg { // no need to track migrated gateways as there are no vesting gateways on mainnet TrackMigratedDelegation { owner: String, - mix_id: MixId, + mix_id: NodeId, }, } impl ExecuteMsg { pub fn name(&self) -> &str { match self { - ExecuteMsg::CreateFamily { .. } => "VestingExecuteMsg::CreateFamily", - ExecuteMsg::JoinFamily { .. } => "VestingExecuteMsg::JoinFamily", - ExecuteMsg::LeaveFamily { .. } => "VestingExecuteMsg::LeaveFamily", - ExecuteMsg::KickFamilyMember { .. } => "VestingExecuteMsg::KickFamilyMember", ExecuteMsg::TrackReward { .. } => "VestingExecuteMsg::TrackReward", ExecuteMsg::ClaimOperatorReward { .. } => "VestingExecuteMsg::ClaimOperatorReward", ExecuteMsg::ClaimDelegatorReward { .. } => "VestingExecuteMsg::ClaimDelegatorReward", @@ -374,7 +353,7 @@ pub enum QueryMsg { address: String, /// Id of the mixnode towards which the delegation has been made. - mix_id: MixId, + mix_id: NodeId, /// Block timestamp of the delegation. block_timestamp_secs: u64, @@ -387,7 +366,7 @@ pub enum QueryMsg { address: String, /// Id of the mixnode towards which the delegations have been made. - mix_id: MixId, + mix_id: NodeId, }, /// Returns timestamps of delegations made towards particular mixnode by the provided vesting account address. @@ -397,14 +376,14 @@ pub enum QueryMsg { address: String, /// Id of the mixnode towards which the delegations have been made. - mix_id: MixId, + mix_id: NodeId, }, /// Returns all active delegations made with vesting tokens stored in this contract. #[cfg_attr(feature = "schema", returns(AllDelegationsResponse))] GetAllDelegations { /// Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response. - start_after: Option<(u32, MixId, u64)>, + start_after: Option<(u32, NodeId, u64)>, /// Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default. limit: Option, diff --git a/common/cosmwasm-smart-contracts/vesting-contract/src/types.rs b/common/cosmwasm-smart-contracts/vesting-contract/src/types.rs index e4c1644a94..0b15346804 100644 --- a/common/cosmwasm-smart-contracts/vesting-contract/src/types.rs +++ b/common/cosmwasm-smart-contracts/vesting-contract/src/types.rs @@ -4,13 +4,13 @@ use contracts_common::Percent; use cosmwasm_schema::cw_serde; use cosmwasm_std::{Coin, Timestamp, Uint128}; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use std::str::FromStr; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Period.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Period.ts") )] #[cw_serde] /// The vesting period. @@ -155,7 +155,7 @@ pub struct VestingDelegation { pub account_id: u32, /// The id of the mixnode towards which the delegation has been made. - pub mix_id: MixId, + pub mix_id: NodeId, /// The block timestamp when the delegation has been made. pub block_timestamp: u64, @@ -165,7 +165,7 @@ pub struct VestingDelegation { } impl VestingDelegation { - pub fn storage_key(&self) -> (u32, MixId, u64) { + pub fn storage_key(&self) -> (u32, NodeId, u64) { (self.account_id, self.mix_id, self.block_timestamp) } } diff --git a/common/credential-storage/.sqlx/query-00d857b624e7edab1198114b17cbad1e16988a3f9989d135840500e1143ce5e5.json b/common/credential-storage/.sqlx/query-00d857b624e7edab1198114b17cbad1e16988a3f9989d135840500e1143ce5e5.json new file mode 100644 index 0000000000..9c0c64d664 --- /dev/null +++ b/common/credential-storage/.sqlx/query-00d857b624e7edab1198114b17cbad1e16988a3f9989d135840500e1143ce5e5.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM expiration_date_signatures\n WHERE expiration_date = ?\n ", + "describe": { + "columns": [ + { + "name": "epoch_id: u32", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "serialised_signatures", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "serialization_revision: u8", + "ordinal": 2, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "00d857b624e7edab1198114b17cbad1e16988a3f9989d135840500e1143ce5e5" +} diff --git a/common/credential-storage/.sqlx/query-0112296b190328a3856d1adf51aafa2525da6c0b871633aad80ad555db9cf47c.json b/common/credential-storage/.sqlx/query-0112296b190328a3856d1adf51aafa2525da6c0b871633aad80ad555db9cf47c.json new file mode 100644 index 0000000000..c8fbd21911 --- /dev/null +++ b/common/credential-storage/.sqlx/query-0112296b190328a3856d1adf51aafa2525da6c0b871633aad80ad555db9cf47c.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_key, serialization_revision as \"serialization_revision: u8\"\n FROM master_verification_key WHERE epoch_id = ?\n ", + "describe": { + "columns": [ + { + "name": "epoch_id: u32", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "serialised_key", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "serialization_revision: u8", + "ordinal": 2, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "0112296b190328a3856d1adf51aafa2525da6c0b871633aad80ad555db9cf47c" +} diff --git a/common/credential-storage/.sqlx/query-16d10f0ac0ed9ce4239937f46df3797a6a9ee7db2aab9f1b5e55f7c13c53bcc1.json b/common/credential-storage/.sqlx/query-16d10f0ac0ed9ce4239937f46df3797a6a9ee7db2aab9f1b5e55f7c13c53bcc1.json new file mode 100644 index 0000000000..e4f26c5053 --- /dev/null +++ b/common/credential-storage/.sqlx/query-16d10f0ac0ed9ce4239937f46df3797a6a9ee7db2aab9f1b5e55f7c13c53bcc1.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO expiration_date_signatures(expiration_date, epoch_id, serialised_signatures, serialization_revision)\n VALUES (?, ?, ?, ?);\n UPDATE expiration_date_signatures\n SET\n serialised_signatures = ?,\n serialization_revision = ?\n WHERE expiration_date = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "16d10f0ac0ed9ce4239937f46df3797a6a9ee7db2aab9f1b5e55f7c13c53bcc1" +} diff --git a/common/credential-storage/.sqlx/query-284b3ceae42f9320c30323dde47765854899103fd3c0fa670eb6809492270e02.json b/common/credential-storage/.sqlx/query-284b3ceae42f9320c30323dde47765854899103fd3c0fa670eb6809492270e02.json new file mode 100644 index 0000000000..6ee25bbc1a --- /dev/null +++ b/common/credential-storage/.sqlx/query-284b3ceae42f9320c30323dde47765854899103fd3c0fa670eb6809492270e02.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO ecash_ticketbook\n (serialization_revision, ticketbook_data, expiration_date, ticketbook_type, epoch_id, total_tickets, used_tickets)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "284b3ceae42f9320c30323dde47765854899103fd3c0fa670eb6809492270e02" +} diff --git a/common/credential-storage/.sqlx/query-37f82c9ec26b53d01601a2d6df82038a77ec37cca9f9aef18008dcd03030c2c4.json b/common/credential-storage/.sqlx/query-37f82c9ec26b53d01601a2d6df82038a77ec37cca9f9aef18008dcd03030c2c4.json new file mode 100644 index 0000000000..ea79b11bbd --- /dev/null +++ b/common/credential-storage/.sqlx/query-37f82c9ec26b53d01601a2d6df82038a77ec37cca9f9aef18008dcd03030c2c4.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM ecash_ticketbook WHERE expiration_date <= ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "37f82c9ec26b53d01601a2d6df82038a77ec37cca9f9aef18008dcd03030c2c4" +} diff --git a/common/credential-storage/.sqlx/query-5c5d4bfabf18bc6fa56e76a9b98e38b7f6ceb8e9191a7b9201922efcf6b07966.json b/common/credential-storage/.sqlx/query-5c5d4bfabf18bc6fa56e76a9b98e38b7f6ceb8e9191a7b9201922efcf6b07966.json new file mode 100644 index 0000000000..ea667b0bc6 --- /dev/null +++ b/common/credential-storage/.sqlx/query-5c5d4bfabf18bc6fa56e76a9b98e38b7f6ceb8e9191a7b9201922efcf6b07966.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM pending_issuance WHERE deposit_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "5c5d4bfabf18bc6fa56e76a9b98e38b7f6ceb8e9191a7b9201922efcf6b07966" +} diff --git a/common/credential-storage/.sqlx/query-81a12a8a419c88b1c28a5533fde4d63462e9ea0049e2edafea1dc3f8476b33e4.json b/common/credential-storage/.sqlx/query-81a12a8a419c88b1c28a5533fde4d63462e9ea0049e2edafea1dc3f8476b33e4.json new file mode 100644 index 0000000000..ae81f52d0e --- /dev/null +++ b/common/credential-storage/.sqlx/query-81a12a8a419c88b1c28a5533fde4d63462e9ea0049e2edafea1dc3f8476b33e4.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO pending_issuance\n (deposit_id, serialization_revision, pending_ticketbook_data, expiration_date)\n VALUES (?, ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "81a12a8a419c88b1c28a5533fde4d63462e9ea0049e2edafea1dc3f8476b33e4" +} diff --git a/common/credential-storage/.sqlx/query-84cad8b1078a4000830835e6349de3eb76fed954b7336530401db72cd008aff3.json b/common/credential-storage/.sqlx/query-84cad8b1078a4000830835e6349de3eb76fed954b7336530401db72cd008aff3.json new file mode 100644 index 0000000000..051c18bba2 --- /dev/null +++ b/common/credential-storage/.sqlx/query-84cad8b1078a4000830835e6349de3eb76fed954b7336530401db72cd008aff3.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE ecash_ticketbook SET used_tickets = used_tickets + ? WHERE id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "84cad8b1078a4000830835e6349de3eb76fed954b7336530401db72cd008aff3" +} diff --git a/common/credential-storage/.sqlx/query-a5b18e66d77ff802e274623605e15dcfcffb502ba8398caefd56c481f44eb84e.json b/common/credential-storage/.sqlx/query-a5b18e66d77ff802e274623605e15dcfcffb502ba8398caefd56c481f44eb84e.json new file mode 100644 index 0000000000..5fd4e4f2e6 --- /dev/null +++ b/common/credential-storage/.sqlx/query-a5b18e66d77ff802e274623605e15dcfcffb502ba8398caefd56c481f44eb84e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO master_verification_key(epoch_id, serialised_key, serialization_revision) VALUES (?, ?, ?);\n UPDATE master_verification_key\n SET\n serialised_key = ?,\n serialization_revision = ?\n WHERE epoch_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "a5b18e66d77ff802e274623605e15dcfcffb502ba8398caefd56c481f44eb84e" +} diff --git a/common/credential-storage/.sqlx/query-ba96344db31b0f2155e2af53eaaeafc9b5f64061b6c9a829e2912945b6cffc82.json b/common/credential-storage/.sqlx/query-ba96344db31b0f2155e2af53eaaeafc9b5f64061b6c9a829e2912945b6cffc82.json new file mode 100644 index 0000000000..e2f05399ee --- /dev/null +++ b/common/credential-storage/.sqlx/query-ba96344db31b0f2155e2af53eaaeafc9b5f64061b6c9a829e2912945b6cffc82.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures, serialization_revision as \"serialization_revision: u8\"\n FROM coin_indices_signatures WHERE epoch_id = ?\n ", + "describe": { + "columns": [ + { + "name": "epoch_id: u32", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "serialised_signatures", + "ordinal": 1, + "type_info": "Blob" + }, + { + "name": "serialization_revision: u8", + "ordinal": 2, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "ba96344db31b0f2155e2af53eaaeafc9b5f64061b6c9a829e2912945b6cffc82" +} diff --git a/common/credential-storage/.sqlx/query-bc823c54143e2dc590b91347cd089dde284b38a3a4960afed758206d03ca1cf4.json b/common/credential-storage/.sqlx/query-bc823c54143e2dc590b91347cd089dde284b38a3a4960afed758206d03ca1cf4.json new file mode 100644 index 0000000000..ec7111bc5a --- /dev/null +++ b/common/credential-storage/.sqlx/query-bc823c54143e2dc590b91347cd089dde284b38a3a4960afed758206d03ca1cf4.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE ecash_ticketbook\n SET used_tickets = used_tickets - ?\n WHERE id = ?\n AND used_tickets = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "bc823c54143e2dc590b91347cd089dde284b38a3a4960afed758206d03ca1cf4" +} diff --git a/common/credential-storage/.sqlx/query-bd1973696121b6128bd75ae80fab253c071e04eb853d4b0f3b21782ea57c2f68.json b/common/credential-storage/.sqlx/query-bd1973696121b6128bd75ae80fab253c071e04eb853d4b0f3b21782ea57c2f68.json new file mode 100644 index 0000000000..a2077f76ae --- /dev/null +++ b/common/credential-storage/.sqlx/query-bd1973696121b6128bd75ae80fab253c071e04eb853d4b0f3b21782ea57c2f68.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO coin_indices_signatures(epoch_id, serialised_signatures, serialization_revision) VALUES (?, ?, ?);\n UPDATE coin_indices_signatures\n SET\n serialised_signatures = ?,\n serialization_revision = ?\n WHERE epoch_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "bd1973696121b6128bd75ae80fab253c071e04eb853d4b0f3b21782ea57c2f68" +} diff --git a/common/credential-storage/src/backends/memory.rs b/common/credential-storage/src/backends/memory.rs index d107677897..91d197d632 100644 --- a/common/credential-storage/src/backends/memory.rs +++ b/common/credential-storage/src/backends/memory.rs @@ -73,6 +73,7 @@ impl MemoryEcachTicketbookManager { pub async fn get_next_unspent_ticketbook_and_update( &self, + ticketbook_type: String, tickets: u32, ) -> Option { let mut guard = self.inner.write().await; @@ -81,6 +82,7 @@ impl MemoryEcachTicketbookManager { if !t.ticketbook.expired() && t.ticketbook.spent_tickets() + tickets as u64 <= t.ticketbook.params_total_tickets() + && t.ticketbook.ticketbook_type().to_string() == ticketbook_type { t.ticketbook .update_spent_tickets(t.ticketbook.spent_tickets() + tickets as u64); diff --git a/common/credential-storage/src/backends/sqlite.rs b/common/credential-storage/src/backends/sqlite.rs index 8c5b20c7d8..9267bbddb3 100644 --- a/common/credential-storage/src/backends/sqlite.rs +++ b/common/credential-storage/src/backends/sqlite.rs @@ -284,6 +284,7 @@ impl SqliteEcashTicketbookManager { pub(crate) async fn get_next_unspent_ticketbook<'a, E>( executor: E, + ticketbook_type: String, deadline: Date, tickets: u32, ) -> Result, sqlx::Error> @@ -296,12 +297,14 @@ where FROM ecash_ticketbook WHERE used_tickets + ? <= total_tickets AND expiration_date >= ? + AND ticketbook_type = ? ORDER BY expiration_date ASC LIMIT 1 "#, ) .bind(tickets) .bind(deadline) + .bind(ticketbook_type) .fetch_optional(executor) .await } diff --git a/common/credential-storage/src/ephemeral_storage.rs b/common/credential-storage/src/ephemeral_storage.rs index 4b3ef93660..91436d4d8c 100644 --- a/common/credential-storage/src/ephemeral_storage.rs +++ b/common/credential-storage/src/ephemeral_storage.rs @@ -85,17 +85,18 @@ impl Storage for EphemeralStorage { Ok(()) } - /// Tries to retrieve one of the stored ticketbook, + /// Tries to retrieve one of the stored ticketbook for the specified type, /// that has not yet expired and has required number of unspent tickets. /// it immediately updated the on-disk number of used tickets so that another task /// could obtain their own tickets at the same time async fn get_next_unspent_usable_ticketbook( &self, + ticketbook_type: String, tickets: u32, ) -> Result, Self::StorageError> { Ok(self .storage_manager - .get_next_unspent_ticketbook_and_update(tickets) + .get_next_unspent_ticketbook_and_update(ticketbook_type, tickets) .await) } diff --git a/common/credential-storage/src/persistent_storage/mod.rs b/common/credential-storage/src/persistent_storage/mod.rs index c5daceabd5..80e746b482 100644 --- a/common/credential-storage/src/persistent_storage/mod.rs +++ b/common/credential-storage/src/persistent_storage/mod.rs @@ -3,27 +3,35 @@ mod legacy_helpers; -use crate::backends::sqlite::{ - get_next_unspent_ticketbook, increase_used_ticketbook_tickets, SqliteEcashTicketbookManager, +use crate::{ + backends::sqlite::{ + get_next_unspent_ticketbook, increase_used_ticketbook_tickets, SqliteEcashTicketbookManager, + }, + error::StorageError, + models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}, + persistent_storage::legacy_helpers::{ + deserialise_v1_coin_index_signatures, deserialise_v1_expiration_date_signatures, + deserialise_v1_master_verification_key, + }, + storage::Storage, }; -use crate::error::StorageError; -use crate::models::{BasicTicketbookInformation, RetrievedPendingTicketbook, RetrievedTicketbook}; -use crate::persistent_storage::legacy_helpers::{ - deserialise_v1_coin_index_signatures, deserialise_v1_expiration_date_signatures, - deserialise_v1_master_verification_key, -}; -use crate::storage::Storage; use async_trait::async_trait; use log::{debug, error}; -use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; -use nym_compact_ecash::scheme::expiration_date_signatures::AnnotatedExpirationDateSignature; -use nym_compact_ecash::VerificationKeyAuth; -use nym_credentials::ecash::bandwidth::serialiser::keys::EpochVerificationKey; -use nym_credentials::ecash::bandwidth::serialiser::signatures::{ - AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, +use nym_compact_ecash::{ + scheme::{ + coin_indices_signatures::AnnotatedCoinIndexSignature, + expiration_date_signatures::AnnotatedExpirationDateSignature, + }, + VerificationKeyAuth, +}; +use nym_credentials::{ + ecash::bandwidth::serialiser::{ + keys::EpochVerificationKey, + signatures::{AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures}, + VersionedSerialise, + }, + IssuanceTicketBook, IssuedTicketBook, }; -use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; -use nym_credentials::{IssuanceTicketBook, IssuedTicketBook}; use nym_ecash_time::{ecash_today, Date, EcashTime}; use sqlx::ConnectOptions; use std::path::Path; @@ -47,11 +55,10 @@ impl PersistentStorage { database_path.as_ref().as_os_str() ); - let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + let opts = sqlx::sqlite::SqliteConnectOptions::new() .filename(database_path) - .create_if_missing(true); - - opts.disable_statement_logging(); + .create_if_missing(true) + .disable_statement_logging(); let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { Ok(db) => db, @@ -171,13 +178,16 @@ impl Storage for PersistentStorage { /// could obtain their own tickets at the same time async fn get_next_unspent_usable_ticketbook( &self, + ticketbook_type: String, tickets: u32, ) -> Result, Self::StorageError> { let deadline = ecash_today().ecash_date(); let mut tx = self.storage_manager.begin_storage_tx().await?; // we don't want ticketbooks with expiration in the past - let Some(raw) = get_next_unspent_ticketbook(&mut tx, deadline, tickets).await? else { + let Some(raw) = + get_next_unspent_ticketbook(&mut *tx, ticketbook_type, deadline, tickets).await? + else { // make sure to finish our tx tx.commit().await?; return Ok(None); @@ -191,7 +201,7 @@ impl Storage for PersistentStorage { )) })?; - increase_used_ticketbook_tickets(&mut tx, raw.id, tickets).await?; + increase_used_ticketbook_tickets(&mut *tx, raw.id, tickets).await?; tx.commit().await?; // set the number of spent tickets on the crypto object diff --git a/common/credential-storage/src/storage.rs b/common/credential-storage/src/storage.rs index 7033cd6e1f..19ddc44e86 100644 --- a/common/credential-storage/src/storage.rs +++ b/common/credential-storage/src/storage.rs @@ -45,12 +45,13 @@ pub trait Storage: Send + Sync { async fn remove_pending_ticketbook(&self, pending_id: i64) -> Result<(), Self::StorageError>; - /// Tries to retrieve one of the stored ticketbook, + /// Tries to retrieve one of the stored ticketbook for the specified type, /// that has not yet expired and has required number of unspent tickets. /// it immediately updated the on-disk number of used tickets so that another task /// could obtain their own tickets at the same time async fn get_next_unspent_usable_ticketbook( &self, + ticketbook_type: String, tickets: u32, ) -> Result, Self::StorageError>; diff --git a/common/credential-verification/src/lib.rs b/common/credential-verification/src/lib.rs index 0ffe090a06..066953fc55 100644 --- a/common/credential-verification/src/lib.rs +++ b/common/credential-verification/src/lib.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use time::{Date, OffsetDateTime}; use tracing::*; -use nym_credentials::ecash::utils::{ecash_today, EcashTime}; +use nym_credentials::ecash::utils::{cred_exp_date, ecash_today, EcashTime}; use nym_credentials_interface::{Bandwidth, ClientTicket, TicketType}; use nym_gateway_requests::models::CredentialSpendingRequest; use nym_gateway_storage::Storage; @@ -131,7 +131,7 @@ impl CredentialVerifier { let bandwidth = Bandwidth::ticket_amount(credential_type.into()); self.bandwidth_storage_manager - .increase_bandwidth(bandwidth, spend_date) + .increase_bandwidth(bandwidth, cred_exp_date()) .await?; Ok(self diff --git a/common/crypto/src/asymmetric/encryption/mod.rs b/common/crypto/src/asymmetric/encryption/mod.rs index 1ea4b9d95a..7d7b988fc8 100644 --- a/common/crypto/src/asymmetric/encryption/mod.rs +++ b/common/crypto/src/asymmetric/encryption/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; -use std::fmt::{self, Display, Formatter}; +use std::fmt::{self, Debug, Display, Formatter}; use std::str::FromStr; use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -12,6 +12,9 @@ use rand::{CryptoRng, RngCore}; #[cfg(feature = "serde")] use serde::{Deserialize, Deserializer, Serialize, Serializer}; +#[cfg(feature = "serde")] +pub mod serde_helpers; + /// Size of a X25519 private key pub const PRIVATE_KEY_SIZE: usize = 32; @@ -109,12 +112,18 @@ impl PemStorableKeyPair for KeyPair { } } -#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug)] +#[derive(PartialEq, Eq, Hash, Copy, Clone)] pub struct PublicKey(x25519_dalek::PublicKey); impl Display for PublicKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.to_base58_string()) + Display::fmt(&self.to_base58_string(), f) + } +} + +impl Debug for PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Debug::fmt(&self.to_base58_string(), f) } } diff --git a/common/crypto/src/asymmetric/encryption/serde_helpers.rs b/common/crypto/src/asymmetric/encryption/serde_helpers.rs new file mode 100644 index 0000000000..02a5282cdd --- /dev/null +++ b/common/crypto/src/asymmetric/encryption/serde_helpers.rs @@ -0,0 +1,46 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::PublicKey; + +pub mod bs58_x25519_pubkey { + use super::*; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(key: &PublicKey, serializer: S) -> Result { + serializer.serialize_str(&key.to_base58_string()) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result { + let s = String::deserialize(deserializer)?; + PublicKey::from_base58_string(s).map_err(serde::de::Error::custom) + } +} + +pub mod option_bs58_x25519_pubkey { + use super::*; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn serialize( + key: &Option, + serializer: S, + ) -> Result { + key.map(|key| key.to_base58_string()).serialize(serializer) + } + + pub fn deserialize<'de, D: Deserializer<'de>>( + deserializer: D, + ) -> Result, D::Error> { + match Option::::deserialize(deserializer)? { + None => Ok(None), + Some(s) => { + if s.is_empty() { + Ok(None) + } else { + Some(PublicKey::from_base58_string(&s).map_err(serde::de::Error::custom)) + .transpose() + } + } + } + } +} diff --git a/common/crypto/src/asymmetric/identity/mod.rs b/common/crypto/src/asymmetric/identity/mod.rs index e2d3df624d..4b51aa2f64 100644 --- a/common/crypto/src/asymmetric/identity/mod.rs +++ b/common/crypto/src/asymmetric/identity/mod.rs @@ -5,7 +5,7 @@ pub use ed25519_dalek::SignatureError; use ed25519_dalek::{Signer, SigningKey}; pub use ed25519_dalek::{Verifier, PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH}; use nym_pemstore::traits::{PemStorableKey, PemStorableKeyPair}; -use std::fmt::{self, Display, Formatter}; +use std::fmt::{self, Debug, Display, Formatter}; use std::str::FromStr; use thiserror::Error; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -119,12 +119,18 @@ impl PemStorableKeyPair for KeyPair { } /// ed25519 EdDSA Public Key -#[derive(Debug, Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone, Eq, PartialEq)] pub struct PublicKey(ed25519_dalek::VerifyingKey); impl Display for PublicKey { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.to_base58_string()) + Display::fmt(&self.to_base58_string(), f) + } +} + +impl Debug for PublicKey { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Debug::fmt(&self.to_base58_string(), f) } } diff --git a/common/crypto/src/asymmetric/identity/serde_helpers.rs b/common/crypto/src/asymmetric/identity/serde_helpers.rs index 927ca77ec7..9d200df27a 100644 --- a/common/crypto/src/asymmetric/identity/serde_helpers.rs +++ b/common/crypto/src/asymmetric/identity/serde_helpers.rs @@ -3,7 +3,7 @@ use super::PublicKey; -pub mod bs58_pubkey { +pub mod bs58_ed25519_pubkey { use super::*; use serde::{Deserialize, Deserializer, Serializer}; diff --git a/common/gateway-storage/.sqlx/query-03fe56298a6d60cdd5304a2953811a533d59b4f1f0e4efecd32c09256b657e24.json b/common/gateway-storage/.sqlx/query-03fe56298a6d60cdd5304a2953811a533d59b4f1f0e4efecd32c09256b657e24.json new file mode 100644 index 0000000000..32d90ac983 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-03fe56298a6d60cdd5304a2953811a533d59b4f1f0e4efecd32c09256b657e24.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT \n id as \"id!\",\n client_address_bs58 as \"client_address_bs58!\",\n content as \"content!\" \n FROM message_store \n WHERE client_address_bs58 = ? AND id > ?\n ORDER BY id ASC\n LIMIT ?;\n ", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "client_address_bs58!", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "content!", + "ordinal": 2, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "03fe56298a6d60cdd5304a2953811a533d59b4f1f0e4efecd32c09256b657e24" +} diff --git a/common/gateway-storage/.sqlx/query-0aaf87e0bec011910fa23aefa9a487247eb416d4203ab22b787fdcf15e47e586.json b/common/gateway-storage/.sqlx/query-0aaf87e0bec011910fa23aefa9a487247eb416d4203ab22b787fdcf15e47e586.json new file mode 100644 index 0000000000..81ee664e71 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-0aaf87e0bec011910fa23aefa9a487247eb416d4203ab22b787fdcf15e47e586.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM ticket_verification WHERE ticket_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "0aaf87e0bec011910fa23aefa9a487247eb416d4203ab22b787fdcf15e47e586" +} diff --git a/common/gateway-storage/.sqlx/query-10af1fe50f990be860548c7cc7ff051ab7d5cd812627a17758c0c8922593b9fd.json b/common/gateway-storage/.sqlx/query-10af1fe50f990be860548c7cc7ff051ab7d5cd812627a17758c0c8922593b9fd.json new file mode 100644 index 0000000000..644b1e4b06 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-10af1fe50f990be860548c7cc7ff051ab7d5cd812627a17758c0c8922593b9fd.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM message_store WHERE id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "10af1fe50f990be860548c7cc7ff051ab7d5cd812627a17758c0c8922593b9fd" +} diff --git a/common/gateway-storage/.sqlx/query-23775a52dcfb001d53ae045898522a8c19a121f11bae07b80bc88e557eb2275e.json b/common/gateway-storage/.sqlx/query-23775a52dcfb001d53ae045898522a8c19a121f11bae07b80bc88e557eb2275e.json new file mode 100644 index 0000000000..83b8b0f226 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-23775a52dcfb001d53ae045898522a8c19a121f11bae07b80bc88e557eb2275e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n DELETE FROM ticket_data\n WHERE ticket_id IN (\n SELECT ticket_id\n FROM verified_tickets\n WHERE proposal_id = ?\n )\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "23775a52dcfb001d53ae045898522a8c19a121f11bae07b80bc88e557eb2275e" +} diff --git a/common/gateway-storage/.sqlx/query-24ce2c053db635df05d98529023a84bf91a622d4b75ad173976e0234c6380a7d.json b/common/gateway-storage/.sqlx/query-24ce2c053db635df05d98529023a84bf91a622d4b75ad173976e0234c6380a7d.json new file mode 100644 index 0000000000..be56fee0ee --- /dev/null +++ b/common/gateway-storage/.sqlx/query-24ce2c053db635df05d98529023a84bf91a622d4b75ad173976e0234c6380a7d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO wireguard_peer(public_key, preshared_key, protocol_version, endpoint, last_handshake, tx_bytes, rx_bytes, persistent_keepalive_interval, allowed_ips, client_id)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n\n UPDATE wireguard_peer \n SET preshared_key = ?, protocol_version = ?, endpoint = ?, last_handshake = ?, tx_bytes = ?, rx_bytes = ?, persistent_keepalive_interval = ?, allowed_ips = ?, client_id = ?\n WHERE public_key = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 20 + }, + "nullable": [] + }, + "hash": "24ce2c053db635df05d98529023a84bf91a622d4b75ad173976e0234c6380a7d" +} diff --git a/common/gateway-storage/.sqlx/query-2a55441d4e6134975b2c75f0b43491e9cf7bb52f41644d45c92e4b83f60b65cc.json b/common/gateway-storage/.sqlx/query-2a55441d4e6134975b2c75f0b43491e9cf7bb52f41644d45c92e4b83f60b65cc.json new file mode 100644 index 0000000000..875f8594b4 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-2a55441d4e6134975b2c75f0b43491e9cf7bb52f41644d45c92e4b83f60b65cc.json @@ -0,0 +1,74 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT *\n FROM wireguard_peer;\n ", + "describe": { + "columns": [ + { + "name": "public_key", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "preshared_key", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "protocol_version", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "endpoint", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "last_handshake", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "tx_bytes", + "ordinal": 5, + "type_info": "Int64" + }, + { + "name": "rx_bytes", + "ordinal": 6, + "type_info": "Int64" + }, + { + "name": "persistent_keepalive_interval", + "ordinal": 7, + "type_info": "Int64" + }, + { + "name": "allowed_ips", + "ordinal": 8, + "type_info": "Blob" + }, + { + "name": "client_id", + "ordinal": 9, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + true, + true, + true, + true, + false, + false, + true, + false, + true + ] + }, + "hash": "2a55441d4e6134975b2c75f0b43491e9cf7bb52f41644d45c92e4b83f60b65cc" +} diff --git a/common/gateway-storage/.sqlx/query-36b5b40e6466b67f6027257068438e4e45dd6506d806a25ce3a4c69723216fd3.json b/common/gateway-storage/.sqlx/query-36b5b40e6466b67f6027257068438e4e45dd6506d806a25ce3a4c69723216fd3.json new file mode 100644 index 0000000000..e6da2236a2 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-36b5b40e6466b67f6027257068438e4e45dd6506d806a25ce3a4c69723216fd3.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT signer_id FROM ecash_signer WHERE epoch_id = ?", + "describe": { + "columns": [ + { + "name": "signer_id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "36b5b40e6466b67f6027257068438e4e45dd6506d806a25ce3a4c69723216fd3" +} diff --git a/common/gateway-storage/.sqlx/query-3fa406efeffd9952af3cb044b028eb6d1770c694cf84097222c2bcf299fec680.json b/common/gateway-storage/.sqlx/query-3fa406efeffd9952af3cb044b028eb6d1770c694cf84097222c2bcf299fec680.json new file mode 100644 index 0000000000..7d86e55145 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-3fa406efeffd9952af3cb044b028eb6d1770c694cf84097222c2bcf299fec680.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE available_bandwidth\n SET available = available - ?\n WHERE client_id = (SELECT client_id FROM received_ticket WHERE id = ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "3fa406efeffd9952af3cb044b028eb6d1770c694cf84097222c2bcf299fec680" +} diff --git a/common/gateway-storage/.sqlx/query-4642e5d93b4bb38b980d02c4cec2569ee16c78e2a71d9bab04b64314aedf4ac0.json b/common/gateway-storage/.sqlx/query-4642e5d93b4bb38b980d02c4cec2569ee16c78e2a71d9bab04b64314aedf4ac0.json new file mode 100644 index 0000000000..16613c4edc --- /dev/null +++ b/common/gateway-storage/.sqlx/query-4642e5d93b4bb38b980d02c4cec2569ee16c78e2a71d9bab04b64314aedf4ac0.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM shared_keys WHERE client_address_bs58 = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "4642e5d93b4bb38b980d02c4cec2569ee16c78e2a71d9bab04b64314aedf4ac0" +} diff --git a/common/gateway-storage/.sqlx/query-4a142e21e2646dfcda32ed5dcd3f0b915bda6266a55b83c8c667fdbcfc90745b.json b/common/gateway-storage/.sqlx/query-4a142e21e2646dfcda32ed5dcd3f0b915bda6266a55b83c8c667fdbcfc90745b.json new file mode 100644 index 0000000000..0657bec82a --- /dev/null +++ b/common/gateway-storage/.sqlx/query-4a142e21e2646dfcda32ed5dcd3f0b915bda6266a55b83c8c667fdbcfc90745b.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM verified_tickets WHERE proposal_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "4a142e21e2646dfcda32ed5dcd3f0b915bda6266a55b83c8c667fdbcfc90745b" +} diff --git a/common/gateway-storage/.sqlx/query-52406dc7b69af59d206542105d3b87114e3b9eda6dfaad180f2d154bc258d6bf.json b/common/gateway-storage/.sqlx/query-52406dc7b69af59d206542105d3b87114e3b9eda6dfaad180f2d154bc258d6bf.json new file mode 100644 index 0000000000..fd595c8c16 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-52406dc7b69af59d206542105d3b87114e3b9eda6dfaad180f2d154bc258d6bf.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE redemption_proposals SET resolved_at = ?, rejected = ? WHERE proposal_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "52406dc7b69af59d206542105d3b87114e3b9eda6dfaad180f2d154bc258d6bf" +} diff --git a/common/gateway-storage/.sqlx/query-564c7da81081fab34754b76eeeedd48f3bc18842c03ef5a5c331bbee4c41c71c.json b/common/gateway-storage/.sqlx/query-564c7da81081fab34754b76eeeedd48f3bc18842c03ef5a5c331bbee4c41c71c.json new file mode 100644 index 0000000000..308bbb2325 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-564c7da81081fab34754b76eeeedd48f3bc18842c03ef5a5c331bbee4c41c71c.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT * FROM shared_keys WHERE client_address_bs58 = ?", + "describe": { + "columns": [ + { + "name": "client_id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "client_address_bs58", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "derived_aes128_ctr_blake3_hmac_keys_bs58", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "derived_aes256_gcm_siv_key", + "ordinal": 3, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + true, + true + ] + }, + "hash": "564c7da81081fab34754b76eeeedd48f3bc18842c03ef5a5c331bbee4c41c71c" +} diff --git a/common/gateway-storage/.sqlx/query-58ba6fb669847d7b82440178bc3ffd8de03c0308d0fc9cb8615a5d151e785864.json b/common/gateway-storage/.sqlx/query-58ba6fb669847d7b82440178bc3ffd8de03c0308d0fc9cb8615a5d151e785864.json new file mode 100644 index 0000000000..19956cccf7 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-58ba6fb669847d7b82440178bc3ffd8de03c0308d0fc9cb8615a5d151e785864.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO message_store(client_address_bs58, content) VALUES (?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "58ba6fb669847d7b82440178bc3ffd8de03c0308d0fc9cb8615a5d151e785864" +} diff --git a/common/gateway-storage/.sqlx/query-5ee58c3050595614d550558879f54696dfcbddfb1b8575f5cc9690c4c2bffe25.json b/common/gateway-storage/.sqlx/query-5ee58c3050595614d550558879f54696dfcbddfb1b8575f5cc9690c4c2bffe25.json new file mode 100644 index 0000000000..73f500093c --- /dev/null +++ b/common/gateway-storage/.sqlx/query-5ee58c3050595614d550558879f54696dfcbddfb1b8575f5cc9690c4c2bffe25.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT client_id FROM shared_keys WHERE client_address_bs58 = ?", + "describe": { + "columns": [ + { + "name": "client_id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "5ee58c3050595614d550558879f54696dfcbddfb1b8575f5cc9690c4c2bffe25" +} diff --git a/common/gateway-storage/.sqlx/query-61c0a61db0eed4ccbe3c623de183a0df5fbe61dff0048b3ab4b6823a7b248239.json b/common/gateway-storage/.sqlx/query-61c0a61db0eed4ccbe3c623de183a0df5fbe61dff0048b3ab4b6823a7b248239.json new file mode 100644 index 0000000000..94d2364b20 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-61c0a61db0eed4ccbe3c623de183a0df5fbe61dff0048b3ab4b6823a7b248239.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE available_bandwidth\n SET expiration = ?\n WHERE client_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "61c0a61db0eed4ccbe3c623de183a0df5fbe61dff0048b3ab4b6823a7b248239" +} diff --git a/common/gateway-storage/.sqlx/query-6c4a1c08a08714be34fe9bda2d2c9c7fb6a4e25272c1addfad87e26e7b16dfdc.json b/common/gateway-storage/.sqlx/query-6c4a1c08a08714be34fe9bda2d2c9c7fb6a4e25272c1addfad87e26e7b16dfdc.json new file mode 100644 index 0000000000..55f1b43bc4 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-6c4a1c08a08714be34fe9bda2d2c9c7fb6a4e25272c1addfad87e26e7b16dfdc.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO verified_tickets (ticket_id) VALUES (?)", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "6c4a1c08a08714be34fe9bda2d2c9c7fb6a4e25272c1addfad87e26e7b16dfdc" +} diff --git a/common/gateway-storage/.sqlx/query-72b268030ca7409c86806d6b5b253272629a3ebda7b89feacf8ed07ecf2e2c13.json b/common/gateway-storage/.sqlx/query-72b268030ca7409c86806d6b5b253272629a3ebda7b89feacf8ed07ecf2e2c13.json new file mode 100644 index 0000000000..45fa5f4a5d --- /dev/null +++ b/common/gateway-storage/.sqlx/query-72b268030ca7409c86806d6b5b253272629a3ebda7b89feacf8ed07ecf2e2c13.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT id, client_type as \"client_type: ClientType\"\n FROM clients\n WHERE id = ?\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "client_type: ClientType", + "ordinal": 1, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + }, + "hash": "72b268030ca7409c86806d6b5b253272629a3ebda7b89feacf8ed07ecf2e2c13" +} diff --git a/common/gateway-storage/.sqlx/query-73e111225be2ce63d05f1a439a1fc9cc0359a960fe17a135a1d7f8975ebe38ef.json b/common/gateway-storage/.sqlx/query-73e111225be2ce63d05f1a439a1fc9cc0359a960fe17a135a1d7f8975ebe38ef.json new file mode 100644 index 0000000000..ec245e2d27 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-73e111225be2ce63d05f1a439a1fc9cc0359a960fe17a135a1d7f8975ebe38ef.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT t1.ticket_id, t1.data as \"data!\"\n FROM ticket_data as t1\n LEFT JOIN verified_tickets as t2\n ON t1.ticket_id = t2.ticket_id\n WHERE\n t2.ticket_id IS NULL\n AND\n t1.data IS NOT NULL\n ", + "describe": { + "columns": [ + { + "name": "ticket_id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "data!", + "ordinal": 1, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + true + ] + }, + "hash": "73e111225be2ce63d05f1a439a1fc9cc0359a960fe17a135a1d7f8975ebe38ef" +} diff --git a/common/gateway-storage/.sqlx/query-76b91895b5cd8b4815f4e5b183117c16b6a36c180d9081ab2cce9992792e5a6a.json b/common/gateway-storage/.sqlx/query-76b91895b5cd8b4815f4e5b183117c16b6a36c180d9081ab2cce9992792e5a6a.json new file mode 100644 index 0000000000..b02aa2decc --- /dev/null +++ b/common/gateway-storage/.sqlx/query-76b91895b5cd8b4815f4e5b183117c16b6a36c180d9081ab2cce9992792e5a6a.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE available_bandwidth\n SET available = available + ?\n WHERE client_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "76b91895b5cd8b4815f4e5b183117c16b6a36c180d9081ab2cce9992792e5a6a" +} diff --git a/common/gateway-storage/.sqlx/query-7cd38d342eddd578f35116cf024901f94bbf5575595e36345e1d25085a79b039.json b/common/gateway-storage/.sqlx/query-7cd38d342eddd578f35116cf024901f94bbf5575595e36345e1d25085a79b039.json new file mode 100644 index 0000000000..5e5d719b82 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-7cd38d342eddd578f35116cf024901f94bbf5575595e36345e1d25085a79b039.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO ticket_data(ticket_id, serial_number, data) VALUES (?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "7cd38d342eddd578f35116cf024901f94bbf5575595e36345e1d25085a79b039" +} diff --git a/common/gateway-storage/.sqlx/query-7f8af0799d7ae5f751b9964e9566589bf768e7079079f584beb0c1ba16d43a5c.json b/common/gateway-storage/.sqlx/query-7f8af0799d7ae5f751b9964e9566589bf768e7079079f584beb0c1ba16d43a5c.json new file mode 100644 index 0000000000..d8931d6e6e --- /dev/null +++ b/common/gateway-storage/.sqlx/query-7f8af0799d7ae5f751b9964e9566589bf768e7079079f584beb0c1ba16d43a5c.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS (SELECT 1 FROM ticket_data WHERE serial_number = ?) AS 'exists'", + "describe": { + "columns": [ + { + "name": "exists", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + null + ] + }, + "hash": "7f8af0799d7ae5f751b9964e9566589bf768e7079079f584beb0c1ba16d43a5c" +} diff --git a/common/gateway-storage/.sqlx/query-8412b95638509f59d2daa30399ba864263e24b2ab75e0d8a2afbde75da8b6d5a.json b/common/gateway-storage/.sqlx/query-8412b95638509f59d2daa30399ba864263e24b2ab75e0d8a2afbde75da8b6d5a.json new file mode 100644 index 0000000000..03dea6199c --- /dev/null +++ b/common/gateway-storage/.sqlx/query-8412b95638509f59d2daa30399ba864263e24b2ab75e0d8a2afbde75da8b6d5a.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE received_ticket SET rejected = true WHERE id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "8412b95638509f59d2daa30399ba864263e24b2ab75e0d8a2afbde75da8b6d5a" +} diff --git a/common/gateway-storage/.sqlx/query-870e426955cf3d0e297522552a87af82979545975f9df3ac3584fd1bf56a46cd.json b/common/gateway-storage/.sqlx/query-870e426955cf3d0e297522552a87af82979545975f9df3ac3584fd1bf56a46cd.json new file mode 100644 index 0000000000..e54a2f1b02 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-870e426955cf3d0e297522552a87af82979545975f9df3ac3584fd1bf56a46cd.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT signer_id FROM ticket_verification WHERE ticket_id = ?", + "describe": { + "columns": [ + { + "name": "signer_id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "870e426955cf3d0e297522552a87af82979545975f9df3ac3584fd1bf56a46cd" +} diff --git a/common/gateway-storage/.sqlx/query-93fc72c1ee7cf5bbd1b69500ebb36928263d839ee515786cc9354cd8eac6f288.json b/common/gateway-storage/.sqlx/query-93fc72c1ee7cf5bbd1b69500ebb36928263d839ee515786cc9354cd8eac6f288.json new file mode 100644 index 0000000000..40ed3c0d24 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-93fc72c1ee7cf5bbd1b69500ebb36928263d839ee515786cc9354cd8eac6f288.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE ticket_data SET data = NULL WHERE ticket_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "93fc72c1ee7cf5bbd1b69500ebb36928263d839ee515786cc9354cd8eac6f288" +} diff --git a/common/gateway-storage/.sqlx/query-9450b5f34620ec901e555f418eec6e0f489ed72d6b4f2b70ae1d905b4c46f0df.json b/common/gateway-storage/.sqlx/query-9450b5f34620ec901e555f418eec6e0f489ed72d6b4f2b70ae1d905b4c46f0df.json new file mode 100644 index 0000000000..36e35bba32 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-9450b5f34620ec901e555f418eec6e0f489ed72d6b4f2b70ae1d905b4c46f0df.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT available FROM available_bandwidth WHERE client_id = ?", + "describe": { + "columns": [ + { + "name": "available", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "9450b5f34620ec901e555f418eec6e0f489ed72d6b4f2b70ae1d905b4c46f0df" +} diff --git a/common/gateway-storage/.sqlx/query-a2fef59c5d6176170920b940be39f7c98de25d36b7f6516fcc78037862aa7795.json b/common/gateway-storage/.sqlx/query-a2fef59c5d6176170920b940be39f7c98de25d36b7f6516fcc78037862aa7795.json new file mode 100644 index 0000000000..0d54f6fe35 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-a2fef59c5d6176170920b940be39f7c98de25d36b7f6516fcc78037862aa7795.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO available_bandwidth(client_id, available, expiration) VALUES (?, 0, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "a2fef59c5d6176170920b940be39f7c98de25d36b7f6516fcc78037862aa7795" +} diff --git a/common/gateway-storage/.sqlx/query-af2a80cb05c0bff096e6eb830598fbb8ba0a69e0d7079e4600ff47db786e6642.json b/common/gateway-storage/.sqlx/query-af2a80cb05c0bff096e6eb830598fbb8ba0a69e0d7079e4600ff47db786e6642.json new file mode 100644 index 0000000000..8dbd639d40 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-af2a80cb05c0bff096e6eb830598fbb8ba0a69e0d7079e4600ff47db786e6642.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT t1.ticket_id, t2.serial_number\n FROM verified_tickets as t1\n JOIN ticket_data as t2\n ON t1.ticket_id = t2.ticket_id\n JOIN received_ticket as t3\n ON t1.ticket_id = t3.id\n\n ORDER BY t3.received_at ASC\n LIMIT 65535\n ", + "describe": { + "columns": [ + { + "name": "ticket_id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "serial_number", + "ordinal": 1, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false + ] + }, + "hash": "af2a80cb05c0bff096e6eb830598fbb8ba0a69e0d7079e4600ff47db786e6642" +} diff --git a/common/gateway-storage/.sqlx/query-b478ea151f9f82bb437fbaf697505c343f5eeedf602f21db6a292b174d3efe3e.json b/common/gateway-storage/.sqlx/query-b478ea151f9f82bb437fbaf697505c343f5eeedf602f21db6a292b174d3efe3e.json new file mode 100644 index 0000000000..4b5190eb07 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-b478ea151f9f82bb437fbaf697505c343f5eeedf602f21db6a292b174d3efe3e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM ticket_data WHERE ticket_id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "b478ea151f9f82bb437fbaf697505c343f5eeedf602f21db6a292b174d3efe3e" +} diff --git a/common/gateway-storage/.sqlx/query-b83c2d75d90284c0f73e4ee1d46aa87273c8eb2e8e631020ed56969fbf5fa457.json b/common/gateway-storage/.sqlx/query-b83c2d75d90284c0f73e4ee1d46aa87273c8eb2e8e631020ed56969fbf5fa457.json new file mode 100644 index 0000000000..a223a4de0e --- /dev/null +++ b/common/gateway-storage/.sqlx/query-b83c2d75d90284c0f73e4ee1d46aa87273c8eb2e8e631020ed56969fbf5fa457.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO redemption_proposals (proposal_id, created_at) VALUES (?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "b83c2d75d90284c0f73e4ee1d46aa87273c8eb2e8e631020ed56969fbf5fa457" +} diff --git a/common/gateway-storage/.sqlx/query-be36bdf12b8f2145cefca3111f146c71205167f1edcaef624b2f80d30bf269cc.json b/common/gateway-storage/.sqlx/query-be36bdf12b8f2145cefca3111f146c71205167f1edcaef624b2f80d30bf269cc.json new file mode 100644 index 0000000000..4573da3fd8 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-be36bdf12b8f2145cefca3111f146c71205167f1edcaef624b2f80d30bf269cc.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT t1.ticket_id, t2.serial_number\n FROM verified_tickets as t1\n JOIN ticket_data as t2\n ON t1.ticket_id = t2.ticket_id\n WHERE t1.proposal_id = ?\n ", + "describe": { + "columns": [ + { + "name": "ticket_id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "serial_number", + "ordinal": 1, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + }, + "hash": "be36bdf12b8f2145cefca3111f146c71205167f1edcaef624b2f80d30bf269cc" +} diff --git a/common/gateway-storage/.sqlx/query-c2c7ca734c87d151f7f260fe7aa1bf6c4db4fd7ee5a90fc15a94099d9b8ebb56.json b/common/gateway-storage/.sqlx/query-c2c7ca734c87d151f7f260fe7aa1bf6c4db4fd7ee5a90fc15a94099d9b8ebb56.json new file mode 100644 index 0000000000..182a1d39c4 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-c2c7ca734c87d151f7f260fe7aa1bf6c4db4fd7ee5a90fc15a94099d9b8ebb56.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO clients(client_type) VALUES (?)", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "c2c7ca734c87d151f7f260fe7aa1bf6c4db4fd7ee5a90fc15a94099d9b8ebb56" +} diff --git a/common/gateway-storage/.sqlx/query-cbf1d857ffc607cbaef81faac1c35f75549c0f959d9ce944d68abc524523daad.json b/common/gateway-storage/.sqlx/query-cbf1d857ffc607cbaef81faac1c35f75549c0f959d9ce944d68abc524523daad.json new file mode 100644 index 0000000000..41ae9bd551 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-cbf1d857ffc607cbaef81faac1c35f75549c0f959d9ce944d68abc524523daad.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE available_bandwidth\n SET available = 0, expiration = ?\n WHERE client_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "cbf1d857ffc607cbaef81faac1c35f75549c0f959d9ce944d68abc524523daad" +} diff --git a/common/gateway-storage/.sqlx/query-d968d8662a2327918b311d4017bf4c73f9e6f3b1be8ff81c1aebdf3791d59d4d.json b/common/gateway-storage/.sqlx/query-d968d8662a2327918b311d4017bf4c73f9e6f3b1be8ff81c1aebdf3791d59d4d.json new file mode 100644 index 0000000000..bbd32fe422 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-d968d8662a2327918b311d4017bf4c73f9e6f3b1be8ff81c1aebdf3791d59d4d.json @@ -0,0 +1,74 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT * FROM wireguard_peer\n WHERE public_key = ?\n LIMIT 1\n ", + "describe": { + "columns": [ + { + "name": "public_key", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "preshared_key", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "protocol_version", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "endpoint", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "last_handshake", + "ordinal": 4, + "type_info": "Datetime" + }, + { + "name": "tx_bytes", + "ordinal": 5, + "type_info": "Int64" + }, + { + "name": "rx_bytes", + "ordinal": 6, + "type_info": "Int64" + }, + { + "name": "persistent_keepalive_interval", + "ordinal": 7, + "type_info": "Int64" + }, + { + "name": "allowed_ips", + "ordinal": 8, + "type_info": "Blob" + }, + { + "name": "client_id", + "ordinal": 9, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + true, + true, + true, + true, + false, + false, + true, + false, + true + ] + }, + "hash": "d968d8662a2327918b311d4017bf4c73f9e6f3b1be8ff81c1aebdf3791d59d4d" +} diff --git a/common/gateway-storage/.sqlx/query-da51030965d8f9ff0e2511ba6d6b9feecd619c043f9d70752521143de4c14959.json b/common/gateway-storage/.sqlx/query-da51030965d8f9ff0e2511ba6d6b9feecd619c043f9d70752521143de4c14959.json new file mode 100644 index 0000000000..0d2a657865 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-da51030965d8f9ff0e2511ba6d6b9feecd619c043f9d70752521143de4c14959.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO ticket_verification (ticket_id, signer_id, verified_at, accepted)\n VALUES (?, ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "da51030965d8f9ff0e2511ba6d6b9feecd619c043f9d70752521143de4c14959" +} diff --git a/common/gateway-storage/.sqlx/query-dd757b04743ab7e80948a60b6ee5ec36e716324498ec2178283a062d5b360464.json b/common/gateway-storage/.sqlx/query-dd757b04743ab7e80948a60b6ee5ec36e716324498ec2178283a062d5b360464.json new file mode 100644 index 0000000000..72a6be5a14 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-dd757b04743ab7e80948a60b6ee5ec36e716324498ec2178283a062d5b360464.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO shared_keys(client_id, client_address_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key) VALUES (?, ?, ?, ?);\n\n UPDATE shared_keys\n SET\n derived_aes128_ctr_blake3_hmac_keys_bs58 = ?,\n derived_aes256_gcm_siv_key = ?\n WHERE client_address_bs58 = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "dd757b04743ab7e80948a60b6ee5ec36e716324498ec2178283a062d5b360464" +} diff --git a/common/gateway-storage/.sqlx/query-e23f7137e1d2c38deba4653b54269b7f58d6fe9c71a7f9ee2b64f8237b7004fb.json b/common/gateway-storage/.sqlx/query-e23f7137e1d2c38deba4653b54269b7f58d6fe9c71a7f9ee2b64f8237b7004fb.json new file mode 100644 index 0000000000..c37e270778 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-e23f7137e1d2c38deba4653b54269b7f58d6fe9c71a7f9ee2b64f8237b7004fb.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n DELETE FROM wireguard_peer\n WHERE public_key = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "e23f7137e1d2c38deba4653b54269b7f58d6fe9c71a7f9ee2b64f8237b7004fb" +} diff --git a/common/gateway-storage/.sqlx/query-e3860c0c31ca03cc0b22ca34cef5f535a94c78d3491d44d7c8bf1b34a840839d.json b/common/gateway-storage/.sqlx/query-e3860c0c31ca03cc0b22ca34cef5f535a94c78d3491d44d7c8bf1b34a840839d.json new file mode 100644 index 0000000000..c24cc93187 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-e3860c0c31ca03cc0b22ca34cef5f535a94c78d3491d44d7c8bf1b34a840839d.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT \n id as \"id!\",\n client_address_bs58 as \"client_address_bs58!\",\n content as \"content!\"\n FROM message_store\n WHERE client_address_bs58 = ?\n ORDER BY id ASC\n LIMIT ?;\n ", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "client_address_bs58!", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "content!", + "ordinal": 2, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "e3860c0c31ca03cc0b22ca34cef5f535a94c78d3491d44d7c8bf1b34a840839d" +} diff --git a/common/gateway-storage/.sqlx/query-f02272f4f0e5a9f806532fa6e2f3323ef3edeeffdc30ce7f07608ba138d4bb02.json b/common/gateway-storage/.sqlx/query-f02272f4f0e5a9f806532fa6e2f3323ef3edeeffdc30ce7f07608ba138d4bb02.json new file mode 100644 index 0000000000..d56b8cf677 --- /dev/null +++ b/common/gateway-storage/.sqlx/query-f02272f4f0e5a9f806532fa6e2f3323ef3edeeffdc30ce7f07608ba138d4bb02.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE available_bandwidth\n SET available = available - ?\n WHERE client_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "f02272f4f0e5a9f806532fa6e2f3323ef3edeeffdc30ce7f07608ba138d4bb02" +} diff --git a/common/gateway-storage/.sqlx/query-f2b630b29a9e85003c0457dc8a07d24996c50a306b81ad2ae0b30af6683cda0d.json b/common/gateway-storage/.sqlx/query-f2b630b29a9e85003c0457dc8a07d24996c50a306b81ad2ae0b30af6683cda0d.json new file mode 100644 index 0000000000..9930c174fc --- /dev/null +++ b/common/gateway-storage/.sqlx/query-f2b630b29a9e85003c0457dc8a07d24996c50a306b81ad2ae0b30af6683cda0d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO received_ticket (client_id, received_at) VALUES (?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "f2b630b29a9e85003c0457dc8a07d24996c50a306b81ad2ae0b30af6683cda0d" +} diff --git a/common/gateway-storage/.sqlx/query-ff485c6b7e02423511b1fa55d5dd81d6c2f7228daf031c4621937e47804ce5b3.json b/common/gateway-storage/.sqlx/query-ff485c6b7e02423511b1fa55d5dd81d6c2f7228daf031c4621937e47804ce5b3.json new file mode 100644 index 0000000000..b8efd9cfff --- /dev/null +++ b/common/gateway-storage/.sqlx/query-ff485c6b7e02423511b1fa55d5dd81d6c2f7228daf031c4621937e47804ce5b3.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT proposal_id FROM redemption_proposals WHERE resolved_at IS NULL", + "describe": { + "columns": [ + { + "name": "proposal_id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "ff485c6b7e02423511b1fa55d5dd81d6c2f7228daf031c4621937e47804ce5b3" +} diff --git a/common/gateway-storage/src/bandwidth.rs b/common/gateway-storage/src/bandwidth.rs index 3554213ffd..1f1cdce1c8 100644 --- a/common/gateway-storage/src/bandwidth.rs +++ b/common/gateway-storage/src/bandwidth.rs @@ -111,14 +111,14 @@ impl BandwidthManager { amount, client_id ) - .execute(&mut tx) + .execute(&mut *tx) .await?; let remaining = sqlx::query!( "SELECT available FROM available_bandwidth WHERE client_id = ?", client_id ) - .fetch_one(&mut tx) + .fetch_one(&mut *tx) .await? .available; @@ -160,14 +160,14 @@ impl BandwidthManager { amount, client_id ) - .execute(&mut tx) + .execute(&mut *tx) .await?; let remaining = sqlx::query!( "SELECT available FROM available_bandwidth WHERE client_id = ?", client_id ) - .fetch_one(&mut tx) + .fetch_one(&mut *tx) .await? .available; diff --git a/common/gateway-storage/src/lib.rs b/common/gateway-storage/src/lib.rs index 86da88c69b..8d5fda0912 100644 --- a/common/gateway-storage/src/lib.rs +++ b/common/gateway-storage/src/lib.rs @@ -286,14 +286,13 @@ impl PersistentStorage { // TODO: we can inject here more stuff based on our gateway global config // struct. Maybe different pool size or timeout intervals? - let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + let opts = sqlx::sqlite::SqliteConnectOptions::new() .filename(database_path) - .create_if_missing(true); + .create_if_missing(true) + .disable_statement_logging(); // TODO: do we want auto_vacuum ? - opts.disable_statement_logging(); - let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { Ok(db) => db, Err(err) => { diff --git a/common/gateway-storage/src/tickets.rs b/common/gateway-storage/src/tickets.rs index ce68d44514..f8b9539b38 100644 --- a/common/gateway-storage/src/tickets.rs +++ b/common/gateway-storage/src/tickets.rs @@ -92,7 +92,7 @@ impl TicketStorageManager { ) .fetch_one(&self.connection_pool) .await - .map(|result| result.exists == 1) + .map(|result| result.exists == Some(1)) } pub(crate) async fn remove_binary_ticket_data( diff --git a/common/http-api-client/src/lib.rs b/common/http-api-client/src/lib.rs index a8ad8e64d1..549c843958 100644 --- a/common/http-api-client/src/lib.rs +++ b/common/http-api-client/src/lib.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; use std::fmt::Display; use std::time::Duration; use thiserror::Error; -use tracing::warn; +use tracing::{instrument, warn}; use url::Url; pub use reqwest::IntoUrl; @@ -35,6 +35,9 @@ pub enum HttpClientError { source: reqwest::Error, }, + #[error("failed to deserialise received response: {source}")] + ResponseDeserialisationFailure { source: serde_json::Error }, + #[error("provided url is malformed: {source}")] MalformedUrl { #[from] @@ -202,6 +205,7 @@ impl Client { self.reqwest_client.get(url) } + #[instrument(level = "debug", skip_all, fields(path=?path))] async fn send_get_request( &self, path: PathSegments<'_>, @@ -212,6 +216,7 @@ impl Client { V: AsRef, E: Display, { + tracing::trace!("Sending GET request"); let url = sanitize_url(&self.base_url, path, params); #[cfg(target_arch = "wasm32")] @@ -277,6 +282,7 @@ impl Client { } } + #[instrument(level = "debug", skip_all)] pub async fn get_json( &self, path: PathSegments<'_>, @@ -309,6 +315,7 @@ impl Client { parse_response(res, true).await } + #[instrument(level = "debug", skip_all)] pub async fn get_json_endpoint(&self, endpoint: S) -> Result> where for<'a> T: Deserialize<'a>, @@ -512,12 +519,14 @@ pub fn sanitize_url, V: AsRef>( url } +#[tracing::instrument(level = "debug", skip_all)] pub async fn parse_response(res: Response, allow_empty: bool) -> Result> where T: DeserializeOwned, E: DeserializeOwned + Display, { let status = res.status(); + tracing::debug!("Status: {} (success: {})", &status, status.is_success()); if !allow_empty { if let Some(0) = res.content_length() { @@ -526,6 +535,18 @@ where } if res.status().is_success() { + #[cfg(debug_assertions)] + { + let text = res.text().await.inspect_err(|err| { + tracing::error!("Couldn't even get response text: {err}"); + })?; + tracing::trace!("Result:\n{:#?}", text); + + serde_json::from_str(&text) + .map_err(|err| HttpClientError::GenericRequestFailure(err.to_string())) + } + + #[cfg(not(debug_assertions))] Ok(res.json().await?) } else if res.status() == StatusCode::NOT_FOUND { Err(HttpClientError::NotFound) diff --git a/common/http-api-common/Cargo.toml b/common/http-api-common/Cargo.toml index b9252c2b05..c7b6db2ff6 100644 --- a/common/http-api-common/Cargo.toml +++ b/common/http-api-common/Cargo.toml @@ -11,6 +11,7 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +axum-client-ip.workspace = true axum.workspace = true bytes = { workspace = true } colored.workspace = true diff --git a/common/http-api-common/src/logging.rs b/common/http-api-common/src/logging.rs index ca2f5c63ce..8de60b338f 100644 --- a/common/http-api-common/src/logging.rs +++ b/common/http-api-common/src/logging.rs @@ -1,18 +1,18 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use axum::extract::{ConnectInfo, Request}; +use axum::extract::Request; use axum::http::header::{HOST, USER_AGENT}; use axum::http::HeaderValue; use axum::middleware::Next; use axum::response::IntoResponse; +use axum_client_ip::InsecureClientIp; use colored::Colorize; -use std::net::SocketAddr; use std::time::Instant; use tracing::info; pub async fn logger( - ConnectInfo(addr): ConnectInfo, + InsecureClientIp(addr): InsecureClientIp, request: Request, next: Next, ) -> impl IntoResponse { diff --git a/common/mixnode-common/src/packet_processor/error.rs b/common/mixnode-common/src/packet_processor/error.rs index 405e10bb91..50ae179e37 100644 --- a/common/mixnode-common/src/packet_processor/error.rs +++ b/common/mixnode-common/src/packet_processor/error.rs @@ -2,30 +2,17 @@ // SPDX-License-Identifier: Apache-2.0 use nym_sphinx_acknowledgements::surb_ack::SurbAckRecoveryError; -use nym_sphinx_addressing::nodes::NymNodeRoutingAddressError; -use nym_sphinx_types::{NymPacketError, OutfoxError, SphinxError}; +use nym_sphinx_framing::processing::PacketProcessingError; use thiserror::Error; #[derive(Error, Debug)] pub enum MixProcessingError { - #[error("failed to process received packet: {0}")] - NymPacketProcessingError(#[from] NymPacketError), - - #[error("failed to process received sphinx packet: {0}")] - SphinxProcessingError(#[from] SphinxError), - - #[error("the forward hop address was malformed: {0}")] - InvalidForwardHopAddress(#[from] NymNodeRoutingAddressError), - - #[error("the final hop did not contain a SURB-Ack")] - NoSurbAckInFinalHop, - #[error("failed to recover the expected SURB-Ack packet: {0}")] MalformedSurbAck(#[from] SurbAckRecoveryError), #[error("the received packet was set to use the very old and very much deprecated 'VPN' mode")] ReceivedOldTypeVpnPacket, - #[error("failed to process received outfox packet: {0}")] - OutfoxProcessingError(#[from] OutfoxError), + #[error("failed to process received Nym packet: {0}")] + NymPacketProcessingError(#[from] PacketProcessingError), } diff --git a/common/mixnode-common/src/packet_processor/processor.rs b/common/mixnode-common/src/packet_processor/processor.rs index 03a8f37880..7ab70cdce6 100644 --- a/common/mixnode-common/src/packet_processor/processor.rs +++ b/common/mixnode-common/src/packet_processor/processor.rs @@ -1,38 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::packet_processor::error::MixProcessingError; -use log::*; -use nym_metrics::nanos; -use nym_sphinx_acknowledgements::surb_ack::SurbAck; -use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; -use nym_sphinx_forwarding::packet::MixPacket; -use nym_sphinx_framing::packet::FramedNymPacket; -use nym_sphinx_params::{PacketSize, PacketType}; -use nym_sphinx_types::{ - Delay as SphinxDelay, DestinationAddressBytes, NodeAddressBytes, NymPacket, NymProcessedPacket, - PrivateKey, ProcessedPacket, -}; - use std::sync::Arc; -type ForwardAck = MixPacket; - -#[derive(Debug)] -pub struct ProcessedFinalHop { - pub destination: DestinationAddressBytes, - pub forward_ack: Option, - pub message: Vec, -} - -#[derive(Debug)] -pub enum MixProcessingResult { - /// Contains unwrapped data that should first get delayed before being sent to next hop. - ForwardHop(MixPacket, Option), - - /// Contains all data extracted out of the final hop packet that could be forwarded to the destination. - FinalHop(ProcessedFinalHop), -} +use nym_sphinx_types::PrivateKey; #[derive(Clone)] pub struct SphinxPacketProcessor { @@ -48,280 +19,7 @@ impl SphinxPacketProcessor { } } - /// Performs a fresh sphinx unwrapping using no cache. - fn perform_initial_packet_processing( - &self, - packet: NymPacket, - ) -> Result { - nanos!("perform_initial_packet_processing", { - packet.process(&self.sphinx_key).map_err(|err| { - debug!("Failed to unwrap NymPacket packet: {err}"); - MixProcessingError::NymPacketProcessingError(err) - }) - }) - } - - /// Takes the received framed packet and tries to unwrap it from the sphinx encryption. - fn perform_initial_unwrapping( - &self, - received: FramedNymPacket, - ) -> Result { - nanos!("perform_initial_unwrapping", { - let packet = received.into_inner(); - self.perform_initial_packet_processing(packet) - }) - } - - /// Processed received forward hop packet - tries to extract next hop address, sets delay - /// and packs all the data in a way that can be easily sent to the next hop. - fn process_forward_hop( - &self, - packet: NymPacket, - forward_address: NodeAddressBytes, - delay: SphinxDelay, - packet_type: PacketType, - ) -> Result { - let next_hop_address = NymNodeRoutingAddress::try_from(forward_address)?; - - let mix_packet = MixPacket::new(next_hop_address, packet, packet_type); - Ok(MixProcessingResult::ForwardHop(mix_packet, Some(delay))) - } - - /// Split data extracted from the final hop sphinx packet into a SURBAck and message - /// that should get delivered to a client. - fn split_hop_data_into_ack_and_message( - &self, - mut extracted_data: Vec, - packet_type: PacketType, - ) -> Result<(Vec, Vec), MixProcessingError> { - let ack_len = SurbAck::len(Some(packet_type)); - - // in theory it's impossible for this to fail since it managed to go into correct `match` - // branch at the caller - if extracted_data.len() < ack_len { - return Err(MixProcessingError::NoSurbAckInFinalHop); - } - - let message = extracted_data.split_off(ack_len); - let ack_data = extracted_data; - Ok((ack_data, message)) - } - - /// Tries to extract a SURBAck that could be sent back into the mix network and message - /// that should get delivered to a client from received Sphinx packet. - fn split_into_ack_and_message( - &self, - data: Vec, - packet_size: PacketSize, - packet_type: PacketType, - ) -> Result<(Option, Vec), MixProcessingError> { - match packet_size { - PacketSize::AckPacket | PacketSize::OutfoxAckPacket => { - trace!("received an ack packet!"); - Ok((None, data)) - } - PacketSize::RegularPacket - | PacketSize::ExtendedPacket8 - | PacketSize::ExtendedPacket16 - | PacketSize::ExtendedPacket32 - | PacketSize::OutfoxRegularPacket => { - trace!("received a normal packet!"); - let (ack_data, message) = - self.split_hop_data_into_ack_and_message(data, packet_type)?; - let (ack_first_hop, ack_packet) = - match SurbAck::try_recover_first_hop_packet(&ack_data, packet_type) { - Ok((first_hop, packet)) => (first_hop, packet), - Err(err) => { - info!("Failed to recover first hop from ack data: {err}"); - return Err(err.into()); - } - }; - let forward_ack = MixPacket::new(ack_first_hop, ack_packet, packet_type); - Ok((Some(forward_ack), message)) - } - } - } - - /// Processed received final hop packet - tries to extract SURBAck out of it (assuming the - /// packet itself is not an ACK) and splits it from the message that should get delivered - /// to the destination. - fn process_final_hop( - &self, - destination: DestinationAddressBytes, - payload: Vec, - packet_size: PacketSize, - packet_type: PacketType, - ) -> Result { - let (forward_ack, message) = - self.split_into_ack_and_message(payload, packet_size, packet_type)?; - - Ok(MixProcessingResult::FinalHop(ProcessedFinalHop { - destination, - forward_ack, - message, - })) - } - - /// Performs final processing for the unwrapped packet based on whether it was a forward hop - /// or a final hop. - fn perform_final_processing( - &self, - packet: NymProcessedPacket, - packet_size: PacketSize, - packet_type: PacketType, - ) -> Result { - match packet { - NymProcessedPacket::Sphinx(packet) => { - match packet { - ProcessedPacket::ForwardHop(packet, address, delay) => self - .process_forward_hop( - NymPacket::Sphinx(*packet), - address, - delay, - packet_type, - ), - // right now there's no use for the surb_id included in the header - probably it should get removed from the - // sphinx all together? - ProcessedPacket::FinalHop(destination, _, payload) => self.process_final_hop( - destination, - payload.recover_plaintext()?, - packet_size, - packet_type, - ), - } - } - NymProcessedPacket::Outfox(packet) => { - let next_address = *packet.next_address(); - let packet = packet.into_packet(); - if packet.is_final_hop() { - self.process_final_hop( - DestinationAddressBytes::from_bytes(next_address), - packet.recover_plaintext()?.to_vec(), - packet_size, - packet_type, - ) - } else { - let mix_packet = MixPacket::new( - NymNodeRoutingAddress::try_from_bytes(&next_address)?, - NymPacket::Outfox(packet), - PacketType::Outfox, - ); - Ok(MixProcessingResult::ForwardHop(mix_packet, None)) - } - } - } - } - - pub fn process_received( - &self, - received: FramedNymPacket, - ) -> Result { - // explicit packet size will help to correctly parse final hop - nanos!("process_received", { - let packet_size = received.packet_size(); - let packet_type = received.packet_type(); - - // unwrap the sphinx packet and if possible and appropriate, cache keys - let processed_packet = self.perform_initial_unwrapping(received)?; - - // for forward packets, extract next hop and set delay (but do NOT delay here) - // for final packets, extract SURBAck - let final_processing_result = - self.perform_final_processing(processed_packet, packet_size, packet_type); - - if final_processing_result.is_err() { - error!("{:?}", final_processing_result) - } - - final_processing_result - }) - } -} - -// TODO: what more could we realistically test here? -#[cfg(test)] -mod tests { - use super::*; - use nym_sphinx_types::crypto::keygen; - - fn fixture() -> SphinxPacketProcessor { - let local_keys = keygen(); - SphinxPacketProcessor::new(local_keys.0) - } - - #[tokio::test] - async fn splitting_hop_data_works_for_sufficiently_long_payload() { - let processor = fixture(); - - let short_data = vec![42u8]; - assert!(processor - .split_hop_data_into_ack_and_message(short_data, PacketType::Mix) - .is_err()); - - let sufficient_data = vec![42u8; SurbAck::len(Some(PacketType::Mix))]; - let (ack, data) = processor - .split_hop_data_into_ack_and_message(sufficient_data.clone(), PacketType::Mix) - .unwrap(); - assert_eq!(sufficient_data, ack); - assert!(data.is_empty()); - - let long_data = vec![42u8; SurbAck::len(Some(PacketType::Mix)) * 5]; - let (ack, data) = processor - .split_hop_data_into_ack_and_message(long_data, PacketType::Mix) - .unwrap(); - assert_eq!(ack.len(), SurbAck::len(Some(PacketType::Mix))); - assert_eq!(data.len(), SurbAck::len(Some(PacketType::Mix)) * 4) - } - - #[tokio::test] - async fn splitting_hop_data_works_for_sufficiently_long_payload_outfox() { - let processor = fixture(); - - let short_data = vec![42u8]; - assert!(processor - .split_hop_data_into_ack_and_message(short_data, PacketType::Outfox) - .is_err()); - - let sufficient_data = vec![42u8; SurbAck::len(Some(PacketType::Outfox))]; - let (ack, data) = processor - .split_hop_data_into_ack_and_message(sufficient_data.clone(), PacketType::Outfox) - .unwrap(); - assert_eq!(sufficient_data, ack); - assert!(data.is_empty()); - - let long_data = vec![42u8; SurbAck::len(Some(PacketType::Outfox)) * 5]; - let (ack, data) = processor - .split_hop_data_into_ack_and_message(long_data, PacketType::Outfox) - .unwrap(); - assert_eq!(ack.len(), SurbAck::len(Some(PacketType::Outfox))); - assert_eq!(data.len(), SurbAck::len(Some(PacketType::Outfox)) * 4) - } - - #[tokio::test] - async fn splitting_into_ack_and_message_returns_whole_data_for_ack() { - let processor = fixture(); - - let data = vec![42u8; SurbAck::len(Some(PacketType::Mix)) + 10]; - let (ack, message) = processor - .split_into_ack_and_message(data.clone(), PacketSize::AckPacket, PacketType::Mix) - .unwrap(); - assert!(ack.is_none()); - assert_eq!(data, message) - } - - #[tokio::test] - async fn splitting_into_ack_and_message_returns_whole_data_for_ack_outfox() { - let processor = fixture(); - - let data = vec![42u8; SurbAck::len(Some(PacketType::Outfox)) + 10]; - let (ack, message) = processor - .split_into_ack_and_message( - data.clone(), - PacketSize::OutfoxAckPacket, - PacketType::Outfox, - ) - .unwrap(); - assert!(ack.is_none()); - assert_eq!(data, message) + pub fn sphinx_key(&self) -> &PrivateKey { + &self.sphinx_key } } diff --git a/common/mixnode-common/src/verloc/mod.rs b/common/mixnode-common/src/verloc/mod.rs index 68004105d2..7bcea71c9d 100644 --- a/common/mixnode-common/src/verloc/mod.rs +++ b/common/mixnode-common/src/verloc/mod.rs @@ -14,7 +14,6 @@ use nym_task::TaskClient; use rand::seq::SliceRandom; use rand::thread_rng; use std::net::SocketAddr; -use std::net::ToSocketAddrs; use std::sync::Arc; use std::time::Duration; use tokio::task::JoinHandle; @@ -313,7 +312,7 @@ impl VerlocMeasurer { info!("Starting verloc measurements"); // TODO: should we also measure gateways? - let all_mixes = match self.validator_client.get_cached_mixnodes().await { + let all_mixes = match self.validator_client.get_all_described_nodes().await { Ok(nodes) => nodes, Err(err) => { error!( @@ -332,22 +331,14 @@ impl VerlocMeasurer { // we only care about address and identity let tested_nodes = all_mixes .into_iter() + .filter(|n| n.description.declared_role.mixnode) .filter_map(|node| { - let mix_node = node.bond_information.mix_node; - // check if the node has sufficient version to be able to understand the packets - let node_version = parse_version(&mix_node.version).ok()?; - if node_version < self.config.minimum_compatible_node_version { - return None; - } - // try to parse the identity and host - let node_identity = - identity::PublicKey::from_base58_string(mix_node.identity_key).ok()?; + let node_identity = node.ed25519_identity_key(); - let verloc_host = (&*mix_node.host, mix_node.verloc_port) - .to_socket_addrs() - .ok()? - .next()?; + let ip = node.description.host_information.ip_address.first()?; + let verloc_port = node.description.verloc_port(); + let verloc_host = SocketAddr::new(*ip, verloc_port); // TODO: possible problem in the future, this does name resolution and theoretically // if a lot of nodes maliciously mis-configured themselves, it might take a while to resolve them all diff --git a/common/models/Cargo.toml b/common/models/Cargo.toml new file mode 100644 index 0000000000..acb6e35682 --- /dev/null +++ b/common/models/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nym-common-models" +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] +serde = { workspace = true, features = ["derive"] } diff --git a/common/models/src/lib.rs b/common/models/src/lib.rs new file mode 100644 index 0000000000..3d85e66947 --- /dev/null +++ b/common/models/src/lib.rs @@ -0,0 +1 @@ +pub mod ns_api; diff --git a/common/models/src/ns_api.rs b/common/models/src/ns_api.rs new file mode 100644 index 0000000000..5d875420e2 --- /dev/null +++ b/common/models/src/ns_api.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct TestrunAssignment { + pub testrun_id: i64, + pub gateway_identity_key: String, +} diff --git a/common/network-defaults/src/ecash.rs b/common/network-defaults/src/ecash.rs index ee5e7b5c61..a673b62826 100644 --- a/common/network-defaults/src/ecash.rs +++ b/common/network-defaults/src/ecash.rs @@ -19,10 +19,10 @@ pub enum TicketTypeRepr { } impl TicketTypeRepr { - pub const WIREGUARD_ENTRY_TICKET_SIZE: u64 = 512 * 1024 * 1024; // 512 MB - pub const WIREGUARD_EXIT_TICKET_SIZE: u64 = 512 * 1024 * 1024; // 512 MB - pub const MIXNET_ENTRY_TICKET_SIZE: u64 = 128 * 1024 * 1024; // 128 MB - pub const MIXNET_EXIT_TICKET_SIZE: u64 = 128 * 1024 * 1024; // 128 MB + pub const WIREGUARD_ENTRY_TICKET_SIZE: u64 = 500 * 1000 * 1000; // 500 MB + pub const WIREGUARD_EXIT_TICKET_SIZE: u64 = 500 * 1000 * 1000; // 500 MB + pub const MIXNET_ENTRY_TICKET_SIZE: u64 = 200 * 1000 * 1000; // 200 MB + pub const MIXNET_EXIT_TICKET_SIZE: u64 = 100 * 1000 * 1000; // 100 MB /// How much bandwidth (in bytes) one ticket can grant pub const fn bandwidth_value(&self) -> u64 { diff --git a/common/node-tester-utils/src/error.rs b/common/node-tester-utils/src/error.rs index 96337d0bc6..4c83ae7d3d 100644 --- a/common/node-tester-utils/src/error.rs +++ b/common/node-tester-utils/src/error.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::MixId; +use crate::NodeId; use nym_sphinx::chunking::ChunkingError; use nym_sphinx::receiver::MessageRecoveryError; use nym_topology::NymTopologyError; @@ -19,7 +19,7 @@ pub enum NetworkTestingError { InvalidTopology(#[from] NymTopologyError), #[error("The specified mixnode (id: {mix_id}) doesn't exist")] - NonExistentMixnode { mix_id: MixId }, + NonExistentMixnode { mix_id: NodeId }, #[error("The specified mixnode (identity: {mix_identity}) doesn't exist")] NonExistentMixnodeIdentity { mix_identity: String }, diff --git a/common/node-tester-utils/src/lib.rs b/common/node-tester-utils/src/lib.rs index 91d2c7c03f..444d1780f8 100644 --- a/common/node-tester-utils/src/lib.rs +++ b/common/node-tester-utils/src/lib.rs @@ -15,7 +15,7 @@ pub use nym_sphinx::{ pub use tester::NodeTester; // it feels wrong to redefine it, but I don't want to import the whole of contract commons just for this one type -pub(crate) type MixId = u32; +pub(crate) type NodeId = u32; #[macro_export] macro_rules! log_err { diff --git a/common/node-tester-utils/src/message.rs b/common/node-tester-utils/src/message.rs index 41dc517956..b17515d5b6 100644 --- a/common/node-tester-utils/src/message.rs +++ b/common/node-tester-utils/src/message.rs @@ -3,6 +3,7 @@ use crate::error::NetworkTestingError; use crate::node::TestableNode; +use crate::NodeId; use nym_sphinx::message::NymMessage; use nym_topology::{gateway, mix}; use serde::de::DeserializeOwned; @@ -34,13 +35,13 @@ impl TestMessage { } } - pub fn new_mix(node: &mix::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self { + pub fn new_mix(node: &mix::LegacyNode, msg_id: u32, total_msgs: u32, ext: T) -> Self { Self::new(node, msg_id, total_msgs, ext) } - pub fn new_gateway(node: &gateway::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self { - Self::new(node, msg_id, total_msgs, ext) - } + // pub fn new_gateway(node: &gateway::Node, msg_id: u32, total_msgs: u32, ext: T) -> Self { + // Self::new(node, msg_id, total_msgs, ext) + // } pub fn new_serialized( node: N, @@ -72,7 +73,7 @@ impl TestMessage { } pub fn mix_plaintexts( - node: &mix::Node, + node: &mix::LegacyNode, total_msgs: u32, ext: T, ) -> Result>, NetworkTestingError> @@ -82,15 +83,16 @@ impl TestMessage { Self::new_plaintexts(node, total_msgs, ext) } - pub fn gateway_plaintexts( - node: &gateway::Node, + pub fn legacy_gateway_plaintexts( + node: &gateway::LegacyNode, + node_id: NodeId, total_msgs: u32, ext: T, ) -> Result>, NetworkTestingError> where T: Serialize + Clone, { - Self::new_plaintexts(node, total_msgs, ext) + Self::new_plaintexts(&(node, node_id), total_msgs, ext) } pub fn as_json_string(&self) -> Result diff --git a/common/node-tester-utils/src/node.rs b/common/node-tester-utils/src/node.rs index ed4aa3b52e..d60623a6e2 100644 --- a/common/node-tester-utils/src/node.rs +++ b/common/node-tester-utils/src/node.rs @@ -1,7 +1,7 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::MixId; +use crate::NodeId; use nym_topology::{gateway, mix}; use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter}; @@ -9,27 +9,27 @@ use std::fmt::{Display, Formatter}; #[derive(Serialize, Deserialize, Debug, Clone, Hash, Eq, PartialEq)] pub struct TestableNode { pub encoded_identity: String, - pub owner: String, + pub node_id: NodeId, #[serde(rename = "type")] pub typ: NodeType, } impl TestableNode { - pub fn new(encoded_identity: String, owner: String, typ: NodeType) -> Self { + pub fn new(encoded_identity: String, typ: NodeType, node_id: NodeId) -> Self { TestableNode { encoded_identity, - owner, + node_id, typ, } } - pub fn new_mixnode(encoded_identity: String, owner: String, mix_id: MixId) -> Self { - TestableNode::new(encoded_identity, owner, NodeType::Mixnode { mix_id }) + pub fn new_mixnode(encoded_identity: String, node_id: NodeId) -> Self { + TestableNode::new(encoded_identity, NodeType::Mixnode, node_id) } - pub fn new_gateway(encoded_identity: String, owner: String) -> Self { - TestableNode::new(encoded_identity, owner, NodeType::Gateway) + pub fn new_gateway(encoded_identity: String, node_id: NodeId) -> Self { + TestableNode::new(encoded_identity, NodeType::Gateway, node_id) } pub fn is_mixnode(&self) -> bool { @@ -37,24 +37,34 @@ impl TestableNode { } } -impl<'a> From<&'a mix::Node> for TestableNode { - fn from(value: &'a mix::Node) -> Self { +impl<'a> From<&'a mix::LegacyNode> for TestableNode { + fn from(value: &'a mix::LegacyNode) -> Self { TestableNode { encoded_identity: value.identity_key.to_base58_string(), - owner: value.owner.as_ref().cloned().unwrap_or_default(), - typ: NodeType::Mixnode { - mix_id: value.mix_id, - }, + typ: NodeType::Mixnode, + node_id: value.mix_id, } } } -impl<'a> From<&'a gateway::Node> for TestableNode { - fn from(value: &'a gateway::Node) -> Self { +impl<'a> From<(&'a gateway::LegacyNode, NodeId)> for TestableNode { + fn from((gateway, node_id): (&'a gateway::LegacyNode, NodeId)) -> Self { + (&(gateway, node_id)).into() + } +} + +impl<'a> From<&'a (gateway::LegacyNode, NodeId)> for TestableNode { + fn from((gateway, node_id): &'a (gateway::LegacyNode, NodeId)) -> Self { + (gateway, *node_id).into() + } +} + +impl<'a, 'b> From<&'a (&'b gateway::LegacyNode, NodeId)> for TestableNode { + fn from((gateway, node_id): &'a (&'b gateway::LegacyNode, NodeId)) -> Self { TestableNode { - encoded_identity: value.identity_key.to_base58_string(), - owner: value.owner.as_ref().cloned().unwrap_or_default(), + encoded_identity: gateway.identity_key.to_base58_string(), typ: NodeType::Gateway, + node_id: *node_id, } } } @@ -63,8 +73,8 @@ impl Display for TestableNode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, - "{} {} owned by {}", - self.typ, self.encoded_identity, self.owner + "{}-{}: {}", + self.typ, self.node_id, self.encoded_identity ) } } @@ -72,7 +82,7 @@ impl Display for TestableNode { #[derive(Serialize, Deserialize, Hash, Clone, Copy, Debug, Eq, PartialEq)] #[serde(rename_all = "snake_case")] pub enum NodeType { - Mixnode { mix_id: MixId }, + Mixnode, Gateway, } @@ -85,7 +95,7 @@ impl NodeType { impl Display for NodeType { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - NodeType::Mixnode { mix_id } => write!(f, "mixnode (mix_id {mix_id})"), + NodeType::Mixnode => write!(f, "mixnode"), NodeType::Gateway => write!(f, "gateway"), } } diff --git a/common/node-tester-utils/src/tester.rs b/common/node-tester-utils/src/tester.rs index e31d632cc7..d60ec55e67 100644 --- a/common/node-tester-utils/src/tester.rs +++ b/common/node-tester-utils/src/tester.rs @@ -3,7 +3,7 @@ use crate::error::NetworkTestingError; use crate::Empty; -use crate::MixId; +use crate::NodeId; use crate::TestMessage; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; @@ -29,6 +29,9 @@ pub struct NodeTester { packet_size: PacketSize, + /// Specify whether route selection should be determined by the packet header. + deterministic_route_selection: bool, + /// Average delay a data packet is going to get delay at a single mixnode. average_packet_delay: Duration, @@ -48,11 +51,13 @@ impl NodeTester where R: Rng + CryptoRng, { + #[allow(clippy::too_many_arguments)] pub fn new( rng: R, base_topology: NymTopology, self_address: Option, packet_size: PacketSize, + deterministic_route_selection: bool, average_packet_delay: Duration, average_ack_delay: Duration, ack_key: Arc, @@ -62,6 +67,7 @@ where base_topology, self_address, packet_size, + deterministic_route_selection, average_packet_delay, average_ack_delay, num_mix_hops: DEFAULT_NUM_MIX_HOPS, @@ -76,13 +82,13 @@ where self } - pub fn testable_mix_topology(&self, node: &mix::Node) -> NymTopology { + pub fn testable_mix_topology(&self, node: &mix::LegacyNode) -> NymTopology { let mut topology = self.base_topology.clone(); topology.set_mixes_in_layer(node.layer as u8, vec![node.clone()]); topology } - pub fn testable_gateway_topology(&self, gateway: &gateway::Node) -> NymTopology { + pub fn testable_gateway_topology(&self, gateway: &gateway::LegacyNode) -> NymTopology { let mut topology = self.base_topology.clone(); topology.set_gateways(vec![gateway.clone()]); topology @@ -90,7 +96,7 @@ where pub fn simple_mixnode_test_packets( &mut self, - mix: &mix::Node, + mix: &mix::LegacyNode, test_packets: u32, ) -> Result, NetworkTestingError> { self.mixnode_test_packets(mix, Empty, test_packets, None) @@ -98,7 +104,7 @@ where pub fn mixnode_test_packets( &mut self, - mix: &mix::Node, + mix: &mix::LegacyNode, msg_ext: T, test_packets: u32, custom_recipient: Option, @@ -122,7 +128,7 @@ where pub fn mixnodes_test_packets( &mut self, - nodes: &[mix::Node], + nodes: &[mix::LegacyNode], msg_ext: T, test_packets: u32, custom_recipient: Option, @@ -145,7 +151,7 @@ where pub fn existing_mixnode_test_packets( &mut self, - mix_id: MixId, + mix_id: NodeId, msg_ext: T, test_packets: u32, custom_recipient: Option, @@ -182,9 +188,10 @@ where self.mixnode_test_packets(&node.clone(), msg_ext, test_packets, custom_recipient) } - pub fn gateway_test_packets( + pub fn legacy_gateway_test_packets( &mut self, - gateway: &gateway::Node, + gateway: &gateway::LegacyNode, + node_id: NodeId, msg_ext: T, test_packets: u32, custom_recipient: Option, @@ -195,7 +202,9 @@ where let ephemeral_topology = self.testable_gateway_topology(gateway); let mut packets = Vec::with_capacity(test_packets as usize); - for plaintext in TestMessage::gateway_plaintexts(gateway, test_packets, msg_ext)? { + for plaintext in + TestMessage::legacy_gateway_plaintexts(gateway, node_id, test_packets, msg_ext)? + { packets.push(self.wrap_plaintext_data( plaintext, &ephemeral_topology, @@ -208,6 +217,7 @@ where pub fn existing_gateway_test_packets( &mut self, + node_id: NodeId, encoded_gateway_identity: String, msg_ext: T, test_packets: u32, @@ -222,7 +232,13 @@ where }); }; - self.gateway_test_packets(&node.clone(), msg_ext, test_packets, custom_recipient) + self.legacy_gateway_test_packets( + &node.clone(), + node_id, + msg_ext, + test_packets, + custom_recipient, + ) } pub fn wrap_plaintext_data( @@ -279,10 +295,18 @@ where impl FragmentPreparer for NodeTester { type Rng = R; + fn deterministic_route_selection(&self) -> bool { + self.deterministic_route_selection + } + fn rng(&mut self) -> &mut Self::Rng { &mut self.rng } + fn nonce(&self) -> i32 { + 1 + } + fn num_mix_hops(&self) -> u8 { self.num_mix_hops } @@ -294,8 +318,4 @@ impl FragmentPreparer for NodeTester { fn average_ack_delay(&self) -> Duration { self.average_ack_delay } - - fn nonce(&self) -> i32 { - 1 - } } diff --git a/common/nym_offline_compact_ecash/Cargo.toml b/common/nym_offline_compact_ecash/Cargo.toml index e9d2acbda5..2497edabd9 100644 --- a/common/nym_offline_compact_ecash/Cargo.toml +++ b/common/nym_offline_compact_ecash/Cargo.toml @@ -14,7 +14,7 @@ license = { workspace = true } bls12_381 = { workspace = true, features = ["alloc", "pairings", "experimental", "zeroize", "experimental_serde"] } bincode.workspace = true cfg-if.workspace = true -itertools = "0.12.1" +itertools = { workspace = true } digest = "0.9" rand = { workspace = true } thiserror = { workspace = true } @@ -25,6 +25,7 @@ rayon = { workspace = true, optional = true } zeroize = { workspace = true, features = ["zeroize_derive"] } ff = { workspace = true } group = { workspace = true } +subtle = { workspace = true } nym-pemstore = { path = "../pemstore" } nym-network-defaults = { path = "../network-defaults", default-features = false } diff --git a/common/nym_offline_compact_ecash/src/common_types.rs b/common/nym_offline_compact_ecash/src/common_types.rs index 6aaf81f673..14f9b1cb66 100644 --- a/common/nym_offline_compact_ecash/src/common_types.rs +++ b/common/nym_offline_compact_ecash/src/common_types.rs @@ -6,6 +6,7 @@ use crate::error::Result; use crate::helpers::{g1_tuple_to_bytes, recover_g1_tuple}; use bls12_381::{G1Projective, Scalar}; use serde::{Deserialize, Serialize}; +use subtle::Choice; pub type SignerIndex = u64; @@ -58,6 +59,11 @@ impl Signature { let (h, s) = recover_g1_tuple::(bytes)?; Ok(Signature { h, s }) } + + /// Checks whether any of the group elements of the signature is an identity element located at infinity + pub fn is_at_infinity(&self) -> Choice { + self.s.is_identity() | self.h.is_identity() + } } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] diff --git a/common/nym_offline_compact_ecash/src/error.rs b/common/nym_offline_compact_ecash/src/error.rs index 0d49581ec6..badccfc4de 100644 --- a/common/nym_offline_compact_ecash/src/error.rs +++ b/common/nym_offline_compact_ecash/src/error.rs @@ -22,9 +22,15 @@ pub enum CompactEcashError { #[error("aggregation verification error")] AggregationVerification, + #[error("the provided signature is at infinity")] + IdentitySignature, + #[error("different element size for aggregation")] AggregationSizeMismatch, + #[error("the provided commitment hash is at infinity")] + IdentityCommitmentHash, + #[error("withdrawal request failed to verify")] WithdrawalRequestVerification, diff --git a/common/nym_offline_compact_ecash/src/proofs/proof_withdrawal.rs b/common/nym_offline_compact_ecash/src/proofs/proof_withdrawal.rs index cba7161713..b16b66d7ff 100644 --- a/common/nym_offline_compact_ecash/src/proofs/proof_withdrawal.rs +++ b/common/nym_offline_compact_ecash/src/proofs/proof_withdrawal.rs @@ -24,12 +24,12 @@ pub struct WithdrawalReqInstance { } // witness: m1, m2, m3, o, o1, o2, o3, -pub struct WithdrawalReqWitness { - pub private_attributes: Vec, +pub struct WithdrawalReqWitness<'a> { + pub private_attributes: Vec<&'a Scalar>, // Opening for the joined commitment com - pub joined_commitment_opening: Scalar, + pub joined_commitment_opening: &'a Scalar, // Openings for the pedersen commitments of private attributes - pub private_attributes_openings: Vec, + pub private_attributes_openings: &'a Vec, } #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] @@ -93,7 +93,7 @@ impl WithdrawalReqProof { let response_opening = produce_response( &r_com_opening, &challenge, - &witness.joined_commitment_opening, + witness.joined_commitment_opening, ); let response_openings = produce_responses( &r_pedcom_openings, @@ -103,11 +103,8 @@ impl WithdrawalReqProof { .iter() .collect::>(), ); - let response_attributes = produce_responses( - &r_attributes, - &challenge, - &witness.private_attributes.iter().collect::>(), - ); + let response_attributes = + produce_responses(&r_attributes, &challenge, &witness.private_attributes); WithdrawalReqProof { challenge, @@ -212,7 +209,7 @@ mod tests { }; let v = params.random_scalar(); let t = params.random_scalar(); - let private_attributes = vec![sk, v, t]; + let private_attributes = vec![&sk, &v, &t]; let joined_commitment_opening = params.random_scalar(); let joined_commitment = params.gen1() * joined_commitment_opening @@ -227,7 +224,7 @@ mod tests { let private_attributes_commitments = private_attributes_openings .iter() .zip(private_attributes.iter()) - .map(|(o_j, m_j)| params.gen1() * o_j + joined_commitment_hash * m_j) + .map(|(o_j, &m_j)| params.gen1() * o_j + joined_commitment_hash * m_j) .collect::>(); let instance = WithdrawalReqInstance { @@ -239,8 +236,8 @@ mod tests { let witness = WithdrawalReqWitness { private_attributes, - joined_commitment_opening, - private_attributes_openings, + joined_commitment_opening: &joined_commitment_opening, + private_attributes_openings: &private_attributes_openings, }; let zk_proof = WithdrawalReqProof::construct(&instance, &witness); assert!(zk_proof.verify(&instance)) diff --git a/common/nym_offline_compact_ecash/src/scheme/aggregation.rs b/common/nym_offline_compact_ecash/src/scheme/aggregation.rs index b5ca003e22..9018cde7ea 100644 --- a/common/nym_offline_compact_ecash/src/scheme/aggregation.rs +++ b/common/nym_offline_compact_ecash/src/scheme/aggregation.rs @@ -50,6 +50,17 @@ where impl Aggregatable for PartialSignature { fn aggregate(sigs: &[PartialSignature], indices: Option<&[u64]>) -> Result { + // Ensure that we have valid signatures + if sigs.is_empty() { + return Err(CompactEcashError::AggregationEmptySet); + } + + // Check each individual signature for point at infinity + for sig in sigs { + if bool::from(sig.is_at_infinity()) { + return Err(CompactEcashError::IdentitySignature); + } + } let h = sigs .first() .ok_or(CompactEcashError::AggregationEmptySet)? @@ -109,6 +120,11 @@ pub fn aggregate_signatures( Err(err) => return Err(err), }; + // Ensure the aggregated signature is not an infinity point + if bool::from(signature.is_at_infinity()) { + return Err(CompactEcashError::IdentitySignature); + } + // Verify the signature let tmp = attributes .iter() diff --git a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs index 77d780f589..3311105d55 100644 --- a/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs +++ b/common/nym_offline_compact_ecash/src/scheme/withdrawal.rs @@ -104,11 +104,11 @@ impl RequestInfo { fn compute_private_attribute_commitments( params: &GroupParameters, joined_commitment_hash: &G1Projective, - private_attributes: &[Scalar], + private_attributes: &[&Scalar], ) -> (Vec, Vec) { let (openings, commitments): (Vec, Vec) = private_attributes .iter() - .map(|m_j| { + .map(|&m_j| { let o_j = params.random_scalar(); (o_j, params.gen1() * o_j + joined_commitment_hash * m_j) }) @@ -116,7 +116,44 @@ fn compute_private_attribute_commitments( (openings, commitments) } +/// Generates a non-identity hash of joined commitment. +/// +/// This function attempts to create a valid joined commitment and hash by +/// repeatedly generating a random `joined_commitment_opening` and computing +/// the corresponding `joined_commitment` and `joined_commitment_hash`. +/// It continues this process until the `joined_commitment_hash` is not the +/// identity element. +fn generate_non_identity_h( + params: &GroupParameters, + sk_user: &SecretKeyUser, + v: &Scalar, + expiration_date: Scalar, + t_type: Scalar, +) -> (G1Projective, G1Projective, Scalar) { + let gamma = params.gammas(); + loop { + let joined_commitment_opening = params.random_scalar(); + + // Compute joined commitment for all attributes (public and private) + let joined_commitment = + params.gen1() * joined_commitment_opening + gamma[0] * sk_user.sk + gamma[1] * v; + + // Compute commitment hash h + let joined_commitment_hash = hash_g1( + (joined_commitment + gamma[2] * expiration_date + gamma[3] * t_type).to_bytes(), + ); + + // Check if the joined_commitment_hash is not the identity element + if !bool::from(joined_commitment_hash.is_identity()) { + return ( + joined_commitment, + joined_commitment_hash, + joined_commitment_opening, + ); + } + } +} /// Generates a withdrawal request for the given user to request a zk-nym credential wallet. /// /// # Arguments @@ -149,22 +186,15 @@ pub fn withdrawal_request( let params = ecash_group_parameters(); // Generate random and unique wallet secret let v = params.random_scalar(); - let joined_commitment_opening = params.random_scalar(); - - let gamma = params.gammas(); let expiration_date = date_scalar(expiration_date); let t_type = type_scalar(t_type); - // Compute joined commitment for all attributes (public and private) - let joined_commitment = - params.gen1() * joined_commitment_opening + gamma[0] * sk_user.sk + gamma[1] * v; - - // Compute commitment hash h - let joined_commitment_hash = - hash_g1((joined_commitment + gamma[2] * expiration_date + gamma[3] * t_type).to_bytes()); + // Generate a non-identity commitment hash + let (joined_commitment, joined_commitment_hash, joined_commitment_opening) = + generate_non_identity_h(params, sk_user, &v, expiration_date, t_type); // Compute Pedersen commitments for private attributes (wallet secret and user's secret) - let private_attributes = vec![sk_user.sk, v]; + let private_attributes = vec![&sk_user.sk, &v]; let (private_attributes_openings, private_attributes_commitments) = compute_private_attribute_commitments(params, &joined_commitment_hash, &private_attributes); @@ -180,8 +210,8 @@ pub fn withdrawal_request( let witness = WithdrawalReqWitness { private_attributes, - joined_commitment_opening, - private_attributes_openings: private_attributes_openings.clone(), + joined_commitment_opening: &joined_commitment_opening, + private_attributes_openings: &private_attributes_openings, }; let zk_proof = WithdrawalReqProof::construct(&instance, &witness); @@ -196,7 +226,7 @@ pub fn withdrawal_request( RequestInfo { joined_commitment_hash, joined_commitment_opening, - private_attributes_openings: private_attributes_openings.clone(), + private_attributes_openings, wallet_secret: v, expiration_date, t_type, @@ -230,6 +260,10 @@ pub fn request_verify( let expiration_date = date_scalar(expiration_date); let t_type = type_scalar(t_type); + if bool::from(req.joined_commitment_hash.is_identity()) { + return Err(CompactEcashError::IdentityCommitmentHash); + } + let expected_commitment_hash = hash_g1( (req.joined_commitment + gamma[2] * expiration_date + gamma[3] * t_type).to_bytes(), ); @@ -387,6 +421,9 @@ pub fn issue_verify( if req_info.joined_commitment_hash != blind_signature.h { return Err(CompactEcashError::IssuanceVerification); } + if bool::from(blind_signature.h.is_identity()) { + return Err(CompactEcashError::IdentitySignature); + } // Unblind the blinded signature on the partial signature let blinding_removers = vk_auth @@ -464,7 +501,12 @@ pub fn verify_partial_blind_signature( if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() { return false; } - + // Note: This check is useful if someone uses the code of those functions + // to verify Pointcheval-Sanders signatures in a context different for their use + // in zk-nyms + if bool::from(blind_sig.h.is_identity()) { + return false; + } // TODO: we're losing some memory here due to extra allocation, // but worst-case scenario (given SANE amount of attributes), it's just few kb at most let c_neg = blind_sig.c.to_affine().neg(); @@ -520,3 +562,63 @@ pub fn verify_partial_blind_signature( .is_identity() .into() } + +#[cfg(test)] +mod tests { + use super::{generate_non_identity_h, verify_partial_blind_signature}; + use crate::common_types::BlindedSignature; + use crate::ecash_group_parameters; + use crate::scheme::keygen::{SecretKeyUser, VerificationKeyAuth}; + use bls12_381::G1Projective; + + #[test] + fn test_generate_non_identity_h() { + let params = ecash_group_parameters(); + // Create dummy values for testing + let sk_user = SecretKeyUser { + sk: params.random_scalar(), + }; + let v = params.random_scalar(); + let expiration_date = params.random_scalar(); + let t_type = params.random_scalar(); + + // Generate the commitment and hash + let (_, joined_commitment_hash, _) = + generate_non_identity_h(params, &sk_user, &v, expiration_date, t_type); + + // Ensure that the joined_commitment_hash is not the identity element + assert!( + !bool::from(joined_commitment_hash.is_identity()), + "Joined commitment hash should not be the identity element" + ); + } + + #[test] + fn test_verify_partial_blind_signature_blind_sig_identity() { + let params = ecash_group_parameters(); + let private_attribute_commitments = vec![params.gen1() * params.random_scalar()]; + let public_attributes = vec![]; + // Create a blinded signature with h being the identity element + let blind_sig = BlindedSignature { + h: G1Projective::identity(), + c: params.gen1() * params.random_scalar(), + }; + // Create a mock partial verification key + let partial_verification_key = VerificationKeyAuth { + alpha: params.gen2() * params.random_scalar(), + beta_g1: vec![params.gen1() * params.random_scalar()], + beta_g2: vec![params.gen2() * params.random_scalar()], + }; + + // Test with identity h, expecting false + assert!( + !verify_partial_blind_signature( + &private_attribute_commitments, + &public_attributes, + &blind_sig, + &partial_verification_key + ), + "Expected verification to return false for identity h in blind signature" + ); + } +} diff --git a/common/nymcoconut/src/scheme/aggregation.rs b/common/nymcoconut/src/scheme/aggregation.rs index c97fd4b8ad..e954e5b8b0 100644 --- a/common/nymcoconut/src/scheme/aggregation.rs +++ b/common/nymcoconut/src/scheme/aggregation.rs @@ -115,6 +115,11 @@ pub fn aggregate_signatures_and_verify( .map(|(&attr, beta_i)| beta_i * attr) .sum::(); + if bool::from(signature.0.is_identity()) { + return Err(CoconutError::Aggregation( + "Verification of the aggregated signature failed - h is an identity point".to_string(), + )); + } if !check_bilinear_pairing( &signature.0.to_affine(), &G2Prepared::from((alpha + tmp).to_affine()), @@ -248,7 +253,7 @@ mod tests { let sigs = sks .iter() - .map(|sk| sign(¶ms, sk, &attributes).unwrap()) + .map(|sk| sign(sk, &attributes).unwrap()) .collect::>(); // aggregating (any) threshold works diff --git a/common/nymcoconut/src/scheme/issuance.rs b/common/nymcoconut/src/scheme/issuance.rs index 3779e377d6..24a4c85bbf 100644 --- a/common/nymcoconut/src/scheme/issuance.rs +++ b/common/nymcoconut/src/scheme/issuance.rs @@ -252,11 +252,25 @@ pub fn prepare_blind_sign( }); } - let (commitment_opening, commitment) = - compute_attributes_commitment(params, private_attributes, public_attributes, hs); - - // Compute the challenge as the commitment hash - let commitment_hash = compute_hash(commitment, public_attributes); + let mut commitment_hash; + let mut commitment; + let mut commitment_opening; + + loop { + // Compute the attributes commitment + let (c_opening, c) = + compute_attributes_commitment(params, private_attributes, public_attributes, hs); + commitment_opening = c_opening; + commitment = c; + + // Compute the commitment hash + commitment_hash = compute_hash(commitment, public_attributes); + + // Check if the commitment hash is not the identity point + if !bool::from(commitment_hash.is_identity()) { + break; + } + } let (pedersen_commitments_openings, pedersen_commitments) = compute_pedersen_commitments_for_private_attributes( @@ -304,6 +318,11 @@ pub fn blind_sign( // Verify the commitment hash let h = compute_hash(blind_sign_request.commitment, public_attributes); + if bool::from(blind_sign_request.commitment_hash.is_identity()) { + return Err(CoconutError::Issuance( + "Commitment hash should not be an identity point".to_string(), + )); + } if !(h == blind_sign_request.commitment_hash) { return Err(CoconutError::Issuance( "Failed to verify the commitment hash".to_string(), @@ -372,6 +391,9 @@ pub fn verify_partial_blind_signature( if num_private_attributes + public_attributes.len() > partial_verification_key.beta_g2.len() { return false; } + if bool::from(blind_sig.0.is_identity()) { + return false; + } // TODO: we're losing some memory here due to extra allocation, // but worst-case scenario (given SANE amount of attributes), it's just few kb at most @@ -430,11 +452,7 @@ pub fn verify_partial_blind_signature( } /// Creates a Coconut Signature under a given secret key on a set of public attributes only. -pub fn sign( - params: &Parameters, - secret_key: &SecretKey, - public_attributes: &[&Attribute], -) -> Result { +pub fn sign(secret_key: &SecretKey, public_attributes: &[&Attribute]) -> Result { if public_attributes.len() > secret_key.ys.len() { return Err(CoconutError::IssuanceMaxAttributes { max: secret_key.ys.len(), @@ -442,13 +460,23 @@ pub fn sign( }); } - // TODO: why in the python implementation this hash onto the curve is present - // while it's not used in the paper? the paper uses random exponent instead. - // (the python implementation hashes string representation of all attributes onto the curve, - // but I think the same can be achieved by just summing the attributes thus avoiding the unnecessary - // transformation. If I'm wrong, please correct me.) - let attributes_sum = public_attributes.iter().copied().sum::(); - let h = hash_g1((params.gen1() * attributes_sum).to_bytes()); + //Serialize the array structure of the public attributes into a byte array + let mut serialized_attributes = Vec::new(); + //Prepend the length of the entire array (in bytes) + let array_len = public_attributes.len() as u64; + serialized_attributes.extend_from_slice(&array_len.to_le_bytes()); + //Serialize each attribute with its length + for &attribute in public_attributes.iter() { + let attr_bytes = attribute.to_bytes(); + let attr_len = attr_bytes.len() as u64; + + // Prefix the attribute with its length + serialized_attributes.extend_from_slice(&attr_len.to_le_bytes()); + serialized_attributes.extend_from_slice(&attr_bytes); + } + + //Hash the resulting byte array to derive the point H + let h = hash_g1(serialized_attributes); // x + m0 * y0 + m1 * y1 + ... mn * yn let exponent = secret_key.x @@ -499,6 +527,61 @@ mod tests { ); } + #[test] + fn test_prepare_blind_sign_non_identity_commitment_hash() { + let params = Parameters::new(1).unwrap(); + random_scalars_refs!(private_attributes, params, 1); + random_scalars_refs!(public_attributes, params, 0); + + // Call the function to prepare the blind sign + let result = prepare_blind_sign(¶ms, &private_attributes, &public_attributes); + + // Ensure the result is Ok + assert!(result.is_ok(), "prepare_blind_sign should succeed"); + + let (_, blind_sign_request) = result.unwrap(); + + // Ensure the commitment_hash is not the identity point + assert!( + !bool::from(blind_sign_request.commitment_hash.is_identity()), + "commitment_hash should not be the identity point" + ); + } + + #[test] + fn test_blind_sign_with_identity_commitment_hash() { + let params = Parameters::new(1).unwrap(); + random_scalars_refs!(private_attributes, params, 1); + random_scalars_refs!(public_attributes, params, 0); + + // Call the function to prepare the blind sign + let (_commitments_openings, blind_sign_request) = + prepare_blind_sign(¶ms, &private_attributes, &public_attributes).unwrap(); + let blind_sign_request = BlindSignRequest { + commitment_hash: G1Projective::identity(), + ..blind_sign_request // This copies the other fields from the existing instance + }; + + let signing_secret_key = SecretKey { + x: params.random_scalar(), + ys: vec![params.random_scalar()], + }; + + // Call blind_sign and ensure it returns an error due to identity commitment hash + let result = blind_sign( + ¶ms, + &signing_secret_key, + &blind_sign_request, + &public_attributes, + ); + + // The result should be an error + assert!( + result.is_err(), + "blind_sign should return an error when commitment_hash is the identity point" + ); + } + #[test] fn successful_verify_partial_blind_signature() { let params = Parameters::new(4).unwrap(); diff --git a/common/nymcoconut/src/scheme/mod.rs b/common/nymcoconut/src/scheme/mod.rs index 691a8dc064..b811b6e250 100644 --- a/common/nymcoconut/src/scheme/mod.rs +++ b/common/nymcoconut/src/scheme/mod.rs @@ -103,6 +103,11 @@ impl Signature { commitment_hash: &G1Projective, ) -> Result<()> { // Verify the commitment hash + if bool::from(self.0.is_identity()) { + return Err(CoconutError::Verification( + "Commitment hash should not be an identity point".to_string(), + )); + } if !(commitment_hash == &self.0) { return Err(CoconutError::Verification( "Verification of commitment hash from signature failed".to_string(), @@ -431,8 +436,8 @@ mod tests { let keypair1 = keygen(¶ms); let keypair2 = keygen(¶ms); - let sig1 = sign(¶ms, keypair1.secret_key(), &attributes).unwrap(); - let sig2 = sign(¶ms, keypair2.secret_key(), &attributes).unwrap(); + let sig1 = sign(keypair1.secret_key(), &attributes).unwrap(); + let sig2 = sign(keypair2.secret_key(), &attributes).unwrap(); assert!(verify( ¶ms, diff --git a/common/nymcoconut/src/scheme/verification.rs b/common/nymcoconut/src/scheme/verification.rs index a1968ced47..149c60d3e3 100644 --- a/common/nymcoconut/src/scheme/verification.rs +++ b/common/nymcoconut/src/scheme/verification.rs @@ -312,6 +312,7 @@ pub fn verify( #[cfg(test)] mod tests { + use crate::scheme::issuance::sign; use crate::scheme::keygen::keygen; use crate::scheme::setup::setup; @@ -355,4 +356,77 @@ mod tests { theta ); } + + #[test] + fn reject_forged_signature_via_linear_combination() { + // This test checks if the protocol correctly rejects forged signatures created + // by linear combinations of valid signatures. The verification for forged + // signatures should fail. + let params = Parameters::new(4).unwrap(); + + let scalar_2 = Scalar::one() + Scalar::one(); + let scalar_2_inv = Scalar::invert(&scalar_2).unwrap(); + + //#1 + let a = params.random_scalar(); + let zero = Scalar::zero(); + let a_zero = vec![&a, &zero]; + let zero_a = vec![&zero, &a]; + + let validator_keypair = keygen(¶ms); + + //#2 + let sig_a_zero = sign(validator_keypair.secret_key(), &a_zero).unwrap(); + let sig_zero_a = sign(validator_keypair.secret_key(), &zero_a).unwrap(); + + assert!(verify( + ¶ms, + validator_keypair.verification_key(), + &a_zero, + &sig_a_zero + )); + assert!(verify( + ¶ms, + validator_keypair.verification_key(), + &zero_a, + &sig_zero_a + )); + + //#3 + let h0 = sig_a_zero.0; + // Removed unnecessary references + let h1 = scalar_2_inv * sig_a_zero.1 + scalar_2_inv * sig_zero_a.1; + let forged_signature = Signature(h0, h1); + let a_half = a * scalar_2_inv; + let new_plaintext = vec![&a_half, &a_half]; + + // The forged signature should not pass verification + assert!(!verify( + ¶ms, + validator_keypair.verification_key(), + &new_plaintext, + &forged_signature + )); + + //#4 + let scalar_3 = Scalar::one() + Scalar::one() + Scalar::one(); + let scalar_4 = Scalar::one() + Scalar::one() + Scalar::one() + Scalar::one(); + let scalar_4_inv = Scalar::invert(&scalar_4).unwrap(); + let scalar_3_over_4 = scalar_3 * scalar_4_inv; + + // Removed unnecessary references + let h1_2 = scalar_4_inv * sig_a_zero.1 + scalar_3_over_4 * sig_zero_a.1; + let forged_signature_2 = Signature(h0, h1_2); + let a_quarter = a * scalar_4_inv; + let a_3_over_4 = a * scalar_3_over_4; + let new_plaintext_2 = vec![&a_quarter, &a_3_over_4]; + + // The second forged signature should also not pass verification + assert!(!verify( + ¶ms, + validator_keypair.verification_key(), + &new_plaintext_2, + &forged_signature_2 + )); + } } diff --git a/common/nymsphinx/chunking/src/lib.rs b/common/nymsphinx/chunking/src/lib.rs index 8f26757099..802d2325a6 100644 --- a/common/nymsphinx/chunking/src/lib.rs +++ b/common/nymsphinx/chunking/src/lib.rs @@ -1,11 +1,8 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::sync::LazyLock; - use crate::fragment::{linked_fragment_payload_max_len, unlinked_fragment_payload_max_len}; -use dashmap::DashMap; -use fragment::{Fragment, FragmentHeader}; +use fragment::FragmentHeader; use nym_crypto::asymmetric::ed25519::PublicKey; use serde::Serialize; pub use set::split_into_sets; @@ -29,6 +26,59 @@ pub mod fragment; pub mod reconstruction; pub mod set; +pub mod monitoring { + use crate::fragment::Fragment; + use crate::{ReceivedFragment, SentFragment}; + use dashmap::DashMap; + use nym_crypto::asymmetric::ed25519::PublicKey; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::LazyLock; + + pub static ENABLED: AtomicBool = AtomicBool::new(false); + + pub static FRAGMENTS_RECEIVED: LazyLock>> = + LazyLock::new(DashMap::new); + + pub static FRAGMENTS_SENT: LazyLock>> = + LazyLock::new(DashMap::new); + + pub fn enable() { + ENABLED.store(true, Ordering::Relaxed) + } + + pub fn enabled() -> bool { + ENABLED.load(Ordering::Relaxed) + } + + #[macro_export] + macro_rules! now { + () => { + match std::time::SystemTime::now().duration_since(std::time::SystemTime::UNIX_EPOCH) { + Ok(n) => n.as_secs(), + Err(_) => 0, + } + }; + } + + pub fn fragment_received(fragment: &Fragment) { + if enabled() { + let id = fragment.fragment_identifier().set_id(); + let mut entry = FRAGMENTS_RECEIVED.entry(id).or_default(); + let r = ReceivedFragment::new(fragment.header(), now!()); + entry.push(r); + } + } + + pub fn fragment_sent(fragment: &Fragment, client_nonce: i32, destination: PublicKey, hops: u8) { + if enabled() { + let id = fragment.fragment_identifier().set_id(); + let mut entry = FRAGMENTS_SENT.entry(id).or_default(); + let s = SentFragment::new(fragment.header(), now!(), client_nonce, destination, hops); + entry.push(s); + } + } +} + #[derive(Debug, Clone)] pub struct FragmentMixParams { destination: PublicKey, @@ -112,35 +162,6 @@ impl ReceivedFragment { } } -pub static FRAGMENTS_RECEIVED: LazyLock>> = - LazyLock::new(DashMap::new); - -pub static FRAGMENTS_SENT: LazyLock>> = LazyLock::new(DashMap::new); - -#[macro_export] -macro_rules! now { - () => { - match std::time::SystemTime::now().duration_since(std::time::SystemTime::UNIX_EPOCH) { - Ok(n) => n.as_secs(), - Err(_) => 0, - } - }; -} - -pub fn fragment_received(fragment: &Fragment) { - let id = fragment.fragment_identifier().set_id(); - let mut entry = FRAGMENTS_RECEIVED.entry(id).or_default(); - let r = ReceivedFragment::new(fragment.header(), now!()); - entry.push(r); -} - -pub fn fragment_sent(fragment: &Fragment, client_nonce: i32, destination: PublicKey, hops: u8) { - let id = fragment.fragment_identifier().set_id(); - let mut entry = FRAGMENTS_SENT.entry(id).or_default(); - let s = SentFragment::new(fragment.header(), now!(), client_nonce, destination, hops); - entry.push(s); -} - /// The idea behind the process of chunking is to incur as little data overhead as possible due /// to very computationally costly sphinx encapsulation procedure. /// diff --git a/common/nymsphinx/chunking/src/reconstruction.rs b/common/nymsphinx/chunking/src/reconstruction.rs index 13383b24ee..db098c3222 100644 --- a/common/nymsphinx/chunking/src/reconstruction.rs +++ b/common/nymsphinx/chunking/src/reconstruction.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::fragment::Fragment; -use crate::{fragment_received, ChunkingError}; +use crate::{monitoring, ChunkingError}; use log::*; use std::collections::HashMap; @@ -110,7 +110,7 @@ impl ReconstructionBuffer { } }); - fragment_received(&fragment); + monitoring::fragment_received(&fragment); let fragment_index = fragment.current_fragment() as usize - 1; if self.fragments[fragment_index].is_some() { diff --git a/common/nymsphinx/framing/Cargo.toml b/common/nymsphinx/framing/Cargo.toml index 250c0dfd4f..03cff4ca9d 100644 --- a/common/nymsphinx/framing/Cargo.toml +++ b/common/nymsphinx/framing/Cargo.toml @@ -11,7 +11,14 @@ repository = { workspace = true } bytes = { workspace = true } tokio-util = { workspace = true, features = ["codec"] } thiserror = { workspace = true } +log = { workspace = true } nym-sphinx-types = { path = "../types", features = ["sphinx", "outfox"] } nym-sphinx-params = { path = "../params", features = ["sphinx", "outfox"] } +nym-sphinx-forwarding = { path = "../forwarding" } +nym-metrics = { path = "../../nym-metrics" } +nym-sphinx-addressing = { path = "../addressing" } +nym-sphinx-acknowledgements = { path = "../acknowledgements" } +[dev-dependencies] +tokio = { workspace = true, features = ["full"] } diff --git a/common/nymsphinx/framing/src/lib.rs b/common/nymsphinx/framing/src/lib.rs index 6c3fe39018..c0da04d8c2 100644 --- a/common/nymsphinx/framing/src/lib.rs +++ b/common/nymsphinx/framing/src/lib.rs @@ -3,3 +3,4 @@ pub mod codec; pub mod packet; +pub mod processing; diff --git a/common/nymsphinx/framing/src/processing.rs b/common/nymsphinx/framing/src/processing.rs new file mode 100644 index 0000000000..f9dd02dcfb --- /dev/null +++ b/common/nymsphinx/framing/src/processing.rs @@ -0,0 +1,284 @@ +use log::{debug, error, info, trace}; +use nym_sphinx_acknowledgements::surb_ack::{SurbAck, SurbAckRecoveryError}; +use nym_sphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; +use nym_sphinx_params::{PacketSize, PacketType}; +use nym_sphinx_types::{ + Delay as SphinxDelay, DestinationAddressBytes, NodeAddressBytes, NymPacket, NymPacketError, + NymProcessedPacket, OutfoxError, PrivateKey, ProcessedPacket, SphinxError, +}; +use thiserror::Error; + +use crate::packet::FramedNymPacket; +use nym_metrics::nanos; +use nym_sphinx_forwarding::packet::MixPacket; + +#[derive(Debug)] +pub enum MixProcessingResult { + /// Contains unwrapped data that should first get delayed before being sent to next hop. + ForwardHop(MixPacket, Option), + + /// Contains all data extracted out of the final hop packet that could be forwarded to the destination. + FinalHop(ProcessedFinalHop), +} + +type ForwardAck = MixPacket; + +#[derive(Debug)] +pub struct ProcessedFinalHop { + pub destination: DestinationAddressBytes, + pub forward_ack: Option, + pub message: Vec, +} + +#[derive(Debug, Error)] +pub enum PacketProcessingError { + #[error("failed to process received packet: {0}")] + NymPacketProcessingError(#[from] NymPacketError), + + #[error("failed to process received sphinx packet: {0}")] + SphinxProcessingError(#[from] SphinxError), + + #[error("the forward hop address was malformed: {0}")] + InvalidForwardHopAddress(#[from] NymNodeRoutingAddressError), + + #[error("the final hop did not contain a SURB-Ack")] + NoSurbAckInFinalHop, + + #[error("failed to recover the expected SURB-Ack packet: {0}")] + MalformedSurbAck(#[from] SurbAckRecoveryError), + + #[error("the received packet was set to use the very old and very much deprecated 'VPN' mode")] + ReceivedOldTypeVpnPacket, + + #[error("failed to process received outfox packet: {0}")] + OutfoxProcessingError(#[from] OutfoxError), +} + +pub fn process_framed_packet( + received: FramedNymPacket, + sphinx_key: &PrivateKey, +) -> Result { + nanos!("process_received", { + let packet_size = received.packet_size(); + let packet_type = received.packet_type(); + + // unwrap the sphinx packet and if possible and appropriate, cache keys + let processed_packet = perform_framed_unwrapping(received, sphinx_key)?; + + // for forward packets, extract next hop and set delay (but do NOT delay here) + // for final packets, extract SURBAck + let final_processing_result = + perform_final_processing(processed_packet, packet_size, packet_type); + + if final_processing_result.is_err() { + error!("{:?}", final_processing_result) + } + + final_processing_result + }) +} + +fn perform_framed_unwrapping( + received: FramedNymPacket, + sphinx_key: &PrivateKey, +) -> Result { + nanos!("perform_initial_unwrapping", { + let packet = received.into_inner(); + perform_framed_packet_processing(packet, sphinx_key) + }) +} + +fn perform_framed_packet_processing( + packet: NymPacket, + sphinx_key: &PrivateKey, +) -> Result { + nanos!("perform_initial_packet_processing", { + packet.process(sphinx_key).map_err(|err| { + debug!("Failed to unwrap NymPacket packet: {err}"); + PacketProcessingError::NymPacketProcessingError(err) + }) + }) +} + +fn perform_final_processing( + packet: NymProcessedPacket, + packet_size: PacketSize, + packet_type: PacketType, +) -> Result { + match packet { + NymProcessedPacket::Sphinx(packet) => { + match packet { + ProcessedPacket::ForwardHop(packet, address, delay) => { + process_forward_hop(NymPacket::Sphinx(*packet), address, delay, packet_type) + } + // right now there's no use for the surb_id included in the header - probably it should get removed from the + // sphinx all together? + ProcessedPacket::FinalHop(destination, _, payload) => process_final_hop( + destination, + payload.recover_plaintext()?, + packet_size, + packet_type, + ), + } + } + NymProcessedPacket::Outfox(packet) => { + let next_address = *packet.next_address(); + let packet = packet.into_packet(); + if packet.is_final_hop() { + process_final_hop( + DestinationAddressBytes::from_bytes(next_address), + packet.recover_plaintext()?.to_vec(), + packet_size, + packet_type, + ) + } else { + let mix_packet = MixPacket::new( + NymNodeRoutingAddress::try_from_bytes(&next_address)?, + NymPacket::Outfox(packet), + PacketType::Outfox, + ); + Ok(MixProcessingResult::ForwardHop(mix_packet, None)) + } + } + } +} + +fn process_final_hop( + destination: DestinationAddressBytes, + payload: Vec, + packet_size: PacketSize, + packet_type: PacketType, +) -> Result { + let (forward_ack, message) = split_into_ack_and_message(payload, packet_size, packet_type)?; + + Ok(MixProcessingResult::FinalHop(ProcessedFinalHop { + destination, + forward_ack, + message, + })) +} + +fn split_into_ack_and_message( + data: Vec, + packet_size: PacketSize, + packet_type: PacketType, +) -> Result<(Option, Vec), PacketProcessingError> { + match packet_size { + PacketSize::AckPacket | PacketSize::OutfoxAckPacket => { + trace!("received an ack packet!"); + Ok((None, data)) + } + PacketSize::RegularPacket + | PacketSize::ExtendedPacket8 + | PacketSize::ExtendedPacket16 + | PacketSize::ExtendedPacket32 + | PacketSize::OutfoxRegularPacket => { + trace!("received a normal packet!"); + let (ack_data, message) = split_hop_data_into_ack_and_message(data, packet_type)?; + let (ack_first_hop, ack_packet) = + match SurbAck::try_recover_first_hop_packet(&ack_data, packet_type) { + Ok((first_hop, packet)) => (first_hop, packet), + Err(err) => { + info!("Failed to recover first hop from ack data: {err}"); + return Err(err.into()); + } + }; + let forward_ack = MixPacket::new(ack_first_hop, ack_packet, packet_type); + Ok((Some(forward_ack), message)) + } + } +} + +fn split_hop_data_into_ack_and_message( + mut extracted_data: Vec, + packet_type: PacketType, +) -> Result<(Vec, Vec), PacketProcessingError> { + let ack_len = SurbAck::len(Some(packet_type)); + + // in theory it's impossible for this to fail since it managed to go into correct `match` + // branch at the caller + if extracted_data.len() < ack_len { + return Err(PacketProcessingError::NoSurbAckInFinalHop); + } + + let message = extracted_data.split_off(ack_len); + let ack_data = extracted_data; + Ok((ack_data, message)) +} + +fn process_forward_hop( + packet: NymPacket, + forward_address: NodeAddressBytes, + delay: SphinxDelay, + packet_type: PacketType, +) -> Result { + let next_hop_address = NymNodeRoutingAddress::try_from(forward_address)?; + + let mix_packet = MixPacket::new(next_hop_address, packet, packet_type); + Ok(MixProcessingResult::ForwardHop(mix_packet, Some(delay))) +} + +// TODO: what more could we realistically test here? +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn splitting_hop_data_works_for_sufficiently_long_payload() { + let short_data = vec![42u8]; + assert!(split_hop_data_into_ack_and_message(short_data, PacketType::Mix).is_err()); + + let sufficient_data = vec![42u8; SurbAck::len(Some(PacketType::Mix))]; + let (ack, data) = + split_hop_data_into_ack_and_message(sufficient_data.clone(), PacketType::Mix).unwrap(); + assert_eq!(sufficient_data, ack); + assert!(data.is_empty()); + + let long_data: Vec = vec![42u8; SurbAck::len(Some(PacketType::Mix)) * 5]; + let (ack, data) = split_hop_data_into_ack_and_message(long_data, PacketType::Mix).unwrap(); + assert_eq!(ack.len(), SurbAck::len(Some(PacketType::Mix))); + assert_eq!(data.len(), SurbAck::len(Some(PacketType::Mix)) * 4) + } + + #[tokio::test] + async fn splitting_hop_data_works_for_sufficiently_long_payload_outfox() { + let short_data = vec![42u8]; + assert!(split_hop_data_into_ack_and_message(short_data, PacketType::Outfox).is_err()); + + let sufficient_data = vec![42u8; SurbAck::len(Some(PacketType::Outfox))]; + let (ack, data) = + split_hop_data_into_ack_and_message(sufficient_data.clone(), PacketType::Outfox) + .unwrap(); + assert_eq!(sufficient_data, ack); + assert!(data.is_empty()); + + let long_data = vec![42u8; SurbAck::len(Some(PacketType::Outfox)) * 5]; + let (ack, data) = + split_hop_data_into_ack_and_message(long_data, PacketType::Outfox).unwrap(); + assert_eq!(ack.len(), SurbAck::len(Some(PacketType::Outfox))); + assert_eq!(data.len(), SurbAck::len(Some(PacketType::Outfox)) * 4) + } + + #[tokio::test] + async fn splitting_into_ack_and_message_returns_whole_data_for_ack() { + let data = vec![42u8; SurbAck::len(Some(PacketType::Mix)) + 10]; + let (ack, message) = + split_into_ack_and_message(data.clone(), PacketSize::AckPacket, PacketType::Mix) + .unwrap(); + assert!(ack.is_none()); + assert_eq!(data, message) + } + + #[tokio::test] + async fn splitting_into_ack_and_message_returns_whole_data_for_ack_outfox() { + let data = vec![42u8; SurbAck::len(Some(PacketType::Outfox)) + 10]; + let (ack, message) = split_into_ack_and_message( + data.clone(), + PacketSize::OutfoxAckPacket, + PacketType::Outfox, + ) + .unwrap(); + assert!(ack.is_none()); + assert_eq!(data, message) + } +} diff --git a/common/nymsphinx/src/preparer/mod.rs b/common/nymsphinx/src/preparer/mod.rs index f7d5e0d256..8578b655b0 100644 --- a/common/nymsphinx/src/preparer/mod.rs +++ b/common/nymsphinx/src/preparer/mod.rs @@ -12,15 +12,15 @@ use nym_sphinx_addressing::clients::Recipient; use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; use nym_sphinx_anonymous_replies::reply_surb::ReplySurb; use nym_sphinx_chunking::fragment::{Fragment, FragmentIdentifier}; -use nym_sphinx_chunking::fragment_sent; use nym_sphinx_forwarding::packet::MixPacket; use nym_sphinx_params::packet_sizes::PacketSize; use nym_sphinx_params::{PacketType, ReplySurbKeyDigestAlgorithm, DEFAULT_NUM_MIX_HOPS}; use nym_sphinx_types::{Delay, NymPacket}; use nym_topology::{NymTopology, NymTopologyError}; use rand::{CryptoRng, Rng, SeedableRng}; -use rand_chacha::ChaCha20Rng; +use rand_chacha::ChaCha8Rng; +use nym_sphinx_chunking::monitoring; use std::time::Duration; pub(crate) mod payload; @@ -51,6 +51,7 @@ impl From for MixPacket { pub trait FragmentPreparer { type Rng: CryptoRng + Rng; + fn deterministic_route_selection(&self) -> bool; fn rng(&mut self) -> &mut Self::Rng; fn nonce(&self) -> i32; fn num_mix_hops(&self) -> u8; @@ -201,12 +202,10 @@ pub trait FragmentPreparer { // could perform diffie-hellman with its own keys followed by a kdf to re-derive // the packet encryption key - let seed = fragment.seed().wrapping_mul(self.nonce()); - let mut rng = ChaCha20Rng::seed_from_u64(seed as u64); - + let fragment_header = fragment.header(); let destination = packet_recipient.gateway(); let hops = mix_hops.unwrap_or(self.num_mix_hops()); - fragment_sent(&fragment, self.nonce(), *destination, hops); + monitoring::fragment_sent(&fragment, self.nonce(), *destination, hops); let non_reply_overhead = encryption::PUBLIC_KEY_SIZE; let expected_plaintext = match packet_type { @@ -241,8 +240,18 @@ pub trait FragmentPreparer { }; // generate pseudorandom route for the packet - log::trace!("Preparing chunk for sending with {} mix hops", hops); - let route = topology.random_route_to_gateway(&mut rng, hops, destination)?; + log::trace!("Preparing chunk for sending with {hops} mix hops"); + let route = if self.deterministic_route_selection() { + log::trace!("using deterministic route selection"); + let seed = fragment_header.seed().wrapping_mul(self.nonce()); + let mut rng = ChaCha8Rng::seed_from_u64(seed as u64); + topology.random_route_to_gateway(&mut rng, hops, destination)? + } else { + log::trace!("using pseudorandom route selection"); + let mut rng = self.rng(); + topology.random_route_to_gateway(&mut rng, hops, destination)? + }; + let destination = packet_recipient.as_sphinx_destination(); // including set of delays @@ -313,6 +322,9 @@ pub struct MessagePreparer { /// Instance of a cryptographically secure random number generator. rng: R, + /// Specify whether route selection should be determined by the packet header. + deterministic_route_selection: bool, + /// Address of this client which also represent an address to which all acknowledgements /// and surb-based are going to be sent. sender_address: Recipient, @@ -336,6 +348,7 @@ where { pub fn new( rng: R, + deterministic_route_selection: bool, sender_address: Recipient, average_packet_delay: Duration, average_ack_delay: Duration, @@ -344,6 +357,7 @@ where let nonce = rng.gen(); MessagePreparer { rng, + deterministic_route_selection, sender_address, average_packet_delay, average_ack_delay, @@ -457,10 +471,18 @@ where impl FragmentPreparer for MessagePreparer { type Rng = R; + fn deterministic_route_selection(&self) -> bool { + self.deterministic_route_selection + } + fn rng(&mut self) -> &mut Self::Rng { &mut self.rng } + fn nonce(&self) -> i32 { + self.nonce + } + fn num_mix_hops(&self) -> u8 { self.num_mix_hops } @@ -472,10 +494,6 @@ impl FragmentPreparer for MessagePreparer { fn average_ack_delay(&self) -> Duration { self.average_ack_delay } - - fn nonce(&self) -> i32 { - self.nonce - } } /* diff --git a/common/nymsphinx/src/receiver.rs b/common/nymsphinx/src/receiver.rs index ac1857fd82..8def09db67 100644 --- a/common/nymsphinx/src/receiver.rs +++ b/common/nymsphinx/src/receiver.rs @@ -220,7 +220,7 @@ impl Default for SphinxMessageReceiver { mod message_receiver { use super::*; use nym_crypto::asymmetric::identity; - use nym_mixnet_contract_common::Layer; + use nym_mixnet_contract_common::LegacyMixLayer; use nym_topology::{gateway, mix, NymTopology}; use std::collections::BTreeMap; @@ -233,9 +233,8 @@ mod message_receiver { let mut mixes = BTreeMap::new(); mixes.insert( 1, - vec![mix::Node { + vec![mix::LegacyNode { mix_id: 123, - owner: None, host: "10.20.30.40".parse().unwrap(), mix_host: "10.20.30.40:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( @@ -246,16 +245,15 @@ mod message_receiver { "B3GzG62aXAZNg14RoMCp3BhELNBrySLr2JqrwyfYFzRc", ) .unwrap(), - layer: Layer::One, + layer: LegacyMixLayer::One, version: "0.8.0-dev".into(), }], ); mixes.insert( 2, - vec![mix::Node { + vec![mix::LegacyNode { mix_id: 234, - owner: None, host: "11.21.31.41".parse().unwrap(), mix_host: "11.21.31.41:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( @@ -266,16 +264,15 @@ mod message_receiver { "5Z1VqYwM2xeKxd8H7fJpGWasNiDFijYBAee7MErkZ5QT", ) .unwrap(), - layer: Layer::Two, + layer: LegacyMixLayer::Two, version: "0.8.0-dev".into(), }], ); mixes.insert( 3, - vec![mix::Node { + vec![mix::LegacyNode { mix_id: 456, - owner: None, host: "12.22.32.42".parse().unwrap(), mix_host: "12.22.32.42:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( @@ -286,7 +283,7 @@ mod message_receiver { "9EyjhCggr2QEA2nakR88YHmXgpy92DWxoe2draDRkYof", ) .unwrap(), - layer: Layer::Three, + layer: LegacyMixLayer::Three, version: "0.8.0-dev".into(), }], ); @@ -294,8 +291,8 @@ mod message_receiver { NymTopology::new( // currently coco_nodes don't really exist so this is still to be determined mixes, - vec![gateway::Node { - owner: None, + vec![gateway::LegacyNode { + node_id: 789, host: "1.2.3.4".parse().unwrap(), mix_host: "1.2.3.4:1789".parse().unwrap(), clients_ws_port: 9000, diff --git a/common/nyxd-scraper/src/block_processor/mod.rs b/common/nyxd-scraper/src/block_processor/mod.rs index 2b1748b633..acb0cbc375 100644 --- a/common/nyxd-scraper/src/block_processor/mod.rs +++ b/common/nyxd-scraper/src/block_processor/mod.rs @@ -10,6 +10,7 @@ use crate::rpc_client::RpcClient; use crate::storage::{persist_block, ScraperStorage}; use crate::PruningOptions; use futures::StreamExt; +use std::cmp::max; use std::collections::{BTreeMap, HashSet, VecDeque}; use std::ops::{Add, Range}; use std::sync::Arc; @@ -99,7 +100,15 @@ impl BlockProcessor { }) } - async fn process_block(&mut self, block: BlockToProcess) -> Result<(), ScraperError> { + pub fn with_pruning(mut self, pruning_options: PruningOptions) -> Self { + self.pruning_options = pruning_options; + self + } + + pub(super) async fn process_block( + &mut self, + block: BlockToProcess, + ) -> Result<(), ScraperError> { info!("processing block at height {}", block.height); let full_info = self.rpc_client.try_get_full_details(block).await?; @@ -169,6 +178,10 @@ impl BlockProcessor { self.msg_modules = modules; } + pub(super) fn last_process_height(&self) -> u32 { + self.last_processed_height + } + async fn maybe_request_missing_blocks(&mut self) -> Result<(), ScraperError> { // we're still processing, so we're good if self.last_processed_at.elapsed() < MAX_MISSING_BLOCKS_DELAY { @@ -254,6 +267,7 @@ impl BlockProcessor { } if to_prune == 0 { + self.last_pruned_height = self.last_processed_height; return Ok(()); } @@ -353,7 +367,14 @@ impl BlockProcessor { self.maybe_prune_storage().await?; let latest_block = self.rpc_client.current_block_height().await? as u32; + if latest_block > self.last_processed_height && self.last_processed_height != 0 { + // in case we were offline for a while, + // make sure we don't request blocks we'd have to prune anyway + let keep_recent = self.pruning_options.strategy_keep_recent(); + let last_to_keep = latest_block - keep_recent; + self.last_processed_height = max(self.last_processed_height, last_to_keep); + let request_range = self.last_processed_height + 1..latest_block + 1; info!("we need to request {request_range:?} to resync"); self.request_missing_blocks(request_range).await?; diff --git a/common/nyxd-scraper/src/error.rs b/common/nyxd-scraper/src/error.rs index 079a28975d..9876536aa5 100644 --- a/common/nyxd-scraper/src/error.rs +++ b/common/nyxd-scraper/src/error.rs @@ -16,7 +16,7 @@ pub enum ScraperError { #[error("failed to perform startup SQL migration: {0}")] StartupMigrationFailure(#[from] sqlx::migrate::MigrateError), - #[error("can't add any modules to the scraper as it's already running")] + #[error("the block scraper is already running")] ScraperAlreadyRunning, #[error("failed to establish websocket connection to {url}: {source}")] diff --git a/common/nyxd-scraper/src/scraper/mod.rs b/common/nyxd-scraper/src/scraper/mod.rs index 608731b489..1b3294c914 100644 --- a/common/nyxd-scraper/src/scraper/mod.rs +++ b/common/nyxd-scraper/src/scraper/mod.rs @@ -1,21 +1,25 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::block_processor::types::BlockToProcess; use crate::block_processor::BlockProcessor; -use crate::block_requester::BlockRequester; +use crate::block_requester::{BlockRequest, BlockRequester}; use crate::error::ScraperError; use crate::modules::{BlockModule, MsgModule, TxModule}; use crate::rpc_client::RpcClient; use crate::scraper::subscriber::ChainSubscriber; use crate::storage::ScraperStorage; use crate::PruningOptions; +use futures::future::join_all; use std::path::PathBuf; use std::sync::Arc; -use tokio::sync::mpsc::{channel, unbounded_channel}; +use tokio::sync::mpsc::{ + channel, unbounded_channel, Receiver, Sender, UnboundedReceiver, UnboundedSender, +}; use tokio::sync::Notify; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker; -use tracing::info; +use tracing::{error, info}; use url::Url; mod subscriber; @@ -115,6 +119,7 @@ pub struct NyxdScraper { cancel_token: CancellationToken, startup_sync: Arc, pub storage: ScraperStorage, + rpc_client: RpcClient, } impl NyxdScraper { @@ -125,6 +130,7 @@ impl NyxdScraper { pub async fn new(config: Config) -> Result { config.pruning_options.validate()?; let storage = ScraperStorage::init(&config.database_path).await?; + let rpc_client = RpcClient::new(&config.rpc_url)?; Ok(NyxdScraper { config, @@ -132,6 +138,7 @@ impl NyxdScraper { cancel_token: CancellationToken::new(), startup_sync: Arc::new(Default::default()), storage, + rpc_client, }) } @@ -151,36 +158,156 @@ impl NyxdScraper { self.task_tracker.close(); } - pub async fn start(&self) -> Result<(), ScraperError> { - let (processing_tx, processing_rx) = unbounded_channel(); - let (req_tx, req_rx) = channel(5); + pub async fn process_single_block(&self, height: u32) -> Result<(), ScraperError> { + info!(height = height, "attempting to process a single block"); + if !self.task_tracker.is_empty() { + return Err(ScraperError::ScraperAlreadyRunning); + } - let rpc_client = RpcClient::new(&self.config.rpc_url)?; + let (_, processing_rx) = unbounded_channel(); + let (req_tx, _) = channel(5); - // create the tasks - let block_requester = BlockRequester::new( + let mut block_processor = self + .new_block_processor(req_tx.clone(), processing_rx) + .await? + .with_pruning(PruningOptions::nothing()); + + let block = self.rpc_client.get_basic_block_details(height).await?; + + block_processor.process_block(block.into()).await + } + + pub async fn process_block_range( + &self, + starting_height: Option, + end_height: Option, + ) -> Result<(), ScraperError> { + if !self.task_tracker.is_empty() { + return Err(ScraperError::ScraperAlreadyRunning); + } + + let (_, processing_rx) = unbounded_channel(); + let (req_tx, _) = channel(5); + + let mut block_processor = self + .new_block_processor(req_tx.clone(), processing_rx) + .await? + .with_pruning(PruningOptions::nothing()); + + let current_height = self.rpc_client.current_block_height().await? as u32; + let last_processed = block_processor.last_process_height(); + + let starting_height = match starting_height { + // always attempt to use whatever the user has provided + Some(explicit) => explicit, + None => { + // otherwise, attempt to resume where we last stopped + // and if we haven't processed anything, start from the current height + if last_processed != 0 { + last_processed + } else { + current_height + } + } + }; + + let end_height = match end_height { + // always attempt to use whatever the user has provided + Some(explicit) => explicit, + None => { + // otherwise, attempt to either go from the start height to the height right + // before the final processed block held in the storage (in case there are gaps) + // or finally, just go to the current block height + if last_processed > starting_height { + last_processed - 1 + } else { + current_height + } + } + }; + + info!( + starting_height = starting_height, + end_height = end_height, + "attempting to process block range" + ); + + let range = (starting_height..=end_height).collect::>(); + + // the most likely bottleneck here are going to be the chain queries, + // so batch multiple requests + for batch in range.chunks(4) { + let batch_result = join_all( + batch + .iter() + .map(|height| self.rpc_client.get_basic_block_details(*height)), + ) + .await; + for result in batch_result { + match result { + Ok(block) => block_processor.process_block(block.into()).await?, + Err(err) => { + error!("failed to retrieve the block: {err}. stopping..."); + return Err(err); + } + } + } + } + + Ok(()) + } + + fn new_block_requester( + &self, + req_rx: Receiver, + processing_tx: UnboundedSender, + ) -> BlockRequester { + BlockRequester::new( self.cancel_token.clone(), - rpc_client.clone(), + self.rpc_client.clone(), req_rx, processing_tx.clone(), - ); - let block_processor = BlockProcessor::new( + ) + } + + async fn new_block_processor( + &self, + req_tx: Sender, + processing_rx: UnboundedReceiver, + ) -> Result { + BlockProcessor::new( self.config.pruning_options, self.cancel_token.clone(), self.startup_sync.clone(), processing_rx, req_tx, self.storage.clone(), - rpc_client, + self.rpc_client.clone(), ) - .await?; - let chain_subscriber = ChainSubscriber::new( + .await + } + + async fn new_chain_subscriber( + &self, + processing_tx: UnboundedSender, + ) -> Result { + ChainSubscriber::new( &self.config.websocket_url, self.cancel_token.clone(), self.task_tracker.clone(), processing_tx, ) - .await?; + .await + } + + pub async fn start(&self) -> Result<(), ScraperError> { + let (processing_tx, processing_rx) = unbounded_channel(); + let (req_tx, req_rx) = channel(5); + + // create the tasks + let block_requester = self.new_block_requester(req_rx, processing_tx.clone()); + let block_processor = self.new_block_processor(req_tx, processing_rx).await?; + let chain_subscriber = self.new_chain_subscriber(processing_tx).await?; // spawn them self.start_tasks(block_requester, block_processor, chain_subscriber); diff --git a/common/nyxd-scraper/src/scraper/subscriber.rs b/common/nyxd-scraper/src/scraper/subscriber.rs index 461d362eb5..c2b5bdbbfd 100644 --- a/common/nyxd-scraper/src/scraper/subscriber.rs +++ b/common/nyxd-scraper/src/scraper/subscriber.rs @@ -16,7 +16,7 @@ use url::Url; const MAX_FAILURES: usize = 10; const MAX_RECONNECTION_ATTEMPTS: usize = 8; -const SOCKET_FAILURE_RESET: Duration = Duration::hours(2); +const SOCKET_FAILURE_RESET: Duration = Duration::minutes(15); pub struct ChainSubscriber { cancel: CancellationToken, diff --git a/common/nyxd-scraper/src/storage/manager.rs b/common/nyxd-scraper/src/storage/manager.rs index 04af3e364b..fb40a065b8 100644 --- a/common/nyxd-scraper/src/storage/manager.rs +++ b/common/nyxd-scraper/src/storage/manager.rs @@ -435,9 +435,12 @@ where trace!("update_last_processed"); let start = Instant::now(); - sqlx::query!("UPDATE metadata SET last_processed_height = ?", height) - .execute(executor) - .await?; + sqlx::query!( + "UPDATE metadata SET last_processed_height = MAX(last_processed_height, ?)", + height + ) + .execute(executor) + .await?; log_db_operation_time("update_last_processed", start); Ok(()) diff --git a/common/nyxd-scraper/src/storage/mod.rs b/common/nyxd-scraper/src/storage/mod.rs index c9ecd64d40..8ac0f07775 100644 --- a/common/nyxd-scraper/src/storage/mod.rs +++ b/common/nyxd-scraper/src/storage/mod.rs @@ -1,20 +1,24 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::block_processor::types::{FullBlockInformation, ParsedTransactionResponse}; -use crate::error::ScraperError; -use crate::storage::manager::{ - insert_block, insert_message, insert_precommit, insert_transaction, insert_validator, - prune_blocks, prune_messages, prune_pre_commits, prune_transactions, update_last_processed, - update_last_pruned, StorageManager, +use crate::{ + block_processor::types::{FullBlockInformation, ParsedTransactionResponse}, + error::ScraperError, + storage::{ + manager::{ + insert_block, insert_message, insert_precommit, insert_transaction, insert_validator, + prune_blocks, prune_messages, prune_pre_commits, prune_transactions, + update_last_processed, update_last_pruned, StorageManager, + }, + models::{CommitSignature, Validator}, + }, +}; +use sqlx::{types::time::OffsetDateTime, ConnectOptions, Sqlite, Transaction}; +use std::{fmt::Debug, path::Path}; +use tendermint::{ + block::{Commit, CommitSig}, + Block, }; -use crate::storage::models::{CommitSignature, Validator}; -use sqlx::types::time::OffsetDateTime; -use sqlx::{ConnectOptions, Sqlite, Transaction}; -use std::fmt::Debug; -use std::path::Path; -use tendermint::block::{Commit, CommitSig}; -use tendermint::Block; use tendermint_rpc::endpoint::validators; use tokio::time::Instant; use tracing::{debug, error, info, instrument, trace, warn}; @@ -46,14 +50,13 @@ pub(crate) fn log_db_operation_time(op_name: &str, start_time: Instant) { impl ScraperStorage { #[instrument] pub async fn init + Debug>(database_path: P) -> Result { - let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + let opts = sqlx::sqlite::SqliteConnectOptions::new() .filename(database_path) - .create_if_missing(true); + .create_if_missing(true) + .disable_statement_logging(); // TODO: do we want auto_vacuum ? - opts.disable_statement_logging(); - let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { Ok(db) => db, Err(err) => { @@ -90,11 +93,11 @@ impl ScraperStorage { let mut tx = self.begin_processing_tx().await?; - prune_messages(oldest_to_keep.into(), &mut tx).await?; - prune_transactions(oldest_to_keep.into(), &mut tx).await?; - prune_pre_commits(oldest_to_keep.into(), &mut tx).await?; - prune_blocks(oldest_to_keep.into(), &mut tx).await?; - update_last_pruned(current_height.into(), &mut tx).await?; + prune_messages(oldest_to_keep.into(), &mut *tx).await?; + prune_transactions(oldest_to_keep.into(), &mut *tx).await?; + prune_pre_commits(oldest_to_keep.into(), &mut *tx).await?; + prune_blocks(oldest_to_keep.into(), &mut *tx).await?; + update_last_pruned(current_height.into(), &mut *tx).await?; let commit_start = Instant::now(); tx.commit() @@ -234,7 +237,7 @@ pub async fn persist_block( // persist messages (inside the transactions) persist_messages(&block.transactions, tx).await?; - update_last_processed(block.block.header.height.into(), tx).await?; + update_last_processed(block.block.header.height.into(), tx.as_mut()).await?; Ok(()) } @@ -251,7 +254,7 @@ async fn persist_validators( insert_validator( consensus_address.to_string(), consensus_pubkey.to_string(), - &mut *tx, + tx.as_mut(), ) .await?; } @@ -274,7 +277,7 @@ async fn persist_block_data( total_gas, proposer_address, block.header.time.into(), - tx, + tx.as_mut(), ) .await?; Ok(()) @@ -320,7 +323,7 @@ async fn persist_commits( (*timestamp).into(), validator.power.into(), validator.proposer_priority.value(), - &mut *tx, + tx.as_mut(), ) .await?; } @@ -345,7 +348,7 @@ async fn persist_txs( chain_tx.tx_result.gas_wanted, chain_tx.tx_result.gas_used, chain_tx.tx_result.log.clone(), - &mut *tx, + tx.as_mut(), ) .await?; } @@ -366,7 +369,7 @@ async fn persist_messages( index as i64, msg.type_url.clone(), chain_tx.height.into(), - &mut *tx, + tx.as_mut(), ) .await? } diff --git a/common/serde-helpers/Cargo.toml b/common/serde-helpers/Cargo.toml index a45ed0afbd..bc9de1862e 100644 --- a/common/serde-helpers/Cargo.toml +++ b/common/serde-helpers/Cargo.toml @@ -13,11 +13,13 @@ license.workspace = true [dependencies] serde = { workspace = true } +hex = { workspace = true, optional = true } bs58 = { workspace = true, optional = true } base64 = { workspace = true, optional = true } time = { workspace = true, features = ["formatting", "parsing"], optional = true } [features] +hex = ["dep:hex"] bs58 = ["dep:bs58"] base64 = ["dep:base64"] date = ["time"] \ No newline at end of file diff --git a/common/serde-helpers/src/lib.rs b/common/serde-helpers/src/lib.rs index fd37be86b8..07ad83face 100644 --- a/common/serde-helpers/src/lib.rs +++ b/common/serde-helpers/src/lib.rs @@ -32,6 +32,20 @@ pub mod bs58 { } } +#[cfg(feature = "hex")] +pub mod hex { + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(bytes: &[u8], serializer: S) -> Result { + serializer.serialize_str(&::hex::encode(bytes)) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + let s = String::deserialize(deserializer)?; + ::hex::decode(&s).map_err(serde::de::Error::custom) + } +} + #[cfg(feature = "date")] pub mod date { use serde::ser::Error; diff --git a/common/service-provider-requests-common/src/lib.rs b/common/service-provider-requests-common/src/lib.rs index d13a7156c9..f9f0564e1d 100644 --- a/common/service-provider-requests-common/src/lib.rs +++ b/common/service-provider-requests-common/src/lib.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[repr(u8)] pub enum ServiceProviderType { NetworkRequester = 0, diff --git a/common/statistics/Cargo.toml b/common/statistics/Cargo.toml new file mode 100644 index 0000000000..3daba412d5 --- /dev/null +++ b/common/statistics/Cargo.toml @@ -0,0 +1,17 @@ +# Copyright 2024 - Nym Technologies SA +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "nym-statistics-common" +version = "0.1.0" +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +futures = { workspace = true } +time = { workspace = true } + +nym-sphinx = { path = "../nymsphinx" } +nym-credentials-interface = { path = "../credentials-interface" } diff --git a/common/statistics/src/events.rs b/common/statistics/src/events.rs new file mode 100644 index 0000000000..a9b81defd6 --- /dev/null +++ b/common/statistics/src/events.rs @@ -0,0 +1,54 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use futures::channel::mpsc; +use nym_credentials_interface::TicketType; +use nym_sphinx::DestinationAddressBytes; +use time::OffsetDateTime; + +pub type StatsEventSender = mpsc::UnboundedSender; +pub type StatsEventReceiver = mpsc::UnboundedReceiver; +pub enum StatsEvent { + SessionStatsEvent(SessionEvent), +} + +impl StatsEvent { + pub fn new_session_start(client: DestinationAddressBytes) -> StatsEvent { + StatsEvent::SessionStatsEvent(SessionEvent::SessionStart { + start_time: OffsetDateTime::now_utc(), + client, + }) + } + + pub fn new_session_stop(client: DestinationAddressBytes) -> StatsEvent { + StatsEvent::SessionStatsEvent(SessionEvent::SessionStop { + stop_time: OffsetDateTime::now_utc(), + client, + }) + } + + pub fn new_ecash_ticket( + client: DestinationAddressBytes, + ticket_type: TicketType, + ) -> StatsEvent { + StatsEvent::SessionStatsEvent(SessionEvent::EcashTicket { + ticket_type, + client, + }) + } +} + +pub enum SessionEvent { + SessionStart { + start_time: OffsetDateTime, + client: DestinationAddressBytes, + }, + SessionStop { + stop_time: OffsetDateTime, + client: DestinationAddressBytes, + }, + EcashTicket { + ticket_type: TicketType, + client: DestinationAddressBytes, + }, +} diff --git a/common/statistics/src/lib.rs b/common/statistics/src/lib.rs new file mode 100644 index 0000000000..222251db6c --- /dev/null +++ b/common/statistics/src/lib.rs @@ -0,0 +1,4 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod events; diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs index ae20786400..e6f4981560 100644 --- a/common/topology/src/gateway.rs +++ b/common/topology/src/gateway.rs @@ -2,13 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{filter, NetworkAddress, NodeVersion}; -use nym_api_requests::models::DescribedGateway; +use nym_api_requests::nym_nodes::SkimmedNode; use nym_crypto::asymmetric::{encryption, identity}; -use nym_mixnet_contract_common::GatewayBond; +use nym_mixnet_contract_common::NodeId; use nym_sphinx_addressing::nodes::{NodeIdentity, NymNodeRoutingAddress}; use nym_sphinx_types::Node as SphinxNode; - -use nym_api_requests::nym_nodes::SkimmedNode; use rand::seq::SliceRandom; use rand::thread_rng; use std::fmt; @@ -49,7 +47,9 @@ pub enum GatewayConversionError { } #[derive(Clone)] -pub struct Node { +pub struct LegacyNode { + pub node_id: NodeId, + pub host: NetworkAddress, // we're keeping this as separate resolved field since we do not want to be resolving the potential // hostname every time we want to construct a path via this node @@ -65,15 +65,13 @@ pub struct Node { pub sphinx_key: encryption::PublicKey, // TODO: or nymsphinx::PublicKey? both are x25519 // to be removed: - pub owner: Option, pub version: NodeVersion, } -impl std::fmt::Debug for Node { +impl std::fmt::Debug for LegacyNode { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { f.debug_struct("gateway::Node") .field("host", &self.host) - .field("owner", &self.owner) .field("mix_host", &self.mix_host) .field("clients_ws_port", &self.clients_ws_port) .field("clients_wss_port", &self.clients_wss_port) @@ -84,7 +82,7 @@ impl std::fmt::Debug for Node { } } -impl Node { +impl LegacyNode { pub fn parse_host(raw: &str) -> Result { // safety: this conversion is infallible // (but we retain result return type for legacy reasons) @@ -122,25 +120,21 @@ impl Node { } } -impl fmt::Display for Node { +impl fmt::Display for LegacyNode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!( - f, - "Node(id: {}, owner: {:?}, host: {})", - self.identity_key, self.owner, self.host, - ) + write!(f, "legacy gateway {} @ {}", self.node_id, self.host) } } -impl filter::Versioned for Node { +impl filter::Versioned for LegacyNode { fn version(&self) -> String { // TODO: return semver instead self.version.to_string() } } -impl<'a> From<&'a Node> for SphinxNode { - fn from(node: &'a Node) -> Self { +impl<'a> From<&'a LegacyNode> for SphinxNode { + fn from(node: &'a LegacyNode) -> Self { let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host) .try_into() .unwrap(); @@ -149,83 +143,7 @@ impl<'a> From<&'a Node> for SphinxNode { } } -impl<'a> TryFrom<&'a GatewayBond> for Node { - type Error = GatewayConversionError; - - fn try_from(bond: &'a GatewayBond) -> Result { - let host = Self::parse_host(&bond.gateway.host)?; - - // try to completely resolve the host in the mix situation to avoid doing it every - // single time we want to construct a path - let mix_host = Self::extract_mix_host(&host, bond.gateway.mix_port)?; - - Ok(Node { - owner: Some(bond.owner.as_str().to_owned()), - host, - mix_host, - clients_ws_port: bond.gateway.clients_port, - clients_wss_port: None, - identity_key: identity::PublicKey::from_base58_string(&bond.gateway.identity_key)?, - sphinx_key: encryption::PublicKey::from_base58_string(&bond.gateway.sphinx_key)?, - version: bond.gateway.version.as_str().into(), - }) - } -} - -impl TryFrom for Node { - type Error = GatewayConversionError; - - fn try_from(bond: GatewayBond) -> Result { - Node::try_from(&bond) - } -} - -impl<'a> TryFrom<&'a DescribedGateway> for Node { - type Error = GatewayConversionError; - - fn try_from(value: &'a DescribedGateway) -> Result { - let Some(ref self_described) = value.self_described else { - return (&value.bond).try_into(); - }; - - let ips = &self_described.host_information.ip_address; - if ips.is_empty() { - return Err(GatewayConversionError::NoIpAddressesProvided { - gateway: value.bond.gateway.identity_key.clone(), - }); - } - - let host = match &self_described.host_information.hostname { - None => NetworkAddress::IpAddr(ips[0]), - Some(hostname) => NetworkAddress::Hostname(hostname.clone()), - }; - - // get ip from the self-reported values so we wouldn't need to do any hostname resolution - // (which doesn't really work in wasm) - let mix_host = SocketAddr::new(ips[0], value.bond.gateway.mix_port); - - Ok(Node { - owner: Some(value.bond.owner.as_str().to_owned()), - host, - mix_host, - clients_ws_port: self_described.mixnet_websockets.ws_port, - clients_wss_port: self_described.mixnet_websockets.wss_port, - identity_key: identity::PublicKey::from_base58_string( - &self_described.host_information.keys.ed25519, - )?, - sphinx_key: encryption::PublicKey::from_base58_string( - &self_described.host_information.keys.x25519, - )?, - version: self_described - .build_information - .build_version - .as_str() - .into(), - }) - } -} - -impl<'a> TryFrom<&'a SkimmedNode> for Node { +impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode { type Error = GatewayConversionError; fn try_from(value: &'a SkimmedNode) -> Result { @@ -235,7 +153,7 @@ impl<'a> TryFrom<&'a SkimmedNode> for Node { if value.ip_addresses.is_empty() { return Err(GatewayConversionError::NoIpAddressesProvided { - gateway: value.ed25519_identity_pubkey.clone(), + gateway: value.ed25519_identity_pubkey.to_base58_string(), }); } @@ -249,23 +167,15 @@ impl<'a> TryFrom<&'a SkimmedNode> for Node { NetworkAddress::IpAddr(*ip) }; - Ok(Node { + Ok(LegacyNode { + node_id: value.node_id, host, mix_host: SocketAddr::new(*ip, value.mix_port), clients_ws_port: entry_details.ws_port, clients_wss_port: entry_details.wss_port, - identity_key: value.ed25519_identity_pubkey.parse()?, - sphinx_key: value.x25519_sphinx_pubkey.parse()?, - owner: None, + identity_key: value.ed25519_identity_pubkey, + sphinx_key: value.x25519_sphinx_pubkey, version: NodeVersion::Unknown, }) } } - -impl TryFrom for Node { - type Error = GatewayConversionError; - - fn try_from(value: DescribedGateway) -> Result { - Node::try_from(&value) - } -} diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 5630122d0c..6e89cf2753 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -7,17 +7,15 @@ use crate::filter::VersionFilterable; pub use error::NymTopologyError; use log::{debug, info, warn}; -use mix::Node; +use nym_api_requests::nym_nodes::{CachedNodesResponse, SkimmedNode}; use nym_config::defaults::var_names::NYM_API; -use nym_mixnet_contract_common::mixnode::MixNodeDetails; -use nym_mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixId}; +use nym_mixnet_contract_common::{IdentityKeyRef, NodeId}; use nym_sphinx_addressing::nodes::NodeIdentity; use nym_sphinx_types::Node as SphinxNode; use rand::prelude::SliceRandom; use rand::{CryptoRng, Rng}; use std::collections::BTreeMap; use std::convert::Infallible; - use std::fmt::{self, Display, Formatter}; use std::io; use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; @@ -25,7 +23,6 @@ use std::str::FromStr; #[cfg(feature = "serializable")] use ::serde::{Deserialize, Deserializer, Serialize, Serializer}; -use nym_api_requests::models::DescribedGateway; pub mod error; pub mod filter; @@ -118,45 +115,54 @@ impl Display for NetworkAddress { pub type MixLayer = u8; +// the reason for those having `Legacy` prefix is that eventually they should be using +// exactly the same types #[derive(Debug, Clone, Default)] pub struct NymTopology { - mixes: BTreeMap>, - gateways: Vec, + mixes: BTreeMap>, + gateways: Vec, } impl NymTopology { pub async fn new_from_env() -> Result { let api_url = std::env::var(NYM_API)?; - info!("Generating topology from {}", api_url); + info!("Generating topology from {api_url}"); - let mixnodes = reqwest::get(&format!("{}/v1/mixnodes", api_url)) + let mixnodes = reqwest::get(&format!("{api_url}/v1/unstable/nym-nodes/mixnodes/skimmed",)) .await? - .json::>() + .json::>() .await? - .into_iter() - .map(|details| details.bond_information) - .map(mix::Node::try_from) + .nodes + .iter() + .map(mix::LegacyNode::try_from) .filter(Result::is_ok) .collect::, _>>()?; - let gateways = reqwest::get(&format!("{}/v1/gateways", api_url)) + let gateways = reqwest::get(&format!("{api_url}/v1/unstable/nym-nodes/gateways/skimmed",)) .await? - .json::>() + .json::>() .await? - .into_iter() - .map(gateway::Node::try_from) + .nodes + .iter() + .map(gateway::LegacyNode::try_from) .filter(Result::is_ok) .collect::, _>>()?; let topology = NymTopology::new_unordered(mixnodes, gateways); Ok(topology) } - pub fn new(mixes: BTreeMap>, gateways: Vec) -> Self { + pub fn new( + mixes: BTreeMap>, + gateways: Vec, + ) -> Self { NymTopology { mixes, gateways } } - pub fn new_unordered(unordered_mixes: Vec, gateways: Vec) -> Self { + pub fn new_unordered( + unordered_mixes: Vec, + gateways: Vec, + ) -> Self { let mut mixes = BTreeMap::new(); for node in unordered_mixes.into_iter() { let layer = node.layer as MixLayer; @@ -171,10 +177,10 @@ impl NymTopology { where MI: Iterator, GI: Iterator, - G: TryInto, - M: TryInto, - >::Error: Display, - >::Error: Display, + G: TryInto, + M: TryInto, + >::Error: Display, + >::Error: Display, { let mut mixes = BTreeMap::new(); let mut gateways = Vec::new(); @@ -205,14 +211,11 @@ impl NymTopology { serde_json::from_reader(file).map_err(Into::into) } - pub fn from_detailed( - mix_details: Vec, - gateway_bonds: Vec, - ) -> Self { - nym_topology_from_detailed(mix_details, gateway_bonds) + pub fn from_basic(basic_mixes: &[SkimmedNode], basic_gateways: &[SkimmedNode]) -> Self { + nym_topology_from_basic_info(basic_mixes, basic_gateways) } - pub fn find_mix(&self, mix_id: MixId) -> Option<&mix::Node> { + pub fn find_mix(&self, mix_id: NodeId) -> Option<&mix::LegacyNode> { for nodes in self.mixes.values() { for node in nodes { if node.mix_id == mix_id { @@ -223,7 +226,10 @@ impl NymTopology { None } - pub fn find_mix_by_identity(&self, mixnode_identity: IdentityKeyRef) -> Option<&mix::Node> { + pub fn find_mix_by_identity( + &self, + mixnode_identity: IdentityKeyRef, + ) -> Option<&mix::LegacyNode> { for nodes in self.mixes.values() { for node in nodes { if node.identity_key.to_base58_string() == mixnode_identity { @@ -234,13 +240,13 @@ impl NymTopology { None } - pub fn find_gateway(&self, gateway_identity: IdentityKeyRef) -> Option<&gateway::Node> { + pub fn find_gateway(&self, gateway_identity: IdentityKeyRef) -> Option<&gateway::LegacyNode> { self.gateways .iter() .find(|&gateway| gateway.identity_key.to_base58_string() == gateway_identity) } - pub fn mixes(&self) -> &BTreeMap> { + pub fn mixes(&self) -> &BTreeMap> { &self.mixes } @@ -248,8 +254,8 @@ impl NymTopology { self.mixes.values().map(|m| m.len()).sum() } - pub fn mixes_as_vec(&self) -> Vec { - let mut mixes: Vec = vec![]; + pub fn mixes_as_vec(&self) -> Vec { + let mut mixes: Vec = vec![]; for layer in self.mixes().values() { mixes.extend(layer.to_owned()) @@ -257,20 +263,20 @@ impl NymTopology { mixes } - pub fn mixes_in_layer(&self, layer: MixLayer) -> Vec { + pub fn mixes_in_layer(&self, layer: MixLayer) -> Vec { assert!([1, 2, 3].contains(&layer)); self.mixes.get(&layer).unwrap().to_owned() } - pub fn gateways(&self) -> &[gateway::Node] { + pub fn gateways(&self) -> &[gateway::LegacyNode] { &self.gateways } - pub fn get_gateways(&self) -> Vec { + pub fn get_gateways(&self) -> Vec { self.gateways.clone() } - pub fn get_gateway(&self, gateway_identity: &NodeIdentity) -> Option<&gateway::Node> { + pub fn get_gateway(&self, gateway_identity: &NodeIdentity) -> Option<&gateway::LegacyNode> { self.gateways .iter() .find(|gateway| gateway.identity() == gateway_identity) @@ -280,11 +286,15 @@ impl NymTopology { self.get_gateway(gateway_identity).is_some() } - pub fn set_gateways(&mut self, gateways: Vec) { + pub fn insert_gateway(&mut self, gateway: gateway::LegacyNode) { + self.gateways.push(gateway) + } + + pub fn set_gateways(&mut self, gateways: Vec) { self.gateways = gateways } - pub fn random_gateway(&self, rng: &mut R) -> Result<&gateway::Node, NymTopologyError> + pub fn random_gateway(&self, rng: &mut R) -> Result<&gateway::LegacyNode, NymTopologyError> where R: Rng + CryptoRng, { @@ -299,7 +309,7 @@ impl NymTopology { &self, rng: &mut R, num_mix_hops: u8, - ) -> Result, NymTopologyError> + ) -> Result, NymTopologyError> where R: Rng + CryptoRng + ?Sized, { @@ -335,7 +345,7 @@ impl NymTopology { rng: &mut R, num_mix_hops: u8, gateway_identity: &NodeIdentity, - ) -> Result<(Vec, gateway::Node), NymTopologyError> + ) -> Result<(Vec, gateway::LegacyNode), NymTopologyError> where R: Rng + CryptoRng + ?Sized, { @@ -376,7 +386,7 @@ impl NymTopology { } /// Overwrites the existing nodes in the specified layer - pub fn set_mixes_in_layer(&mut self, layer: u8, mixes: Vec) { + pub fn set_mixes_in_layer(&mut self, layer: u8, mixes: Vec) { self.mixes.insert(layer, mixes); } @@ -491,66 +501,33 @@ impl<'de> Deserialize<'de> for NymTopology { } } -pub trait IntoGatewayNode: TryInto -where - >::Error: Display, -{ - fn identity(&self) -> IdentityKeyRef; -} - -impl IntoGatewayNode for GatewayBond { - fn identity(&self) -> IdentityKeyRef { - &self.gateway.identity_key - } -} - -impl IntoGatewayNode for DescribedGateway { - fn identity(&self) -> IdentityKeyRef { - &self.bond.gateway.identity_key - } -} - -pub fn nym_topology_from_detailed( - mix_details: Vec, - gateway_bonds: Vec, -) -> NymTopology -where - G: IntoGatewayNode, - >::Error: Display, -{ +pub fn nym_topology_from_basic_info( + basic_mixes: &[SkimmedNode], + basic_gateways: &[SkimmedNode], +) -> NymTopology { let mut mixes = BTreeMap::new(); - for bond in mix_details - .into_iter() - .map(|details| details.bond_information) - { - let layer = bond.layer as MixLayer; - if layer == 0 || layer > 3 { - warn!( - "{} says it's on invalid layer {layer}!", - bond.mix_node.identity_key - ); + for mix in basic_mixes { + let Some(layer) = mix.get_mix_layer() else { + warn!("node {} doesn't have any assigned mix layer!", mix.node_id); continue; - } - let mix_id = bond.mix_id; - let mix_identity = bond.mix_node.identity_key.clone(); + }; let layer_entry = mixes.entry(layer).or_insert_with(Vec::new); - match bond.try_into() { + match mix.try_into() { Ok(mix) => layer_entry.push(mix), Err(err) => { - warn!("Mix {mix_id} / {mix_identity} is malformed: {err}"); + warn!("node (mixnode) {} is malformed: {err}", mix.node_id); continue; } } } - let mut gateways = Vec::with_capacity(gateway_bonds.len()); - for bond in gateway_bonds.into_iter() { - let gate_id = bond.identity().to_owned(); - match bond.try_into() { + let mut gateways = Vec::with_capacity(basic_gateways.len()); + for gateway in basic_gateways { + match gateway.try_into() { Ok(gate) => gateways.push(gate), Err(err) => { - warn!("Gateway {gate_id} is malformed: {err}"); + warn!("node (gateway) {} is malformed: {err}", gateway.node_id); continue; } } @@ -568,13 +545,12 @@ mod converting_mixes_to_vec { use nym_crypto::asymmetric::{encryption, identity}; use super::*; - use nym_mixnet_contract_common::Layer; + use nym_mixnet_contract_common::LegacyMixLayer; #[test] fn returns_a_vec_with_hashmap_values() { - let node1 = mix::Node { + let node1 = mix::LegacyNode { mix_id: 42, - owner: Some("N/A".to_string()), host: "3.3.3.3".parse().unwrap(), mix_host: "3.3.3.3:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( @@ -585,21 +561,15 @@ mod converting_mixes_to_vec { "C7cown6dYCLZpLiMFC1PaBmhvLvmJmLDJGeRTbPD45bX", ) .unwrap(), - layer: Layer::One, + layer: LegacyMixLayer::One, version: "0.2.0".into(), }; - let node2 = mix::Node { - owner: Some("Alice".to_string()), - ..node1.clone() - }; + let node2 = mix::LegacyNode { ..node1.clone() }; - let node3 = mix::Node { - owner: Some("Bob".to_string()), - ..node1.clone() - }; + let node3 = mix::LegacyNode { ..node1.clone() }; - let mut mixes: BTreeMap> = BTreeMap::new(); + let mut mixes = BTreeMap::new(); mixes.insert(1, vec![node1, node2]); mixes.insert(2, vec![node3]); @@ -607,7 +577,8 @@ mod converting_mixes_to_vec { let mixvec = topology.mixes_as_vec(); assert!(mixvec .iter() - .any(|node| node.owner.as_ref() == Some(&"N/A".to_string()))); + .any(|node| &node.identity_key.to_base58_string() + == "3ebjp1Fb9hdcS1AR6AZihgeJiMHkB5jjJUsvqNnfQwU7")); } } diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index c1f6efeaa0..170f1000a5 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -2,13 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{filter, NetworkAddress, NodeVersion}; +use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; use nym_crypto::asymmetric::{encryption, identity}; -pub use nym_mixnet_contract_common::Layer; -use nym_mixnet_contract_common::{MixId, MixNodeBond}; +pub use nym_mixnet_contract_common::LegacyMixLayer; +use nym_mixnet_contract_common::NodeId; use nym_sphinx_addressing::nodes::NymNodeRoutingAddress; use nym_sphinx_types::Node as SphinxNode; - -use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; use rand::seq::SliceRandom; use rand::thread_rng; use std::fmt::Formatter; @@ -42,26 +41,24 @@ pub enum MixnodeConversionError { } #[derive(Clone)] -pub struct Node { - pub mix_id: MixId, +pub struct LegacyNode { + pub mix_id: NodeId, pub host: NetworkAddress, // we're keeping this as separate resolved field since we do not want to be resolving the potential // hostname every time we want to construct a path via this node pub mix_host: SocketAddr, pub identity_key: identity::PublicKey, pub sphinx_key: encryption::PublicKey, // TODO: or nymsphinx::PublicKey? both are x25519 - pub layer: Layer, + pub layer: LegacyMixLayer, // to be removed: pub version: NodeVersion, - pub owner: Option, } -impl std::fmt::Debug for Node { +impl std::fmt::Debug for LegacyNode { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { f.debug_struct("mix::Node") .field("mix_id", &self.mix_id) - .field("owner", &self.owner) .field("host", &self.host) .field("mix_host", &self.mix_host) .field("identity_key", &self.identity_key.to_base58_string()) @@ -72,7 +69,7 @@ impl std::fmt::Debug for Node { } } -impl Node { +impl LegacyNode { pub fn parse_host(raw: &str) -> Result { // safety: this conversion is infallible // (but we retain result return type for legacy reasons) @@ -92,15 +89,15 @@ impl Node { } } -impl filter::Versioned for Node { +impl filter::Versioned for LegacyNode { fn version(&self) -> String { // TODO: return semver instead self.version.to_string() } } -impl<'a> From<&'a Node> for SphinxNode { - fn from(node: &'a Node) -> Self { +impl<'a> From<&'a LegacyNode> for SphinxNode { + fn from(node: &'a LegacyNode) -> Self { let node_address_bytes = NymNodeRoutingAddress::from(node.mix_host) .try_into() .unwrap(); @@ -109,36 +106,13 @@ impl<'a> From<&'a Node> for SphinxNode { } } -impl<'a> TryFrom<&'a MixNodeBond> for Node { - type Error = MixnodeConversionError; - - fn try_from(bond: &'a MixNodeBond) -> Result { - let host = Self::parse_host(&bond.mix_node.host)?; - - // try to completely resolve the host in the mix situation to avoid doing it every - // single time we want to construct a path - let mix_host = Self::extract_mix_host(&host, bond.mix_node.mix_port)?; - - Ok(Node { - mix_id: bond.mix_id, - owner: Some(bond.owner.as_str().to_owned()), - host, - mix_host, - identity_key: identity::PublicKey::from_base58_string(&bond.mix_node.identity_key)?, - sphinx_key: encryption::PublicKey::from_base58_string(&bond.mix_node.sphinx_key)?, - layer: bond.layer, - version: bond.mix_node.version.as_str().into(), - }) - } -} - -impl<'a> TryFrom<&'a SkimmedNode> for Node { +impl<'a> TryFrom<&'a SkimmedNode> for LegacyNode { type Error = MixnodeConversionError; fn try_from(value: &'a SkimmedNode) -> Result { if value.ip_addresses.is_empty() { return Err(MixnodeConversionError::NoIpAddressesProvided { - mixnode: value.ed25519_identity_pubkey.clone(), + mixnode: value.ed25519_identity_pubkey.to_base58_string(), }); } @@ -155,23 +129,14 @@ impl<'a> TryFrom<&'a SkimmedNode> for Node { let host = NetworkAddress::IpAddr(*ip); - Ok(Node { + Ok(LegacyNode { mix_id: value.node_id, host, mix_host: SocketAddr::new(*ip, value.mix_port), - identity_key: value.ed25519_identity_pubkey.parse()?, - sphinx_key: value.x25519_sphinx_pubkey.parse()?, + identity_key: value.ed25519_identity_pubkey, + sphinx_key: value.x25519_sphinx_pubkey, layer, - owner: None, version: NodeVersion::Unknown, }) } } - -impl TryFrom for Node { - type Error = MixnodeConversionError; - - fn try_from(bond: MixNodeBond) -> Result { - Node::try_from(&bond) - } -} diff --git a/common/topology/src/serde.rs b/common/topology/src/serde.rs index b68dd470bc..601b78dfd3 100644 --- a/common/topology/src/serde.rs +++ b/common/topology/src/serde.rs @@ -20,6 +20,7 @@ use thiserror::Error; #[cfg(feature = "wasm-serde-types")] use tsify::Tsify; +use nym_mixnet_contract_common::NodeId; #[cfg(feature = "wasm-serde-types")] use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; @@ -109,9 +110,6 @@ pub struct SerializableMixNode { #[serde(alias = "mix_id")] pub mix_id: u32, - #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] - pub owner: Option, - pub host: String, #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] @@ -131,40 +129,38 @@ pub struct SerializableMixNode { pub version: Option, } -impl TryFrom for mix::Node { +impl TryFrom for mix::LegacyNode { type Error = SerializableTopologyError; fn try_from(value: SerializableMixNode) -> Result { - let host = mix::Node::parse_host(&value.host)?; + let host = mix::LegacyNode::parse_host(&value.host)?; let mix_port = value.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT); let version = value.version.map(|v| v.as_str().into()).unwrap_or_default(); // try to completely resolve the host in the mix situation to avoid doing it every // single time we want to construct a path - let mix_host = mix::Node::extract_mix_host(&host, mix_port)?; + let mix_host = mix::LegacyNode::extract_mix_host(&host, mix_port)?; - Ok(mix::Node { + Ok(mix::LegacyNode { mix_id: value.mix_id, - owner: value.owner, host, mix_host, identity_key: identity::PublicKey::from_base58_string(&value.identity_key) .map_err(MixnodeConversionError::from)?, sphinx_key: encryption::PublicKey::from_base58_string(&value.sphinx_key) .map_err(MixnodeConversionError::from)?, - layer: mix::Layer::try_from(value.layer) + layer: mix::LegacyMixLayer::try_from(value.layer) .map_err(|_| SerializableTopologyError::InvalidMixLayer { value: value.layer })?, version, }) } } -impl<'a> From<&'a mix::Node> for SerializableMixNode { - fn from(value: &'a mix::Node) -> Self { +impl<'a> From<&'a mix::LegacyNode> for SerializableMixNode { + fn from(value: &'a mix::LegacyNode) -> Self { SerializableMixNode { mix_id: value.mix_id, - owner: value.owner.clone(), host: value.host.to_string(), mix_port: Some(value.mix_host.port()), identity_key: value.identity_key.to_base58_string(), @@ -181,11 +177,10 @@ impl<'a> From<&'a mix::Node> for SerializableMixNode { #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] pub struct SerializableGateway { - #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] - pub owner: Option, - pub host: String, + pub node_id: NodeId, + // optional ip address in the case of host being a hostname that can't be resolved // (thank you wasm) #[cfg_attr(feature = "wasm-serde-types", tsify(optional))] @@ -215,11 +210,11 @@ pub struct SerializableGateway { pub version: Option, } -impl TryFrom for gateway::Node { +impl TryFrom for gateway::LegacyNode { type Error = SerializableTopologyError; fn try_from(value: SerializableGateway) -> Result { - let host = gateway::Node::parse_host(&value.host)?; + let host = gateway::LegacyNode::parse_host(&value.host)?; let mix_port = value.mix_port.unwrap_or(DEFAULT_MIX_LISTENING_PORT); let clients_ws_port = value @@ -232,11 +227,11 @@ impl TryFrom for gateway::Node { let mix_host = if let Some(explicit_ip) = value.explicit_ip { SocketAddr::new(explicit_ip, mix_port) } else { - gateway::Node::extract_mix_host(&host, mix_port)? + gateway::LegacyNode::extract_mix_host(&host, mix_port)? }; - Ok(gateway::Node { - owner: value.owner, + Ok(gateway::LegacyNode { + node_id: value.node_id, host, mix_host, clients_ws_port, @@ -250,11 +245,11 @@ impl TryFrom for gateway::Node { } } -impl<'a> From<&'a gateway::Node> for SerializableGateway { - fn from(value: &'a gateway::Node) -> Self { +impl<'a> From<&'a gateway::LegacyNode> for SerializableGateway { + fn from(value: &'a gateway::LegacyNode) -> Self { SerializableGateway { - owner: value.owner.clone(), host: value.host.to_string(), + node_id: value.node_id, explicit_ip: Some(value.mix_host.ip()), mix_port: Some(value.mix_host.port()), clients_ws_port: Some(value.clients_ws_port), diff --git a/common/types/src/account.rs b/common/types/src/account.rs index 12bc12efd4..6315cccccd 100644 --- a/common/types/src/account.rs +++ b/common/types/src/account.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Account.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Account.ts") )] #[derive(Serialize, Deserialize, JsonSchema)] pub struct Account { @@ -31,7 +31,10 @@ impl Account { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/AccountWithMnemonic.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/AccountWithMnemonic.ts" + ) )] #[derive(Serialize, Deserialize)] pub struct AccountWithMnemonic { @@ -42,7 +45,7 @@ pub struct AccountWithMnemonic { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/AccountEntry.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/AccountEntry.ts") )] #[derive(Clone, Debug, Serialize, Deserialize)] pub struct AccountEntry { @@ -53,7 +56,7 @@ pub struct AccountEntry { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Balance.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Balance.ts") )] #[derive(Serialize, Deserialize)] pub struct Balance { diff --git a/common/types/src/currency.rs b/common/types/src/currency.rs index 26cda1a2db..9d13bfbc56 100644 --- a/common/types/src/currency.rs +++ b/common/types/src/currency.rs @@ -7,17 +7,16 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::collections::HashMap; - use std::fmt::{Display, Formatter}; use strum::{Display, EnumString, VariantNames}; -#[cfg(feature = "generate-ts")] -use ts_rs::{Dependency, TS}; - #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/CurrencyDenom.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/CurrencyDenom.ts" + ) )] #[cfg_attr(feature = "generate-ts", ts(rename_all = "lowercase"))] #[derive( @@ -248,40 +247,20 @@ impl From for CoinMetadata { // tries to semi-replicate cosmos-sdk's DecCoin for being able to handle tokens with decimal amounts // https://github.com/cosmos/cosmos-sdk/blob/v0.45.4/types/dec_coin.go #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export, export_to = "ts-packages/types/src/types/rust/DecCoin.ts") +)] pub struct DecCoin { + #[cfg_attr(feature = "generate-ts", ts(as = "CurrencyDenom"))] pub denom: Denom, // Decimal is already serialized to string and using string in its schema, so lets also go straight to string for ts_rs // todo: is `Decimal` the correct type to use? Do we want to depend on cosmwasm_std here? + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub amount: Decimal, } -// I had to implement it manually to correctly set dependencies -#[cfg(feature = "generate-ts")] -impl TS for DecCoin { - const EXPORT_TO: Option<&'static str> = Some("ts-packages/types/src/types/rust/DecCoin.ts"); - - fn decl() -> String { - format!("type {} = {};", Self::name(), Self::inline()) - } - - fn name() -> String { - "DecCoin".into() - } - - fn inline() -> String { - "{ denom: CurrencyDenom, amount: string }".into() - } - - fn dependencies() -> Vec { - vec![Dependency::from_ty::() - .expect("TS was incorrectly defined on `CurrencyDenom`")] - } - - fn transparent() -> bool { - false - } -} - impl DecCoin { pub fn new_base>(amount: impl Into, denom: S) -> Self { DecCoin { diff --git a/common/types/src/delegation.rs b/common/types/src/delegation.rs index e98c56fd40..3060a02e3a 100644 --- a/common/types/src/delegation.rs +++ b/common/types/src/delegation.rs @@ -1,21 +1,21 @@ use crate::currency::{DecCoin, RegisteredCoins}; use crate::deprecated::DelegationEvent; use crate::error::TypesError; -use crate::mixnode::MixNodeCostParams; +use crate::mixnode::NodeCostParams; use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::{Delegation as MixnetContractDelegation, MixId}; +use nym_mixnet_contract_common::{Delegation as MixnetContractDelegation, NodeId, NodeRewarding}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Delegation.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Delegation.ts") )] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct Delegation { pub owner: String, - pub mix_id: MixId, + pub mix_id: NodeId, pub amount: DecCoin, pub height: u64, pub proxy: Option, // proxy address used to delegate the funds on behalf of another address @@ -28,7 +28,7 @@ impl Delegation { ) -> Result { Ok(Delegation { owner: delegation.owner.to_string(), - mix_id: delegation.mix_id, + mix_id: delegation.node_id, amount: reg.attempt_convert_to_display_dec_coin(delegation.amount.into())?, height: delegation.height, proxy: delegation.proxy.map(|d| d.to_string()), @@ -39,19 +39,22 @@ impl Delegation { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DelegationWithEverything.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/DelegationWithEverything.ts" + ) )] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub struct DelegationWithEverything { pub owner: String, - pub mix_id: MixId, + pub mix_id: NodeId, pub node_identity: String, pub amount: DecCoin, pub accumulated_by_delegates: Option, pub accumulated_by_operator: Option, pub block_height: u64, pub delegated_on_iso_datetime: Option, - pub cost_params: Option, + pub cost_params: Option, pub avg_uptime_percent: Option, #[cfg_attr(feature = "generate-ts", ts(type = "string | null"))] @@ -67,10 +70,21 @@ pub struct DelegationWithEverything { pub mixnode_is_unbonding: Option, } +pub struct NodeInformation { + pub owner: String, + pub mix_id: NodeId, + pub node_identity: String, + pub rewarding_details: NodeRewarding, + pub is_unbonding: bool, +} + #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DelegationResult.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/DelegationResult.ts" + ) )] #[derive(Serialize, Deserialize, JsonSchema, Clone, PartialEq, Eq, Debug)] pub struct DelegationResult { @@ -82,7 +96,10 @@ pub struct DelegationResult { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DelegationSummaryResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/DelegationSummaryResponse.ts" + ) )] #[derive(Deserialize, Serialize)] pub struct DelegationsSummaryResponse { diff --git a/common/types/src/deprecated.rs b/common/types/src/deprecated.rs index 87f1f04e50..09b7b210d5 100644 --- a/common/types/src/deprecated.rs +++ b/common/types/src/deprecated.rs @@ -4,14 +4,17 @@ use crate::currency::DecCoin; use crate::error::TypesError; use crate::pending_events::{PendingEpochEvent, PendingEpochEventData}; -use nym_mixnet_contract_common::{IdentityKey, MixId}; +use nym_mixnet_contract_common::{IdentityKey, NodeId}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DelegationEventKind.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/DelegationEventKind.ts" + ) )] #[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] pub enum DelegationEventKind { @@ -22,12 +25,15 @@ pub enum DelegationEventKind { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/DelegationEvent.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/DelegationEvent.ts" + ) )] #[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] pub struct DelegationEvent { pub kind: DelegationEventKind, - pub mix_id: MixId, + pub mix_id: NodeId, pub address: String, pub amount: Option, pub proxy: Option, @@ -36,7 +42,10 @@ pub struct DelegationEvent { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/WrappedDelegationEvent.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/WrappedDelegationEvent.ts" + ) )] #[derive(Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, Debug)] pub struct WrappedDelegationEvent { diff --git a/common/types/src/fees.rs b/common/types/src/fees.rs index cd98be9d40..23c5e322f7 100644 --- a/common/types/src/fees.rs +++ b/common/types/src/fees.rs @@ -2,13 +2,16 @@ use crate::currency::DecCoin; use nym_validator_client::nyxd::Fee; use serde::{Deserialize, Serialize}; -#[cfg(feature = "generate-ts")] -use ts_rs::{Dependency, TS}; - #[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export, export_to = "ts-packages/types/src/types/rust/FeeDetails.ts") +)] pub struct FeeDetails { // expected to be used by the wallet in order to display detailed fee information to the user pub amount: Option, + #[cfg_attr(feature = "generate-ts", ts(as = "ts_type_helpers::Fee"))] pub fee: Fee, } @@ -18,35 +21,6 @@ impl FeeDetails { } } -#[cfg(feature = "generate-ts")] -impl TS for FeeDetails { - const EXPORT_TO: Option<&'static str> = Some("ts-packages/types/src/types/rust/FeeDetails.ts"); - - fn decl() -> String { - format!("type {} = {};", Self::name(), Self::inline()) - } - - fn name() -> String { - "FeeDetails".into() - } - - fn inline() -> String { - "{ amount: DecCoin | null, fee: Fee }".into() - } - - fn dependencies() -> Vec { - vec![ - Dependency::from_ty::().expect("TS was incorrectly defined on `DecCoin`"), - Dependency::from_ty::() - .expect("TS was incorrectly defined on `ts_type_helpers::Fee`"), - ] - } - - fn transparent() -> bool { - false - } -} - // this should really be sealed and NEVER EVER used as "normal" types, // but due to our typescript requirements, we have to expose it to generate // the types... @@ -56,14 +30,14 @@ pub mod ts_type_helpers { use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)] - #[ts(export_to = "ts-packages/types/src/types/rust/Fee.ts")] + #[ts(export, export_to = "ts-packages/types/src/types/rust/Fee.ts")] pub enum Fee { Manual(CosmosFee), Auto(Option), } #[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)] - #[ts(export_to = "ts-packages/types/src/types/rust/CosmosFee.ts")] + #[ts(export, export_to = "ts-packages/types/src/types/rust/CosmosFee.ts")] // this should corresponds to cosmrs::tx::Fee // IMPORTANT NOTE: this should work as of cosmrs 0.7.1 due to their `FromStr` implementations // on the type. The below struct might have to get readjusted if we update cosmrs!! @@ -76,7 +50,7 @@ pub mod ts_type_helpers { // Note: I've got a feeling this one will bite us hard at some point... #[derive(Debug, Clone, Serialize, Deserialize, ts_rs::TS)] - #[ts(export_to = "ts-packages/types/src/types/rust/Coin.ts")] + #[ts(export, export_to = "ts-packages/types/src/types/rust/Coin.ts")] // this should corresponds to cosmrs::Coin // IMPORTANT NOTE: this should work as of cosmrs 0.7.1 due to their `FromStr` implementations // on the type. The below struct might have to get readjusted if we update cosmrs!! diff --git a/common/types/src/gas.rs b/common/types/src/gas.rs index 9c389cc863..5e67d81fda 100644 --- a/common/types/src/gas.rs +++ b/common/types/src/gas.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Gas.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Gas.ts") )] #[derive(Deserialize, Serialize, Copy, Clone, Debug)] pub struct Gas { @@ -36,7 +36,7 @@ impl From for Gas { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/GasInfo.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/GasInfo.ts") )] #[derive(Deserialize, Serialize, Copy, Clone, Debug)] pub struct GasInfo { diff --git a/common/types/src/gateway.rs b/common/types/src/gateway.rs index 6be4e6939b..93127460b0 100644 --- a/common/types/src/gateway.rs +++ b/common/types/src/gateway.rs @@ -10,7 +10,7 @@ use std::fmt; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/Gateway.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/Gateway.ts") )] #[derive(Clone, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize, JsonSchema)] pub struct Gateway { @@ -51,7 +51,7 @@ impl From for Gateway { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/GatewayBond.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/GatewayBond.ts") )] #[derive(Clone, Debug, Deserialize, PartialEq, Eq, Serialize, JsonSchema)] pub struct GatewayBond { diff --git a/common/types/src/lib.rs b/common/types/src/lib.rs index 76aa6b2662..dbe40858d6 100644 --- a/common/types/src/lib.rs +++ b/common/types/src/lib.rs @@ -1,6 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +#![warn(clippy::todo)] + pub mod account; pub mod currency; pub mod delegation; @@ -12,6 +14,7 @@ pub mod gateway; pub mod helpers; pub mod mixnode; pub mod monitoring; +pub mod nym_node; pub mod pending_events; pub mod transaction; pub mod vesting; diff --git a/common/types/src/mixnode.rs b/common/types/src/mixnode.rs index 02246aad68..187cddf5cd 100644 --- a/common/types/src/mixnode.rs +++ b/common/types/src/mixnode.rs @@ -5,10 +5,9 @@ use crate::currency::{DecCoin, RegisteredCoins}; use crate::error::TypesError; use cosmwasm_std::Decimal; use nym_mixnet_contract_common::{ - EpochId, MixId, MixNode, MixNodeBond as MixnetContractMixNodeBond, - MixNodeCostParams as MixnetContractMixNodeCostParams, - MixNodeDetails as MixnetContractMixNodeDetails, - MixNodeRewarding as MixnetContractMixNodeRewarding, Percent, + EpochId, MixNode, MixNodeBond as MixnetContractMixNodeBond, + MixNodeDetails as MixnetContractMixNodeDetails, NodeCostParams as MixnetContractNodeCostParams, + NodeId, NodeRewarding as MixnetContractNodeRewarding, Percent, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -18,12 +17,15 @@ use std::net::IpAddr; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixNodeDetails.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/MixNodeDetails.ts" + ) )] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixNodeDetails { pub bond_information: MixNodeBond, - pub rewarding_details: MixNodeRewarding, + pub rewarding_details: NodeRewarding, } impl MixNodeDetails { @@ -36,7 +38,7 @@ impl MixNodeDetails { details.bond_information, reg, )?, - rewarding_details: MixNodeRewarding::from_mixnet_contract_mixnode_rewarding( + rewarding_details: NodeRewarding::from_mixnet_contract_node_rewarding( details.rewarding_details, reg, )?, @@ -47,14 +49,13 @@ impl MixNodeDetails { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixNodeBond.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/MixNodeBond.ts") )] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixNodeBond { - pub mix_id: MixId, + pub mix_id: NodeId, pub owner: String, pub original_pledge: DecCoin, - pub layer: String, pub mix_node: MixNode, pub proxy: Option, pub bonding_height: u64, @@ -71,7 +72,6 @@ impl MixNodeBond { owner: bond.owner.into_string(), original_pledge: reg .attempt_convert_to_display_dec_coin(bond.original_pledge.into())?, - layer: bond.layer.into(), mix_node: bond.mix_node, proxy: bond.proxy.map(|p| p.into_string()), bonding_height: bond.bonding_height, @@ -83,11 +83,14 @@ impl MixNodeBond { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixNodeRewarding.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/NodeRewarding.ts" + ) )] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] -pub struct MixNodeRewarding { - pub cost_params: MixNodeCostParams, +pub struct NodeRewarding { + pub cost_params: NodeCostParams, #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub operator: Decimal, @@ -106,13 +109,13 @@ pub struct MixNodeRewarding { pub unique_delegations: u32, } -impl MixNodeRewarding { - pub fn from_mixnet_contract_mixnode_rewarding( - mix_rewarding: MixnetContractMixNodeRewarding, +impl NodeRewarding { + pub fn from_mixnet_contract_node_rewarding( + mix_rewarding: MixnetContractNodeRewarding, reg: &RegisteredCoins, - ) -> Result { - Ok(MixNodeRewarding { - cost_params: MixNodeCostParams::from_mixnet_contract_mixnode_cost_params( + ) -> Result { + Ok(NodeRewarding { + cost_params: NodeCostParams::from_mixnet_contract_mixnode_cost_params( mix_rewarding.cost_params, reg, )?, @@ -129,22 +132,25 @@ impl MixNodeRewarding { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixNodeCostParams.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/MixNodeCostParams.ts" + ) )] #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] -pub struct MixNodeCostParams { +pub struct NodeCostParams { #[cfg_attr(feature = "generate-ts", ts(type = "string"))] pub profit_margin_percent: Percent, pub interval_operating_cost: DecCoin, } -impl MixNodeCostParams { +impl NodeCostParams { pub fn from_mixnet_contract_mixnode_cost_params( - cost_params: MixnetContractMixNodeCostParams, + cost_params: MixnetContractNodeCostParams, reg: &RegisteredCoins, - ) -> Result { - Ok(MixNodeCostParams { + ) -> Result { + Ok(NodeCostParams { profit_margin_percent: cost_params.profit_margin_percent, interval_operating_cost: reg .attempt_convert_to_display_dec_coin(cost_params.interval_operating_cost.into())?, @@ -154,8 +160,8 @@ impl MixNodeCostParams { pub fn try_convert_to_mixnet_contract_cost_params( self, reg: &RegisteredCoins, - ) -> Result { - Ok(MixnetContractMixNodeCostParams { + ) -> Result { + Ok(MixnetContractNodeCostParams { profit_margin_percent: self.profit_margin_percent, interval_operating_cost: reg .attempt_convert_to_base_coin(self.interval_operating_cost)? diff --git a/common/types/src/monitoring.rs b/common/types/src/monitoring.rs index 7769e61384..4768964fc1 100644 --- a/common/types/src/monitoring.rs +++ b/common/types/src/monitoring.rs @@ -1,9 +1,8 @@ -use std::{collections::HashSet, sync::LazyLock, time::SystemTime}; - use nym_crypto::asymmetric::identity::{PrivateKey, PublicKey, Signature}; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use std::{collections::HashSet, sync::LazyLock, time::SystemTime}; static NETWORK_MONITORS: LazyLock> = LazyLock::new(|| { let mut nm = HashSet::new(); @@ -13,45 +12,17 @@ static NETWORK_MONITORS: LazyLock> = LazyLock::new(|| { #[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] pub struct NodeResult { - pub node_id: MixId, - pub identity: String, - pub reliability: u8, -} - -#[derive(Debug, Serialize, Deserialize, JsonSchema, Clone)] -pub struct MixnodeResult { - pub mix_id: MixId, - pub identity: String, - pub owner: String, - pub reliability: u8, -} - -impl MixnodeResult { - pub fn new(mix_id: MixId, identity: String, owner: String, reliability: u8) -> Self { - MixnodeResult { - mix_id, - identity, - owner, - reliability, - } - } -} - -#[derive(Debug, Deserialize, Serialize, JsonSchema, Clone)] -pub struct GatewayResult { + pub node_id: NodeId, pub identity: String, - pub owner: String, pub reliability: u8, - pub mix_id: MixId, } -impl GatewayResult { - pub fn new(identity: String, owner: String, reliability: u8) -> Self { - GatewayResult { +impl NodeResult { + pub fn new(node_id: NodeId, identity: String, reliability: u8) -> Self { + NodeResult { + node_id, identity, - owner, reliability, - mix_id: 0, } } } @@ -59,8 +30,8 @@ impl GatewayResult { #[derive(Serialize, Deserialize, JsonSchema)] #[serde(untagged)] pub enum MonitorResults { - Mixnode(Vec), - Gateway(Vec), + Mixnode(Vec), + Gateway(Vec), } #[derive(Serialize, Deserialize, JsonSchema)] @@ -105,7 +76,7 @@ impl MonitorMessage { } } - pub fn from_allowed(&self) -> bool { + pub fn is_in_allowed(&self) -> bool { NETWORK_MONITORS.contains(&self.signer) } diff --git a/common/types/src/nym_node.rs b/common/types/src/nym_node.rs new file mode 100644 index 0000000000..a842bbb788 --- /dev/null +++ b/common/types/src/nym_node.rs @@ -0,0 +1,97 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::currency::{DecCoin, RegisteredCoins}; +use crate::error::TypesError; +use crate::mixnode::NodeRewarding; +use nym_mixnet_contract_common::{NodeId, NymNode, PendingNodeChanges}; +use nym_mixnet_contract_common::{ + NymNodeBond as MixnetContractNymNodeBond, NymNodeDetails as MixnetContractNymNodeDetails, +}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +/// Full details associated with given node. +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/NymNodeDetails.ts" + ) +)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct NymNodeDetails { + /// Basic bond information of this node, such as owner address, original pledge, etc. + pub bond_information: NymNodeBond, + + /// Details used for computation of rewarding related data. + pub rewarding_details: NodeRewarding, + + /// Adjustments to the node that are scheduled to happen during future epoch/interval transitions. + pub pending_changes: PendingNodeChanges, +} + +impl NymNodeDetails { + pub fn from_mixnet_contract_nym_node_details( + details: MixnetContractNymNodeDetails, + reg: &RegisteredCoins, + ) -> Result { + Ok(NymNodeDetails { + bond_information: NymNodeBond::from_mixnet_contract_mixnode_bond( + details.bond_information, + reg, + )?, + rewarding_details: NodeRewarding::from_mixnet_contract_node_rewarding( + details.rewarding_details, + reg, + )?, + pending_changes: details.pending_changes, + }) + } +} + +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export, export_to = "ts-packages/types/src/types/rust/NymNodeBond.ts") +)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct NymNodeBond { + /// Unique id assigned to the bonded node. + pub node_id: NodeId, + + /// Address of the owner of this nym-node. + pub owner: String, + + /// Original amount pledged by the operator of this node. + pub original_pledge: DecCoin, + + /// Block height at which this nym-node has been bonded. + pub bonding_height: u64, + + /// Flag to indicate whether this node is in the process of unbonding, + /// that will conclude upon the epoch finishing. + pub is_unbonding: bool, + + #[serde(flatten)] + /// Information provided by the operator for the purposes of bonding. + pub node: NymNode, +} + +impl NymNodeBond { + pub fn from_mixnet_contract_mixnode_bond( + bond: MixnetContractNymNodeBond, + reg: &RegisteredCoins, + ) -> Result { + Ok(NymNodeBond { + node_id: bond.node_id, + owner: bond.owner.into_string(), + original_pledge: reg + .attempt_convert_to_display_dec_coin(bond.original_pledge.into())?, + node: bond.node, + bonding_height: bond.bonding_height, + is_unbonding: bond.is_unbonding, + }) + } +} diff --git a/common/types/src/pending_events.rs b/common/types/src/pending_events.rs index d36e0b978c..9f98af61a3 100644 --- a/common/types/src/pending_events.rs +++ b/common/types/src/pending_events.rs @@ -3,13 +3,14 @@ use crate::currency::{DecCoin, RegisteredCoins}; use crate::error::TypesError; -use crate::mixnode::MixNodeCostParams; +use crate::mixnode::NodeCostParams; +use nym_mixnet_contract_common::reward_params::ActiveSetUpdate; use nym_mixnet_contract_common::{ - BlockHeight, EpochEventId, IntervalEventId, IntervalRewardingParamsUpdate, MixId, + BlockHeight, EpochEventId, IntervalEventId, IntervalRewardingParamsUpdate, NodeId, PendingEpochEvent as MixnetContractPendingEpochEvent, PendingEpochEventKind as MixnetContractPendingEpochEventKind, PendingIntervalEvent as MixnetContractPendingIntervalEvent, - PendingIntervalEventKind as MixnetContractPendingIntervalEventKind, + PendingIntervalEventKind as MixnetContractPendingIntervalEventKind, PendingIntervalEventKind, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -17,9 +18,12 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PendingEpochEvent.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/PendingEpochEvent.ts" + ) )] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub struct PendingEpochEvent { pub id: EpochEventId, pub created_at: BlockHeight, @@ -42,35 +46,52 @@ impl PendingEpochEvent { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PendingEpochEventData.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/PendingEpochEventData.ts" + ) )] -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub enum PendingEpochEventData { Delegate { owner: String, - mix_id: MixId, + mix_id: NodeId, amount: DecCoin, proxy: Option, }, Undelegate { owner: String, - mix_id: MixId, + mix_id: NodeId, proxy: Option, }, PledgeMore { - mix_id: MixId, + mix_id: NodeId, amount: DecCoin, }, DecreasePledge { - mix_id: MixId, + mix_id: NodeId, decrease_by: DecCoin, }, UnbondMixnode { - mix_id: MixId, + mix_id: NodeId, }, UpdateActiveSetSize { new_size: u32, }, + NymNodePledgeMore { + node_id: NodeId, + amount: DecCoin, + }, + NymNodeDecreasePledge { + node_id: NodeId, + decrease_by: DecCoin, + }, + UnbondNymNode { + node_id: NodeId, + }, + UpdateActiveSet { + update: ActiveSetUpdate, + }, } impl PendingEpochEventData { @@ -81,7 +102,7 @@ impl PendingEpochEventData { match pending_event { MixnetContractPendingEpochEventKind::Delegate { owner, - mix_id, + node_id: mix_id, amount, proxy, .. @@ -93,7 +114,7 @@ impl PendingEpochEventData { }), MixnetContractPendingEpochEventKind::Undelegate { owner, - mix_id, + node_id: mix_id, proxy, .. } => Ok(PendingEpochEventData::Undelegate { @@ -101,13 +122,13 @@ impl PendingEpochEventData { mix_id, proxy: proxy.map(|p| p.into_string()), }), - MixnetContractPendingEpochEventKind::PledgeMore { mix_id, amount } => { + MixnetContractPendingEpochEventKind::MixnodePledgeMore { mix_id, amount } => { Ok(PendingEpochEventData::PledgeMore { mix_id, amount: reg.attempt_convert_to_display_dec_coin(amount.into())?, }) } - MixnetContractPendingEpochEventKind::DecreasePledge { + MixnetContractPendingEpochEventKind::MixnodeDecreasePledge { mix_id, decrease_by, } => Ok(PendingEpochEventData::DecreasePledge { @@ -117,8 +138,24 @@ impl PendingEpochEventData { MixnetContractPendingEpochEventKind::UnbondMixnode { mix_id } => { Ok(PendingEpochEventData::UnbondMixnode { mix_id }) } - MixnetContractPendingEpochEventKind::UpdateActiveSetSize { new_size } => { - Ok(PendingEpochEventData::UpdateActiveSetSize { new_size }) + MixnetContractPendingEpochEventKind::NymNodePledgeMore { node_id, amount } => { + Ok(PendingEpochEventData::NymNodePledgeMore { + node_id, + amount: reg.attempt_convert_to_display_dec_coin(amount.into())?, + }) + } + MixnetContractPendingEpochEventKind::NymNodeDecreasePledge { + node_id, + decrease_by, + } => Ok(PendingEpochEventData::NymNodeDecreasePledge { + node_id, + decrease_by: reg.attempt_convert_to_display_dec_coin(decrease_by.into())?, + }), + MixnetContractPendingEpochEventKind::UnbondNymNode { node_id } => { + Ok(PendingEpochEventData::UnbondNymNode { node_id }) + } + MixnetContractPendingEpochEventKind::UpdateActiveSet { update } => { + Ok(PendingEpochEventData::UpdateActiveSet { update }) } } } @@ -127,7 +164,10 @@ impl PendingEpochEventData { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PendingIntervalEvent.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/PendingIntervalEvent.ts" + ) )] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub struct PendingIntervalEvent { @@ -155,15 +195,21 @@ impl PendingIntervalEvent { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PendingIntervalEventData.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/PendingIntervalEventData.ts" + ) )] #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)] pub enum PendingIntervalEventData { ChangeMixCostParams { - mix_id: MixId, - new_costs: MixNodeCostParams, + mix_id: NodeId, + new_costs: NodeCostParams, + }, + ChangeNymNodeCostParams { + node_id: NodeId, + new_costs: NodeCostParams, }, - UpdateRewardingParams { update: IntervalRewardingParamsUpdate, }, @@ -182,7 +228,7 @@ impl PendingIntervalEventData { MixnetContractPendingIntervalEventKind::ChangeMixCostParams { mix_id, new_costs } => { Ok(PendingIntervalEventData::ChangeMixCostParams { mix_id, - new_costs: MixNodeCostParams::from_mixnet_contract_mixnode_cost_params( + new_costs: NodeCostParams::from_mixnet_contract_mixnode_cost_params( new_costs, reg, )?, }) @@ -197,6 +243,14 @@ impl PendingIntervalEventData { epochs_in_interval, epoch_duration_secs, }), + PendingIntervalEventKind::ChangeNymNodeCostParams { node_id, new_costs } => { + Ok(PendingIntervalEventData::ChangeNymNodeCostParams { + node_id, + new_costs: NodeCostParams::from_mixnet_contract_mixnode_cost_params( + new_costs, reg, + )?, + }) + } } } } diff --git a/common/types/src/transaction.rs b/common/types/src/transaction.rs index 9179a0170b..7599e4799d 100644 --- a/common/types/src/transaction.rs +++ b/common/types/src/transaction.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/SendTxResult.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/SendTxResult.ts") )] #[derive(Deserialize, Serialize, Debug)] pub struct SendTxResult { @@ -38,7 +38,10 @@ impl SendTxResult { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/TransactionDetails.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/TransactionDetails.ts" + ) )] #[derive(Deserialize, Serialize, Debug)] pub struct TransactionDetails { @@ -60,7 +63,10 @@ impl TransactionDetails { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/TransactionExecuteResult.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/TransactionExecuteResult.ts" + ) )] #[derive(Deserialize, Serialize, Debug)] pub struct TransactionExecuteResult { @@ -89,7 +95,10 @@ impl TransactionExecuteResult { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/RpcTransactionResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/RpcTransactionResponse.ts" + ) )] #[derive(Deserialize, Serialize)] pub struct RpcTransactionResponse { diff --git a/common/types/src/vesting.rs b/common/types/src/vesting.rs index 1071b21324..6e62081327 100644 --- a/common/types/src/vesting.rs +++ b/common/types/src/vesting.rs @@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/PledgeData.ts") + ts(export, export_to = "ts-packages/types/src/types/rust/PledgeData.ts") )] #[derive(Serialize, Deserialize, Debug)] pub struct PledgeData { @@ -32,7 +32,10 @@ impl PledgeData { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/OriginalVestingResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/OriginalVestingResponse.ts" + ) )] #[derive(Serialize, Deserialize, Debug)] pub struct OriginalVestingResponse { @@ -57,7 +60,10 @@ impl OriginalVestingResponse { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/VestingAccountInfo.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/VestingAccountInfo.ts" + ) )] #[derive(Serialize, Deserialize, Debug)] pub struct VestingAccountInfo { @@ -86,7 +92,10 @@ impl VestingAccountInfo { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/VestingPeriod.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/VestingPeriod.ts" + ) )] #[derive(Serialize, Deserialize, Debug)] pub struct VestingPeriod { diff --git a/common/wasm/client-core/src/config/mod.rs b/common/wasm/client-core/src/config/mod.rs index eb69a735bc..05fb1cfe9a 100644 --- a/common/wasm/client-core/src/config/mod.rs +++ b/common/wasm/client-core/src/config/mod.rs @@ -156,6 +156,13 @@ pub struct TrafficWasm { /// a loop cover message is sent instead in order to preserve the rate. pub message_sending_average_delay_ms: u32, + /// Specify how many times particular packet can be retransmitted + /// None - no limit + pub maximum_number_of_retransmissions: Option, + + /// Specify whether route selection should be determined by the packet header. + pub deterministic_route_selection: bool, + /// Controls whether the main packet stream constantly produces packets according to the predefined /// poisson distribution. pub disable_main_poisson_packet_distribution: bool, @@ -190,6 +197,8 @@ impl From for ConfigTraffic { message_sending_average_delay: Duration::from_millis( traffic.message_sending_average_delay_ms as u64, ), + deterministic_route_selection: traffic.deterministic_route_selection, + maximum_number_of_retransmissions: traffic.maximum_number_of_retransmissions, disable_main_poisson_packet_distribution: traffic .disable_main_poisson_packet_distribution, primary_packet_size: PacketSize::RegularPacket, @@ -205,6 +214,8 @@ impl From for TrafficWasm { average_packet_delay_ms: traffic.average_packet_delay.as_millis() as u32, message_sending_average_delay_ms: traffic.message_sending_average_delay.as_millis() as u32, + deterministic_route_selection: traffic.deterministic_route_selection, + maximum_number_of_retransmissions: traffic.maximum_number_of_retransmissions, disable_main_poisson_packet_distribution: traffic .disable_main_poisson_packet_distribution, use_extended_packet_size: traffic.secondary_packet_size.is_some(), diff --git a/common/wasm/client-core/src/config/override.rs b/common/wasm/client-core/src/config/override.rs index 91fa4ec20e..75d4228e4d 100644 --- a/common/wasm/client-core/src/config/override.rs +++ b/common/wasm/client-core/src/config/override.rs @@ -83,6 +83,14 @@ pub struct TrafficWasmOverride { #[tsify(optional)] pub message_sending_average_delay_ms: Option, + /// Specify how many times particular packet can be retransmitted + #[tsify(optional)] + pub maximum_number_of_retransmissions: Option, + + /// Specify whether route selection should be determined by the packet header. + #[tsify(optional)] + pub deterministic_route_selection: Option, + /// Controls whether the main packet stream constantly produces packets according to the predefined /// poisson distribution. #[tsify(optional)] @@ -108,6 +116,10 @@ impl From for TrafficWasm { message_sending_average_delay_ms: value .message_sending_average_delay_ms .unwrap_or(def.message_sending_average_delay_ms), + maximum_number_of_retransmissions: value.maximum_number_of_retransmissions, + deterministic_route_selection: value + .deterministic_route_selection + .unwrap_or(def.deterministic_route_selection), disable_main_poisson_packet_distribution: value .disable_main_poisson_packet_distribution .unwrap_or(def.disable_main_poisson_packet_distribution), diff --git a/common/wasm/client-core/src/helpers.rs b/common/wasm/client-core/src/helpers.rs index 05caa7d88b..c403e9a8de 100644 --- a/common/wasm/client-core/src/helpers.rs +++ b/common/wasm/client-core/src/helpers.rs @@ -67,10 +67,12 @@ pub async fn current_network_topology_async( }; let api_client = NymApiClient::new(url); - let mixnodes = api_client.get_cached_active_mixnodes().await?; - let gateways = api_client.get_cached_gateways().await?; + let mixnodes = api_client + .get_all_basic_active_mixing_assigned_nodes(None) + .await?; + let gateways = api_client.get_all_basic_entry_assigned_nodes(None).await?; - Ok(NymTopology::from_detailed(mixnodes, gateways).into()) + Ok(NymTopology::from_basic(&mixnodes, &gateways).into()) } #[wasm_bindgen(js_name = "currentNetworkTopology")] @@ -88,7 +90,7 @@ pub async fn setup_gateway_wasm( client_store: &ClientStorage, force_tls: bool, chosen_gateway: Option, - gateways: &[gateway::Node], + gateways: &[gateway::LegacyNode], ) -> Result { // TODO: so much optimization and extra features could be added here, but that's for the future diff --git a/common/wireguard/src/lib.rs b/common/wireguard/src/lib.rs index 6567c2102c..ecd1576926 100644 --- a/common/wireguard/src/lib.rs +++ b/common/wireguard/src/lib.rs @@ -99,12 +99,25 @@ pub async fn start_wireguard let peers = all_peers .into_iter() .map(Peer::try_from) - .collect::, _>>()?; + .collect::, _>>()? + .into_iter() + .map(|mut peer| { + // since WGApi doesn't set those values on init, let's set them to 0 + peer.rx_bytes = 0; + peer.tx_bytes = 0; + peer + }) + .collect::>(); for peer in peers.iter() { let bandwidth_manager = PeerController::generate_bandwidth_manager(storage.clone(), &peer.public_key) .await? .map(|bw_m| Arc::new(RwLock::new(bw_m))); + // Update storage with *x_bytes set to 0, as in kernel peers we can't set those values + // so we need to restart counting. Hopefully the bandwidth was counted in available_bandwidth + storage + .insert_wireguard_peer(peer, bandwidth_manager.is_some()) + .await?; peer_bandwidth_managers.insert(peer.public_key.clone(), bandwidth_manager); } wg_api.create_interface()?; diff --git a/common/wireguard/src/peer_controller.rs b/common/wireguard/src/peer_controller.rs index 8655f68c49..8c7d94784a 100644 --- a/common/wireguard/src/peer_controller.rs +++ b/common/wireguard/src/peer_controller.rs @@ -7,8 +7,8 @@ use defguard_wireguard_rs::{ WireguardInterfaceApi, }; use futures::channel::oneshot; -use nym_authenticator_requests::{ - v1::registration::BANDWIDTH_CAP_PER_DAY, v2::registration::RemainingBandwidthData, +use nym_authenticator_requests::latest::registration::{ + RemainingBandwidthData, BANDWIDTH_CAP_PER_DAY, }; use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, BandwidthFlushingBehaviourConfig, @@ -27,7 +27,7 @@ use crate::{error::Error, peer_handle::SharedBandwidthStorageManager}; pub enum PeerControlRequest { AddPeer { peer: Peer, - ticket_validation: bool, + client_id: Option, response_tx: oneshot::Sender, }, RemovePeer { @@ -46,7 +46,6 @@ pub enum PeerControlRequest { pub struct AddPeerControlResponse { pub success: bool, - pub client_id: Option, } pub struct RemovePeerControlResponse { @@ -118,13 +117,13 @@ impl PeerController { } // Function that should be used for peer insertion, to handle both storage and kernel interaction - pub async fn add_peer(&self, peer: &Peer, with_client_id: bool) -> Result, Error> { - let client_id = self - .storage - .insert_wireguard_peer(peer, with_client_id) - .await?; - let ret = self.wg_api.inner.configure_peer(peer); - if ret.is_err() { + pub async fn add_peer(&self, peer: &Peer, client_id: Option) -> Result<(), Error> { + if client_id.is_none() { + self.storage.insert_wireguard_peer(peer, false).await?; + } + let ret: Result<(), defguard_wireguard_rs::error::WireguardInterfaceError> = + self.wg_api.inner.configure_peer(peer); + if client_id.is_none() && ret.is_err() { // Try to revert the insertion in storage if self .storage @@ -135,8 +134,7 @@ impl PeerController { log::error!("The storage has been corrupted. Wireguard peer {} will persist in storage indefinitely.", peer.public_key); } } - ret?; - Ok(client_id) + Ok(ret?) } // Function that should be used for peer removal, to handle both storage and kernel interaction @@ -160,10 +158,13 @@ impl PeerController { .ok_or(Error::MissingClientBandwidthEntry)? .client_id { - storage.create_bandwidth_entry(client_id).await?; + let bandwidth = storage + .get_available_bandwidth(client_id) + .await? + .ok_or(Error::MissingClientBandwidthEntry)?; Ok(Some(BandwidthStorageManager::new( storage, - ClientBandwidth::new(Default::default()), + ClientBandwidth::new(bandwidth.into()), client_id, BandwidthFlushingBehaviourConfig::default(), true, @@ -176,9 +177,9 @@ impl PeerController { async fn handle_add_request( &mut self, peer: &Peer, - with_client_id: bool, - ) -> Result, Error> { - let client_id = self.add_peer(peer, with_client_id).await?; + client_id: Option, + ) -> Result<(), Error> { + self.add_peer(peer, client_id).await?; let bandwidth_storage_manager = Self::generate_bandwidth_manager(self.storage.clone(), &peer.public_key) .await? @@ -193,12 +194,16 @@ impl PeerController { ); self.bw_storage_managers .insert(peer.public_key.clone(), bandwidth_storage_manager); + // try to immediately update the host information, to eliminate races + if let Ok(host_information) = self.wg_api.inner.read_interface_data() { + *self.host_information.write().await = host_information; + } tokio::spawn(async move { if let Err(e) = handle.run().await { log::error!("Peer handle shut down ungracefully - {e}"); } }); - Ok(client_id) + Ok(()) } async fn handle_query_peer(&self, key: &Key) -> Result, Error> { @@ -229,7 +234,7 @@ impl PeerController { // host information not updated yet return Ok(None); }; - BANDWIDTH_CAP_PER_DAY.saturating_sub((peer.rx_bytes + peer.tx_bytes) as i64) + BANDWIDTH_CAP_PER_DAY.saturating_sub(peer.rx_bytes + peer.tx_bytes) as i64 }; Ok(Some(RemainingBandwidthData { @@ -253,12 +258,12 @@ impl PeerController { } msg = self.request_rx.recv() => { match msg { - Some(PeerControlRequest::AddPeer { peer, ticket_validation, response_tx }) => { - let ret = self.handle_add_request(&peer, ticket_validation).await; - if let Ok(client_id) = ret { - response_tx.send(AddPeerControlResponse { success: true, client_id }).ok(); + Some(PeerControlRequest::AddPeer { peer, client_id, response_tx }) => { + let ret = self.handle_add_request(&peer, client_id).await; + if ret.is_ok() { + response_tx.send(AddPeerControlResponse { success: true }).ok(); } else { - response_tx.send(AddPeerControlResponse { success: false, client_id: None }).ok(); + response_tx.send(AddPeerControlResponse { success: false }).ok(); } } Some(PeerControlRequest::RemovePeer { key, response_tx }) => { diff --git a/common/wireguard/src/peer_handle.rs b/common/wireguard/src/peer_handle.rs index cd91d99b3c..71fa06f847 100644 --- a/common/wireguard/src/peer_handle.rs +++ b/common/wireguard/src/peer_handle.rs @@ -6,7 +6,7 @@ use crate::peer_controller::PeerControlRequest; use defguard_wireguard_rs::host::Peer; use defguard_wireguard_rs::{host::Host, key::Key}; use futures::channel::oneshot; -use nym_authenticator_requests::v2::registration::BANDWIDTH_CAP_PER_DAY; +use nym_authenticator_requests::latest::registration::BANDWIDTH_CAP_PER_DAY; use nym_credential_verification::bandwidth_storage_manager::BandwidthStorageManager; use nym_gateway_storage::models::WireguardPeer; use nym_gateway_storage::Storage; @@ -18,7 +18,7 @@ use tokio::sync::{mpsc, RwLock}; use tokio_stream::{wrappers::IntervalStream, StreamExt}; pub(crate) type SharedBandwidthStorageManager = Arc>>; -const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60 * 24); // 24 hours +const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60 * 24 * 30); // 30 days pub struct PeerHandle { storage: St, @@ -75,8 +75,8 @@ impl PeerHandle { async fn active_peer( &mut self, - storage_peer: WireguardPeer, - kernel_peer: Peer, + storage_peer: &WireguardPeer, + kernel_peer: &Peer, ) -> Result { if let Some(bandwidth_manager) = &self.bandwidth_storage_manager { let spent_bandwidth = (kernel_peer.rx_bytes + kernel_peer.tx_bytes) @@ -84,12 +84,13 @@ impl PeerHandle { .ok_or(Error::InconsistentConsumedBytes)? .try_into() .map_err(|_| Error::InconsistentConsumedBytes)?; - if bandwidth_manager - .write() - .await - .try_use_bandwidth(spent_bandwidth) - .await - .is_err() + if spent_bandwidth > 0 + && bandwidth_manager + .write() + .await + .try_use_bandwidth(spent_bandwidth) + .await + .is_err() { let success = self.remove_peer().await?; return Ok(!success); @@ -97,7 +98,7 @@ impl PeerHandle { } else { if SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER { log::debug!( - "Peer {} has been present for 24 hours, removing it", + "Peer {} has been present for 30 days, removing it", self.public_key.to_string() ); let success = self.remove_peer().await?; @@ -135,9 +136,12 @@ impl PeerHandle { log::debug!("Peer {:?} not in storage anymore, shutting down handle", self.public_key); return Ok(()); }; - if !self.active_peer(storage_peer, kernel_peer).await? { + if !self.active_peer(&storage_peer, &kernel_peer).await? { log::debug!("Peer {:?} doesn't have bandwidth anymore, shutting down handle", self.public_key); return Ok(()); + } else { + // Update storage values + self.storage.insert_wireguard_peer(&kernel_peer, self.bandwidth_storage_manager.is_some()).await?; } } diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 2f57ea58ef..44f70d3fc4 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -35,9 +35,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.82" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" +checksum = "4e1496f8fb1fbf272686b8d37f523dab3e4a7443300055e74cdaa449f3114356" [[package]] name = "arrayref" @@ -1271,6 +1271,7 @@ dependencies = [ name = "nym-mixnet-contract" version = "1.5.1" dependencies = [ + "anyhow", "bs58 0.4.0", "cosmwasm-derive", "cosmwasm-schema", @@ -1283,6 +1284,7 @@ dependencies = [ "nym-crypto", "nym-mixnet-contract-common", "nym-vesting-contract-common", + "rand", "rand_chacha", "serde", "thiserror", @@ -1297,6 +1299,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-controllers", + "cw-storage-plus", "cw2", "humantime-serde", "log", @@ -1748,11 +1751,12 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.116" +version = "1.0.128" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813" +checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" dependencies = [ "itoa", + "memchr", "ryu", "serde", ] @@ -1935,18 +1939,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index a7c49c9099..b5e53222d6 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -1,9 +1,9 @@ [workspace] resolver = "2" members = [ - "coconut-bandwidth", + # "coconut-bandwidth", "coconut-dkg", - "coconut-test", + "coconut-test", "ecash", "mixnet", "mixnet-vesting-integration-tests", @@ -32,6 +32,7 @@ incremental = false overflow-checks = true [workspace.dependencies] +anyhow = "1.0.86" bs58 = "0.4.0" cosmwasm-crypto = "=1.4.3" cosmwasm-derive = "=1.4.3" diff --git a/contracts/Makefile b/contracts/Makefile index e9ff461be0..62b4abcf18 100644 --- a/contracts/Makefile +++ b/contracts/Makefile @@ -1,7 +1,7 @@ -schema: coconut-bandwidth-schema coconut-dkg-schema mixnet-schema vesting-schema multisig-schema group-schema ecash-schema +schema: coconut-dkg-schema mixnet-schema vesting-schema multisig-schema group-schema ecash-schema -coconut-bandwidth-schema: - $(MAKE) -C coconut-bandwidth generate-schema +#coconut-bandwidth-schema: +# $(MAKE) -C coconut-bandwidth generate-schema coconut-dkg-schema: $(MAKE) -C coconut-dkg generate-schema diff --git a/contracts/coconut-bandwidth/src/storage.rs b/contracts/coconut-bandwidth/src/storage.rs index f18bc6fa28..aa671bbbcb 100644 --- a/contracts/coconut-bandwidth/src/storage.rs +++ b/contracts/coconut-bandwidth/src/storage.rs @@ -18,7 +18,7 @@ pub(crate) struct SpendCredentialIndex<'a> { // IndexList is just boilerplate code for fetching a struct's indexes // note that from my understanding this will be converted into a macro at some point in the future -impl<'a> IndexList for SpendCredentialIndex<'a> { +impl IndexList for SpendCredentialIndex<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.blinded_serial_number]; Box::new(v.into_iter()) diff --git a/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs b/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs index 7ca350120a..6eda195180 100644 --- a/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs +++ b/contracts/coconut-dkg/src/epoch_state/transactions/advance_epoch_state.rs @@ -57,10 +57,9 @@ pub fn try_advance_epoch_state(deps: DepsMut<'_>, env: Env) -> Result { pub(crate) epoch_id: MultiIndex<'a, EpochId, ContractVKShare, VKShareKey<'a>>, } -impl<'a> IndexList for VkShareIndex<'a> { +impl IndexList for VkShareIndex<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.epoch_id]; Box::new(v.into_iter()) diff --git a/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs b/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs index 9910720bb7..ac03d341ea 100644 --- a/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs +++ b/contracts/mixnet-vesting-integration-tests/src/support/fixtures.rs @@ -4,6 +4,7 @@ use crate::support::setup::{MIX_DENOM, REWARDING_VALIDATOR}; use cosmwasm_std::Decimal; use nym_contracts_common::Percent; +use nym_mixnet_contract_common::reward_params::RewardedSetParams; use nym_mixnet_contract_common::InitialRewardingParams; use std::time::Duration; @@ -21,8 +22,12 @@ pub fn default_mixnet_init_msg() -> nym_mixnet_contract_common::InstantiateMsg { 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_size: 240, - active_set_size: 100, + rewarded_set_params: RewardedSetParams { + entry_gateways: 70, + exit_gateways: 50, + mixnodes: 120, + standby: 0, + }, }, profit_margin: Default::default(), interval_operating_cost: Default::default(), diff --git a/contracts/mixnet-vesting-integration-tests/src/support/setup.rs b/contracts/mixnet-vesting-integration-tests/src/support/setup.rs index 4ef1b04cba..5c4b08e1d4 100644 --- a/contracts/mixnet-vesting-integration-tests/src/support/setup.rs +++ b/contracts/mixnet-vesting-integration-tests/src/support/setup.rs @@ -5,14 +5,15 @@ use crate::support::fixtures; use crate::support::helpers::{ mixnet_contract_wrapper, rewarding_validator, test_rng, vesting_contract_wrapper, }; -use cosmwasm_std::{coins, Addr, Coin, Timestamp}; +use cosmwasm_std::{coins, Addr, Coin, Decimal, Timestamp}; use cw_multi_test::{App, AppBuilder, Executor}; use nym_contracts_common::signing::{ContractMessageContent, MessageSignature, Nonce}; use nym_crypto::asymmetric::identity; -use nym_mixnet_contract_common::reward_params::Performance; +use nym_mixnet_contract_common::nym_node::{EpochAssignmentResponse, Role, RolesMetadataResponse}; +use nym_mixnet_contract_common::reward_params::{NodeRewardingParameters, Performance}; use nym_mixnet_contract_common::{ - CurrentIntervalResponse, LayerAssignment, MixNodeCostParams, MixnodeBondingPayload, - PagedRewardedSetResponse, RewardingParams, SignableMixNodeBondingMsg, + CurrentIntervalResponse, MixnodeBondingPayload, NodeCostParams, RewardedSet, RewardingParams, + RoleAssignment, SignableMixNodeBondingMsg, }; use nym_mixnet_contract_common::{ ExecuteMsg as MixnetExecuteMsg, MixNode, QueryMsg as MixnetQueryMsg, @@ -118,18 +119,95 @@ impl TestSetup { }) } - pub fn full_mixnet_epoch_operations(&mut self) { - let current_rewarded_set: PagedRewardedSetResponse = self + fn get_rewarded_set(&self) -> RewardedSet { + let metadata: RolesMetadataResponse = self + .app + .wrap() + .query_wasm_smart( + self.mixnet_contract(), + &MixnetQueryMsg::GetRewardedSetMetadata {}, + ) + .unwrap(); + + let entry: EpochAssignmentResponse = self .app .wrap() .query_wasm_smart( self.mixnet_contract(), - &MixnetQueryMsg::GetRewardedSet { - limit: Some(9999), - start_after: None, + &MixnetQueryMsg::GetRoleAssignment { + role: Role::EntryGateway, }, ) .unwrap(); + assert_eq!(entry.epoch_id, metadata.metadata.epoch_id); + + let exit: EpochAssignmentResponse = self + .app + .wrap() + .query_wasm_smart( + self.mixnet_contract(), + &MixnetQueryMsg::GetRoleAssignment { + role: Role::ExitGateway, + }, + ) + .unwrap(); + assert_eq!(exit.epoch_id, metadata.metadata.epoch_id); + + let layer1: EpochAssignmentResponse = self + .app + .wrap() + .query_wasm_smart( + self.mixnet_contract(), + &MixnetQueryMsg::GetRoleAssignment { role: Role::Layer1 }, + ) + .unwrap(); + assert_eq!(layer1.epoch_id, metadata.metadata.epoch_id); + + let layer2: EpochAssignmentResponse = self + .app + .wrap() + .query_wasm_smart( + self.mixnet_contract(), + &MixnetQueryMsg::GetRoleAssignment { role: Role::Layer2 }, + ) + .unwrap(); + assert_eq!(layer2.epoch_id, metadata.metadata.epoch_id); + + let layer3: EpochAssignmentResponse = self + .app + .wrap() + .query_wasm_smart( + self.mixnet_contract(), + &MixnetQueryMsg::GetRoleAssignment { role: Role::Layer3 }, + ) + .unwrap(); + assert_eq!(layer3.epoch_id, metadata.metadata.epoch_id); + + let standby: EpochAssignmentResponse = self + .app + .wrap() + .query_wasm_smart( + self.mixnet_contract(), + &MixnetQueryMsg::GetRoleAssignment { + role: Role::Standby, + }, + ) + .unwrap(); + assert_eq!(standby.epoch_id, metadata.metadata.epoch_id); + + RewardedSet { + entry_gateways: entry.nodes, + exit_gateways: exit.nodes, + layer1: layer1.nodes, + layer2: layer2.nodes, + layer3: layer3.nodes, + standby: standby.nodes, + } + } + + pub fn full_mixnet_epoch_operations(&mut self) { + let rewarded_set = self.get_rewarded_set(); + let current_params: RewardingParams = self .app .wrap() @@ -150,16 +228,30 @@ impl TestSetup { ) .unwrap(); + let work = + Decimal::one() / Decimal::from_ratio(rewarded_set.rewarded_set_size() as u64, 1u64); + let params = NodeRewardingParameters::new(Performance::hundred(), work); + + let mut nodes = rewarded_set + .layer1 + .iter() + .chain(rewarded_set.layer2.iter()) + .chain(rewarded_set.layer3.iter()) + .chain(rewarded_set.entry_gateways.iter()) + .chain(rewarded_set.exit_gateways.iter()) + .chain(rewarded_set.standby.iter()) + .copied() + .collect::>(); + + nodes.sort(); + // reward - for (mix_id, _status) in ¤t_rewarded_set.nodes { + for (node_id) in nodes { self.app .execute_contract( rewarding_validator(), self.mixnet_contract(), - &MixnetExecuteMsg::RewardMixnode { - mix_id: *mix_id, - performance: Performance::hundred(), - }, + &MixnetExecuteMsg::RewardNode { node_id, params }, &[], ) .unwrap(); @@ -176,22 +268,86 @@ impl TestSetup { .unwrap(); // don't bother changing the active set, use the same node for update and advance - let new_rewarded_set = current_rewarded_set - .nodes - .into_iter() - .enumerate() - .map(|(i, (node, _))| { - LayerAssignment::new(node, ((i as u8 % 3) + 1).try_into().unwrap()) - }) - .collect(); self.app .execute_contract( rewarding_validator(), self.mixnet_contract(), - &MixnetExecuteMsg::AdvanceCurrentEpoch { - new_rewarded_set, - expected_active_set_size: current_params.active_set_size, + &MixnetExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::EntryGateway, + nodes: rewarded_set.entry_gateways, + }, + }, + &[], + ) + .unwrap(); + + self.app + .execute_contract( + rewarding_validator(), + self.mixnet_contract(), + &MixnetExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::ExitGateway, + nodes: rewarded_set.exit_gateways, + }, + }, + &[], + ) + .unwrap(); + + self.app + .execute_contract( + rewarding_validator(), + self.mixnet_contract(), + &MixnetExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::Layer1, + nodes: rewarded_set.layer1, + }, + }, + &[], + ) + .unwrap(); + + self.app + .execute_contract( + rewarding_validator(), + self.mixnet_contract(), + &MixnetExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::Layer2, + nodes: rewarded_set.layer2, + }, + }, + &[], + ) + .unwrap(); + + self.app + .execute_contract( + rewarding_validator(), + self.mixnet_contract(), + &MixnetExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::Layer3, + nodes: rewarded_set.layer3, + }, + }, + &[], + ) + .unwrap(); + + self.app + .execute_contract( + rewarding_validator(), + self.mixnet_contract(), + &MixnetExecuteMsg::AssignRoles { + assignment: RoleAssignment { + role: Role::Standby, + nodes: rewarded_set.standby, + }, }, &[], ) @@ -206,7 +362,7 @@ impl TestSetup { pub fn valid_mixnode_with_sig( &mut self, owner: &str, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, stake: Coin, ) -> (MixNode, MessageSignature) { let signing_nonce: Nonce = self @@ -308,6 +464,7 @@ pub fn instantiate_contracts( mixnet_contract_address.clone(), &nym_mixnet_contract_common::MigrateMsg { vesting_contract_address: Some(vesting_contract_address.to_string()), + unsafe_skip_state_updates: None, }, mixnet_code_id, ) diff --git a/contracts/mixnet/Cargo.toml b/contracts/mixnet/Cargo.toml index 957f981f2e..796c6afb3f 100644 --- a/contracts/mixnet/Cargo.toml +++ b/contracts/mixnet/Cargo.toml @@ -44,7 +44,9 @@ thiserror = { workspace = true } time = { version = "0.3", features = ["macros"] } [dev-dependencies] +anyhow.workspace = true rand_chacha = "0.3" +rand = "0.8.5" nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } [features] diff --git a/contracts/mixnet/artifacts/checksums.txt b/contracts/mixnet/artifacts/checksums.txt deleted file mode 100644 index 770528b866..0000000000 --- a/contracts/mixnet/artifacts/checksums.txt +++ /dev/null @@ -1 +0,0 @@ -d8a3bddfd2d9f530ca373dfadf60a83eb1a199febec596a3ed37024a22012767 mixnode.wasm diff --git a/contracts/mixnet/artifacts/mixnode.wasm b/contracts/mixnet/artifacts/mixnode.wasm deleted file mode 100644 index 2d7f2adaa3..0000000000 Binary files a/contracts/mixnet/artifacts/mixnode.wasm and /dev/null differ diff --git a/contracts/mixnet/schema/nym-mixnet-contract.json b/contracts/mixnet/schema/nym-mixnet-contract.json index c754e65fdb..4fa3465b4b 100644 --- a/contracts/mixnet/schema/nym-mixnet-contract.json +++ b/contracts/mixnet/schema/nym-mixnet-contract.json @@ -86,21 +86,15 @@ "InitialRewardingParams": { "type": "object", "required": [ - "active_set_size", "active_set_work_factor", "initial_reward_pool", "initial_staking_supply", "interval_pool_emission", - "rewarded_set_size", + "rewarded_set_params", "staking_supply_scale_factor", "sybil_resistance" ], "properties": { - "active_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "active_set_work_factor": { "$ref": "#/definitions/Decimal" }, @@ -113,10 +107,8 @@ "interval_pool_emission": { "$ref": "#/definitions/Percent" }, - "rewarded_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "$ref": "#/definitions/RewardedSetParams" }, "staking_supply_scale_factor": { "$ref": "#/definitions/Percent" @@ -167,6 +159,42 @@ }, "additionalProperties": false }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" @@ -199,228 +227,6 @@ }, "additionalProperties": false }, - { - "type": "object", - "required": [ - "assign_node_layer" - ], - "properties": { - "assign_node_layer": { - "type": "object", - "required": [ - "layer", - "mix_id" - ], - "properties": { - "layer": { - "$ref": "#/definitions/Layer" - }, - "mix_id": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only owner of the node can crate the family with node as head", - "type": "object", - "required": [ - "create_family" - ], - "properties": { - "create_family": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Family head needs to sign the joining node IdentityKey", - "type": "object", - "required": [ - "join_family" - ], - "properties": { - "join_family": { - "type": "object", - "required": [ - "family_head", - "join_permit" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "join_permit": { - "$ref": "#/definitions/MessageSignature" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "leave_family" - ], - "properties": { - "leave_family": { - "type": "object", - "required": [ - "family_head" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "kick_family_member" - ], - "properties": { - "kick_family_member": { - "type": "object", - "required": [ - "member" - ], - "properties": { - "member": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "create_family_on_behalf" - ], - "properties": { - "create_family_on_behalf": { - "type": "object", - "required": [ - "label", - "owner_address" - ], - "properties": { - "label": { - "type": "string" - }, - "owner_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Family head needs to sign the joining node IdentityKey, MixNode needs to provide its signature proving that it wants to join the family", - "type": "object", - "required": [ - "join_family_on_behalf" - ], - "properties": { - "join_family_on_behalf": { - "type": "object", - "required": [ - "family_head", - "join_permit", - "member_address" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "join_permit": { - "$ref": "#/definitions/MessageSignature" - }, - "member_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "leave_family_on_behalf" - ], - "properties": { - "leave_family_on_behalf": { - "type": "object", - "required": [ - "family_head", - "member_address" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "member_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "kick_family_member_on_behalf" - ], - "properties": { - "kick_family_member_on_behalf": { - "type": "object", - "required": [ - "head_address", - "member" - ], - "properties": { - "head_address": { - "type": "string" - }, - "member": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -466,23 +272,21 @@ { "type": "object", "required": [ - "update_active_set_size" + "update_active_set_distribution" ], "properties": { - "update_active_set_size": { + "update_active_set_distribution": { "type": "object", "required": [ - "active_set_size", - "force_immediately" + "force_immediately", + "update" ], "properties": { - "active_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "force_immediately": { "type": "boolean" + }, + "update": { + "$ref": "#/definitions/ActiveSetUpdate" } }, "additionalProperties": false @@ -564,26 +368,19 @@ { "type": "object", "required": [ - "advance_current_epoch" + "reconcile_epoch_events" ], "properties": { - "advance_current_epoch": { + "reconcile_epoch_events": { "type": "object", - "required": [ - "expected_active_set_size", - "new_rewarded_set" - ], "properties": { - "expected_active_set_size": { - "type": "integer", + "limit": { + "type": [ + "integer", + "null" + ], "format": "uint32", "minimum": 0.0 - }, - "new_rewarded_set": { - "type": "array", - "items": { - "$ref": "#/definitions/LayerAssignment" - } } }, "additionalProperties": false @@ -594,19 +391,17 @@ { "type": "object", "required": [ - "reconcile_epoch_events" + "assign_roles" ], "properties": { - "reconcile_epoch_events": { + "assign_roles": { "type": "object", + "required": [ + "assignment" + ], "properties": { - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "assignment": { + "$ref": "#/definitions/RoleAssignment" } }, "additionalProperties": false @@ -629,7 +424,7 @@ ], "properties": { "cost_params": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "mix_node": { "$ref": "#/definitions/MixNode" @@ -659,7 +454,7 @@ ], "properties": { "cost_params": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "mix_node": { "$ref": "#/definitions/MixNode" @@ -793,17 +588,17 @@ { "type": "object", "required": [ - "update_mixnode_cost_params" + "update_cost_params" ], "properties": { - "update_mixnode_cost_params": { + "update_cost_params": { "type": "object", "required": [ "new_costs" ], "properties": { "new_costs": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } }, "additionalProperties": false @@ -825,7 +620,7 @@ ], "properties": { "new_costs": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "owner": { "type": "string" @@ -882,6 +677,19 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "migrate_mixnode" + ], + "properties": { + "migrate_mixnode": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -1019,19 +827,21 @@ { "type": "object", "required": [ - "delegate_to_mixnode" + "migrate_gateway" ], "properties": { - "delegate_to_mixnode": { + "migrate_gateway": { "type": "object", - "required": [ - "mix_id" - ], "properties": { - "mix_id": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "cost_params": { + "anyOf": [ + { + "$ref": "#/definitions/NodeCostParams" + }, + { + "type": "null" + } + ] } }, "additionalProperties": false @@ -1042,14 +852,100 @@ { "type": "object", "required": [ - "delegate_to_mixnode_on_behalf" + "bond_nym_node" ], "properties": { - "delegate_to_mixnode_on_behalf": { + "bond_nym_node": { "type": "object", "required": [ - "delegate", - "mix_id" + "cost_params", + "node", + "owner_signature" + ], + "properties": { + "cost_params": { + "$ref": "#/definitions/NodeCostParams" + }, + "node": { + "$ref": "#/definitions/NymNode" + }, + "owner_signature": { + "$ref": "#/definitions/MessageSignature" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "unbond_nym_node" + ], + "properties": { + "unbond_nym_node": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_node_config" + ], + "properties": { + "update_node_config": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/NodeConfigUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "delegate" + ], + "properties": { + "delegate": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "delegate_to_mixnode_on_behalf" + ], + "properties": { + "delegate_to_mixnode_on_behalf": { + "type": "object", + "required": [ + "delegate", + "mix_id" ], "properties": { "delegate": { @@ -1069,16 +965,16 @@ { "type": "object", "required": [ - "undelegate_from_mixnode" + "undelegate" ], "properties": { - "undelegate_from_mixnode": { + "undelegate": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -1119,23 +1015,23 @@ { "type": "object", "required": [ - "reward_mixnode" + "reward_node" ], "properties": { - "reward_mixnode": { + "reward_node": { "type": "object", "required": [ - "mix_id", - "performance" + "node_id", + "params" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 }, - "performance": { - "$ref": "#/definitions/Percent" + "params": { + "$ref": "#/definitions/NodeRewardingParameters" } }, "additionalProperties": false @@ -1186,10 +1082,10 @@ "withdraw_delegator_reward": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -1265,6 +1161,36 @@ } ], "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Coin": { "type": "object", "required": [ @@ -1284,8 +1210,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -1300,15 +1225,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -1319,8 +1236,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -1346,10 +1263,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - }, "Gateway": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -1467,14 +1380,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -1512,39 +1427,6 @@ }, "additionalProperties": false }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, - "LayerAssignment": { - "description": "Specifies layer assignment for the given mixnode.", - "type": "object", - "required": [ - "layer", - "mix_id" - ], - "properties": { - "layer": { - "description": "The layer to which it's going to be assigned", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, - "mix_id": { - "description": "The id of the mixnode.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, "MessageSignature": { "type": "array", "items": { @@ -1637,7 +1519,31 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeConfigUpdate": { + "type": "object", + "properties": { + "custom_http_port": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "type": [ + "string", + "null" + ] + }, + "restore_default_http_port": { + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -1646,7 +1552,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -1654,12 +1560,67 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewardingParameters": { + "description": "Parameters used for rewarding particular node.", + "type": "object", + "required": [ + "performance", + "work_factor" + ], + "properties": { + "performance": { + "description": "Performance of the particular node in the current epoch.", "allOf": [ { "$ref": "#/definitions/Percent" } ] + }, + "work_factor": { + "description": "Amount of work performed by this node in the current epoch also known as 'omega' in the paper", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" } }, "additionalProperties": false @@ -1704,6 +1665,74 @@ }, "additionalProperties": false }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] + }, + "RoleAssignment": { + "type": "object", + "required": [ + "nodes", + "role" + ], + "properties": { + "nodes": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "role": { + "$ref": "#/definitions/Role" + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" @@ -1728,165 +1757,13 @@ "additionalProperties": false }, { - "description": "Gets the list of families registered in this contract.", + "description": "Gets build information of this contract, such as the commit hash used for the build or rustc version.", "type": "object", "required": [ - "get_all_families_paged" + "get_contract_version" ], "properties": { - "get_all_families_paged": { - "type": "object", - "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Gets the list of all family members registered in this contract.", - "type": "object", - "required": [ - "get_all_members_paged" - ], - "properties": { - "get_all_members_paged": { - "type": "object", - "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to lookup family information given the family head.", - "type": "object", - "required": [ - "get_family_by_head" - ], - "properties": { - "get_family_by_head": { - "type": "object", - "required": [ - "head" - ], - "properties": { - "head": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to lookup family information given the family label.", - "type": "object", - "required": [ - "get_family_by_label" - ], - "properties": { - "get_family_by_label": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to retrieve family members given the family head.", - "type": "object", - "required": [ - "get_family_members_by_head" - ], - "properties": { - "get_family_members_by_head": { - "type": "object", - "required": [ - "head" - ], - "properties": { - "head": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Attempts to retrieve family members given the family label.", - "type": "object", - "required": [ - "get_family_members_by_label" - ], - "properties": { - "get_family_members_by_label": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Gets build information of this contract, such as the commit hash used for the build or rustc version.", - "type": "object", - "required": [ - "get_contract_version" - ], - "properties": { - "get_contract_version": { + "get_contract_version": { "type": "object", "additionalProperties": false } @@ -1991,40 +1868,6 @@ }, "additionalProperties": false }, - { - "description": "Gets the current list of mixnodes in the rewarded set.", - "type": "object", - "required": [ - "get_rewarded_set" - ], - "properties": { - "get_rewarded_set": { - "type": "object", - "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Gets the basic list of all currently bonded mixnodes.", "type": "object", @@ -2150,7 +1993,7 @@ "minimum": 0.0 }, "owner": { - "description": "The address of the owner of the the mixnodes used for the query.", + "description": "The address of the owner of the mixnodes used for the query.", "type": "string" }, "start_after": { @@ -2355,20 +2198,6 @@ }, "additionalProperties": false }, - { - "description": "Gets the current layer configuration of the mix network.", - "type": "object", - "required": [ - "get_layer_distribution" - ], - "properties": { - "get_layer_distribution": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "description": "Gets the basic list of all currently bonded gateways.", "type": "object", @@ -2448,17 +2277,14 @@ "additionalProperties": false }, { - "description": "Gets all delegations associated with particular mixnode", + "description": "Get the `NodeId`s of all the legacy gateways that they will get assigned once migrated into NymNodes", "type": "object", "required": [ - "get_mixnode_delegations" + "get_preassigned_gateway_ids" ], "properties": { - "get_mixnode_delegations": { + "get_preassigned_gateway_ids": { "type": "object", - "required": [ - "mix_id" - ], "properties": { "limit": { "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", @@ -2469,12 +2295,6 @@ "format": "uint32", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the node to query.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "start_after": { "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", "type": [ @@ -2489,22 +2309,15 @@ "additionalProperties": false }, { - "description": "Gets all delegations associated with particular delegator", + "description": "Gets the basic list of all currently bonded nymnodes.", "type": "object", "required": [ - "get_delegator_delegations" + "get_nym_node_bonds_paged" ], "properties": { - "get_delegator_delegations": { + "get_nym_node_bonds_paged": { "type": "object", - "required": [ - "delegator" - ], "properties": { - "delegator": { - "description": "The address of the owner of the delegations.", - "type": "string" - }, "limit": { "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", "type": [ @@ -2517,58 +2330,11 @@ "start_after": { "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", "type": [ - "array", + "integer", "null" ], - "items": [ - { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - { - "type": "string" - } - ], - "maxItems": 2, - "minItems": 2 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Gets delegation information associated with particular mixnode - delegator pair", - "type": "object", - "required": [ - "get_delegation_details" - ], - "properties": { - "get_delegation_details": { - "type": "object", - "required": [ - "delegator", - "mix_id" - ], - "properties": { - "delegator": { - "description": "The address of the owner of the delegation.", - "type": "string" - }, - "mix_id": { - "description": "Id of the node to query.", - "type": "integer", "format": "uint32", "minimum": 0.0 - }, - "proxy": { - "description": "Entity who made the delegation on behalf of the owner. If present, it's most likely the address of the vesting contract.", - "type": [ - "string", - "null" - ] } }, "additionalProperties": false @@ -2577,13 +2343,13 @@ "additionalProperties": false }, { - "description": "Gets all delegations in the system", + "description": "Gets the detailed list of all currently bonded nymnodes.", "type": "object", "required": [ - "get_all_delegations" + "get_nym_nodes_detailed_paged" ], "properties": { - "get_all_delegations": { + "get_nym_nodes_detailed_paged": { "type": "object", "properties": { "limit": { @@ -2598,21 +2364,11 @@ "start_after": { "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", "type": [ - "array", + "integer", "null" ], - "items": [ - { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - { - "type": "string" - } - ], - "maxItems": 2, - "minItems": 2 + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false @@ -2621,21 +2377,23 @@ "additionalProperties": false }, { - "description": "Gets the reward amount accrued by the node operator that has not yet been claimed.", + "description": "Gets the basic information of an unbonded nym-node with the provided id.", "type": "object", "required": [ - "get_pending_operator_reward" + "get_unbonded_nym_node" ], "properties": { - "get_pending_operator_reward": { + "get_unbonded_nym_node": { "type": "object", "required": [ - "address" + "node_id" ], "properties": { - "address": { - "description": "Address of the operator to use for the query.", - "type": "string" + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false @@ -2644,21 +2402,30 @@ "additionalProperties": false }, { - "description": "Gets the reward amount accrued by the particular mixnode that has not yet been claimed.", + "description": "Gets the basic list of all unbonded nymnodes.", "type": "object", "required": [ - "get_pending_mix_node_operator_reward" + "get_unbonded_nym_nodes_paged" ], "properties": { - "get_pending_mix_node_operator_reward": { + "get_unbonded_nym_nodes_paged": { "type": "object", - "required": [ - "mix_id" - ], "properties": { - "mix_id": { - "description": "Id of the node to query.", - "type": "integer", + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], "format": "uint32", "minimum": 0.0 } @@ -2669,35 +2436,39 @@ "additionalProperties": false }, { - "description": "Gets the reward amount accrued by the particular delegator that has not yet been claimed.", + "description": "Gets the basic list of all unbonded nymnodes that belonged to a particular owner.", "type": "object", "required": [ - "get_pending_delegator_reward" + "get_unbonded_nym_nodes_by_owner_paged" ], "properties": { - "get_pending_delegator_reward": { + "get_unbonded_nym_nodes_by_owner_paged": { "type": "object", "required": [ - "address", - "mix_id" + "owner" ], "properties": { - "address": { - "description": "Address of the delegator to use for the query.", - "type": "string" - }, - "mix_id": { - "description": "Id of the node to query.", - "type": "integer", + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], "format": "uint32", "minimum": 0.0 }, - "proxy": { - "description": "Entity who made the delegation on behalf of the owner. If present, it's most likely the address of the vesting contract.", + "owner": { + "description": "The address of the owner of the nym-node used for the query", + "type": "string" + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", "type": [ - "string", + "integer", "null" - ] + ], + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false @@ -2706,30 +2477,37 @@ "additionalProperties": false }, { - "description": "Given the provided node performance, attempt to estimate the operator reward for the current epoch.", + "description": "Gets the basic list of all unbonded nymnodes that used the particular identity key.", "type": "object", "required": [ - "get_estimated_current_epoch_operator_reward" + "get_unbonded_nym_nodes_by_identity_key_paged" ], "properties": { - "get_estimated_current_epoch_operator_reward": { + "get_unbonded_nym_nodes_by_identity_key_paged": { "type": "object", "required": [ - "estimated_performance", - "mix_id" + "identity_key" ], "properties": { - "estimated_performance": { - "description": "The estimated performance for the current epoch of the given node.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] + "identity_key": { + "description": "The identity key (base58-encoded ed25519 public key) of the node used for the query.", + "type": "string" }, - "mix_id": { - "description": "Id of the node to query.", - "type": "integer", + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], "format": "uint32", "minimum": 0.0 } @@ -2740,44 +2518,46 @@ "additionalProperties": false }, { - "description": "Given the provided node performance, attempt to estimate the delegator reward for the current epoch.", + "description": "Gets the detailed nymnode information belonging to the particular owner.", "type": "object", "required": [ - "get_estimated_current_epoch_delegator_reward" + "get_owned_nym_node" ], "properties": { - "get_estimated_current_epoch_delegator_reward": { + "get_owned_nym_node": { "type": "object", "required": [ - "address", - "estimated_performance", - "mix_id" + "address" ], "properties": { "address": { - "description": "Address of the delegator to use for the query.", + "description": "Address of the node owner to use for the query.", "type": "string" - }, - "estimated_performance": { - "description": "The estimated performance for the current epoch of the given node.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - }, - "mix_id": { + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the detailed nymnode information of a node with the provided id.", + "type": "object", + "required": [ + "get_nym_node_details" + ], + "properties": { + "get_nym_node_details": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", "minimum": 0.0 - }, - "proxy": { - "description": "Entity who made the delegation on behalf of the owner. If present, it's most likely the address of the vesting contract.", - "type": [ - "string", - "null" - ] } }, "additionalProperties": false @@ -2786,32 +2566,21 @@ "additionalProperties": false }, { - "description": "Gets the list of all currently pending epoch events that will be resolved once the current epoch finishes.", + "description": "Gets the detailed nym-node information given its current identity key.", "type": "object", "required": [ - "get_pending_epoch_events" + "get_nym_node_details_by_identity_key" ], "properties": { - "get_pending_epoch_events": { + "get_nym_node_details_by_identity_key": { "type": "object", + "required": [ + "node_identity" + ], "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "node_identity": { + "description": "The identity key (base58-encoded ed25519 public key) of the nym-node used for the query.", + "type": "string" } }, "additionalProperties": false @@ -2820,30 +2589,21 @@ "additionalProperties": false }, { - "description": "Gets the list of all currently pending interval events that will be resolved once the current interval finishes.", + "description": "Gets the rewarding information of a nym-node with the provided id.", "type": "object", "required": [ - "get_pending_interval_events" + "get_node_rewarding_details" ], "properties": { - "get_pending_interval_events": { + "get_node_rewarding_details": { "type": "object", + "required": [ + "node_id" + ], "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "integer", - "null" - ], + "node_id": { + "description": "Id of the node to query.", + "type": "integer", "format": "uint32", "minimum": 0.0 } @@ -2854,20 +2614,20 @@ "additionalProperties": false }, { - "description": "Gets detailed information about a pending epoch event given its id.", + "description": "Gets the stake saturation of a nym-node with the provided id.", "type": "object", "required": [ - "get_pending_epoch_event" + "get_node_stake_saturation" ], "properties": { - "get_pending_epoch_event": { + "get_node_stake_saturation": { "type": "object", "required": [ - "event_id" + "node_id" ], "properties": { - "event_id": { - "description": "The unique id associated with the event.", + "node_id": { + "description": "Id of the node to query.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -2879,23 +2639,19 @@ "additionalProperties": false }, { - "description": "Gets detailed information about a pending interval event given its id.", "type": "object", "required": [ - "get_pending_interval_event" + "get_role_assignment" ], "properties": { - "get_pending_interval_event": { + "get_role_assignment": { "type": "object", "required": [ - "event_id" + "role" ], "properties": { - "event_id": { - "description": "The unique id associated with the event.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "role": { + "$ref": "#/definitions/Role" } }, "additionalProperties": false @@ -2904,13 +2660,12 @@ "additionalProperties": false }, { - "description": "Gets the information about the number of currently pending epoch and interval events.", "type": "object", "required": [ - "get_number_of_pending_events" + "get_rewarded_set_metadata" ], "properties": { - "get_number_of_pending_events": { + "get_rewarded_set_metadata": { "type": "object", "additionalProperties": false } @@ -2918,79 +2673,1336 @@ "additionalProperties": false }, { - "description": "Gets the signing nonce associated with the particular cosmos address.", + "description": "Gets all delegations associated with particular node", "type": "object", "required": [ - "get_signing_nonce" + "get_node_delegations" ], "properties": { - "get_signing_nonce": { + "get_node_delegations": { "type": "object", "required": [ - "address" + "node_id" ], "properties": { - "address": { - "description": "Cosmos address used for the query of the signing nonce.", - "type": "string" + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false } }, "additionalProperties": false - } - ], - "definitions": { - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" }, - "Percent": { - "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - } - } - }, - "migrate": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MigrateMsg", - "type": "object", - "properties": { - "vesting_contract_address": { - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - }, - "sudo": null, - "responses": { - "admin": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "AdminResponse", - "description": "Returned from Admin.query_admin()", - "type": "object", - "properties": { + { + "description": "Gets all delegations associated with particular delegator", + "type": "object", + "required": [ + "get_delegator_delegations" + ], + "properties": { + "get_delegator_delegations": { + "type": "object", + "required": [ + "delegator" + ], + "properties": { + "delegator": { + "description": "The address of the owner of the delegations.", + "type": "string" + }, + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "string" + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets delegation information associated with particular mixnode - delegator pair", + "type": "object", + "required": [ + "get_delegation_details" + ], + "properties": { + "get_delegation_details": { + "type": "object", + "required": [ + "delegator", + "node_id" + ], + "properties": { + "delegator": { + "description": "The address of the owner of the delegation.", + "type": "string" + }, + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "proxy": { + "description": "Entity who made the delegation on behalf of the owner. If present, it's most likely the address of the vesting contract.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets all delegations in the system", + "type": "object", + "required": [ + "get_all_delegations" + ], + "properties": { + "get_all_delegations": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "string" + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the reward amount accrued by the node operator that has not yet been claimed.", + "type": "object", + "required": [ + "get_pending_operator_reward" + ], + "properties": { + "get_pending_operator_reward": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Address of the operator to use for the query.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the reward amount accrued by the particular mixnode that has not yet been claimed.", + "type": "object", + "required": [ + "get_pending_node_operator_reward" + ], + "properties": { + "get_pending_node_operator_reward": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the reward amount accrued by the particular delegator that has not yet been claimed.", + "type": "object", + "required": [ + "get_pending_delegator_reward" + ], + "properties": { + "get_pending_delegator_reward": { + "type": "object", + "required": [ + "address", + "node_id" + ], + "properties": { + "address": { + "description": "Address of the delegator to use for the query.", + "type": "string" + }, + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "proxy": { + "description": "Entity who made the delegation on behalf of the owner. If present, it's most likely the address of the vesting contract.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Given the provided node performance, attempt to estimate the operator reward for the current epoch.", + "type": "object", + "required": [ + "get_estimated_current_epoch_operator_reward" + ], + "properties": { + "get_estimated_current_epoch_operator_reward": { + "type": "object", + "required": [ + "estimated_performance", + "node_id" + ], + "properties": { + "estimated_performance": { + "description": "The estimated performance for the current epoch of the given node.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + }, + "estimated_work": { + "description": "The estimated work for the current epoch of the given node.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Given the provided node performance, attempt to estimate the delegator reward for the current epoch.", + "type": "object", + "required": [ + "get_estimated_current_epoch_delegator_reward" + ], + "properties": { + "get_estimated_current_epoch_delegator_reward": { + "type": "object", + "required": [ + "address", + "estimated_performance", + "node_id" + ], + "properties": { + "address": { + "description": "Address of the delegator to use for the query.", + "type": "string" + }, + "estimated_performance": { + "description": "The estimated performance for the current epoch of the given node.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + }, + "estimated_work": { + "description": "The estimated work for the current epoch of the given node.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the list of all currently pending epoch events that will be resolved once the current epoch finishes.", + "type": "object", + "required": [ + "get_pending_epoch_events" + ], + "properties": { + "get_pending_epoch_events": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the list of all currently pending interval events that will be resolved once the current interval finishes.", + "type": "object", + "required": [ + "get_pending_interval_events" + ], + "properties": { + "get_pending_interval_events": { + "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets detailed information about a pending epoch event given its id.", + "type": "object", + "required": [ + "get_pending_epoch_event" + ], + "properties": { + "get_pending_epoch_event": { + "type": "object", + "required": [ + "event_id" + ], + "properties": { + "event_id": { + "description": "The unique id associated with the event.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets detailed information about a pending interval event given its id.", + "type": "object", + "required": [ + "get_pending_interval_event" + ], + "properties": { + "get_pending_interval_event": { + "type": "object", + "required": [ + "event_id" + ], + "properties": { + "event_id": { + "description": "The unique id associated with the event.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the information about the number of currently pending epoch and interval events.", + "type": "object", + "required": [ + "get_number_of_pending_events" + ], + "properties": { + "get_number_of_pending_events": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the signing nonce associated with the particular cosmos address.", + "type": "object", + "required": [ + "get_signing_nonce" + ], + "properties": { + "get_signing_nonce": { + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Cosmos address used for the query of the signing nonce.", + "type": "string" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ], + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] + } + } + }, + "migrate": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MigrateMsg", + "type": "object", + "properties": { + "unsafe_skip_state_updates": { + "type": [ + "boolean", + "null" + ] + }, + "vesting_contract_address": { + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false + }, + "sudo": null, + "responses": { + "admin": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AdminResponse", + "description": "Returned from Admin.query_admin()", + "type": "object", + "properties": { "admin": { "type": [ "string", "null" ] } - }, - "additionalProperties": false + }, + "additionalProperties": false + }, + "get_all_delegations": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedAllDelegationsResponse", + "description": "Response containing paged list of all delegations currently active.", + "type": "object", + "required": [ + "delegations" + ], + "properties": { + "delegations": { + "description": "Each individual delegation made.", + "type": "array", + "items": { + "$ref": "#/definitions/Delegation" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "array", + "null" + ], + "items": [ + { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + { + "type": "string" + } + ], + "maxItems": 2, + "minItems": 2 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Delegation": { + "description": "Information about tokens being delegated towards given mixnode in order to accrue rewards with their work.", + "type": "object", + "required": [ + "amount", + "cumulative_reward_ratio", + "height", + "node_id", + "owner" + ], + "properties": { + "amount": { + "description": "Original delegation amount. Note that it is never mutated as delegation accumulates rewards.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "cumulative_reward_ratio": { + "description": "Value of the \"unit delegation\" associated with the mixnode at the time of delegation.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "height": { + "description": "Block height where this delegation occurred.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "node_id": { + "description": "Id of the Node that this delegation was performed against.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this delegation.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "proxy": { + "description": "Proxy address used to delegate the funds on behalf of another address", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_bonded_mixnode_details_by_identity": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MixnodeDetailsByIdentityResponse", + "description": "Response containing details of a bonded mixnode with the provided identity key.", + "type": "object", + "required": [ + "identity_key" + ], + "properties": { + "identity_key": { + "description": "The identity key (base58-encoded ed25519 public key) of the mixnode.", + "type": "string" + }, + "mixnode_details": { + "description": "If there exists a bonded mixnode with the provided identity key, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/MixNodeDetails" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "MixNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "http_api_port", + "identity_key", + "mix_port", + "sphinx_key", + "verloc_port", + "version" + ], + "properties": { + "host": { + "description": "Network address of this mixnode, for example 1.1.1.1 or foo.mixnode.com", + "type": "string" + }, + "http_api_port": { + "description": "Port used by this mixnode for its http(s) API", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "mix_port": { + "description": "Port used by this mixnode for listening for mix packets.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "sphinx_key": { + "description": "Base58-encoded x25519 public key used for sphinx key derivation.", + "type": "string" + }, + "verloc_port": { + "description": "Port used by this mixnode for listening for verloc requests.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "version": { + "description": "The self-reported semver version of this mixnode.", + "type": "string" + } + }, + "additionalProperties": false + }, + "MixNodeBond": { + "description": "Basic mixnode information provided by the node operator.", + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "mix_id", + "mix_node", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this mixnode has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "mix_id": { + "description": "Unique id assigned to the bonded mixnode.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mix_node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/MixNode" + } + ] + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this mixnode.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "proxy": { + "description": "Entity who bonded this mixnode on behalf of the owner. If exists, it's most likely the address of the vesting contract.", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + } + }, + "MixNodeDetails": { + "description": "Full details associated with given mixnode.", + "type": "object", + "required": [ + "bond_information", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this mixnode, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/MixNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", + "default": { + "cost_params_change": null, + "pledge_change": null + }, + "allOf": [ + { + "$ref": "#/definitions/PendingMixNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "PendingMixNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_c_w2_contract_version": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ContractVersion", + "type": "object", + "required": [ + "contract", + "version" + ], + "properties": { + "contract": { + "description": "contract is the crate name of the implementing contract, eg. `crate:cw20-base` we will use other prefixes for other languages, and their standard global namespacing", + "type": "string" + }, + "version": { + "description": "version is any string that this implementation knows. It may be simple counter \"1\", \"2\". or semantic version on release tags \"v0.7.0\", or some custom feature flag list. the only code that needs to understand the version parsing is code that knows how to migrate from the given contract (and is tied to it's implementation somehow)", + "type": "string" + } + }, + "additionalProperties": false + }, + "get_contract_version": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ContractBuildInformation", + "type": "object", + "required": [ + "build_timestamp", + "build_version", + "commit_branch", + "commit_sha", + "commit_timestamp", + "rustc_version" + ], + "properties": { + "build_timestamp": { + "description": "Provides the build timestamp, for example `2021-02-23T20:14:46.558472672+00:00`.", + "type": "string" + }, + "build_version": { + "description": "Provides the build version, for example `0.1.0-9-g46f83e1`.", + "type": "string" + }, + "cargo_debug": { + "description": "Provides the cargo debug mode that was used for the build.", + "default": "unknown", + "type": "string" + }, + "cargo_opt_level": { + "description": "Provides the opt value set by cargo during the build", + "default": "unknown", + "type": "string" + }, + "commit_branch": { + "description": "Provides the name of the git branch that was used for the build, for example `master`.", + "type": "string" + }, + "commit_sha": { + "description": "Provides the hash of the commit that was used for the build, for example `46f83e112520533338245862d366f6a02cef07d4`.", + "type": "string" + }, + "commit_timestamp": { + "description": "Provides the timestamp of the commit that was used for the build, for example `2021-02-23T08:08:02-05:00`.", + "type": "string" + }, + "contract_name": { + "description": "Provides the name of the binary, i.e. the content of `CARGO_PKG_NAME` environmental variable.", + "default": "unknown", + "type": "string" + }, + "rustc_version": { + "description": "Provides the rustc version that was used for the build, for example `1.52.0-nightly`.", + "type": "string" + } + }, + "additionalProperties": false + }, + "get_current_interval_details": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "CurrentIntervalResponse", + "description": "Information about the current rewarding interval.", + "type": "object", + "required": [ + "current_blocktime", + "interval", + "is_current_epoch_over", + "is_current_interval_over" + ], + "properties": { + "current_blocktime": { + "description": "The current blocktime", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "interval": { + "description": "Detailed information about the underlying interval.", + "allOf": [ + { + "$ref": "#/definitions/Interval" + } + ] + }, + "is_current_epoch_over": { + "description": "Flag indicating whether the current epoch is over and it should be advanced.", + "type": "boolean" + }, + "is_current_interval_over": { + "description": "Flag indicating whether the current interval is over and it should be advanced.", + "type": "boolean" + } + }, + "additionalProperties": false, + "definitions": { + "Duration": { + "type": "object", + "required": [ + "nanos", + "secs" + ], + "properties": { + "nanos": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "secs": { + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + } + }, + "Interval": { + "type": "object", + "required": [ + "current_epoch_id", + "current_epoch_start", + "epoch_length", + "epochs_in_interval", + "id", + "total_elapsed_epochs" + ], + "properties": { + "current_epoch_id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "current_epoch_start": { + "type": "string" + }, + "epoch_length": { + "$ref": "#/definitions/Duration" + }, + "epochs_in_interval": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "id": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "total_elapsed_epochs": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + } + } + }, + "get_delegation_details": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeDelegationResponse", + "description": "Response containing delegation details.", + "type": "object", + "required": [ + "mixnode_still_bonded", + "node_still_bonded" + ], + "properties": { + "delegation": { + "description": "If the delegation exists, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/Delegation" + }, + { + "type": "null" + } + ] + }, + "mixnode_still_bonded": { + "description": "Flag indicating whether the node towards which the delegation was made is still bonded in the network.", + "deprecated": true, + "type": "boolean" + }, + "node_still_bonded": { + "type": "boolean" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Delegation": { + "description": "Information about tokens being delegated towards given mixnode in order to accrue rewards with their work.", + "type": "object", + "required": [ + "amount", + "cumulative_reward_ratio", + "height", + "node_id", + "owner" + ], + "properties": { + "amount": { + "description": "Original delegation amount. Note that it is never mutated as delegation accumulates rewards.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "cumulative_reward_ratio": { + "description": "Value of the \"unit delegation\" associated with the mixnode at the time of delegation.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "height": { + "description": "Block height where this delegation occurred.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "node_id": { + "description": "Id of the Node that this delegation was performed against.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this delegation.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "proxy": { + "description": "Proxy address used to delegate the funds on behalf of another address", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } }, - "get_all_delegations": { + "get_delegator_delegations": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedAllDelegationsResponse", - "description": "Response containing paged list of all delegations currently active.", + "title": "PagedDelegatorDelegationsResponse", + "description": "Response containing paged list of all delegations made by the particular address.", "type": "object", "required": [ "delegations" @@ -3055,7 +4067,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -3081,8 +4093,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -3115,133 +4127,326 @@ } } }, - "get_all_families_paged": { + "get_epoch_status": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedFamiliesResponse", - "description": "Response containing paged list of all families registered in the contract.", + "title": "EpochStatus", + "description": "The status of the current rewarding epoch.", "type": "object", "required": [ - "families" + "being_advanced_by", + "state" ], "properties": { - "families": { - "description": "The families registered in the contract.", - "type": "array", - "items": { - "$ref": "#/definitions/Family" + "being_advanced_by": { + "description": "Specifies either, which validator is currently performing progression into the following epoch (if the epoch is currently being progressed), or which validator was responsible for progressing into the current epoch (if the epoch is currently in progress)", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "state": { + "description": "The concrete state of the epoch.", + "allOf": [ + { + "$ref": "#/definitions/EpochState" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "EpochState": { + "description": "The state of the current rewarding epoch.", + "oneOf": [ + { + "description": "Represents the state of an epoch that's in progress (well, duh.) All actions are allowed to be issued.", + "type": "string", + "enum": [ + "in_progress" + ] + }, + { + "description": "Represents the state of an epoch when the rewarding entity has been decided on, and the mixnodes are in the process of being rewarded for their work in this epoch.", + "type": "object", + "required": [ + "rewarding" + ], + "properties": { + "rewarding": { + "type": "object", + "required": [ + "final_node_id", + "last_rewarded" + ], + "properties": { + "final_node_id": { + "description": "The id of the last node that's going to be rewarded before progressing into the next state.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "last_rewarded": { + "description": "The id of the last node that has already received its rewards.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Represents the state of an epoch when all mixnodes have already been rewarded for their work in this epoch and all issued actions should now get resolved before being allowed to advance into the next epoch.", + "type": "string", + "enum": [ + "reconciling_events" + ] + }, + { + "description": "Represents the state of an epoch when all nodes have already been rewarded for their work in this epoch, all issued actions got resolved and node roles should now be assigned before advancing into the next epoch.", + "type": "object", + "required": [ + "role_assignment" + ], + "properties": { + "role_assignment": { + "type": "object", + "required": [ + "next" + ], + "properties": { + "next": { + "$ref": "#/definitions/Role" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + } + ] + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] + } + } + }, + "get_estimated_current_epoch_delegator_reward": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EstimatedCurrentEpochRewardResponse", + "description": "Response containing estimation of node rewards for the current epoch.", + "type": "object", + "properties": { + "current_stake_value": { + "description": "The current stake value given all past rewarding and compounding since the original staking was performed.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "current_stake_value_detailed_amount": { + "description": "The current stake value. Note that it's nearly identical to `current_stake_value`, however, it contains few additional decimal points for more accurate reward calculation.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "detailed_estimation_amount": { + "description": "The full reward estimation. Note that it's nearly identical to `estimation`, however, it contains few additional decimal points for more accurate reward calculation.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "estimation": { + "description": "The reward estimation for the current epoch, i.e. the amount of tokens that could be claimable after the epoch finishes and the state of the network does not change.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "original_stake": { + "description": "The amount of tokens initially staked.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } } }, - "start_next_after": { - "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", - "type": [ - "string", - "null" + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_estimated_current_epoch_operator_reward": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EstimatedCurrentEpochRewardResponse", + "description": "Response containing estimation of node rewards for the current epoch.", + "type": "object", + "properties": { + "current_stake_value": { + "description": "The current stake value given all past rewarding and compounding since the original staking was performed.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "current_stake_value_detailed_amount": { + "description": "The current stake value. Note that it's nearly identical to `current_stake_value`, however, it contains few additional decimal points for more accurate reward calculation.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "detailed_estimation_amount": { + "description": "The full reward estimation. Note that it's nearly identical to `estimation`, however, it contains few additional decimal points for more accurate reward calculation.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "estimation": { + "description": "The reward estimation for the current epoch, i.e. the amount of tokens that could be claimable after the epoch finishes and the state of the network does not change.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "original_stake": { + "description": "The amount of tokens initially staked.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } ] } }, "additionalProperties": false, "definitions": { - "Family": { - "description": "A group of mixnodes associated with particular staking entity. When defined all nodes belonging to the same family will be prioritised to be put onto the same layer.", + "Coin": { "type": "object", "required": [ - "head", - "label" + "amount", + "denom" ], "properties": { - "head": { - "description": "Owner of this family.", - "allOf": [ - { - "$ref": "#/definitions/FamilyHead" - } - ] + "amount": { + "$ref": "#/definitions/Uint128" }, - "label": { - "description": "Human readable label for this family.", + "denom": { "type": "string" - }, - "proxy": { - "description": "Optional proxy (i.e. vesting contract address) used when creating the family.", - "type": [ - "string", - "null" - ] } - }, - "additionalProperties": false + } }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" - } - } - }, - "get_all_members_paged": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedMembersResponse", - "description": "Response containing paged list of all family members (of ALL families) registered in the contract.", - "type": "object", - "required": [ - "members" - ], - "properties": { - "members": { - "description": "The members alongside their family heads.", - "type": "array", - "items": { - "type": "array", - "items": [ - { - "type": "string" - }, - { - "$ref": "#/definitions/FamilyHead" - } - ], - "maxItems": 2, - "minItems": 2 - } }, - "start_next_after": { - "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false, - "definitions": { - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" } } }, - "get_bonded_mixnode_details_by_identity": { + "get_gateway_bond": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MixnodeDetailsByIdentityResponse", - "description": "Response containing details of a bonded mixnode with the provided identity key.", + "title": "GatewayBondResponse", + "description": "Response containing details of a gateway with the provided identity key.", "type": "object", "required": [ - "identity_key" + "identity" ], "properties": { - "identity_key": { - "description": "The identity key (base58-encoded ed25519 public key) of the mixnode.", - "type": "string" - }, - "mixnode_details": { - "description": "If there exists a bonded mixnode with the provided identity key, this field contains its detailed information.", + "gateway": { + "description": "If there exists a gateway with the provided identity key, this field contains its details.", "anyOf": [ { - "$ref": "#/definitions/MixNodeDetails" + "$ref": "#/definitions/GatewayBond" }, { "type": "null" } ] + }, + "identity": { + "description": "The identity key (base58-encoded ed25519 public key) of the gateway.", + "type": "string" } }, "additionalProperties": false, @@ -3265,497 +4470,480 @@ } } }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, - "MixNode": { + "Gateway": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", "required": [ + "clients_port", "host", - "http_api_port", "identity_key", + "location", "mix_port", "sphinx_key", - "verloc_port", "version" ], "properties": { - "host": { - "description": "Network address of this mixnode, for example 1.1.1.1 or foo.mixnode.com", - "type": "string" - }, - "http_api_port": { - "description": "Port used by this mixnode for its http(s) API", + "clients_port": { + "description": "Port used by this gateway for listening for client requests.", "type": "integer", "format": "uint16", "minimum": 0.0 }, - "identity_key": { - "description": "Base58-encoded ed25519 EdDSA public key.", + "host": { + "description": "Network address of this gateway, for example 1.1.1.1 or foo.gateway.com", "type": "string" }, - "mix_port": { - "description": "Port used by this mixnode for listening for mix packets.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "sphinx_key": { - "description": "Base58-encoded x25519 public key used for sphinx key derivation.", + "identity_key": { + "description": "Base58 encoded ed25519 EdDSA public key of the gateway used to derive shared keys with clients", "type": "string" }, - "verloc_port": { - "description": "Port used by this mixnode for listening for verloc requests.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "version": { - "description": "The self-reported semver version of this mixnode.", + "location": { + "description": "The physical, self-reported, location of this gateway.", "type": "string" - } - }, - "additionalProperties": false - }, - "MixNodeBond": { - "description": "Basic mixnode information provided by the node operator.", - "type": "object", - "required": [ - "bonding_height", - "is_unbonding", - "layer", - "mix_id", - "mix_node", - "original_pledge", - "owner" - ], - "properties": { - "bonding_height": { - "description": "Block height at which this mixnode has been bonded.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "is_unbonding": { - "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", - "type": "boolean" - }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] }, - "mix_id": { - "description": "Unique id assigned to the bonded mixnode.", + "mix_port": { + "description": "Port used by this gateway for listening for mix packets.", "type": "integer", - "format": "uint32", + "format": "uint16", "minimum": 0.0 }, - "mix_node": { - "description": "Information provided by the operator for the purposes of bonding.", - "allOf": [ - { - "$ref": "#/definitions/MixNode" - } - ] - }, - "original_pledge": { - "description": "Original amount pledged by the operator of this node.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "owner": { - "description": "Address of the owner of this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "proxy": { - "description": "Entity who bonded this mixnode on behalf of the owner. If exists, it's most likely the address of the vesting contract.", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false - }, - "MixNodeDetails": { - "description": "Full details associated with given mixnode.", - "type": "object", - "required": [ - "bond_information", - "rewarding_details" - ], - "properties": { - "bond_information": { - "description": "Basic bond information of this mixnode, such as owner address, original pledge, etc.", - "allOf": [ - { - "$ref": "#/definitions/MixNodeBond" - } - ] - }, - "pending_changes": { - "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", - "default": { - "pledge_change": null - }, - "allOf": [ - { - "$ref": "#/definitions/PendingMixNodeChanges" - } - ] - }, - "rewarding_details": { - "description": "Details used for computation of rewarding related data.", - "allOf": [ - { - "$ref": "#/definitions/MixNodeRewarding" - } - ] + "sphinx_key": { + "description": "Base58-encoded x25519 public key used for sphinx key derivation.", + "type": "string" + }, + "version": { + "description": "The self-reported semver version of this gateway.", + "type": "string" } }, "additionalProperties": false }, - "MixNodeRewarding": { + "GatewayBond": { + "description": "Basic gateway information provided by the node operator.", "type": "object", "required": [ - "cost_params", - "delegates", - "last_rewarded_epoch", - "operator", - "total_unit_reward", - "unique_delegations", - "unit_delegation" + "block_height", + "gateway", + "owner", + "pledge_amount" ], "properties": { - "cost_params": { - "description": "Information provided by the operator that influence the cost function.", - "allOf": [ - { - "$ref": "#/definitions/MixNodeCostParams" - } - ] + "block_height": { + "description": "Block height at which this gateway has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 }, - "delegates": { - "description": "Total delegation and compounded reward earned by all node delegators.", + "gateway": { + "description": "Information provided by the operator for the purposes of bonding.", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/Gateway" } ] }, - "last_rewarded_epoch": { - "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "operator": { - "description": "Total pledge and compounded reward earned by the node operator.", + "owner": { + "description": "Address of the owner of this gateway.", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/Addr" } ] }, - "total_unit_reward": { - "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "pledge_amount": { + "description": "Original amount pledged by the operator of this node.", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/Coin" } ] }, - "unique_delegations": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", - "allOf": [ + "proxy": { + "description": "Entity who bonded this gateway on behalf of the owner. If exists, it's most likely the address of the vesting contract.", + "anyOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/Addr" + }, + { + "type": "null" } ] } }, "additionalProperties": false }, - "PendingMixNodeChanges": { - "type": "object", - "properties": { - "pledge_change": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, - "Percent": { - "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" } } }, - "get_c_w2_contract_version": { + "get_gateways": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ContractVersion", + "title": "PagedGatewayResponse", + "description": "Response containing paged list of all gateway bonds in the contract.", "type": "object", "required": [ - "contract", - "version" + "nodes", + "per_page" ], "properties": { - "contract": { - "description": "contract is the crate name of the implementing contract, eg. `crate:cw20-base` we will use other prefixes for other languages, and their standard global namespacing", - "type": "string" + "nodes": { + "description": "The gateway bond information present in the contract.", + "type": "array", + "items": { + "$ref": "#/definitions/GatewayBond" + } }, - "version": { - "description": "version is any string that this implementation knows. It may be simple counter \"1\", \"2\". or semantic version on release tags \"v0.7.0\", or some custom feature flag list. the only code that needs to understand the version parsing is code that knows how to migrate from the given contract (and is tied to it's implementation somehow)", - "type": "string" + "per_page": { + "description": "Maximum number of entries that could be included in a response. `per_page <= nodes.len()`", + "type": "integer", + "format": "uint", + "minimum": 0.0 + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "string", + "null" + ] } }, - "additionalProperties": false - }, - "get_contract_version": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "ContractBuildInformation", - "type": "object", - "required": [ - "build_timestamp", - "build_version", - "commit_branch", - "commit_sha", - "commit_timestamp", - "rustc_version" - ], - "properties": { - "build_timestamp": { - "description": "Provides the build timestamp, for example `2021-02-23T20:14:46.558472672+00:00`.", - "type": "string" - }, - "build_version": { - "description": "Provides the build version, for example `0.1.0-9-g46f83e1`.", - "type": "string" - }, - "cargo_debug": { - "description": "Provides the cargo debug mode that was used for the build.", - "default": "unknown", - "type": "string" - }, - "cargo_opt_level": { - "description": "Provides the opt value set by cargo during the build", - "default": "unknown", - "type": "string" - }, - "commit_branch": { - "description": "Provides the name of the git branch that was used for the build, for example `master`.", + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", "type": "string" }, - "commit_sha": { - "description": "Provides the hash of the commit that was used for the build, for example `46f83e112520533338245862d366f6a02cef07d4`.", - "type": "string" + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } }, - "commit_timestamp": { - "description": "Provides the timestamp of the commit that was used for the build, for example `2021-02-23T08:08:02-05:00`.", - "type": "string" + "Gateway": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "clients_port", + "host", + "identity_key", + "location", + "mix_port", + "sphinx_key", + "version" + ], + "properties": { + "clients_port": { + "description": "Port used by this gateway for listening for client requests.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this gateway, for example 1.1.1.1 or foo.gateway.com", + "type": "string" + }, + "identity_key": { + "description": "Base58 encoded ed25519 EdDSA public key of the gateway used to derive shared keys with clients", + "type": "string" + }, + "location": { + "description": "The physical, self-reported, location of this gateway.", + "type": "string" + }, + "mix_port": { + "description": "Port used by this gateway for listening for mix packets.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "sphinx_key": { + "description": "Base58-encoded x25519 public key used for sphinx key derivation.", + "type": "string" + }, + "version": { + "description": "The self-reported semver version of this gateway.", + "type": "string" + } + }, + "additionalProperties": false }, - "contract_name": { - "description": "Provides the name of the binary, i.e. the content of `CARGO_PKG_NAME` environmental variable.", - "default": "unknown", - "type": "string" + "GatewayBond": { + "description": "Basic gateway information provided by the node operator.", + "type": "object", + "required": [ + "block_height", + "gateway", + "owner", + "pledge_amount" + ], + "properties": { + "block_height": { + "description": "Block height at which this gateway has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "gateway": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/Gateway" + } + ] + }, + "owner": { + "description": "Address of the owner of this gateway.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "pledge_amount": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "proxy": { + "description": "Entity who bonded this gateway on behalf of the owner. If exists, it's most likely the address of the vesting contract.", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false }, - "rustc_version": { - "description": "Provides the rustc version that was used for the build, for example `1.52.0-nightly`.", + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" } - }, - "additionalProperties": false + } }, - "get_current_interval_details": { + "get_mix_node_bonds": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "CurrentIntervalResponse", - "description": "Information about the current rewarding interval.", + "title": "PagedMixnodeBondsResponse", + "description": "Response containing paged list of all mixnode bonds in the contract.", "type": "object", "required": [ - "current_blocktime", - "interval", - "is_current_epoch_over", - "is_current_interval_over" + "nodes", + "per_page" ], "properties": { - "current_blocktime": { - "description": "The current blocktime", + "nodes": { + "description": "The mixnode bond information present in the contract.", + "type": "array", + "items": { + "$ref": "#/definitions/MixNodeBond" + } + }, + "per_page": { + "description": "Maximum number of entries that could be included in a response. `per_page <= nodes.len()`", "type": "integer", - "format": "uint64", + "format": "uint", "minimum": 0.0 }, - "interval": { - "description": "Detailed information about the underlying interval.", - "allOf": [ - { - "$ref": "#/definitions/Interval" - } - ] - }, - "is_current_epoch_over": { - "description": "Flag indicating whether the current epoch is over and it should be advanced.", - "type": "boolean" - }, - "is_current_interval_over": { - "description": "Flag indicating whether the current interval is over and it should be advanced.", - "type": "boolean" + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false, "definitions": { - "Duration": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { "type": "object", "required": [ - "nanos", - "secs" + "amount", + "denom" ], "properties": { - "nanos": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "amount": { + "$ref": "#/definitions/Uint128" }, - "secs": { - "type": "integer", - "format": "uint64", - "minimum": 0.0 + "denom": { + "type": "string" } } }, - "Interval": { + "MixNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", "required": [ - "current_epoch_id", - "current_epoch_start", - "epoch_length", - "epochs_in_interval", - "id", - "total_elapsed_epochs" + "host", + "http_api_port", + "identity_key", + "mix_port", + "sphinx_key", + "verloc_port", + "version" ], "properties": { - "current_epoch_id": { + "host": { + "description": "Network address of this mixnode, for example 1.1.1.1 or foo.mixnode.com", + "type": "string" + }, + "http_api_port": { + "description": "Port used by this mixnode for its http(s) API", "type": "integer", - "format": "uint32", + "format": "uint16", "minimum": 0.0 }, - "current_epoch_start": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", "type": "string" }, - "epoch_length": { - "$ref": "#/definitions/Duration" + "mix_port": { + "description": "Port used by this mixnode for listening for mix packets.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 }, - "epochs_in_interval": { + "sphinx_key": { + "description": "Base58-encoded x25519 public key used for sphinx key derivation.", + "type": "string" + }, + "verloc_port": { + "description": "Port used by this mixnode for listening for verloc requests.", "type": "integer", - "format": "uint32", + "format": "uint16", "minimum": 0.0 }, - "id": { + "version": { + "description": "The self-reported semver version of this mixnode.", + "type": "string" + } + }, + "additionalProperties": false + }, + "MixNodeBond": { + "description": "Basic mixnode information provided by the node operator.", + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "mix_id", + "mix_node", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this mixnode has been bonded.", "type": "integer", - "format": "uint32", + "format": "uint64", "minimum": 0.0 }, - "total_elapsed_epochs": { + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "mix_id": { + "description": "Unique id assigned to the bonded mixnode.", "type": "integer", "format": "uint32", "minimum": 0.0 + }, + "mix_node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/MixNode" + } + ] + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this mixnode.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "proxy": { + "description": "Entity who bonded this mixnode on behalf of the owner. If exists, it's most likely the address of the vesting contract.", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] } } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" } } }, - "get_delegation_details": { + "get_mix_nodes_detailed": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MixNodeDelegationResponse", - "description": "Response containing delegation details.", + "title": "PagedMixnodesDetailsResponse", + "description": "Response containing paged list of all mixnode details in the contract.", "type": "object", "required": [ - "mixnode_still_bonded" + "nodes", + "per_page" ], "properties": { - "delegation": { - "description": "If the delegation exists, this field contains its detailed information.", - "anyOf": [ - { - "$ref": "#/definitions/Delegation" - }, - { - "type": "null" - } - ] + "nodes": { + "description": "All mixnode details stored in the contract. Apart from the basic bond information it also contains details required for all future reward calculation as well as any pending changes requested by the operator.", + "type": "array", + "items": { + "$ref": "#/definitions/MixNodeDetails" + } + }, + "per_page": { + "description": "Maximum number of entries that could be included in a response. `per_page <= nodes.len()`", + "type": "integer", + "format": "uint", + "minimum": 0.0 }, - "mixnode_still_bonded": { - "description": "Flag indicating whether the node towards which the delegation was made is still bonded in the network.", - "type": "boolean" + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false, @@ -3783,47 +4971,102 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Delegation": { - "description": "Information about tokens being delegated towards given mixnode in order to accrue rewards with their work.", + "MixNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", "required": [ - "amount", - "cumulative_reward_ratio", - "height", + "host", + "http_api_port", + "identity_key", + "mix_port", + "sphinx_key", + "verloc_port", + "version" + ], + "properties": { + "host": { + "description": "Network address of this mixnode, for example 1.1.1.1 or foo.mixnode.com", + "type": "string" + }, + "http_api_port": { + "description": "Port used by this mixnode for its http(s) API", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "mix_port": { + "description": "Port used by this mixnode for listening for mix packets.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "sphinx_key": { + "description": "Base58-encoded x25519 public key used for sphinx key derivation.", + "type": "string" + }, + "verloc_port": { + "description": "Port used by this mixnode for listening for verloc requests.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "version": { + "description": "The self-reported semver version of this mixnode.", + "type": "string" + } + }, + "additionalProperties": false + }, + "MixNodeBond": { + "description": "Basic mixnode information provided by the node operator.", + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", "mix_id", + "mix_node", + "original_pledge", "owner" ], "properties": { - "amount": { - "description": "Original delegation amount. Note that it is never mutated as delegation accumulates rewards.", + "bonding_height": { + "description": "Block height at which this mixnode has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "mix_id": { + "description": "Unique id assigned to the bonded mixnode.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mix_node": { + "description": "Information provided by the operator for the purposes of bonding.", "allOf": [ { - "$ref": "#/definitions/Coin" + "$ref": "#/definitions/MixNode" } ] }, - "cumulative_reward_ratio": { - "description": "Value of the \"unit delegation\" associated with the mixnode at the time of delegation.", + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/Coin" } ] }, - "height": { - "description": "Block height where this delegation occurred.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "owner": { - "description": "Address of the owner of this delegation.", + "description": "Address of the owner of this mixnode.", "allOf": [ { "$ref": "#/definitions/Addr" @@ -3831,7 +5074,7 @@ ] }, "proxy": { - "description": "Proxy address used to delegate the funds on behalf of another address", + "description": "Entity who bonded this mixnode on behalf of the owner. If exists, it's most likely the address of the vesting contract.", "anyOf": [ { "$ref": "#/definitions/Addr" @@ -3841,8 +5084,169 @@ } ] } - }, - "additionalProperties": false + } + }, + "MixNodeDetails": { + "description": "Full details associated with given mixnode.", + "type": "object", + "required": [ + "bond_information", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this mixnode, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/MixNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", + "default": { + "cost_params_change": null, + "pledge_change": null + }, + "allOf": [ + { + "$ref": "#/definitions/PendingMixNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "PendingMixNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", @@ -3850,40 +5254,31 @@ } } }, - "get_delegator_delegations": { + "get_mixnode_details": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedDelegatorDelegationsResponse", - "description": "Response containing paged list of all delegations made by the particular address.", + "title": "MixnodeDetailsResponse", + "description": "Response containing details of a mixnode with the provided id.", "type": "object", "required": [ - "delegations" + "mix_id" ], "properties": { - "delegations": { - "description": "Each individual delegation made.", - "type": "array", - "items": { - "$ref": "#/definitions/Delegation" - } + "mix_id": { + "description": "Id of the requested mixnode.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 }, - "start_next_after": { - "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", - "type": [ - "array", - "null" - ], - "items": [ + "mixnode_details": { + "description": "If there exists a mixnode with the provided id, this field contains its detailed information.", + "anyOf": [ { - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "$ref": "#/definitions/MixNodeDetails" }, { - "type": "string" + "type": "null" } - ], - "maxItems": 2, - "minItems": 2 + ] } }, "additionalProperties": false, @@ -3911,310 +5306,309 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Delegation": { - "description": "Information about tokens being delegated towards given mixnode in order to accrue rewards with their work.", + "MixNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", "required": [ - "amount", - "cumulative_reward_ratio", - "height", - "mix_id", - "owner" + "host", + "http_api_port", + "identity_key", + "mix_port", + "sphinx_key", + "verloc_port", + "version" ], "properties": { - "amount": { - "description": "Original delegation amount. Note that it is never mutated as delegation accumulates rewards.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] + "host": { + "description": "Network address of this mixnode, for example 1.1.1.1 or foo.mixnode.com", + "type": "string" }, - "cumulative_reward_ratio": { - "description": "Value of the \"unit delegation\" associated with the mixnode at the time of delegation.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] + "http_api_port": { + "description": "Port used by this mixnode for its http(s) API", + "type": "integer", + "format": "uint16", + "minimum": 0.0 }, - "height": { - "description": "Block height where this delegation occurred.", + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "mix_port": { + "description": "Port used by this mixnode for listening for mix packets.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "sphinx_key": { + "description": "Base58-encoded x25519 public key used for sphinx key derivation.", + "type": "string" + }, + "verloc_port": { + "description": "Port used by this mixnode for listening for verloc requests.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "version": { + "description": "The self-reported semver version of this mixnode.", + "type": "string" + } + }, + "additionalProperties": false + }, + "MixNodeBond": { + "description": "Basic mixnode information provided by the node operator.", + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "mix_id", + "mix_node", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this mixnode has been bonded.", "type": "integer", "format": "uint64", "minimum": 0.0 }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "description": "Unique id assigned to the bonded mixnode.", "type": "integer", "format": "uint32", "minimum": 0.0 }, - "owner": { - "description": "Address of the owner of this delegation.", + "mix_node": { + "description": "Information provided by the operator for the purposes of bonding.", "allOf": [ { - "$ref": "#/definitions/Addr" + "$ref": "#/definitions/MixNode" } ] }, - "proxy": { - "description": "Proxy address used to delegate the funds on behalf of another address", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "get_epoch_status": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "EpochStatus", - "description": "The status of the current rewarding epoch.", - "type": "object", - "required": [ - "being_advanced_by", - "state" - ], - "properties": { - "being_advanced_by": { - "description": "Specifies either, which validator is currently performing progression into the following epoch (if the epoch is currently being progressed), or which validator was responsible for progressing into the current epoch (if the epoch is currently in progress)", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "state": { - "description": "The concrete state of the epoch.", - "allOf": [ - { - "$ref": "#/definitions/EpochState" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "EpochState": { - "description": "The state of the current rewarding epoch.", - "oneOf": [ - { - "description": "Represents the state of an epoch that's in progress (well, duh.) All actions are allowed to be issued.", - "type": "string", - "enum": [ - "in_progress" - ] - }, - { - "description": "Represents the state of an epoch when the rewarding entity has been decided on, and the mixnodes are in the process of being rewarded for their work in this epoch.", - "type": "object", - "required": [ - "rewarding" - ], - "properties": { - "rewarding": { - "type": "object", - "required": [ - "final_node_id", - "last_rewarded" - ], - "properties": { - "final_node_id": { - "description": "The id of the last node that's going to be rewarded before progressing into the next state.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "last_rewarded": { - "description": "The id of the last node that has already received its rewards.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false + "$ref": "#/definitions/Coin" } - }, - "additionalProperties": false - }, - { - "description": "Represents the state of an epoch when all mixnodes have already been rewarded for their work in this epoch and all issued actions should now get resolved before being allowed to advance into the next epoch.", - "type": "string", - "enum": [ - "reconciling_events" ] }, - { - "description": "Represents the state of an epoch when all mixnodes have already been rewarded for their work in this epoch, all issued actions got resolved and the epoch should now be advanced whilst assigning new rewarded set.", - "type": "string", - "enum": [ - "advancing_epoch" + "owner": { + "description": "Address of the owner of this mixnode.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } ] - } - ] - } - } - }, - "get_estimated_current_epoch_delegator_reward": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "EstimatedCurrentEpochRewardResponse", - "description": "Response containing estimation of node rewards for the current epoch.", - "type": "object", - "properties": { - "current_stake_value": { - "description": "The current stake value given all past rewarding and compounding since the original staking was performed.", - "anyOf": [ - { - "$ref": "#/definitions/Coin" }, - { - "type": "null" + "proxy": { + "description": "Entity who bonded this mixnode on behalf of the owner. If exists, it's most likely the address of the vesting contract.", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] } - ] + } }, - "current_stake_value_detailed_amount": { - "description": "The current stake value. Note that it's nearly identical to `current_stake_value`, however, it contains few additional decimal points for more accurate reward calculation.", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" + "MixNodeDetails": { + "description": "Full details associated with given mixnode.", + "type": "object", + "required": [ + "bond_information", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this mixnode, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/MixNodeBond" + } + ] }, - { - "type": "null" - } - ] - }, - "detailed_estimation_amount": { - "description": "The full reward estimation. Note that it's nearly identical to `estimation`, however, it contains few additional decimal points for more accurate reward calculation.", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" + "pending_changes": { + "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", + "default": { + "cost_params_change": null, + "pledge_change": null + }, + "allOf": [ + { + "$ref": "#/definitions/PendingMixNodeChanges" + } + ] }, - { - "type": "null" + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] } - ] + }, + "additionalProperties": false }, - "estimation": { - "description": "The reward estimation for the current epoch, i.e. the amount of tokens that could be claimable after the epoch finishes and the state of the network does not change.", - "anyOf": [ - { - "$ref": "#/definitions/Coin" + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] }, - { - "type": "null" + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] } - ] + }, + "additionalProperties": false }, - "original_stake": { - "description": "The amount of tokens initially staked.", - "anyOf": [ - { - "$ref": "#/definitions/Coin" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Coin": { + "NodeRewarding": { "type": "object", "required": [ - "amount", - "denom" + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" ], "properties": { - "amount": { - "$ref": "#/definitions/Uint128" + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] }, - "denom": { - "type": "string" - } - } - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "get_estimated_current_epoch_operator_reward": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "EstimatedCurrentEpochRewardResponse", - "description": "Response containing estimation of node rewards for the current epoch.", - "type": "object", - "properties": { - "current_stake_value": { - "description": "The current stake value given all past rewarding and compounding since the original staking was performed.", - "anyOf": [ - { - "$ref": "#/definitions/Coin" + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] }, - { - "type": "null" - } - ] - }, - "current_stake_value_detailed_amount": { - "description": "The current stake value. Note that it's nearly identical to `current_stake_value`, however, it contains few additional decimal points for more accurate reward calculation.", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 }, - { - "type": "null" + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] } - ] + }, + "additionalProperties": false }, - "detailed_estimation_amount": { - "description": "The full reward estimation. Note that it's nearly identical to `estimation`, however, it contains few additional decimal points for more accurate reward calculation.", - "anyOf": [ - { - "$ref": "#/definitions/Decimal" + "PendingMixNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 }, - { - "type": "null" + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 } - ] + } }, - "estimation": { - "description": "The reward estimation for the current epoch, i.e. the amount of tokens that could be claimable after the epoch finishes and the state of the network does not change.", - "anyOf": [ - { - "$ref": "#/definitions/Coin" - }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ { - "type": "null" + "$ref": "#/definitions/Decimal" } ] }, - "original_stake": { - "description": "The amount of tokens initially staked.", + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_mixnode_rewarding_details": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "MixnodeRewardingDetailsResponse", + "description": "Response containing rewarding information of a mixnode with the provided id.", + "type": "object", + "required": [ + "mix_id" + ], + "properties": { + "mix_id": { + "description": "Id of the requested mixnode.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "rewarding_details": { + "description": "If there exists a mixnode with the provided id, this field contains its rewarding information.", "anyOf": [ { - "$ref": "#/definitions/Coin" + "$ref": "#/definitions/NodeRewarding" }, { "type": "null" @@ -4243,223 +5637,135 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "get_family_by_head": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "FamilyByHeadResponse", - "description": "Response containing family information.", - "type": "object", - "required": [ - "head" - ], - "properties": { - "family": { - "description": "If applicable, the family associated with the provided head.", - "anyOf": [ - { - "$ref": "#/definitions/Family" - }, - { - "type": "null" - } - ] - }, - "head": { - "description": "The family head used for the query.", - "allOf": [ - { - "$ref": "#/definitions/FamilyHead" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Family": { - "description": "A group of mixnodes associated with particular staking entity. When defined all nodes belonging to the same family will be prioritised to be put onto the same layer.", + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ - "head", - "label" + "interval_operating_cost", + "profit_margin_percent" ], "properties": { - "head": { - "description": "Owner of this family.", + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { - "$ref": "#/definitions/FamilyHead" + "$ref": "#/definitions/Coin" } ] }, - "label": { - "description": "Human readable label for this family.", - "type": "string" - }, - "proxy": { - "description": "Optional proxy (i.e. vesting contract address) used when creating the family.", - "type": [ - "string", - "null" + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } ] } }, "additionalProperties": false }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - } - } - }, - "get_family_by_label": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "FamilyByLabelResponse", - "description": "Response containing family information.", - "type": "object", - "required": [ - "label" - ], - "properties": { - "family": { - "description": "If applicable, the family associated with the provided label.", - "anyOf": [ - { - "$ref": "#/definitions/Family" - }, - { - "type": "null" - } - ] - }, - "label": { - "description": "The family label used for the query.", - "type": "string" - } - }, - "additionalProperties": false, - "definitions": { - "Family": { - "description": "A group of mixnodes associated with particular staking entity. When defined all nodes belonging to the same family will be prioritised to be put onto the same layer.", + "NodeRewarding": { "type": "object", "required": [ - "head", - "label" + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" ], "properties": { - "head": { - "description": "Owner of this family.", + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/FamilyHead" + "$ref": "#/definitions/NodeCostParams" } ] }, - "label": { - "description": "Human readable label for this family.", - "type": "string" + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] }, - "proxy": { - "description": "Optional proxy (i.e. vesting contract address) used when creating the family.", - "type": [ - "string", - "null" + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } ] } }, "additionalProperties": false }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - } - } - }, - "get_family_members_by_head": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "FamilyMembersByHeadResponse", - "description": "Response containing family members information.", - "type": "object", - "required": [ - "head", - "members" - ], - "properties": { - "head": { - "description": "The family head used for the query.", + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", "allOf": [ { - "$ref": "#/definitions/FamilyHead" + "$ref": "#/definitions/Decimal" } ] }, - "members": { - "description": "All members belonging to the specified family.", - "type": "array", - "items": { - "type": "string" - } - } - }, - "additionalProperties": false, - "definitions": { - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" } } }, - "get_family_members_by_label": { + "get_node_delegations": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "FamilyMembersByLabelResponse", - "description": "Response containing family members information.", + "title": "PagedNodeDelegationsResponse", + "description": "Response containing paged list of all delegations made towards particular node.", "type": "object", "required": [ - "label", - "members" + "delegations" ], "properties": { - "label": { - "description": "The family label used for the query.", - "type": "string" - }, - "members": { - "description": "All members belonging to the specified family.", + "delegations": { + "description": "Each individual delegation made.", "type": "array", "items": { - "type": "string" + "$ref": "#/definitions/Delegation" } - } - }, - "additionalProperties": false - }, - "get_gateway_bond": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "GatewayBondResponse", - "description": "Response containing details of a gateway with the provided identity key.", - "type": "object", - "required": [ - "identity" - ], - "properties": { - "gateway": { - "description": "If there exists a gateway with the provided identity key, this field contains its details.", - "anyOf": [ - { - "$ref": "#/definitions/GatewayBond" - }, - { - "type": "null" - } - ] }, - "identity": { - "description": "The identity key (base58-encoded ed25519 public key) of the gateway.", - "type": "string" + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "string", + "null" + ] } }, "additionalProperties": false, @@ -4477,102 +5783,65 @@ "properties": { "amount": { "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - } - } - }, - "Gateway": { - "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", - "type": "object", - "required": [ - "clients_port", - "host", - "identity_key", - "location", - "mix_port", - "sphinx_key", - "version" - ], - "properties": { - "clients_port": { - "description": "Port used by this gateway for listening for client requests.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "host": { - "description": "Network address of this gateway, for example 1.1.1.1 or foo.gateway.com", - "type": "string" - }, - "identity_key": { - "description": "Base58 encoded ed25519 EdDSA public key of the gateway used to derive shared keys with clients", - "type": "string" - }, - "location": { - "description": "The physical, self-reported, location of this gateway.", - "type": "string" - }, - "mix_port": { - "description": "Port used by this gateway for listening for mix packets.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "sphinx_key": { - "description": "Base58-encoded x25519 public key used for sphinx key derivation.", - "type": "string" - }, - "version": { - "description": "The self-reported semver version of this gateway.", + }, + "denom": { "type": "string" } - }, - "additionalProperties": false + } }, - "GatewayBond": { - "description": "Basic gateway information provided by the node operator.", + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Delegation": { + "description": "Information about tokens being delegated towards given mixnode in order to accrue rewards with their work.", "type": "object", "required": [ - "block_height", - "gateway", - "owner", - "pledge_amount" + "amount", + "cumulative_reward_ratio", + "height", + "node_id", + "owner" ], "properties": { - "block_height": { - "description": "Block height at which this gateway has been bonded.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "gateway": { - "description": "Information provided by the operator for the purposes of bonding.", + "amount": { + "description": "Original delegation amount. Note that it is never mutated as delegation accumulates rewards.", "allOf": [ { - "$ref": "#/definitions/Gateway" + "$ref": "#/definitions/Coin" } ] }, - "owner": { - "description": "Address of the owner of this gateway.", + "cumulative_reward_ratio": { + "description": "Value of the \"unit delegation\" associated with the mixnode at the time of delegation.", "allOf": [ { - "$ref": "#/definitions/Addr" + "$ref": "#/definitions/Decimal" } ] }, - "pledge_amount": { - "description": "Original amount pledged by the operator of this node.", + "height": { + "description": "Block height where this delegation occurred.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "node_id": { + "description": "Id of the Node that this delegation was performed against.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this delegation.", "allOf": [ { - "$ref": "#/definitions/Coin" + "$ref": "#/definitions/Addr" } ] }, "proxy": { - "description": "Entity who bonded this gateway on behalf of the owner. If exists, it's most likely the address of the vesting contract.", + "description": "Proxy address used to delegate the funds on behalf of another address", "anyOf": [ { "$ref": "#/definitions/Addr" @@ -4591,43 +5860,35 @@ } } }, - "get_gateways": { + "get_node_rewarding_details": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedGatewayResponse", - "description": "Response containing paged list of all gateway bonds in the contract.", + "title": "NodeRewardingDetailsResponse", + "description": "Response containing rewarding information of a node with the provided id.", "type": "object", "required": [ - "nodes", - "per_page" + "node_id" ], "properties": { - "nodes": { - "description": "The gateway bond information present in the contract.", - "type": "array", - "items": { - "$ref": "#/definitions/GatewayBond" - } - }, - "per_page": { - "description": "Maximum number of entries that could be included in a response. `per_page <= nodes.len()`", + "node_id": { + "description": "Id of the requested node.", "type": "integer", - "format": "uint", + "format": "uint32", "minimum": 0.0 }, - "start_next_after": { - "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", - "type": [ - "string", - "null" + "rewarding_details": { + "description": "If there exists a node with the provided id, this field contains its rewarding information.", + "anyOf": [ + { + "$ref": "#/definitions/NodeRewarding" + }, + { + "type": "null" + } ] } }, "additionalProperties": false, "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, "Coin": { "type": "object", "required": [ @@ -4643,169 +5904,203 @@ } } }, - "Gateway": { - "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ - "clients_port", - "host", - "identity_key", - "location", - "mix_port", - "sphinx_key", - "version" + "interval_operating_cost", + "profit_margin_percent" ], "properties": { - "clients_port": { - "description": "Port used by this gateway for listening for client requests.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "host": { - "description": "Network address of this gateway, for example 1.1.1.1 or foo.gateway.com", - "type": "string" - }, - "identity_key": { - "description": "Base58 encoded ed25519 EdDSA public key of the gateway used to derive shared keys with clients", - "type": "string" - }, - "location": { - "description": "The physical, self-reported, location of this gateway.", - "type": "string" - }, - "mix_port": { - "description": "Port used by this gateway for listening for mix packets.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "sphinx_key": { - "description": "Base58-encoded x25519 public key used for sphinx key derivation.", - "type": "string" + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] }, - "version": { - "description": "The self-reported semver version of this gateway.", - "type": "string" + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] } }, "additionalProperties": false }, - "GatewayBond": { - "description": "Basic gateway information provided by the node operator.", + "NodeRewarding": { "type": "object", "required": [ - "block_height", - "gateway", - "owner", - "pledge_amount" + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" ], "properties": { - "block_height": { - "description": "Block height at which this gateway has been bonded.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "gateway": { - "description": "Information provided by the operator for the purposes of bonding.", + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/Gateway" + "$ref": "#/definitions/NodeCostParams" } ] }, - "owner": { - "description": "Address of the owner of this gateway.", + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", "allOf": [ { - "$ref": "#/definitions/Addr" + "$ref": "#/definitions/Decimal" } ] }, - "pledge_amount": { - "description": "Original amount pledged by the operator of this node.", + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", "allOf": [ { - "$ref": "#/definitions/Coin" + "$ref": "#/definitions/Decimal" } ] }, - "proxy": { - "description": "Entity who bonded this gateway on behalf of the owner. If exists, it's most likely the address of the vesting contract.", - "anyOf": [ + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ { - "$ref": "#/definitions/Addr" - }, + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ { - "type": "null" + "$ref": "#/definitions/Decimal" } ] } - }, - "additionalProperties": false - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_node_stake_saturation": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "StakeSaturationResponse", + "description": "Response containing the current state of the stake saturation of a node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "current_saturation": { + "description": "The current stake saturation of this node that is indirectly used in reward calculation formulas. Note that it can't be larger than 1.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "uncapped_saturation": { + "description": "The current, absolute, stake saturation of this node. Note that as the name suggests it can be larger than 1. However, anything beyond that value has no effect on the total node reward.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" } } }, - "get_layer_distribution": { + "get_number_of_pending_events": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "LayerDistribution", - "description": "The current layer distribution of the mix network.", + "title": "NumberOfPendingEventsResponse", + "description": "Response containing number of currently pending epoch and interval events.", "type": "object", "required": [ - "layer1", - "layer2", - "layer3" + "epoch_events", + "interval_events" ], "properties": { - "layer1": { - "description": "Number of nodes on the first layer.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "layer2": { - "description": "Number of nodes on the second layer.", + "epoch_events": { + "description": "The number of the currently pending epoch events.", "type": "integer", - "format": "uint64", + "format": "uint32", "minimum": 0.0 }, - "layer3": { - "description": "Number of nodes on the third layer.", + "interval_events": { + "description": "The number of the currently pending epoch events.", "type": "integer", - "format": "uint64", + "format": "uint32", "minimum": 0.0 } }, "additionalProperties": false }, - "get_mix_node_bonds": { + "get_nym_node_bonds_paged": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedMixnodeBondsResponse", - "description": "Response containing paged list of all mixnode bonds in the contract.", + "title": "PagedNymNodeBondsResponse", "type": "object", "required": [ - "nodes", - "per_page" + "nodes" ], "properties": { "nodes": { - "description": "The mixnode bond information present in the contract.", + "description": "The nym node bond information present in the contract.", "type": "array", "items": { - "$ref": "#/definitions/MixNodeBond" + "$ref": "#/definitions/NymNodeBond" } }, - "per_page": { - "description": "Maximum number of entries that could be included in a response. `per_page <= nodes.len()`", - "type": "integer", - "format": "uint", - "minimum": 0.0 - }, "start_next_after": { "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", "type": [ @@ -4837,79 +6132,47 @@ } } }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, - "MixNode": { + "NymNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", "required": [ "host", - "http_api_port", - "identity_key", - "mix_port", - "sphinx_key", - "verloc_port", - "version" + "identity_key" ], "properties": { - "host": { - "description": "Network address of this mixnode, for example 1.1.1.1 or foo.mixnode.com", - "type": "string" - }, - "http_api_port": { - "description": "Port used by this mixnode for its http(s) API", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "identity_key": { - "description": "Base58-encoded ed25519 EdDSA public key.", - "type": "string" - }, - "mix_port": { - "description": "Port used by this mixnode for listening for mix packets.", - "type": "integer", + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], "format": "uint16", "minimum": 0.0 }, - "sphinx_key": { - "description": "Base58-encoded x25519 public key used for sphinx key derivation.", + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", "type": "string" }, - "verloc_port": { - "description": "Port used by this mixnode for listening for verloc requests.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "version": { - "description": "The self-reported semver version of this mixnode.", + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", "type": "string" } }, "additionalProperties": false }, - "MixNodeBond": { - "description": "Basic mixnode information provided by the node operator.", + "NymNodeBond": { "type": "object", "required": [ "bonding_height", "is_unbonding", - "layer", - "mix_id", - "mix_node", + "node", + "node_id", "original_pledge", "owner" ], "properties": { "bonding_height": { - "description": "Block height at which this mixnode has been bonded.", + "description": "Block height at which this nym-node has been bonded.", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -4918,28 +6181,20 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", + "node": { + "description": "Information provided by the operator for the purposes of bonding.", "allOf": [ { - "$ref": "#/definitions/Layer" + "$ref": "#/definitions/NymNode" } ] }, - "mix_id": { - "description": "Unique id assigned to the bonded mixnode.", + "node_id": { + "description": "Unique id assigned to the bonded node.", "type": "integer", "format": "uint32", "minimum": 0.0 }, - "mix_node": { - "description": "Information provided by the operator for the purposes of bonding.", - "allOf": [ - { - "$ref": "#/definitions/MixNode" - } - ] - }, "original_pledge": { "description": "Original amount pledged by the operator of this node.", "allOf": [ @@ -4949,23 +6204,12 @@ ] }, "owner": { - "description": "Address of the owner of this mixnode.", + "description": "Address of the owner of this nym-node.", "allOf": [ { "$ref": "#/definitions/Addr" } ] - }, - "proxy": { - "description": "Entity who bonded this mixnode on behalf of the owner. If exists, it's most likely the address of the vesting contract.", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] } }, "additionalProperties": false @@ -4976,35 +6220,29 @@ } } }, - "get_mix_nodes_detailed": { + "get_nym_node_details": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedMixnodesDetailsResponse", - "description": "Response containing paged list of all mixnode details in the contract.", + "title": "NodeDetailsResponse", + "description": "Response containing details of a node with the provided id.", "type": "object", "required": [ - "nodes", - "per_page" + "node_id" ], "properties": { - "nodes": { - "description": "All mixnode details stored in the contract. Apart from the basic bond information it also contains details required for all future reward calculation as well as any pending changes requested by the operator.", - "type": "array", - "items": { - "$ref": "#/definitions/MixNodeDetails" - } + "details": { + "description": "If there exists a node with the provided id, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] }, - "per_page": { - "description": "Maximum number of entries that could be included in a response. `per_page <= nodes.len()`", + "node_id": { + "description": "Id of the requested node.", "type": "integer", - "format": "uint", - "minimum": 0.0 - }, - "start_next_after": { - "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", - "type": [ - "integer", - "null" - ], "format": "uint32", "minimum": 0.0 } @@ -5034,79 +6272,140 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false }, - "MixNode": { - "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "NodeRewarding": { "type": "object", "required": [ - "host", - "http_api_port", - "identity_key", - "mix_port", - "sphinx_key", - "verloc_port", - "version" + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" ], "properties": { - "host": { - "description": "Network address of this mixnode, for example 1.1.1.1 or foo.mixnode.com", - "type": "string" + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] }, - "http_api_port": { - "description": "Port used by this mixnode for its http(s) API", + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", "type": "integer", - "format": "uint16", + "format": "uint32", "minimum": 0.0 }, - "identity_key": { - "description": "Base58-encoded ed25519 EdDSA public key.", - "type": "string" + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] }, - "mix_port": { - "description": "Port used by this mixnode for listening for mix packets.", + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { "type": "integer", - "format": "uint16", + "format": "uint32", "minimum": 0.0 }, - "sphinx_key": { - "description": "Base58-encoded x25519 public key used for sphinx key derivation.", - "type": "string" - }, - "verloc_port": { - "description": "Port used by this mixnode for listening for verloc requests.", - "type": "integer", + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], "format": "uint16", "minimum": 0.0 }, - "version": { - "description": "The self-reported semver version of this mixnode.", + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", "type": "string" } }, "additionalProperties": false }, - "MixNodeBond": { - "description": "Basic mixnode information provided by the node operator.", + "NymNodeBond": { "type": "object", "required": [ "bonding_height", "is_unbonding", - "layer", - "mix_id", - "mix_node", + "node", + "node_id", "original_pledge", "owner" ], "properties": { "bonding_height": { - "description": "Block height at which this mixnode has been bonded.", + "description": "Block height at which this nym-node has been bonded.", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -5115,28 +6414,20 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", + "node": { + "description": "Information provided by the operator for the purposes of bonding.", "allOf": [ { - "$ref": "#/definitions/Layer" + "$ref": "#/definitions/NymNode" } ] }, - "mix_id": { - "description": "Unique id assigned to the bonded mixnode.", + "node_id": { + "description": "Unique id assigned to the bonded node.", "type": "integer", "format": "uint32", "minimum": 0.0 }, - "mix_node": { - "description": "Information provided by the operator for the purposes of bonding.", - "allOf": [ - { - "$ref": "#/definitions/MixNode" - } - ] - }, "original_pledge": { "description": "Original amount pledged by the operator of this node.", "allOf": [ @@ -5146,93 +6437,166 @@ ] }, "owner": { - "description": "Address of the owner of this mixnode.", + "description": "Address of the owner of this nym-node.", "allOf": [ { "$ref": "#/definitions/Addr" } ] - }, - "proxy": { - "description": "Entity who bonded this mixnode on behalf of the owner. If exists, it's most likely the address of the vesting contract.", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] } }, "additionalProperties": false }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "NymNodeDetails": { + "description": "Full details associated with given node.", "type": "object", "required": [ - "interval_operating_cost", - "profit_margin_percent" + "bond_information", + "pending_changes", + "rewarding_details" ], "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", "allOf": [ { - "$ref": "#/definitions/Coin" + "$ref": "#/definitions/NymNodeBond" } ] }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", "allOf": [ { - "$ref": "#/definitions/Percent" + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeDetails": { - "description": "Full details associated with given mixnode.", + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } + }, + "get_nym_node_details_by_identity_key": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeDetailsByIdentityResponse", + "description": "Response containing details of a bonded node with the provided identity key.", + "type": "object", + "required": [ + "identity_key" + ], + "properties": { + "details": { + "description": "If there exists a bonded node with the provided identity key, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] + }, + "identity_key": { + "description": "The identity key (base58-encoded ed25519 public key) of the node.", + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { "type": "object", "required": [ - "bond_information", - "rewarding_details" + "amount", + "denom" ], "properties": { - "bond_information": { - "description": "Basic bond information of this mixnode, such as owner address, original pledge, etc.", - "allOf": [ - { - "$ref": "#/definitions/MixNodeBond" - } - ] + "amount": { + "$ref": "#/definitions/Uint128" }, - "pending_changes": { - "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", - "default": { - "pledge_change": null - }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { - "$ref": "#/definitions/PendingMixNodeChanges" + "$ref": "#/definitions/Coin" } ] }, - "rewarding_details": { - "description": "Details used for computation of rewarding related data.", + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/Percent" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -5248,7 +6612,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -5288,7 +6652,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -5298,9 +6662,135 @@ }, "additionalProperties": false }, - "PendingMixNodeChanges": { + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", @@ -5326,28 +6816,29 @@ } } }, - "get_mixnode_delegations": { + "get_nym_nodes_detailed_paged": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedMixNodeDelegationsResponse", - "description": "Response containing paged list of all delegations made towards particular mixnode.", + "title": "PagedNymNodeDetailsResponse", "type": "object", "required": [ - "delegations" + "nodes" ], "properties": { - "delegations": { - "description": "Each individual delegation made.", + "nodes": { + "description": "All nym-node details stored in the contract. Apart from the basic bond information it also contains details required for all future reward calculation as well as any pending changes requested by the operator.", "type": "array", "items": { - "$ref": "#/definitions/Delegation" + "$ref": "#/definitions/NymNodeDetails" } }, "start_next_after": { "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", "type": [ - "string", + "integer", "null" - ] + ], + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false, @@ -5375,198 +6866,140 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Delegation": { - "description": "Information about tokens being delegated towards given mixnode in order to accrue rewards with their work.", + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ - "amount", - "cumulative_reward_ratio", - "height", - "mix_id", - "owner" + "interval_operating_cost", + "profit_margin_percent" ], "properties": { - "amount": { - "description": "Original delegation amount. Note that it is never mutated as delegation accumulates rewards.", + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" } ] }, - "cumulative_reward_ratio": { - "description": "Value of the \"unit delegation\" associated with the mixnode at the time of delegation.", + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" } ] }, - "height": { - "description": "Block height where this delegation occurred.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", "type": "integer", "format": "uint32", "minimum": 0.0 }, - "owner": { - "description": "Address of the owner of this delegation.", + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", "allOf": [ { - "$ref": "#/definitions/Addr" + "$ref": "#/definitions/Decimal" } ] }, - "proxy": { - "description": "Proxy address used to delegate the funds on behalf of another address", - "anyOf": [ + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ { - "$ref": "#/definitions/Addr" - }, + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ { - "type": "null" + "$ref": "#/definitions/Decimal" } ] } }, "additionalProperties": false }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "get_mixnode_details": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MixnodeDetailsResponse", - "description": "Response containing details of a mixnode with the provided id.", - "type": "object", - "required": [ - "mix_id" - ], - "properties": { - "mix_id": { - "description": "Id of the requested mixnode.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "mixnode_details": { - "description": "If there exists a mixnode with the provided id, this field contains its detailed information.", - "anyOf": [ - { - "$ref": "#/definitions/MixNodeDetails" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false, - "definitions": { - "Addr": { - "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", - "type": "string" - }, - "Coin": { - "type": "object", - "required": [ - "amount", - "denom" - ], - "properties": { - "amount": { - "$ref": "#/definitions/Uint128" - }, - "denom": { - "type": "string" - } - } - }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, - "MixNode": { + "NymNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", "required": [ "host", - "http_api_port", - "identity_key", - "mix_port", - "sphinx_key", - "verloc_port", - "version" + "identity_key" ], "properties": { - "host": { - "description": "Network address of this mixnode, for example 1.1.1.1 or foo.mixnode.com", - "type": "string" - }, - "http_api_port": { - "description": "Port used by this mixnode for its http(s) API", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "identity_key": { - "description": "Base58-encoded ed25519 EdDSA public key.", - "type": "string" - }, - "mix_port": { - "description": "Port used by this mixnode for listening for mix packets.", - "type": "integer", + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], "format": "uint16", "minimum": 0.0 }, - "sphinx_key": { - "description": "Base58-encoded x25519 public key used for sphinx key derivation.", + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", "type": "string" }, - "verloc_port": { - "description": "Port used by this mixnode for listening for verloc requests.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "version": { - "description": "The self-reported semver version of this mixnode.", + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", "type": "string" } }, "additionalProperties": false }, - "MixNodeBond": { - "description": "Basic mixnode information provided by the node operator.", + "NymNodeBond": { "type": "object", "required": [ "bonding_height", "is_unbonding", - "layer", - "mix_id", - "mix_node", + "node", + "node_id", "original_pledge", "owner" ], "properties": { "bonding_height": { - "description": "Block height at which this mixnode has been bonded.", + "description": "Block height at which this nym-node has been bonded.", "type": "integer", "format": "uint64", "minimum": 0.0 @@ -5575,28 +7008,20 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", + "node": { + "description": "Information provided by the operator for the purposes of bonding.", "allOf": [ { - "$ref": "#/definitions/Layer" + "$ref": "#/definitions/NymNode" } ] }, - "mix_id": { - "description": "Unique id assigned to the bonded mixnode.", + "node_id": { + "description": "Unique id assigned to the bonded node.", "type": "integer", "format": "uint32", "minimum": 0.0 }, - "mix_node": { - "description": "Information provided by the operator for the purposes of bonding.", - "allOf": [ - { - "$ref": "#/definitions/MixNode" - } - ] - }, "original_pledge": { "description": "Original amount pledged by the operator of this node.", "allOf": [ @@ -5605,79 +7030,39 @@ } ] }, - "owner": { - "description": "Address of the owner of this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] - }, - "proxy": { - "description": "Entity who bonded this mixnode on behalf of the owner. If exists, it's most likely the address of the vesting contract.", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, - { - "type": "null" - } - ] - } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "owner": { + "description": "Address of the owner of this nym-node.", "allOf": [ { - "$ref": "#/definitions/Percent" + "$ref": "#/definitions/Addr" } ] } }, "additionalProperties": false }, - "MixNodeDetails": { - "description": "Full details associated with given mixnode.", + "NymNodeDetails": { + "description": "Full details associated with given node.", "type": "object", "required": [ "bond_information", + "pending_changes", "rewarding_details" ], "properties": { "bond_information": { - "description": "Basic bond information of this mixnode, such as owner address, original pledge, etc.", + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", "allOf": [ { - "$ref": "#/definitions/MixNodeBond" + "$ref": "#/definitions/NymNodeBond" } ] }, "pending_changes": { - "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", - "default": { - "pledge_change": null - }, + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", "allOf": [ { - "$ref": "#/definitions/PendingMixNodeChanges" + "$ref": "#/definitions/PendingNodeChanges" } ] }, @@ -5685,82 +7070,24 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "PendingNodeChanges": { "type": "object", - "required": [ - "cost_params", - "delegates", - "last_rewarded_epoch", - "operator", - "total_unit_reward", - "unique_delegations", - "unit_delegation" - ], "properties": { - "cost_params": { - "description": "Information provided by the operator that influence the cost function.", - "allOf": [ - { - "$ref": "#/definitions/MixNodeCostParams" - } - ] - }, - "delegates": { - "description": "Total delegation and compounded reward earned by all node delegators.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "last_rewarded_epoch": { - "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "operator": { - "description": "Total pledge and compounded reward earned by the node operator.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "total_unit_reward": { - "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "unique_delegations": { - "type": "integer", + "cost_params_change": { + "type": [ + "integer", + "null" + ], "format": "uint32", "minimum": 0.0 }, - "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - } - }, - "additionalProperties": false - }, - "PendingMixNodeChanges": { - "type": "object", - "properties": { "pledge_change": { "type": [ "integer", @@ -5786,26 +7113,28 @@ } } }, - "get_mixnode_rewarding_details": { + "get_owned_gateway": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MixnodeRewardingDetailsResponse", - "description": "Response containing rewarding information of a mixnode with the provided id.", + "title": "GatewayOwnershipResponse", + "description": "Response containing details of a gateway belonging to the particular owner.", "type": "object", "required": [ - "mix_id" + "address" ], "properties": { - "mix_id": { - "description": "Id of the requested mixnode.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "address": { + "description": "Validated address of the gateway owner.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] }, - "rewarding_details": { - "description": "If there exists a mixnode with the provided id, this field contains its rewarding information.", + "gateway": { + "description": "If the provided address owns a gateway, this field contains its details.", "anyOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/GatewayBond" }, { "type": "null" @@ -5815,6 +7144,10 @@ }, "additionalProperties": false, "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, "Coin": { "type": "object", "required": [ @@ -5830,164 +7163,136 @@ } } }, - "Decimal": { - "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", - "type": "string" - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "Gateway": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", "required": [ - "interval_operating_cost", - "profit_margin_percent" + "clients_port", + "host", + "identity_key", + "location", + "mix_port", + "sphinx_key", + "version" ], "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] + "clients_port": { + "description": "Port used by this gateway for listening for client requests.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] + "host": { + "description": "Network address of this gateway, for example 1.1.1.1 or foo.gateway.com", + "type": "string" + }, + "identity_key": { + "description": "Base58 encoded ed25519 EdDSA public key of the gateway used to derive shared keys with clients", + "type": "string" + }, + "location": { + "description": "The physical, self-reported, location of this gateway.", + "type": "string" + }, + "mix_port": { + "description": "Port used by this gateway for listening for mix packets.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, + "sphinx_key": { + "description": "Base58-encoded x25519 public key used for sphinx key derivation.", + "type": "string" + }, + "version": { + "description": "The self-reported semver version of this gateway.", + "type": "string" } }, "additionalProperties": false }, - "MixNodeRewarding": { + "GatewayBond": { + "description": "Basic gateway information provided by the node operator.", "type": "object", "required": [ - "cost_params", - "delegates", - "last_rewarded_epoch", - "operator", - "total_unit_reward", - "unique_delegations", - "unit_delegation" + "block_height", + "gateway", + "owner", + "pledge_amount" ], "properties": { - "cost_params": { - "description": "Information provided by the operator that influence the cost function.", - "allOf": [ - { - "$ref": "#/definitions/MixNodeCostParams" - } - ] - }, - "delegates": { - "description": "Total delegation and compounded reward earned by all node delegators.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "last_rewarded_epoch": { - "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "block_height": { + "description": "Block height at which this gateway has been bonded.", "type": "integer", - "format": "uint32", + "format": "uint64", "minimum": 0.0 }, - "operator": { - "description": "Total pledge and compounded reward earned by the node operator.", + "gateway": { + "description": "Information provided by the operator for the purposes of bonding.", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/Gateway" } ] }, - "total_unit_reward": { - "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "owner": { + "description": "Address of the owner of this gateway.", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/Addr" } ] }, - "unique_delegations": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "pledge_amount": { + "description": "Original amount pledged by the operator of this node.", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/Coin" } ] - } - }, - "additionalProperties": false - }, - "Percent": { - "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "Uint128": { - "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", - "type": "string" - } - } - }, - "get_number_of_pending_events": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "NumberOfPendingEventsResponse", - "description": "Response containing number of currently pending epoch and interval events.", - "type": "object", - "required": [ - "epoch_events", - "interval_events" - ], - "properties": { - "epoch_events": { - "description": "The number of the currently pending epoch events.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 + }, + "proxy": { + "description": "Entity who bonded this gateway on behalf of the owner. If exists, it's most likely the address of the vesting contract.", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false }, - "interval_events": { - "description": "The number of the currently pending epoch events.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" } - }, - "additionalProperties": false + } }, - "get_owned_gateway": { + "get_owned_mixnode": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "GatewayOwnershipResponse", - "description": "Response containing details of a gateway belonging to the particular owner.", + "title": "MixOwnershipResponse", + "description": "Response containing details of a mixnode belonging to the particular owner.", "type": "object", "required": [ "address" ], "properties": { "address": { - "description": "Validated address of the gateway owner.", + "description": "Validated address of the mixnode owner.", "allOf": [ { "$ref": "#/definitions/Addr" } ] }, - "gateway": { - "description": "If the provided address owns a gateway, this field contains its details.", + "mixnode_details": { + "description": "If the provided address owns a mixnode, this field contains its detailed information.", "anyOf": [ { - "$ref": "#/definitions/GatewayBond" + "$ref": "#/definitions/MixNodeDetails" }, { "type": "null" @@ -6016,39 +7321,39 @@ } } }, - "Gateway": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", "required": [ - "clients_port", "host", + "http_api_port", "identity_key", - "location", "mix_port", "sphinx_key", + "verloc_port", "version" ], "properties": { - "clients_port": { - "description": "Port used by this gateway for listening for client requests.", + "host": { + "description": "Network address of this mixnode, for example 1.1.1.1 or foo.mixnode.com", + "type": "string" + }, + "http_api_port": { + "description": "Port used by this mixnode for its http(s) API", "type": "integer", "format": "uint16", "minimum": 0.0 }, - "host": { - "description": "Network address of this gateway, for example 1.1.1.1 or foo.gateway.com", - "type": "string" - }, "identity_key": { - "description": "Base58 encoded ed25519 EdDSA public key of the gateway used to derive shared keys with clients", - "type": "string" - }, - "location": { - "description": "The physical, self-reported, location of this gateway.", + "description": "Base58-encoded ed25519 EdDSA public key.", "type": "string" }, "mix_port": { - "description": "Port used by this gateway for listening for mix packets.", + "description": "Port used by this mixnode for listening for mix packets.", "type": "integer", "format": "uint16", "minimum": 0.0 @@ -6057,55 +7362,73 @@ "description": "Base58-encoded x25519 public key used for sphinx key derivation.", "type": "string" }, + "verloc_port": { + "description": "Port used by this mixnode for listening for verloc requests.", + "type": "integer", + "format": "uint16", + "minimum": 0.0 + }, "version": { - "description": "The self-reported semver version of this gateway.", + "description": "The self-reported semver version of this mixnode.", "type": "string" } }, "additionalProperties": false }, - "GatewayBond": { - "description": "Basic gateway information provided by the node operator.", + "MixNodeBond": { + "description": "Basic mixnode information provided by the node operator.", "type": "object", "required": [ - "block_height", - "gateway", - "owner", - "pledge_amount" + "bonding_height", + "is_unbonding", + "mix_id", + "mix_node", + "original_pledge", + "owner" ], "properties": { - "block_height": { - "description": "Block height at which this gateway has been bonded.", + "bonding_height": { + "description": "Block height at which this mixnode has been bonded.", "type": "integer", "format": "uint64", "minimum": 0.0 }, - "gateway": { + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "mix_id": { + "description": "Unique id assigned to the bonded mixnode.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mix_node": { "description": "Information provided by the operator for the purposes of bonding.", "allOf": [ { - "$ref": "#/definitions/Gateway" + "$ref": "#/definitions/MixNode" } ] }, - "owner": { - "description": "Address of the owner of this gateway.", + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", "allOf": [ { - "$ref": "#/definitions/Addr" + "$ref": "#/definitions/Coin" } ] }, - "pledge_amount": { - "description": "Original amount pledged by the operator of this node.", + "owner": { + "description": "Address of the owner of this mixnode.", "allOf": [ { - "$ref": "#/definitions/Coin" + "$ref": "#/definitions/Addr" } ] }, "proxy": { - "description": "Entity who bonded this gateway on behalf of the owner. If exists, it's most likely the address of the vesting contract.", + "description": "Entity who bonded this mixnode on behalf of the owner. If exists, it's most likely the address of the vesting contract.", "anyOf": [ { "$ref": "#/definitions/Addr" @@ -6115,8 +7438,169 @@ } ] } - }, - "additionalProperties": false + } + }, + "MixNodeDetails": { + "description": "Full details associated with given mixnode.", + "type": "object", + "required": [ + "bond_information", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this mixnode, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/MixNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", + "default": { + "cost_params_change": null, + "pledge_change": null + }, + "allOf": [ + { + "$ref": "#/definitions/PendingMixNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "PendingMixNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + } + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", @@ -6124,28 +7608,28 @@ } } }, - "get_owned_mixnode": { + "get_owned_nym_node": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MixOwnershipResponse", - "description": "Response containing details of a mixnode belonging to the particular owner.", + "title": "NodeOwnershipResponse", + "description": "Response containing details of a node belonging to the particular owner.", "type": "object", "required": [ "address" ], "properties": { "address": { - "description": "Validated address of the mixnode owner.", + "description": "Validated address of the node owner.", "allOf": [ { "$ref": "#/definitions/Addr" } ] }, - "mixnode_details": { - "description": "If the provided address owns a mixnode, this field contains its detailed information.", + "details": { + "description": "If the provided address owns a nym-node, this field contains its detailed information.", "anyOf": [ { - "$ref": "#/definitions/MixNodeDetails" + "$ref": "#/definitions/NymNodeDetails" }, { "type": "null" @@ -6178,273 +7662,228 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, - "MixNode": { - "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ - "host", - "http_api_port", - "identity_key", - "mix_port", - "sphinx_key", - "verloc_port", - "version" + "interval_operating_cost", + "profit_margin_percent" ], "properties": { - "host": { - "description": "Network address of this mixnode, for example 1.1.1.1 or foo.mixnode.com", - "type": "string" - }, - "http_api_port": { - "description": "Port used by this mixnode for its http(s) API", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "identity_key": { - "description": "Base58-encoded ed25519 EdDSA public key.", - "type": "string" - }, - "mix_port": { - "description": "Port used by this mixnode for listening for mix packets.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 - }, - "sphinx_key": { - "description": "Base58-encoded x25519 public key used for sphinx key derivation.", - "type": "string" - }, - "verloc_port": { - "description": "Port used by this mixnode for listening for verloc requests.", - "type": "integer", - "format": "uint16", - "minimum": 0.0 + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] }, - "version": { - "description": "The self-reported semver version of this mixnode.", - "type": "string" + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] } }, "additionalProperties": false }, - "MixNodeBond": { - "description": "Basic mixnode information provided by the node operator.", + "NodeRewarding": { "type": "object", "required": [ - "bonding_height", - "is_unbonding", - "layer", - "mix_id", - "mix_node", - "original_pledge", - "owner" + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" ], "properties": { - "bonding_height": { - "description": "Block height at which this mixnode has been bonded.", - "type": "integer", - "format": "uint64", - "minimum": 0.0 - }, - "is_unbonding": { - "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", - "type": "boolean" + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] }, - "layer": { - "description": "Layer assigned to this mixnode.", + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", "allOf": [ { - "$ref": "#/definitions/Layer" + "$ref": "#/definitions/Decimal" } ] }, - "mix_id": { - "description": "Unique id assigned to the bonded mixnode.", + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", "type": "integer", "format": "uint32", "minimum": 0.0 }, - "mix_node": { - "description": "Information provided by the operator for the purposes of bonding.", + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", "allOf": [ { - "$ref": "#/definitions/MixNode" + "$ref": "#/definitions/Decimal" } ] }, - "original_pledge": { - "description": "Original amount pledged by the operator of this node.", + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", "allOf": [ { - "$ref": "#/definitions/Coin" + "$ref": "#/definitions/Decimal" } ] }, - "owner": { - "description": "Address of the owner of this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Addr" - } - ] + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 }, - "proxy": { - "description": "Entity who bonded this mixnode on behalf of the owner. If exists, it's most likely the address of the vesting contract.", - "anyOf": [ - { - "$ref": "#/definitions/Addr" - }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ { - "type": "null" + "$ref": "#/definitions/Decimal" } ] } }, "additionalProperties": false }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", "required": [ - "interval_operating_cost", - "profit_margin_percent" + "host", + "identity_key" ], "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" } }, "additionalProperties": false }, - "MixNodeDetails": { - "description": "Full details associated with given mixnode.", + "NymNodeBond": { "type": "object", "required": [ - "bond_information", - "rewarding_details" + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" ], "properties": { - "bond_information": { - "description": "Basic bond information of this mixnode, such as owner address, original pledge, etc.", + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", "allOf": [ { - "$ref": "#/definitions/MixNodeBond" + "$ref": "#/definitions/NymNode" } ] }, - "pending_changes": { - "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", - "default": { - "pledge_change": null - }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", "allOf": [ { - "$ref": "#/definitions/PendingMixNodeChanges" + "$ref": "#/definitions/Coin" } ] }, - "rewarding_details": { - "description": "Details used for computation of rewarding related data.", + "owner": { + "description": "Address of the owner of this nym-node.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/Addr" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NymNodeDetails": { + "description": "Full details associated with given node.", "type": "object", "required": [ - "cost_params", - "delegates", - "last_rewarded_epoch", - "operator", - "total_unit_reward", - "unique_delegations", - "unit_delegation" - ], - "properties": { - "cost_params": { - "description": "Information provided by the operator that influence the cost function.", - "allOf": [ - { - "$ref": "#/definitions/MixNodeCostParams" - } - ] - }, - "delegates": { - "description": "Total delegation and compounded reward earned by all node delegators.", - "allOf": [ - { - "$ref": "#/definitions/Decimal" - } - ] - }, - "last_rewarded_epoch": { - "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "operator": { - "description": "Total pledge and compounded reward earned by the node operator.", + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/NymNodeBond" } ] }, - "total_unit_reward": { - "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/PendingNodeChanges" } ] }, - "unique_delegations": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/Decimal" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "PendingMixNodeChanges": { + "PendingNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", @@ -6476,7 +7915,8 @@ "description": "Response containing information about accrued rewards.", "type": "object", "required": [ - "mixnode_still_fully_bonded" + "mixnode_still_fully_bonded", + "node_still_fully_bonded" ], "properties": { "amount_earned": { @@ -6514,6 +7954,10 @@ }, "mixnode_still_fully_bonded": { "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { "type": "boolean" } }, @@ -6570,6 +8014,36 @@ }, "additionalProperties": false, "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Addr": { "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", "type": "string" @@ -6618,7 +8092,7 @@ "description": "Enum encompassing all possible epoch events.", "oneOf": [ { - "description": "Request to create a delegation towards particular mixnode. Note that if such delegation already exists, it will get updated with the provided token amount.", + "description": "Request to create a delegation towards particular node. Note that if such delegation already exists, it will get updated with the provided token amount.", "type": "object", "required": [ "delegate" @@ -6628,7 +8102,7 @@ "type": "object", "required": [ "amount", - "mix_id", + "node_id", "owner" ], "properties": { @@ -6640,8 +8114,8 @@ } ] }, - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -6672,7 +8146,7 @@ "additionalProperties": false }, { - "description": "Request to remove delegation from particular mixnode.", + "description": "Request to remove delegation from particular node.", "type": "object", "required": [ "undelegate" @@ -6681,12 +8155,12 @@ "undelegate": { "type": "object", "required": [ - "mix_id", + "node_id", "owner" ], "properties": { - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -6720,10 +8194,44 @@ "description": "Request to pledge more tokens (by the node operator) towards its node.", "type": "object", "required": [ - "pledge_more" + "nym_node_pledge_more" + ], + "properties": { + "nym_node_pledge_more": { + "type": "object", + "required": [ + "amount", + "node_id" + ], + "properties": { + "amount": { + "description": "The amount of additional tokens to use in the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to pledge more tokens (by the node operator) towards its node.", + "type": "object", + "required": [ + "mixnode_pledge_more" ], "properties": { - "pledge_more": { + "mixnode_pledge_more": { "type": "object", "required": [ "amount", @@ -6731,7 +8239,7 @@ ], "properties": { "amount": { - "description": "The amount of additional tokens to use by the pledge.", + "description": "The amount of additional tokens to use in the pledge.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -6754,10 +8262,44 @@ "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", "type": "object", "required": [ - "decrease_pledge" + "nym_node_decrease_pledge" + ], + "properties": { + "nym_node_decrease_pledge": { + "type": "object", + "required": [ + "decrease_by", + "node_id" + ], + "properties": { + "decrease_by": { + "description": "The amount of tokens that should be removed from the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", + "type": "object", + "required": [ + "mixnode_decrease_pledge" ], "properties": { - "decrease_pledge": { + "mixnode_decrease_pledge": { "type": "object", "required": [ "decrease_by", @@ -6810,20 +8352,20 @@ "additionalProperties": false }, { - "description": "Request to update the current size of the active set.", + "description": "Request to unbond a nym node and completely remove it from the network.", "type": "object", "required": [ - "update_active_set_size" + "unbond_nym_node" ], "properties": { - "update_active_set_size": { + "unbond_nym_node": { "type": "object", "required": [ - "new_size" + "node_id" ], "properties": { - "new_size": { - "description": "The new desired size of the active set.", + "node_id": { + "description": "The id of the node that will get unbonded.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -6833,6 +8375,28 @@ } }, "additionalProperties": false + }, + { + "description": "Request to update the current active set.", + "type": "object", + "required": [ + "update_active_set" + ], + "properties": { + "update_active_set": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/ActiveSetUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -6876,6 +8440,36 @@ }, "additionalProperties": false, "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Addr": { "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", "type": "string" @@ -6949,7 +8543,7 @@ "description": "Enum encompassing all possible epoch events.", "oneOf": [ { - "description": "Request to create a delegation towards particular mixnode. Note that if such delegation already exists, it will get updated with the provided token amount.", + "description": "Request to create a delegation towards particular node. Note that if such delegation already exists, it will get updated with the provided token amount.", "type": "object", "required": [ "delegate" @@ -6959,7 +8553,7 @@ "type": "object", "required": [ "amount", - "mix_id", + "node_id", "owner" ], "properties": { @@ -6971,8 +8565,8 @@ } ] }, - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -7003,7 +8597,7 @@ "additionalProperties": false }, { - "description": "Request to remove delegation from particular mixnode.", + "description": "Request to remove delegation from particular node.", "type": "object", "required": [ "undelegate" @@ -7012,12 +8606,12 @@ "undelegate": { "type": "object", "required": [ - "mix_id", + "node_id", "owner" ], "properties": { - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -7051,10 +8645,44 @@ "description": "Request to pledge more tokens (by the node operator) towards its node.", "type": "object", "required": [ - "pledge_more" + "nym_node_pledge_more" + ], + "properties": { + "nym_node_pledge_more": { + "type": "object", + "required": [ + "amount", + "node_id" + ], + "properties": { + "amount": { + "description": "The amount of additional tokens to use in the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to pledge more tokens (by the node operator) towards its node.", + "type": "object", + "required": [ + "mixnode_pledge_more" ], "properties": { - "pledge_more": { + "mixnode_pledge_more": { "type": "object", "required": [ "amount", @@ -7062,7 +8690,7 @@ ], "properties": { "amount": { - "description": "The amount of additional tokens to use by the pledge.", + "description": "The amount of additional tokens to use in the pledge.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -7085,10 +8713,44 @@ "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", "type": "object", "required": [ - "decrease_pledge" + "nym_node_decrease_pledge" + ], + "properties": { + "nym_node_decrease_pledge": { + "type": "object", + "required": [ + "decrease_by", + "node_id" + ], + "properties": { + "decrease_by": { + "description": "The amount of tokens that should be removed from the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", + "type": "object", + "required": [ + "mixnode_decrease_pledge" ], "properties": { - "decrease_pledge": { + "mixnode_decrease_pledge": { "type": "object", "required": [ "decrease_by", @@ -7141,20 +8803,20 @@ "additionalProperties": false }, { - "description": "Request to update the current size of the active set.", + "description": "Request to unbond a nym node and completely remove it from the network.", "type": "object", "required": [ - "update_active_set_size" + "unbond_nym_node" ], "properties": { - "update_active_set_size": { + "unbond_nym_node": { "type": "object", "required": [ - "new_size" + "node_id" ], "properties": { - "new_size": { - "description": "The new desired size of the active set.", + "node_id": { + "description": "The id of the node that will get unbonded.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -7164,6 +8826,28 @@ } }, "additionalProperties": false + }, + { + "description": "Request to update the current active set.", + "type": "object", + "required": [ + "update_active_set" + ], + "properties": { + "update_active_set": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/ActiveSetUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, @@ -7255,14 +8939,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -7300,7 +8986,7 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -7309,7 +8995,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -7317,7 +9003,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -7376,12 +9062,46 @@ "minimum": 0.0 }, "new_costs": { - "description": "The new updated cost function of this mixnode.", + "description": "The new updated cost function of this mixnode.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to update cost parameters of given nym node.", + "type": "object", + "required": [ + "change_nym_node_cost_params" + ], + "properties": { + "change_nym_node_cost_params": { + "type": "object", + "required": [ + "new_costs", + "node_id" + ], + "properties": { + "new_costs": { + "description": "The new updated cost function of this nym node.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] + }, + "node_id": { + "description": "The id of the nym node that will have its cost parameters updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false @@ -7458,6 +9178,42 @@ } ] }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" @@ -7554,14 +9310,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -7599,7 +9357,7 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -7608,7 +9366,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -7616,7 +9374,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -7703,9 +9461,43 @@ "description": "The new updated cost function of this mixnode.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to update cost parameters of given nym node.", + "type": "object", + "required": [ + "change_nym_node_cost_params" + ], + "properties": { + "change_nym_node_cost_params": { + "type": "object", + "required": [ + "new_costs", + "node_id" + ], + "properties": { + "new_costs": { + "description": "The new updated cost function of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" } ] + }, + "node_id": { + "description": "The id of the nym node that will have its cost parameters updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false @@ -7782,19 +9574,56 @@ } ] }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" } } }, - "get_pending_mix_node_operator_reward": { + "get_pending_node_operator_reward": { "$schema": "http://json-schema.org/draft-07/schema#", "title": "PendingRewardResponse", "description": "Response containing information about accrued rewards.", "type": "object", "required": [ - "mixnode_still_fully_bonded" + "mixnode_still_fully_bonded", + "node_still_fully_bonded" ], "properties": { "amount_earned": { @@ -7832,6 +9661,10 @@ }, "mixnode_still_fully_bonded": { "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { "type": "boolean" } }, @@ -7868,7 +9701,8 @@ "description": "Response containing information about accrued rewards.", "type": "object", "required": [ - "mixnode_still_fully_bonded" + "mixnode_still_fully_bonded", + "node_still_fully_bonded" ], "properties": { "amount_earned": { @@ -7906,6 +9740,10 @@ }, "mixnode_still_fully_bonded": { "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { "type": "boolean" } }, @@ -7936,64 +9774,163 @@ } } }, - "get_rewarded_set": { + "get_preassigned_gateway_ids": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "PagedRewardedSetResponse", - "description": "Response containing paged list of all mixnodes in the rewarded set.", + "title": "PreassignedGatewayIdsResponse", "type": "object", "required": [ - "nodes" + "ids" ], "properties": { - "nodes": { - "description": "Nodes in the current rewarded set.", + "ids": { "type": "array", "items": { - "type": "array", - "items": [ - { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, - { - "$ref": "#/definitions/RewardedSetNodeStatus" - } - ], - "maxItems": 2, - "minItems": 2 + "$ref": "#/definitions/PreassignedId" } }, "start_next_after": { "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", "type": [ - "integer", + "string", "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "PreassignedId": { + "type": "object", + "required": [ + "identity", + "node_id" ], - "format": "uint32", - "minimum": 0.0 + "properties": { + "identity": { + "description": "The identity key (base58-encoded ed25519 public key) of the gateway.", + "type": "string" + }, + "node_id": { + "description": "The id pre-assigned to this gateway", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "get_rewarded_set_metadata": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RolesMetadataResponse", + "type": "object", + "required": [ + "metadata" + ], + "properties": { + "metadata": { + "$ref": "#/definitions/RewardedSetMetadata" } }, "additionalProperties": false, "definitions": { - "RewardedSetNodeStatus": { - "description": "Current state of given node in the rewarded set.", - "oneOf": [ - { - "description": "Node that is currently active, i.e. is expected to be used by clients for mixing packets.", - "type": "string", - "enum": [ - "active" + "RewardedSetMetadata": { + "description": "Metadata associated with the rewarded set.", + "type": "object", + "required": [ + "entry_gateway_metadata", + "epoch_id", + "exit_gateway_metadata", + "fully_assigned", + "layer1_metadata", + "layer2_metadata", + "layer3_metadata", + "standby_metadata" + ], + "properties": { + "entry_gateway_metadata": { + "description": "Metadata for the 'EntryGateway' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } ] }, - { - "description": "Node that is currently in standby, i.e. it's present in the rewarded set but is not active.", - "type": "string", - "enum": [ - "standby" + "epoch_id": { + "description": "Epoch that this data corresponds to.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateway_metadata": { + "description": "Metadata for the 'ExitGateway' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "fully_assigned": { + "description": "Indicates whether all roles got assigned to the set for this epoch.", + "type": "boolean" + }, + "layer1_metadata": { + "description": "Metadata for the 'Layer1' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "layer2_metadata": { + "description": "Metadata for the 'Layer2' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "layer3_metadata": { + "description": "Metadata for the 'Layer3' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "standby_metadata": { + "description": "Metadata for the 'Standby' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } ] } - ] + }, + "additionalProperties": false + }, + "RoleMetadata": { + "description": "Metadata associated with particular node role.", + "type": "object", + "required": [ + "highest_id", + "num_nodes" + ], + "properties": { + "highest_id": { + "description": "Highest, also latest, node-id of a node assigned this role.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "num_nodes": { + "description": "Number of nodes assigned this particular role.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false } } }, @@ -8003,17 +9940,10 @@ "description": "Parameters used for reward calculation.", "type": "object", "required": [ - "active_set_size", "interval", - "rewarded_set_size" + "rewarded_set" ], "properties": { - "active_set_size": { - "description": "The expected number of mixnodes in the active set.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "interval": { "description": "Parameters that should remain unchanged throughout an interval.", "allOf": [ @@ -8022,11 +9952,8 @@ } ] }, - "rewarded_set_size": { - "description": "The expected number of mixnodes in the rewarded set (i.e. active + standby).", - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "rewarded_set": { + "$ref": "#/definitions/RewardedSetParams" } }, "additionalProperties": false, @@ -8123,13 +10050,75 @@ "$ref": "#/definitions/Decimal" } ] + }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "get_rewarding_validator_address": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "String", + "type": "string" + }, + "get_role_assignment": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochAssignmentResponse", + "type": "object", + "required": [ + "epoch_id", + "nodes" + ], + "properties": { + "epoch_id": { + "description": "Epoch that this data corresponds to.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "nodes": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } } - } - }, - "get_rewarding_validator_address": { - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "String", - "type": "string" + }, + "additionalProperties": false }, "get_signing_nonce": { "$schema": "http://json-schema.org/draft-07/schema#", @@ -8140,7 +10129,7 @@ }, "get_stake_saturation": { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "StakeSaturationResponse", + "title": "MixStakeSaturationResponse", "description": "Response containing the current state of the stake saturation of a mixnode with the provided id.", "type": "object", "required": [ @@ -8263,8 +10252,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -8279,15 +10267,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -8298,8 +10278,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -8377,8 +10357,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -8393,15 +10372,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -8412,8 +10383,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -8857,6 +10828,291 @@ "additionalProperties": false } } + }, + "get_unbonded_nym_node": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UnbondedNodeResponse", + "description": "Response containing basic information of an unbonded nym-node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "details": { + "description": "If there existed a nym-node with the provided id, this field contains its basic information.", + "anyOf": [ + { + "$ref": "#/definitions/UnbondedNymNode" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested nym-node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "get_unbonded_nym_nodes_by_identity_key_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "get_unbonded_nym_nodes_by_owner_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } + }, + "get_unbonded_nym_nodes_paged": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } } } } diff --git a/contracts/mixnet/schema/raw/execute.json b/contracts/mixnet/schema/raw/execute.json index c5c773e76d..2e220e58a3 100644 --- a/contracts/mixnet/schema/raw/execute.json +++ b/contracts/mixnet/schema/raw/execute.json @@ -24,228 +24,6 @@ }, "additionalProperties": false }, - { - "type": "object", - "required": [ - "assign_node_layer" - ], - "properties": { - "assign_node_layer": { - "type": "object", - "required": [ - "layer", - "mix_id" - ], - "properties": { - "layer": { - "$ref": "#/definitions/Layer" - }, - "mix_id": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Only owner of the node can crate the family with node as head", - "type": "object", - "required": [ - "create_family" - ], - "properties": { - "create_family": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Family head needs to sign the joining node IdentityKey", - "type": "object", - "required": [ - "join_family" - ], - "properties": { - "join_family": { - "type": "object", - "required": [ - "family_head", - "join_permit" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "join_permit": { - "$ref": "#/definitions/MessageSignature" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "leave_family" - ], - "properties": { - "leave_family": { - "type": "object", - "required": [ - "family_head" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "kick_family_member" - ], - "properties": { - "kick_family_member": { - "type": "object", - "required": [ - "member" - ], - "properties": { - "member": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "create_family_on_behalf" - ], - "properties": { - "create_family_on_behalf": { - "type": "object", - "required": [ - "label", - "owner_address" - ], - "properties": { - "label": { - "type": "string" - }, - "owner_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Family head needs to sign the joining node IdentityKey, MixNode needs to provide its signature proving that it wants to join the family", - "type": "object", - "required": [ - "join_family_on_behalf" - ], - "properties": { - "join_family_on_behalf": { - "type": "object", - "required": [ - "family_head", - "join_permit", - "member_address" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "join_permit": { - "$ref": "#/definitions/MessageSignature" - }, - "member_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "leave_family_on_behalf" - ], - "properties": { - "leave_family_on_behalf": { - "type": "object", - "required": [ - "family_head", - "member_address" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "member_address": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "kick_family_member_on_behalf" - ], - "properties": { - "kick_family_member_on_behalf": { - "type": "object", - "required": [ - "head_address", - "member" - ], - "properties": { - "head_address": { - "type": "string" - }, - "member": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -291,23 +69,21 @@ { "type": "object", "required": [ - "update_active_set_size" + "update_active_set_distribution" ], "properties": { - "update_active_set_size": { + "update_active_set_distribution": { "type": "object", "required": [ - "active_set_size", - "force_immediately" + "force_immediately", + "update" ], "properties": { - "active_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "force_immediately": { "type": "boolean" + }, + "update": { + "$ref": "#/definitions/ActiveSetUpdate" } }, "additionalProperties": false @@ -389,26 +165,19 @@ { "type": "object", "required": [ - "advance_current_epoch" + "reconcile_epoch_events" ], "properties": { - "advance_current_epoch": { + "reconcile_epoch_events": { "type": "object", - "required": [ - "expected_active_set_size", - "new_rewarded_set" - ], "properties": { - "expected_active_set_size": { - "type": "integer", + "limit": { + "type": [ + "integer", + "null" + ], "format": "uint32", "minimum": 0.0 - }, - "new_rewarded_set": { - "type": "array", - "items": { - "$ref": "#/definitions/LayerAssignment" - } } }, "additionalProperties": false @@ -419,19 +188,17 @@ { "type": "object", "required": [ - "reconcile_epoch_events" + "assign_roles" ], "properties": { - "reconcile_epoch_events": { + "assign_roles": { "type": "object", + "required": [ + "assignment" + ], "properties": { - "limit": { - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "assignment": { + "$ref": "#/definitions/RoleAssignment" } }, "additionalProperties": false @@ -454,7 +221,7 @@ ], "properties": { "cost_params": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "mix_node": { "$ref": "#/definitions/MixNode" @@ -484,7 +251,7 @@ ], "properties": { "cost_params": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "mix_node": { "$ref": "#/definitions/MixNode" @@ -618,17 +385,17 @@ { "type": "object", "required": [ - "update_mixnode_cost_params" + "update_cost_params" ], "properties": { - "update_mixnode_cost_params": { + "update_cost_params": { "type": "object", "required": [ "new_costs" ], "properties": { "new_costs": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } }, "additionalProperties": false @@ -650,7 +417,7 @@ ], "properties": { "new_costs": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "owner": { "type": "string" @@ -707,6 +474,19 @@ }, "additionalProperties": false }, + { + "type": "object", + "required": [ + "migrate_mixnode" + ], + "properties": { + "migrate_mixnode": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, { "type": "object", "required": [ @@ -844,16 +624,104 @@ { "type": "object", "required": [ - "delegate_to_mixnode" + "migrate_gateway" + ], + "properties": { + "migrate_gateway": { + "type": "object", + "properties": { + "cost_params": { + "anyOf": [ + { + "$ref": "#/definitions/NodeCostParams" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "bond_nym_node" ], "properties": { - "delegate_to_mixnode": { + "bond_nym_node": { "type": "object", "required": [ - "mix_id" + "cost_params", + "node", + "owner_signature" ], "properties": { - "mix_id": { + "cost_params": { + "$ref": "#/definitions/NodeCostParams" + }, + "node": { + "$ref": "#/definitions/NymNode" + }, + "owner_signature": { + "$ref": "#/definitions/MessageSignature" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "unbond_nym_node" + ], + "properties": { + "unbond_nym_node": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "update_node_config" + ], + "properties": { + "update_node_config": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/NodeConfigUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "delegate" + ], + "properties": { + "delegate": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -894,16 +762,16 @@ { "type": "object", "required": [ - "undelegate_from_mixnode" + "undelegate" ], "properties": { - "undelegate_from_mixnode": { + "undelegate": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -944,23 +812,23 @@ { "type": "object", "required": [ - "reward_mixnode" + "reward_node" ], "properties": { - "reward_mixnode": { + "reward_node": { "type": "object", "required": [ - "mix_id", - "performance" + "node_id", + "params" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 }, - "performance": { - "$ref": "#/definitions/Percent" + "params": { + "$ref": "#/definitions/NodeRewardingParameters" } }, "additionalProperties": false @@ -1011,10 +879,10 @@ "withdraw_delegator_reward": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "type": "integer", "format": "uint32", "minimum": 0.0 @@ -1090,6 +958,36 @@ } ], "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Coin": { "type": "object", "required": [ @@ -1109,8 +1007,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -1125,15 +1022,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -1144,8 +1033,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -1171,10 +1060,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - }, "Gateway": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -1292,14 +1177,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -1337,39 +1224,6 @@ }, "additionalProperties": false }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, - "LayerAssignment": { - "description": "Specifies layer assignment for the given mixnode.", - "type": "object", - "required": [ - "layer", - "mix_id" - ], - "properties": { - "layer": { - "description": "The layer to which it's going to be assigned", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, - "mix_id": { - "description": "The id of the mixnode.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - } - }, - "additionalProperties": false - }, "MessageSignature": { "type": "array", "items": { @@ -1462,7 +1316,31 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeConfigUpdate": { + "type": "object", + "properties": { + "custom_http_port": { + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "type": [ + "string", + "null" + ] + }, + "restore_default_http_port": { + "default": false, + "type": "boolean" + } + }, + "additionalProperties": false + }, + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -1471,7 +1349,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -1479,7 +1357,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -1489,6 +1367,61 @@ }, "additionalProperties": false }, + "NodeRewardingParameters": { + "description": "Parameters used for rewarding particular node.", + "type": "object", + "required": [ + "performance", + "work_factor" + ], + "properties": { + "performance": { + "description": "Performance of the particular node in the current epoch.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + }, + "work_factor": { + "description": "Amount of work performed by this node in the current epoch also known as 'omega' in the paper", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", "allOf": [ @@ -1529,6 +1462,74 @@ }, "additionalProperties": false }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] + }, + "RoleAssignment": { + "type": "object", + "required": [ + "nodes", + "role" + ], + "properties": { + "nodes": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "role": { + "$ref": "#/definitions/Role" + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/mixnet/schema/raw/instantiate.json b/contracts/mixnet/schema/raw/instantiate.json index e025d9f11f..bb2dd70702 100644 --- a/contracts/mixnet/schema/raw/instantiate.json +++ b/contracts/mixnet/schema/raw/instantiate.json @@ -82,21 +82,15 @@ "InitialRewardingParams": { "type": "object", "required": [ - "active_set_size", "active_set_work_factor", "initial_reward_pool", "initial_staking_supply", "interval_pool_emission", - "rewarded_set_size", + "rewarded_set_params", "staking_supply_scale_factor", "sybil_resistance" ], "properties": { - "active_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "active_set_work_factor": { "$ref": "#/definitions/Decimal" }, @@ -109,10 +103,8 @@ "interval_pool_emission": { "$ref": "#/definitions/Percent" }, - "rewarded_set_size": { - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "$ref": "#/definitions/RewardedSetParams" }, "staking_supply_scale_factor": { "$ref": "#/definitions/Percent" @@ -163,6 +155,42 @@ }, "additionalProperties": false }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/mixnet/schema/raw/migrate.json b/contracts/mixnet/schema/raw/migrate.json index e9962aaf92..57f0d2acdb 100644 --- a/contracts/mixnet/schema/raw/migrate.json +++ b/contracts/mixnet/schema/raw/migrate.json @@ -3,6 +3,12 @@ "title": "MigrateMsg", "type": "object", "properties": { + "unsafe_skip_state_updates": { + "type": [ + "boolean", + "null" + ] + }, "vesting_contract_address": { "type": [ "string", diff --git a/contracts/mixnet/schema/raw/query.json b/contracts/mixnet/schema/raw/query.json index c938a56f64..90d0477a30 100644 --- a/contracts/mixnet/schema/raw/query.json +++ b/contracts/mixnet/schema/raw/query.json @@ -16,13 +16,125 @@ "additionalProperties": false }, { - "description": "Gets the list of families registered in this contract.", + "description": "Gets build information of this contract, such as the commit hash used for the build or rustc version.", + "type": "object", + "required": [ + "get_contract_version" + ], + "properties": { + "get_contract_version": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the stored contract version information that's required by the CW2 spec interface for migrations.", + "type": "object", + "required": [ + "get_cw2_contract_version" + ], + "properties": { + "get_cw2_contract_version": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the address of the validator that's allowed to send rewarding transactions and transition the epoch.", + "type": "object", + "required": [ + "get_rewarding_validator_address" + ], + "properties": { + "get_rewarding_validator_address": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the contract parameters that could be adjusted in a transaction by the contract admin.", + "type": "object", + "required": [ + "get_state_params" + ], + "properties": { + "get_state_params": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the current state of the contract.", + "type": "object", + "required": [ + "get_state" + ], + "properties": { + "get_state": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the current parameters used for reward calculation.", + "type": "object", + "required": [ + "get_rewarding_params" + ], + "properties": { + "get_rewarding_params": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the status of the current rewarding epoch.", "type": "object", "required": [ - "get_all_families_paged" + "get_epoch_status" ], "properties": { - "get_all_families_paged": { + "get_epoch_status": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Get the details of the current rewarding interval.", + "type": "object", + "required": [ + "get_current_interval_details" + ], + "properties": { + "get_current_interval_details": { + "type": "object", + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all currently bonded mixnodes.", + "type": "object", + "required": [ + "get_mix_node_bonds" + ], + "properties": { + "get_mix_node_bonds": { "type": "object", "properties": { "limit": { @@ -37,9 +149,11 @@ "start_after": { "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", "type": [ - "string", + "integer", "null" - ] + ], + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false @@ -48,13 +162,13 @@ "additionalProperties": false }, { - "description": "Gets the list of all family members registered in this contract.", + "description": "Gets the detailed list of all currently bonded mixnodes.", "type": "object", "required": [ - "get_all_members_paged" + "get_mix_nodes_detailed" ], "properties": { - "get_all_members_paged": { + "get_mix_nodes_detailed": { "type": "object", "properties": { "limit": { @@ -69,9 +183,11 @@ "start_after": { "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", "type": [ - "string", + "integer", "null" - ] + ], + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false @@ -80,20 +196,32 @@ "additionalProperties": false }, { - "description": "Attempts to lookup family information given the family head.", + "description": "Gets the basic list of all unbonded mixnodes.", "type": "object", "required": [ - "get_family_by_head" + "get_unbonded_mix_nodes" ], "properties": { - "get_family_by_head": { + "get_unbonded_mix_nodes": { "type": "object", - "required": [ - "head" - ], "properties": { - "head": { - "type": "string" + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false @@ -102,20 +230,39 @@ "additionalProperties": false }, { - "description": "Attempts to lookup family information given the family label.", + "description": "Gets the basic list of all unbonded mixnodes that belonged to a particular owner.", "type": "object", "required": [ - "get_family_by_label" + "get_unbonded_mix_nodes_by_owner" ], "properties": { - "get_family_by_label": { + "get_unbonded_mix_nodes_by_owner": { "type": "object", "required": [ - "label" + "owner" ], "properties": { - "label": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "The address of the owner of the mixnodes used for the query.", "type": "string" + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false @@ -124,20 +271,39 @@ "additionalProperties": false }, { - "description": "Attempts to retrieve family members given the family head.", + "description": "Gets the basic list of all unbonded mixnodes that used the particular identity key.", "type": "object", "required": [ - "get_family_members_by_head" + "get_unbonded_mix_nodes_by_identity_key" ], "properties": { - "get_family_members_by_head": { + "get_unbonded_mix_nodes_by_identity_key": { "type": "object", "required": [ - "head" + "identity_key" ], "properties": { - "head": { + "identity_key": { + "description": "The identity key (base58-encoded ed25519 public key) of the mixnode used for the query.", "type": "string" + }, + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false @@ -146,19 +312,20 @@ "additionalProperties": false }, { - "description": "Attempts to retrieve family members given the family label.", + "description": "Gets the detailed mixnode information belonging to the particular owner.", "type": "object", "required": [ - "get_family_members_by_label" + "get_owned_mixnode" ], "properties": { - "get_family_members_by_label": { + "get_owned_mixnode": { "type": "object", "required": [ - "label" + "address" ], "properties": { - "label": { + "address": { + "description": "Address of the mixnode owner to use for the query.", "type": "string" } }, @@ -168,125 +335,214 @@ "additionalProperties": false }, { - "description": "Gets build information of this contract, such as the commit hash used for the build or rustc version.", + "description": "Gets the detailed mixnode information of a node with the provided id.", "type": "object", "required": [ - "get_contract_version" + "get_mixnode_details" ], "properties": { - "get_contract_version": { + "get_mixnode_details": { "type": "object", + "required": [ + "mix_id" + ], + "properties": { + "mix_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, "additionalProperties": false } }, "additionalProperties": false }, { - "description": "Gets the stored contract version information that's required by the CW2 spec interface for migrations.", + "description": "Gets the rewarding information of a mixnode with the provided id.", "type": "object", "required": [ - "get_cw2_contract_version" + "get_mixnode_rewarding_details" ], "properties": { - "get_cw2_contract_version": { + "get_mixnode_rewarding_details": { "type": "object", + "required": [ + "mix_id" + ], + "properties": { + "mix_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, "additionalProperties": false } }, "additionalProperties": false }, { - "description": "Gets the address of the validator that's allowed to send rewarding transactions and transition the epoch.", + "description": "Gets the stake saturation of a mixnode with the provided id.", "type": "object", "required": [ - "get_rewarding_validator_address" + "get_stake_saturation" ], "properties": { - "get_rewarding_validator_address": { + "get_stake_saturation": { "type": "object", + "required": [ + "mix_id" + ], + "properties": { + "mix_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, "additionalProperties": false } }, "additionalProperties": false }, { - "description": "Gets the contract parameters that could be adjusted in a transaction by the contract admin.", + "description": "Gets the basic information of an unbonded mixnode with the provided id.", "type": "object", "required": [ - "get_state_params" + "get_unbonded_mix_node_information" ], "properties": { - "get_state_params": { + "get_unbonded_mix_node_information": { "type": "object", + "required": [ + "mix_id" + ], + "properties": { + "mix_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, "additionalProperties": false } }, "additionalProperties": false }, { - "description": "Gets the current state of the contract.", + "description": "Gets the detailed mixnode information of a node given its current identity key.", "type": "object", "required": [ - "get_state" + "get_bonded_mixnode_details_by_identity" ], "properties": { - "get_state": { + "get_bonded_mixnode_details_by_identity": { "type": "object", + "required": [ + "mix_identity" + ], + "properties": { + "mix_identity": { + "description": "The identity key (base58-encoded ed25519 public key) of the mixnode used for the query.", + "type": "string" + } + }, "additionalProperties": false } }, "additionalProperties": false }, { - "description": "Gets the current parameters used for reward calculation.", + "description": "Gets the basic list of all currently bonded gateways.", "type": "object", "required": [ - "get_rewarding_params" + "get_gateways" ], "properties": { - "get_rewarding_params": { + "get_gateways": { "type": "object", + "properties": { + "limit": { + "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "start_after": { + "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", + "type": [ + "string", + "null" + ] + } + }, "additionalProperties": false } }, "additionalProperties": false }, { - "description": "Gets the status of the current rewarding epoch.", + "description": "Gets the gateway details of a node given its identity key.", "type": "object", "required": [ - "get_epoch_status" + "get_gateway_bond" ], "properties": { - "get_epoch_status": { + "get_gateway_bond": { "type": "object", + "required": [ + "identity" + ], + "properties": { + "identity": { + "description": "The identity key (base58-encoded ed25519 public key) of the gateway used for the query.", + "type": "string" + } + }, "additionalProperties": false } }, "additionalProperties": false }, { - "description": "Get the details of the current rewarding interval.", + "description": "Gets the detailed gateway information belonging to the particular owner.", "type": "object", "required": [ - "get_current_interval_details" + "get_owned_gateway" ], "properties": { - "get_current_interval_details": { + "get_owned_gateway": { "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Address of the gateway owner to use for the query.", + "type": "string" + } + }, "additionalProperties": false } }, "additionalProperties": false }, { - "description": "Gets the current list of mixnodes in the rewarded set.", + "description": "Get the `NodeId`s of all the legacy gateways that they will get assigned once migrated into NymNodes", "type": "object", "required": [ - "get_rewarded_set" + "get_preassigned_gateway_ids" ], "properties": { - "get_rewarded_set": { + "get_preassigned_gateway_ids": { "type": "object", "properties": { "limit": { @@ -301,11 +557,9 @@ "start_after": { "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", "type": [ - "integer", + "string", "null" - ], - "format": "uint32", - "minimum": 0.0 + ] } }, "additionalProperties": false @@ -314,13 +568,13 @@ "additionalProperties": false }, { - "description": "Gets the basic list of all currently bonded mixnodes.", + "description": "Gets the basic list of all currently bonded nymnodes.", "type": "object", "required": [ - "get_mix_node_bonds" + "get_nym_node_bonds_paged" ], "properties": { - "get_mix_node_bonds": { + "get_nym_node_bonds_paged": { "type": "object", "properties": { "limit": { @@ -348,13 +602,13 @@ "additionalProperties": false }, { - "description": "Gets the detailed list of all currently bonded mixnodes.", + "description": "Gets the detailed list of all currently bonded nymnodes.", "type": "object", "required": [ - "get_mix_nodes_detailed" + "get_nym_nodes_detailed_paged" ], "properties": { - "get_mix_nodes_detailed": { + "get_nym_nodes_detailed_paged": { "type": "object", "properties": { "limit": { @@ -382,13 +636,38 @@ "additionalProperties": false }, { - "description": "Gets the basic list of all unbonded mixnodes.", + "description": "Gets the basic information of an unbonded nym-node with the provided id.", "type": "object", "required": [ - "get_unbonded_mix_nodes" + "get_unbonded_nym_node" ], "properties": { - "get_unbonded_mix_nodes": { + "get_unbonded_nym_node": { + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the node to query.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Gets the basic list of all unbonded nymnodes.", + "type": "object", + "required": [ + "get_unbonded_nym_nodes_paged" + ], + "properties": { + "get_unbonded_nym_nodes_paged": { "type": "object", "properties": { "limit": { @@ -416,13 +695,13 @@ "additionalProperties": false }, { - "description": "Gets the basic list of all unbonded mixnodes that belonged to a particular owner.", + "description": "Gets the basic list of all unbonded nymnodes that belonged to a particular owner.", "type": "object", "required": [ - "get_unbonded_mix_nodes_by_owner" + "get_unbonded_nym_nodes_by_owner_paged" ], "properties": { - "get_unbonded_mix_nodes_by_owner": { + "get_unbonded_nym_nodes_by_owner_paged": { "type": "object", "required": [ "owner" @@ -438,7 +717,7 @@ "minimum": 0.0 }, "owner": { - "description": "The address of the owner of the the mixnodes used for the query.", + "description": "The address of the owner of the nym-node used for the query", "type": "string" }, "start_after": { @@ -457,20 +736,20 @@ "additionalProperties": false }, { - "description": "Gets the basic list of all unbonded mixnodes that used the particular identity key.", + "description": "Gets the basic list of all unbonded nymnodes that used the particular identity key.", "type": "object", "required": [ - "get_unbonded_mix_nodes_by_identity_key" + "get_unbonded_nym_nodes_by_identity_key_paged" ], "properties": { - "get_unbonded_mix_nodes_by_identity_key": { + "get_unbonded_nym_nodes_by_identity_key_paged": { "type": "object", "required": [ "identity_key" ], "properties": { "identity_key": { - "description": "The identity key (base58-encoded ed25519 public key) of the mixnode used for the query.", + "description": "The identity key (base58-encoded ed25519 public key) of the node used for the query.", "type": "string" }, "limit": { @@ -498,20 +777,20 @@ "additionalProperties": false }, { - "description": "Gets the detailed mixnode information belonging to the particular owner.", + "description": "Gets the detailed nymnode information belonging to the particular owner.", "type": "object", "required": [ - "get_owned_mixnode" + "get_owned_nym_node" ], "properties": { - "get_owned_mixnode": { + "get_owned_nym_node": { "type": "object", "required": [ "address" ], "properties": { "address": { - "description": "Address of the mixnode owner to use for the query.", + "description": "Address of the node owner to use for the query.", "type": "string" } }, @@ -521,19 +800,19 @@ "additionalProperties": false }, { - "description": "Gets the detailed mixnode information of a node with the provided id.", + "description": "Gets the detailed nymnode information of a node with the provided id.", "type": "object", "required": [ - "get_mixnode_details" + "get_nym_node_details" ], "properties": { - "get_mixnode_details": { + "get_nym_node_details": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -546,23 +825,21 @@ "additionalProperties": false }, { - "description": "Gets the rewarding information of a mixnode with the provided id.", + "description": "Gets the detailed nym-node information given its current identity key.", "type": "object", "required": [ - "get_mixnode_rewarding_details" + "get_nym_node_details_by_identity_key" ], "properties": { - "get_mixnode_rewarding_details": { + "get_nym_node_details_by_identity_key": { "type": "object", "required": [ - "mix_id" + "node_identity" ], "properties": { - "mix_id": { - "description": "Id of the node to query.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "node_identity": { + "description": "The identity key (base58-encoded ed25519 public key) of the nym-node used for the query.", + "type": "string" } }, "additionalProperties": false @@ -571,19 +848,19 @@ "additionalProperties": false }, { - "description": "Gets the stake saturation of a mixnode with the provided id.", + "description": "Gets the rewarding information of a nym-node with the provided id.", "type": "object", "required": [ - "get_stake_saturation" + "get_node_rewarding_details" ], "properties": { - "get_stake_saturation": { + "get_node_rewarding_details": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -596,19 +873,19 @@ "additionalProperties": false }, { - "description": "Gets the basic information of an unbonded mixnode with the provided id.", + "description": "Gets the stake saturation of a nym-node with the provided id.", "type": "object", "required": [ - "get_unbonded_mix_node_information" + "get_node_stake_saturation" ], "properties": { - "get_unbonded_mix_node_information": { + "get_node_stake_saturation": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -621,90 +898,19 @@ "additionalProperties": false }, { - "description": "Gets the detailed mixnode information of a node given its current identity key.", - "type": "object", - "required": [ - "get_bonded_mixnode_details_by_identity" - ], - "properties": { - "get_bonded_mixnode_details_by_identity": { - "type": "object", - "required": [ - "mix_identity" - ], - "properties": { - "mix_identity": { - "description": "The identity key (base58-encoded ed25519 public key) of the mixnode used for the query.", - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Gets the current layer configuration of the mix network.", - "type": "object", - "required": [ - "get_layer_distribution" - ], - "properties": { - "get_layer_distribution": { - "type": "object", - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Gets the basic list of all currently bonded gateways.", - "type": "object", - "required": [ - "get_gateways" - ], - "properties": { - "get_gateways": { - "type": "object", - "properties": { - "limit": { - "description": "Controls the maximum number of entries returned by the query. Note that too large values will be overwritten by a saner default.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 - }, - "start_after": { - "description": "Pagination control for the values returned by the query. Note that the provided value itself will **not** be used for the response.", - "type": [ - "string", - "null" - ] - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Gets the gateway details of a node given its identity key.", "type": "object", "required": [ - "get_gateway_bond" + "get_role_assignment" ], "properties": { - "get_gateway_bond": { + "get_role_assignment": { "type": "object", "required": [ - "identity" + "role" ], "properties": { - "identity": { - "description": "The identity key (base58-encoded ed25519 public key) of the gateway used for the query.", - "type": "string" + "role": { + "$ref": "#/definitions/Role" } }, "additionalProperties": false @@ -713,39 +919,29 @@ "additionalProperties": false }, { - "description": "Gets the detailed gateway information belonging to the particular owner.", "type": "object", "required": [ - "get_owned_gateway" + "get_rewarded_set_metadata" ], "properties": { - "get_owned_gateway": { + "get_rewarded_set_metadata": { "type": "object", - "required": [ - "address" - ], - "properties": { - "address": { - "description": "Address of the gateway owner to use for the query.", - "type": "string" - } - }, "additionalProperties": false } }, "additionalProperties": false }, { - "description": "Gets all delegations associated with particular mixnode", + "description": "Gets all delegations associated with particular node", "type": "object", "required": [ - "get_mixnode_delegations" + "get_node_delegations" ], "properties": { - "get_mixnode_delegations": { + "get_node_delegations": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { "limit": { @@ -757,7 +953,7 @@ "format": "uint32", "minimum": 0.0 }, - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -838,14 +1034,14 @@ "type": "object", "required": [ "delegator", - "mix_id" + "node_id" ], "properties": { "delegator": { "description": "The address of the owner of the delegation.", "type": "string" }, - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -935,16 +1131,16 @@ "description": "Gets the reward amount accrued by the particular mixnode that has not yet been claimed.", "type": "object", "required": [ - "get_pending_mix_node_operator_reward" + "get_pending_node_operator_reward" ], "properties": { - "get_pending_mix_node_operator_reward": { + "get_pending_node_operator_reward": { "type": "object", "required": [ - "mix_id" + "node_id" ], "properties": { - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -967,14 +1163,14 @@ "type": "object", "required": [ "address", - "mix_id" + "node_id" ], "properties": { "address": { "description": "Address of the delegator to use for the query.", "type": "string" }, - "mix_id": { + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -1004,7 +1200,7 @@ "type": "object", "required": [ "estimated_performance", - "mix_id" + "node_id" ], "properties": { "estimated_performance": { @@ -1015,7 +1211,18 @@ } ] }, - "mix_id": { + "estimated_work": { + "description": "The estimated work for the current epoch of the given node.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", @@ -1039,7 +1246,7 @@ "required": [ "address", "estimated_performance", - "mix_id" + "node_id" ], "properties": { "address": { @@ -1054,18 +1261,22 @@ } ] }, - "mix_id": { + "estimated_work": { + "description": "The estimated work for the current epoch of the given node.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { "description": "Id of the node to query.", "type": "integer", "format": "uint32", "minimum": 0.0 - }, - "proxy": { - "description": "Entity who made the delegation on behalf of the owner. If present, it's most likely the address of the vesting contract.", - "type": [ - "string", - "null" - ] } }, "additionalProperties": false @@ -1241,6 +1452,17 @@ "$ref": "#/definitions/Decimal" } ] + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] } } } diff --git a/contracts/mixnet/schema/raw/response_to_get_all_delegations.json b/contracts/mixnet/schema/raw/response_to_get_all_delegations.json index a919220c46..71878b1827 100644 --- a/contracts/mixnet/schema/raw/response_to_get_all_delegations.json +++ b/contracts/mixnet/schema/raw/response_to_get_all_delegations.json @@ -66,7 +66,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -92,8 +92,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 diff --git a/contracts/mixnet/schema/raw/response_to_get_bonded_mixnode_details_by_identity.json b/contracts/mixnet/schema/raw/response_to_get_bonded_mixnode_details_by_identity.json index 59fc029331..239f0fc353 100644 --- a/contracts/mixnet/schema/raw/response_to_get_bonded_mixnode_details_by_identity.json +++ b/contracts/mixnet/schema/raw/response_to_get_bonded_mixnode_details_by_identity.json @@ -48,14 +48,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -112,7 +104,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -129,14 +120,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -178,35 +161,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -227,6 +182,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -239,14 +195,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -262,7 +245,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -302,7 +285,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -315,6 +298,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", @@ -323,8 +315,7 @@ "format": "uint32", "minimum": 0.0 } - }, - "additionalProperties": false + } }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", diff --git a/contracts/mixnet/schema/raw/response_to_get_delegation_details.json b/contracts/mixnet/schema/raw/response_to_get_delegation_details.json index 8371493b30..f8807370a5 100644 --- a/contracts/mixnet/schema/raw/response_to_get_delegation_details.json +++ b/contracts/mixnet/schema/raw/response_to_get_delegation_details.json @@ -1,10 +1,11 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "MixNodeDelegationResponse", + "title": "NodeDelegationResponse", "description": "Response containing delegation details.", "type": "object", "required": [ - "mixnode_still_bonded" + "mixnode_still_bonded", + "node_still_bonded" ], "properties": { "delegation": { @@ -20,6 +21,10 @@ }, "mixnode_still_bonded": { "description": "Flag indicating whether the node towards which the delegation was made is still bonded in the network.", + "deprecated": true, + "type": "boolean" + }, + "node_still_bonded": { "type": "boolean" } }, @@ -55,7 +60,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -81,8 +86,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 diff --git a/contracts/mixnet/schema/raw/response_to_get_delegator_delegations.json b/contracts/mixnet/schema/raw/response_to_get_delegator_delegations.json index b6e7e7a6ee..e3556f1db2 100644 --- a/contracts/mixnet/schema/raw/response_to_get_delegator_delegations.json +++ b/contracts/mixnet/schema/raw/response_to_get_delegator_delegations.json @@ -66,7 +66,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -92,8 +92,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 diff --git a/contracts/mixnet/schema/raw/response_to_get_epoch_status.json b/contracts/mixnet/schema/raw/response_to_get_epoch_status.json index 1a85e633bf..cd4a2b0016 100644 --- a/contracts/mixnet/schema/raw/response_to_get_epoch_status.json +++ b/contracts/mixnet/schema/raw/response_to_get_epoch_status.json @@ -81,13 +81,39 @@ ] }, { - "description": "Represents the state of an epoch when all mixnodes have already been rewarded for their work in this epoch, all issued actions got resolved and the epoch should now be advanced whilst assigning new rewarded set.", - "type": "string", - "enum": [ - "advancing_epoch" - ] + "description": "Represents the state of an epoch when all nodes have already been rewarded for their work in this epoch, all issued actions got resolved and node roles should now be assigned before advancing into the next epoch.", + "type": "object", + "required": [ + "role_assignment" + ], + "properties": { + "role_assignment": { + "type": "object", + "required": [ + "next" + ], + "properties": { + "next": { + "$ref": "#/definitions/Role" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] + }, + "Role": { + "type": "string", + "enum": [ + "eg", + "l1", + "l2", + "l3", + "xg", + "stb" + ] } } } diff --git a/contracts/mixnet/schema/raw/response_to_get_mix_node_bonds.json b/contracts/mixnet/schema/raw/response_to_get_mix_node_bonds.json index 61bdf6b3f1..ceb99ca8cb 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mix_node_bonds.json +++ b/contracts/mixnet/schema/raw/response_to_get_mix_node_bonds.json @@ -52,14 +52,6 @@ } } }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -116,7 +108,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -133,14 +124,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -182,8 +165,7 @@ } ] } - }, - "additionalProperties": false + } }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", diff --git a/contracts/mixnet/schema/raw/response_to_get_mix_nodes_detailed.json b/contracts/mixnet/schema/raw/response_to_get_mix_nodes_detailed.json index bc95bb0839..8db9d629b6 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mix_nodes_detailed.json +++ b/contracts/mixnet/schema/raw/response_to_get_mix_nodes_detailed.json @@ -56,14 +56,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -120,7 +112,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -137,14 +128,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -186,35 +169,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -235,6 +190,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -247,14 +203,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -270,7 +253,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -310,7 +293,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -323,6 +306,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", @@ -331,8 +323,7 @@ "format": "uint32", "minimum": 0.0 } - }, - "additionalProperties": false + } }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", diff --git a/contracts/mixnet/schema/raw/response_to_get_mixnode_delegations.json b/contracts/mixnet/schema/raw/response_to_get_mixnode_delegations.json index 82d8c56b90..dccb346034 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mixnode_delegations.json +++ b/contracts/mixnet/schema/raw/response_to_get_mixnode_delegations.json @@ -54,7 +54,7 @@ "amount", "cumulative_reward_ratio", "height", - "mix_id", + "node_id", "owner" ], "properties": { @@ -80,8 +80,8 @@ "format": "uint64", "minimum": 0.0 }, - "mix_id": { - "description": "Id of the MixNode that this delegation was performed against.", + "node_id": { + "description": "Id of the Node that this delegation was performed against.", "type": "integer", "format": "uint32", "minimum": 0.0 diff --git a/contracts/mixnet/schema/raw/response_to_get_mixnode_details.json b/contracts/mixnet/schema/raw/response_to_get_mixnode_details.json index 4d7bd42da4..bee2b30522 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mixnode_details.json +++ b/contracts/mixnet/schema/raw/response_to_get_mixnode_details.json @@ -50,14 +50,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -114,7 +106,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -131,14 +122,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -180,35 +163,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -229,6 +184,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -241,14 +197,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -264,7 +247,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -304,7 +287,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -317,6 +300,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", @@ -325,8 +317,7 @@ "format": "uint32", "minimum": 0.0 } - }, - "additionalProperties": false + } }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", diff --git a/contracts/mixnet/schema/raw/response_to_get_mixnode_rewarding_details.json b/contracts/mixnet/schema/raw/response_to_get_mixnode_rewarding_details.json index 9b30dc4a75..dcb9cb67cd 100644 --- a/contracts/mixnet/schema/raw/response_to_get_mixnode_rewarding_details.json +++ b/contracts/mixnet/schema/raw/response_to_get_mixnode_rewarding_details.json @@ -17,7 +17,7 @@ "description": "If there exists a mixnode with the provided id, this field contains its rewarding information.", "anyOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" }, { "type": "null" @@ -46,7 +46,7 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -55,7 +55,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -63,7 +63,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -73,7 +73,7 @@ }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -89,7 +89,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -129,7 +129,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" diff --git a/contracts/mixnet/schema/raw/response_to_get_node_delegations.json b/contracts/mixnet/schema/raw/response_to_get_node_delegations.json new file mode 100644 index 0000000000..c8ff4df221 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_node_delegations.json @@ -0,0 +1,116 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedNodeDelegationsResponse", + "description": "Response containing paged list of all delegations made towards particular node.", + "type": "object", + "required": [ + "delegations" + ], + "properties": { + "delegations": { + "description": "Each individual delegation made.", + "type": "array", + "items": { + "$ref": "#/definitions/Delegation" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Delegation": { + "description": "Information about tokens being delegated towards given mixnode in order to accrue rewards with their work.", + "type": "object", + "required": [ + "amount", + "cumulative_reward_ratio", + "height", + "node_id", + "owner" + ], + "properties": { + "amount": { + "description": "Original delegation amount. Note that it is never mutated as delegation accumulates rewards.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "cumulative_reward_ratio": { + "description": "Value of the \"unit delegation\" associated with the mixnode at the time of delegation.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "height": { + "description": "Block height where this delegation occurred.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "node_id": { + "description": "Id of the Node that this delegation was performed against.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this delegation.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "proxy": { + "description": "Proxy address used to delegate the funds on behalf of another address", + "anyOf": [ + { + "$ref": "#/definitions/Addr" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_node_rewarding_details.json b/contracts/mixnet/schema/raw/response_to_get_node_rewarding_details.json new file mode 100644 index 0000000000..e9b3a6e545 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_node_rewarding_details.json @@ -0,0 +1,155 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeRewardingDetailsResponse", + "description": "Response containing rewarding information of a node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "node_id": { + "description": "Id of the requested node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "rewarding_details": { + "description": "If there exists a node with the provided id, this field contains its rewarding information.", + "anyOf": [ + { + "$ref": "#/definitions/NodeRewarding" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_node_stake_saturation.json b/contracts/mixnet/schema/raw/response_to_get_node_stake_saturation.json new file mode 100644 index 0000000000..3aa1cddc13 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_node_stake_saturation.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "StakeSaturationResponse", + "description": "Response containing the current state of the stake saturation of a node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "current_saturation": { + "description": "The current stake saturation of this node that is indirectly used in reward calculation formulas. Note that it can't be larger than 1.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "uncapped_saturation": { + "description": "The current, absolute, stake saturation of this node. Note that as the name suggests it can be larger than 1. However, anything beyond that value has no effect on the total node reward.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_nym_node_bonds_paged.json b/contracts/mixnet/schema/raw/response_to_get_nym_node_bonds_paged.json new file mode 100644 index 0000000000..6be0563f7a --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_nym_node_bonds_paged.json @@ -0,0 +1,134 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedNymNodeBondsResponse", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "The nym node bond information present in the contract.", + "type": "array", + "items": { + "$ref": "#/definitions/NymNodeBond" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_nym_node_details.json b/contracts/mixnet/schema/raw/response_to_get_nym_node_details.json new file mode 100644 index 0000000000..4707dce857 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_nym_node_details.json @@ -0,0 +1,299 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeDetailsResponse", + "description": "Response containing details of a node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "details": { + "description": "If there exists a node with the provided id, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_nym_node_details_by_identity_key.json b/contracts/mixnet/schema/raw/response_to_get_nym_node_details_by_identity_key.json new file mode 100644 index 0000000000..aea64e0746 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_nym_node_details_by_identity_key.json @@ -0,0 +1,297 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeDetailsByIdentityResponse", + "description": "Response containing details of a bonded node with the provided identity key.", + "type": "object", + "required": [ + "identity_key" + ], + "properties": { + "details": { + "description": "If there exists a bonded node with the provided identity key, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] + }, + "identity_key": { + "description": "The identity key (base58-encoded ed25519 public key) of the node.", + "type": "string" + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_nym_nodes_detailed_paged.json b/contracts/mixnet/schema/raw/response_to_get_nym_nodes_detailed_paged.json new file mode 100644 index 0000000000..0b96dd856c --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_nym_nodes_detailed_paged.json @@ -0,0 +1,297 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedNymNodeDetailsResponse", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "All nym-node details stored in the contract. Apart from the basic bond information it also contains details required for all future reward calculation as well as any pending changes requested by the operator.", + "type": "array", + "items": { + "$ref": "#/definitions/NymNodeDetails" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_owned_mixnode.json b/contracts/mixnet/schema/raw/response_to_get_owned_mixnode.json index da895c4bf4..94499e2fd0 100644 --- a/contracts/mixnet/schema/raw/response_to_get_owned_mixnode.json +++ b/contracts/mixnet/schema/raw/response_to_get_owned_mixnode.json @@ -52,14 +52,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "Layer": { - "type": "string", - "enum": [ - "One", - "Two", - "Three" - ] - }, "MixNode": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -116,7 +108,6 @@ "required": [ "bonding_height", "is_unbonding", - "layer", "mix_id", "mix_node", "original_pledge", @@ -133,14 +124,6 @@ "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", "type": "boolean" }, - "layer": { - "description": "Layer assigned to this mixnode.", - "allOf": [ - { - "$ref": "#/definitions/Layer" - } - ] - }, "mix_id": { "description": "Unique id assigned to the bonded mixnode.", "type": "integer", @@ -182,35 +165,7 @@ } ] } - }, - "additionalProperties": false - }, - "MixNodeCostParams": { - "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", - "type": "object", - "required": [ - "interval_operating_cost", - "profit_margin_percent" - ], - "properties": { - "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", - "allOf": [ - { - "$ref": "#/definitions/Percent" - } - ] - } - }, - "additionalProperties": false + } }, "MixNodeDetails": { "description": "Full details associated with given mixnode.", @@ -231,6 +186,7 @@ "pending_changes": { "description": "Adjustments to the mixnode that are ought to happen during future epoch transitions.", "default": { + "cost_params_change": null, "pledge_change": null }, "allOf": [ @@ -243,14 +199,41 @@ "description": "Details used for computation of rewarding related data.", "allOf": [ { - "$ref": "#/definitions/MixNodeRewarding" + "$ref": "#/definitions/NodeRewarding" } ] } }, "additionalProperties": false }, - "MixNodeRewarding": { + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { "type": "object", "required": [ "cost_params", @@ -266,7 +249,7 @@ "description": "Information provided by the operator that influence the cost function.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } ] }, @@ -306,7 +289,7 @@ "minimum": 0.0 }, "unit_delegation": { - "description": "Value of the theoretical \"unit delegation\" that has delegated to this mixnode at block 0.", + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", "allOf": [ { "$ref": "#/definitions/Decimal" @@ -319,6 +302,15 @@ "PendingMixNodeChanges": { "type": "object", "properties": { + "cost_params_change": { + "default": null, + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, "pledge_change": { "type": [ "integer", @@ -327,8 +319,7 @@ "format": "uint32", "minimum": 0.0 } - }, - "additionalProperties": false + } }, "Percent": { "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", diff --git a/contracts/mixnet/schema/raw/response_to_get_owned_nym_node.json b/contracts/mixnet/schema/raw/response_to_get_owned_nym_node.json new file mode 100644 index 0000000000..ed1ccc194d --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_owned_nym_node.json @@ -0,0 +1,301 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "NodeOwnershipResponse", + "description": "Response containing details of a node belonging to the particular owner.", + "type": "object", + "required": [ + "address" + ], + "properties": { + "address": { + "description": "Validated address of the node owner.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "details": { + "description": "If the provided address owns a nym-node, this field contains its detailed information.", + "anyOf": [ + { + "$ref": "#/definitions/NymNodeDetails" + }, + { + "type": "null" + } + ] + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "NodeCostParams": { + "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", + "type": "object", + "required": [ + "interval_operating_cost", + "profit_margin_percent" + ], + "properties": { + "interval_operating_cost": { + "description": "Operating cost of the associated node per the entire interval.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "profit_margin_percent": { + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", + "allOf": [ + { + "$ref": "#/definitions/Percent" + } + ] + } + }, + "additionalProperties": false + }, + "NodeRewarding": { + "type": "object", + "required": [ + "cost_params", + "delegates", + "last_rewarded_epoch", + "operator", + "total_unit_reward", + "unique_delegations", + "unit_delegation" + ], + "properties": { + "cost_params": { + "description": "Information provided by the operator that influence the cost function.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" + } + ] + }, + "delegates": { + "description": "Total delegation and compounded reward earned by all node delegators.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "last_rewarded_epoch": { + "description": "Marks the epoch when this node was last rewarded so that we wouldn't accidentally attempt to reward it multiple times in the same epoch.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "operator": { + "description": "Total pledge and compounded reward earned by the node operator.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "total_unit_reward": { + "description": "Cumulative reward earned by the \"unit delegation\" since the block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "unique_delegations": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "unit_delegation": { + "description": "Value of the theoretical \"unit delegation\" that has delegated to this node at block 0.", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + } + }, + "additionalProperties": false + }, + "NymNode": { + "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", + "type": "object", + "required": [ + "host", + "identity_key" + ], + "properties": { + "custom_http_port": { + "description": "Allow specifying custom port for accessing the http, and thus self-described, api of this node for the capabilities discovery.", + "type": [ + "integer", + "null" + ], + "format": "uint16", + "minimum": 0.0 + }, + "host": { + "description": "Network address of this nym-node, for example 1.1.1.1 or foo.mixnode.com that is used to discover other capabilities of this node.", + "type": "string" + }, + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + } + }, + "additionalProperties": false + }, + "NymNodeBond": { + "type": "object", + "required": [ + "bonding_height", + "is_unbonding", + "node", + "node_id", + "original_pledge", + "owner" + ], + "properties": { + "bonding_height": { + "description": "Block height at which this nym-node has been bonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + }, + "is_unbonding": { + "description": "Flag to indicate whether this node is in the process of unbonding, that will conclude upon the epoch finishing.", + "type": "boolean" + }, + "node": { + "description": "Information provided by the operator for the purposes of bonding.", + "allOf": [ + { + "$ref": "#/definitions/NymNode" + } + ] + }, + "node_id": { + "description": "Unique id assigned to the bonded node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "original_pledge": { + "description": "Original amount pledged by the operator of this node.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "owner": { + "description": "Address of the owner of this nym-node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + } + }, + "additionalProperties": false + }, + "NymNodeDetails": { + "description": "Full details associated with given node.", + "type": "object", + "required": [ + "bond_information", + "pending_changes", + "rewarding_details" + ], + "properties": { + "bond_information": { + "description": "Basic bond information of this node, such as owner address, original pledge, etc.", + "allOf": [ + { + "$ref": "#/definitions/NymNodeBond" + } + ] + }, + "pending_changes": { + "description": "Adjustments to the node that are scheduled to happen during future epoch/interval transitions.", + "allOf": [ + { + "$ref": "#/definitions/PendingNodeChanges" + } + ] + }, + "rewarding_details": { + "description": "Details used for computation of rewarding related data.", + "allOf": [ + { + "$ref": "#/definitions/NodeRewarding" + } + ] + } + }, + "additionalProperties": false + }, + "PendingNodeChanges": { + "type": "object", + "properties": { + "cost_params_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + }, + "pledge_change": { + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, + "Percent": { + "description": "Percent represents a value between 0 and 100% (i.e. between 0.0 and 1.0)", + "allOf": [ + { + "$ref": "#/definitions/Decimal" + } + ] + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_delegator_reward.json b/contracts/mixnet/schema/raw/response_to_get_pending_delegator_reward.json index e885d5976c..4ae2297c32 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_delegator_reward.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_delegator_reward.json @@ -4,7 +4,8 @@ "description": "Response containing information about accrued rewards.", "type": "object", "required": [ - "mixnode_still_fully_bonded" + "mixnode_still_fully_bonded", + "node_still_fully_bonded" ], "properties": { "amount_earned": { @@ -42,6 +43,10 @@ }, "mixnode_still_fully_bonded": { "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { "type": "boolean" } }, diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_epoch_event.json b/contracts/mixnet/schema/raw/response_to_get_pending_epoch_event.json index 7588cad3e9..67b89db672 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_epoch_event.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_epoch_event.json @@ -24,6 +24,36 @@ }, "additionalProperties": false, "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Addr": { "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", "type": "string" @@ -72,7 +102,7 @@ "description": "Enum encompassing all possible epoch events.", "oneOf": [ { - "description": "Request to create a delegation towards particular mixnode. Note that if such delegation already exists, it will get updated with the provided token amount.", + "description": "Request to create a delegation towards particular node. Note that if such delegation already exists, it will get updated with the provided token amount.", "type": "object", "required": [ "delegate" @@ -82,7 +112,7 @@ "type": "object", "required": [ "amount", - "mix_id", + "node_id", "owner" ], "properties": { @@ -94,8 +124,8 @@ } ] }, - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -126,7 +156,7 @@ "additionalProperties": false }, { - "description": "Request to remove delegation from particular mixnode.", + "description": "Request to remove delegation from particular node.", "type": "object", "required": [ "undelegate" @@ -135,12 +165,12 @@ "undelegate": { "type": "object", "required": [ - "mix_id", + "node_id", "owner" ], "properties": { - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -174,10 +204,44 @@ "description": "Request to pledge more tokens (by the node operator) towards its node.", "type": "object", "required": [ - "pledge_more" + "nym_node_pledge_more" + ], + "properties": { + "nym_node_pledge_more": { + "type": "object", + "required": [ + "amount", + "node_id" + ], + "properties": { + "amount": { + "description": "The amount of additional tokens to use in the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to pledge more tokens (by the node operator) towards its node.", + "type": "object", + "required": [ + "mixnode_pledge_more" ], "properties": { - "pledge_more": { + "mixnode_pledge_more": { "type": "object", "required": [ "amount", @@ -185,7 +249,7 @@ ], "properties": { "amount": { - "description": "The amount of additional tokens to use by the pledge.", + "description": "The amount of additional tokens to use in the pledge.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -208,10 +272,44 @@ "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", "type": "object", "required": [ - "decrease_pledge" + "nym_node_decrease_pledge" ], "properties": { - "decrease_pledge": { + "nym_node_decrease_pledge": { + "type": "object", + "required": [ + "decrease_by", + "node_id" + ], + "properties": { + "decrease_by": { + "description": "The amount of tokens that should be removed from the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", + "type": "object", + "required": [ + "mixnode_decrease_pledge" + ], + "properties": { + "mixnode_decrease_pledge": { "type": "object", "required": [ "decrease_by", @@ -264,20 +362,20 @@ "additionalProperties": false }, { - "description": "Request to update the current size of the active set.", + "description": "Request to unbond a nym node and completely remove it from the network.", "type": "object", "required": [ - "update_active_set_size" + "unbond_nym_node" ], "properties": { - "update_active_set_size": { + "unbond_nym_node": { "type": "object", "required": [ - "new_size" + "node_id" ], "properties": { - "new_size": { - "description": "The new desired size of the active set.", + "node_id": { + "description": "The id of the node that will get unbonded.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -287,6 +385,28 @@ } }, "additionalProperties": false + }, + { + "description": "Request to update the current active set.", + "type": "object", + "required": [ + "update_active_set" + ], + "properties": { + "update_active_set": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/ActiveSetUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_epoch_events.json b/contracts/mixnet/schema/raw/response_to_get_pending_epoch_events.json index 09a4d03787..54a6a8e52f 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_epoch_events.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_epoch_events.json @@ -32,6 +32,36 @@ }, "additionalProperties": false, "definitions": { + "ActiveSetUpdate": { + "description": "Specification on how the active set should be updated.", + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Addr": { "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", "type": "string" @@ -105,7 +135,7 @@ "description": "Enum encompassing all possible epoch events.", "oneOf": [ { - "description": "Request to create a delegation towards particular mixnode. Note that if such delegation already exists, it will get updated with the provided token amount.", + "description": "Request to create a delegation towards particular node. Note that if such delegation already exists, it will get updated with the provided token amount.", "type": "object", "required": [ "delegate" @@ -115,7 +145,7 @@ "type": "object", "required": [ "amount", - "mix_id", + "node_id", "owner" ], "properties": { @@ -127,8 +157,8 @@ } ] }, - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -159,7 +189,7 @@ "additionalProperties": false }, { - "description": "Request to remove delegation from particular mixnode.", + "description": "Request to remove delegation from particular node.", "type": "object", "required": [ "undelegate" @@ -168,12 +198,12 @@ "undelegate": { "type": "object", "required": [ - "mix_id", + "node_id", "owner" ], "properties": { - "mix_id": { - "description": "The id of the mixnode used for the delegation.", + "node_id": { + "description": "The id of the node used for the delegation.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -207,10 +237,44 @@ "description": "Request to pledge more tokens (by the node operator) towards its node.", "type": "object", "required": [ - "pledge_more" + "nym_node_pledge_more" + ], + "properties": { + "nym_node_pledge_more": { + "type": "object", + "required": [ + "amount", + "node_id" + ], + "properties": { + "amount": { + "description": "The amount of additional tokens to use in the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to pledge more tokens (by the node operator) towards its node.", + "type": "object", + "required": [ + "mixnode_pledge_more" ], "properties": { - "pledge_more": { + "mixnode_pledge_more": { "type": "object", "required": [ "amount", @@ -218,7 +282,7 @@ ], "properties": { "amount": { - "description": "The amount of additional tokens to use by the pledge.", + "description": "The amount of additional tokens to use in the pledge.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -241,10 +305,44 @@ "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", "type": "object", "required": [ - "decrease_pledge" + "nym_node_decrease_pledge" ], "properties": { - "decrease_pledge": { + "nym_node_decrease_pledge": { + "type": "object", + "required": [ + "decrease_by", + "node_id" + ], + "properties": { + "decrease_by": { + "description": "The amount of tokens that should be removed from the pledge.", + "allOf": [ + { + "$ref": "#/definitions/Coin" + } + ] + }, + "node_id": { + "description": "The id of the nym node that will have its pledge updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to decrease amount of pledged tokens (by the node operator) from its node.", + "type": "object", + "required": [ + "mixnode_decrease_pledge" + ], + "properties": { + "mixnode_decrease_pledge": { "type": "object", "required": [ "decrease_by", @@ -297,20 +395,20 @@ "additionalProperties": false }, { - "description": "Request to update the current size of the active set.", + "description": "Request to unbond a nym node and completely remove it from the network.", "type": "object", "required": [ - "update_active_set_size" + "unbond_nym_node" ], "properties": { - "update_active_set_size": { + "unbond_nym_node": { "type": "object", "required": [ - "new_size" + "node_id" ], "properties": { - "new_size": { - "description": "The new desired size of the active set.", + "node_id": { + "description": "The id of the node that will get unbonded.", "type": "integer", "format": "uint32", "minimum": 0.0 @@ -320,6 +418,28 @@ } }, "additionalProperties": false + }, + { + "description": "Request to update the current active set.", + "type": "object", + "required": [ + "update_active_set" + ], + "properties": { + "update_active_set": { + "type": "object", + "required": [ + "update" + ], + "properties": { + "update": { + "$ref": "#/definitions/ActiveSetUpdate" + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false } ] }, diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_interval_event.json b/contracts/mixnet/schema/raw/response_to_get_pending_interval_event.json index a0877027a9..f6926bf188 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_interval_event.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_interval_event.json @@ -80,14 +80,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -125,7 +127,7 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -134,7 +136,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -142,7 +144,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -204,9 +206,43 @@ "description": "The new updated cost function of this mixnode.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to update cost parameters of given nym node.", + "type": "object", + "required": [ + "change_nym_node_cost_params" + ], + "properties": { + "change_nym_node_cost_params": { + "type": "object", + "required": [ + "new_costs", + "node_id" + ], + "properties": { + "new_costs": { + "description": "The new updated cost function of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" } ] + }, + "node_id": { + "description": "The id of the nym node that will have its cost parameters updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false @@ -283,6 +319,42 @@ } ] }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_interval_events.json b/contracts/mixnet/schema/raw/response_to_get_pending_interval_events.json index ccbdc22178..188e99023a 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_interval_events.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_interval_events.json @@ -88,14 +88,16 @@ } ] }, - "rewarded_set_size": { - "description": "Defines the new size of the rewarded set.", - "type": [ - "integer", - "null" - ], - "format": "uint32", - "minimum": 0.0 + "rewarded_set_params": { + "description": "Defines the parameters of the rewarded set.", + "anyOf": [ + { + "$ref": "#/definitions/RewardedSetParams" + }, + { + "type": "null" + } + ] }, "staking_supply": { "description": "Defines the new value of the staking supply.", @@ -133,7 +135,7 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -142,7 +144,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -150,7 +152,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" @@ -237,9 +239,43 @@ "description": "The new updated cost function of this mixnode.", "allOf": [ { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" + } + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + { + "description": "Request to update cost parameters of given nym node.", + "type": "object", + "required": [ + "change_nym_node_cost_params" + ], + "properties": { + "change_nym_node_cost_params": { + "type": "object", + "required": [ + "new_costs", + "node_id" + ], + "properties": { + "new_costs": { + "description": "The new updated cost function of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/NodeCostParams" } ] + }, + "node_id": { + "description": "The id of the nym node that will have its cost parameters updated.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 } }, "additionalProperties": false @@ -316,6 +352,42 @@ } ] }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + }, "Uint128": { "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", "type": "string" diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_node_operator_reward.json b/contracts/mixnet/schema/raw/response_to_get_pending_node_operator_reward.json new file mode 100644 index 0000000000..4ae2297c32 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_pending_node_operator_reward.json @@ -0,0 +1,79 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PendingRewardResponse", + "description": "Response containing information about accrued rewards.", + "type": "object", + "required": [ + "mixnode_still_fully_bonded", + "node_still_fully_bonded" + ], + "properties": { + "amount_earned": { + "description": "The amount of tokens that could be claimed.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "amount_earned_detailed": { + "description": "The full pending rewards. Note that it's nearly identical to `amount_earned`, however, it contains few additional decimal points for more accurate reward calculation.", + "anyOf": [ + { + "$ref": "#/definitions/Decimal" + }, + { + "type": "null" + } + ] + }, + "amount_staked": { + "description": "The amount of tokens initially staked.", + "anyOf": [ + { + "$ref": "#/definitions/Coin" + }, + { + "type": "null" + } + ] + }, + "mixnode_still_fully_bonded": { + "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { + "type": "boolean" + } + }, + "additionalProperties": false, + "definitions": { + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Decimal": { + "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", + "type": "string" + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_pending_operator_reward.json b/contracts/mixnet/schema/raw/response_to_get_pending_operator_reward.json index e885d5976c..4ae2297c32 100644 --- a/contracts/mixnet/schema/raw/response_to_get_pending_operator_reward.json +++ b/contracts/mixnet/schema/raw/response_to_get_pending_operator_reward.json @@ -4,7 +4,8 @@ "description": "Response containing information about accrued rewards.", "type": "object", "required": [ - "mixnode_still_fully_bonded" + "mixnode_still_fully_bonded", + "node_still_fully_bonded" ], "properties": { "amount_earned": { @@ -42,6 +43,10 @@ }, "mixnode_still_fully_bonded": { "description": "The associated mixnode is still fully bonded, meaning it is neither unbonded nor in the process of unbonding that would have finished at the epoch transition.", + "deprecated": true, + "type": "boolean" + }, + "node_still_fully_bonded": { "type": "boolean" } }, diff --git a/contracts/mixnet/schema/raw/response_to_get_preassigned_gateway_ids.json b/contracts/mixnet/schema/raw/response_to_get_preassigned_gateway_ids.json new file mode 100644 index 0000000000..bfd6be0f84 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_preassigned_gateway_ids.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PreassignedGatewayIdsResponse", + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "items": { + "$ref": "#/definitions/PreassignedId" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "string", + "null" + ] + } + }, + "additionalProperties": false, + "definitions": { + "PreassignedId": { + "type": "object", + "required": [ + "identity", + "node_id" + ], + "properties": { + "identity": { + "description": "The identity key (base58-encoded ed25519 public key) of the gateway.", + "type": "string" + }, + "node_id": { + "description": "The id pre-assigned to this gateway", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_rewarded_set_metadata.json b/contracts/mixnet/schema/raw/response_to_get_rewarded_set_metadata.json new file mode 100644 index 0000000000..54bdbf9f6c --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_rewarded_set_metadata.json @@ -0,0 +1,114 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "RolesMetadataResponse", + "type": "object", + "required": [ + "metadata" + ], + "properties": { + "metadata": { + "$ref": "#/definitions/RewardedSetMetadata" + } + }, + "additionalProperties": false, + "definitions": { + "RewardedSetMetadata": { + "description": "Metadata associated with the rewarded set.", + "type": "object", + "required": [ + "entry_gateway_metadata", + "epoch_id", + "exit_gateway_metadata", + "fully_assigned", + "layer1_metadata", + "layer2_metadata", + "layer3_metadata", + "standby_metadata" + ], + "properties": { + "entry_gateway_metadata": { + "description": "Metadata for the 'EntryGateway' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "epoch_id": { + "description": "Epoch that this data corresponds to.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateway_metadata": { + "description": "Metadata for the 'ExitGateway' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "fully_assigned": { + "description": "Indicates whether all roles got assigned to the set for this epoch.", + "type": "boolean" + }, + "layer1_metadata": { + "description": "Metadata for the 'Layer1' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "layer2_metadata": { + "description": "Metadata for the 'Layer2' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "layer3_metadata": { + "description": "Metadata for the 'Layer3' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + }, + "standby_metadata": { + "description": "Metadata for the 'Standby' role", + "allOf": [ + { + "$ref": "#/definitions/RoleMetadata" + } + ] + } + }, + "additionalProperties": false + }, + "RoleMetadata": { + "description": "Metadata associated with particular node role.", + "type": "object", + "required": [ + "highest_id", + "num_nodes" + ], + "properties": { + "highest_id": { + "description": "Highest, also latest, node-id of a node assigned this role.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "num_nodes": { + "description": "Number of nodes assigned this particular role.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_rewarding_params.json b/contracts/mixnet/schema/raw/response_to_get_rewarding_params.json index 8840bc31f2..d99be0119b 100644 --- a/contracts/mixnet/schema/raw/response_to_get_rewarding_params.json +++ b/contracts/mixnet/schema/raw/response_to_get_rewarding_params.json @@ -4,17 +4,10 @@ "description": "Parameters used for reward calculation.", "type": "object", "required": [ - "active_set_size", "interval", - "rewarded_set_size" + "rewarded_set" ], "properties": { - "active_set_size": { - "description": "The expected number of mixnodes in the active set.", - "type": "integer", - "format": "uint32", - "minimum": 0.0 - }, "interval": { "description": "Parameters that should remain unchanged throughout an interval.", "allOf": [ @@ -23,11 +16,8 @@ } ] }, - "rewarded_set_size": { - "description": "The expected number of mixnodes in the rewarded set (i.e. active + standby).", - "type": "integer", - "format": "uint32", - "minimum": 0.0 + "rewarded_set": { + "$ref": "#/definitions/RewardedSetParams" } }, "additionalProperties": false, @@ -124,6 +114,42 @@ "$ref": "#/definitions/Decimal" } ] + }, + "RewardedSetParams": { + "type": "object", + "required": [ + "entry_gateways", + "exit_gateways", + "mixnodes", + "standby" + ], + "properties": { + "entry_gateways": { + "description": "The expected number of nodes assigned entry gateway role (i.e. [`Role::EntryGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "exit_gateways": { + "description": "The expected number of nodes assigned exit gateway role (i.e. [`Role::ExitGateway`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "mixnodes": { + "description": "The expected number of nodes assigned the 'mixnode' role, i.e. total of [`Role::Layer1`], [`Role::Layer2`] and [`Role::Layer3`].", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "standby": { + "description": "Number of nodes in the 'standby' set. (i.e. [`Role::Standby`])", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false } } } diff --git a/contracts/mixnet/schema/raw/response_to_get_role_assignment.json b/contracts/mixnet/schema/raw/response_to_get_role_assignment.json new file mode 100644 index 0000000000..9d7ff6c4e3 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_role_assignment.json @@ -0,0 +1,26 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "EpochAssignmentResponse", + "type": "object", + "required": [ + "epoch_id", + "nodes" + ], + "properties": { + "epoch_id": { + "description": "Epoch that this data corresponds to.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "nodes": { + "type": "array", + "items": { + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + } + }, + "additionalProperties": false +} diff --git a/contracts/mixnet/schema/raw/response_to_get_stake_saturation.json b/contracts/mixnet/schema/raw/response_to_get_stake_saturation.json index 4a86c03934..c6a38d46a2 100644 --- a/contracts/mixnet/schema/raw/response_to_get_stake_saturation.json +++ b/contracts/mixnet/schema/raw/response_to_get_stake_saturation.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "title": "StakeSaturationResponse", + "title": "MixStakeSaturationResponse", "description": "Response containing the current state of the stake saturation of a mixnode with the provided id.", "type": "object", "required": [ diff --git a/contracts/mixnet/schema/raw/response_to_get_state.json b/contracts/mixnet/schema/raw/response_to_get_state.json index 9103e87b72..df4584dde6 100644 --- a/contracts/mixnet/schema/raw/response_to_get_state.json +++ b/contracts/mixnet/schema/raw/response_to_get_state.json @@ -77,8 +77,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -93,15 +92,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -112,8 +103,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" diff --git a/contracts/mixnet/schema/raw/response_to_get_state_params.json b/contracts/mixnet/schema/raw/response_to_get_state_params.json index 52d0191167..7ef87647d8 100644 --- a/contracts/mixnet/schema/raw/response_to_get_state_params.json +++ b/contracts/mixnet/schema/raw/response_to_get_state_params.json @@ -4,8 +4,7 @@ "description": "Contract parameters that could be adjusted in a transaction by the contract admin.", "type": "object", "required": [ - "minimum_gateway_pledge", - "minimum_mixnode_pledge" + "minimum_pledge" ], "properties": { "interval_operating_cost": { @@ -20,15 +19,7 @@ } ] }, - "minimum_gateway_pledge": { - "description": "Minimum amount a gateway must pledge to get into the system.", - "allOf": [ - { - "$ref": "#/definitions/Coin" - } - ] - }, - "minimum_mixnode_delegation": { + "minimum_delegation": { "description": "Minimum amount a delegator must stake in orders for his delegation to get accepted.", "anyOf": [ { @@ -39,8 +30,8 @@ } ] }, - "minimum_mixnode_pledge": { - "description": "Minimum amount a mixnode must pledge to get into the system.", + "minimum_pledge": { + "description": "Minimum amount a node must pledge to get into the system.", "allOf": [ { "$ref": "#/definitions/Coin" diff --git a/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_node.json b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_node.json new file mode 100644 index 0000000000..dde90a69e3 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_node.json @@ -0,0 +1,72 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "UnbondedNodeResponse", + "description": "Response containing basic information of an unbonded nym-node with the provided id.", + "type": "object", + "required": [ + "node_id" + ], + "properties": { + "details": { + "description": "If there existed a nym-node with the provided id, this field contains its basic information.", + "anyOf": [ + { + "$ref": "#/definitions/UnbondedNymNode" + }, + { + "type": "null" + } + ] + }, + "node_id": { + "description": "Id of the requested nym-node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_identity_key_paged.json b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_identity_key_paged.json new file mode 100644 index 0000000000..6d6b347b21 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_identity_key_paged.json @@ -0,0 +1,71 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_owner_paged.json b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_owner_paged.json new file mode 100644 index 0000000000..6d6b347b21 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_by_owner_paged.json @@ -0,0 +1,71 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_paged.json b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_paged.json new file mode 100644 index 0000000000..6d6b347b21 --- /dev/null +++ b/contracts/mixnet/schema/raw/response_to_get_unbonded_nym_nodes_paged.json @@ -0,0 +1,71 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PagedUnbondedNymNodesResponse", + "description": "Response containing paged list of all nym-nodes that have ever unbonded.", + "type": "object", + "required": [ + "nodes" + ], + "properties": { + "nodes": { + "description": "Basic information of the node such as the owner or the identity key.", + "type": "array", + "items": { + "$ref": "#/definitions/UnbondedNymNode" + } + }, + "start_next_after": { + "description": "Field indicating paging information for the following queries if the caller wishes to get further entries.", + "type": [ + "integer", + "null" + ], + "format": "uint32", + "minimum": 0.0 + } + }, + "additionalProperties": false, + "definitions": { + "Addr": { + "description": "A human readable address.\n\nIn Cosmos, this is typically bech32 encoded. But for multi-chain smart contracts no assumptions should be made other than being UTF-8 encoded and of reasonable length.\n\nThis type represents a validated address. It can be created in the following ways 1. Use `Addr::unchecked(input)` 2. Use `let checked: Addr = deps.api.addr_validate(input)?` 3. Use `let checked: Addr = deps.api.addr_humanize(canonical_addr)?` 4. Deserialize from JSON. This must only be done from JSON that was validated before such as a contract's state. `Addr` must not be used in messages sent by the user because this would result in unvalidated instances.\n\nThis type is immutable. If you really need to mutate it (Really? Are you sure?), create a mutable copy using `let mut mutable = Addr::to_string()` and operate on that `String` instance.", + "type": "string" + }, + "UnbondedNymNode": { + "description": "Basic information of a node that used to be part of the nym network but has already unbonded.", + "type": "object", + "required": [ + "identity_key", + "node_id", + "owner", + "unbonding_height" + ], + "properties": { + "identity_key": { + "description": "Base58-encoded ed25519 EdDSA public key.", + "type": "string" + }, + "node_id": { + "description": "NodeId assigned to this node.", + "type": "integer", + "format": "uint32", + "minimum": 0.0 + }, + "owner": { + "description": "Address of the owner of this nym node.", + "allOf": [ + { + "$ref": "#/definitions/Addr" + } + ] + }, + "unbonding_height": { + "description": "Block height at which this nym node has unbonded.", + "type": "integer", + "format": "uint64", + "minimum": 0.0 + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/mixnet/src/compat/helpers.rs b/contracts/mixnet/src/compat/helpers.rs new file mode 100644 index 0000000000..46b845a05d --- /dev/null +++ b/contracts/mixnet/src/compat/helpers.rs @@ -0,0 +1,576 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::mixnodes::storage as mixnode_storage; +use crate::nodes::storage as nymnodes_storage; +use crate::support::helpers::ensure_epoch_in_progress_state; +use cosmwasm_std::{Coin, StdResult, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::helpers::{NodeBond, NodeDetails, PendingChanges}; +use mixnet_contract_common::NodeId; + +pub fn ensure_can_withdraw_rewards(node_details: &D) -> Result<(), MixnetContractError> +where + D: NodeDetails, +{ + // we can only withdraw rewards for a bonded node (i.e. not in the process of unbonding) + // otherwise we know there are no rewards to withdraw + node_details.bond_info().ensure_bonded()?; + + Ok(()) +} + +pub fn ensure_can_modify_cost_params( + storage: &dyn Storage, + node_details: &D, +) -> Result<(), MixnetContractError> +where + D: NodeDetails, +{ + // changing cost params is only allowed if the epoch is currently not in the process of being advanced + ensure_epoch_in_progress_state(storage)?; + + // we can only change cost params for a bonded node (i.e. not in the process of unbonding) + node_details.bond_info().ensure_bonded()?; + + Ok(()) +} + +fn ensure_can_modify_pledge( + storage: &dyn Storage, + node_details: &D, +) -> Result<(), MixnetContractError> +where + D: NodeDetails, +{ + // changing pledge is only allowed if the epoch is currently not in the process of being advanced + ensure_epoch_in_progress_state(storage)?; + + // we can only change pledge for a bonded node (i.e. not in the process of unbonding) + node_details.bond_info().ensure_bonded()?; + + // the node can't have any pending pledge changes + node_details + .pending_changes() + .ensure_no_pending_pledge_changes()?; + + Ok(()) +} + +// remove duplicate code and make sure the same checks are performed everywhere +// (so nothing is accidentally missing) +pub fn ensure_can_increase_pledge( + storage: &dyn Storage, + node_details: &D, +) -> Result<(), MixnetContractError> +where + D: NodeDetails, +{ + ensure_can_modify_pledge(storage, node_details) +} + +// remove duplicate code and make sure the same checks are performed everywhere +// (so nothing is accidentally missing) +pub fn ensure_can_decrease_pledge( + storage: &dyn Storage, + node_details: &D, + decrease_by: &Coin, +) -> Result<(), MixnetContractError> +where + D: NodeDetails, +{ + ensure_can_modify_pledge(storage, node_details)?; + + let minimum_pledge = mixnet_params_storage::minimum_node_pledge(storage)?; + + // check that the denomination is correct + if decrease_by.denom != minimum_pledge.denom { + return Err(MixnetContractError::WrongDenom { + received: decrease_by.denom.clone(), + expected: minimum_pledge.denom, + }); + } + + // also check if the request contains non-zero amount + // (otherwise it's a no-op and we should we waste gas when resolving events?) + if decrease_by.amount.is_zero() { + return Err(MixnetContractError::ZeroCoinAmount); + } + + // decreasing pledge can't result in the new pledge being lower than the minimum amount + let new_pledge_amount = node_details + .bond_info() + .original_pledge() + .amount + .saturating_sub(decrease_by.amount); + if new_pledge_amount < minimum_pledge.amount { + return Err(MixnetContractError::InvalidPledgeReduction { + current: node_details.bond_info().original_pledge().amount, + decrease_by: decrease_by.amount, + minimum: minimum_pledge.amount, + denom: minimum_pledge.denom, + }); + } + + Ok(()) +} + +pub fn get_bond( + storage: &dyn Storage, + node_id: NodeId, +) -> Result, MixnetContractError> { + if let Ok(mix_bond) = mixnode_storage::mixnode_bonds().load(storage, node_id) { + Ok(Box::new(mix_bond)) + } else { + let node_bond = nymnodes_storage::nym_nodes() + .load(storage, node_id) + .map_err(|_| MixnetContractError::NymNodeBondNotFound { node_id })?; + Ok(Box::new(node_bond)) + } +} + +pub fn may_get_bond( + storage: &dyn Storage, + node_id: NodeId, +) -> StdResult>> { + if let Some(mix_bond) = mixnode_storage::mixnode_bonds().may_load(storage, node_id)? { + Ok(Some(Box::new(mix_bond))) + } else if let Some(node_bond) = nymnodes_storage::nym_nodes().may_load(storage, node_id)? { + Ok(Some(Box::new(node_bond))) + } else { + Ok(None) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(test)] + mod reward_withdrawing_permission { + use super::*; + use crate::support::tests::test_helpers::TestSetup; + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_legacy_mixnode("owner", None); + let details = test.mixnode_by_id(node_id).unwrap(); + + // node must not be in the process of unbonding + assert!(ensure_can_withdraw_rewards(&details).is_ok()); + + test.start_unbonding_mixnode(node_id); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_withdraw_rewards(&details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("owner", None); + let details = test.nymnode_by_id(node_id).unwrap(); + + // node must not be in the process of unbonding + assert!(ensure_can_withdraw_rewards(&details).is_ok()); + + test.start_unbonding_nymnode(node_id); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_withdraw_rewards(&details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + Ok(()) + } + } + + #[cfg(test)] + mod modifying_cost_params_permission { + use super::*; + use crate::support::tests::test_helpers::TestSetup; + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_legacy_mixnode("owner", None); + + let details = test.mixnode_by_id(node_id).unwrap(); + assert!(ensure_can_modify_cost_params(test.deps().storage, &details).is_ok()); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_modify_cost_params(test.deps().storage, &details).unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_mixnode(node_id); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_modify_cost_params(test.deps().storage, &details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("owner", None); + + let details = test.nymnode_by_id(node_id).unwrap(); + assert!(ensure_can_modify_cost_params(test.deps().storage, &details).is_ok()); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_modify_cost_params(test.deps().storage, &details).unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_nymnode(node_id); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_modify_cost_params(test.deps().storage, &details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + Ok(()) + } + } + + #[cfg(test)] + mod increasing_pledge_permission { + use super::*; + use crate::compat::transactions::{try_decrease_pledge, try_increase_pledge}; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::testing::mock_info; + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_legacy_mixnode("owner", None); + + let details = test.mixnode_by_id(node_id).unwrap(); + assert!(ensure_can_increase_pledge(test.deps().storage, &details).is_ok()); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_mixnode(node_id); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + // node can't have any pending pledge changes: + // - increase + let node_id = test.add_legacy_mixnode("owner2", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + test.execute_fn(try_increase_pledge, mock_info("owner2", &[pledge_change]))?; + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // - decrease + let node_id = test.add_legacy_mixnode("owner3", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + let env = test.env(); + try_decrease_pledge( + test.deps_mut(), + env, + mock_info("owner3", &[]), + pledge_change, + )?; + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("owner", None); + + let details = test.nymnode_by_id(node_id).unwrap(); + assert!(ensure_can_increase_pledge(test.deps().storage, &details).is_ok()); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_nymnode(node_id); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + // node can't have any pending pledge changes: + // - increase + let node_id = test.add_dummy_nymnode("owner2", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + test.execute_fn(try_increase_pledge, mock_info("owner2", &[pledge_change]))?; + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // - decrease + let node_id = test.add_dummy_nymnode("owner3", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + let env = test.env(); + try_decrease_pledge( + test.deps_mut(), + env, + mock_info("owner3", &[]), + pledge_change, + )?; + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_increase_pledge(test.deps().storage, &details).unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + Ok(()) + } + } + + #[cfg(test)] + mod decreasing_pledge_permission { + use super::*; + use crate::compat::transactions::{try_decrease_pledge, try_increase_pledge}; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::coin; + use cosmwasm_std::testing::mock_info; + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_legacy_mixnode("owner", Some(100_000_000_000u128.into())); + let valid_decrease = test.coin(100); + + let details = test.mixnode_by_id(node_id).unwrap(); + assert!( + ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease).is_ok() + ); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_mixnode(node_id); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + // node can't have any pending pledge changes: + // - increase + let node_id = test.add_legacy_mixnode("owner2", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + test.execute_fn(try_increase_pledge, mock_info("owner2", &[pledge_change]))?; + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // - decrease + let node_id = test.add_legacy_mixnode("owner3", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + let env = test.env(); + try_decrease_pledge( + test.deps_mut(), + env, + mock_info("owner3", &[]), + pledge_change, + )?; + let details = test.mixnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // denom must match + let node_id = test.add_legacy_mixnode("owner4", Some(100_000_000_000u128.into())); + let details = test.mixnode_by_id(node_id).unwrap(); + let bad_decrease = coin(123, "weird-denom"); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert!(matches!(res, MixnetContractError::WrongDenom { .. })); + + // value must be non-zero + let node_id = test.add_legacy_mixnode("owner5", Some(100_000_000_000u128.into())); + let details = test.mixnode_by_id(node_id).unwrap(); + let bad_decrease = test.coin(0); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert_eq!(res, MixnetContractError::ZeroCoinAmount); + + // new pledge must be bigger than minimum + let node_id = test.add_legacy_mixnode("owner6", Some(100_000_100u128.into())); + let details = test.mixnode_by_id(node_id).unwrap(); + let bad_decrease = test.coin(101); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert!(matches!( + res, + MixnetContractError::InvalidPledgeReduction { .. } + )); + + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("owner", Some(100_000_000_000u128.into())); + let valid_decrease = test.coin(100); + + let details = test.nymnode_by_id(node_id).unwrap(); + assert!( + ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease).is_ok() + ); + + // epoch must not be mid-transition + test.skip_to_current_epoch_end(); + test.start_epoch_transition(); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert!(matches!( + res, + MixnetContractError::EpochAdvancementInProgress { .. } + )); + test.set_epoch_in_progress_state(); + + // node must not be in the process of unbonding + test.start_unbonding_nymnode(node_id); + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!(res, MixnetContractError::NodeIsUnbonding { node_id }); + + // node can't have any pending pledge changes: + // - increase + let node_id = test.add_dummy_nymnode("owner2", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + test.execute_fn(try_increase_pledge, mock_info("owner2", &[pledge_change]))?; + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // - decrease + let node_id = test.add_dummy_nymnode("owner3", Some(100_000_000_000u128.into())); + let pledge_change = test.coin(100000); + let env = test.env(); + try_decrease_pledge( + test.deps_mut(), + env, + mock_info("owner3", &[]), + pledge_change, + )?; + let details = test.nymnode_by_id(node_id).unwrap(); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &valid_decrease) + .unwrap_err(); + assert_eq!( + res, + MixnetContractError::PendingPledgeChange { + pending_event_id: details.pending_changes.pledge_change.unwrap() + } + ); + + // denom must match + let node_id = test.add_dummy_nymnode("owner4", Some(100_000_000_000u128.into())); + let details = test.nymnode_by_id(node_id).unwrap(); + let bad_decrease = coin(123, "weird-denom"); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert!(matches!(res, MixnetContractError::WrongDenom { .. })); + + // value must be non-zero + let node_id = test.add_dummy_nymnode("owner5", Some(100_000_000_000u128.into())); + let details = test.nymnode_by_id(node_id).unwrap(); + let bad_decrease = test.coin(0); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert_eq!(res, MixnetContractError::ZeroCoinAmount); + + // new pledge must be bigger than minimum + let node_id = test.add_dummy_nymnode("owner6", Some(100_000_100u128.into())); + let details = test.nymnode_by_id(node_id).unwrap(); + let bad_decrease = test.coin(101); + let res = ensure_can_decrease_pledge(test.deps().storage, &details, &bad_decrease) + .unwrap_err(); + assert!(matches!( + res, + MixnetContractError::InvalidPledgeReduction { .. } + )); + + Ok(()) + } + } +} diff --git a/contracts/mixnet/src/compat/mod.rs b/contracts/mixnet/src/compat/mod.rs new file mode 100644 index 0000000000..94d181c9dc --- /dev/null +++ b/contracts/mixnet/src/compat/mod.rs @@ -0,0 +1,7 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod helpers; +pub(crate) mod queries; +mod storage_traits; +pub(crate) mod transactions; diff --git a/contracts/mixnet/src/compat/queries.rs b/contracts/mixnet/src/compat/queries.rs new file mode 100644 index 0000000000..908c7122bd --- /dev/null +++ b/contracts/mixnet/src/compat/queries.rs @@ -0,0 +1,76 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// // will return details of either nym-node, legacy mixnode or legacy gateway +// pub fn query_owned_node() { +// todo!() +// } + +pub(crate) mod rewards { + use crate::mixnodes::helpers::{get_mixnode_details_by_id, get_mixnode_details_by_owner}; + use crate::nodes::helpers::{get_node_details_by_id, get_node_details_by_owner}; + use cosmwasm_std::{Addr, Deps, StdResult}; + use mixnet_contract_common::{NodeId, PendingRewardResponse}; + + #[allow(deprecated)] + pub(crate) fn pending_operator_reward( + deps: Deps<'_>, + operator: Addr, + ) -> StdResult { + // check if owns mixnode or nymnode and query accordingly + if let Some(nym_node_details) = get_node_details_by_owner(deps.storage, operator.clone())? { + Ok(PendingRewardResponse { + amount_staked: Some(nym_node_details.original_pledge().clone()), + amount_earned: Some(nym_node_details.pending_operator_reward()), + amount_earned_detailed: Some(nym_node_details.pending_detailed_operator_reward()?), + mixnode_still_fully_bonded: !nym_node_details.is_unbonding(), + node_still_fully_bonded: !nym_node_details.is_unbonding(), + }) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_owner(deps.storage, operator)? + { + Ok(PendingRewardResponse { + amount_staked: Some(legacy_mixnode_details.original_pledge().clone()), + amount_earned: Some(legacy_mixnode_details.pending_operator_reward()), + amount_earned_detailed: Some( + legacy_mixnode_details.pending_detailed_operator_reward()?, + ), + mixnode_still_fully_bonded: !legacy_mixnode_details.is_unbonding(), + node_still_fully_bonded: !legacy_mixnode_details.is_unbonding(), + }) + } else { + Ok(PendingRewardResponse::default()) + } + } + + #[allow(deprecated)] + pub(crate) fn pending_operator_reward_by_id( + deps: Deps<'_>, + node_id: NodeId, + ) -> StdResult { + // check if owns mixnode or nymnode and query accordingly + if let Some(nym_node_details) = get_node_details_by_id(deps.storage, node_id)? { + Ok(PendingRewardResponse { + amount_staked: Some(nym_node_details.original_pledge().clone()), + amount_earned: Some(nym_node_details.pending_operator_reward()), + amount_earned_detailed: Some(nym_node_details.pending_detailed_operator_reward()?), + mixnode_still_fully_bonded: !nym_node_details.is_unbonding(), + node_still_fully_bonded: !nym_node_details.is_unbonding(), + }) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_id(deps.storage, node_id)? + { + Ok(PendingRewardResponse { + amount_staked: Some(legacy_mixnode_details.original_pledge().clone()), + amount_earned: Some(legacy_mixnode_details.pending_operator_reward()), + amount_earned_detailed: Some( + legacy_mixnode_details.pending_detailed_operator_reward()?, + ), + mixnode_still_fully_bonded: !legacy_mixnode_details.is_unbonding(), + node_still_fully_bonded: !legacy_mixnode_details.is_unbonding(), + }) + } else { + Ok(PendingRewardResponse::default()) + } + } +} diff --git a/contracts/mixnet/src/compat/storage_traits.rs b/contracts/mixnet/src/compat/storage_traits.rs new file mode 100644 index 0000000000..5cb369ff51 --- /dev/null +++ b/contracts/mixnet/src/compat/storage_traits.rs @@ -0,0 +1,48 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// use crate::mixnodes::storage as mixnodes_storage; +// use crate::nodes::storage as nymnodes_storage; +// use crate::rewards::storage as rewards_storage; +// use cosmwasm_std::Storage; +// use cw_storage_plus::Map; +// use mixnet_contract_common::error::MixnetContractError; +// use mixnet_contract_common::mixnode::PendingMixNodeChanges; +// use mixnet_contract_common::{NodeId, PendingNodeChanges}; +// use serde::de::DeserializeOwned; +// use serde::{Deserialize, Serialize}; +// +// // I've created this trait to ensure everything is always stored in the right storage bucket, +// // because I fear I might have accidentally missed something during the transition period +// // of having BOTH mixnodes and nym-nodes +// pub(crate) trait NodeDetailsStorage { +// // +// } +// +// pub(crate) trait NodeBondStorage { +// // +// } +// +// pub(crate) trait PendingChangesStorage: Sized + Serialize + DeserializeOwned { +// const STORAGE_MAP: Map<'static, NodeId, Self>; +// +// fn save(&self, storage: &mut dyn Storage, node_id: NodeId) -> Result<(), MixnetContractError> { +// Ok(Self::STORAGE_MAP.save(storage, node_id, self)?) +// } +// +// fn load(storage: &dyn Storage, node_id: NodeId) -> Result { +// Ok(Self::STORAGE_MAP.load(storage, node_id)?) +// } +// } +// +// pub(crate) trait RewardingStorage { +// // +// } +// +// impl PendingChangesStorage for PendingNodeChanges { +// const STORAGE_MAP: Map<'static, NodeId, Self> = nymnodes_storage::PENDING_NYMNODE_CHANGES; +// } +// +// impl PendingChangesStorage for PendingMixNodeChanges { +// const STORAGE_MAP: Map<'static, NodeId, Self> = mixnodes_storage::PENDING_MIXNODE_CHANGES; +// } diff --git a/contracts/mixnet/src/compat/transactions.rs b/contracts/mixnet/src/compat/transactions.rs new file mode 100644 index 0000000000..64630b564e --- /dev/null +++ b/contracts/mixnet/src/compat/transactions.rs @@ -0,0 +1,658 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::mixnodes::helpers::get_mixnode_details_by_owner; +use crate::mixnodes::transactions::{ + try_decrease_mixnode_pledge, try_increase_mixnode_pledge, try_update_mixnode_cost_params, +}; +use crate::nodes::helpers::get_node_details_by_owner; +use crate::nodes::transactions::{ + try_decrease_nym_node_pledge, try_increase_nym_node_pledge, try_update_nym_node_cost_params, +}; +use crate::rewards::transactions::{ + try_withdraw_mixnode_operator_reward, try_withdraw_nym_node_operator_reward, +}; +use crate::support::helpers::{ + ensure_operating_cost_within_range, ensure_profit_margin_within_range, +}; +use cosmwasm_std::{Coin, DepsMut, Env, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::NodeCostParams; + +pub(crate) fn try_increase_pledge( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, +) -> Result { + // check if owns mixnode or nymnode and change accordingly + if let Some(nym_node_details) = get_node_details_by_owner(deps.storage, info.sender.clone())? { + try_increase_nym_node_pledge(deps, env, info.funds, nym_node_details) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_owner(deps.storage, info.sender.clone())? + { + try_increase_mixnode_pledge(deps, env, info.funds, legacy_mixnode_details) + } else { + Err(MixnetContractError::NoAssociatedNodeBond { owner: info.sender }) + } +} + +pub fn try_decrease_pledge( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + decrease_by: Coin, +) -> Result { + // check if owns mixnode or nymnode and change accordingly + if let Some(nym_node_details) = get_node_details_by_owner(deps.storage, info.sender.clone())? { + try_decrease_nym_node_pledge(deps, env, decrease_by, nym_node_details) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_owner(deps.storage, info.sender.clone())? + { + try_decrease_mixnode_pledge(deps, env, decrease_by, legacy_mixnode_details) + } else { + Err(MixnetContractError::NoAssociatedNodeBond { owner: info.sender }) + } +} + +pub(crate) fn try_update_cost_params( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + new_costs: NodeCostParams, +) -> Result { + // ensure the profit margin is within the defined range + ensure_profit_margin_within_range(deps.storage, new_costs.profit_margin_percent)?; + + // ensure the operating cost is within the defined range + ensure_operating_cost_within_range(deps.storage, &new_costs.interval_operating_cost)?; + + // check if owns mixnode or nymnode and change accordingly + if let Some(nym_node_details) = get_node_details_by_owner(deps.storage, info.sender.clone())? { + try_update_nym_node_cost_params(deps, env, new_costs, nym_node_details) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_owner(deps.storage, info.sender.clone())? + { + try_update_mixnode_cost_params(deps, env, new_costs, legacy_mixnode_details) + } else { + Err(MixnetContractError::NoAssociatedNodeBond { owner: info.sender }) + } +} + +pub(crate) fn try_withdraw_operator_reward( + deps: DepsMut<'_>, + info: MessageInfo, +) -> Result { + // check if owns mixnode or nymnode and change accordingly + if let Some(nym_node_details) = get_node_details_by_owner(deps.storage, info.sender.clone())? { + try_withdraw_nym_node_operator_reward(deps, nym_node_details) + } else if let Some(legacy_mixnode_details) = + get_mixnode_details_by_owner(deps.storage, info.sender.clone())? + { + try_withdraw_mixnode_operator_reward(deps, legacy_mixnode_details) + } else { + Err(MixnetContractError::NoAssociatedNodeBond { owner: info.sender }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(test)] + mod increasing_pledge { + use super::*; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::Addr; + + #[test] + fn when_there_are_no_nodes() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let sender = mock_info("owner", &[test.coin(100000)]); + let err = test.execute_fn(try_increase_pledge, sender).unwrap_err(); + + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_legacy_mixnode("owner", Some(100_000_000u128.into())); + let sender = mock_info("owner", &[test.coin(100_000)]); + test.assert_simple_execution(try_increase_pledge, sender); + + let after = test.mixnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.pledge_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.mixnode_by_id(node_id).unwrap(); + assert_eq!( + after.bond_information.original_pledge.amount.u128(), + 100_100_000u128 + ); + + Ok(()) + } + + #[test] + fn for_legacy_gateway() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + test.add_legacy_gateway("owner", None); + let sender = mock_info("owner", &[test.coin(100000)]); + let err = test.execute_fn(try_increase_pledge, sender).unwrap_err(); + + // it's illegal to increase pledge for legacy gateways + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_dummy_nymnode("owner", Some(100_000_000u128.into())); + let sender = mock_info("owner", &[test.coin(100_000)]); + test.assert_simple_execution(try_increase_pledge, sender); + + let after = test.nymnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.pledge_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.nymnode_by_id(node_id).unwrap(); + assert_eq!( + after.bond_information.original_pledge.amount.u128(), + 100_100_000u128 + ); + + Ok(()) + } + } + + #[cfg(test)] + mod decreasing_pledge { + use super::*; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::Addr; + + #[test] + fn when_there_are_no_nodes() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let sender = mock_info("owner", &[]); + let env = test.env(); + let decrease_by = test.coin(1000); + let err = try_decrease_pledge(test.deps_mut(), env, sender, decrease_by).unwrap_err(); + + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_legacy_mixnode("owner", Some(120_000_000u128.into())); + let sender = mock_info("owner", &[]); + let env = test.env(); + let decrease_by = test.coin(1000); + try_decrease_pledge(test.deps_mut(), env, sender, decrease_by)?; + + let after = test.mixnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.pledge_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.mixnode_by_id(node_id).unwrap(); + assert_eq!( + after.bond_information.original_pledge.amount.u128(), + 119_999_000u128 + ); + + Ok(()) + } + + #[test] + fn for_legacy_gateway() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + test.add_legacy_gateway("owner", None); + let sender = mock_info("owner", &[]); + let env = test.env(); + let decrease_by = test.coin(1000); + let err = try_decrease_pledge(test.deps_mut(), env, sender, decrease_by).unwrap_err(); + + // it's illegal to decrease pledge for legacy gateways + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_dummy_nymnode("owner", Some(120_000_000u128.into())); + let sender = mock_info("owner", &[]); + let env = test.env(); + let decrease_by = test.coin(1000); + + try_decrease_pledge(test.deps_mut(), env, sender, decrease_by)?; + + let after = test.nymnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.pledge_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.nymnode_by_id(node_id).unwrap(); + assert_eq!( + after.bond_information.original_pledge.amount.u128(), + 119_999_000u128 + ); + + Ok(()) + } + } + + #[cfg(test)] + mod updating_cost_params { + use super::*; + use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{Addr, Uint128}; + use mixnet_contract_common::{OperatingCostRange, ProfitMarginRange}; + use nym_contracts_common::Percent; + + fn new_dummy_params() -> NodeCostParams { + NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(69).unwrap(), + interval_operating_cost: Coin { + denom: TEST_COIN_DENOM.to_string(), + amount: 123456789u128.into(), + }, + } + } + + #[test] + fn profit_margin_must_be_within_range() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let minimum = Percent::from_percentage_value(10)?; + let maximum = Percent::from_percentage_value(80)?; + let range = ProfitMarginRange::new(minimum, maximum); + test.update_profit_margin_range(range); + + // below lower + test.add_dummy_nymnode("owner1", None); + let sender = mock_info("owner1", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = Percent::from_percentage_value(9)?; + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::ProfitMarginOutsideRange { .. } + )); + + // zero + test.add_dummy_nymnode("owner2", None); + let sender = mock_info("owner2", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = Percent::zero(); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::ProfitMarginOutsideRange { .. } + )); + + // exactly at lower + test.add_dummy_nymnode("owner3", None); + let sender = mock_info("owner3", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = minimum; + let res = try_update_cost_params(test.deps_mut(), env, sender, update.clone()); + assert!(res.is_ok()); + + // above upper + test.add_dummy_nymnode("owner4", None); + let sender = mock_info("owner4", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = Percent::from_percentage_value(81)?; + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::ProfitMarginOutsideRange { .. } + )); + + // a hundred + test.add_dummy_nymnode("owner5", None); + let sender = mock_info("owner5", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = Percent::hundred(); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::ProfitMarginOutsideRange { .. } + )); + + // exactly at upper + test.add_dummy_nymnode("owner6", None); + let sender = mock_info("owner6", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.profit_margin_percent = maximum; + let res = try_update_cost_params(test.deps_mut(), env, sender, update.clone()); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn operating_cost_must_be_within_range() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let minimum = Uint128::new(1000); + let maximum = Uint128::new(100_000_000); + let range = OperatingCostRange::new(minimum, maximum); + test.update_operating_cost_range(range); + + // below lower + test.add_dummy_nymnode("owner1", None); + let sender = mock_info("owner1", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(999); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::OperatingCostOutsideRange { .. } + )); + + // zero + test.add_dummy_nymnode("owner2", None); + let sender = mock_info("owner2", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(0); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::OperatingCostOutsideRange { .. } + )); + + // exactly at lower + test.add_dummy_nymnode("owner3", None); + let sender = mock_info("owner3", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(minimum.u128()); + let res = try_update_cost_params(test.deps_mut(), env, sender, update.clone()); + assert!(res.is_ok()); + + // above upper + test.add_dummy_nymnode("owner4", None); + let sender = mock_info("owner4", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(100_000_001); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::OperatingCostOutsideRange { .. } + )); + + // max + test.add_dummy_nymnode("owner5", None); + let sender = mock_info("owner5", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(u128::MAX); + let err = + try_update_cost_params(test.deps_mut(), env, sender, update.clone()).unwrap_err(); + assert!(matches!( + err, + MixnetContractError::OperatingCostOutsideRange { .. } + )); + + // exactly at upper + test.add_dummy_nymnode("owner6", None); + let sender = mock_info("owner6", &[]); + let env = test.env(); + let mut update = new_dummy_params(); + update.interval_operating_cost = test.coin(100_000_000); + let res = try_update_cost_params(test.deps_mut(), env, sender, update.clone()); + assert!(res.is_ok()); + + Ok(()) + } + + #[test] + fn when_there_are_no_nodes() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let sender = mock_info("owner", &[]); + let env = test.env(); + let err = try_update_cost_params(test.deps_mut(), env, sender, new_dummy_params()) + .unwrap_err(); + + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_legacy_mixnode("owner", None); + let sender = mock_info("owner", &[]); + let env = test.env(); + + let update = new_dummy_params(); + try_update_cost_params(test.deps_mut(), env, sender, update.clone())?; + + let after = test.mixnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.cost_params_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.mixnode_by_id(node_id).unwrap(); + assert_eq!(update, after.rewarding_details.cost_params); + + Ok(()) + } + + #[test] + fn for_legacy_gateway() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + test.add_legacy_gateway("owner", None); + + let sender = mock_info("owner", &[]); + let env = test.env(); + let err = try_update_cost_params(test.deps_mut(), env, sender, new_dummy_params()) + .unwrap_err(); + + // it's illegal to update cost parameters for legacy gateways + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let node_id = test.add_dummy_nymnode("owner", None); + let sender = mock_info("owner", &[]); + let env = test.env(); + + let update = new_dummy_params(); + try_update_cost_params(test.deps_mut(), env, sender, update.clone())?; + + let after = test.nymnode_by_id(node_id).unwrap(); + let event_id = after.pending_changes.cost_params_change.unwrap(); + assert_eq!(event_id, 1); + + test.execute_all_pending_events(); + + let after = test.nymnode_by_id(node_id).unwrap(); + assert_eq!(update, after.rewarding_details.cost_params); + + Ok(()) + } + } + + #[cfg(test)] + mod withdrawing_operator_reward { + use super::*; + use crate::support::tests::test_helpers::{ExtractBankMsg, TestSetup}; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::Addr; + + #[test] + fn when_there_are_no_nodes() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + let sender = mock_info("owner", &[]); + let err = try_withdraw_operator_reward(test.deps_mut(), sender).unwrap_err(); + + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + + Ok(()) + } + + #[test] + fn for_legacy_mixnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let active_params = test.active_node_params(100.0); + + // no rewards + test.add_legacy_mixnode("owner1", None); + let sender = mock_info("owner1", &[]); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender)?; + let maybe_bank = res.unwrap_bank_msg(); + assert!(maybe_bank.is_none()); + + let node_id = test.add_legacy_mixnode("owner2", None); + let sender = mock_info("owner2", &[]); + test.skip_to_next_epoch_end(); + test.force_change_mix_rewarded_set(vec![node_id]); + test.start_epoch_transition(); + test.reward_with_distribution(node_id, active_params); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender)?; + let maybe_bank = res.unwrap_bank_msg(); + assert!(maybe_bank.is_some()); + + Ok(()) + } + + #[test] + fn for_legacy_gateway() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + + test.add_legacy_gateway("owner", None); + + let sender = mock_info("owner", &[]); + let err = try_withdraw_operator_reward(test.deps_mut(), sender).unwrap_err(); + + // no rewards for legacy gateways... + assert_eq!( + MixnetContractError::NoAssociatedNodeBond { + owner: Addr::unchecked("owner"), + }, + err + ); + Ok(()) + } + + #[test] + fn for_nymnode() -> anyhow::Result<()> { + let mut test = TestSetup::new(); + let active_params = test.active_node_params(100.0); + + // no rewards + test.add_dummy_nymnode("owner1", None); + let sender = mock_info("owner1", &[]); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender)?; + let maybe_bank = res.unwrap_bank_msg(); + assert!(maybe_bank.is_none()); + + let node_id = test.add_dummy_nymnode("owner2", None); + let sender = mock_info("owner2", &[]); + test.skip_to_next_epoch_end(); + test.force_change_mix_rewarded_set(vec![node_id]); + test.start_epoch_transition(); + test.reward_with_distribution(node_id, active_params); + + let res = try_withdraw_operator_reward(test.deps_mut(), sender)?; + let maybe_bank = res.unwrap_bank_msg(); + assert!(maybe_bank.is_some()); + + Ok(()) + } + } +} diff --git a/contracts/mixnet/src/constants.rs b/contracts/mixnet/src/constants.rs index 0ea867c00c..3d2d165113 100644 --- a/contracts/mixnet/src/constants.rs +++ b/contracts/mixnet/src/constants.rs @@ -1,42 +1,54 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::Uint128; - -/// Constant specifying minimum of coin amount required to bond a gateway -pub const INITIAL_GATEWAY_PLEDGE_AMOUNT: Uint128 = Uint128::new(100_000_000); - -/// Constant specifying minimum of coin amount required to bond a mixnode -pub const INITIAL_MIXNODE_PLEDGE_AMOUNT: Uint128 = Uint128::new(100_000_000); +use cosmwasm_std::{Coin, Uint128}; +use mixnet_contract_common::{ + NodeCostParams, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, DEFAULT_PROFIT_MARGIN_PERCENT, +}; +use nym_contracts_common::Percent; + +/// Constant specifying minimum of coin amount required to bond a node +pub const INITIAL_PLEDGE_AMOUNT: Uint128 = Uint128::new(100_000_000); + +pub fn default_node_costs>(rewarding_denom: S) -> NodeCostParams { + // safety: our hardcoded PM value is a valid percent + #[allow(clippy::unwrap_used)] + NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT) + .unwrap(), + interval_operating_cost: Coin::new(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, rewarding_denom), + } +} // retrieval limits // TODO: those would need to be empirically verified whether they're not way too small or way too high -pub const GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; -pub const GATEWAY_BOND_MAX_RETRIEVAL_LIMIT: u32 = 150; +pub const GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const GATEWAY_BOND_MAX_RETRIEVAL_LIMIT: u32 = 100; -pub const MIXNODE_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; -pub const MIXNODE_BOND_MAX_RETRIEVAL_LIMIT: u32 = 150; +pub const MIXNODE_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const MIXNODE_BOND_MAX_RETRIEVAL_LIMIT: u32 = 100; -pub const MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT: u32 = 75; -pub const MIXNODE_DETAILS_MAX_RETRIEVAL_LIMIT: u32 = 100; +pub const NYM_NODE_BOND_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const NYM_NODE_BOND_MAX_RETRIEVAL_LIMIT: u32 = 100; -pub const UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT: u32 = 250; -pub const UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT: u32 = 300; +pub const MIXNODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const MIXNODE_DETAILS_MAX_RETRIEVAL_LIMIT: u32 = 75; +pub const NYM_NODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const NYM_NODE_DETAILS_MAX_RETRIEVAL_LIMIT: u32 = 75; -pub const DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT: u32 = 250; -pub const DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT: u32 = 300; +pub const UNBONDED_MIXNODES_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; +pub const UNBONDED_MIXNODES_MAX_RETRIEVAL_LIMIT: u32 = 200; +pub const UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; +pub const UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT: u32 = 200; -pub const EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT: u32 = 200; -pub const EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT: u32 = 250; +pub const DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT: u32 = 100; +pub const DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT: u32 = 500; -pub const INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT: u32 = 200; -pub const INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT: u32 = 250; +pub const EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT: u32 = 100; -pub const REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT: u32 = 500; -pub const REWARDED_SET_MAX_RETRIEVAL_LIMIT: u32 = 1000; - -pub const FAMILIES_DEFAULT_RETRIEVAL_LIMIT: u32 = 10; -pub const FAMILIES_MAX_RETRIEVAL_LIMIT: u32 = 20; +pub const INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT: u32 = 50; +pub const INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT: u32 = 100; // storage keys pub const DELEGATION_PK_NAMESPACE: &str = "dl"; @@ -46,7 +58,6 @@ pub const DELEGATION_MIXNODE_IDX_NAMESPACE: &str = "dlm"; pub const GATEWAYS_PK_NAMESPACE: &str = "gt"; pub const GATEWAYS_OWNER_IDX_NAMESPACE: &str = "gto"; -pub const REWARDED_SET_KEY: &str = "rs"; pub const CURRENT_EPOCH_STATUS_KEY: &str = "ces"; pub const CURRENT_INTERVAL_KEY: &str = "ci"; pub const EPOCH_EVENT_ID_COUNTER_KEY: &str = "eic"; @@ -60,7 +71,10 @@ pub const LAST_INTERVAL_EVENT_ID_KEY: &str = "lie"; pub const ADMIN_STORAGE_KEY: &str = "admin"; pub const CONTRACT_STATE_KEY: &str = "state"; -pub const LAYER_DISTRIBUTION_KEY: &str = "layers"; +pub const NYMNODE_ROLES_ASSIGNMENT_NAMESPACE: &str = "roles"; +pub const NYMNODE_REWARDED_SET_METADATA_NAMESPACE: &str = "roles_metadata"; +pub const NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY: &str = "active_roles"; + pub const NODE_ID_COUNTER_KEY: &str = "nic"; pub const PENDING_MIXNODE_CHANGES_NAMESPACE: &str = "pmc"; pub const MIXNODES_PK_NAMESPACE: &str = "mnn"; @@ -68,16 +82,26 @@ pub const MIXNODES_OWNER_IDX_NAMESPACE: &str = "mno"; pub const MIXNODES_IDENTITY_IDX_NAMESPACE: &str = "mni"; pub const MIXNODES_SPHINX_IDX_NAMESPACE: &str = "mns"; +pub const PENDING_NYMNODE_CHANGES_NAMESPACE: &str = "pnc"; +pub const NYMNODE_PK_NAMESPACE: &str = "nn"; +pub const NYMNODE_OWNER_IDX_NAMESPACE: &str = "nno"; +pub const NYMNODE_IDENTITY_IDX_NAMESPACE: &str = "nni"; + pub const UNBONDED_MIXNODES_PK_NAMESPACE: &str = "ubm"; pub const UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE: &str = "umo"; pub const UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE: &str = "umi"; +pub const UNBONDED_NYMNODE_PK_NAMESPACE: &str = "ubnn"; +pub const UNBONDED_NYMNODE_OWNER_IDX_NAMESPACE: &str = "ubno"; +pub const UNBONDED_NYMNODE_IDENTITY_IDX_NAMESPACE: &str = "ubni"; + +pub const CUMULATIVE_EPOCH_WORK_KEY: &str = "cumulative_epoch_work"; pub const REWARDING_PARAMS_KEY: &str = "rparams"; pub const PENDING_REWARD_POOL_KEY: &str = "prp"; pub const MIXNODES_REWARDING_PK_NAMESPACE: &str = "mnr"; - -pub const FAMILIES_INDEX_NAMESPACE: &str = "faml2"; -pub const FAMILIES_MAP_NAMESPACE: &str = "fam2"; -pub const MEMBERS_MAP_NAMESPACE: &str = "memb2"; +pub const NYMNODE_REWARDING_PK_NAMESPACE: &str = MIXNODES_REWARDING_PK_NAMESPACE; pub const SIGNING_NONCES_NAMESPACE: &str = "sn"; + +// temporary storage keys created for the transition period: +pub const LEGACY_GATEWAY_ID_NAMESPACE: &str = "lgidr"; diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index cda8264afa..694eb256a0 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -1,11 +1,12 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMOUNT}; +use crate::constants::INITIAL_PLEDGE_AMOUNT; use crate::interval::storage as interval_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::mixnodes::storage as mixnode_storage; -use crate::rewards::storage as rewards_storage; +use crate::nodes::storage as nymnodes_storage; +use crate::queued_migrations::migrate_to_nym_nodes_usage; +use crate::rewards::storage::RewardingStorage; use cosmwasm_std::{ entry_point, to_binary, Addr, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, }; @@ -36,14 +37,10 @@ fn default_initial_state( vesting_contract_address, rewarding_denom: rewarding_denom.clone(), params: ContractStateParams { - minimum_mixnode_delegation: None, - minimum_mixnode_pledge: Coin { + minimum_delegation: None, + minimum_pledge: Coin { denom: rewarding_denom.clone(), - amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, - }, - minimum_gateway_pledge: Coin { - denom: rewarding_denom, - amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, + amount: INITIAL_PLEDGE_AMOUNT, }, profit_margin, interval_operating_cost, @@ -93,8 +90,8 @@ pub fn instantiate( rewarding_validator_address, )?; mixnet_params_storage::initialise_storage(deps.branch(), state, info.sender)?; - mixnode_storage::initialise_storage(deps.storage)?; - rewards_storage::initialise_storage(deps.storage, reward_params)?; + RewardingStorage::new().initialise(deps.storage, reward_params)?; + nymnodes_storage::initialise_storage(deps.storage)?; cw2::set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; set_build_information!(deps.storage)?; @@ -110,29 +107,12 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { + // state/sys-params-related ExecuteMsg::UpdateAdmin { admin } => { crate::mixnet_contract_settings::transactions::try_update_contract_admin( deps, info, admin, ) } - ExecuteMsg::AssignNodeLayer { mix_id, layer } => { - crate::mixnodes::transactions::assign_mixnode_layer(deps, info, mix_id, layer) - } - // families - ExecuteMsg::CreateFamily { label } => { - crate::families::transactions::try_create_family(deps, info, label) - } - ExecuteMsg::JoinFamily { - join_permit, - family_head, - } => crate::families::transactions::try_join_family(deps, info, join_permit, family_head), - ExecuteMsg::LeaveFamily { family_head } => { - crate::families::transactions::try_leave_family(deps, info, family_head) - } - ExecuteMsg::KickFamilyMember { member } => { - crate::families::transactions::try_head_kick_member(deps, info, member) - } - // state/sys-params-related ExecuteMsg::UpdateRewardingValidatorAddress { address } => { crate::mixnet_contract_settings::transactions::try_update_rewarding_validator_address( deps, info, address, @@ -145,14 +125,14 @@ pub fn execute( updated_parameters, ) } - ExecuteMsg::UpdateActiveSetSize { - active_set_size, + ExecuteMsg::UpdateActiveSetDistribution { + update, force_immediately, - } => crate::rewards::transactions::try_update_active_set_size( + } => crate::rewards::transactions::try_update_active_set_distribution( deps, env, info, - active_set_size, + update, force_immediately, ), ExecuteMsg::UpdateRewardingParams { @@ -180,17 +160,9 @@ pub fn execute( ExecuteMsg::BeginEpochTransition {} => { crate::interval::transactions::try_begin_epoch_transition(deps, env, info) } - ExecuteMsg::AdvanceCurrentEpoch { - new_rewarded_set, - // families_in_layer, - expected_active_set_size, - } => crate::interval::transactions::try_advance_epoch( - deps, - env, - info, - new_rewarded_set, - expected_active_set_size, - ), + ExecuteMsg::AssignRoles { assignment } => { + crate::interval::transactions::try_assign_roles(deps, env, info, assignment) + } ExecuteMsg::ReconcileEpochEvents { limit } => { crate::interval::transactions::try_reconcile_epoch_events(deps, env, info, limit) } @@ -208,23 +180,15 @@ pub fn execute( cost_params, owner_signature, ), - ExecuteMsg::PledgeMore {} => { - crate::mixnodes::transactions::try_increase_pledge(deps, env, info) - } - ExecuteMsg::DecreasePledge { decrease_by } => { - crate::mixnodes::transactions::try_decrease_pledge(deps, env, info, decrease_by) - } ExecuteMsg::UnbondMixnode {} => { crate::mixnodes::transactions::try_remove_mixnode(deps, env, info) } - ExecuteMsg::UpdateMixnodeCostParams { new_costs } => { - crate::mixnodes::transactions::try_update_mixnode_cost_params( - deps, env, info, new_costs, - ) - } ExecuteMsg::UpdateMixnodeConfig { new_config } => { crate::mixnodes::transactions::try_update_mixnode_config(deps, info, new_config) } + ExecuteMsg::MigrateMixnode {} => { + crate::mixnodes::transactions::try_migrate_to_nymnode(deps, info) + } // gateway-related: ExecuteMsg::BondGateway { @@ -243,27 +207,60 @@ pub fn execute( ExecuteMsg::UpdateGatewayConfig { new_config } => { crate::gateways::transactions::try_update_gateway_config(deps, info, new_config) } + ExecuteMsg::MigrateGateway { cost_params } => { + crate::gateways::transactions::try_migrate_to_nymnode(deps, info, cost_params) + } + + // nym-node related: + ExecuteMsg::BondNymNode { + node, + cost_params, + owner_signature, + } => crate::nodes::transactions::try_add_nym_node( + deps, + env, + info, + node, + cost_params, + owner_signature, + ), + ExecuteMsg::UnbondNymNode {} => { + crate::nodes::transactions::try_remove_nym_node(deps, env, info) + } + ExecuteMsg::UpdateNodeConfig { update } => { + crate::nodes::transactions::try_update_node_config(deps, info, update) + } + + // nym-node/mixnode-related: + ExecuteMsg::PledgeMore {} => { + crate::compat::transactions::try_increase_pledge(deps, env, info) + } + ExecuteMsg::DecreasePledge { decrease_by } => { + crate::compat::transactions::try_decrease_pledge(deps, env, info, decrease_by) + } + ExecuteMsg::UpdateCostParams { new_costs } => { + crate::compat::transactions::try_update_cost_params(deps, env, info, new_costs) + } // delegation-related: - ExecuteMsg::DelegateToMixnode { mix_id } => { - crate::delegations::transactions::try_delegate_to_mixnode(deps, env, info, mix_id) + ExecuteMsg::Delegate { node_id } => { + crate::delegations::transactions::try_delegate_to_node(deps, env, info, node_id) } - ExecuteMsg::UndelegateFromMixnode { mix_id } => { - crate::delegations::transactions::try_remove_delegation_from_mixnode( - deps, env, info, mix_id, + ExecuteMsg::Undelegate { node_id } => { + crate::delegations::transactions::try_remove_delegation_from_node( + deps, env, info, node_id, ) } // reward-related - ExecuteMsg::RewardMixnode { - mix_id, - performance, - } => crate::rewards::transactions::try_reward_mixnode(deps, env, info, mix_id, performance), + ExecuteMsg::RewardNode { node_id, params } => { + crate::rewards::transactions::try_reward_node(deps, env, info, node_id, params) + } ExecuteMsg::WithdrawOperatorReward {} => { - crate::rewards::transactions::try_withdraw_operator_reward(deps, info) + crate::compat::transactions::try_withdraw_operator_reward(deps, info) } - ExecuteMsg::WithdrawDelegatorReward { mix_id } => { + ExecuteMsg::WithdrawDelegatorReward { node_id: mix_id } => { crate::rewards::transactions::try_withdraw_delegator_reward(deps, info, mix_id) } @@ -272,15 +269,11 @@ pub fn execute( crate::vesting_migration::try_migrate_vested_mixnode(deps, info) } ExecuteMsg::MigrateVestedDelegation { mix_id } => { - crate::vesting_migration::try_migrate_vested_delegation(deps, info, mix_id) + crate::vesting_migration::try_migrate_vested_delegation(deps, env, info, mix_id) } // legacy vesting - ExecuteMsg::CreateFamilyOnBehalf { .. } - | ExecuteMsg::JoinFamilyOnBehalf { .. } - | ExecuteMsg::LeaveFamilyOnBehalf { .. } - | ExecuteMsg::KickFamilyMemberOnBehalf { .. } - | ExecuteMsg::BondMixnodeOnBehalf { .. } + ExecuteMsg::BondMixnodeOnBehalf { .. } | ExecuteMsg::PledgeMoreOnBehalf { .. } | ExecuteMsg::DecreasePledgeOnBehalf { .. } | ExecuteMsg::UnbondMixnodeOnBehalf { .. } @@ -311,24 +304,6 @@ pub fn query( msg: QueryMsg, ) -> Result { let query_res = match msg { - QueryMsg::GetAllFamiliesPaged { limit, start_after } => to_binary( - &crate::families::queries::get_all_families_paged(deps.storage, start_after, limit)?, - ), - QueryMsg::GetAllMembersPaged { limit, start_after } => to_binary( - &crate::families::queries::get_all_members_paged(deps.storage, start_after, limit)?, - ), - QueryMsg::GetFamilyByHead { head } => to_binary( - &crate::families::queries::get_family_by_head(&head, deps.storage)?, - ), - QueryMsg::GetFamilyByLabel { label } => to_binary( - &crate::families::queries::get_family_by_label(label, deps.storage)?, - ), - QueryMsg::GetFamilyMembersByHead { head } => to_binary( - &crate::families::queries::get_family_members_by_head(&head, deps.storage)?, - ), - QueryMsg::GetFamilyMembersByLabel { label } => to_binary( - &crate::families::queries::get_family_members_by_label(label, deps.storage)?, - ), QueryMsg::GetContractVersion {} => { to_binary(&crate::mixnet_contract_settings::queries::query_contract_version()) } @@ -354,9 +329,6 @@ pub fn query( QueryMsg::GetCurrentIntervalDetails {} => to_binary( &crate::interval::queries::query_current_interval_details(deps, env)?, ), - QueryMsg::GetRewardedSet { limit, start_after } => to_binary( - &crate::interval::queries::query_rewarded_set_paged(deps, start_after, limit)?, - ), // mixnode-related: QueryMsg::GetMixNodeBonds { start_after, limit } => to_binary( @@ -410,9 +382,6 @@ pub fn query( QueryMsg::GetBondedMixnodeDetailsByIdentity { mix_identity } => to_binary( &crate::mixnodes::queries::query_mixnode_details_by_identity(deps, mix_identity)?, ), - QueryMsg::GetLayerDistribution {} => { - to_binary(&crate::mixnodes::queries::query_layer_distribution(deps)?) - } // gateway-related: QueryMsg::GetGateways { limit, start_after } => to_binary( @@ -424,20 +393,80 @@ pub fn query( QueryMsg::GetOwnedGateway { address } => to_binary( &crate::gateways::queries::query_owned_gateway(deps, address)?, ), + QueryMsg::GetPreassignedGatewayIds { limit, start_after } => to_binary( + &crate::gateways::queries::query_preassigned_ids_paged(deps, start_after, limit)?, + ), - // delegation-related: - QueryMsg::GetMixnodeDelegations { - mix_id, - start_after, + // nym-node-related: + QueryMsg::GetNymNodeBondsPaged { start_after, limit } => to_binary( + &crate::nodes::queries::query_nymnode_bonds_paged(deps, start_after, limit)?, + ), + QueryMsg::GetNymNodesDetailedPaged { limit, start_after } => to_binary( + &crate::nodes::queries::query_nymnodes_details_paged(deps, start_after, limit)?, + ), + QueryMsg::GetUnbondedNymNode { node_id } => to_binary( + &crate::nodes::queries::query_unbonded_nymnode(deps, node_id)?, + ), + QueryMsg::GetUnbondedNymNodesPaged { limit, start_after } => to_binary( + &crate::nodes::queries::query_unbonded_nymnodes_paged(deps, limit, start_after)?, + ), + QueryMsg::GetUnbondedNymNodesByOwnerPaged { + owner, limit, + start_after, } => to_binary( - &crate::delegations::queries::query_mixnode_delegations_paged( + &crate::nodes::queries::query_unbonded_nymnodes_by_owner_paged( deps, - mix_id, + owner, + limit, start_after, + )?, + ), + QueryMsg::GetUnbondedNymNodesByIdentityKeyPaged { + identity_key, + limit, + start_after, + } => to_binary( + &crate::nodes::queries::query_unbonded_nymnodes_by_identity_paged( + deps, + identity_key, limit, + start_after, )?, ), + QueryMsg::GetOwnedNymNode { address } => { + to_binary(&crate::nodes::queries::query_owned_nymnode(deps, address)?) + } + QueryMsg::GetNymNodeDetails { node_id } => to_binary( + &crate::nodes::queries::query_nymnode_details(deps, node_id)?, + ), + QueryMsg::GetNymNodeDetailsByIdentityKey { node_identity } => to_binary( + &crate::nodes::queries::query_nymnode_details_by_identity(deps, node_identity)?, + ), + QueryMsg::GetNodeRewardingDetails { node_id } => to_binary( + &crate::nodes::queries::query_nymnode_rewarding_details(deps, node_id)?, + ), + QueryMsg::GetNodeStakeSaturation { node_id } => to_binary( + &crate::nodes::queries::query_stake_saturation(deps, node_id)?, + ), + QueryMsg::GetRoleAssignment { role } => { + to_binary(&crate::nodes::queries::query_epoch_assignment(deps, role)?) + } + QueryMsg::GetRewardedSetMetadata {} => { + to_binary(&crate::nodes::queries::query_rewarded_set_metadata(deps)?) + } + + // delegation-related: + QueryMsg::GetNodeDelegations { + node_id, + start_after, + limit, + } => to_binary(&crate::delegations::queries::query_node_delegations_paged( + deps, + node_id, + start_after, + limit, + )?), QueryMsg::GetDelegatorDelegations { delegator, start_after, @@ -451,11 +480,11 @@ pub fn query( )?, ), QueryMsg::GetDelegationDetails { - mix_id, + node_id, delegator, proxy, - } => to_binary(&crate::delegations::queries::query_mixnode_delegation( - deps, mix_id, delegator, proxy, + } => to_binary(&crate::delegations::queries::query_node_delegation( + deps, node_id, delegator, proxy, )?), QueryMsg::GetAllDelegations { start_after, limit } => to_binary( &crate::delegations::queries::query_all_delegations_paged(deps, start_after, limit)?, @@ -465,38 +494,40 @@ pub fn query( QueryMsg::GetPendingOperatorReward { address } => to_binary( &crate::rewards::queries::query_pending_operator_reward(deps, address)?, ), - QueryMsg::GetPendingMixNodeOperatorReward { mix_id } => to_binary( - &crate::rewards::queries::query_pending_mixnode_operator_reward(deps, mix_id)?, + QueryMsg::GetPendingNodeOperatorReward { node_id } => to_binary( + &crate::rewards::queries::query_pending_mixnode_operator_reward(deps, node_id)?, ), QueryMsg::GetPendingDelegatorReward { address, - mix_id, + node_id, proxy, } => to_binary(&crate::rewards::queries::query_pending_delegator_reward( - deps, address, mix_id, proxy, + deps, address, node_id, proxy, )?), QueryMsg::GetEstimatedCurrentEpochOperatorReward { - mix_id, + node_id, estimated_performance, + estimated_work, } => to_binary( &crate::rewards::queries::query_estimated_current_epoch_operator_reward( deps, - mix_id, + node_id, estimated_performance, + estimated_work, )?, ), QueryMsg::GetEstimatedCurrentEpochDelegatorReward { address, - mix_id, - proxy, + node_id, estimated_performance, + estimated_work, } => to_binary( &crate::rewards::queries::query_estimated_current_epoch_delegator_reward( deps, address, - mix_id, - proxy, + node_id, estimated_performance, + estimated_work, )?, ), @@ -538,13 +569,23 @@ pub fn query( #[entry_point] pub fn migrate( - deps: DepsMut<'_>, + mut deps: DepsMut<'_>, _env: Env, msg: MigrateMsg, ) -> Result { set_build_information!(deps.storage)?; cw2::ensure_from_older_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + let skip_state_updates = msg.unsafe_skip_state_updates.unwrap_or(false); + + if !skip_state_updates { + // remove all family-related things + crate::queued_migrations::families_purge(deps.branch())?; + + // prepare the ground for using nym-nodes rather than standalone mixnodes/gateways + migrate_to_nym_nodes_usage(deps.branch(), &msg)?; + } + // due to circular dependency on contract addresses (i.e. mixnet contract requiring vesting contract address // and vesting contract requiring the mixnet contract address), if we ever want to deploy any new fresh // environment, one of the contracts will HAVE TO go through a migration @@ -561,9 +602,12 @@ pub fn migrate( #[cfg(test)] mod tests { use super::*; + use crate::rewards::storage as rewards_storage; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{Decimal, Uint128}; - use mixnet_contract_common::reward_params::{IntervalRewardParams, RewardingParams}; + use mixnet_contract_common::reward_params::{ + IntervalRewardParams, RewardedSetParams, RewardingParams, + }; use mixnet_contract_common::{InitialRewardingParams, Percent}; use std::time::Duration; @@ -585,8 +629,12 @@ mod tests { sybil_resistance: Percent::from_percentage_value(23).unwrap(), active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), interval_pool_emission: Percent::from_percentage_value(1).unwrap(), - rewarded_set_size: 543, - active_set_size: 123, + rewarded_set_params: RewardedSetParams { + entry_gateways: 123, + exit_gateways: 70, + mixnodes: 120, + standby: 0, + }, }, profit_margin: ProfitMarginRange { minimum: "0.05".parse().unwrap(), @@ -609,14 +657,10 @@ mod tests { vesting_contract_address: Addr::unchecked("bar456"), rewarding_denom: "uatom".into(), params: ContractStateParams { - minimum_mixnode_delegation: None, - minimum_mixnode_pledge: Coin { + minimum_delegation: None, + minimum_pledge: Coin { denom: "uatom".into(), - amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, - }, - minimum_gateway_pledge: Coin { - denom: "uatom".into(), - amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, + amount: INITIAL_PLEDGE_AMOUNT, }, profit_margin: ProfitMarginRange { minimum: Percent::from_percentage_value(5).unwrap(), @@ -631,7 +675,7 @@ mod tests { let expected_epoch_reward_budget = Decimal::from_ratio(100_000_000_000_000u128, 1234u32) * Decimal::percent(1); - let expected_stake_saturation_point = Decimal::from_ratio(123_456_000_000_000u128, 543u32); + let expected_stake_saturation_point = Decimal::from_ratio(123_456_000_000_000u128, 313u32); let expected_rewarding_params = RewardingParams { interval: IntervalRewardParams { @@ -644,8 +688,12 @@ mod tests { active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(), interval_pool_emission: Percent::from_percentage_value(1).unwrap(), }, - rewarded_set_size: 543, - active_set_size: 123, + rewarded_set: RewardedSetParams { + entry_gateways: 123, + exit_gateways: 70, + mixnodes: 120, + standby: 0, + }, }; let state = mixnet_params_storage::CONTRACT_STATE diff --git a/contracts/mixnet/src/delegations/helpers.rs b/contracts/mixnet/src/delegations/helpers.rs index 3991711a06..20ae4e8582 100644 --- a/contracts/mixnet/src/delegations/helpers.rs +++ b/contracts/mixnet/src/delegations/helpers.rs @@ -5,17 +5,17 @@ use crate::delegations::storage; use crate::rewards::storage as rewards_storage; use cosmwasm_std::{Coin, Storage}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::mixnode::MixNodeRewarding; +use mixnet_contract_common::mixnode::NodeRewarding; use mixnet_contract_common::Delegation; pub(crate) fn undelegate( store: &mut dyn Storage, delegation: Delegation, - mut mix_rewarding: MixNodeRewarding, + mut mix_rewarding: NodeRewarding, ) -> Result { let tokens = mix_rewarding.undelegate(&delegation)?; - rewards_storage::MIXNODE_REWARDING.save(store, delegation.mix_id, &mix_rewarding)?; + rewards_storage::MIXNODE_REWARDING.save(store, delegation.node_id, &mix_rewarding)?; storage::delegations().replace(store, delegation.storage_key(), None, Some(&delegation))?; Ok(tokens) @@ -24,24 +24,26 @@ pub(crate) fn undelegate( #[cfg(test)] mod tests { use super::*; - use crate::support::tests::test_helpers::{performance, TestSetup}; + use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::{Addr, Decimal, Uint128}; use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; #[test] fn undelegation_updates_mix_rewarding_storage_and_deletes_delegation() { let mut test = TestSetup::new(); + let active_params = test.active_node_params(100.0); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(100_000_000_000))); + let mix_id = + test.add_rewarded_set_nymnode_id("mix-owner", Some(Uint128::new(100_000_000_000))); let delegator = "delegator"; let og_amount = Uint128::new(200_000_000); test.add_immediate_delegation(delegator, og_amount, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - let dist1 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + test.force_change_mix_rewarded_set(vec![mix_id]); + let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist2 = test.reward_with_distribution_ignore_state(mix_id, active_params); let mix_rewarding = test.mix_rewarding(mix_id); let delegation = test.delegation(mix_id, delegator, &None); diff --git a/contracts/mixnet/src/delegations/queries.rs b/contracts/mixnet/src/delegations/queries.rs index 01e7b65648..fb46e8056f 100644 --- a/contracts/mixnet/src/delegations/queries.rs +++ b/contracts/mixnet/src/delegations/queries.rs @@ -2,38 +2,40 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; +use crate::compat; use crate::constants::{ DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT, DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT, }; -use crate::mixnodes::storage as mixnodes_storage; use cosmwasm_std::Deps; use cosmwasm_std::Order; use cosmwasm_std::StdResult; use cw_storage_plus::Bound; -use mixnet_contract_common::delegation::{MixNodeDelegationResponse, OwnerProxySubKey}; +use mixnet_contract_common::delegation::{NodeDelegationResponse, OwnerProxySubKey}; use mixnet_contract_common::{ - delegation, Delegation, MixId, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, - PagedMixNodeDelegationsResponse, + delegation, Delegation, NodeId, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse, + PagedNodeDelegationsResponse, }; -pub(crate) fn query_mixnode_delegations_paged( +pub(crate) fn query_node_delegations_paged( deps: Deps<'_>, - mix_id: MixId, + node_id: NodeId, start_after: Option, limit: Option, -) -> StdResult { +) -> StdResult { let limit = limit .unwrap_or(DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT) .min(DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT) as usize; let start = start_after.map(|subkey| { - Bound::exclusive(Delegation::generate_storage_key_with_subkey(mix_id, subkey)) + Bound::exclusive(Delegation::generate_storage_key_with_subkey( + node_id, subkey, + )) }); let delegations = storage::delegations() .idx .mixnode - .prefix(mix_id) + .prefix(node_id) .range(deps.storage, start, None, Order::Ascending) .take(limit) .map(|record| record.map(|r| r.1)) @@ -41,7 +43,7 @@ pub(crate) fn query_mixnode_delegations_paged( let start_next_after = delegations.last().map(|del| del.proxy_storage_key()); - Ok(PagedMixNodeDelegationsResponse::new( + Ok(PagedNodeDelegationsResponse::new( delegations, start_next_after, )) @@ -50,7 +52,7 @@ pub(crate) fn query_mixnode_delegations_paged( pub(crate) fn query_delegator_delegations_paged( deps: Deps<'_>, delegation_owner: String, - start_after: Option<(MixId, OwnerProxySubKey)>, + start_after: Option<(NodeId, OwnerProxySubKey)>, limit: Option, ) -> StdResult { let validated_owner = deps.api.addr_validate(&delegation_owner)?; @@ -74,7 +76,7 @@ pub(crate) fn query_delegator_delegations_paged( let start_next_after = delegations .last() - .map(|del| (del.mix_id, del.proxy_storage_key())); + .map(|del| (del.node_id, del.proxy_storage_key())); Ok(PagedDelegatorDelegationsResponse::new( delegations, @@ -83,30 +85,26 @@ pub(crate) fn query_delegator_delegations_paged( } // queries for delegation value of given address for particular node -pub(crate) fn query_mixnode_delegation( +pub(crate) fn query_node_delegation( deps: Deps<'_>, - mix_id: MixId, + node_id: NodeId, delegation_owner: String, proxy: Option, -) -> StdResult { +) -> StdResult { let validated_owner = deps.api.addr_validate(&delegation_owner)?; let validated_proxy = proxy .map(|proxy| deps.api.addr_validate(&proxy)) .transpose()?; let storage_key = - Delegation::generate_storage_key(mix_id, &validated_owner, validated_proxy.as_ref()); + Delegation::generate_storage_key(node_id, &validated_owner, validated_proxy.as_ref()); let delegation = storage::delegations().may_load(deps.storage, storage_key)?; - let mixnode_still_bonded = mixnodes_storage::mixnode_bonds() - .may_load(deps.storage, mix_id)? - .map(|bond| !bond.is_unbonding) + let node_still_bonded = compat::helpers::may_get_bond(deps.storage, node_id)? + .map(|bond| !bond.is_unbonding()) .unwrap_or_default(); - Ok(MixNodeDelegationResponse::new( - delegation, - mixnode_still_bonded, - )) + Ok(NodeDelegationResponse::new(delegation, node_still_bonded)) } pub(crate) fn query_all_delegations_paged( @@ -141,7 +139,7 @@ mod tests { fn add_dummy_mixes_with_delegations(test: &mut TestSetup, delegators: usize, mixes: usize) { for i in 0..mixes { - let mix_id = test.add_dummy_mixnode(&format!("mix-owner{}", i), None); + let mix_id = test.add_legacy_mixnode(&format!("mix-owner{}", i), None); for delegator in 0..delegators { let name = &format!("delegator{}", delegator); test.add_immediate_delegation(name, 100_000_000u32, mix_id) @@ -157,7 +155,7 @@ mod tests { #[test] fn obeys_limits() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let env = test.env(); test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id, 200); @@ -165,20 +163,20 @@ mod tests { let limit = 2; let page1 = - query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(limit)).unwrap(); + query_node_delegations_paged(test.deps(), mix_id, None, Some(limit)).unwrap(); assert_eq!(limit, page1.delegations.len() as u32); } #[test] fn has_default_limit() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let env = test.env(); test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id, 500); // query without explicitly setting a limit - let page1 = query_mixnode_delegations_paged(test.deps(), mix_id, None, None).unwrap(); + let page1 = query_node_delegations_paged(test.deps(), mix_id, None, None).unwrap(); assert_eq!( DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT, @@ -189,7 +187,7 @@ mod tests { #[test] fn has_max_limit() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let env = test.env(); test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id, 5000); @@ -197,8 +195,7 @@ mod tests { // query with a crazily high limit in an attempt to use too many resources let crazy_limit = 10000; let page1 = - query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(crazy_limit)) - .unwrap(); + query_node_delegations_paged(test.deps(), mix_id, None, Some(crazy_limit)).unwrap(); assert_eq!( DELEGATION_PAGE_MAX_RETRIEVAL_LIMIT, @@ -210,12 +207,12 @@ mod tests { fn pagination_works() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); test.add_immediate_delegation("addr1", 1000u32, mix_id); let per_page = 2; let page1 = - query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); + query_node_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); // page should have 1 result on it assert_eq!(1, page1.delegations.len()); @@ -225,20 +222,20 @@ mod tests { // page1 should have 2 results on it let page1 = - query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); + query_node_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); assert_eq!(2, page1.delegations.len()); test.add_immediate_delegation("addr3", 1000u32, mix_id); // page1 still has the same 2 results let another_page1 = - query_mixnode_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); + query_node_delegations_paged(test.deps(), mix_id, None, Some(per_page)).unwrap(); assert_eq!(2, another_page1.delegations.len()); assert_eq!(page1, another_page1); // retrieving the next page should start after the last key on this page let start_after = page1.start_next_after.unwrap(); - let page2 = query_mixnode_delegations_paged( + let page2 = query_node_delegations_paged( test.deps(), mix_id, Some(start_after.clone()), @@ -251,7 +248,7 @@ mod tests { // save another one test.add_immediate_delegation("addr4", 1000u32, mix_id); - let page2 = query_mixnode_delegations_paged( + let page2 = query_node_delegations_paged( test.deps(), mix_id, Some(start_after), @@ -266,10 +263,10 @@ mod tests { #[test] fn all_retrieved_delegations_are_towards_specified_mixnode() { let mut test = TestSetup::new(); - let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); - let mix_id3 = test.add_dummy_mixnode("mix-owner3", None); - let mix_id4 = test.add_dummy_mixnode("mix-owner4", None); + let mix_id1 = test.add_legacy_mixnode("mix-owner1", None); + let mix_id2 = test.add_legacy_mixnode("mix-owner2", None); + let mix_id3 = test.add_legacy_mixnode("mix-owner3", None); + let mix_id4 = test.add_legacy_mixnode("mix-owner4", None); let env = test.env(); // add other "out of order" delegations manually @@ -282,21 +279,21 @@ mod tests { test_helpers::add_dummy_delegations(test.deps_mut(), env, mix_id4, 10); test.add_immediate_delegation("random-delegator4", 1000u32, mix_id2); - let res1 = query_mixnode_delegations_paged(test.deps(), mix_id1, None, None).unwrap(); + let res1 = query_node_delegations_paged(test.deps(), mix_id1, None, None).unwrap(); assert_eq!(res1.delegations.len(), 10); - assert!(res1.delegations.into_iter().all(|d| d.mix_id == mix_id1)); + assert!(res1.delegations.into_iter().all(|d| d.node_id == mix_id1)); - let res2 = query_mixnode_delegations_paged(test.deps(), mix_id2, None, None).unwrap(); + let res2 = query_node_delegations_paged(test.deps(), mix_id2, None, None).unwrap(); assert_eq!(res2.delegations.len(), 14); - assert!(res2.delegations.into_iter().all(|d| d.mix_id == mix_id2)); + assert!(res2.delegations.into_iter().all(|d| d.node_id == mix_id2)); - let res3 = query_mixnode_delegations_paged(test.deps(), mix_id3, None, None).unwrap(); + let res3 = query_node_delegations_paged(test.deps(), mix_id3, None, None).unwrap(); assert_eq!(res3.delegations.len(), 10); - assert!(res3.delegations.into_iter().all(|d| d.mix_id == mix_id3)); + assert!(res3.delegations.into_iter().all(|d| d.node_id == mix_id3)); - let res4 = query_mixnode_delegations_paged(test.deps(), mix_id4, None, None).unwrap(); + let res4 = query_node_delegations_paged(test.deps(), mix_id4, None, None).unwrap(); assert_eq!(res4.delegations.len(), 10); - assert!(res4.delegations.into_iter().all(|d| d.mix_id == mix_id4)); + assert!(res4.delegations.into_iter().all(|d| d.node_id == mix_id4)); } } @@ -365,11 +362,11 @@ mod tests { let mut test = TestSetup::new(); // note that mix_ids are monotonically increasing - let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); - let mix_id3 = test.add_dummy_mixnode("mix-owner3", None); - let mix_id4 = test.add_dummy_mixnode("mix-owner4", None); - let mix_id5 = test.add_dummy_mixnode("mix-owner5", None); + let mix_id1 = test.add_legacy_mixnode("mix-owner1", None); + let mix_id2 = test.add_legacy_mixnode("mix-owner2", None); + let mix_id3 = test.add_legacy_mixnode("mix-owner3", None); + let mix_id4 = test.add_legacy_mixnode("mix-owner4", None); + let mix_id5 = test.add_legacy_mixnode("mix-owner5", None); // add few delegations from unrelated delegators for mix_id in [mix_id1, mix_id2, mix_id3, mix_id4, mix_id5] { @@ -526,8 +523,8 @@ mod tests { // note that mix_ids are monotonically increasing and are the first chunk of all // delegation storage keys, - let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); + let mix_id1 = test.add_legacy_mixnode("mix-owner1", None); + let mix_id2 = test.add_legacy_mixnode("mix-owner2", None); let delegator1 = "delegator1"; let delegator2 = "delegator2"; @@ -541,7 +538,7 @@ mod tests { assert_eq!(1, page1.delegations.len()); assert!( page1.delegations[0].owner.as_str() == delegator1 - && page1.delegations[0].mix_id == mix_id1 + && page1.delegations[0].node_id == mix_id1 ); test.add_immediate_delegation(delegator1, 1000u32, mix_id2); @@ -552,11 +549,11 @@ mod tests { assert_eq!(2, page1.delegations.len()); assert!( page1.delegations[0].owner.as_str() == delegator1 - && page1.delegations[0].mix_id == mix_id1 + && page1.delegations[0].node_id == mix_id1 ); assert!( page1.delegations[1].owner.as_str() == delegator1 - && page1.delegations[1].mix_id == mix_id2 + && page1.delegations[1].node_id == mix_id2 ); test.add_immediate_delegation(delegator2, 1000u32, mix_id1); @@ -567,11 +564,11 @@ mod tests { assert_eq!(2, another_page1.delegations.len()); assert!( another_page1.delegations[0].owner.as_str() == delegator1 - && another_page1.delegations[0].mix_id == mix_id1 + && another_page1.delegations[0].node_id == mix_id1 ); assert!( another_page1.delegations[1].owner.as_str() == delegator2 - && another_page1.delegations[1].mix_id == mix_id1 + && another_page1.delegations[1].node_id == mix_id1 ); // retrieving the next page should start after the last key on this page @@ -583,7 +580,7 @@ mod tests { assert_eq!(1, page2.delegations.len()); assert!( page2.delegations[0].owner.as_str() == delegator1 - && page2.delegations[0].mix_id == mix_id2 + && page2.delegations[0].node_id == mix_id2 ); // save another one @@ -596,72 +593,145 @@ mod tests { assert_eq!(2, page2.delegations.len()); assert!( page2.delegations[0].owner.as_str() == delegator1 - && page2.delegations[0].mix_id == mix_id2 + && page2.delegations[0].node_id == mix_id2 ); assert!( page2.delegations[1].owner.as_str() == delegator2 - && page2.delegations[1].mix_id == mix_id2 + && page2.delegations[1].node_id == mix_id2 ); } } #[cfg(test)] - mod querying_for_specific_mixnode_delegation { + mod querying_for_specific_node_delegation { use super::*; - #[test] - fn when_delegation_doesnt_exist() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let owner = "owner"; + #[cfg(test)] + mod legacy_mixnodes { + use super::*; + + #[allow(deprecated)] + #[test] + fn when_delegation_doesnt_exist() { + let mut test = TestSetup::new(); + let mix_id = test.add_legacy_mixnode("mix-owner", None); + let owner = "owner"; + + let res = query_node_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert!(res.delegation.is_none()); + assert!(res.mixnode_still_bonded); + assert!(res.node_still_bonded); + } - let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); - assert!(res.delegation.is_none()); - assert!(res.mixnode_still_bonded); - } + #[allow(deprecated)] + #[test] + fn when_delegation_exists_but_mixnode_has_unbonded() { + let mut test = TestSetup::new(); + let mix_id = test.add_legacy_mixnode("mix-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, mix_id); + test.immediately_unbond_mixnode(mix_id); + + let res = query_node_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(!res.mixnode_still_bonded); + assert!(!res.node_still_bonded); + } - #[test] - fn when_delegation_exists_but_mixnode_has_unbonded() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let owner = "owner"; + #[allow(deprecated)] + #[test] + fn when_delegation_exists_but_mixnode_is_unbonding() { + let mut test = TestSetup::new(); + let mix_id = test.add_legacy_mixnode("mix-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, mix_id); + test.start_unbonding_mixnode(mix_id); + + let res = query_node_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(!res.mixnode_still_bonded); + assert!(!res.node_still_bonded); + } - test.add_immediate_delegation(owner, 1000u32, mix_id); - test.immediately_unbond_mixnode(mix_id); + #[allow(deprecated)] + #[test] + fn when_delegation_exists_with_fully_bonded_node() { + let mut test = TestSetup::new(); + let mix_id = test.add_legacy_mixnode("mix-owner", None); + let owner = "owner"; - let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); - assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); - assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); - assert!(!res.mixnode_still_bonded); + test.add_immediate_delegation(owner, 1000u32, mix_id); + + let res = query_node_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(res.mixnode_still_bonded); + assert!(res.node_still_bonded); + } } - #[test] - fn when_delegation_exists_but_mixnode_is_unbonding() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let owner = "owner"; + #[cfg(test)] + mod nym_nodes { + use super::*; - test.add_immediate_delegation(owner, 1000u32, mix_id); - test.start_unbonding_mixnode(mix_id); + #[test] + fn when_delegation_doesnt_exist() { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("bond-owner", None); + let owner = "owner"; - let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); - assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); - assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); - assert!(!res.mixnode_still_bonded); - } + let res = query_node_delegation(test.deps(), node_id, owner.into(), None).unwrap(); + assert!(res.delegation.is_none()); + assert!(res.node_still_bonded); + } - #[test] - fn when_delegation_exists_with_fully_bonded_node() { - let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let owner = "owner"; + #[test] + fn when_delegation_exists_but_mixnode_has_unbonded() { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("bond-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, node_id); + test.immediately_unbond_nymnode(node_id); + + let res = query_node_delegation(test.deps(), node_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(!res.node_still_bonded); + } + + #[test] + fn when_delegation_exists_but_mixnode_is_unbonding() { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("bond-owner", None); + let owner = "owner"; - test.add_immediate_delegation(owner, 1000u32, mix_id); + test.add_immediate_delegation(owner, 1000u32, node_id); + test.start_unbonding_nymnode(node_id); + + let res = query_node_delegation(test.deps(), node_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(!res.node_still_bonded); + } - let res = query_mixnode_delegation(test.deps(), mix_id, owner.into(), None).unwrap(); - assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); - assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); - assert!(res.mixnode_still_bonded); + #[test] + fn when_delegation_exists_with_fully_bonded_node() { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("bond-owner", None); + let owner = "owner"; + + test.add_immediate_delegation(owner, 1000u32, node_id); + + let res = query_node_delegation(test.deps(), node_id, owner.into(), None).unwrap(); + assert_eq!(res.delegation.as_ref().unwrap().owner.as_str(), owner); + assert_eq!(res.delegation.as_ref().unwrap().amount.amount.u128(), 1000); + assert!(res.node_still_bonded); + } } } } diff --git a/contracts/mixnet/src/delegations/storage.rs b/contracts/mixnet/src/delegations/storage.rs index 5d3e6b45f2..25cd6844c9 100644 --- a/contracts/mixnet/src/delegations/storage.rs +++ b/contracts/mixnet/src/delegations/storage.rs @@ -6,18 +6,18 @@ use crate::constants::{ }; use cw_storage_plus::{Index, IndexList, IndexedMap, MultiIndex}; use mixnet_contract_common::delegation::OwnerProxySubKey; -use mixnet_contract_common::{Addr, Delegation, MixId}; +use mixnet_contract_common::{Addr, Delegation, NodeId}; // It's a composite key on node's id and delegator address -type PrimaryKey = (MixId, OwnerProxySubKey); +type PrimaryKey = (NodeId, OwnerProxySubKey); pub(crate) struct DelegationIndex<'a> { pub(crate) owner: MultiIndex<'a, Addr, Delegation, PrimaryKey>, - pub(crate) mixnode: MultiIndex<'a, MixId, Delegation, PrimaryKey>, + pub(crate) mixnode: MultiIndex<'a, NodeId, Delegation, PrimaryKey>, } -impl<'a> IndexList for DelegationIndex<'a> { +impl IndexList for DelegationIndex<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.owner, &self.mixnode]; Box::new(v.into_iter()) @@ -32,7 +32,7 @@ pub(crate) fn delegations<'a>() -> IndexedMap<'a, PrimaryKey, Delegation, Delega DELEGATION_OWNER_IDX_NAMESPACE, ), mixnode: MultiIndex::new( - |_pk, d| d.mix_id, + |_pk, d| d.node_id, DELEGATION_PK_NAMESPACE, DELEGATION_MIXNODE_IDX_NAMESPACE, ), diff --git a/contracts/mixnet/src/delegations/transactions.rs b/contracts/mixnet/src/delegations/transactions.rs index b91c6c8582..714afe33f0 100644 --- a/contracts/mixnet/src/delegations/transactions.rs +++ b/contracts/mixnet/src/delegations/transactions.rs @@ -4,21 +4,22 @@ use super::storage; use crate::interval::storage as interval_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::mixnodes::storage as mixnodes_storage; -use crate::support::helpers::{ensure_epoch_in_progress_state, validate_delegation_stake}; +use crate::support::helpers::{ + ensure_any_node_bonded, ensure_epoch_in_progress_state, validate_delegation_stake, +}; use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_pending_delegation_event, new_pending_undelegation_event, }; use mixnet_contract_common::pending_events::PendingEpochEventKind; -use mixnet_contract_common::{Delegation, MixId}; +use mixnet_contract_common::{Delegation, NodeId}; -pub(crate) fn try_delegate_to_mixnode( +pub(crate) fn try_delegate_to_node( deps: DepsMut<'_>, env: Env, info: MessageInfo, - mix_id: MixId, + mix_id: NodeId, ) -> Result { // delegation is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; @@ -27,18 +28,12 @@ pub(crate) fn try_delegate_to_mixnode( let contract_state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?; let delegation = validate_delegation_stake( info.funds, - contract_state.params.minimum_mixnode_delegation, + contract_state.params.minimum_delegation, contract_state.rewarding_denom, )?; // check if the target node actually exists and is still bonded - match mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)? { - None => return Err(MixnetContractError::MixNodeBondNotFound { mix_id }), - Some(bond) if bond.is_unbonding => { - return Err(MixnetContractError::MixnodeIsUnbonding { mix_id }) - } - _ => (), - } + ensure_any_node_bonded(deps.storage, mix_id)?; // push the event onto the queue and wait for it to be picked up at the end of the epoch let cosmos_event = new_pending_delegation_event(&info.sender, &delegation, mix_id); @@ -49,33 +44,33 @@ pub(crate) fn try_delegate_to_mixnode( Ok(Response::new().add_event(cosmos_event)) } -pub(crate) fn try_remove_delegation_from_mixnode( +pub(crate) fn try_remove_delegation_from_node( deps: DepsMut<'_>, env: Env, info: MessageInfo, - mix_id: MixId, + node_id: NodeId, ) -> Result { // undelegation is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; // see if the delegation even exists - let storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None); + let storage_key = Delegation::generate_storage_key(node_id, &info.sender, None); if storage::delegations() .may_load(deps.storage, storage_key)? .is_none() { - return Err(MixnetContractError::NoMixnodeDelegationFound { - mix_id, + return Err(MixnetContractError::NodeDelegationNotFound { + node_id, address: info.sender.into_string(), proxy: None, }); } // push the event onto the queue and wait for it to be picked up at the end of the epoch - let cosmos_event = new_pending_undelegation_event(&info.sender, mix_id); + let cosmos_event = new_pending_undelegation_event(&info.sender, node_id); - let epoch_event = PendingEpochEventKind::new_undelegate(info.sender, mix_id); + let epoch_event = PendingEpochEventKind::new_undelegate(info.sender, node_id); interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; Ok(Response::new().add_event(cosmos_event)) @@ -94,6 +89,7 @@ mod tests { use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::testing::mock_info; use cosmwasm_std::{coin, Addr, Decimal}; + use mixnet_contract_common::nym_node::Role; use mixnet_contract_common::{EpochState, EpochStatus}; #[test] @@ -104,7 +100,9 @@ mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -118,10 +116,10 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let sender = mock_info(owner, &[coin(50_000_000, TEST_COIN_DENOM)]); - let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env.clone(), sender, mix_id); assert!(matches!( res, Err(MixnetContractError::EpochAdvancementInProgress { .. }) @@ -136,10 +134,10 @@ mod tests { let owner = "delegator"; let sender = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); - let res = try_delegate_to_mixnode(test.deps_mut(), env, sender, 42); + let res = try_delegate_to_node(test.deps_mut(), env, sender, 42); assert_eq!( res, - Err(MixnetContractError::MixNodeBondNotFound { mix_id: 42 }) + Err(MixnetContractError::NymNodeBondNotFound { node_id: 42 }) ) } @@ -149,16 +147,16 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let sender1 = mock_info(owner, &[coin(0, TEST_COIN_DENOM)]); let sender2 = mock_info(owner, &[]); let sender3 = mock_info(owner, &[coin(1000, "some-weird-coin")]); - let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env.clone(), sender1, mix_id); assert_eq!(res, Err(MixnetContractError::EmptyDelegation)); - let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender2, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env.clone(), sender2, mix_id); assert_eq!(res, Err(MixnetContractError::EmptyDelegation)); - let res = try_delegate_to_mixnode(test.deps_mut(), env, sender3, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env, sender3, mix_id); assert_eq!( res, Err(MixnetContractError::WrongDenom { @@ -174,7 +172,7 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); let sender2 = mock_info(owner, &[coin(150_000_000, TEST_COIN_DENOM)]); @@ -182,12 +180,12 @@ mod tests { let mut contract_state = mixnet_params_storage::CONTRACT_STATE .load(test.deps().storage) .unwrap(); - contract_state.params.minimum_mixnode_delegation = Some(min_delegation.clone()); + contract_state.params.minimum_delegation = Some(min_delegation.clone()); mixnet_params_storage::CONTRACT_STATE .save(test.deps_mut().storage, &contract_state) .unwrap(); - let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env.clone(), sender1, mix_id); assert_eq!( res, Err(MixnetContractError::InsufficientDelegation { @@ -196,7 +194,7 @@ mod tests { }) ); - let res = try_delegate_to_mixnode(test.deps_mut(), env, sender2, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env, sender2, mix_id); assert!(res.is_ok()) } @@ -207,10 +205,10 @@ mod tests { let owner = "delegator"; let sender = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); - let mix_id_unbonding = test.add_dummy_mixnode("mix-owner-unbonding", None); - let mix_id_unbonded = test.add_dummy_mixnode("mix-owner-unbonded", None); + let mix_id_unbonding = test.add_legacy_mixnode("mix-owner-unbonding", None); + let mix_id_unbonded = test.add_legacy_mixnode("mix-owner-unbonded", None); let mix_id_unbonded_leftover = - test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); + test.add_legacy_mixnode("mix-owner-unbonded-leftover", None); // manually adjust delegation info as to indicate the rewarding information shouldnt get removed let mut rewarding_details = rewards_storage::MIXNODE_REWARDING @@ -247,7 +245,7 @@ mod tests { ) .unwrap(); - let res = try_delegate_to_mixnode( + let res = try_delegate_to_node( test.deps_mut(), env.clone(), sender.clone(), @@ -260,7 +258,7 @@ mod tests { }) ); - let res = try_delegate_to_mixnode( + let res = try_delegate_to_node( test.deps_mut(), env.clone(), sender.clone(), @@ -268,17 +266,16 @@ mod tests { ); assert_eq!( res, - Err(MixnetContractError::MixNodeBondNotFound { - mix_id: mix_id_unbonded + Err(MixnetContractError::NymNodeBondNotFound { + node_id: mix_id_unbonded }) ); - let res = - try_delegate_to_mixnode(test.deps_mut(), env, sender, mix_id_unbonded_leftover); + let res = try_delegate_to_node(test.deps_mut(), env, sender, mix_id_unbonded_leftover); assert_eq!( res, - Err(MixnetContractError::MixNodeBondNotFound { - mix_id: mix_id_unbonded_leftover + Err(MixnetContractError::NymNodeBondNotFound { + node_id: mix_id_unbonded_leftover }) ); } @@ -289,15 +286,15 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); let sender2 = mock_info(owner, &[coin(50_000_000, TEST_COIN_DENOM)]); - let res = try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env.clone(), sender1, mix_id); assert!(res.is_ok()); // still OK - let res = try_delegate_to_mixnode(test.deps_mut(), env, sender2, mix_id); + let res = try_delegate_to_node(test.deps_mut(), env, sender2, mix_id); assert!(res.is_ok()) } @@ -307,13 +304,13 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let amount1 = coin(100_000_000, TEST_COIN_DENOM); let sender1 = mock_info(owner, &[amount1.clone()]); - try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id).unwrap(); + try_delegate_to_node(test.deps_mut(), env.clone(), sender1, mix_id).unwrap(); let events = test.pending_epoch_events(); @@ -332,6 +329,7 @@ mod tests { use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::coin; use cosmwasm_std::testing::mock_info; + use mixnet_contract_common::nym_node::Role; use mixnet_contract_common::{EpochState, EpochStatus}; #[test] @@ -342,12 +340,14 @@ mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("owner", None); + let mix_id = test.add_legacy_mixnode("owner", None); test.add_immediate_delegation("foomp", 1000u32, mix_id); let mut status = EpochStatus::new(test.rewarding_validator().sender); @@ -356,7 +356,7 @@ mod tests { .unwrap(); let env = test.env(); - let res = try_remove_delegation_from_mixnode( + let res = try_remove_delegation_from_node( test.deps_mut(), env.clone(), mock_info("sender", &[]), @@ -375,13 +375,13 @@ mod tests { let env = test.env(); let owner = "delegator"; let sender = mock_info(owner, &[]); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_legacy_mixnode("mix-owner", None); - let res = try_remove_delegation_from_mixnode(test.deps_mut(), env, sender, mix_id); + let res = try_remove_delegation_from_node(test.deps_mut(), env, sender, node_id); assert_eq!( res, - Err(MixnetContractError::NoMixnodeDelegationFound { - mix_id, + Err(MixnetContractError::NodeDelegationNotFound { + node_id, address: owner.to_string(), proxy: None }) @@ -394,17 +394,17 @@ mod tests { let env = test.env(); let owner = "delegator"; - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_legacy_mixnode("mix-owner", None); let sender1 = mock_info(owner, &[coin(100_000_000, TEST_COIN_DENOM)]); let sender2 = mock_info(owner, &[]); - try_delegate_to_mixnode(test.deps_mut(), env.clone(), sender1, mix_id).unwrap(); + try_delegate_to_node(test.deps_mut(), env.clone(), sender1, node_id).unwrap(); - let res = try_remove_delegation_from_mixnode(test.deps_mut(), env, sender2, mix_id); + let res = try_remove_delegation_from_node(test.deps_mut(), env, sender2, node_id); assert_eq!( res, - Err(MixnetContractError::NoMixnodeDelegationFound { - mix_id, + Err(MixnetContractError::NodeDelegationNotFound { + node_id, address: owner.to_string(), proxy: None }) @@ -419,10 +419,10 @@ mod tests { let owner = "delegator"; let sender = mock_info(owner, &[]); - let normal_mix_id = test.add_dummy_mixnode("mix-owner", None); - let mix_id_unbonding = test.add_dummy_mixnode("mix-owner-unbonding", None); + let normal_mix_id = test.add_legacy_mixnode("mix-owner", None); + let mix_id_unbonding = test.add_legacy_mixnode("mix-owner-unbonding", None); let mix_id_unbonded_leftover = - test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); + test.add_legacy_mixnode("mix-owner-unbonded-leftover", None); test.add_immediate_delegation(owner, 10000u32, normal_mix_id); test.add_immediate_delegation(owner, 10000u32, mix_id_unbonding); @@ -443,7 +443,7 @@ mod tests { ) .unwrap(); - let res = try_remove_delegation_from_mixnode( + let res = try_remove_delegation_from_node( test.deps_mut(), env.clone(), sender.clone(), @@ -451,7 +451,7 @@ mod tests { ); assert!(res.is_ok()); - let res = try_remove_delegation_from_mixnode( + let res = try_remove_delegation_from_node( test.deps_mut(), env.clone(), sender.clone(), @@ -459,7 +459,7 @@ mod tests { ); assert!(res.is_ok()); - let res = try_remove_delegation_from_mixnode( + let res = try_remove_delegation_from_node( test.deps_mut(), env, sender, diff --git a/contracts/mixnet/src/families/mod.rs b/contracts/mixnet/src/families/mod.rs deleted file mode 100644 index f3c80cce37..0000000000 --- a/contracts/mixnet/src/families/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -pub mod queries; -pub mod signature_helpers; -pub mod storage; -pub mod transactions; diff --git a/contracts/mixnet/src/families/queries.rs b/contracts/mixnet/src/families/queries.rs deleted file mode 100644 index 96c7f79e23..0000000000 --- a/contracts/mixnet/src/families/queries.rs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use super::storage::{families, get_members, must_get_family, MEMBERS}; -use crate::constants::{FAMILIES_DEFAULT_RETRIEVAL_LIMIT, FAMILIES_MAX_RETRIEVAL_LIMIT}; -use crate::families::storage::must_get_family_by_label; -use cosmwasm_std::{Order, Storage}; -use cw_storage_plus::Bound; -use mixnet_contract_common::families::{ - Family, FamilyByHeadResponse, FamilyByLabelResponse, FamilyHead, FamilyMembersByHeadResponse, - PagedFamiliesResponse, PagedMembersResponse, -}; -use mixnet_contract_common::{error::MixnetContractError, IdentityKeyRef}; -use mixnet_contract_common::{FamilyMembersByLabelResponse, IdentityKey}; - -pub fn get_family_by_label( - label: String, - storage: &dyn Storage, -) -> Result { - let family = families() - .idx - .label - .item(storage, label.clone())? - .map(|o| o.1); - Ok(FamilyByLabelResponse { label, family }) -} - -pub fn get_family_by_head( - head: IdentityKeyRef<'_>, - storage: &dyn Storage, -) -> Result { - let family = families().may_load(storage, head.to_string())?; - Ok(FamilyByHeadResponse { - head: FamilyHead::new(head), - family, - }) -} - -// TODO: this should be returning a paged response! -pub fn get_family_members_by_head( - head: IdentityKeyRef<'_>, - storage: &dyn Storage, -) -> Result { - let family_head = FamilyHead::new(head); - let family = must_get_family(&family_head, storage)?; - let members = get_members(&family, storage)?; - - Ok(FamilyMembersByHeadResponse { - head: family.head().to_owned(), - members, - }) -} - -// TODO: this should be returning a paged response! -pub fn get_family_members_by_label( - label: String, - storage: &dyn Storage, -) -> Result { - let family = must_get_family_by_label(label.clone(), storage)?; - let members = get_members(&family, storage)?; - - Ok(FamilyMembersByLabelResponse { label, members }) -} - -pub fn get_all_families_paged( - storage: &dyn Storage, - start_after: Option, - limit: Option, -) -> Result { - let limit = limit - .unwrap_or(FAMILIES_DEFAULT_RETRIEVAL_LIMIT) - .min(FAMILIES_MAX_RETRIEVAL_LIMIT) as usize; - - let start = start_after.map(Bound::exclusive); - - let response = families() - .range(storage, start, None, Order::Ascending) - .take(limit) - .filter_map(|r| r.ok()) - .map(|(_key, family)| family) - .collect::>(); - - let start_next_after = response - .last() - .map(|response| response.head_identity().to_string()); - - Ok(PagedFamiliesResponse { - families: response, - start_next_after, - }) -} - -pub fn get_all_members_paged( - storage: &dyn Storage, - start_after: Option, - limit: Option, -) -> Result { - let limit = limit - .unwrap_or(FAMILIES_DEFAULT_RETRIEVAL_LIMIT) - .min(FAMILIES_MAX_RETRIEVAL_LIMIT) as usize; - - let start = start_after.map(Bound::exclusive); - - let response = MEMBERS - .range(storage, start, None, Order::Ascending) - .take(limit) - .filter_map(|r| r.ok()) - .collect::>(); - - let start_next_after = response.last().map(|r| r.0.clone()); - - Ok(PagedMembersResponse { - members: response, - start_next_after, - }) -} diff --git a/contracts/mixnet/src/families/signature_helpers.rs b/contracts/mixnet/src/families/signature_helpers.rs deleted file mode 100644 index 3c6bb79626..0000000000 --- a/contracts/mixnet/src/families/signature_helpers.rs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::mixnodes::storage as mixnodes_storage; -use crate::signing::storage as signing_storage; -use crate::support::helpers::decode_ed25519_identity_key; -use cosmwasm_std::Deps; -use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::families::FamilyHead; -use mixnet_contract_common::{construct_family_join_permit, IdentityKeyRef}; -use nym_contracts_common::signing::{MessageSignature, Verifier}; - -pub(crate) fn verify_family_join_permit( - deps: Deps<'_>, - granter: FamilyHead, - member: IdentityKeyRef, - signature: MessageSignature, -) -> Result<(), MixnetContractError> { - // recover the public key - let public_key = decode_ed25519_identity_key(granter.identity())?; - - // that's kinda a backwards way of getting the granter's nonce, but it works, so ¯\_(ツ)_/¯ - let Some(head_mixnode) = mixnodes_storage::mixnode_bonds() - .idx - .identity_key - .item(deps.storage, granter.identity().to_owned())? - .map(|record| record.1) - else { - return Err(MixnetContractError::FamilyDoesNotExist { - head: granter.identity().to_string(), - }); - }; - let nonce = signing_storage::get_signing_nonce(deps.storage, head_mixnode.owner)?; - let msg = construct_family_join_permit(nonce, granter, member.to_owned()); - - if deps.api.verify_message(msg, signature, &public_key)? { - Ok(()) - } else { - Err(MixnetContractError::InvalidEd25519Signature) - } -} diff --git a/contracts/mixnet/src/families/storage.rs b/contracts/mixnet/src/families/storage.rs deleted file mode 100644 index 05b9566b7d..0000000000 --- a/contracts/mixnet/src/families/storage.rs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use cosmwasm_std::{Order, Storage}; -use cw_storage_plus::{Index, IndexList, IndexedMap, Map, UniqueIndex}; -use mixnet_contract_common::families::{Family, FamilyHead}; -use mixnet_contract_common::{error::MixnetContractError, IdentityKey, IdentityKeyRef}; - -use crate::constants::{FAMILIES_INDEX_NAMESPACE, FAMILIES_MAP_NAMESPACE, MEMBERS_MAP_NAMESPACE}; - -type FamilyHeadKey = IdentityKey; - -pub struct FamilyIndex<'a> { - pub label: UniqueIndex<'a, FamilyHeadKey, Family>, -} - -impl<'a> IndexList for FamilyIndex<'a> { - fn get_indexes(&'_ self) -> Box> + '_> { - let v: Vec<&dyn Index> = vec![&self.label]; - Box::new(v.into_iter()) - } -} - -// storage access function. -pub fn families<'a>() -> IndexedMap<'a, FamilyHeadKey, Family, FamilyIndex<'a>> { - let indexes = FamilyIndex { - label: UniqueIndex::new(|d| d.label().to_string(), FAMILIES_INDEX_NAMESPACE), - }; - IndexedMap::new(FAMILIES_MAP_NAMESPACE, indexes) -} - -pub const MEMBERS: Map = Map::new(MEMBERS_MAP_NAMESPACE); - -// TODO: this introduces an unbounded query. We should redesign it. -pub fn get_members( - family: &Family, - store: &dyn Storage, -) -> Result, MixnetContractError> { - Ok(MEMBERS - .range(store, None, None, Order::Ascending) - .filter_map(|res| res.ok()) - .filter(|(_member, head)| head == family.head()) - .map(|(member, _storage_key)| member) - .collect()) -} - -pub fn must_get_family( - head: &FamilyHead, - store: &dyn Storage, -) -> Result { - let key = head.identity(); - - families() - .may_load(store, key.to_string())? - .ok_or(MixnetContractError::FamilyDoesNotExist { - head: head.identity().to_string(), - }) -} - -pub fn must_get_family_by_label( - label: String, - store: &dyn Storage, -) -> Result { - families() - .idx - .label - .item(store, label.clone())? - .map(|record| record.1) - .ok_or(MixnetContractError::FamilyLabelDoesNotExist { label }) -} - -pub fn save_family(f: &Family, store: &mut dyn Storage) -> Result<(), MixnetContractError> { - Ok(families().save(store, f.head_identity().to_string(), f)?) -} - -pub fn add_family_member( - f: &Family, - store: &mut dyn Storage, - member: IdentityKeyRef<'_>, -) -> Result<(), MixnetContractError> { - Ok(MEMBERS.save(store, member.to_string(), f.head())?) -} - -pub fn remove_family_member(store: &mut dyn Storage, member: IdentityKeyRef<'_>) { - MEMBERS.remove(store, member.to_string()) -} - -pub fn is_family_member( - store: &dyn Storage, - f: &Family, - member: IdentityKeyRef<'_>, -) -> Result { - let existing_head = MEMBERS.may_load(store, member.to_owned())?; - Ok(existing_head.as_ref() == Some(f.head())) -} - -pub fn is_any_member( - store: &dyn Storage, - member: IdentityKeyRef<'_>, -) -> Result, MixnetContractError> { - Ok(MEMBERS.may_load(store, member.to_string())?) -} diff --git a/contracts/mixnet/src/families/transactions.rs b/contracts/mixnet/src/families/transactions.rs deleted file mode 100644 index ee88f98e63..0000000000 --- a/contracts/mixnet/src/families/transactions.rs +++ /dev/null @@ -1,291 +0,0 @@ -// Copyright 2022-2024 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use super::storage::{ - add_family_member, is_any_member, is_family_member, must_get_family, remove_family_member, - save_family, -}; -use crate::families::queries::get_family_by_label; -use crate::families::signature_helpers::verify_family_join_permit; -use crate::support::helpers::ensure_bonded; -use cosmwasm_std::{DepsMut, MessageInfo, Response}; -use mixnet_contract_common::families::{Family, FamilyHead}; -use mixnet_contract_common::{error::MixnetContractError, IdentityKey}; -use nym_contracts_common::signing::MessageSignature; - -/// Creates a new MixNode family with senders node as head -pub(crate) fn try_create_family( - deps: DepsMut, - info: MessageInfo, - label: String, -) -> Result { - let existing_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; - - ensure_bonded(&existing_bond)?; - - let family_head = FamilyHead::new(existing_bond.identity()); - - // can't overwrite existing family - if must_get_family(&family_head, deps.storage).is_ok() { - return Err(MixnetContractError::FamilyCanHaveOnlyOne); - } - - // the label must be unique - if get_family_by_label(label.clone(), deps.storage)? - .family - .is_some() - { - return Err(MixnetContractError::FamilyWithLabelExists(label)); - } - - let family = Family::new(family_head, label); - save_family(&family, deps.storage)?; - Ok(Response::default()) -} - -pub(crate) fn try_join_family( - deps: DepsMut, - info: MessageInfo, - join_permit: MessageSignature, - family_head: FamilyHead, -) -> Result { - let existing_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; - - ensure_bonded(&existing_bond)?; - - if family_head.identity() == existing_bond.identity() { - return Err(MixnetContractError::CantJoinOwnFamily { - head: family_head.identity().to_string(), - member: existing_bond.identity().to_string(), - }); - } - - if let Some(family) = is_any_member(deps.storage, existing_bond.identity())? { - return Err(MixnetContractError::AlreadyMemberOfFamily( - family.identity().to_string(), - )); - } - - verify_family_join_permit( - deps.as_ref(), - family_head.clone(), - existing_bond.identity(), - join_permit, - )?; - - let family = must_get_family(&family_head, deps.storage)?; - - add_family_member(&family, deps.storage, existing_bond.identity())?; - - Ok(Response::default()) -} - -pub(crate) fn try_leave_family( - deps: DepsMut, - info: MessageInfo, - family_head: FamilyHead, -) -> Result { - let existing_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; - - ensure_bonded(&existing_bond)?; - - if family_head.identity() == existing_bond.identity() { - return Err(MixnetContractError::CantLeaveOwnFamily { - head: family_head.identity().to_string(), - member: existing_bond.identity().to_string(), - }); - } - - let family = must_get_family(&family_head, deps.storage)?; - if !is_family_member(deps.storage, &family, existing_bond.identity())? { - return Err(MixnetContractError::NotAMember { - head: family_head.identity().to_string(), - member: existing_bond.identity().to_string(), - }); - } - - remove_family_member(deps.storage, existing_bond.identity()); - - Ok(Response::default()) -} - -pub(crate) fn try_head_kick_member( - deps: DepsMut, - info: MessageInfo, - member: IdentityKey, -) -> Result { - let head_bond = - crate::mixnodes::helpers::must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; - - // make sure we're still in the mixnet - ensure_bonded(&head_bond)?; - - // make sure we're not trying to kick ourselves... - if member == head_bond.identity() { - return Err(MixnetContractError::CantLeaveOwnFamily { - head: head_bond.identity().to_string(), - member, - }); - } - - // get the family details - let family_head = FamilyHead::new(head_bond.identity()); - let family = must_get_family(&family_head, deps.storage)?; - - // make sure the member we're trying to kick is an actual member - if !is_family_member(deps.storage, &family, &member)? { - return Err(MixnetContractError::NotAMember { - head: family_head.identity().to_string(), - member, - }); - } - - // finally get rid of the member - remove_family_member(deps.storage, &member); - Ok(Response::default()) -} - -#[cfg(test)] -mod test { - use super::*; - use crate::families::queries::get_family_by_head; - use crate::mixnet_contract_settings::storage::minimum_mixnode_pledge; - use crate::support::tests::fixtures; - use crate::support::tests::test_helpers::TestSetup; - use cosmwasm_std::testing::mock_info; - - #[test] - fn test_family_crud() { - let mut test = TestSetup::new(); - let env = test.env(); - - let head = "alice"; - let malicious_head = "timmy"; - let member = "bob"; - - let minimum_pledge = minimum_mixnode_pledge(test.deps().storage).unwrap(); - let cost_params = fixtures::mix_node_cost_params_fixture(); - - let (head_mixnode, head_bond_sig, head_keypair) = test.mixnode_with_signature(head, None); - let (malicious_mixnode, malicious_bond_sig, _malicious_keypair) = - test.mixnode_with_signature(malicious_head, None); - let (member_mixnode, member_bond_sig, _member_keypair) = - test.mixnode_with_signature(member, None); - - crate::mixnodes::transactions::try_add_mixnode( - test.deps_mut(), - env.clone(), - mock_info(head, &[minimum_pledge.clone()]), - head_mixnode.clone(), - cost_params.clone(), - head_bond_sig, - ) - .unwrap(); - - crate::mixnodes::transactions::try_add_mixnode( - test.deps_mut(), - env.clone(), - mock_info(malicious_head, &[minimum_pledge.clone()]), - malicious_mixnode, - cost_params.clone(), - malicious_bond_sig, - ) - .unwrap(); - - crate::mixnodes::transactions::try_add_mixnode( - test.deps_mut(), - env, - mock_info(member, &[minimum_pledge]), - member_mixnode.clone(), - cost_params, - member_bond_sig, - ) - .unwrap(); - - try_create_family(test.deps_mut(), mock_info(head, &[]), "test".to_string()).unwrap(); - let family_head = FamilyHead::new(&head_mixnode.identity_key); - assert!(must_get_family(&family_head, test.deps().storage).is_ok()); - - let nope = try_create_family( - test.deps_mut(), - mock_info(malicious_head, &[]), - "test".to_string(), - ); - - match nope { - Ok(_) => panic!("This should fail, since family with label already exists"), - Err(e) => match e { - MixnetContractError::FamilyWithLabelExists(label) => assert_eq!(label, "test"), - _ => panic!("This should return FamilyWithLabelExists"), - }, - } - - let family = get_family_by_label("test".to_string(), test.deps().storage) - .unwrap() - .family; - assert!(family.is_some()); - assert_eq!(family.unwrap().head_identity(), family_head.identity()); - - let family = get_family_by_head(family_head.identity(), test.deps().storage) - .unwrap() - .family - .unwrap(); - assert_eq!(family.head_identity(), family_head.identity()); - - let join_permit = - test.generate_family_join_permit(&head_keypair, &member_mixnode.identity_key); - - try_join_family( - test.deps_mut(), - mock_info(member, &[]), - join_permit, - family_head.clone(), - ) - .unwrap(); - - let family = must_get_family(&family_head, test.deps().storage).unwrap(); - - assert!( - is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() - ); - - try_leave_family(test.deps_mut(), mock_info(member, &[]), family_head.clone()).unwrap(); - - let family = must_get_family(&family_head, test.deps().storage).unwrap(); - assert!( - !is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() - ); - - let new_join_permit = - test.generate_family_join_permit(&head_keypair, &member_mixnode.identity_key); - - try_join_family( - test.deps_mut(), - mock_info(member, &[]), - new_join_permit, - family_head.clone(), - ) - .unwrap(); - - let family = must_get_family(&family_head, test.deps().storage).unwrap(); - - assert!( - is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() - ); - - try_head_kick_member( - test.deps_mut(), - mock_info(head, &[]), - member_mixnode.identity_key.clone(), - ) - .unwrap(); - - let family = must_get_family(&family_head, test.deps().storage).unwrap(); - assert!( - !is_family_member(test.deps().storage, &family, &member_mixnode.identity_key).unwrap() - ); - } -} diff --git a/contracts/mixnet/src/gateways/mod.rs b/contracts/mixnet/src/gateways/mod.rs index ebff4a6ace..f3a3c9941f 100644 --- a/contracts/mixnet/src/gateways/mod.rs +++ b/contracts/mixnet/src/gateways/mod.rs @@ -3,6 +3,5 @@ pub mod helpers; pub mod queries; -pub mod signature_helpers; pub mod storage; pub mod transactions; diff --git a/contracts/mixnet/src/gateways/queries.rs b/contracts/mixnet/src/gateways/queries.rs index bfd7eb0c4d..d6eae59487 100644 --- a/contracts/mixnet/src/gateways/queries.rs +++ b/contracts/mixnet/src/gateways/queries.rs @@ -5,6 +5,7 @@ use super::storage; use crate::constants::{GATEWAY_BOND_DEFAULT_RETRIEVAL_LIMIT, GATEWAY_BOND_MAX_RETRIEVAL_LIMIT}; // Keeps gateway and mixnode retrieval in sync by re-using the constant. Could be split into its own constant. use cosmwasm_std::{Deps, Order, StdResult}; use cw_storage_plus::Bound; +use mixnet_contract_common::gateway::{PreassignedGatewayIdsResponse, PreassignedId}; use mixnet_contract_common::{ GatewayBond, GatewayBondResponse, GatewayOwnershipResponse, IdentityKey, PagedGatewayResponse, }; @@ -56,14 +57,36 @@ pub fn query_gateway_bond(deps: Deps<'_>, identity: IdentityKey) -> StdResult, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit.unwrap_or(50).min(100) as usize; + + let start = start_after.as_deref().map(Bound::exclusive); + + let ids = storage::PREASSIGNED_LEGACY_IDS + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|(identity, node_id)| PreassignedId { identity, node_id })) + .collect::>>()?; + + let start_next_after = ids.last().map(|id| id.identity.clone()); + + Ok(PreassignedGatewayIdsResponse { + ids, + start_next_after, + }) +} + #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::contract::execute; use crate::support::tests; use crate::support::tests::test_helpers; use crate::support::tests::test_helpers::TestSetup; - use cosmwasm_std::testing::{mock_env, mock_info}; + use cosmwasm_std::testing::mock_info; #[test] fn gateways_empty_on_init() { @@ -113,61 +136,44 @@ pub(crate) mod tests { #[test] fn gateway_pagination_works() { - let mut deps = test_helpers::init_contract(); - let mut rng = test_helpers::test_rng(); let stake = tests::fixtures::good_gateway_pledge(); + let mut test = TestSetup::new(); - // prepare 4 messages and identities that are sorted by the generated identities + // prepare 4 gateways that are sorted by the generated identities // (because we query them in an ascended manner) - let mut exec_data = (0..4) - .map(|i| { - let sender = format!("nym-addr{}", i); - let (msg, identity) = tests::messages::valid_bond_gateway_msg( - &mut rng, - deps.as_ref(), - stake.clone(), - &sender, - ); - (msg, (sender, identity)) - }) + let mut gateways = (0..4) + .map(|i| test.gateway_with_signature(format!("sender{}", i), None).0) .collect::>(); - exec_data.sort_by(|(_, (_, id1)), (_, (_, id2))| id1.cmp(id2)); - let (messages, sender_identities): (Vec<_>, Vec<_>) = exec_data.into_iter().unzip(); + gateways.sort_by(|g1, g2| g1.identity_key.cmp(&g2.identity_key)); - let info = mock_info(&sender_identities[0].0.clone(), &stake); - execute(deps.as_mut(), mock_env(), info, messages[0].clone()).unwrap(); + let info = mock_info("sender0", &stake); + test.save_legacy_gateway(gateways[0].clone(), &info); let per_page = 2; - let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + let page1 = query_gateways_paged(test.deps(), None, Option::from(per_page)).unwrap(); // page should have 1 result on it assert_eq!(1, page1.nodes.len()); // save another - let info = mock_info( - &sender_identities[1].0.clone(), - &tests::fixtures::good_gateway_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[1].clone()).unwrap(); + let info = mock_info("sender1", &stake); + test.save_legacy_gateway(gateways[1].clone(), &info); // page1 should have 2 results on it - let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + let page1 = query_gateways_paged(test.deps(), None, Option::from(per_page)).unwrap(); assert_eq!(2, page1.nodes.len()); - let info = mock_info( - &sender_identities[2].0.clone(), - &tests::fixtures::good_gateway_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[2].clone()).unwrap(); + let info = mock_info("sender2", &stake); + test.save_legacy_gateway(gateways[2].clone(), &info); // page1 still has 2 results - let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(per_page)).unwrap(); + let page1 = query_gateways_paged(test.deps(), None, Option::from(per_page)).unwrap(); assert_eq!(2, page1.nodes.len()); // retrieving the next page should start after the last key on this page let start_after = page1.start_next_after.unwrap(); let page2 = query_gateways_paged( - deps.as_ref(), + test.deps(), Option::from(start_after.clone()), Option::from(per_page), ) @@ -176,14 +182,11 @@ pub(crate) mod tests { assert_eq!(1, page2.nodes.len()); // save another one - let info = mock_info( - &sender_identities[3].0.clone(), - &tests::fixtures::good_gateway_pledge(), - ); - execute(deps.as_mut(), mock_env(), info, messages[3].clone()).unwrap(); + let info = mock_info("sender3", &stake); + test.save_legacy_gateway(gateways[3].clone(), &info); let page2 = query_gateways_paged( - deps.as_ref(), + test.deps(), Option::from(start_after), Option::from(per_page), ) @@ -202,13 +205,13 @@ pub(crate) mod tests { assert!(res.gateway.is_none()); // gateway was added to "bob", "fred" still does not own one - test.add_dummy_gateway("bob", None); + test.add_legacy_gateway("bob", None); let res = query_owned_gateway(test.deps(), "fred".to_string()).unwrap(); assert!(res.gateway.is_none()); // "fred" now owns a gateway! - test.add_dummy_gateway("fred", None); + test.add_legacy_gateway("fred", None); let res = query_owned_gateway(test.deps(), "fred".to_string()).unwrap(); assert!(res.gateway.is_some()); diff --git a/contracts/mixnet/src/gateways/signature_helpers.rs b/contracts/mixnet/src/gateways/signature_helpers.rs deleted file mode 100644 index 6f9b712554..0000000000 --- a/contracts/mixnet/src/gateways/signature_helpers.rs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::signing::storage as signing_storage; -use crate::support::helpers::decode_ed25519_identity_key; -use cosmwasm_std::{Addr, Coin, Deps}; -use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::{ - construct_gateway_bonding_sign_payload, construct_legacy_gateway_bonding_sign_payload, Gateway, -}; -use nym_contracts_common::signing::MessageSignature; -use nym_contracts_common::signing::Verifier; - -pub(crate) fn verify_gateway_bonding_signature( - deps: Deps<'_>, - sender: Addr, - pledge: Coin, - gateway: Gateway, - signature: MessageSignature, -) -> Result<(), MixnetContractError> { - // recover the public key - let public_key = decode_ed25519_identity_key(&gateway.identity_key)?; - - // reconstruct the payload, first try the current format, then attempt legacy - let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; - let msg = construct_gateway_bonding_sign_payload( - nonce, - sender.clone(), - pledge.clone(), - gateway.clone(), - ); - - if deps - .api - .verify_message(msg, signature.clone(), &public_key)? - { - Ok(()) - } else { - // attempt to use legacy - let msg_legacy = - construct_legacy_gateway_bonding_sign_payload(nonce, sender, pledge, gateway); - if deps - .api - .verify_message(msg_legacy, signature, &public_key)? - { - Ok(()) - } else { - Err(MixnetContractError::InvalidEd25519Signature) - } - } -} diff --git a/contracts/mixnet/src/gateways/storage.rs b/contracts/mixnet/src/gateways/storage.rs index 9b5892844a..9a530fec8d 100644 --- a/contracts/mixnet/src/gateways/storage.rs +++ b/contracts/mixnet/src/gateways/storage.rs @@ -1,10 +1,16 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::constants::{GATEWAYS_OWNER_IDX_NAMESPACE, GATEWAYS_PK_NAMESPACE}; +use crate::constants::{ + GATEWAYS_OWNER_IDX_NAMESPACE, GATEWAYS_PK_NAMESPACE, LEGACY_GATEWAY_ID_NAMESPACE, +}; use cosmwasm_std::Addr; -use cw_storage_plus::{Index, IndexList, IndexedMap, UniqueIndex}; -use mixnet_contract_common::{GatewayBond, IdentityKeyRef}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Map, UniqueIndex}; +use mixnet_contract_common::{GatewayBond, IdentityKeyRef, NodeId}; +use nym_contracts_common::IdentityKey; + +pub(crate) const PREASSIGNED_LEGACY_IDS: Map = + Map::new(LEGACY_GATEWAY_ID_NAMESPACE); pub(crate) struct GatewayBondIndex<'a> { pub(crate) owner: UniqueIndex<'a, Addr, GatewayBond>, @@ -12,7 +18,7 @@ pub(crate) struct GatewayBondIndex<'a> { // IndexList is just boilerplate code for fetching a struct's indexes // note that from my understanding this will be converted into a macro at some point in the future -impl<'a> IndexList for GatewayBondIndex<'a> { +impl IndexList for GatewayBondIndex<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.owner]; Box::new(v.into_iter()) diff --git a/contracts/mixnet/src/gateways/transactions.rs b/contracts/mixnet/src/gateways/transactions.rs index 6075c25894..ef8f282f3e 100644 --- a/contracts/mixnet/src/gateways/transactions.rs +++ b/contracts/mixnet/src/gateways/transactions.rs @@ -3,21 +3,22 @@ use super::helpers::must_get_gateway_bond_by_owner; use super::storage; -use crate::gateways::signature_helpers::verify_gateway_bonding_signature; +use crate::constants::default_node_costs; +use crate::interval::storage as interval_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::signing::storage as signing_storage; -use crate::support::helpers::{ensure_no_existing_bond, validate_pledge}; -use cosmwasm_std::{BankMsg, DepsMut, Env, MessageInfo, Response}; +use crate::nodes::helpers::save_new_nymnode_with_id; +use crate::nodes::transactions::add_nym_node_inner; +use crate::support::helpers::ensure_epoch_in_progress_state; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ - new_gateway_bonding_event, new_gateway_config_update_event, new_gateway_unbonding_event, + new_gateway_config_update_event, new_gateway_unbonding_event, new_migrated_gateway_event, }; use mixnet_contract_common::gateway::GatewayConfigUpdate; -use mixnet_contract_common::{Gateway, GatewayBond}; +use mixnet_contract_common::{Gateway, GatewayBondingPayload, NodeCostParams}; +use nym_contracts_common::helpers::ResponseExt; use nym_contracts_common::signing::MessageSignature; -// TODO: perhaps also require the user to explicitly provide what it thinks is the current nonce -// so that we could return a better error message if it doesn't match? pub(crate) fn try_add_gateway( deps: DepsMut<'_>, env: Env, @@ -25,51 +26,19 @@ pub(crate) fn try_add_gateway( gateway: Gateway, owner_signature: MessageSignature, ) -> Result { - // check if the pledge contains any funds of the appropriate denomination - let minimum_pledge = mixnet_params_storage::minimum_gateway_pledge(deps.storage)?; - let pledge = validate_pledge(info.funds, minimum_pledge)?; - - // if the client has an active bonded mixnode or gateway, don't allow bonding - ensure_no_existing_bond(&info.sender, deps.storage)?; - - // check if somebody else has already bonded a gateway with this identity - if let Some(existing_bond) = - storage::gateways().may_load(deps.storage, &gateway.identity_key)? - { - if existing_bond.owner != info.sender { - return Err(MixnetContractError::DuplicateGateway { - owner: existing_bond.owner, - }); - } - } - - // check if this sender actually owns the gateway by checking the signature - verify_gateway_bonding_signature( - deps.as_ref(), - info.sender.clone(), - pledge.clone(), - gateway.clone(), + let signed_payload = GatewayBondingPayload::new(gateway.clone()); + let denom = mixnet_params_storage::rewarding_denom(deps.storage)?; + let cost_params = default_node_costs(denom); + + add_nym_node_inner( + deps, + env, + info, + gateway.into(), + cost_params, owner_signature, - )?; - - // update the signing nonce associated with this sender so that the future signature would be made on the new value - signing_storage::increment_signing_nonce(deps.storage, info.sender.clone())?; - - let gateway_identity = gateway.identity_key.clone(); - let bond = GatewayBond::new( - pledge.clone(), - info.sender.clone(), - env.block.height, - gateway, - ); - - storage::gateways().save(deps.storage, bond.identity(), &bond)?; - - Ok(Response::new().add_event(new_gateway_bonding_event( - &info.sender, - &pledge, - &gateway_identity, - ))) + signed_payload, + ) } pub(crate) fn try_remove_gateway( @@ -77,31 +46,18 @@ pub(crate) fn try_remove_gateway( info: MessageInfo, ) -> Result { // try to find the node of the sender - let gateway_bond = match storage::gateways() - .idx - .owner - .item(deps.storage, info.sender.clone())? - { - Some(record) => record.1, - None => return Err(MixnetContractError::NoAssociatedGatewayBond { owner: info.sender }), - }; - - // send bonded funds back to the bond owner - let return_tokens = BankMsg::Send { - to_address: info.sender.to_string(), - amount: vec![gateway_bond.pledge_amount()], - }; + let gateway_bond = must_get_gateway_bond_by_owner(deps.storage, &info.sender)?; // remove the bond storage::gateways().remove(deps.storage, gateway_bond.identity())?; Ok(Response::new() - .add_message(return_tokens) .add_event(new_gateway_unbonding_event( &info.sender, &gateway_bond.pledge_amount, gateway_bond.identity(), - ))) + )) + .send_tokens(&info.sender, gateway_bond.pledge_amount)) } pub(crate) fn try_update_gateway_config( @@ -129,28 +85,86 @@ pub(crate) fn try_update_gateway_config( Ok(Response::new().add_event(cfg_update_event)) } +pub fn try_migrate_to_nymnode( + deps: DepsMut, + info: MessageInfo, + cost_params: Option, +) -> Result { + let gateway_bond = must_get_gateway_bond_by_owner(deps.storage, &info.sender)?; + + // currently on mainnet there are no gateways bonded with vesting tokens + // if somebody decides to make one between now and when this is deployed, + // it's on them. they have to unbond and rebond. simple as that. + if gateway_bond.proxy.is_some() { + return Err(MixnetContractError::VestingNodeMigration); + } + + ensure_epoch_in_progress_state(deps.storage)?; + + // remove the bond + storage::gateways().remove(deps.storage, gateway_bond.identity())?; + + let cost_params = + cost_params.unwrap_or_else(|| default_node_costs(&gateway_bond.pledge_amount.denom)); + + let gateway_identity = gateway_bond.gateway.identity_key.clone(); + + // this should have been added during migration + let node_id = storage::PREASSIGNED_LEGACY_IDS + .may_load(deps.storage, gateway_identity.clone())? + .ok_or_else(|| MixnetContractError::InconsistentState { + comment: "legacy gateway did not have a pre-assigned node id".to_string(), + })?; + + let current_epoch = + interval_storage::current_interval(deps.storage)?.current_epoch_absolute_id(); + let previous_epoch = current_epoch.saturating_sub(1); + + // create nym-node entry + // for gateways it's quite straightforward as there are no delegations or rewards to worry about + save_new_nymnode_with_id( + deps.storage, + node_id, + gateway_bond.block_height, + gateway_bond.gateway.into(), + cost_params, + info.sender.clone(), + gateway_bond.pledge_amount, + previous_epoch, + )?; + + storage::PREASSIGNED_LEGACY_IDS.remove(deps.storage, gateway_identity.clone()); + + Ok(Response::new().add_event(new_migrated_gateway_event( + &info.sender, + &gateway_identity, + node_id, + ))) +} + #[cfg(test)] pub mod tests { use super::*; use crate::contract::execute; use crate::gateways::queries; use crate::interval::pending_events; - use crate::mixnet_contract_settings::storage::minimum_gateway_pledge; + use crate::mixnet_contract_settings::storage::minimum_node_pledge; + use crate::nodes::helpers::{get_node_details_by_identity, must_get_node_bond_by_owner}; + use crate::signing::storage as signing_storage; use crate::support::tests; - use crate::support::tests::fixtures; - use crate::support::tests::fixtures::good_mixnode_pledge; + use crate::support::tests::fixtures::{good_gateway_pledge, good_mixnode_pledge}; use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{Addr, Uint128}; + use cosmwasm_std::{Addr, BankMsg, Uint128}; use mixnet_contract_common::ExecuteMsg; #[test] - fn gateway_add() { + fn gateway_add() -> anyhow::Result<()> { let mut test = TestSetup::new(); // if we fail validation (by say not sending enough funds let sender = "alice"; - let minimum_pledge = minimum_gateway_pledge(test.deps().storage).unwrap(); + let minimum_pledge = minimum_node_pledge(test.deps().storage).unwrap(); let mut insufficient_pledge = minimum_pledge.clone(); insufficient_pledge.amount -= Uint128::new(1000); @@ -180,7 +194,7 @@ pub mod tests { let info = mock_info(sender, &[minimum_pledge]); // if there was already a gateway bonded by particular user - test.add_dummy_gateway(sender, None); + test.add_legacy_gateway(sender, None); // it fails let result = try_add_gateway(test.deps_mut(), env.clone(), info, gateway, sig); @@ -189,9 +203,9 @@ pub mod tests { // the same holds if the user already owns a mixnode let sender2 = "mixnode-owner"; - let mix_id = test.add_dummy_mixnode(sender2, None); + let mix_id = test.add_legacy_mixnode(sender2, None); - let info = mock_info(sender2, &fixtures::good_gateway_pledge()); + let info = mock_info(sender2, &good_gateway_pledge()); let (gateway, sig) = test.gateway_with_signature(sender2, None); let result = try_add_gateway( @@ -206,8 +220,26 @@ pub mod tests { // but after he unbonds it, it's all fine again pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap(); - let result = try_add_gateway(test.deps_mut(), env, info, gateway, sig); + let result = try_add_gateway(test.deps_mut(), env, info.clone(), gateway.clone(), sig); assert!(result.is_ok()); + + // and the node has been added as a nym-node + let nym_node = + get_node_details_by_identity(test.deps().storage, gateway.identity_key.clone()) + .unwrap() + .unwrap(); + assert_eq!(nym_node.bond_information.owner, info.sender); + + let maybe_legacy = + storage::gateways().may_load(test.deps().storage, &gateway.identity_key)?; + assert!(maybe_legacy.is_none()); + + // make sure we got assigned the next id (note: we have already bonded a mixnode and a gateway before in this test) + let bond = + must_get_node_bond_by_owner(test.deps().storage, &Addr::unchecked(sender2)).unwrap(); + assert_eq!(3, bond.node_id); + + Ok(()) } #[test] @@ -276,7 +308,8 @@ pub mod tests { .unwrap(); assert_eq!(1, updated_nonce); - try_remove_gateway(test.deps_mut(), info.clone()).unwrap(); + // the moment gateway got bonded, it got added as a nymnode thus we have to remove nym-node + test.immediately_unbond_node(gateway.identity_key.clone()); let res = try_add_gateway(test.deps_mut(), env, info, gateway, signature); assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); @@ -301,7 +334,7 @@ pub mod tests { ); // let's add a node owned by bob - test.add_dummy_gateway("bob", None); + test.add_legacy_gateway("bob", None); // attempt to unbond fred's node, which doesn't exist let info = mock_info("fred", &[]); @@ -324,7 +357,7 @@ pub mod tests { assert_eq!(&Addr::unchecked("bob"), first_node.owner()); // add a node owned by fred - let fred_identity = test.add_dummy_gateway("fred", None); + let (fred_identity, _) = test.add_legacy_gateway("fred", None); // let's make sure we now have 2 nodes: let nodes = queries::query_gateways_paged(test.deps(), None, None) @@ -340,7 +373,7 @@ pub mod tests { // we should see a funds transfer from the contract back to fred let expected_message = BankMsg::Send { to_address: String::from(info.sender), - amount: tests::fixtures::good_gateway_pledge(), + amount: good_gateway_pledge(), }; // run the executor and check that we got back the correct results @@ -386,7 +419,7 @@ pub mod tests { }) ); - test.add_dummy_gateway(owner, None); + test.add_legacy_gateway(owner, None); // "normal" update succeeds let res = try_update_gateway_config(test.deps_mut(), info, update.clone()); diff --git a/contracts/mixnet/src/interval/helpers.rs b/contracts/mixnet/src/interval/helpers.rs index dd45c7511a..f1802973ac 100644 --- a/contracts/mixnet/src/interval/helpers.rs +++ b/contracts/mixnet/src/interval/helpers.rs @@ -1,12 +1,13 @@ -// Copyright 2022 - Nym Technologies SA +// Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 use crate::interval::storage; +use crate::rewards; use crate::rewards::storage as rewards_storage; -use cosmwasm_std::{Response, Storage}; +use cosmwasm_std::{Env, Response, Storage}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::new_interval_config_update_event; -use mixnet_contract_common::{BlockHeight, Interval}; +use mixnet_contract_common::{BlockHeight, EpochId, Interval}; use std::time::Duration; pub(crate) fn change_interval_config( @@ -33,6 +34,26 @@ pub(crate) fn change_interval_config( ))) } +/// Update the storage to advance into the next epoch. +/// It's responsibility of the caller to ensure the epoch is actually over. +pub(crate) fn advance_epoch( + storage: &mut dyn Storage, + env: Env, +) -> Result { + let current_interval = storage::current_interval(storage)?; + + // if the current **INTERVAL** is over, apply reward pool changes + if current_interval.is_current_interval_over(&env) { + // this one is a very important one! + rewards::helpers::apply_reward_pool_changes(storage)?; + } + + let updated_interval = current_interval.advance_epoch(); + storage::save_interval(storage, &updated_interval)?; + + Ok(updated_interval.current_epoch_absolute_id()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/contracts/mixnet/src/interval/pending_events.rs b/contracts/mixnet/src/interval/pending_events.rs index fa44e36ec3..75d78c17b6 100644 --- a/contracts/mixnet/src/interval/pending_events.rs +++ b/contracts/mixnet/src/interval/pending_events.rs @@ -1,21 +1,23 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{Addr, Coin, DepsMut, Env, Response}; +use cosmwasm_std::{Addr, BankMsg, Coin, DepsMut, Env, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ - new_active_set_update_event, new_delegation_event, new_delegation_on_unbonded_node_event, - new_mixnode_cost_params_update_event, new_mixnode_unbonding_event, new_pledge_decrease_event, - new_pledge_increase_event, new_rewarding_params_update_event, new_undelegation_event, + new_active_set_update_event, new_active_set_update_failure, new_cost_params_update_event, + new_delegation_event, new_delegation_on_unbonded_node_event, new_mixnode_unbonding_event, + new_nym_node_unbonding_event, new_pledge_decrease_event, new_pledge_increase_event, + new_rewarding_params_update_event, new_undelegation_event, }; -use mixnet_contract_common::mixnode::MixNodeCostParams; +use mixnet_contract_common::mixnode::NodeCostParams; use mixnet_contract_common::pending_events::{ PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData, PendingIntervalEventKind, }; -use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; -use mixnet_contract_common::{BlockHeight, Delegation, MixId}; +use mixnet_contract_common::reward_params::{ActiveSetUpdate, IntervalRewardingParamsUpdate}; +use mixnet_contract_common::{BlockHeight, Delegation, NodeId}; +use nym_contracts_common::helpers::ResponseExt; use crate::delegations; use crate::delegations::storage as delegations_storage; @@ -23,8 +25,11 @@ use crate::interval::helpers::change_interval_config; use crate::interval::storage; use crate::mixnodes::helpers::{cleanup_post_unbond_mixnode_storage, get_mixnode_details_by_id}; use crate::mixnodes::storage as mixnodes_storage; +use crate::nodes::helpers::{cleanup_post_unbond_nym_node_storage, get_node_details_by_id}; +use crate::nodes::storage as nymnodes_storage; use crate::rewards::storage as rewards_storage; -use crate::support::helpers::AttachSendTokens; +use crate::rewards::storage::RewardingStorage; +use crate::support::helpers::ensure_any_node_bonded; pub(crate) trait ContractExecutableEvent { // note: the error only means a HARD error like we failed to read from storage. @@ -38,33 +43,37 @@ pub(crate) fn delegate( env: &Env, created_at: BlockHeight, owner: Addr, - mix_id: MixId, + node_id: NodeId, amount: Coin, ) -> Result { - // check if the target node still exists (it might have unbonded between this event getting created - // and being executed). Do note that it's absolutely possible for a mixnode to get immediately + // first check - see if we have any rewarding information in the storage, if not, the node has fully unbonded + // (and also didn't have any delegations) + let Some(mut node_rewarding) = + rewards_storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? + else { + return Ok(Response::new() + .send_tokens(&owner, amount) + .add_event(new_delegation_on_unbonded_node_event(&owner, node_id))); + }; + + // more extensive check to see if node is still bonded, however, this time we have to unfortunately check + // BOTH nym-node and legacy nym-node. + // the underlying target node might have unbonded between this event getting created + // and being executed. Do note that it's absolutely possible for a node to get immediately // unbonded at this very block (if the event was pending), but that's tough luck, then it's up // to the delegator to click the undelegate button - let mixnode_details = match get_mixnode_details_by_id(deps.storage, mix_id)? { - Some(details) - if details.rewarding_details.still_bonded() - && !details.bond_information.is_unbonding => - { - details - } - _ => { - // if mixnode is no longer bonded or in the process of unbonding, return the tokens back to the - // delegator; - let response = Response::new() - .send_tokens(&owner, amount.clone()) - .add_event(new_delegation_on_unbonded_node_event(&owner, mix_id)); - - return Ok(response); + match ensure_any_node_bonded(deps.storage, node_id) { + // cosmwasm-std errors are critical and recoverable because they imply issues with the underlying storage + Err(MixnetContractError::StdErr { source }) => return Err(source.into()), + Err(_unbonded) => { + return Ok(Response::new() + .send_tokens(&owner, amount) + .add_event(new_delegation_on_unbonded_node_event(&owner, node_id))); } - }; + Ok(_) => {} + } let new_delegation_amount = amount.clone(); - let mut mix_rewarding = mixnode_details.rewarding_details; // the delegation_amount might get increased if there's already a pre-existing delegation on this mixnode // (in that case we just create a fresh delegation with the sum of both) @@ -72,12 +81,12 @@ pub(crate) fn delegate( // if there's an existing delegation, then withdraw the full reward and create a new delegation // with the sum of both - let storage_key = Delegation::generate_storage_key(mix_id, &owner, None); + let storage_key = Delegation::generate_storage_key(node_id, &owner, None); let old_delegation = if let Some(existing_delegation) = delegations_storage::delegations().may_load(deps.storage, storage_key.clone())? { // completely remove the delegation from the node - let og_with_reward = mix_rewarding.undelegate(&existing_delegation)?; + let og_with_reward = node_rewarding.undelegate(&existing_delegation)?; // and adjust the new value by the amount removed (which contains the original delegation // alongside any earned rewards) @@ -89,20 +98,20 @@ pub(crate) fn delegate( }; // add the amount we're intending to delegate (whether it's fresh or we're adding to the existing one) - mix_rewarding.add_base_delegation(stored_delegation_amount.amount)?; + node_rewarding.add_base_delegation(stored_delegation_amount.amount)?; let cosmos_event = new_delegation_event( created_at, &owner, &new_delegation_amount, - mix_id, - mix_rewarding.total_unit_reward, + node_id, + node_rewarding.total_unit_reward, ); let delegation = Delegation::new( owner, - mix_id, - mix_rewarding.total_unit_reward, + node_id, + node_rewarding.total_unit_reward, stored_delegation_amount, env.block.height, ); @@ -114,7 +123,7 @@ pub(crate) fn delegate( Some(&delegation), old_delegation.as_ref(), )?; - rewards_storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; + rewards_storage::NYMNODE_REWARDING.save(deps.storage, node_id, &node_rewarding)?; Ok(Response::new().add_event(cosmos_event)) } @@ -123,7 +132,7 @@ pub(crate) fn undelegate( deps: DepsMut<'_>, created_at: BlockHeight, owner: Addr, - mix_id: MixId, + mix_id: NodeId, ) -> Result { // see if the delegation still exists (in case of impatient user who decided to send multiple // undelegation requests in an epoch) @@ -132,13 +141,17 @@ pub(crate) fn undelegate( None => return Ok(Response::default()), Some(delegation) => delegation, }; - let mix_rewarding = - rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::inconsistent_state( - "mixnode rewarding got removed from the storage whilst there's still an existing delegation", + + // note: rewarding information for nym-nodes and mixnodes are stored under the same storage structure + // and in the same underlying map so it doesn't matter which one we load + + let rewarding_info = + rewards_storage::NYMNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::inconsistent_state( + "node rewarding got removed from the storage whilst there's still an existing delegation", ))?; // this also appropriately adjusts the storage let tokens_to_return = - delegations::helpers::undelegate(deps.storage, delegation, mix_rewarding)?; + delegations::helpers::undelegate(deps.storage, delegation, rewarding_info)?; let response = Response::new() .send_tokens(&owner, tokens_to_return.clone()) @@ -147,11 +160,55 @@ pub(crate) fn undelegate( Ok(response) } +pub(crate) fn unbond_nym_node( + deps: DepsMut<'_>, + env: &Env, + created_at: BlockHeight, + node_id: NodeId, +) -> Result { + // if we're here it means user executed `try_remove_nym_node` and as a result node was set to be + // in unbonding state and thus nothing could have been done to it (such as attempting to double unbond it) + // thus the node with all its associated information MUST exist in the storage. + let node_details = get_node_details_by_id(deps.storage, node_id)?.ok_or( + MixnetContractError::inconsistent_state( + "nym node getting processed to get unbonded doesn't exist in the storage", + ), + )?; + if node_details.pending_changes.pledge_change.is_some() { + return Err(MixnetContractError::inconsistent_state( + "attempted to unbond nym node while there are associated pending pledge changes", + )); + } + + // the denom on the original pledge was validated at the time of bonding, so we can safely reuse it here + let rewarding_denom = &node_details.bond_information.original_pledge.denom; + let tokens = node_details + .rewarding_details + .operator_pledge_with_reward(rewarding_denom); + + let owner = &node_details.bond_information.owner; + + // send bonded funds (alongside all earned rewards) to the bond owner + let return_tokens = BankMsg::Send { + to_address: owner.to_string(), + amount: vec![tokens], + }; + + // remove the bond and if there are no delegations left, also the rewarding information + cleanup_post_unbond_nym_node_storage(deps.storage, env, &node_details)?; + + let response = Response::new() + .add_message(return_tokens) + .add_event(new_nym_node_unbonding_event(created_at, node_id)); + + Ok(response) +} + pub(crate) fn unbond_mixnode( deps: DepsMut<'_>, env: &Env, created_at: BlockHeight, - mix_id: MixId, + mix_id: NodeId, ) -> Result { // if we're here it means user executed `_try_remove_mixnode` and as a result node was set to be // in unbonding state and thus nothing could have been done to it (such as attempting to double unbond it) @@ -186,28 +243,76 @@ pub(crate) fn unbond_mixnode( Ok(response) } -pub(crate) fn update_active_set_size( +pub(crate) fn update_active_set( deps: DepsMut<'_>, created_at: BlockHeight, - active_set_size: u32, + update: ActiveSetUpdate, ) -> Result { // We don't have to check for authorization as this event can only be pushed // by the authorized entity. // Furthermore, we don't need to check whether the epoch is finished as the // queue is only emptied upon the epoch finishing. - // Also, we know the update is valid as we checked for that before pushing the event onto the queue. + let global_rewarding_params_storage = RewardingStorage::load().global_rewarding_params; + let mut rewarding_params = global_rewarding_params_storage.load(deps.storage)?; + + // unfortunately this will be a silent error, + // but for this to happen an admin must have been screwing around by pushing active set update onto the queue + // but updating rewarded set immediately, so it's on them + if let Err(err) = rewarding_params.try_change_active_set(update) { + return Ok(Response::new().add_event(new_active_set_update_failure(err))); + } + global_rewarding_params_storage.save(deps.storage, &rewarding_params)?; - let mut rewarding_params = rewards_storage::REWARDING_PARAMS.load(deps.storage)?; - rewarding_params.try_change_active_set_size(active_set_size)?; - rewards_storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?; + Ok(Response::new().add_event(new_active_set_update_event(created_at, update))) +} - Ok(Response::new().add_event(new_active_set_update_event(created_at, active_set_size))) +pub(crate) fn increase_nym_node_pledge( + deps: DepsMut<'_>, + created_at: BlockHeight, + node_id: NodeId, + increase: Coin, +) -> Result { + // note: we have already validated the amount to know it has the correct denomination + + // the target node MUST exist - we have checked it at the time of putting this event onto the queue + // we have also verified there were no preceding unbond events + let node_details = get_node_details_by_id(deps.storage, node_id)?.ok_or( + MixnetContractError::inconsistent_state( + "nym node getting processed to increase its pledge doesn't exist in the storage", + ), + )?; + if node_details.pending_changes.pledge_change.is_none() { + return Err(MixnetContractError::inconsistent_state( + "attempted to increase nym node pledge while there are no associated pending changes", + )); + } + + let mut updated_bond = node_details.bond_information.clone(); + let mut updated_rewarding = node_details.rewarding_details; + + updated_bond.original_pledge.amount += increase.amount; + updated_rewarding.increase_operator_uint128(increase.amount)?; + + let mut pending_changes = node_details.pending_changes; + pending_changes.pledge_change = None; + + // update all: bond information, rewarding details and pending pledge changes + nymnodes_storage::nym_nodes().replace( + deps.storage, + node_id, + Some(&updated_bond), + Some(&node_details.bond_information), + )?; + rewards_storage::NYMNODE_REWARDING.save(deps.storage, node_id, &updated_rewarding)?; + nymnodes_storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; + + Ok(Response::new().add_event(new_pledge_increase_event(created_at, node_id, &increase))) } -pub(crate) fn increase_pledge( +pub(crate) fn increase_mixnode_pledge( deps: DepsMut<'_>, created_at: BlockHeight, - mix_id: MixId, + mix_id: NodeId, increase: Coin, ) -> Result { // note: we have already validated the amount to know it has the correct denomination @@ -247,10 +352,10 @@ pub(crate) fn increase_pledge( Ok(Response::new().add_event(new_pledge_increase_event(created_at, mix_id, &increase))) } -pub(crate) fn decrease_pledge( +pub(crate) fn decrease_mixnode_pledge( deps: DepsMut<'_>, created_at: BlockHeight, - mix_id: MixId, + mix_id: NodeId, decrease_by: Coin, ) -> Result { // the target node MUST exist - we have checked it at the time of putting this event onto the queue @@ -296,6 +401,61 @@ pub(crate) fn decrease_pledge( Ok(response) } +pub(crate) fn decrease_nym_node_pledge( + deps: DepsMut<'_>, + created_at: BlockHeight, + node_id: NodeId, + decrease_by: Coin, +) -> Result { + // the target node MUST exist - we have checked it at the time of putting this event onto the queue + // we have also verified there were no preceding unbond events + let node_details = get_node_details_by_id(deps.storage, node_id)?.ok_or( + MixnetContractError::inconsistent_state( + "nym node getting processed to increase its pledge doesn't exist in the storage", + ), + )?; + if node_details.pending_changes.pledge_change.is_none() { + return Err(MixnetContractError::inconsistent_state( + "attempted to increase nym node pledge while there are no associated pending changes", + )); + } + + let mut updated_bond = node_details.bond_information.clone(); + let mut updated_rewarding = node_details.rewarding_details; + + let mut pending_changes = node_details.pending_changes; + pending_changes.pledge_change = None; + + // SAFETY: the subtraction here can't overflow as before the event was pushed into the queue, + // we checked that the new value will be higher than minimum pledge (which is also strictly positive) + updated_bond.original_pledge.amount -= decrease_by.amount; + updated_rewarding.decrease_operator_uint128(decrease_by.amount)?; + + let owner = &node_details.bond_information.owner; + + // send the removed tokens back to the operator + let return_tokens = BankMsg::Send { + to_address: owner.to_string(), + amount: vec![decrease_by.clone()], + }; + + // update all: bond information, rewarding details and pending pledge changes + nymnodes_storage::nym_nodes().replace( + deps.storage, + node_id, + Some(&updated_bond), + Some(&node_details.bond_information), + )?; + rewards_storage::NYMNODE_REWARDING.save(deps.storage, node_id, &updated_rewarding)?; + nymnodes_storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; + + let response = Response::new() + .add_message(return_tokens) + .add_event(new_pledge_decrease_event(created_at, node_id, &decrease_by)); + + Ok(response) +} + impl ContractExecutableEvent for PendingEpochEventData { fn execute(self, deps: DepsMut<'_>, env: &Env) -> Result { // note that the basic validation on all those events was already performed before @@ -303,25 +463,37 @@ impl ContractExecutableEvent for PendingEpochEventData { match self.kind { PendingEpochEventKind::Delegate { owner, - mix_id, + node_id: mix_id, amount, .. } => delegate(deps, env, self.created_at, owner, mix_id, amount), - PendingEpochEventKind::Undelegate { owner, mix_id, .. } => { - undelegate(deps, self.created_at, owner, mix_id) + PendingEpochEventKind::Undelegate { + owner, + node_id: mix_id, + .. + } => undelegate(deps, self.created_at, owner, mix_id), + PendingEpochEventKind::NymNodePledgeMore { node_id, amount } => { + increase_nym_node_pledge(deps, self.created_at, node_id, amount) } - PendingEpochEventKind::PledgeMore { mix_id, amount } => { - increase_pledge(deps, self.created_at, mix_id, amount) + PendingEpochEventKind::MixnodePledgeMore { mix_id, amount } => { + increase_mixnode_pledge(deps, self.created_at, mix_id, amount) } - PendingEpochEventKind::DecreasePledge { + PendingEpochEventKind::NymNodeDecreasePledge { + node_id, + decrease_by, + } => decrease_nym_node_pledge(deps, self.created_at, node_id, decrease_by), + PendingEpochEventKind::MixnodeDecreasePledge { mix_id, decrease_by, - } => decrease_pledge(deps, self.created_at, mix_id, decrease_by), + } => decrease_mixnode_pledge(deps, self.created_at, mix_id, decrease_by), PendingEpochEventKind::UnbondMixnode { mix_id } => { unbond_mixnode(deps, env, self.created_at, mix_id) } - PendingEpochEventKind::UpdateActiveSetSize { new_size } => { - update_active_set_size(deps, self.created_at, new_size) + PendingEpochEventKind::UnbondNymNode { node_id } => { + unbond_nym_node(deps, env, self.created_at, node_id) + } + PendingEpochEventKind::UpdateActiveSet { update } => { + update_active_set(deps, self.created_at, update) } } } @@ -330,8 +502,8 @@ impl ContractExecutableEvent for PendingEpochEventData { pub(crate) fn change_mix_cost_params( deps: DepsMut<'_>, created_at: BlockHeight, - mix_id: MixId, - new_costs: MixNodeCostParams, + mix_id: NodeId, + new_costs: NodeCostParams, ) -> Result { // almost an entire interval might have passed since the request was issued -> check if the // node still exists @@ -345,12 +517,48 @@ pub(crate) fn change_mix_cost_params( _ => return Ok(Response::default()), }; - let cosmos_event = new_mixnode_cost_params_update_event(created_at, mix_id, &new_costs); + let mut pending_changes = + mixnodes_storage::PENDING_MIXNODE_CHANGES.load(deps.storage, mix_id)?; + pending_changes.cost_params_change = None; + + let cosmos_event = new_cost_params_update_event(created_at, mix_id, &new_costs); // TODO: can we just change cost_params without breaking rewarding calculation? // (I'm almost certain we can, but well, it has to be tested) mix_rewarding.cost_params = new_costs; rewards_storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; + mixnodes_storage::PENDING_MIXNODE_CHANGES.save(deps.storage, mix_id, &pending_changes)?; + + Ok(Response::new().add_event(cosmos_event)) +} + +pub(crate) fn change_nym_node_cost_params( + deps: DepsMut<'_>, + created_at: BlockHeight, + node_id: NodeId, + new_costs: NodeCostParams, +) -> Result { + // almost an entire interval might have passed since the request was issued -> check if the + // node still exists + // + // note: there's no check if the bond is in "unbonding" state, as epoch actions would get + // cleared before touching interval actions + let mut node_rewarding = + match rewards_storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { + Some(node_rewarding) if node_rewarding.still_bonded() => node_rewarding, + // if node doesn't exist anymore, don't do anything, simple as that. + _ => return Ok(Response::default()), + }; + + let mut pending_changes = + nymnodes_storage::PENDING_NYMNODE_CHANGES.load(deps.storage, node_id)?; + pending_changes.cost_params_change = None; + + let cosmos_event = new_cost_params_update_event(created_at, node_id, &new_costs); + + node_rewarding.cost_params = new_costs; + rewards_storage::NYMNODE_REWARDING.save(deps.storage, node_id, &node_rewarding)?; + nymnodes_storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; Ok(Response::new().add_event(cosmos_event)) } @@ -404,10 +612,12 @@ impl ContractExecutableEvent for PendingIntervalEventData { // note that the basic validation on all those events was already performed before // they were pushed onto the queue match self.kind { - PendingIntervalEventKind::ChangeMixCostParams { - mix_id: mix, - new_costs, - } => change_mix_cost_params(deps, self.created_at, mix, new_costs), + PendingIntervalEventKind::ChangeMixCostParams { mix_id, new_costs } => { + change_mix_cost_params(deps, self.created_at, mix_id, new_costs) + } + PendingIntervalEventKind::ChangeNymNodeCostParams { node_id, new_costs } => { + change_nym_node_cost_params(deps, self.created_at, node_id, new_costs) + } PendingIntervalEventKind::UpdateRewardingParams { update } => { update_rewarding_params(deps, self.created_at, update) } @@ -427,7 +637,6 @@ impl ContractExecutableEvent for PendingIntervalEventData { #[cfg(test)] mod tests { use super::*; - use crate::support::tests::test_helpers; use crate::support::tests::test_helpers::{assert_decimals, TestSetup}; use cosmwasm_std::Decimal; use mixnet_contract_common::Percent; @@ -449,7 +658,7 @@ mod tests { #[test] fn returns_the_tokens_if_mixnode_has_unbonded() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let delegation = 120_000_000u128; let delegation_coin = coin(delegation, TEST_COIN_DENOM); @@ -512,7 +721,7 @@ mod tests { #[test] fn returns_the_tokens_is_mixnode_is_unbonding() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let delegation = 120_000_000u128; let delegation_coin = coin(delegation, TEST_COIN_DENOM); @@ -575,7 +784,8 @@ mod tests { #[test] fn if_delegation_already_exists_a_fresh_one_with_sum_of_both_is_created() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + let mix_id = + test.add_rewarded_legacy_mixnode("mix-owner", Some(100_000_000_000u128.into())); let delegation_og = 120_000_000u128; let delegation_new = 543_000_000u128; @@ -619,38 +829,30 @@ mod tests { #[test] fn if_delegation_already_exists_with_unclaimed_rewards_fresh_one_is_created() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + let mix_id = + test.add_rewarded_legacy_mixnode("mix-owner", Some(100_000_000_000u128.into())); let delegation_og = 120_000_000u128; let delegation_new = 543_000_000u128; let delegation_coin_new = coin(delegation_new, TEST_COIN_DENOM); + let active_params = test.active_node_params(100.0); // perform some rewarding here to advance the unit delegation beyond the initial value - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); + test.skip_to_next_epoch_end(); + test.reward_with_distribution_ignore_state(mix_id, active_params); let owner = "delegator"; test.add_immediate_delegation(owner, delegation_og, mix_id); test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist2 = test.reward_with_distribution_ignore_state(mix_id, active_params); let storage_key = Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner), None); @@ -701,24 +903,20 @@ mod tests { #[test] fn appropriately_updates_state_for_fresh_delegation() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + let mix_id = + test.add_rewarded_legacy_mixnode("mix-owner", Some(100_000_000_000u128.into())); let owner = "delegator"; let delegation = 120_000_000u128; let delegation_coin = coin(120_000_000u128, TEST_COIN_DENOM); + let active_params = test.active_node_params(100.0); // perform some rewarding here to advance the unit delegation beyond the initial value - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); let storage_key = Delegation::generate_storage_key(mix_id, &Addr::unchecked(owner), None); @@ -766,7 +964,7 @@ mod tests { #[test] fn doesnt_return_any_tokens_if_it_doesnt_exist() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let owner = Addr::unchecked("delegator"); @@ -777,7 +975,7 @@ mod tests { #[test] fn errors_out_if_mix_rewarding_doesnt_exist() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let owner = Addr::unchecked("delegator"); test.add_immediate_delegation(owner.as_str(), 100_000_000u32, mix_id); @@ -795,36 +993,27 @@ mod tests { #[test] fn returns_all_delegated_tokens_with_earned_rewards() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(100_000_000_000u128.into())); + let mix_id = + test.add_rewarded_legacy_mixnode("mix-owner", Some(100_000_000_000u128.into())); let owner = "delegator"; let delegation = 120_000_000u128; + let active_params = test.active_node_params(100.0); + // perform some rewarding here to advance the unit delegation beyond the initial value - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.add_immediate_delegation(owner, delegation, mix_id); test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist2 = test.reward_with_distribution_ignore_state(mix_id, active_params); let expected_reward = dist1.delegates + dist2.delegates; let truncated_reward = truncate_reward_amount(expected_reward); @@ -853,12 +1042,14 @@ mod tests { #[cfg(test)] mod mixnode_unbonding { - use super::*; + use crate::compat::transactions::{try_decrease_pledge, try_increase_pledge}; + use crate::interval::pending_events::unbond_mixnode; use crate::mixnodes::storage as mixnodes_storage; - use crate::mixnodes::transactions::{try_decrease_pledge, try_increase_pledge}; - use crate::support::tests::test_helpers::get_bank_send_msg; + use crate::rewards::storage as rewards_storage; + use crate::support::tests::test_helpers::{get_bank_send_msg, TestSetup}; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::Uint128; + use cosmwasm_std::{Addr, Uint128}; + use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::mixnode::{PendingMixNodeChanges, UnbondedMixnode}; use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; @@ -884,14 +1075,9 @@ mod tests { // increase let owner = "mix-owner1"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); - try_increase_pledge( - test.deps_mut(), - env.clone(), - mock_info(owner, &change.clone()), - ) - .unwrap(); + try_increase_pledge(test.deps_mut(), env.clone(), mock_info(owner, &change)).unwrap(); let res = unbond_mixnode(test.deps_mut(), &env, 123, mix_id); assert!(matches!( @@ -902,7 +1088,7 @@ mod tests { // decrease let owner = "mix-owner2"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); try_decrease_pledge( test.deps_mut(), @@ -921,10 +1107,11 @@ mod tests { // artificial let owner = "mix-owner3"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); let changes = PendingMixNodeChanges { pledge_change: Some(1234), + cost_params_change: None, }; mixnodes_storage::PENDING_MIXNODE_CHANGES @@ -943,23 +1130,17 @@ mod tests { let owner = "mix-owner"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); let mix_details = mixnodes_storage::mixnode_bonds() .load(test.deps().storage, mix_id) .unwrap(); - let layer = mix_details.layer; + let active_params = test.active_node_params(100.0); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist2 = test.reward_with_distribution_ignore_state(mix_id, active_params); let expected_reward = dist1.operator + dist2.operator; let truncated_reward = truncate_reward_amount(expected_reward); @@ -991,10 +1172,6 @@ mod tests { .load(test.deps().storage, mix_id) .unwrap() ); - assert_eq!( - mixnodes_storage::LAYERS.load(test.deps().storage).unwrap()[layer], - 0 - ) } } @@ -1012,7 +1189,7 @@ mod tests { let mut test = TestSetup::new(); let amount = test.coin(123); - let res = increase_pledge(test.deps_mut(), 123, 1, amount); + let res = increase_mixnode_pledge(test.deps_mut(), 123, 1, amount); assert!(matches!( res, Err(MixnetContractError::InconsistentState { .. }) @@ -1026,9 +1203,9 @@ mod tests { let owner = "mix-owner"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); - let res = increase_pledge(test.deps_mut(), 123, mix_id, change); + let res = increase_mixnode_pledge(test.deps_mut(), 123, mix_id, change); assert!(matches!( res, Err(MixnetContractError::InconsistentState { .. }) @@ -1038,7 +1215,7 @@ mod tests { #[test] fn updates_stored_bond_information_and_rewarding_details() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.set_pending_pledge_change(mix_id, None); let old_details = get_mixnode_details_by_id(test.deps().storage, mix_id) @@ -1046,7 +1223,7 @@ mod tests { .unwrap(); let amount = test.coin(12345); - increase_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); + increase_mixnode_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); let updated_details = get_mixnode_details_by_id(test.deps().storage, mix_id) .unwrap() @@ -1070,14 +1247,15 @@ mod tests { let pledge1 = Uint128::new(150_000_000); let pledge2 = Uint128::new(50_000_000); let pledge3 = Uint128::new(200_000_000); + let active_params = test.active_node_params(100.0); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); let increase = test.coin(pledge2.u128()); - increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); + increase_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789u128, mix_id_repledge); test.add_immediate_delegation("bob", 500_000_000u128, mix_id_repledge); @@ -1088,16 +1266,11 @@ mod tests { test.add_immediate_delegation("carol", 111_111_111u128, mix_id_full_pledge); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); - let dist1 = test.reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); - let dist2 = test.reward_with_distribution_with_state_bypass( - mix_id_full_pledge, - test_helpers::performance(100.0), - ); + let dist1 = test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); + let dist2 = + test.reward_with_distribution_ignore_state(mix_id_full_pledge, active_params); assert_eq!(dist1, dist2) } @@ -1107,8 +1280,9 @@ mod tests { let mut test = TestSetup::new(); let pledge1 = Uint128::new(150_000_000_000); let pledge2 = Uint128::new(50_000_000_000); + let active_params = test.active_node_params(100.0); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); @@ -1116,18 +1290,15 @@ mod tests { test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_repledge); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge]); - let dist = test.reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); let increase = test.coin(pledge2.u128()); - increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); + increase_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); let pledge3 = Uint128::new(200_000_000_000) + truncate_reward_amount(dist.operator); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge); test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge); @@ -1157,19 +1328,15 @@ mod tests { ); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); // go through few epochs of rewarding for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); - let dist2 = test.reward_with_distribution_with_state_bypass( - mix_id_full_pledge, - test_helpers::performance(100.0), - ); + let dist1 = + test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); + let dist2 = + test.reward_with_distribution_ignore_state(mix_id_full_pledge, active_params); assert_eq!(dist1, dist2) } @@ -1180,8 +1347,9 @@ mod tests { let mut test = TestSetup::new(); let pledge1 = Uint128::new(150_000_000_000); let pledge2 = Uint128::new(50_000_000_000); + let active_params = test.active_node_params(100.0); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); @@ -1189,7 +1357,7 @@ mod tests { test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_repledge); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge]); let mut cumulative_op_reward = Decimal::zero(); let mut cumulative_del_reward = Decimal::zero(); @@ -1197,20 +1365,18 @@ mod tests { // go few epochs of rewarding before adding more pledge for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); + let dist = + test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); cumulative_op_reward += dist.operator; cumulative_del_reward += dist.delegates; } let increase = test.coin(pledge2.u128()); - increase_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); + increase_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, increase).unwrap(); let pledge3 = Uint128::new(200_000_000_000) + truncate_reward_amount(cumulative_op_reward); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge); test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge); @@ -1240,19 +1406,15 @@ mod tests { ); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); // go through few more epochs of rewarding for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); - let dist2 = test.reward_with_distribution_with_state_bypass( - mix_id_full_pledge, - test_helpers::performance(100.0), - ); + let dist1 = + test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); + let dist2 = + test.reward_with_distribution_ignore_state(mix_id_full_pledge, active_params); assert_eq!(dist1, dist2) } @@ -1261,11 +1423,11 @@ mod tests { #[test] fn updates_the_pending_pledge_changes_field() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.set_pending_pledge_change(mix_id, None); let amount = test.coin(12345); - increase_pledge(test.deps_mut(), 123, mix_id, amount).unwrap(); + increase_mixnode_pledge(test.deps_mut(), 123, mix_id, amount).unwrap(); let pending = mixnodes_storage::PENDING_MIXNODE_CHANGES .load(test.deps().storage, mix_id) .unwrap(); @@ -1285,7 +1447,7 @@ mod tests { let mut test = TestSetup::new(); let amount = test.coin(123); - let res = decrease_pledge(test.deps_mut(), 123, 1, amount); + let res = decrease_mixnode_pledge(test.deps_mut(), 123, 1, amount); assert!(matches!( res, Err(MixnetContractError::InconsistentState { .. }) @@ -1299,9 +1461,9 @@ mod tests { let owner = "mix-owner"; let pledge = Uint128::new(250_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(pledge)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(pledge)); - let res = decrease_pledge(test.deps_mut(), 123, mix_id, change); + let res = decrease_mixnode_pledge(test.deps_mut(), 123, mix_id, change); assert!(matches!( res, Err(MixnetContractError::InconsistentState { .. }) @@ -1311,7 +1473,7 @@ mod tests { #[test] fn updates_stored_bond_information_and_rewarding_details() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.set_pending_pledge_change(mix_id, None); let old_details = get_mixnode_details_by_id(test.deps().storage, mix_id) @@ -1319,7 +1481,7 @@ mod tests { .unwrap(); let amount = test.coin(12345); - decrease_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); let updated_details = get_mixnode_details_by_id(test.deps().storage, mix_id) .unwrap() @@ -1341,11 +1503,12 @@ mod tests { fn returns_tokens_back_to_the_owner() { let mut test = TestSetup::new(); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_rewarded_legacy_mixnode(owner, None); test.set_pending_pledge_change(mix_id, None); let amount = test.coin(12345); - let res = decrease_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); + let res = + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id, amount.clone()).unwrap(); assert_eq!(res.messages.len(), 1); assert_eq!( @@ -1363,14 +1526,15 @@ mod tests { let pledge1 = Uint128::new(200_000_000); let pledge_change = Uint128::new(50_000_000); let pledge3 = Uint128::new(150_000_000); + let active_params = test.active_node_params(100.0); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); let decrease = test.coin(pledge_change.u128()); - decrease_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789u128, mix_id_repledge); test.add_immediate_delegation("bob", 500_000_000u128, mix_id_repledge); @@ -1381,16 +1545,11 @@ mod tests { test.add_immediate_delegation("carol", 111_111_111u128, mix_id_full_pledge); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); - let dist1 = test.reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); - let dist2 = test.reward_with_distribution_with_state_bypass( - mix_id_full_pledge, - test_helpers::performance(100.0), - ); + let dist1 = test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); + let dist2 = + test.reward_with_distribution_ignore_state(mix_id_full_pledge, active_params); assert_eq!(dist1, dist2) } @@ -1400,8 +1559,9 @@ mod tests { let mut test = TestSetup::new(); let pledge1 = Uint128::new(200_000_000_000); let pledge_change = Uint128::new(50_000_000_000); + let active_params = test.active_node_params(100.0); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); @@ -1409,18 +1569,15 @@ mod tests { test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_repledge); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge]); - let dist = test.reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); let decrease = test.coin(pledge_change.u128()); - decrease_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); let pledge3 = Uint128::new(150_000_000_000) + truncate_reward_amount(dist.operator); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge); test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge); @@ -1450,19 +1607,15 @@ mod tests { ); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); // go through few epochs of rewarding for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); - let dist2 = test.reward_with_distribution_with_state_bypass( - mix_id_full_pledge, - test_helpers::performance(100.0), - ); + let dist1 = + test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); + let dist2 = + test.reward_with_distribution_ignore_state(mix_id_full_pledge, active_params); assert_eq!(dist1, dist2) } @@ -1473,8 +1626,9 @@ mod tests { let mut test = TestSetup::new(); let pledge1 = Uint128::new(200_000_000_000); let pledge_change = Uint128::new(50_000_000_000); + let active_params = test.active_node_params(100.0); - let mix_id_repledge = test.add_dummy_mixnode("mix-owner1", Some(pledge1)); + let mix_id_repledge = test.add_rewarded_legacy_mixnode("mix-owner1", Some(pledge1)); test.set_pending_pledge_change(mix_id_repledge, None); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_repledge); @@ -1482,7 +1636,7 @@ mod tests { test.add_immediate_delegation("carol", 111_111_111_000u128, mix_id_repledge); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge]); let mut cumulative_op_reward = Decimal::zero(); let mut cumulative_del_reward = Decimal::zero(); @@ -1490,20 +1644,18 @@ mod tests { // go few epochs of rewarding before decreasing pledge for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); + let dist = + test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); cumulative_op_reward += dist.operator; cumulative_del_reward += dist.delegates; } let decrease = test.coin(pledge_change.u128()); - decrease_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id_repledge, decrease).unwrap(); let pledge3 = Uint128::new(150_000_000_000) + truncate_reward_amount(cumulative_op_reward); - let mix_id_full_pledge = test.add_dummy_mixnode("mix-owner2", Some(pledge3)); + let mix_id_full_pledge = test.add_rewarded_legacy_mixnode("mix-owner2", Some(pledge3)); test.add_immediate_delegation("alice", 123_456_789_000u128, mix_id_full_pledge); test.add_immediate_delegation("bob", 500_000_000_000u128, mix_id_full_pledge); @@ -1533,19 +1685,15 @@ mod tests { ); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); + test.force_change_mix_rewarded_set(vec![mix_id_repledge, mix_id_full_pledge]); // go through few more epochs of rewarding for _ in 0..500 { test.skip_to_next_epoch_end(); - let dist1 = test.reward_with_distribution_with_state_bypass( - mix_id_repledge, - test_helpers::performance(100.0), - ); - let dist2 = test.reward_with_distribution_with_state_bypass( - mix_id_full_pledge, - test_helpers::performance(100.0), - ); + let dist1 = + test.reward_with_distribution_ignore_state(mix_id_repledge, active_params); + let dist2 = + test.reward_with_distribution_ignore_state(mix_id_full_pledge, active_params); assert_eq!(dist1, dist2) } @@ -1554,11 +1702,11 @@ mod tests { #[test] fn updates_the_pending_pledge_changes_field() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.set_pending_pledge_change(mix_id, None); let amount = test.coin(12345); - decrease_pledge(test.deps_mut(), 123, mix_id, amount).unwrap(); + decrease_mixnode_pledge(test.deps_mut(), 123, mix_id, amount).unwrap(); let pending = mixnodes_storage::PENDING_MIXNODE_CHANGES .load(test.deps().storage, mix_id) .unwrap(); @@ -1573,12 +1721,23 @@ mod tests { .load(test.deps().storage) .unwrap(); - update_active_set_size(test.deps_mut(), 123, 50).unwrap(); + update_active_set( + test.deps_mut(), + 123, + ActiveSetUpdate { + entry_gateways: 50, + exit_gateways: 50, + mixnodes: 100, + }, + ) + .unwrap(); let updated = rewards_storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap(); - assert_ne!(current.active_set_size, updated.active_set_size); - assert_eq!(updated.active_set_size, 50) + assert_ne!(updated.rewarded_set, current.rewarded_set); + assert_eq!(updated.rewarded_set.mixnodes, 100); + assert_eq!(updated.rewarded_set.entry_gateways, 50); + assert_eq!(updated.rewarded_set.exit_gateways, 50); } #[cfg(test)] @@ -1591,12 +1750,12 @@ mod tests { #[test] fn doesnt_do_anything_if_mixnode_has_unbonded() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let env = test.env(); unbond_mixnode(test.deps_mut(), &env, 123, mix_id).unwrap(); - let new_params = MixNodeCostParams { + let new_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(42).unwrap(), interval_operating_cost: coin(123_456_789, TEST_COIN_DENOM), }; @@ -1608,11 +1767,16 @@ mod tests { #[test] fn for_bonded_mixnode_updates_saved_value() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let before = test.mix_rewarding(mix_id).cost_params; - let new_params = MixNodeCostParams { + // this would have been normally populated when creating the event itself + mixnodes_storage::PENDING_MIXNODE_CHANGES + .save(test.deps_mut().storage, mix_id, &Default::default()) + .unwrap(); + + let new_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(42).unwrap(), interval_operating_cost: coin(123_456_789, TEST_COIN_DENOM), }; @@ -1620,13 +1784,11 @@ mod tests { let res = change_mix_cost_params(test.deps_mut(), 123, mix_id, new_params.clone()); assert_eq!( res, - Ok( - Response::new().add_event(new_mixnode_cost_params_update_event( - 123, - mix_id, - &new_params, - )) - ) + Ok(Response::new().add_event(new_cost_params_update_event( + 123, + mix_id, + &new_params, + ))) ); let after = test.mix_rewarding(mix_id).cost_params; @@ -1655,7 +1817,7 @@ mod tests { sybil_resistance_percent: Some(Percent::from_percentage_value(42).unwrap()), active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: None, + rewarded_set_params: None, }; let res = update_rewarding_params(test.deps_mut(), 123, update); diff --git a/contracts/mixnet/src/interval/queries.rs b/contracts/mixnet/src/interval/queries.rs index 423d9a90ae..5649d5bb83 100644 --- a/contracts/mixnet/src/interval/queries.rs +++ b/contracts/mixnet/src/interval/queries.rs @@ -4,7 +4,6 @@ use crate::constants::{ EPOCH_EVENTS_DEFAULT_RETRIEVAL_LIMIT, EPOCH_EVENTS_MAX_RETRIEVAL_LIMIT, INTERVAL_EVENTS_DEFAULT_RETRIEVAL_LIMIT, INTERVAL_EVENTS_MAX_RETRIEVAL_LIMIT, - REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT, REWARDED_SET_MAX_RETRIEVAL_LIMIT, }; use crate::interval::storage; use cosmwasm_std::{Deps, Env, Order, StdResult}; @@ -12,9 +11,9 @@ use cw_storage_plus::Bound; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::pending_events::{PendingEpochEvent, PendingIntervalEvent}; use mixnet_contract_common::{ - CurrentIntervalResponse, EpochEventId, EpochStatus, IntervalEventId, MixId, - NumberOfPendingEventsResponse, PagedRewardedSetResponse, PendingEpochEventResponse, - PendingEpochEventsResponse, PendingIntervalEventResponse, PendingIntervalEventsResponse, + CurrentIntervalResponse, EpochEventId, EpochStatus, IntervalEventId, + NumberOfPendingEventsResponse, PendingEpochEventResponse, PendingEpochEventsResponse, + PendingIntervalEventResponse, PendingIntervalEventsResponse, }; pub fn query_epoch_status(deps: Deps<'_>) -> StdResult { @@ -30,30 +29,6 @@ pub fn query_current_interval_details( Ok(CurrentIntervalResponse::new(interval, env)) } -pub fn query_rewarded_set_paged( - deps: Deps<'_>, - start_after: Option, - limit: Option, -) -> StdResult { - let limit = limit - .unwrap_or(REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT) - .min(REWARDED_SET_MAX_RETRIEVAL_LIMIT) as usize; - - let start = start_after.map(Bound::exclusive); - - let nodes = storage::REWARDED_SET - .range(deps.storage, start, None, Order::Ascending) - .take(limit) - .collect::>>()?; - - let start_next_after = nodes.last().map(|node| node.0); - - Ok(PagedRewardedSetResponse { - nodes, - start_next_after, - }) -} - pub fn query_pending_epoch_events_paged( deps: Deps<'_>, env: Env, @@ -174,7 +149,7 @@ mod tests { fn push_dummy_interval_action(test: &mut TestSetup) { let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { mix_id: test.rng.next_u32(), - new_costs: fixtures::mix_node_cost_params_fixture(), + new_costs: fixtures::node_cost_params_fixture(), }; let env = test.env(); storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap(); @@ -214,95 +189,6 @@ mod tests { assert_eq!(res.current_blocktime, env.block.time.seconds()); } - #[cfg(test)] - mod rewarded_set { - use super::*; - - fn set_rewarded_set_to_n_nodes(test: &mut TestSetup, n: usize) { - let set = (1u32..).take(n).collect::>(); - test.force_change_rewarded_set(set) - } - - #[test] - fn obeys_limits() { - let mut test = TestSetup::new(); - set_rewarded_set_to_n_nodes(&mut test, 200); - - let limit = 2; - let page1 = query_rewarded_set_paged(test.deps(), None, Some(limit)).unwrap(); - assert_eq!(limit, page1.nodes.len() as u32); - } - - #[test] - fn has_default_limit() { - let mut test = TestSetup::new(); - set_rewarded_set_to_n_nodes(&mut test, 2000); - - // query without explicitly setting a limit - let page1 = query_rewarded_set_paged(test.deps(), None, None).unwrap(); - - assert_eq!( - REWARDED_SET_DEFAULT_RETRIEVAL_LIMIT, - page1.nodes.len() as u32 - ); - } - - #[test] - fn has_max_limit() { - let mut test = TestSetup::new(); - set_rewarded_set_to_n_nodes(&mut test, 2000); - - // query with a crazily high limit in an attempt to use too many resources - let crazy_limit = 10000; - let page1 = query_rewarded_set_paged(test.deps(), None, Some(crazy_limit)).unwrap(); - - assert_eq!(REWARDED_SET_MAX_RETRIEVAL_LIMIT, page1.nodes.len() as u32); - } - - #[test] - fn pagination_works() { - let mut test = TestSetup::new(); - - set_rewarded_set_to_n_nodes(&mut test, 1); - - let per_page = 2; - let page1 = query_rewarded_set_paged(test.deps(), None, Some(per_page)).unwrap(); - - // page should have 1 result on it - assert_eq!(1, page1.nodes.len()); - - set_rewarded_set_to_n_nodes(&mut test, 2); - - // page1 should have 2 results on it - let page1 = query_rewarded_set_paged(test.deps(), None, Some(per_page)).unwrap(); - assert_eq!(2, page1.nodes.len()); - - set_rewarded_set_to_n_nodes(&mut test, 3); - - // page1 still has the same 2 results - let another_page1 = - query_rewarded_set_paged(test.deps(), None, Some(per_page)).unwrap(); - assert_eq!(2, another_page1.nodes.len()); - assert_eq!(page1, another_page1); - - // retrieving the next page should start after the last key on this page - let start_after = page1.start_next_after.unwrap(); - let page2 = - query_rewarded_set_paged(test.deps(), Some(start_after), Some(per_page)).unwrap(); - - assert_eq!(1, page2.nodes.len()); - - // save another one - set_rewarded_set_to_n_nodes(&mut test, 4); - - let page2 = - query_rewarded_set_paged(test.deps(), Some(start_after), Some(per_page)).unwrap(); - - // now we have 2 pages, with 2 results on the second page - assert_eq!(2, page2.nodes.len()); - } - } - #[cfg(test)] mod pending_epoch_events { use super::*; @@ -605,7 +491,7 @@ mod tests { // it exists let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { mix_id: test.rng.next_u32(), - new_costs: fixtures::mix_node_cost_params_fixture(), + new_costs: fixtures::node_cost_params_fixture(), }; let env = test.env(); storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action.clone()) diff --git a/contracts/mixnet/src/interval/storage.rs b/contracts/mixnet/src/interval/storage.rs index 33eddd4d4e..2a359cd43f 100644 --- a/contracts/mixnet/src/interval/storage.rs +++ b/contracts/mixnet/src/interval/storage.rs @@ -4,22 +4,19 @@ use crate::constants::{ CURRENT_EPOCH_STATUS_KEY, CURRENT_INTERVAL_KEY, EPOCH_EVENT_ID_COUNTER_KEY, INTERVAL_EVENT_ID_COUNTER_KEY, LAST_EPOCH_EVENT_ID_KEY, LAST_INTERVAL_EVENT_ID_KEY, - PENDING_EPOCH_EVENTS_NAMESPACE, PENDING_INTERVAL_EVENTS_NAMESPACE, REWARDED_SET_KEY, + PENDING_EPOCH_EVENTS_NAMESPACE, PENDING_INTERVAL_EVENTS_NAMESPACE, }; -use cosmwasm_std::{Addr, Env, Order, StdResult, Storage}; +use cosmwasm_std::{Addr, Env, StdResult, Storage}; use cw_storage_plus::{Item, Map}; use mixnet_contract_common::pending_events::{ PendingEpochEventData, PendingEpochEventKind, PendingIntervalEventData, }; use mixnet_contract_common::{ - EpochEventId, EpochStatus, Interval, IntervalEventId, MixId, PendingIntervalEventKind, - RewardedSetNodeStatus, + EpochEventId, EpochStatus, Interval, IntervalEventId, PendingIntervalEventKind, }; -use std::collections::HashMap; pub(crate) const CURRENT_EPOCH_STATUS: Item<'_, EpochStatus> = Item::new(CURRENT_EPOCH_STATUS_KEY); pub(crate) const CURRENT_INTERVAL: Item<'_, Interval> = Item::new(CURRENT_INTERVAL_KEY); -pub(crate) const REWARDED_SET: Map = Map::new(REWARDED_SET_KEY); pub(crate) const EPOCH_EVENT_ID_COUNTER: Item = Item::new(EPOCH_EVENT_ID_COUNTER_KEY); pub(crate) const INTERVAL_EVENT_ID_COUNTER: Item = @@ -111,49 +108,6 @@ pub(crate) fn push_new_interval_event( Ok(event_id) } -pub(crate) fn update_rewarded_set( - storage: &mut dyn Storage, - active_set_size: u32, - new_set: Vec, -) -> StdResult<()> { - // our goal is to reduce the number of reads and writes to the underlying storage, - // whilst completely overwriting the current rewarded set. - // the naive implementation would be to read the entire current rewarded set, - // remove all of those entries - // and write the new one in its place. - // However, very often it might turn out that a node hasn't changed its status in the updated epoch, - // and in those cases we can save on having to remove the entry and writing a new one. - - // Note: so far it seems the contract compiles (and stores) fine with a `HashMap`, but if we ever - // run into any issues due to any randomness? we can switch it up for a BTreeMap - let mut old_nodes = REWARDED_SET - .range(storage, None, None, Order::Ascending) - .collect::, _>>()?; - - for (i, node_id) in new_set.into_iter().enumerate() { - // first k nodes are active - let set_status = if i < active_set_size as usize { - RewardedSetNodeStatus::Active - } else { - RewardedSetNodeStatus::Standby - }; - - if !matches!(old_nodes.get(&node_id), Some(status) if status == &set_status) { - // if the status changed, or didn't exist, write it down: - REWARDED_SET.save(storage, node_id, &set_status)?; - } - - old_nodes.remove(&node_id); - } - - // finally remove the entries for nodes that no longer exist [in the rewarded set] - for old_node_id in old_nodes.keys() { - REWARDED_SET.remove(storage, *old_node_id) - } - - Ok(()) -} - pub(crate) fn initialise_storage( storage: &mut dyn Storage, starting_interval: Interval, @@ -172,50 +126,8 @@ mod tests { use super::*; use crate::support::tests::fixtures; use crate::support::tests::test_helpers::TestSetup; - use cosmwasm_std::testing::mock_dependencies; use rand_chacha::rand_core::RngCore; - fn read_entire_set(storage: &dyn Storage) -> HashMap { - REWARDED_SET - .range(storage, None, None, Order::Ascending) - .map(|r| r.unwrap()) - .collect() - } - - #[test] - fn updating_rewarded_set() { - // just some variables to keep test assertions more concise - let active = &RewardedSetNodeStatus::Active; - let standby = &RewardedSetNodeStatus::Standby; - - let mut deps = mock_dependencies(); - let store = deps.as_mut().storage; - assert!(read_entire_set(store).is_empty()); - - // writing initial rewarded set shouldn't do anything fancy - update_rewarded_set(store, 2, vec![6, 2, 7, 4, 1]).unwrap(); - let current_set = read_entire_set(store); - assert_eq!(current_set.len(), 5); - assert_eq!(active, current_set.get(&6).unwrap()); - assert_eq!(active, current_set.get(&2).unwrap()); - assert_eq!(standby, current_set.get(&7).unwrap()); - assert_eq!(standby, current_set.get(&4).unwrap()); - assert_eq!(standby, current_set.get(&1).unwrap()); - assert!(!current_set.contains_key(&42)); - - update_rewarded_set(store, 2, vec![2, 5, 6, 3, 4]).unwrap(); - let current_set = read_entire_set(store); - assert_eq!(current_set.len(), 5); - assert_eq!(active, current_set.get(&2).unwrap()); - assert_eq!(active, current_set.get(&5).unwrap()); - assert_eq!(standby, current_set.get(&6).unwrap()); - assert_eq!(standby, current_set.get(&3).unwrap()); - assert_eq!(standby, current_set.get(&4).unwrap()); - // those no longer are in the rewarded set - assert!(!current_set.contains_key(&7)); - assert!(!current_set.contains_key(&1)); - } - #[test] fn pushing_new_epoch_event_returns_its_id() { let mut test = TestSetup::new(); @@ -252,7 +164,7 @@ mod tests { for _ in 0..500 { let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { mix_id: test.rng.next_u32(), - new_costs: fixtures::mix_node_cost_params_fixture(), + new_costs: fixtures::node_cost_params_fixture(), }; let id = push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap(); let expected = INTERVAL_EVENT_ID_COUNTER.load(test.deps().storage).unwrap(); @@ -264,7 +176,7 @@ mod tests { for _ in 0..10 { let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { mix_id: test.rng.next_u32(), - new_costs: fixtures::mix_node_cost_params_fixture(), + new_costs: fixtures::node_cost_params_fixture(), }; let id = push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap(); let expected = INTERVAL_EVENT_ID_COUNTER.load(test.deps().storage).unwrap(); diff --git a/contracts/mixnet/src/interval/transactions.rs b/contracts/mixnet/src/interval/transactions.rs index adc8faa9cc..c60e4440c0 100644 --- a/contracts/mixnet/src/interval/transactions.rs +++ b/contracts/mixnet/src/interval/transactions.rs @@ -2,26 +2,26 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::interval::helpers::change_interval_config; +use crate::interval::helpers::{advance_epoch, change_interval_config}; use crate::interval::pending_events::ContractExecutableEvent; use crate::interval::storage::push_new_interval_event; use crate::mixnet_contract_settings::storage::ADMIN; -use crate::mixnodes::transactions::update_mixnode_layer; -use crate::rewards; -use crate::rewards::storage as rewards_storage; +use crate::nodes::storage as nymnodes_storage; +use crate::nodes::storage::{read_rewarded_set_metadata, reset_inactive_metadata}; +use crate::rewards::storage::RewardingStorage; use crate::support::helpers::{ ensure_can_advance_epoch, ensure_epoch_in_progress_state, ensure_is_authorized, }; -use cosmwasm_std::{DepsMut, Env, MessageInfo, Order, Response, Storage}; +use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ - new_advance_epoch_event, new_epoch_transition_start_event, + new_advance_epoch_event, new_assigned_role_event, new_epoch_transition_start_event, new_pending_epoch_events_execution_event, new_pending_interval_config_update_event, new_pending_interval_events_execution_event, new_reconcile_pending_events, }; +use mixnet_contract_common::nym_node::Role; use mixnet_contract_common::pending_events::PendingIntervalEventKind; -use mixnet_contract_common::{EpochState, EpochStatus, LayerAssignment, MixId}; -use std::collections::BTreeSet; +use mixnet_contract_common::{EpochState, EpochStatus, RoleAssignment}; // those two should be called in separate tx (from advancing epoch), // since there might be a lot of events to execute. @@ -176,51 +176,15 @@ pub fn try_reconcile_epoch_events( }; if progress { - current_epoch_status.state = EpochState::AdvancingEpoch; + current_epoch_status.state = EpochState::RoleAssignment { + next: Role::first(), + }; storage::save_current_epoch_status(deps.storage, ¤t_epoch_status)?; } Ok(response) } -fn update_rewarded_set( - storage: &mut dyn Storage, - new_rewarded_set: Vec, - expected_active_set_size: u32, -) -> Result<(), MixnetContractError> { - let reward_params = rewards_storage::REWARDING_PARAMS.load(storage)?; - - // the rewarded set has been determined based off active set size taken from the contract, - // thus the expected value HAS TO match - if expected_active_set_size != reward_params.active_set_size { - return Err(MixnetContractError::UnexpectedActiveSetSize { - received: expected_active_set_size, - expected: reward_params.active_set_size, - }); - } - - if new_rewarded_set.len() as u32 > reward_params.rewarded_set_size { - return Err(MixnetContractError::UnexpectedRewardedSetSize { - received: new_rewarded_set.len() as u32, - expected: reward_params.rewarded_set_size, - }); - } - - // check for duplicates - let mut tmp_set = BTreeSet::new(); - for node_id in &new_rewarded_set { - if !tmp_set.insert(node_id) { - return Err(MixnetContractError::DuplicateRewardedSetNode { mix_id: *node_id }); - } - } - - Ok(storage::update_rewarded_set( - storage, - expected_active_set_size, - new_rewarded_set, - )?) -} - pub fn try_begin_epoch_transition( deps: DepsMut<'_>, env: Env, @@ -231,31 +195,29 @@ pub fn try_begin_epoch_transition( // can't do pre-mature epoch transition... let current_interval = storage::current_interval(deps.storage)?; - if !current_interval.is_current_epoch_over(&env) { - return Err(MixnetContractError::EpochInProgress { - current_block_time: env.block.time.seconds(), - epoch_start: current_interval.current_epoch_start_unix_timestamp(), - epoch_end: current_interval.current_epoch_end_unix_timestamp(), - }); - } + current_interval.ensure_current_epoch_is_over(&env)?; // ensure some other validator (currently not a problem), hasn't already committed to epoch progression ensure_epoch_in_progress_state(deps.storage)?; - // Note: if at any point we decide to change our rewarded set to be few thousand nodes - // and the below call fails, we'll have to pass `last_node_in_rewarded_set` as an argument to this function - // and then verify whether the provided value is valid (by using range iterator on `REWARDED_SET` - // and checking if there are any other entries following the provided value) - let rewarded_set = storage::REWARDED_SET - .range(deps.storage, None, None, Order::Ascending) - .map(|kv| kv.map(|kv| kv.0)) - .collect::, _>>()?; + let metadata = read_rewarded_set_metadata(deps.storage)?; + + // TODO: with pre-announcing rewarded set, this will have to happen elsewhere + reset_inactive_metadata( + deps.storage, + current_interval.current_epoch_absolute_id() + 1, + )?; + + // make sure to reset the submitted work for this epoch (since it's 0 now) + RewardingStorage::load().reset_cumulative_epoch_work(deps.storage)?; + + let final_node_id = metadata.highest_rewarded_id(); // if there are no nodes to reward (i.e. empty rewarded set), we go straight into event reconciliation - let new_epoch_state = if let Some(last) = rewarded_set.last() { + let new_epoch_state = if final_node_id != 0 { EpochState::Rewarding { last_rewarded: 0, - final_node_id: *last, + final_node_id, } } else { EpochState::ReconcilingEvents @@ -271,51 +233,51 @@ pub fn try_begin_epoch_transition( Ok(Response::new().add_event(new_epoch_transition_start_event(current_interval))) } -pub fn try_advance_epoch( +pub fn try_assign_roles( deps: DepsMut<'_>, env: Env, info: MessageInfo, - layer_assignments: Vec, - expected_active_set_size: u32, + assignment: RoleAssignment, ) -> Result { // Only rewarding validator can attempt to advance epoch let mut current_epoch_status = ensure_can_advance_epoch(&info.sender, deps.storage)?; - current_epoch_status.ensure_is_in_advancement_state()?; + current_epoch_status.ensure_is_in_expected_role_assignment_state(assignment.role)?; - // we must make sure that we roll into new epoch / interval with up to date state - // with no pending actions (like somebody wanting to update their profit margin) - let current_interval = storage::current_interval(deps.storage)?; - if !current_interval.is_current_epoch_over(&env) { - return Err(MixnetContractError::EpochInProgress { - current_block_time: env.block.time.seconds(), - epoch_start: current_interval.current_epoch_start_unix_timestamp(), - epoch_end: current_interval.current_epoch_end_unix_timestamp(), - }); - } + let role = assignment.role; + let assigned = assignment.nodes.len() as u32; - // if the current interval is over, apply reward pool changes - if current_interval.is_current_interval_over(&env) { - // this one is a very important one! - rewards::helpers::apply_reward_pool_changes(deps.storage)?; - } + let rewarded_set_params = RewardingStorage::load() + .global_rewarding_params + .load(deps.storage)? + .rewarded_set; - let updated_interval = current_interval.advance_epoch(); - let num_nodes = layer_assignments.len(); + // make sure we're not attempting to assign too many nodes to particular role + rewarded_set_params.ensure_role_count(role, assigned)?; - let new_rewarded_set = layer_assignments.iter().map(|l| l.mix_id()).collect(); + let next = assignment.role.next(); - // finally save updated interval and the rewarded set - storage::save_interval(deps.storage, &updated_interval)?; - update_rewarded_set(deps.storage, new_rewarded_set, expected_active_set_size)?; + // save the nodes for this layer + nymnodes_storage::save_assignment(deps.storage, assignment)?; - for a in layer_assignments { - update_mixnode_layer(a.mix_id(), a.layer(), deps.storage)?; - } + // TODO: optimise: if next is standby and standby set is empty, immediately advance + let event = match next { + Some(next_roles) => { + // update the state for the next assignment + current_epoch_status.state = EpochState::RoleAssignment { next: next_roles }; + new_assigned_role_event(role, assigned) + } + None => { + // the last role has been assigned => we're ready to progress into the next epoch + nymnodes_storage::swap_active_role_bucket(deps.storage)?; + let epoch_id = advance_epoch(deps.storage, env)?; + current_epoch_status.state = EpochState::InProgress; + new_advance_epoch_event(epoch_id) + } + }; - current_epoch_status.state = EpochState::InProgress; storage::save_current_epoch_status(deps.storage, ¤t_epoch_status)?; - Ok(Response::new().add_event(new_advance_epoch_event(updated_interval, num_nodes as u32))) + Ok(Response::new().add_event(event)) } pub(crate) fn try_update_interval_config( @@ -370,10 +332,13 @@ pub(crate) fn try_update_interval_config( #[cfg(test)] mod tests { use super::*; + use crate::mixnodes::storage as mixnodes_storage; + use crate::rewards::storage as rewards_storage; use crate::support::tests::fixtures; use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::Addr; use mixnet_contract_common::pending_events::PendingEpochEventKind; + use mixnet_contract_common::NodeId; fn push_n_dummy_epoch_actions(test: &mut TestSetup, n: usize) { // if you attempt to undelegate non-existent delegation, @@ -381,7 +346,7 @@ mod tests { let env = test.env(); for i in 0..n { let dummy_action = - PendingEpochEventKind::new_undelegate(Addr::unchecked("foomp"), i as MixId); + PendingEpochEventKind::new_undelegate(Addr::unchecked("foomp"), i as NodeId); storage::push_new_epoch_event(test.deps_mut().storage, &env, dummy_action).unwrap(); } } @@ -392,8 +357,8 @@ mod tests { let env = test.env(); for i in 0..n { let dummy_action = PendingIntervalEventKind::ChangeMixCostParams { - mix_id: i as MixId, - new_costs: fixtures::mix_node_cost_params_fixture(), + mix_id: i as NodeId, + new_costs: fixtures::node_cost_params_fixture(), }; storage::push_new_interval_event(test.deps_mut().storage, &env, dummy_action).unwrap(); } @@ -402,7 +367,7 @@ mod tests { #[cfg(test)] mod performing_pending_epoch_actions { use super::*; - use crate::support::tests::fixtures::TEST_COIN_DENOM; + use crate::support::tests::fixtures::{active_set_update_fixture, TEST_COIN_DENOM}; use cosmwasm_std::{coin, coins, BankMsg, Empty, SubMsg}; use mixnet_contract_common::events::{ new_active_set_update_event, new_delegation_on_unbonded_node_event, @@ -470,7 +435,9 @@ mod tests { ); push_n_dummy_epoch_actions(&mut test, 10); - let action_with_event = PendingEpochEventKind::UpdateActiveSetSize { new_size: 50 }; + let action_with_event = PendingEpochEventKind::UpdateActiveSet { + update: active_set_update_fixture(), + }; storage::push_new_epoch_event(test.deps_mut().storage, &env, action_with_event) .unwrap(); push_n_dummy_epoch_actions(&mut test, 10); @@ -478,7 +445,10 @@ mod tests { perform_pending_epoch_actions(test.deps_mut(), &env, None).unwrap(); assert_eq!( res, - Response::new().add_event(new_active_set_update_event(env.block.height, 50)) + Response::new().add_event(new_active_set_update_event( + env.block.height, + active_set_update_fixture() + )) ); assert_eq!(executed, 21); assert_eq!( @@ -494,7 +464,7 @@ mod tests { let mut test = TestSetup::new(); let env = test.env(); - let legit_mix = test.add_dummy_mixnode("mix-owner", None); + let legit_mix = test.add_legacy_mixnode("mix-owner", None); let delegator = Addr::unchecked("delegator"); let amount = 123_456_789u128; test.add_immediate_delegation(delegator.as_str(), amount, legit_mix); @@ -522,10 +492,15 @@ mod tests { })); // updating active set should only emit events and no cosmos messages - let action_with_event = PendingEpochEventKind::UpdateActiveSetSize { new_size: 50 }; + let action_with_event = PendingEpochEventKind::UpdateActiveSet { + update: active_set_update_fixture(), + }; storage::push_new_epoch_event(test.deps_mut().storage, &env, action_with_event) .unwrap(); - expected_events.push(new_active_set_update_event(env.block.height, 50)); + expected_events.push(new_active_set_update_event( + env.block.height, + active_set_update_fixture(), + )); // undelegation just returns tokens and emits event let legit_undelegate = @@ -613,10 +588,10 @@ mod tests { use crate::support::tests::fixtures::TEST_COIN_DENOM; use cosmwasm_std::{coin, Empty, SubMsg}; use mixnet_contract_common::events::{ - new_interval_config_update_event, new_mixnode_cost_params_update_event, + new_cost_params_update_event, new_interval_config_update_event, new_rewarding_params_update_event, }; - use mixnet_contract_common::mixnode::MixNodeCostParams; + use mixnet_contract_common::mixnode::NodeCostParams; use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; use mixnet_contract_common::Percent; @@ -682,7 +657,7 @@ mod tests { push_n_dummy_interval_actions(&mut test, 10); let update = IntervalRewardingParamsUpdate { - rewarded_set_size: Some(500), + interval_pool_emission: Some(Percent::from_percentage_value(42).unwrap()), ..Default::default() }; let action_with_event = PendingIntervalEventKind::UpdateRewardingParams { update }; @@ -717,25 +692,30 @@ mod tests { let mut expected_events = Vec::new(); let expected_messages: Vec> = Vec::new(); - let legit_mix = test.add_dummy_mixnode("mix-owner", None); - let new_costs = MixNodeCostParams { + let legit_mix = test.add_legacy_mixnode("mix-owner", None); + let new_costs = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(12).unwrap(), interval_operating_cost: coin(123_000, TEST_COIN_DENOM), }; + // this would have been normally populated when creating the event itself + mixnodes_storage::PENDING_MIXNODE_CHANGES + .save(test.deps_mut().storage, legit_mix, &Default::default()) + .unwrap(); + let cost_change = PendingIntervalEventKind::ChangeMixCostParams { mix_id: legit_mix, new_costs: new_costs.clone(), }; storage::push_new_interval_event(test.deps_mut().storage, &env, cost_change).unwrap(); - expected_events.push(new_mixnode_cost_params_update_event( + expected_events.push(new_cost_params_update_event( env.block.height, legit_mix, &new_costs, )); let update = IntervalRewardingParamsUpdate { - rewarded_set_size: Some(500), + interval_pool_emission: Some(Percent::from_percentage_value(42).unwrap()), ..Default::default() }; let change_params = PendingIntervalEventKind::UpdateRewardingParams { update }; @@ -858,7 +838,9 @@ mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -942,7 +924,7 @@ mod tests { let mut test = TestSetup::new(); let rewarding_validator = test.rewarding_validator(); - test.force_change_rewarded_set(vec![1, 2, 3, 4, 5]); + test.force_change_mix_rewarded_set(vec![1, 2, 3, 4, 5]); test.skip_to_current_epoch_end(); let env = test.env(); @@ -972,6 +954,7 @@ mod tests { new_delegation_on_unbonded_node_event, new_rewarding_params_update_event, }; use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; + use nym_contracts_common::Percent; #[test] fn can_only_be_performed_if_in_reconciling_state() { @@ -981,7 +964,9 @@ mod tests { last_rewarded: 0, final_node_id: 0, }, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -1019,7 +1004,9 @@ mod tests { let expected = EpochStatus { being_advanced_by: test.rewarding_validator().sender, - state: EpochState::AdvancingEpoch, + state: EpochState::RoleAssignment { + next: Role::first(), + }, }; assert_eq!( expected, @@ -1068,7 +1055,9 @@ mod tests { let expected = EpochStatus { being_advanced_by: test.rewarding_validator().sender, - state: EpochState::AdvancingEpoch, + state: EpochState::RoleAssignment { + next: Role::first(), + }, }; assert_eq!( expected, @@ -1092,7 +1081,9 @@ mod tests { let expected = EpochStatus { being_advanced_by: test.rewarding_validator().sender, - state: EpochState::AdvancingEpoch, + state: EpochState::RoleAssignment { + next: Role::first(), + }, }; assert_eq!( expected, @@ -1312,7 +1303,7 @@ mod tests { // interval event let update = IntervalRewardingParamsUpdate { - rewarded_set_size: Some(500), + interval_pool_emission: Some(Percent::from_percentage_value(42).unwrap()), ..Default::default() }; let change_params = PendingIntervalEventKind::UpdateRewardingParams { update }; @@ -1354,89 +1345,24 @@ mod tests { } } - #[test] - fn updating_rewarded_set() { - // the actual logic behind writing stuff to the storage has been tested in - // different unit test - let mut test = TestSetup::new(); - let current_active_set = test.rewarding_params().active_set_size; - let current_rewarded_set = test.rewarding_params().rewarded_set_size; - - // active set size has to match the expectation - let err = update_rewarded_set( - test.deps_mut().storage, - vec![1, 2, 3], - current_active_set - 10, - ) - .unwrap_err(); - assert_eq!( - err, - MixnetContractError::UnexpectedActiveSetSize { - received: current_active_set - 10, - expected: current_active_set, - } - ); - - // number of nodes provided has to be equal or smaller than the current rewarded set size - - // fewer nodes - let res = update_rewarded_set(test.deps_mut().storage, vec![1, 2, 3], current_active_set); - assert!(res.is_ok()); - - let exact_num = (1u32..) - .take(current_rewarded_set as usize) - .collect::>(); - let res = update_rewarded_set(test.deps_mut().storage, exact_num, current_active_set); - assert!(res.is_ok()); - - // one more - let too_many = (1u32..) - .take((current_rewarded_set + 1) as usize) - .collect::>(); - let err = - update_rewarded_set(test.deps_mut().storage, too_many, current_active_set).unwrap_err(); - assert_eq!( - err, - MixnetContractError::UnexpectedRewardedSetSize { - received: current_rewarded_set + 1, - expected: current_rewarded_set, - } - ); - - // doesn't allow for duplicates - let nodes_with_duplicate = vec![1, 2, 3, 4, 5, 1]; - let err = update_rewarded_set( - test.deps_mut().storage, - nodes_with_duplicate, - current_active_set, - ) - .unwrap_err(); - assert_eq!( - err, - MixnetContractError::DuplicateRewardedSetNode { mix_id: 1 } - ); - let nodes_with_duplicate = vec![1, 2, 3, 5, 4, 5]; - let err = update_rewarded_set( - test.deps_mut().storage, - nodes_with_duplicate, - current_active_set, - ) - .unwrap_err(); - assert_eq!( - err, - MixnetContractError::DuplicateRewardedSetNode { mix_id: 5 } - ); - } - #[cfg(test)] - mod advancing_epoch { + mod assigning_roles { use super::*; - use crate::mixnodes::queries::query_mixnode_details; - use crate::rewards::models::RewardPoolChange; use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{Decimal, Uint128}; - use mixnet_contract_common::reward_params::IntervalRewardingParamsUpdate; - use mixnet_contract_common::{Layer, RewardedSetNodeStatus}; + use cosmwasm_std::Uint128; + + fn setup_test() -> TestSetup { + let mut test = TestSetup::new(); + + for i in 0..10 { + test.add_dummy_nymnode(&format!("node-owner-{i}"), None); + } + + test.skip_to_current_epoch_end(); + test.set_epoch_role_assignment_state(); + + test + } #[test] fn can_only_be_performed_if_in_advancing_epoch_state() { @@ -1451,10 +1377,9 @@ mod tests { for bad_state in bad_states { let mut test = TestSetup::new(); - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); - let current_active_set = test.rewarding_params().active_set_size; + test.add_legacy_mixnode("1", Some(Uint128::new(100000000))); + test.add_legacy_mixnode("2", Some(Uint128::new(100000000))); + test.add_legacy_mixnode("3", Some(Uint128::new(100000000))); test.skip_to_current_epoch_end(); @@ -1462,24 +1387,17 @@ mod tests { status.state = bad_state; storage::save_current_epoch_status(test.deps_mut().storage, &status).unwrap(); - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), - ]; + let role_assignment = RoleAssignment { + role: Role::Layer1, + nodes: vec![1, 2, 3], + }; let env = test.env(); let sender = test.rewarding_validator(); - let res = try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ); + let res = try_assign_roles(test.deps_mut(), env, sender, role_assignment); assert_eq!( res, - Err(MixnetContractError::EpochNotInAdvancementState { + Err(MixnetContractError::EpochNotInRoleAssignmentState { current_state: bad_state }) ); @@ -1489,276 +1407,381 @@ mod tests { #[test] fn epoch_state_is_correctly_updated() { let mut test = TestSetup::new(); - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); - let current_active_set = test.rewarding_params().active_set_size; - test.skip_to_current_epoch_end(); - test.set_epoch_advancement_state(); + test.set_epoch_role_assignment_state(); - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), + let cases = vec![ + ( + RoleAssignment { + role: Role::ExitGateway, + nodes: vec![1, 2, 3], + }, + EpochState::RoleAssignment { + next: Role::EntryGateway, + }, + ), + ( + RoleAssignment { + role: Role::EntryGateway, + nodes: vec![4, 5, 6], + }, + EpochState::RoleAssignment { next: Role::Layer1 }, + ), + ( + RoleAssignment { + role: Role::Layer1, + nodes: vec![7, 8, 9], + }, + EpochState::RoleAssignment { next: Role::Layer2 }, + ), + ( + RoleAssignment { + role: Role::Layer2, + nodes: vec![9, 10, 11], + }, + EpochState::RoleAssignment { next: Role::Layer3 }, + ), + ( + RoleAssignment { + role: Role::Layer3, + nodes: vec![12], + }, + EpochState::RoleAssignment { + next: Role::Standby, + }, + ), + ( + RoleAssignment { + role: Role::Standby, + nodes: vec![42], + }, + EpochState::InProgress, + ), ]; - let env = test.env(); - let sender = test.rewarding_validator(); - try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ) - .unwrap(); + for (assignment, expected) in cases { + let env = test.env(); + let sender = test.rewarding_validator(); + try_assign_roles(test.deps_mut(), env, sender, assignment).unwrap(); - let expected = EpochStatus { - being_advanced_by: test.rewarding_validator().sender, - state: EpochState::InProgress, - }; - assert_eq!( - expected, - storage::current_epoch_status(test.deps().storage).unwrap() - ) + let expected = EpochStatus { + being_advanced_by: test.rewarding_validator().sender, + state: expected, + }; + assert_eq!( + expected, + storage::current_epoch_status(test.deps().storage).unwrap() + ); + } } #[test] fn can_only_be_performed_by_specified_rewarding_validator() { let mut test = TestSetup::new(); - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); - let current_active_set = test.rewarding_params().active_set_size; + test.add_dummy_nymnode("1", Some(Uint128::new(100000000))); + test.add_dummy_nymnode("2", Some(Uint128::new(100000000))); + test.add_dummy_nymnode("3", Some(Uint128::new(100000000))); let some_sender = mock_info("foomper", &[]); test.skip_to_current_epoch_end(); - test.set_epoch_advancement_state(); + test.set_epoch_role_assignment_state(); - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), - ]; + let role_assignment = RoleAssignment { + role: Role::first(), + nodes: vec![1, 2, 3], + }; let env = test.env(); - let res = try_advance_epoch( - test.deps_mut(), - env, - some_sender, - layer_assignments.clone(), - current_active_set, - ); + let res = try_assign_roles(test.deps_mut(), env, some_sender, role_assignment.clone()); assert_eq!(res, Err(MixnetContractError::Unauthorized)); // good address (sanity check) let env = test.env(); let sender = test.rewarding_validator(); - let res = try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ); + let res = try_assign_roles(test.deps_mut(), env, sender, role_assignment); assert!(res.is_ok()) } #[test] - fn can_only_be_performed_if_epoch_is_over() { - let mut test = TestSetup::new(); - test.set_epoch_advancement_state(); - - let current_active_set = test.rewarding_params().active_set_size; + fn has_maximum_nodes_per_role() -> anyhow::Result<()> { + fn nodes_vec(start: NodeId, count: u32) -> Vec { + (start..start + count).collect() + } - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); + let mut test = setup_test(); - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), + let roles = [ + Role::ExitGateway, + Role::EntryGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, ]; let env = test.env(); let sender = test.rewarding_validator(); - let res = try_advance_epoch( - test.deps_mut(), - env, - sender.clone(), - layer_assignments.clone(), - current_active_set, - ); - assert!(matches!( - res, - Err(MixnetContractError::EpochInProgress { .. }) - )); - let mixnode_1 = query_mixnode_details(test.deps.as_ref(), 1).unwrap(); - assert_eq!( - mixnode_1.mixnode_details.unwrap().bond_information.layer, - Layer::One - ); + for role in roles { + let max_count = test.max_role_count(role); - let mixnode_1 = query_mixnode_details(test.deps.as_ref(), 2).unwrap(); - assert_eq!( - mixnode_1.mixnode_details.unwrap().bond_information.layer, - Layer::Two - ); - - let mixnode_1 = query_mixnode_details(test.deps.as_ref(), 3).unwrap(); - assert_eq!( - mixnode_1.mixnode_details.unwrap().bond_information.layer, - Layer::Three - ); + let res = try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role, + nodes: nodes_vec(1, max_count + 1), + }, + ); + assert_eq!( + res.unwrap_err(), + MixnetContractError::IllegalRoleCount { + role, + assigned: max_count + 1, + allowed: max_count, + } + ); - // sanity check - test.skip_to_current_epoch_end(); + let res = try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role, + nodes: nodes_vec(1, max_count), + }, + ); + assert!(res.is_ok()); + } - let env = test.env(); - let res = try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ); - assert!(res.is_ok()) + Ok(()) } #[test] - fn if_interval_is_over_applies_reward_pool_changes() { - let mut test = TestSetup::new(); - test.set_epoch_advancement_state(); + fn cant_be_performed_out_of_order() -> anyhow::Result<()> { + let mut test = setup_test(); - let current_active_set = test.rewarding_params().active_set_size; + let env = test.env(); + let sender = test.rewarding_validator(); - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); + let expected_order = [ + Role::ExitGateway, + Role::EntryGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, + ]; - let start_params = test.rewarding_params(); + for (i, role) in expected_order.iter().enumerate() { + let wrong_role = if role == &Role::Layer1 { + Role::Layer2 + } else { + Role::Layer1 + }; - let pool_update = Decimal::from_atomics(100_000_000u32, 0).unwrap(); - // push some changes - rewards_storage::PENDING_REWARD_POOL_CHANGE - .save( - test.deps_mut().storage, - &RewardPoolChange { - removed: pool_update, - added: Default::default(), + let res = try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role: wrong_role, + nodes: vec![i as u32], }, - ) - .unwrap(); + ); + assert_eq!( + res.unwrap_err(), + MixnetContractError::UnexpectedRoleAssignment { + expected: *role, + got: wrong_role + } + ); - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), - ]; + let res = try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role: *role, + nodes: vec![i as u32], + }, + ); + assert!(res.is_ok()); + } - // end of epoch - nothing has happened - let sender = test.rewarding_validator(); - test.skip_to_current_epoch_end(); + Ok(()) + } - let env = test.env(); - try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments.clone(), - current_active_set, - ) - .unwrap(); + #[cfg(test)] + mod correctly_updates_storage { + use super::*; + use mixnet_contract_common::nym_node::RoleMetadata; - let params = test.rewarding_params(); - let pool_change = rewards_storage::PENDING_REWARD_POOL_CHANGE - .load(test.deps().storage) - .unwrap(); - assert_eq!(params, start_params); - assert_eq!(pool_change.removed, pool_update); + fn perform_partial_assignment(test: &mut TestSetup) -> anyhow::Result<()> { + let env = test.env(); + let sender = test.rewarding_validator(); + try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role: Role::ExitGateway, + nodes: vec![1, 2, 3], + }, + )?; - let sender = test.rewarding_validator(); - test.skip_to_current_interval_end(); - test.set_epoch_advancement_state(); + try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role: Role::EntryGateway, + nodes: vec![4, 5, 6], + }, + )?; - let env = test.env(); - try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ) - .unwrap(); + try_assign_roles( + test.deps_mut(), + env, + sender, + RoleAssignment { + role: Role::Layer1, + nodes: vec![7, 8], + }, + )?; - let epochs_in_interval = test.current_interval().epochs_in_interval(); - let update = IntervalRewardingParamsUpdate { - reward_pool: Some(start_params.interval.reward_pool - pool_update), - staking_supply: Some(start_params.interval.staking_supply + pool_update), - ..Default::default() - }; - let mut expected = start_params; - expected - .try_apply_updates(update, epochs_in_interval) - .unwrap(); + Ok(()) + } - let params = test.rewarding_params(); - let pool_change = rewards_storage::PENDING_REWARD_POOL_CHANGE - .load(test.deps().storage) - .unwrap(); - assert_eq!(params, expected); - assert_eq!(pool_change.removed, Decimal::zero()); - } + #[test] + fn updates_metadata() -> anyhow::Result<()> { + let mut test = setup_test(); - #[test] - fn updates_rewarded_set_and_interval_data() { - let mut test = TestSetup::new(); - test.set_epoch_advancement_state(); + let initial = test.inactive_roles_metadata(); - let current_active_set = test.rewarding_params().active_set_size; + // initial state + let empty = RoleMetadata::default(); + assert_eq!(empty, initial.entry_gateway_metadata); + assert_eq!(empty, initial.layer1_metadata); + assert_eq!(empty, initial.layer2_metadata); + assert_eq!(empty, initial.layer3_metadata); + assert_eq!(empty, initial.exit_gateway_metadata); + assert_eq!(empty, initial.standby_metadata); - test.add_dummy_mixnode("1", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("2", Some(Uint128::new(100000000))); - test.add_dummy_mixnode("3", Some(Uint128::new(100000000))); + perform_partial_assignment(&mut test)?; - let interval_pre = test.current_interval(); - let rewarded_set_pre = test.rewarded_set(); - assert!(rewarded_set_pre.is_empty()); + let updated = test.inactive_roles_metadata(); + assert_eq!(3, updated.exit_gateway_metadata.highest_id); + assert_eq!(3, updated.exit_gateway_metadata.num_nodes); + assert_eq!(6, updated.entry_gateway_metadata.highest_id); + assert_eq!(3, updated.entry_gateway_metadata.num_nodes); + assert_eq!(8, updated.layer1_metadata.highest_id); + assert_eq!(2, updated.layer1_metadata.num_nodes); - let layer_assignments = vec![ - LayerAssignment::new(1, Layer::One), - LayerAssignment::new(2, Layer::Two), - LayerAssignment::new(3, Layer::Three), - ]; + assert_eq!(empty, updated.layer2_metadata); + assert_eq!(empty, updated.layer3_metadata); + assert_eq!(empty, updated.standby_metadata); - let sender = test.rewarding_validator(); - test.skip_to_current_interval_end(); - let env = test.env(); - try_advance_epoch( - test.deps_mut(), - env, - sender, - layer_assignments, - current_active_set, - ) - .unwrap(); + Ok(()) + } - let interval_post = test.current_interval(); - let rewarded_set = test.rewarded_set(); + #[test] + fn updates_role_data() -> anyhow::Result<()> { + let mut test = setup_test(); - let expected_id = interval_pre.current_epoch_absolute_id() + 1; - assert_eq!(interval_post.current_epoch_absolute_id(), expected_id); - assert_eq!( - rewarded_set, - vec![ - (1, RewardedSetNodeStatus::Active), - (2, RewardedSetNodeStatus::Active), - (3, RewardedSetNodeStatus::Active) - ] - ); + assert!(test.inactive_roles(Role::ExitGateway).is_empty()); + assert!(test.inactive_roles(Role::EntryGateway).is_empty()); + assert!(test.inactive_roles(Role::Layer1).is_empty()); + assert!(test.inactive_roles(Role::Layer2).is_empty()); + assert!(test.inactive_roles(Role::Layer3).is_empty()); + assert!(test.inactive_roles(Role::Standby).is_empty()); + + perform_partial_assignment(&mut test)?; + + assert_eq!(3, test.inactive_roles(Role::ExitGateway).len()); + assert_eq!(3, test.inactive_roles(Role::EntryGateway).len()); + assert_eq!(2, test.inactive_roles(Role::Layer1).len()); + assert!(test.inactive_roles(Role::Layer2).is_empty()); + assert!(test.inactive_roles(Role::Layer3).is_empty()); + assert!(test.inactive_roles(Role::Standby).is_empty()); + + Ok(()) + } + + #[test] + fn updates_epoch_status() -> anyhow::Result<()> { + let mut test = setup_test(); + + let env = test.env(); + let sender = test.rewarding_validator(); + + let roles = [ + Role::ExitGateway, + Role::EntryGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, + ]; + + for (i, role) in roles.into_iter().enumerate() { + let expected_next = role.next(); + + try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role, + nodes: vec![i as u32], + }, + )?; + + let state = test.epoch_state(); + match expected_next { + None => assert_eq!(state, EpochState::InProgress), + Some(next) => assert_eq!(state, EpochState::RoleAssignment { next }), + } + } + + Ok(()) + } + + #[test] + fn swaps_roles_buckets_after_final_role() -> anyhow::Result<()> { + let mut test = setup_test(); + + let env = test.env(); + let sender = test.rewarding_validator(); + + let active = test.active_roles_bucket(); + + let roles = [ + Role::ExitGateway, + Role::EntryGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, + ]; + + for (i, role) in roles.into_iter().enumerate() { + try_assign_roles( + test.deps_mut(), + env.clone(), + sender.clone(), + RoleAssignment { + role, + nodes: vec![i as u32], + }, + )?; + } + + assert_eq!(test.active_roles_bucket(), active.other()); + + Ok(()) + } } } @@ -1778,7 +1801,9 @@ mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { diff --git a/contracts/mixnet/src/lib.rs b/contracts/mixnet/src/lib.rs index 985cffda57..8e1450421d 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -1,17 +1,19 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 #![warn(clippy::expect_used)] #![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] -mod constants; +pub(crate) mod compat; +pub mod constants; pub mod contract; mod delegations; -mod families; mod gateways; mod interval; mod mixnet_contract_settings; mod mixnodes; +mod nodes; mod queued_migrations; mod rewards; pub mod signing; diff --git a/contracts/mixnet/src/mixnet_contract_settings/queries.rs b/contracts/mixnet/src/mixnet_contract_settings/queries.rs index 5d80fec78a..415704d04a 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/queries.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/queries.rs @@ -49,9 +49,8 @@ pub(crate) mod tests { vesting_contract_address: Addr::unchecked("foomp"), rewarding_denom: "unym".to_string(), params: ContractStateParams { - minimum_mixnode_delegation: None, - minimum_mixnode_pledge: coin(123u128, "unym"), - minimum_gateway_pledge: coin(456u128, "unym"), + minimum_delegation: None, + minimum_pledge: coin(123u128, "unym"), profit_margin: Default::default(), interval_operating_cost: Default::default(), }, diff --git a/contracts/mixnet/src/mixnet_contract_settings/storage.rs b/contracts/mixnet/src/mixnet_contract_settings/storage.rs index 0c8502089e..f9024673f2 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/storage.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/storage.rs @@ -7,7 +7,9 @@ use cosmwasm_std::{Coin, StdResult}; use cw_controllers::Admin; use cw_storage_plus::Item; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::{ContractState, OperatingCostRange, ProfitMarginRange}; +use mixnet_contract_common::{ + ContractState, ContractStateParams, OperatingCostRange, ProfitMarginRange, +}; pub(crate) const CONTRACT_STATE: Item<'_, ContractState> = Item::new(CONTRACT_STATE_KEY); pub(crate) const ADMIN: Admin = Admin::new(ADMIN_STORAGE_KEY); @@ -18,16 +20,10 @@ pub fn rewarding_validator_address(storage: &dyn Storage) -> Result Result { +pub(crate) fn minimum_node_pledge(storage: &dyn Storage) -> Result { Ok(CONTRACT_STATE .load(storage) - .map(|state| state.params.minimum_mixnode_pledge)?) -} - -pub(crate) fn minimum_gateway_pledge(storage: &dyn Storage) -> Result { - Ok(CONTRACT_STATE - .load(storage) - .map(|state| state.params.minimum_gateway_pledge)?) + .map(|state| state.params.minimum_pledge)?) } pub(crate) fn profit_margin_range( @@ -38,7 +34,7 @@ pub(crate) fn profit_margin_range( .map(|state| state.params.profit_margin)?) } -pub(crate) fn interval_oprating_cost_range( +pub(crate) fn interval_operating_cost_range( storage: &dyn Storage, ) -> Result { Ok(CONTRACT_STATE @@ -52,7 +48,7 @@ pub(crate) fn minimum_delegation_stake( ) -> Result, MixnetContractError> { Ok(CONTRACT_STATE .load(storage) - .map(|state| state.params.minimum_mixnode_delegation)?) + .map(|state| state.params.minimum_delegation)?) } pub(crate) fn rewarding_denom(storage: &dyn Storage) -> Result { @@ -67,6 +63,12 @@ pub(crate) fn vesting_contract_address(storage: &dyn Storage) -> Result Result { + Ok(CONTRACT_STATE.load(storage).map(|state| state.params)?) +} + pub(crate) fn initialise_storage( deps: DepsMut<'_>, initial_state: ContractState, diff --git a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs index b458f6fbbc..47199ce605 100644 --- a/contracts/mixnet/src/mixnet_contract_settings/transactions.rs +++ b/contracts/mixnet/src/mixnet_contract_settings/transactions.rs @@ -75,7 +75,6 @@ pub(crate) fn try_update_contract_settings( #[cfg(test)] pub mod tests { use super::*; - use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMOUNT}; use crate::mixnet_contract_settings::queries::query_rewarding_validator_address; use crate::mixnet_contract_settings::storage::rewarding_denom; use crate::support::tests::test_helpers; @@ -84,7 +83,7 @@ pub mod tests { use cw_controllers::AdminError::NotAdmin; #[test] - fn update_contract_rewarding_validtor_address() { + fn update_contract_rewarding_validator_address() { let mut deps = test_helpers::init_contract(); let info = mock_info("not-the-creator", &[]); @@ -129,14 +128,10 @@ pub mod tests { let denom = rewarding_denom(deps.as_ref().storage).unwrap(); let new_params = ContractStateParams { - minimum_mixnode_delegation: None, - minimum_mixnode_pledge: Coin { + minimum_delegation: None, + minimum_pledge: Coin { denom: denom.clone(), - amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, - }, - minimum_gateway_pledge: Coin { - denom, - amount: INITIAL_GATEWAY_PLEDGE_AMOUNT + Uint128::new(1234), + amount: Uint128::new(12345), }, profit_margin: Default::default(), interval_operating_cost: Default::default(), diff --git a/contracts/mixnet/src/mixnodes/helpers.rs b/contracts/mixnet/src/mixnodes/helpers.rs index b8dd15ae6e..ef78ffb741 100644 --- a/contracts/mixnet/src/mixnodes/helpers.rs +++ b/contracts/mixnet/src/mixnodes/helpers.rs @@ -2,15 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; -use crate::interval::storage as interval_storage; -use crate::mixnodes::storage::{assign_layer, next_mixnode_id_counter}; use crate::rewards::storage as rewards_storage; -use cosmwasm_std::{Addr, Coin, Decimal, Env, StdResult, Storage}; +use cosmwasm_std::{Addr, Decimal, Env, StdResult, Storage}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::mixnode::{ - MixNodeCostParams, MixNodeDetails, MixNodeRewarding, UnbondedMixnode, -}; -use mixnet_contract_common::{IdentityKey, Layer, MixId, MixNode, MixNodeBond}; +use mixnet_contract_common::mixnode::{MixNodeDetails, UnbondedMixnode}; +use mixnet_contract_common::{IdentityKey, MixNodeBond, NodeId}; pub(crate) fn must_get_mixnode_bond_by_owner( store: &dyn Storage, @@ -50,7 +46,7 @@ pub(crate) fn attach_mix_details( pub(crate) fn get_mixnode_details_by_id( store: &dyn Storage, - mix_id: MixId, + mix_id: NodeId, ) -> StdResult> { if let Some(bond_information) = storage::mixnode_bonds().may_load(store, mix_id)? { attach_mix_details(store, bond_information).map(Some) @@ -91,38 +87,13 @@ pub(crate) fn get_mixnode_details_by_identity( } } -pub(crate) fn save_new_mixnode( - storage: &mut dyn Storage, - env: Env, - mixnode: MixNode, - cost_params: MixNodeCostParams, - owner: Addr, - pledge: Coin, -) -> Result<(MixId, Layer), MixnetContractError> { - let layer = assign_layer(storage)?; - let mix_id = next_mixnode_id_counter(storage)?; - let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id(); - - let mixnode_rewarding = MixNodeRewarding::initialise_new(cost_params, &pledge, current_epoch)?; - let mixnode_bond = MixNodeBond::new(mix_id, owner, pledge, layer, mixnode, env.block.height); - - // save mixnode bond data - // note that this implicitly checks for uniqueness on identity key, sphinx key and owner - storage::mixnode_bonds().save(storage, mix_id, &mixnode_bond)?; - - // save rewarding data - rewards_storage::MIXNODE_REWARDING.save(storage, mix_id, &mixnode_rewarding)?; - - Ok((mix_id, layer)) -} - pub(crate) fn cleanup_post_unbond_mixnode_storage( storage: &mut dyn Storage, env: &Env, current_details: &MixNodeDetails, ) -> Result<(), MixnetContractError> { let mix_id = current_details.bond_information.mix_id; - // remove all bond information (we don't need it anymore + // remove all bond information since we don't need it anymore // note that "normal" remove is `may_load` followed by `replace` with a `None` // and we have already loaded the data from the storage storage::mixnode_bonds().replace( @@ -160,20 +131,17 @@ pub(crate) fn cleanup_post_unbond_mixnode_storage( unbonding_height: env.block.height, }, )?; - storage::decrement_layer_count(storage, current_details.bond_information.layer) + Ok(()) } #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::support::tests::fixtures::{ - mix_node_cost_params_fixture, mix_node_fixture, TEST_COIN_DENOM, - }; use crate::support::tests::test_helpers::TestSetup; - use cosmwasm_std::{coin, Uint128}; + use cosmwasm_std::Uint128; pub(crate) struct DummyMixnode { - pub mix_id: MixId, + pub mix_id: NodeId, pub owner: Addr, pub identity: IdentityKey, } @@ -188,28 +156,29 @@ pub(crate) mod tests { test: &mut TestSetup, stake: Option, ) -> Vec { - let (mix_id, keypair) = test.add_dummy_mixnode_with_keypair(OWNER_EXISTS, stake); + let (mix_id, keypair) = test.add_legacy_mixnode_with_keypair(OWNER_EXISTS, stake); let mix_exists = DummyMixnode { mix_id, owner: Addr::unchecked(OWNER_EXISTS), identity: keypair.public_key().to_base58_string(), }; - let (mix_id, keypair) = test.add_dummy_mixnode_with_keypair(OWNER_UNBONDING, stake); + let (mix_id, keypair) = test.add_legacy_mixnode_with_keypair(OWNER_UNBONDING, stake); let mix_unbonding = DummyMixnode { mix_id, owner: Addr::unchecked(OWNER_UNBONDING), identity: keypair.public_key().to_base58_string(), }; - let (mix_id, keypair) = test.add_dummy_mixnode_with_keypair(OWNER_UNBONDED, stake); + let (mix_id, keypair) = test.add_legacy_mixnode_with_keypair(OWNER_UNBONDED, stake); let mix_unbonded = DummyMixnode { mix_id, owner: Addr::unchecked(OWNER_UNBONDED), identity: keypair.public_key().to_base58_string(), }; - let (mix_id, keypair) = test.add_dummy_mixnode_with_keypair(OWNER_UNBONDED_LEFTOVER, stake); + let (mix_id, keypair) = + test.add_legacy_mixnode_with_keypair(OWNER_UNBONDED_LEFTOVER, stake); let mix_unbonded_leftover = DummyMixnode { mix_id, owner: Addr::unchecked(OWNER_UNBONDED_LEFTOVER), @@ -375,101 +344,12 @@ pub(crate) mod tests { assert!(res.is_none()); } - #[test] - fn saving_new_mixnode() { - let mut test = TestSetup::new(); - - // get some mixnodes in - test.add_dummy_mixnode("owner1", None); - test.add_dummy_mixnode("owner2", None); - test.add_dummy_mixnode("owner3", None); - test.add_dummy_mixnode("owner4", None); - test.add_dummy_mixnode("owner5", None); - - let env = test.env(); - let id_key = "identity-key"; - let sphinx_key = "sphinx-key"; - let mut mixnode = mix_node_fixture(); - mixnode.identity_key = id_key.into(); - mixnode.sphinx_key = sphinx_key.into(); - let cost_params = mix_node_cost_params_fixture(); - let owner = Addr::unchecked("mix-owner"); - let pledge = coin(100_000_000, TEST_COIN_DENOM); - - let (id, layer) = save_new_mixnode( - test.deps_mut().storage, - env.clone(), - mixnode, - cost_params.clone(), - owner.clone(), - pledge.clone(), - ) - .unwrap(); - assert_eq!(id, 6); - assert_eq!(layer, Layer::Three); - - assert_eq!( - storage::MIXNODE_ID_COUNTER - .load(test.deps().storage) - .unwrap(), - 6 - ); - assert_eq!(storage::LAYERS.load(test.deps().storage).unwrap().layer3, 2); - let mix_details = get_mixnode_details_by_id(test.deps().storage, id) - .unwrap() - .unwrap(); - assert_eq!(mix_details.mix_id(), id); - assert_eq!(mix_details.original_pledge(), &pledge); - assert_eq!( - mix_details.bond_information.bonding_height, - env.block.height - ); - - // try to add node with duplicate identity... - let mut mixnode = mix_node_fixture(); - mixnode.identity_key = id_key.into(); - let res = save_new_mixnode( - test.deps_mut().storage, - env.clone(), - mixnode, - cost_params.clone(), - Addr::unchecked("different-owner"), - pledge.clone(), - ); - assert!(res.is_err()); - - // and duplicate owner... - let mixnode = mix_node_fixture(); - let res = save_new_mixnode( - test.deps_mut().storage, - env.clone(), - mixnode, - cost_params.clone(), - owner, - pledge.clone(), - ); - assert!(res.is_err()); - - // and duplicate sphinx key... - let mut mixnode = mix_node_fixture(); - mixnode.sphinx_key = sphinx_key.into(); - let res = save_new_mixnode( - test.deps_mut().storage, - env, - mixnode, - cost_params, - Addr::unchecked("different-owner"), - pledge, - ); - assert!(res.is_err()); - } - #[test] fn cleaning_post_unbond_storage() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let mix_id_leftover = test.add_dummy_mixnode("mix-owner-leftover", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); + let mix_id_leftover = test.add_legacy_mixnode("mix-owner-leftover", None); // manually adjust delegation info as to indicate the rewarding information shouldnt get removed let mut rewarding_details = test.mix_rewarding(mix_id_leftover); @@ -516,9 +396,6 @@ pub(crate) mod tests { }; assert_eq!(unbonded_details, expected); - // layers are decremented - assert_eq!(storage::LAYERS.load(test.deps().storage).unwrap().layer1, 0); - let details2 = get_mixnode_details_by_id(test.deps().storage, mix_id_leftover) .unwrap() .unwrap(); @@ -549,8 +426,5 @@ pub(crate) mod tests { unbonding_height: env.block.height, }; assert_eq!(unbonded_details, expected); - - // layers are decremented - assert_eq!(storage::LAYERS.load(test.deps().storage).unwrap().layer2, 0); } } diff --git a/contracts/mixnet/src/mixnodes/mod.rs b/contracts/mixnet/src/mixnodes/mod.rs index ebff4a6ace..f3a3c9941f 100644 --- a/contracts/mixnet/src/mixnodes/mod.rs +++ b/contracts/mixnet/src/mixnodes/mod.rs @@ -3,6 +3,5 @@ pub mod helpers; pub mod queries; -pub mod signature_helpers; pub mod storage; pub mod transactions; diff --git a/contracts/mixnet/src/mixnodes/queries.rs b/contracts/mixnet/src/mixnodes/queries.rs index 5d7b61054d..8f127ed9f5 100644 --- a/contracts/mixnet/src/mixnodes/queries.rs +++ b/contracts/mixnet/src/mixnodes/queries.rs @@ -15,17 +15,17 @@ use crate::rewards::storage as rewards_storage; use cosmwasm_std::{Deps, Order, StdResult, Storage}; use cw_storage_plus::Bound; use mixnet_contract_common::mixnode::{ - MixNodeBond, MixNodeDetails, MixnodeRewardingDetailsResponse, PagedMixnodesDetailsResponse, - PagedUnbondedMixnodesResponse, StakeSaturationResponse, UnbondedMixnodeResponse, + MixNodeBond, MixNodeDetails, MixStakeSaturationResponse, MixnodeRewardingDetailsResponse, + PagedMixnodesDetailsResponse, PagedUnbondedMixnodesResponse, UnbondedMixnodeResponse, }; use mixnet_contract_common::{ - IdentityKey, LayerDistribution, MixId, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, - MixnodeDetailsResponse, PagedMixnodeBondsResponse, + IdentityKey, MixOwnershipResponse, MixnodeDetailsByIdentityResponse, MixnodeDetailsResponse, + NodeId, PagedMixnodeBondsResponse, }; pub fn query_mixnode_bonds_paged( deps: Deps<'_>, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -51,7 +51,7 @@ pub fn query_mixnode_bonds_paged( fn attach_node_details( storage: &dyn Storage, - read_bond: StdResult<(MixId, MixNodeBond)>, + read_bond: StdResult<(NodeId, MixNodeBond)>, ) -> StdResult { match read_bond { Ok((_, bond)) => attach_mix_details(storage, bond), @@ -61,7 +61,7 @@ fn attach_node_details( pub fn query_mixnodes_details_paged( deps: Deps<'_>, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -87,7 +87,7 @@ pub fn query_mixnodes_details_paged( pub fn query_unbonded_mixnodes_paged( deps: Deps<'_>, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -113,7 +113,7 @@ pub fn query_unbonded_mixnodes_paged( pub fn query_unbonded_mixnodes_by_owner_paged( deps: Deps<'_>, owner: String, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let owner = deps.api.addr_validate(&owner)?; @@ -144,7 +144,7 @@ pub fn query_unbonded_mixnodes_by_owner_paged( pub fn query_unbonded_mixnodes_by_identity_paged( deps: Deps<'_>, identity_key: String, - start_after: Option, + start_after: Option, limit: Option, ) -> StdResult { let limit = limit @@ -180,7 +180,7 @@ pub fn query_owned_mixnode(deps: Deps<'_>, address: String) -> StdResult, mix_id: MixId) -> StdResult { +pub fn query_mixnode_details(deps: Deps<'_>, mix_id: NodeId) -> StdResult { let mixnode_details = get_mixnode_details_by_id(deps.storage, mix_id)?; Ok(MixnodeDetailsResponse { @@ -203,7 +203,7 @@ pub fn query_mixnode_details_by_identity( pub fn query_mixnode_rewarding_details( deps: Deps<'_>, - mix_id: MixId, + mix_id: NodeId, ) -> StdResult { let rewarding_details = rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?; @@ -213,7 +213,10 @@ pub fn query_mixnode_rewarding_details( }) } -pub fn query_unbonded_mixnode(deps: Deps<'_>, mix_id: MixId) -> StdResult { +pub fn query_unbonded_mixnode( + deps: Deps<'_>, + mix_id: NodeId, +) -> StdResult { let unbonded_info = storage::unbonded_mixnodes().may_load(deps.storage, mix_id)?; Ok(UnbondedMixnodeResponse { @@ -222,11 +225,14 @@ pub fn query_unbonded_mixnode(deps: Deps<'_>, mix_id: MixId) -> StdResult, mix_id: MixId) -> StdResult { +pub fn query_stake_saturation( + deps: Deps<'_>, + mix_id: NodeId, +) -> StdResult { let mix_rewarding = match rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { Some(mix_rewarding) => mix_rewarding, None => { - return Ok(StakeSaturationResponse { + return Ok(MixStakeSaturationResponse { mix_id, current_saturation: None, uncapped_saturation: None, @@ -236,17 +242,13 @@ pub fn query_stake_saturation(deps: Deps<'_>, mix_id: MixId) -> StdResult) -> StdResult { - storage::LAYERS.load(deps.storage) -} - #[cfg(test)] pub(crate) mod tests { use super::*; @@ -264,7 +266,7 @@ pub(crate) mod tests { #[test] fn obeys_limits() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); let limit = 2; let page1 = query_mixnode_bonds_paged(test.deps(), None, Some(limit)).unwrap(); @@ -274,7 +276,7 @@ pub(crate) mod tests { #[test] fn has_default_limit() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); // query without explicitly setting a limit let page1 = query_mixnode_bonds_paged(test.deps(), None, None).unwrap(); @@ -288,7 +290,7 @@ pub(crate) mod tests { #[test] fn has_max_limit() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); // query with a crazily high limit in an attempt to use too many resources let crazy_limit = 1000; @@ -303,7 +305,7 @@ pub(crate) mod tests { // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id let mut test = TestSetup::new(); - test.add_dummy_mixnode("addr1", None); + test.add_legacy_mixnode("addr1", None); let per_page = 2; let page1 = query_mixnode_bonds_paged(test.deps(), None, Some(per_page)).unwrap(); @@ -312,13 +314,13 @@ pub(crate) mod tests { assert_eq!(1, page1.nodes.len()); // save another - test.add_dummy_mixnode("addr2", None); + test.add_legacy_mixnode("addr2", None); // page1 should have 2 results on it let page1 = query_mixnode_bonds_paged(test.deps(), None, Some(per_page)).unwrap(); assert_eq!(2, page1.nodes.len()); - test.add_dummy_mixnode("addr3", None); + test.add_legacy_mixnode("addr3", None); // page1 still has the same 2 results let another_page1 = @@ -334,7 +336,7 @@ pub(crate) mod tests { assert_eq!(1, page2.nodes.len()); // save another one - test.add_dummy_mixnode("addr4", None); + test.add_legacy_mixnode("addr4", None); let page2 = query_mixnode_bonds_paged(test.deps(), Some(start_after), Some(per_page)).unwrap(); @@ -351,7 +353,7 @@ pub(crate) mod tests { #[test] fn obeys_limits() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); let limit = 2; let page1 = query_mixnodes_details_paged(test.deps(), None, Some(limit)).unwrap(); @@ -361,7 +363,7 @@ pub(crate) mod tests { #[test] fn has_default_limit() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); // query without explicitly setting a limit let page1 = query_mixnodes_details_paged(test.deps(), None, None).unwrap(); @@ -375,7 +377,7 @@ pub(crate) mod tests { #[test] fn has_max_limit() { let mut test = TestSetup::new(); - test.add_dummy_mixnodes(1000); + test.add_legacy_mixnodes(1000); // query with a crazily high limit in an attempt to use too many resources let crazy_limit = 1000; @@ -393,7 +395,7 @@ pub(crate) mod tests { // as we add mixnodes, we're always inserting them in ascending manner due to monotonically increasing id let mut test = TestSetup::new(); - test.add_dummy_mixnode("addr1", None); + test.add_legacy_mixnode("addr1", None); let per_page = 2; let page1 = query_mixnodes_details_paged(test.deps(), None, Some(per_page)).unwrap(); @@ -402,13 +404,13 @@ pub(crate) mod tests { assert_eq!(1, page1.nodes.len()); // save another - test.add_dummy_mixnode("addr2", None); + test.add_legacy_mixnode("addr2", None); // page1 should have 2 results on it let page1 = query_mixnodes_details_paged(test.deps(), None, Some(per_page)).unwrap(); assert_eq!(2, page1.nodes.len()); - test.add_dummy_mixnode("addr3", None); + test.add_legacy_mixnode("addr3", None); // page1 still has the same 2 results let another_page1 = @@ -425,7 +427,7 @@ pub(crate) mod tests { assert_eq!(1, page2.nodes.len()); // save another one - test.add_dummy_mixnode("addr4", None); + test.add_legacy_mixnode("addr4", None); let page2 = query_mixnodes_details_paged(test.deps(), Some(start_after), Some(per_page)) @@ -491,7 +493,7 @@ pub(crate) mod tests { #[test] fn pagination_works() { - fn add_unbonded(storage: &mut dyn Storage, id: MixId) { + fn add_unbonded(storage: &mut dyn Storage, id: NodeId) { storage::unbonded_mixnodes() .save( storage, @@ -557,7 +559,7 @@ pub(crate) mod tests { use cosmwasm_std::Addr; use mixnet_contract_common::mixnode::UnbondedMixnode; - fn add_unbonded_with_owner(storage: &mut dyn Storage, id: MixId, owner: &str) { + fn add_unbonded_with_owner(storage: &mut dyn Storage, id: NodeId, owner: &str) { storage::unbonded_mixnodes() .save( storage, @@ -789,7 +791,7 @@ pub(crate) mod tests { use cosmwasm_std::Addr; use mixnet_contract_common::mixnode::UnbondedMixnode; - fn add_unbonded_with_identity(storage: &mut dyn Storage, id: MixId, identity: &str) { + fn add_unbonded_with_identity(storage: &mut dyn Storage, id: NodeId, identity: &str) { storage::unbonded_mixnodes() .save( storage, @@ -1061,7 +1063,7 @@ pub(crate) mod tests { assert_eq!(address, res.address); // when it [fully] exists - let id = test.add_dummy_mixnode(&address, None); + let id = test.add_legacy_mixnode(&address, None); let res = query_owned_mixnode(test.deps(), address.clone()).unwrap(); let details = res.mixnode_details.unwrap(); assert_eq!(address, details.bond_information.owner); @@ -1098,7 +1100,7 @@ pub(crate) mod tests { assert_eq!(42, res.mix_id); // it exists - let mix_id = test.add_dummy_mixnode("foomp", None); + let mix_id = test.add_legacy_mixnode("foomp", None); let res = query_mixnode_details(test.deps(), mix_id).unwrap(); let details = res.mixnode_details.unwrap(); assert_eq!(mix_id, details.bond_information.mix_id); @@ -1120,7 +1122,7 @@ pub(crate) mod tests { assert!(res.is_none()); // it exists - let mix_id = test.add_dummy_mixnode("owner", None); + let mix_id = test.add_legacy_mixnode("owner", None); // this was already tested to be working : ) let expected = query_mixnode_details(test.deps(), mix_id) .unwrap() @@ -1143,13 +1145,10 @@ pub(crate) mod tests { assert!(res.rewarding_details.is_none()); assert_eq!(42, res.mix_id); - let mix_id = test.add_dummy_mixnode("foomp", None); + let mix_id = test.add_legacy_mixnode("foomp", None); let res = query_mixnode_rewarding_details(test.deps(), mix_id).unwrap(); let details = res.rewarding_details.unwrap(); - assert_eq!( - fixtures::mix_node_cost_params_fixture(), - details.cost_params - ); + assert_eq!(fixtures::node_cost_params_fixture(), details.cost_params); assert_eq!(mix_id, res.mix_id); } @@ -1165,7 +1164,7 @@ pub(crate) mod tests { assert_eq!(42, res.mix_id); // add and unbond the mixnode - let mix_id = test.add_dummy_mixnode(sender, None); + let mix_id = test.add_legacy_mixnode(sender, None); pending_events::unbond_mixnode(test.deps_mut(), &mock_env(), 123, mix_id).unwrap(); let res = query_unbonded_mixnode(test.deps(), mix_id).unwrap(); @@ -1188,7 +1187,7 @@ pub(crate) mod tests { .unwrap(); let saturation_point = rewarding_params.interval.stake_saturation_point; - let mix_id = test.add_dummy_mixnode("foomp", None); + let mix_id = test.add_legacy_mixnode("foomp", None); // below saturation point // there's only the base pledge without any delegation diff --git a/contracts/mixnet/src/mixnodes/signature_helpers.rs b/contracts/mixnet/src/mixnodes/signature_helpers.rs deleted file mode 100644 index 04942aaff5..0000000000 --- a/contracts/mixnet/src/mixnodes/signature_helpers.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: Apache-2.0 - -use crate::signing::storage as signing_storage; -use crate::support::helpers::decode_ed25519_identity_key; -use cosmwasm_std::{Addr, Coin, Deps}; -use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::{ - construct_legacy_mixnode_bonding_sign_payload, construct_mixnode_bonding_sign_payload, MixNode, - MixNodeCostParams, -}; -use nym_contracts_common::signing::MessageSignature; -use nym_contracts_common::signing::Verifier; - -pub(crate) fn verify_mixnode_bonding_signature( - deps: Deps<'_>, - sender: Addr, - pledge: Coin, - mixnode: MixNode, - cost_params: MixNodeCostParams, - signature: MessageSignature, -) -> Result<(), MixnetContractError> { - // recover the public key - let public_key = decode_ed25519_identity_key(&mixnode.identity_key)?; - - // reconstruct the payload, first try the current format, then attempt legacy - let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; - let msg = construct_mixnode_bonding_sign_payload( - nonce, - sender.clone(), - pledge.clone(), - mixnode.clone(), - cost_params.clone(), - ); - - if deps - .api - .verify_message(msg, signature.clone(), &public_key)? - { - Ok(()) - } else { - // attempt to use legacy - let msg_legacy = construct_legacy_mixnode_bonding_sign_payload( - nonce, - sender, - pledge, - mixnode, - cost_params, - ); - if deps - .api - .verify_message(msg_legacy, signature, &public_key)? - { - Ok(()) - } else { - Err(MixnetContractError::InvalidEd25519Signature) - } - } -} diff --git a/contracts/mixnet/src/mixnodes/storage.rs b/contracts/mixnet/src/mixnodes/storage.rs index 2007456463..8344926b7e 100644 --- a/contracts/mixnet/src/mixnodes/storage.rs +++ b/contracts/mixnet/src/mixnodes/storage.rs @@ -2,33 +2,29 @@ // SPDX-License-Identifier: Apache-2.0 use crate::constants::{ - LAYER_DISTRIBUTION_KEY, MIXNODES_IDENTITY_IDX_NAMESPACE, MIXNODES_OWNER_IDX_NAMESPACE, - MIXNODES_PK_NAMESPACE, MIXNODES_SPHINX_IDX_NAMESPACE, NODE_ID_COUNTER_KEY, - PENDING_MIXNODE_CHANGES_NAMESPACE, UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE, - UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE, UNBONDED_MIXNODES_PK_NAMESPACE, + MIXNODES_IDENTITY_IDX_NAMESPACE, MIXNODES_OWNER_IDX_NAMESPACE, MIXNODES_PK_NAMESPACE, + MIXNODES_SPHINX_IDX_NAMESPACE, PENDING_MIXNODE_CHANGES_NAMESPACE, + UNBONDED_MIXNODES_IDENTITY_IDX_NAMESPACE, UNBONDED_MIXNODES_OWNER_IDX_NAMESPACE, + UNBONDED_MIXNODES_PK_NAMESPACE, }; -use cosmwasm_std::{StdResult, Storage}; -use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex, UniqueIndex}; -use mixnet_contract_common::error::MixnetContractError; +use cw_storage_plus::{Index, IndexList, IndexedMap, Map, MultiIndex, UniqueIndex}; use mixnet_contract_common::mixnode::{PendingMixNodeChanges, UnbondedMixnode}; use mixnet_contract_common::SphinxKey; -use mixnet_contract_common::{Addr, IdentityKey, Layer, LayerDistribution, MixId, MixNodeBond}; +use mixnet_contract_common::{Addr, IdentityKey, MixNodeBond, NodeId}; -pub const LAYERS: Item<'_, LayerDistribution> = Item::new(LAYER_DISTRIBUTION_KEY); -pub const MIXNODE_ID_COUNTER: Item = Item::new(NODE_ID_COUNTER_KEY); -pub const PENDING_MIXNODE_CHANGES: Map = +pub const PENDING_MIXNODE_CHANGES: Map = Map::new(PENDING_MIXNODE_CHANGES_NAMESPACE); // keeps track of `node_id -> IdentityKey, Owner, unbonding_height` so we'd known a bit more about past mixnodes // if we ever decide it's too bloaty, we can deprecate it and start removing all data in // subsequent migrations pub(crate) struct UnbondedMixnodeIndex<'a> { - pub(crate) owner: MultiIndex<'a, Addr, UnbondedMixnode, MixId>, + pub(crate) owner: MultiIndex<'a, Addr, UnbondedMixnode, NodeId>, - pub(crate) identity_key: MultiIndex<'a, IdentityKey, UnbondedMixnode, MixId>, + pub(crate) identity_key: MultiIndex<'a, IdentityKey, UnbondedMixnode, NodeId>, } -impl<'a> IndexList for UnbondedMixnodeIndex<'a> { +impl IndexList for UnbondedMixnodeIndex<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.owner, &self.identity_key]; Box::new(v.into_iter()) @@ -36,7 +32,7 @@ impl<'a> IndexList for UnbondedMixnodeIndex<'a> { } pub(crate) fn unbonded_mixnodes<'a>( -) -> IndexedMap<'a, MixId, UnbondedMixnode, UnbondedMixnodeIndex<'a>> { +) -> IndexedMap<'a, NodeId, UnbondedMixnode, UnbondedMixnodeIndex<'a>> { let indexes = UnbondedMixnodeIndex { owner: MultiIndex::new( |_pk, d| d.owner.clone(), @@ -62,7 +58,7 @@ pub(crate) struct MixnodeBondIndex<'a> { // IndexList is just boilerplate code for fetching a struct's indexes // note that from my understanding this will be converted into a macro at some point in the future -impl<'a> IndexList for MixnodeBondIndex<'a> { +impl IndexList for MixnodeBondIndex<'_> { fn get_indexes(&'_ self) -> Box> + '_> { let v: Vec<&dyn Index> = vec![&self.owner, &self.identity_key, &self.sphinx_key]; @@ -71,7 +67,7 @@ impl<'a> IndexList for MixnodeBondIndex<'a> { } // mixnode_bonds() is the storage access function. -pub(crate) fn mixnode_bonds<'a>() -> IndexedMap<'a, MixId, MixNodeBond, MixnodeBondIndex<'a>> { +pub(crate) fn mixnode_bonds<'a>() -> IndexedMap<'a, NodeId, MixNodeBond, MixnodeBondIndex<'a>> { let indexes = MixnodeBondIndex { owner: UniqueIndex::new(|d| d.owner.clone(), MIXNODES_OWNER_IDX_NAMESPACE), identity_key: UniqueIndex::new( @@ -85,130 +81,3 @@ pub(crate) fn mixnode_bonds<'a>() -> IndexedMap<'a, MixId, MixNodeBond, MixnodeB }; IndexedMap::new(MIXNODES_PK_NAMESPACE, indexes) } - -pub fn decrement_layer_count( - storage: &mut dyn Storage, - layer: Layer, -) -> Result<(), MixnetContractError> { - let mut layers = LAYERS.load(storage)?; - layers.decrement_layer_count(layer)?; - Ok(LAYERS.save(storage, &layers)?) -} - -pub(crate) fn assign_layer(store: &mut dyn Storage) -> StdResult { - // load current distribution - let mut layers = LAYERS.load(store)?; - - // choose the one with fewest nodes - let fewest = layers.choose_with_fewest(); - - // increment the existing count - layers.increment_layer_count(fewest); - - // and resave it - LAYERS.save(store, &layers)?; - Ok(fewest) -} - -pub(crate) fn next_mixnode_id_counter(store: &mut dyn Storage) -> StdResult { - let id: MixId = MIXNODE_ID_COUNTER.may_load(store)?.unwrap_or_default() + 1; - MIXNODE_ID_COUNTER.save(store, &id)?; - Ok(id) -} - -pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> StdResult<()> { - LAYERS.save(storage, &LayerDistribution::default()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::support::tests::test_helpers; - use cosmwasm_std::testing::mock_dependencies; - - #[test] - fn decrementing_layer() { - let mut deps = test_helpers::init_contract(); - - // we never underflow, if it were to happen we're going to return an error instead - assert_eq!( - Err(MixnetContractError::OverflowSubtraction { - minuend: 0, - subtrahend: 1 - }), - decrement_layer_count(deps.as_mut().storage, Layer::One) - ); - - LAYERS - .save( - deps.as_mut().storage, - &LayerDistribution { - layer1: 3, - layer2: 2, - layer3: 1, - }, - ) - .unwrap(); - - assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_ok()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_ok()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_ok()); - - assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_ok()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_ok()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_err()); - - assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_ok()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_err()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_err()); - - assert!(decrement_layer_count(deps.as_mut().storage, Layer::One).is_err()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Two).is_err()); - assert!(decrement_layer_count(deps.as_mut().storage, Layer::Three).is_err()); - } - - #[test] - fn assigning_layer() { - let mut deps = test_helpers::init_contract(); - - let layers = LayerDistribution { - layer1: 3, - layer2: 2, - layer3: 1, - }; - LAYERS.save(deps.as_mut().storage, &layers).unwrap(); - - // always assigns layer with fewest nodes - assert_eq!(Layer::Three, assign_layer(deps.as_mut().storage).unwrap()); - assert_eq!(2, LAYERS.load(deps.as_ref().storage).unwrap().layer3); - - // we have 3, 2, 2, so the 2nd layer should get chosen now - assert_eq!(Layer::Two, assign_layer(deps.as_mut().storage).unwrap()); - assert_eq!(3, LAYERS.load(deps.as_ref().storage).unwrap().layer2); - - // 3, 3, 2, so 3rd one again - assert_eq!(Layer::Three, assign_layer(deps.as_mut().storage).unwrap()); - assert_eq!(3, LAYERS.load(deps.as_ref().storage).unwrap().layer3); - } - - #[test] - fn next_id() { - let mut deps = test_helpers::init_contract(); - - for i in 1u32..1000 { - assert_eq!(i, next_mixnode_id_counter(deps.as_mut().storage).unwrap()); - } - } - - #[test] - fn initialising() { - let mut deps = mock_dependencies(); - assert!(LAYERS.load(deps.as_ref().storage).is_err()); - - initialise_storage(deps.as_mut().storage).unwrap(); - assert_eq!( - LayerDistribution::default(), - LAYERS.load(deps.as_ref().storage).unwrap() - ); - } -} diff --git a/contracts/mixnet/src/mixnodes/transactions.rs b/contracts/mixnet/src/mixnodes/transactions.rs index 59ebc432c0..7541de10b4 100644 --- a/contracts/mixnet/src/mixnodes/transactions.rs +++ b/contracts/mixnet/src/mixnodes/transactions.rs @@ -1,149 +1,73 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use cosmwasm_std::{coin, Coin, DepsMut, Env, MessageInfo, Response, Storage}; +use super::storage; +use crate::compat::helpers::{ + ensure_can_decrease_pledge, ensure_can_increase_pledge, ensure_can_modify_cost_params, +}; +use crate::interval::storage as interval_storage; +use crate::interval::storage::push_new_interval_event; +use crate::mixnodes::helpers::{get_mixnode_details_by_owner, must_get_mixnode_bond_by_owner}; +use crate::nodes::storage as nymnodes_storage; +use crate::nodes::transactions::add_nym_node_inner; +use crate::support::helpers::{ + ensure_bonded, ensure_epoch_in_progress_state, ensure_no_pending_params_changes, + ensure_no_pending_pledge_changes, validate_pledge, +}; +use cosmwasm_std::{coin, Coin, DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ - new_mixnode_bonding_event, new_mixnode_config_update_event, - new_mixnode_pending_cost_params_update_event, new_pending_mixnode_unbonding_event, + new_migrated_mixnode_event, new_mixnode_config_update_event, + new_pending_cost_params_update_event, new_pending_mixnode_unbonding_event, new_pending_pledge_decrease_event, new_pending_pledge_increase_event, }; -use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, MixNodeCostParams}; +use mixnet_contract_common::mixnode::{MixNodeConfigUpdate, NodeCostParams}; use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind}; -use mixnet_contract_common::{Layer, MixId, MixNode}; -use nym_contracts_common::signing::MessageSignature; - -use crate::interval::storage as interval_storage; -use crate::interval::storage::push_new_interval_event; -use crate::mixnet_contract_settings::storage as mixnet_params_storage; -use crate::mixnet_contract_settings::storage::rewarding_denom; -use crate::mixnodes::helpers::{ - get_mixnode_details_by_owner, must_get_mixnode_bond_by_owner, save_new_mixnode, -}; -use crate::mixnodes::signature_helpers::verify_mixnode_bonding_signature; -use crate::signing::storage as signing_storage; -use crate::support::helpers::{ - ensure_bonded, ensure_epoch_in_progress_state, ensure_is_authorized, ensure_no_existing_bond, - ensure_no_pending_pledge_changes, ensure_operating_cost_within_range, - ensure_profit_margin_within_range, validate_pledge, +use mixnet_contract_common::{ + MixNode, MixNodeDetails, MixnodeBondingPayload, NymNodeBond, PendingNodeChanges, }; +use nym_contracts_common::signing::MessageSignature; -use super::storage; - -pub(crate) fn update_mixnode_layer( - mix_id: MixId, - layer: Layer, - storage: &mut dyn Storage, -) -> Result<(), MixnetContractError> { - let bond = if let Some(bond_information) = storage::mixnode_bonds().may_load(storage, mix_id)? { - bond_information - } else { - return Err(MixnetContractError::MixNodeBondNotFound { mix_id }); - }; - let mut updated_bond = bond.clone(); - updated_bond.layer = layer; - - storage::mixnode_bonds().replace(storage, bond.mix_id, Some(&updated_bond), Some(&bond))?; - Ok(()) -} - -pub fn assign_mixnode_layer( - deps: DepsMut<'_>, - info: MessageInfo, - mix_id: MixId, - layer: Layer, -) -> Result { - ensure_is_authorized(&info.sender, deps.storage)?; - - update_mixnode_layer(mix_id, layer, deps.storage)?; - - Ok(Response::default()) -} - -// TODO: perhaps also require the user to explicitly provide what it thinks is the current nonce -// so that we could return a better error message if it doesn't match? -pub(crate) fn try_add_mixnode( +pub fn try_add_mixnode( deps: DepsMut<'_>, env: Env, info: MessageInfo, - mixnode: MixNode, - cost_params: MixNodeCostParams, + mix_node: MixNode, + cost_params: NodeCostParams, owner_signature: MessageSignature, ) -> Result { - // ensure the profit margin is within the defined range - ensure_profit_margin_within_range(deps.storage, cost_params.profit_margin_percent)?; - - // ensure the operating cost is within the defined range - ensure_operating_cost_within_range(deps.storage, &cost_params.interval_operating_cost)?; - - // check if the pledge contains any funds of the appropriate denomination - let minimum_pledge = mixnet_params_storage::minimum_mixnode_pledge(deps.storage)?; - let pledge = validate_pledge(info.funds, minimum_pledge)?; - - // if the client has an active bonded mixnode or gateway, don't allow bonding - // note that this has to be done explicitly as `UniqueIndex` constraint would not protect us - // against attempting to use different node types (i.e. gateways and mixnodes) - ensure_no_existing_bond(&info.sender, deps.storage)?; - - // there's no need to explicitly check whether there already exists mixnode with the same - // identity or sphinx keys as this is going to be done implicitly when attempting to save - // the bond information due to `UniqueIndex` constraint defined on those fields. - - // check if this sender actually owns the mixnode by checking the signature - verify_mixnode_bonding_signature( - deps.as_ref(), - info.sender.clone(), - pledge.clone(), - mixnode.clone(), - cost_params.clone(), - owner_signature, - )?; - - // update the signing nonce associated with this sender so that the future signature would be made on the new value - signing_storage::increment_signing_nonce(deps.storage, info.sender.clone())?; + let signed_payload = MixnodeBondingPayload::new(mix_node.clone(), cost_params.clone()); - let node_identity = mixnode.identity_key.clone(); - let (node_id, layer) = save_new_mixnode( - deps.storage, + // any mixnode added via 'BondMixnode' endpoint should get added as a NymNode + add_nym_node_inner( + deps, env, - mixnode, + info, + mix_node.into(), cost_params, - info.sender.clone(), - pledge.clone(), - )?; - - Ok(Response::new().add_event(new_mixnode_bonding_event( - &info.sender, - &pledge, - &node_identity, - node_id, - layer, - ))) + owner_signature, + signed_payload, + ) } -pub fn try_increase_pledge( +pub fn try_increase_mixnode_pledge( deps: DepsMut<'_>, env: Env, - info: MessageInfo, + increase: Vec, + mix_details: MixNodeDetails, ) -> Result { - let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())? - .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner: info.sender })?; let mut pending_changes = mix_details.pending_changes; let mix_id = mix_details.mix_id(); - // increasing pledge is only allowed if the epoch is currently not in the process of being advanced - ensure_epoch_in_progress_state(deps.storage)?; - - ensure_bonded(&mix_details.bond_information)?; - ensure_no_pending_pledge_changes(&pending_changes)?; + ensure_can_increase_pledge(deps.storage, &mix_details)?; - let rewarding_denom = rewarding_denom(deps.storage)?; - let pledge_increase = validate_pledge(info.funds, coin(1, rewarding_denom))?; + let rewarding_denom = &mix_details.original_pledge().denom; + let pledge_increase = validate_pledge(increase, coin(1, rewarding_denom))?; let cosmos_event = new_pending_pledge_increase_event(mix_id, &pledge_increase); // push the event to execute it at the end of the epoch - let epoch_event = PendingEpochEventKind::PledgeMore { + let epoch_event = PendingEpochEventKind::MixnodePledgeMore { mix_id, amount: pledge_increase, }; @@ -154,57 +78,21 @@ pub fn try_increase_pledge( Ok(Response::new().add_event(cosmos_event)) } -pub fn try_decrease_pledge( +pub fn try_decrease_mixnode_pledge( deps: DepsMut<'_>, env: Env, - info: MessageInfo, decrease_by: Coin, + mix_details: MixNodeDetails, ) -> Result { - let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())? - .ok_or(MixnetContractError::NoAssociatedMixNodeBond { owner: info.sender })?; let mut pending_changes = mix_details.pending_changes; let mix_id = mix_details.mix_id(); - // decreasing pledge is only allowed if the epoch is currently not in the process of being advanced - ensure_epoch_in_progress_state(deps.storage)?; - - ensure_bonded(&mix_details.bond_information)?; - ensure_no_pending_pledge_changes(&pending_changes)?; - - let minimum_pledge = mixnet_params_storage::minimum_mixnode_pledge(deps.storage)?; - - // check that the denomination is correct - if decrease_by.denom != minimum_pledge.denom { - return Err(MixnetContractError::WrongDenom { - received: decrease_by.denom, - expected: minimum_pledge.denom, - }); - } - - // also check if the request contains non-zero amount - // (otherwise it's a no-op and we should we waste gas when resolving events?) - if decrease_by.amount.is_zero() { - return Err(MixnetContractError::ZeroCoinAmount); - } - - // decreasing pledge can't result in the new pledge being lower than the minimum amount - let new_pledge_amount = mix_details - .original_pledge() - .amount - .saturating_sub(decrease_by.amount); - if new_pledge_amount < minimum_pledge.amount { - return Err(MixnetContractError::InvalidPledgeReduction { - current: mix_details.original_pledge().amount, - decrease_by: decrease_by.amount, - minimum: minimum_pledge.amount, - denom: minimum_pledge.denom, - }); - } + ensure_can_decrease_pledge(deps.storage, &mix_details, &decrease_by)?; let cosmos_event = new_pending_pledge_decrease_event(mix_id, &decrease_by); // push the event to execute it at the end of the epoch - let epoch_event = PendingEpochEventKind::DecreasePledge { + let epoch_event = PendingEpochEventKind::MixnodeDecreasePledge { mix_id, decrease_by, }; @@ -289,65 +177,108 @@ pub(crate) fn try_update_mixnode_config( } pub(crate) fn try_update_mixnode_cost_params( - deps: DepsMut<'_>, + deps: DepsMut, env: Env, + new_costs: NodeCostParams, + mix_details: MixNodeDetails, +) -> Result { + let mut pending_changes = mix_details.pending_changes; + let mix_id = mix_details.mix_id(); + + ensure_can_modify_cost_params(deps.storage, &mix_details)?; + + let cosmos_event = new_pending_cost_params_update_event(mix_id, &new_costs); + + // push the interval event + let interval_event = PendingIntervalEventKind::ChangeMixCostParams { mix_id, new_costs }; + let interval_event_id = push_new_interval_event(deps.storage, &env, interval_event)?; + pending_changes.cost_params_change = Some(interval_event_id); + storage::PENDING_MIXNODE_CHANGES.save(deps.storage, mix_id, &pending_changes)?; + + Ok(Response::new().add_event(cosmos_event)) +} + +pub fn try_migrate_to_nymnode( + deps: DepsMut, info: MessageInfo, - new_costs: MixNodeCostParams, ) -> Result { - // see if the node still exists - let existing_bond = must_get_mixnode_bond_by_owner(deps.storage, &info.sender)?; + let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())?.ok_or( + MixnetContractError::NoAssociatedMixNodeBond { + owner: info.sender.clone(), + }, + )?; + let node_id = mix_details.mix_id(); + let pending_changes = mix_details.pending_changes; + let mixnode_bond = mix_details.bond_information; + + if mixnode_bond.proxy.is_some() { + return Err(MixnetContractError::VestingNodeMigration); + } - // changing cost params is only allowed if the epoch is currently not in the process of being advanced ensure_epoch_in_progress_state(deps.storage)?; + ensure_no_pending_pledge_changes(&pending_changes)?; + ensure_no_pending_params_changes(&pending_changes)?; + ensure_bonded(&mixnode_bond)?; - ensure_bonded(&existing_bond)?; + let mixnode_identity = mixnode_bond.mix_node.identity_key.clone(); - // ensure the profit margin is within the defined range - ensure_profit_margin_within_range(deps.storage, new_costs.profit_margin_percent)?; + // remove mixnode bond data + storage::mixnode_bonds().replace(deps.storage, node_id, None, Some(&mixnode_bond))?; - // ensure the operating cost is within the defined range - ensure_operating_cost_within_range(deps.storage, &new_costs.interval_operating_cost)?; + // NOTE: nothing happens to rewarding data as its structure hasn't changed, and it's accessible under `node_id` key - let cosmos_event = new_mixnode_pending_cost_params_update_event( - existing_bond.mix_id, - &info.sender, - &new_costs, + // create nym-node entry + // note: since the starting value of nymnode counter was the same one as the final value of mixnode counter, + // we know there's definitely nothing under this key saved. + let nym_node_bond = NymNodeBond::new( + node_id, + mixnode_bond.owner, + mixnode_bond.original_pledge, + mixnode_bond.mix_node, + mixnode_bond.bonding_height, ); + nymnodes_storage::nym_nodes().save(deps.storage, node_id, &nym_node_bond)?; - // push the interval event - let interval_event = PendingIntervalEventKind::ChangeMixCostParams { - mix_id: existing_bond.mix_id, - new_costs, - }; - push_new_interval_event(deps.storage, &env, interval_event)?; + // move pending changes + // TODO: what if node has pending PM change? + storage::PENDING_MIXNODE_CHANGES.remove(deps.storage, node_id); + nymnodes_storage::PENDING_NYMNODE_CHANGES.save( + deps.storage, + node_id, + &PendingNodeChanges::new_empty(), + )?; - Ok(Response::new().add_event(cosmos_event)) + Ok(Response::new().add_event(new_migrated_mixnode_event( + &info.sender, + &mixnode_identity, + node_id, + ))) } - #[cfg(test)] pub mod tests { - use cosmwasm_std::testing::mock_info; - use cosmwasm_std::{Addr, Order, StdResult, Uint128}; - - use mixnet_contract_common::mixnode::PendingMixNodeChanges; - use mixnet_contract_common::{EpochState, EpochStatus, ExecuteMsg, LayerDistribution, Percent}; - + use super::*; + use crate::compat::transactions::try_increase_pledge; use crate::contract::execute; - use crate::mixnet_contract_settings::storage::minimum_mixnode_pledge; - use crate::mixnodes::helpers::get_mixnode_details_by_id; + use crate::mixnet_contract_settings::storage::minimum_node_pledge; + use crate::mixnodes::helpers::{get_mixnode_details_by_id, get_mixnode_details_by_identity}; + use crate::nodes::helpers::{get_node_details_by_identity, must_get_node_bond_by_owner}; + use crate::signing::storage as signing_storage; use crate::support::tests::fixtures::{good_mixnode_pledge, TEST_COIN_DENOM}; use crate::support::tests::test_helpers::TestSetup; use crate::support::tests::{fixtures, test_helpers}; - - use super::*; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{Addr, Order, StdResult, Uint128}; + use mixnet_contract_common::mixnode::PendingMixNodeChanges; + use mixnet_contract_common::nym_node::Role; + use mixnet_contract_common::{EpochState, EpochStatus, ExecuteMsg, Percent}; #[test] - fn mixnode_add() { + fn mixnode_add() -> anyhow::Result<()> { let mut test = TestSetup::new(); let env = test.env(); let sender = "alice"; - let minimum_pledge = minimum_mixnode_pledge(test.deps().storage).unwrap(); + let minimum_pledge = minimum_node_pledge(test.deps().storage).unwrap(); let mut insufficient_pledge = minimum_pledge.clone(); insufficient_pledge.amount -= Uint128::new(1000); @@ -355,7 +286,7 @@ pub mod tests { let info = mock_info(sender, &[insufficient_pledge.clone()]); let (mixnode, sig, _) = test.mixnode_with_signature(sender, Some(vec![insufficient_pledge.clone()])); - let cost_params = fixtures::mix_node_cost_params_fixture(); + let cost_params = fixtures::node_cost_params_fixture(); // we are informed that we didn't send enough funds let result = try_add_mixnode( @@ -378,7 +309,7 @@ pub mod tests { let info = mock_info(sender, &[minimum_pledge]); // if there was already a mixnode bonded by particular user - test.add_dummy_mixnode(sender, None); + test.add_legacy_mixnode(sender, None); // it fails let result = try_add_mixnode( @@ -394,7 +325,7 @@ pub mod tests { // the same holds if the user already owns a gateway let sender2 = "gateway-owner"; - test.add_dummy_gateway(sender2, None); + test.add_legacy_gateway(sender2, None); let info = mock_info(sender2, &tests::fixtures::good_mixnode_pledge()); let (mixnode, sig, _) = test.mixnode_with_signature(sender2, None); @@ -413,24 +344,33 @@ pub mod tests { let msg = ExecuteMsg::UnbondGateway {}; execute(test.deps_mut(), env.clone(), info.clone(), msg).unwrap(); - let result = try_add_mixnode(test.deps_mut(), env, info, mixnode, cost_params, sig); + let result = try_add_mixnode( + test.deps_mut(), + env, + info.clone(), + mixnode.clone(), + cost_params, + sig, + ); assert!(result.is_ok()); - // make sure we got assigned the next id (note: we have already bonded a mixnode before in this test) - let bond = - must_get_mixnode_bond_by_owner(test.deps().storage, &Addr::unchecked(sender2)).unwrap(); - assert_eq!(2, bond.mix_id); + // and the node has been added as a nym-node + let nym_node = + get_node_details_by_identity(test.deps().storage, mixnode.identity_key.clone()) + .unwrap() + .unwrap(); + assert_eq!(nym_node.bond_information.owner, info.sender); - // and make sure we're on layer 2 (because it was the next empty one) - assert_eq!(Layer::Two, bond.layer); + let maybe_legacy = + get_mixnode_details_by_identity(test.deps().storage, mixnode.identity_key)?; + assert!(maybe_legacy.is_none()); - // and see if the layer distribution matches our expectation - let expected = LayerDistribution { - layer1: 1, - layer2: 1, - layer3: 0, - }; - assert_eq!(expected, storage::LAYERS.load(test.deps().storage).unwrap()) + // make sure we got assigned the next id (note: we have already bonded a mixnode and a gateway before in this test) + let bond = + must_get_node_bond_by_owner(test.deps().storage, &Addr::unchecked(sender2)).unwrap(); + assert_eq!(3, bond.node_id); + + Ok(()) } #[test] @@ -444,7 +384,7 @@ pub mod tests { let (mixnode, signature, _) = test.mixnode_with_signature(sender, Some(pledge.clone())); // the above using cost params fixture - let cost_params = fixtures::mix_node_cost_params_fixture(); + let cost_params = fixtures::node_cost_params_fixture(); // using different parameters than what the signature was made on let mut modified_mixnode = mixnode.clone(); @@ -505,7 +445,7 @@ pub mod tests { .unwrap(); assert_eq!(1, updated_nonce); - test.immediately_unbond_mixnode(1); + test.immediately_unbond_node(1); let res = try_add_mixnode(test.deps_mut(), env, info, mixnode, cost_params, signature); assert_eq!(res, Err(MixnetContractError::InvalidEd25519Signature)); } @@ -518,7 +458,9 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -527,7 +469,7 @@ pub mod tests { let owner = "alice"; let info = mock_info(owner, &[]); - test.add_dummy_mixnode(owner, None); + test.add_legacy_mixnode(owner, None); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; @@ -558,7 +500,7 @@ pub mod tests { }) ); - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_legacy_mixnode(owner, None); // "normal" unbonding succeeds and unbonding event is pushed to the pending epoch events let res = try_remove_mixnode(test.deps_mut(), env.clone(), info.clone()); @@ -587,7 +529,8 @@ pub mod tests { // prior increase let owner = "mix-owner1"; - test.add_dummy_mixnode(owner, None); + test.add_legacy_mixnode(owner, None); + let sender = mock_info(owner, &[test.coin(1000)]); try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap(); @@ -601,10 +544,10 @@ pub mod tests { // prior decrease let owner = "mix-owner2"; - test.add_dummy_mixnode(owner, Some(Uint128::new(10000000000))); - let sender = mock_info(owner, &[]); + let node_id = test.add_legacy_mixnode(owner, Some(Uint128::new(10000000000))); + let details = test.mixnode_by_id(node_id).unwrap(); let amount = test.coin(1000); - try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap(); + try_decrease_mixnode_pledge(test.deps_mut(), env.clone(), amount, details).unwrap(); let sender = mock_info(owner, &[test.coin(1000)]); let res = try_remove_mixnode(test.deps_mut(), env.clone(), sender); @@ -617,9 +560,10 @@ pub mod tests { // artificial event let owner = "mix-owner3"; - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_legacy_mixnode(owner, None); let pending_change = PendingMixNodeChanges { pledge_change: Some(1234), + cost_params_change: None, }; storage::PENDING_MIXNODE_CHANGES .save(test.deps_mut().storage, mix_id, &pending_change) @@ -659,7 +603,7 @@ pub mod tests { }) ); - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_legacy_mixnode(owner, None); // "normal" update succeeds let res = try_update_mixnode_config(test.deps_mut(), info.clone(), update.clone()); @@ -688,10 +632,12 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; - let update = MixNodeCostParams { + let update = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(42).unwrap(), interval_operating_cost: Coin::new(12345678, TEST_COIN_DENOM), }; @@ -700,16 +646,20 @@ pub mod tests { let mut test = TestSetup::new(); let env = test.env(); let owner = "alice"; - let info = mock_info(owner, &[]); - test.add_dummy_mixnode(owner, None); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; interval_storage::save_current_epoch_status(test.deps_mut().storage, &status).unwrap(); - let res = - try_update_mixnode_cost_params(test.deps_mut(), env.clone(), info, update.clone()); + let res = try_update_mixnode_cost_params( + test.deps_mut(), + env.clone(), + update.clone(), + details, + ); assert!(matches!( res, Err(MixnetContractError::EpochAdvancementInProgress { .. }) @@ -724,33 +674,20 @@ pub mod tests { let owner = "alice"; let info = mock_info(owner, &[]); - let update = MixNodeCostParams { + let update = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(42).unwrap(), interval_operating_cost: Coin::new(12345678, TEST_COIN_DENOM), }; - // try updating a non existing mixnode bond - let res = try_update_mixnode_cost_params( - test.deps_mut(), - env.clone(), - info.clone(), - update.clone(), - ); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedMixNodeBond { - owner: Addr::unchecked(owner) - }) - ); - - let mix_id = test.add_dummy_mixnode(owner, None); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); // "normal" update succeeds let res = try_update_mixnode_cost_params( test.deps_mut(), env.clone(), - info.clone(), update.clone(), + details.clone(), ); assert!(res.is_ok()); @@ -764,7 +701,7 @@ pub mod tests { assert_eq!(1, event.0); assert_eq!( PendingIntervalEventKind::ChangeMixCostParams { - mix_id, + mix_id: node_id, new_costs: update.clone(), }, event.1.kind @@ -774,15 +711,16 @@ pub mod tests { test_helpers::execute_all_pending_events(test.deps_mut(), env.clone()); // and see if the config has actually been updated - let mix = get_mixnode_details_by_id(test.deps().storage, mix_id) + let mix = get_mixnode_details_by_id(test.deps().storage, node_id) .unwrap() .unwrap(); assert_eq!(mix.rewarding_details.cost_params, update); // but we cannot perform any updates whilst the mixnode is already unbonding try_remove_mixnode(test.deps_mut(), env.clone(), info.clone()).unwrap(); - let res = try_update_mixnode_cost_params(test.deps_mut(), env, info, update); - assert_eq!(res, Err(MixnetContractError::MixnodeIsUnbonding { mix_id })) + let details = test.mixnode_by_id(node_id).unwrap(); + let res = try_update_mixnode_cost_params(test.deps_mut(), env, update, details); + assert_eq!(res, Err(MixnetContractError::NodeIsUnbonding { node_id })) } #[test] @@ -793,7 +731,7 @@ pub mod tests { let keypair1 = nym_crypto::asymmetric::identity::KeyPair::new(&mut test.rng); let keypair2 = nym_crypto::asymmetric::identity::KeyPair::new(&mut test.rng); - let cost_params = fixtures::mix_node_cost_params_fixture(); + let cost_params = fixtures::node_cost_params_fixture(); let mixnode1 = MixNode { host: "1.2.3.4".to_string(), mix_port: 1234, @@ -838,11 +776,8 @@ pub mod tests { #[cfg(test)] mod increasing_mixnode_pledge { - use crate::mixnodes::helpers::tests::{ - setup_mix_combinations, OWNER_UNBONDED, OWNER_UNBONDED_LEFTOVER, OWNER_UNBONDING, - }; - use super::*; + use crate::mixnodes::helpers::tests::setup_mix_combinations; #[test] fn cant_be_performed_if_epoch_transition_is_in_progress() { @@ -852,7 +787,9 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -860,15 +797,15 @@ pub mod tests { let env = test.env(); let owner = "mix-owner"; - test.add_dummy_mixnode(owner, None); - let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; interval_storage::save_current_epoch_status(test.deps_mut().storage, &status) .unwrap(); - let sender = mock_info(owner, &[test.coin(1000)]); - let res = try_increase_pledge(test.deps_mut(), env, sender); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); + let increase = test.coins(1000); + let res = try_increase_mixnode_pledge(test.deps_mut(), env, increase, details); assert!(matches!( res, @@ -878,71 +815,28 @@ pub mod tests { } #[test] - fn is_not_allowed_if_account_doesnt_own_mixnode() { + fn is_not_allowed_if_mixnode_has_unbonded() { let mut test = TestSetup::new(); let env = test.env(); - let sender = mock_info("not-mix-owner", &[]); - - let res = try_increase_pledge(test.deps_mut(), env, sender); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedMixNodeBond { - owner: Addr::unchecked("not-mix-owner") - }) - ) - } - - #[test] - fn is_not_allowed_if_mixnode_has_unbonded_or_is_unbonding() { - let mut test = TestSetup::new(); - let env = test.env(); - - // TODO: I dislike this cross-test access, but it provides us with exactly what we need - // perhaps it should be refactored a bit? - let owner_unbonding = Addr::unchecked(OWNER_UNBONDING); - let owner_unbonded = Addr::unchecked(OWNER_UNBONDED); - let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER); let ids = setup_mix_combinations(&mut test, None); let mix_id_unbonding = ids[1].mix_id; - let res = try_increase_pledge( - test.deps_mut(), - env.clone(), - mock_info(owner_unbonding.as_str(), &[]), - ); - assert_eq!( - res, - Err(MixnetContractError::MixnodeIsUnbonding { - mix_id: mix_id_unbonding - }) - ); + let increase = test.coins(1000); + let details = test.mixnode_by_id(mix_id_unbonding).unwrap(); - // if the nodes are gone we treat them as tey never existed in the first place - // (regardless of if there's some leftover data) - let res = try_increase_pledge( + let res = try_increase_mixnode_pledge( test.deps_mut(), env.clone(), - mock_info(owner_unbonded_leftover.as_str(), &[]), + increase.clone(), + details, ); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { - owner: owner_unbonded_leftover + Err(MixnetContractError::NodeIsUnbonding { + node_id: mix_id_unbonding }) ); - - let res = try_increase_pledge( - test.deps_mut(), - env, - mock_info(owner_unbonded.as_str(), &[]), - ); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedMixNodeBond { - owner: owner_unbonded - }) - ) } #[test] @@ -951,14 +845,21 @@ pub mod tests { let env = test.env(); let owner = "mix-owner"; - test.add_dummy_mixnode(owner, None); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); - let sender_empty = mock_info(owner, &[]); - let res = try_increase_pledge(test.deps_mut(), env.clone(), sender_empty); + let sender_empty = Vec::new(); + let res = try_increase_mixnode_pledge( + test.deps_mut(), + env.clone(), + sender_empty, + details.clone(), + ); assert_eq!(res, Err(MixnetContractError::NoBondFound)); - let sender_zero = mock_info(owner, &[test.coin(0)]); - let res = try_increase_pledge(test.deps_mut(), env, sender_zero); + let sender_zero = test.coins(0); + let res = + try_increase_mixnode_pledge(test.deps_mut(), env, sender_zero, details.clone()); assert_eq!( res, Err(MixnetContractError::InsufficientPledge { @@ -975,11 +876,19 @@ pub mod tests { // prior increase let owner = "mix-owner1"; - test.add_dummy_mixnode(owner, None); - let sender = mock_info(owner, &[test.coin(1000)]); - try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap(); + let node_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(node_id).unwrap(); + let sender = test.coins(1000); + try_increase_mixnode_pledge( + test.deps_mut(), + env.clone(), + sender.clone(), + details.clone(), + ) + .unwrap(); - let res = try_increase_pledge(test.deps_mut(), env.clone(), sender); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = try_increase_mixnode_pledge(test.deps_mut(), env.clone(), sender, details); assert_eq!( res, Err(MixnetContractError::PendingPledgeChange { @@ -989,36 +898,20 @@ pub mod tests { // prior decrease let owner = "mix-owner2"; - test.add_dummy_mixnode(owner, Some(Uint128::new(10000000000))); - let sender = mock_info(owner, &[]); - let amount = test.coin(1000); - try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap(); + let node_id = test.add_legacy_mixnode(owner, Some(Uint128::new(10000000000))); + let details = test.mixnode_by_id(node_id).unwrap(); - let sender = mock_info(owner, &[test.coin(1000)]); - let res = try_increase_pledge(test.deps_mut(), env.clone(), sender); - assert_eq!( - res, - Err(MixnetContractError::PendingPledgeChange { - pending_event_id: 2 - }) - ); - - // artificial event - let owner = "mix-owner3"; - let mix_id = test.add_dummy_mixnode(owner, None); - let pending_change = PendingMixNodeChanges { - pledge_change: Some(1234), - }; - storage::PENDING_MIXNODE_CHANGES - .save(test.deps_mut().storage, mix_id, &pending_change) + let amount = test.coin(1000); + try_decrease_mixnode_pledge(test.deps_mut(), env.clone(), amount, details.clone()) .unwrap(); - let sender = mock_info(owner, &[test.coin(1000)]); - let res = try_increase_pledge(test.deps_mut(), env, sender); + let sender = test.coins(10000); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = try_increase_mixnode_pledge(test.deps_mut(), env.clone(), sender, details); assert_eq!( res, Err(MixnetContractError::PendingPledgeChange { - pending_event_id: 1234 + pending_event_id: 2 }) ); } @@ -1028,19 +921,20 @@ pub mod tests { let mut test = TestSetup::new(); let env = test.env(); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, None); + let mix_id = test.add_legacy_mixnode(owner, None); + let details = test.mixnode_by_id(mix_id).unwrap(); let events = test.pending_epoch_events(); assert!(events.is_empty()); - let sender = mock_info(owner, &[test.coin(1000)]); - try_increase_pledge(test.deps_mut(), env, sender).unwrap(); + let sender = test.coins(1000); + try_increase_mixnode_pledge(test.deps_mut(), env, sender, details).unwrap(); let events = test.pending_epoch_events(); assert_eq!( events[0].kind, - PendingEpochEventKind::PledgeMore { + PendingEpochEventKind::MixnodePledgeMore { mix_id, amount: test.coin(1000), } @@ -1050,11 +944,8 @@ pub mod tests { #[cfg(test)] mod decreasing_mixnode_pledge { - use crate::mixnodes::helpers::tests::{ - setup_mix_combinations, OWNER_UNBONDED, OWNER_UNBONDED_LEFTOVER, OWNER_UNBONDING, - }; - use super::*; + use crate::mixnodes::helpers::tests::setup_mix_combinations; #[test] fn cant_be_performed_if_epoch_transition_is_in_progress() { @@ -1064,7 +955,9 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -1073,15 +966,15 @@ pub mod tests { let owner = "mix-owner"; let decrease = test.coin(1000); - test.add_dummy_mixnode(owner, Some(Uint128::new(100_000_000_000))); + let node_id = test.add_legacy_mixnode(owner, Some(Uint128::new(100_000_000_000))); + let details = test.mixnode_by_id(node_id).unwrap(); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; interval_storage::save_current_epoch_status(test.deps_mut().storage, &status) .unwrap(); - let sender = mock_info(owner, &[]); - let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease); + let res = try_decrease_mixnode_pledge(test.deps_mut(), env, decrease, details); assert!(matches!( res, @@ -1091,79 +984,28 @@ pub mod tests { } #[test] - fn is_not_allowed_if_account_doesnt_own_mixnode() { + fn is_not_allowed_if_mixnode_is_unbonding() { let mut test = TestSetup::new(); let env = test.env(); - let sender = mock_info("not-mix-owner", &[]); - let decrease = test.coin(1000); - - let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedMixNodeBond { - owner: Addr::unchecked("not-mix-owner") - }) - ) - } - - #[test] - fn is_not_allowed_if_mixnode_has_unbonded_or_is_unbonding() { - let mut test = TestSetup::new(); - let env = test.env(); - - // just to make sure that after decrease the value would still be above the minimum - let stake = Uint128::new(100_000_000_000); - let decrease = test.coin(1000); - // TODO: I dislike this cross-test access, but it provides us with exactly what we need - // perhaps it should be refactored a bit? - let owner_unbonding = Addr::unchecked(OWNER_UNBONDING); - let owner_unbonded = Addr::unchecked(OWNER_UNBONDED); - let owner_unbonded_leftover = Addr::unchecked(OWNER_UNBONDED_LEFTOVER); - - let ids = setup_mix_combinations(&mut test, Some(stake)); + let ids = setup_mix_combinations(&mut test, None); let mix_id_unbonding = ids[1].mix_id; - let res = try_decrease_pledge( - test.deps_mut(), - env.clone(), - mock_info(owner_unbonding.as_str(), &[]), - decrease.clone(), - ); - assert_eq!( - res, - Err(MixnetContractError::MixnodeIsUnbonding { - mix_id: mix_id_unbonding - }) - ); + let decrease = test.coin(1000); + let details = test.mixnode_by_id(mix_id_unbonding).unwrap(); - // if the nodes are gone we treat them as tey never existed in the first place - // (regardless of if there's some leftover data) - let res = try_decrease_pledge( + let res = try_decrease_mixnode_pledge( test.deps_mut(), env.clone(), - mock_info(owner_unbonded_leftover.as_str(), &[]), decrease.clone(), + details, ); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { - owner: owner_unbonded_leftover + Err(MixnetContractError::NodeIsUnbonding { + node_id: mix_id_unbonding }) ); - - let res = try_decrease_pledge( - test.deps_mut(), - env, - mock_info(owner_unbonded.as_str(), &[]), - decrease, - ); - assert_eq!( - res, - Err(MixnetContractError::NoAssociatedMixNodeBond { - owner: owner_unbonded - }) - ) } #[test] @@ -1172,20 +1014,20 @@ pub mod tests { let env = test.env(); let owner = "mix-owner"; - let minimum_pledge = minimum_mixnode_pledge(test.deps().storage).unwrap(); + let minimum_pledge = minimum_node_pledge(test.deps().storage).unwrap(); let pledge_amount = minimum_pledge.amount + Uint128::new(100); let pledged = test.coin(pledge_amount.u128()); - test.add_dummy_mixnode(owner, Some(pledge_amount)); + let node_id = test.add_legacy_mixnode(owner, Some(pledge_amount)); + let details = test.mixnode_by_id(node_id).unwrap(); let invalid_decrease = test.coin(150); let valid_decrease = test.coin(50); - let sender = mock_info(owner, &[]); - let res = try_decrease_pledge( + let res = try_decrease_mixnode_pledge( test.deps_mut(), env.clone(), - sender.clone(), invalid_decrease.clone(), + details.clone(), ); assert_eq!( res, @@ -1197,7 +1039,7 @@ pub mod tests { }) ); - let res = try_decrease_pledge(test.deps_mut(), env, sender, valid_decrease); + let res = try_decrease_mixnode_pledge(test.deps_mut(), env, valid_decrease, details); assert!(res.is_ok()) } @@ -1210,10 +1052,10 @@ pub mod tests { let decrease = test.coin(0); let owner = "mix-owner"; - test.add_dummy_mixnode(owner, Some(stake)); + let node_id = test.add_legacy_mixnode(owner, Some(stake)); + let details = test.mixnode_by_id(node_id).unwrap(); - let sender = mock_info(owner, &[]); - let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease); + let res = try_decrease_mixnode_pledge(test.deps_mut(), env, decrease, details); assert_eq!(res, Err(MixnetContractError::ZeroCoinAmount)) } @@ -1226,11 +1068,20 @@ pub mod tests { // prior increase let owner = "mix-owner1"; - test.add_dummy_mixnode(owner, Some(stake)); - let sender = mock_info(owner, &[test.coin(1000)]); - try_increase_pledge(test.deps_mut(), env.clone(), sender.clone()).unwrap(); + let node_id = test.add_legacy_mixnode(owner, Some(stake)); + let details = test.mixnode_by_id(node_id).unwrap(); - let res = try_decrease_pledge(test.deps_mut(), env.clone(), sender, decrease.clone()); + let sender = test.coins(1000); + try_increase_mixnode_pledge(test.deps_mut(), env.clone(), sender.clone(), details) + .unwrap(); + + let details = test.mixnode_by_id(node_id).unwrap(); + let res = try_decrease_mixnode_pledge( + test.deps_mut(), + env.clone(), + decrease.clone(), + details, + ); assert_eq!( res, Err(MixnetContractError::PendingPledgeChange { @@ -1240,13 +1091,18 @@ pub mod tests { // prior decrease let owner = "mix-owner2"; - test.add_dummy_mixnode(owner, Some(stake)); - let sender = mock_info(owner, &[]); + let node_id = test.add_legacy_mixnode(owner, Some(stake)); + let details = test.mixnode_by_id(node_id).unwrap(); let amount = test.coin(1000); - try_decrease_pledge(test.deps_mut(), env.clone(), sender, amount).unwrap(); + try_decrease_mixnode_pledge(test.deps_mut(), env.clone(), amount, details).unwrap(); - let sender = mock_info(owner, &[test.coin(1000)]); - let res = try_decrease_pledge(test.deps_mut(), env.clone(), sender, decrease.clone()); + let details = test.mixnode_by_id(node_id).unwrap(); + let res = try_decrease_mixnode_pledge( + test.deps_mut(), + env.clone(), + decrease.clone(), + details, + ); assert_eq!( res, Err(MixnetContractError::PendingPledgeChange { @@ -1256,16 +1112,17 @@ pub mod tests { // artificial event let owner = "mix-owner3"; - let mix_id = test.add_dummy_mixnode(owner, Some(stake)); + let mix_id = test.add_legacy_mixnode(owner, Some(stake)); let pending_change = PendingMixNodeChanges { pledge_change: Some(1234), + cost_params_change: None, }; storage::PENDING_MIXNODE_CHANGES .save(test.deps_mut().storage, mix_id, &pending_change) .unwrap(); - let sender = mock_info(owner, &[test.coin(1000)]); - let res = try_decrease_pledge(test.deps_mut(), env, sender, decrease); + let details = test.mixnode_by_id(mix_id).unwrap(); + let res = try_decrease_mixnode_pledge(test.deps_mut(), env, decrease, details); assert_eq!( res, Err(MixnetContractError::PendingPledgeChange { @@ -1284,19 +1141,19 @@ pub mod tests { let decrease = test.coin(1000); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, Some(stake)); + let mix_id = test.add_legacy_mixnode(owner, Some(stake)); + let details = test.mixnode_by_id(mix_id).unwrap(); let events = test.pending_epoch_events(); assert!(events.is_empty()); - let sender = mock_info(owner, &[]); - try_decrease_pledge(test.deps_mut(), env, sender, decrease.clone()).unwrap(); + try_decrease_mixnode_pledge(test.deps_mut(), env, decrease.clone(), details).unwrap(); let events = test.pending_epoch_events(); assert_eq!( events[0].kind, - PendingEpochEventKind::DecreasePledge { + PendingEpochEventKind::MixnodeDecreasePledge { mix_id, decrease_by: decrease, } diff --git a/contracts/mixnet/src/nodes/helpers.rs b/contracts/mixnet/src/nodes/helpers.rs new file mode 100644 index 0000000000..12c01ab1f6 --- /dev/null +++ b/contracts/mixnet/src/nodes/helpers.rs @@ -0,0 +1,186 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::interval::storage as interval_storage; +use crate::nodes::storage; +use crate::nodes::storage::next_nymnode_id_counter; +use crate::rewards::storage as rewards_storage; +use cosmwasm_std::{Addr, Coin, Env, StdResult, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::nym_node::UnbondedNymNode; +use mixnet_contract_common::{ + NodeCostParams, NodeId, NodeRewarding, NymNode, NymNodeBond, NymNodeDetails, PendingNodeChanges, +}; +use nym_contracts_common::IdentityKey; + +pub(crate) fn save_new_nymnode( + storage: &mut dyn Storage, + bonding_height: u64, + node: NymNode, + cost_params: NodeCostParams, + owner: Addr, + pledge: Coin, +) -> Result { + let node_id = next_nymnode_id_counter(storage)?; + let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id(); + + save_new_nymnode_with_id( + storage, + node_id, + bonding_height, + node, + cost_params, + owner, + pledge, + current_epoch, + )?; + + Ok(node_id) +} + +#[allow(clippy::too_many_arguments)] +pub(crate) fn save_new_nymnode_with_id( + storage: &mut dyn Storage, + node_id: NodeId, + bonding_height: u64, + node: NymNode, + cost_params: NodeCostParams, + owner: Addr, + pledge: Coin, + last_rewarding_epoch: u32, +) -> Result<(), MixnetContractError> { + let node_rewarding = NodeRewarding::initialise_new(cost_params, &pledge, last_rewarding_epoch)?; + let node_bond = NymNodeBond::new(node_id, owner, pledge, node, bonding_height); + + // save node bond data + // note that this implicitly checks for uniqueness on identity key and owner + storage::nym_nodes().save(storage, node_id, &node_bond)?; + + // save rewarding data + rewards_storage::NYMNODE_REWARDING.save(storage, node_id, &node_rewarding)?; + + // initialise pending changes + storage::PENDING_NYMNODE_CHANGES.save(storage, node_id, &PendingNodeChanges::new_empty())?; + + Ok(()) +} + +pub(crate) fn attach_nym_node_details( + store: &dyn Storage, + bond_information: NymNodeBond, +) -> StdResult { + // if bond exists, rewarding details MUST also exist + let rewarding_details = + rewards_storage::NYMNODE_REWARDING.load(store, bond_information.node_id)?; + + // the same is true for the pending changes + let pending_changes = storage::PENDING_NYMNODE_CHANGES.load(store, bond_information.node_id)?; + + Ok(NymNodeDetails::new( + bond_information, + rewarding_details, + pending_changes, + )) +} + +pub(crate) fn get_node_details_by_id( + store: &dyn Storage, + mix_id: NodeId, +) -> StdResult> { + if let Some(bond_information) = storage::nym_nodes().may_load(store, mix_id)? { + attach_nym_node_details(store, bond_information).map(Some) + } else { + Ok(None) + } +} + +pub(crate) fn get_node_details_by_owner( + store: &dyn Storage, + address: Addr, +) -> StdResult> { + if let Some(bond_information) = storage::nym_nodes() + .idx + .owner + .item(store, address)? + .map(|record| record.1) + { + attach_nym_node_details(store, bond_information).map(Some) + } else { + Ok(None) + } +} + +pub(crate) fn get_node_details_by_identity( + store: &dyn Storage, + identity: IdentityKey, +) -> StdResult> { + if let Some(bond_information) = storage::nym_nodes() + .idx + .identity_key + .item(store, identity)? + .map(|record| record.1) + { + attach_nym_node_details(store, bond_information).map(Some) + } else { + Ok(None) + } +} + +pub(crate) fn must_get_node_bond_by_owner( + store: &dyn Storage, + owner: &Addr, +) -> Result { + Ok(storage::nym_nodes() + .idx + .owner + .item(store, owner.clone())? + .ok_or(MixnetContractError::NoAssociatedNodeBond { + owner: owner.clone(), + })? + .1) +} + +pub(crate) fn cleanup_post_unbond_nym_node_storage( + storage: &mut dyn Storage, + env: &Env, + current_details: &NymNodeDetails, +) -> Result<(), MixnetContractError> { + let node_id = current_details.bond_information.node_id; + // remove all bond information since we don't need it anymore + // note that "normal" remove is `may_load` followed by `replace` with a `None` + // and we have already loaded the data from the storage + storage::nym_nodes().replace( + storage, + node_id, + None, + Some(¤t_details.bond_information), + )?; + + // if there are no pending delegations to return, we can also + // purge all information regarding rewarding parameters + if current_details.rewarding_details.unique_delegations == 0 { + rewards_storage::NYMNODE_REWARDING.remove(storage, node_id); + } else { + // otherwise just set operator's tokens to zero as to indicate they have unbonded + // and already claimed those + let zeroed = current_details.rewarding_details.clear_operator(); + rewards_storage::NYMNODE_REWARDING.save(storage, node_id, &zeroed)?; + } + + let identity_key = current_details.bond_information.identity().to_owned(); + let owner = current_details.bond_information.owner.clone(); + + // save minimal information about this node + storage::unbonded_nym_nodes().save( + storage, + node_id, + &UnbondedNymNode { + identity_key, + node_id, + owner, + unbonding_height: env.block.height, + }, + )?; + + Ok(()) +} diff --git a/contracts/mixnet/src/nodes/mod.rs b/contracts/mixnet/src/nodes/mod.rs new file mode 100644 index 0000000000..7707e3b780 --- /dev/null +++ b/contracts/mixnet/src/nodes/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod helpers; +pub mod queries; +mod signature_helpers; +pub mod storage; +pub mod transactions; diff --git a/contracts/mixnet/src/nodes/queries.rs b/contracts/mixnet/src/nodes/queries.rs new file mode 100644 index 0000000000..6283d21d75 --- /dev/null +++ b/contracts/mixnet/src/nodes/queries.rs @@ -0,0 +1,259 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::{ + NYM_NODE_BOND_DEFAULT_RETRIEVAL_LIMIT, NYM_NODE_BOND_MAX_RETRIEVAL_LIMIT, + NYM_NODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT, NYM_NODE_DETAILS_MAX_RETRIEVAL_LIMIT, + UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT, UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT, +}; +use crate::nodes::helpers::{ + attach_nym_node_details, get_node_details_by_id, get_node_details_by_identity, + get_node_details_by_owner, +}; +use crate::nodes::storage; +use crate::rewards::storage as rewards_storage; +use cosmwasm_std::{Deps, Order, StdResult, Storage}; +use cw_storage_plus::Bound; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::nym_node::{ + EpochAssignmentResponse, NodeDetailsByIdentityResponse, NodeDetailsResponse, + NodeOwnershipResponse, NodeRewardingDetailsResponse, PagedNymNodeBondsResponse, + PagedNymNodeDetailsResponse, PagedUnbondedNymNodesResponse, Role, RolesMetadataResponse, + StakeSaturationResponse, UnbondedNodeResponse, +}; +use mixnet_contract_common::{NodeId, NymNodeBond, NymNodeDetails}; +use nym_contracts_common::IdentityKey; + +pub(crate) fn query_nymnode_bonds_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(NYM_NODE_BOND_DEFAULT_RETRIEVAL_LIMIT) + .min(NYM_NODE_BOND_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::nym_nodes() + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|item| item.1)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|node| node.node_id); + + Ok(PagedNymNodeBondsResponse { + nodes, + start_next_after, + }) +} + +pub(crate) fn query_rewarded_set_metadata( + deps: Deps<'_>, +) -> Result { + let metadata = storage::read_rewarded_set_metadata(deps.storage)?; + Ok(RolesMetadataResponse { metadata }) +} + +pub(crate) fn query_epoch_assignment( + deps: Deps<'_>, + role: Role, +) -> Result { + let metadata = storage::read_rewarded_set_metadata(deps.storage)?; + let nodes = storage::read_assigned_roles(deps.storage, role)?; + Ok(EpochAssignmentResponse { + epoch_id: metadata.epoch_id, + nodes, + }) +} + +fn attach_node_details( + storage: &dyn Storage, + read_bond: StdResult<(NodeId, NymNodeBond)>, +) -> StdResult { + match read_bond { + Ok((_, bond)) => attach_nym_node_details(storage, bond), + Err(err) => Err(err), + } +} + +pub fn query_nymnodes_details_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(NYM_NODE_DETAILS_DEFAULT_RETRIEVAL_LIMIT) + .min(NYM_NODE_DETAILS_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::nym_nodes() + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| attach_node_details(deps.storage, res)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|details| details.node_id()); + + Ok(PagedNymNodeDetailsResponse { + nodes, + start_next_after, + }) +} + +pub fn query_unbonded_nymnodes_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT) + .min(UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::unbonded_nym_nodes() + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|item| item.1)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|res| res.node_id); + + Ok(PagedUnbondedNymNodesResponse { + nodes, + start_next_after, + }) +} + +pub fn query_unbonded_nymnodes_by_owner_paged( + deps: Deps<'_>, + owner: String, + start_after: Option, + limit: Option, +) -> StdResult { + let owner = deps.api.addr_validate(&owner)?; + + let limit = limit + .unwrap_or(UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT) + .min(UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::unbonded_nym_nodes() + .idx + .owner + .prefix(owner) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|r| r.map(|r| r.1)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|res| res.node_id); + + Ok(PagedUnbondedNymNodesResponse { + nodes, + start_next_after, + }) +} + +pub fn query_unbonded_nymnodes_by_identity_paged( + deps: Deps<'_>, + identity_key: String, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(UNBONDED_NYM_NODES_DEFAULT_RETRIEVAL_LIMIT) + .min(UNBONDED_NYM_NODES_MAX_RETRIEVAL_LIMIT) as usize; + + let start = start_after.map(Bound::exclusive); + + let nodes = storage::unbonded_nym_nodes() + .idx + .identity_key + .prefix(identity_key) + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|r| r.map(|r| r.1)) + .collect::>>()?; + + let start_next_after = nodes.last().map(|res| res.node_id); + + Ok(PagedUnbondedNymNodesResponse { + nodes, + start_next_after, + }) +} + +pub fn query_owned_nymnode(deps: Deps<'_>, address: String) -> StdResult { + let validated_addr = deps.api.addr_validate(&address)?; + + let details = get_node_details_by_owner(deps.storage, validated_addr.clone())?; + Ok(NodeOwnershipResponse { + address: validated_addr, + details, + }) +} + +pub fn query_nymnode_details(deps: Deps<'_>, node_id: NodeId) -> StdResult { + let details = get_node_details_by_id(deps.storage, node_id)?; + + Ok(NodeDetailsResponse { node_id, details }) +} + +pub fn query_nymnode_details_by_identity( + deps: Deps<'_>, + identity_key: IdentityKey, +) -> StdResult { + let details = get_node_details_by_identity(deps.storage, identity_key.clone())?; + + Ok(NodeDetailsByIdentityResponse { + identity_key, + details, + }) +} + +pub fn query_nymnode_rewarding_details( + deps: Deps<'_>, + node_id: NodeId, +) -> StdResult { + let rewarding_details = rewards_storage::MIXNODE_REWARDING.may_load(deps.storage, node_id)?; + + Ok(NodeRewardingDetailsResponse { + node_id, + rewarding_details, + }) +} + +pub fn query_unbonded_nymnode(deps: Deps<'_>, node_id: NodeId) -> StdResult { + let details = storage::unbonded_nym_nodes().may_load(deps.storage, node_id)?; + + Ok(UnbondedNodeResponse { node_id, details }) +} + +pub fn query_stake_saturation( + deps: Deps<'_>, + node_id: NodeId, +) -> StdResult { + let node_rewarding = match rewards_storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { + Some(node_rewarding) => node_rewarding, + None => { + return Ok(StakeSaturationResponse { + node_id, + current_saturation: None, + uncapped_saturation: None, + }) + } + }; + + let rewarding_params = rewards_storage::REWARDING_PARAMS.load(deps.storage)?; + + Ok(StakeSaturationResponse { + node_id, + current_saturation: Some(node_rewarding.bond_saturation(&rewarding_params)), + uncapped_saturation: Some(node_rewarding.uncapped_bond_saturation(&rewarding_params)), + }) +} diff --git a/contracts/mixnet/src/nodes/signature_helpers.rs b/contracts/mixnet/src/nodes/signature_helpers.rs new file mode 100644 index 0000000000..db2e074540 --- /dev/null +++ b/contracts/mixnet/src/nodes/signature_helpers.rs @@ -0,0 +1,38 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::signing::storage as signing_storage; +use crate::support::helpers::decode_ed25519_identity_key; +use cosmwasm_std::{Addr, Coin, Deps}; +use mixnet_contract_common::construct_generic_node_bonding_payload; +use mixnet_contract_common::error::MixnetContractError; +use nym_contracts_common::signing::Verifier; +use nym_contracts_common::signing::{MessageSignature, SigningPurpose}; +use nym_contracts_common::IdentityKeyRef; +use serde::Serialize; + +/// Verifies the bonding signature on either a legacy mixnode, legacy gateway or a nym-node. +pub(crate) fn verify_bonding_signature( + deps: Deps<'_>, + sender: Addr, + identity_key: IdentityKeyRef, + pledge: Coin, + message: T, + signature: MessageSignature, +) -> Result<(), MixnetContractError> +where + T: SigningPurpose + Serialize, +{ + // recover the public key + let public_key = decode_ed25519_identity_key(identity_key)?; + + // reconstruct the payload + let nonce = signing_storage::get_signing_nonce(deps.storage, sender.clone())?; + let msg = construct_generic_node_bonding_payload(nonce, sender, pledge, message); + + if deps.api.verify_message(msg, signature, &public_key)? { + Ok(()) + } else { + Err(MixnetContractError::InvalidEd25519Signature) + } +} diff --git a/contracts/mixnet/src/nodes/storage/helpers.rs b/contracts/mixnet/src/nodes/storage/helpers.rs new file mode 100644 index 0000000000..169a0d42bc --- /dev/null +++ b/contracts/mixnet/src/nodes/storage/helpers.rs @@ -0,0 +1,173 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nodes::storage::rewarded_set::{ACTIVE_ROLES_BUCKET, ROLES, ROLES_METADATA}; +use crate::nodes::storage::{nym_nodes, NYMNODE_ID_COUNTER}; +use cosmwasm_std::{StdResult, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role}; +use mixnet_contract_common::{EpochId, NodeId, NymNodeBond, RoleAssignment}; +use serde::{Deserialize, Serialize}; + +#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, Eq, PartialEq)] +#[repr(u8)] +pub enum RoleStorageBucket { + #[default] + A = 0, + B = 1, +} + +impl RoleStorageBucket { + pub fn other(&self) -> Self { + match self { + RoleStorageBucket::A => RoleStorageBucket::B, + RoleStorageBucket::B => RoleStorageBucket::A, + } + } + + pub fn swap(&self) -> Self { + self.other() + } +} + +pub(crate) fn reset_inactive_metadata( + storage: &mut dyn Storage, + epoch_id: EpochId, +) -> Result<(), MixnetContractError> { + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage)?; + let inactive = active_bucket.other() as u8; + + ROLES_METADATA.save(storage, inactive, &RewardedSetMetadata::new(epoch_id))?; + Ok(()) +} + +pub(crate) fn save_assignment( + storage: &mut dyn Storage, + assignment: RoleAssignment, +) -> Result<(), MixnetContractError> { + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage)?; + + // we're always assigning to the INACTIVE bucket, because it's still being built + let inactive = active_bucket.other() as u8; + + // update metadata + let mut metadata = ROLES_METADATA.load(storage, inactive)?; + let highest_id = assignment.nodes.iter().max().copied().unwrap_or_default(); + metadata.set_highest_id(highest_id, assignment.role); + metadata.set_role_count(assignment.role, assignment.nodes.len() as u32); + if assignment.is_final_assignment() { + metadata.fully_assigned = true + } + ROLES_METADATA.save(storage, inactive, &metadata)?; + + // set the actual roles + Ok(ROLES.save(storage, (inactive, assignment.role), &assignment.nodes)?) +} + +pub(crate) fn read_rewarded_set_metadata( + storage: &dyn Storage, +) -> Result { + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage)?; + Ok(ROLES_METADATA.load(storage, active_bucket as u8)?) +} + +pub(crate) fn read_assigned_roles( + storage: &dyn Storage, + role: Role, +) -> Result, MixnetContractError> { + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage)?; + // we're always reading from the ACTIVE bucket + Ok(ROLES.load(storage, (active_bucket as u8, role))?) +} + +pub(crate) fn swap_active_role_bucket( + storage: &mut dyn Storage, +) -> Result<(), MixnetContractError> { + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage)?; + Ok(ACTIVE_ROLES_BUCKET.save(storage, &active_bucket.swap())?) +} + +pub(crate) fn set_unbonding( + storage: &mut dyn Storage, + bond: &NymNodeBond, +) -> Result<(), MixnetContractError> { + let mut updated_bond = bond.clone(); + updated_bond.is_unbonding = true; + nym_nodes().replace(storage, bond.node_id, Some(&updated_bond), Some(bond))?; + Ok(()) +} + +pub(crate) fn next_nymnode_id_counter(store: &mut dyn Storage) -> StdResult { + let id: NodeId = NYMNODE_ID_COUNTER.may_load(store)?.unwrap_or_default() + 1; + NYMNODE_ID_COUNTER.save(store, &id)?; + Ok(id) +} + +pub(crate) fn initialise_storage(storage: &mut dyn Storage) -> Result<(), MixnetContractError> { + let active_bucket = RoleStorageBucket::default(); + let inactive_bucket = active_bucket.other(); + + ACTIVE_ROLES_BUCKET.save(storage, &active_bucket)?; + let roles = vec![ + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::EntryGateway, + Role::ExitGateway, + Role::Standby, + ]; + for role in roles { + ROLES.save(storage, (active_bucket as u8, role), &vec![])?; + ROLES.save(storage, (inactive_bucket as u8, role), &vec![])? + } + + ROLES_METADATA.save(storage, active_bucket as u8, &Default::default())?; + ROLES_METADATA.save(storage, inactive_bucket as u8, &Default::default())?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::support::tests::test_helpers; + use crate::support::tests::test_helpers::TestSetup; + + #[test] + fn next_id() { + let mut deps = test_helpers::init_contract(); + + for i in 1u32..1000 { + assert_eq!(i, next_nymnode_id_counter(deps.as_mut().storage).unwrap()); + } + } + + #[test] + fn assigning_role_uses_highest_id_even_if_not_sorted() { + let mut test = TestSetup::new(); + let deps = test.deps_mut(); + + let sorted = RoleAssignment { + role: Role::EntryGateway, + nodes: vec![1, 2, 3], + }; + + let unsorted = RoleAssignment { + role: Role::Layer1, + nodes: vec![8, 5, 4], + }; + + save_assignment(deps.storage, sorted).unwrap(); + save_assignment(deps.storage, unsorted).unwrap(); + + let storage = deps.as_ref().storage; + + let active_bucket = ACTIVE_ROLES_BUCKET.load(storage).unwrap(); + let inactive = active_bucket.other() as u8; + let metadata = ROLES_METADATA.load(storage, inactive).unwrap(); + + assert_eq!(metadata.entry_gateway_metadata.highest_id, 3); + assert_eq!(metadata.layer1_metadata.highest_id, 8); + assert_eq!(metadata.highest_rewarded_id(), 8) + } +} diff --git a/contracts/mixnet/src/nodes/storage/mod.rs b/contracts/mixnet/src/nodes/storage/mod.rs new file mode 100644 index 0000000000..63584010a6 --- /dev/null +++ b/contracts/mixnet/src/nodes/storage/mod.rs @@ -0,0 +1,128 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::{ + NODE_ID_COUNTER_KEY, NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY, NYMNODE_IDENTITY_IDX_NAMESPACE, + NYMNODE_OWNER_IDX_NAMESPACE, NYMNODE_PK_NAMESPACE, NYMNODE_REWARDED_SET_METADATA_NAMESPACE, + NYMNODE_ROLES_ASSIGNMENT_NAMESPACE, PENDING_NYMNODE_CHANGES_NAMESPACE, + UNBONDED_NYMNODE_IDENTITY_IDX_NAMESPACE, UNBONDED_NYMNODE_OWNER_IDX_NAMESPACE, + UNBONDED_NYMNODE_PK_NAMESPACE, +}; +use crate::nodes::storage::helpers::RoleStorageBucket; +use cosmwasm_std::Addr; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, Map, MultiIndex, UniqueIndex}; +use mixnet_contract_common::nym_node::{NymNodeBond, RewardedSetMetadata, Role, UnbondedNymNode}; +use mixnet_contract_common::{NodeId, PendingNodeChanges}; +use nym_contracts_common::IdentityKey; + +pub(crate) mod helpers; + +pub(crate) use helpers::*; + +// IMPORTANT NOTE: we're using the same storage key as we had for MIXNODE_ID_COUNTER, +// so that we could start from the old values +pub const NYMNODE_ID_COUNTER: Item = Item::new(NODE_ID_COUNTER_KEY); + +// each nym-node has 3 storage buckets: +// - `NymNodeBondIndex` to keep track of the actual node information +// - `PENDING_NYMNODE_CHANGES` to keep track of current params/pledge changes +// - `rewards_storage::NYMNODE_REWARDING` to keep track of data needed for reward calculation + +pub const PENDING_NYMNODE_CHANGES: Map = + Map::new(PENDING_NYMNODE_CHANGES_NAMESPACE); + +pub mod rewarded_set { + use super::*; + + // role assignment period is an awkward time for querying for up-to-date data + // for example if we have assigned layer1 and layer2 but not yet touched layer3, + // the state would be inconsistent since it'd have data of layer3 from previous epoch + // + // thus we just toggle the virtual pointer between 2 buckets + // since we also don't want to keep state for all epochs. + // + // general rule of thumb: we're always READING from the active bucket, + // but we're WRITING to the inactive bucket (because it's still being built) + /// Item keeping track of the current active node assignment + pub const ACTIVE_ROLES_BUCKET: Item = + Item::new(NYMNODE_ACTIVE_ROLE_ASSIGNMENT_KEY); + + // NOTES FOR FUTURE IMPLEMENTATION: + // to implement pre-announcement of nodes, you don't have to do much. literally almost nothing at all, + // you'd just have to expose the current inactive bucket and make sure to correctly invalidate it when being written to + + // it feels more efficient to have a single bulk read/write operation per role + // as opposed to storing everything under separate keys. + // however, the drawback is a potentially huge writing cost, but I don't think + // we're going to have 1k+ nodes per layer any time soon for it to be a problem + // + // note: the actual resolution of which node id corresponds to which ip/identity + // is left to up the caller + // + // storage note: we use `u8` rather than `RoleStorageBucket` in the composite key + // to avoid having to derive all required traits + /// Storage map of `(RoleStorageBucket, Role)` => set of nodes with that assigned role + pub const ROLES: Map<(u8, Role), Vec> = Map::new(NYMNODE_ROLES_ASSIGNMENT_NAMESPACE); + + /// Storage map of metadata associated with particular `RoleStorageBucket` + pub const ROLES_METADATA: Map = + Map::new(NYMNODE_REWARDED_SET_METADATA_NAMESPACE); +} + +pub(crate) struct NymNodeBondIndex<'a> { + pub(crate) owner: UniqueIndex<'a, Addr, NymNodeBond>, + + pub(crate) identity_key: UniqueIndex<'a, IdentityKey, NymNodeBond>, +} + +impl IndexList for NymNodeBondIndex<'_> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.owner, &self.identity_key]; + Box::new(v.into_iter()) + } +} + +// nym_nodes() is the storage access function. +pub(crate) fn nym_nodes<'a>() -> IndexedMap<'a, NodeId, NymNodeBond, NymNodeBondIndex<'a>> { + let indexes = NymNodeBondIndex { + owner: UniqueIndex::new(|d| d.owner.clone(), NYMNODE_OWNER_IDX_NAMESPACE), + identity_key: UniqueIndex::new( + |d| d.node.identity_key.clone(), + NYMNODE_IDENTITY_IDX_NAMESPACE, + ), + }; + IndexedMap::new(NYMNODE_PK_NAMESPACE, indexes) +} + +// keeps track of `node_id -> IdentityKey, Owner, unbonding_height` so we'd known a bit more about past nodes +// if we ever decide it's too bloaty, we can deprecate it and start removing all data in +// subsequent migrations +pub(crate) struct UnbondedNymNodeIndex<'a> { + pub(crate) owner: MultiIndex<'a, Addr, UnbondedNymNode, NodeId>, + + pub(crate) identity_key: MultiIndex<'a, IdentityKey, UnbondedNymNode, NodeId>, +} + +impl IndexList for UnbondedNymNodeIndex<'_> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.owner, &self.identity_key]; + Box::new(v.into_iter()) + } +} + +pub(crate) fn unbonded_nym_nodes<'a>( +) -> IndexedMap<'a, NodeId, UnbondedNymNode, UnbondedNymNodeIndex<'a>> { + let indexes = UnbondedNymNodeIndex { + owner: MultiIndex::new( + |_pk, d| d.owner.clone(), + UNBONDED_NYMNODE_PK_NAMESPACE, + UNBONDED_NYMNODE_OWNER_IDX_NAMESPACE, + ), + identity_key: MultiIndex::new( + |_pk, d| d.identity_key.clone(), + UNBONDED_NYMNODE_PK_NAMESPACE, + UNBONDED_NYMNODE_IDENTITY_IDX_NAMESPACE, + ), + }; + IndexedMap::new(UNBONDED_NYMNODE_PK_NAMESPACE, indexes) +} diff --git a/contracts/mixnet/src/nodes/transactions.rs b/contracts/mixnet/src/nodes/transactions.rs new file mode 100644 index 0000000000..646bd48dd9 --- /dev/null +++ b/contracts/mixnet/src/nodes/transactions.rs @@ -0,0 +1,264 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::compat::helpers::{ + ensure_can_decrease_pledge, ensure_can_increase_pledge, ensure_can_modify_cost_params, +}; +use crate::interval::storage as interval_storage; +use crate::interval::storage::push_new_interval_event; +use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::nodes::helpers::{must_get_node_bond_by_owner, save_new_nymnode}; +use crate::nodes::signature_helpers::verify_bonding_signature; +use crate::nodes::storage; +use crate::nodes::storage::set_unbonding; +use crate::signing::storage as signing_storage; +use crate::support::helpers::{ + ensure_epoch_in_progress_state, ensure_no_existing_bond, ensure_operating_cost_within_range, + ensure_profit_margin_within_range, validate_pledge, +}; +use cosmwasm_std::{coin, Coin, DepsMut, Env, MessageInfo, Response}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::events::{ + new_nym_node_bonding_event, new_pending_cost_params_update_event, + new_pending_nym_node_unbonding_event, new_pending_pledge_decrease_event, + new_pending_pledge_increase_event, +}; +use mixnet_contract_common::nym_node::{NodeConfigUpdate, NymNode}; +use mixnet_contract_common::{ + NodeCostParams, NymNodeBondingPayload, NymNodeDetails, PendingEpochEventKind, + PendingIntervalEventKind, +}; +use nym_contracts_common::signing::{MessageSignature, SigningPurpose}; +use serde::Serialize; + +pub fn try_add_nym_node( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + nym_node: NymNode, + cost_params: NodeCostParams, + owner_signature: MessageSignature, +) -> Result { + // TODO: here be backwards compatibility checks for making sure there's no pre-existing mixnode/gateway + + add_nym_node_inner( + deps, + env, + info, + nym_node.clone(), + cost_params.clone(), + owner_signature, + NymNodeBondingPayload::new(nym_node, cost_params), + ) +} + +// allow bonding nym-node through mixnode/gateway entry points for backwards compatibility, +// and make sure to check correct signatures +pub(crate) fn add_nym_node_inner( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + nym_node: NymNode, + cost_params: NodeCostParams, + owner_signature: MessageSignature, + signed_message_payload: T, +) -> Result +where + T: SigningPurpose + Serialize, +{ + // ensure the provided values for host and public key are not insane + nym_node.ensure_host_in_range()?; + nym_node.naive_ensure_valid_pubkey()?; + + // check if the pledge contains any funds of the appropriate denomination + let minimum_pledge = mixnet_params_storage::minimum_node_pledge(deps.storage)?; + let pledge = validate_pledge(info.funds, minimum_pledge)?; + + // ensure the profit margin is within the defined range + ensure_profit_margin_within_range(deps.storage, cost_params.profit_margin_percent)?; + + // ensure the operating cost is within the defined range + ensure_operating_cost_within_range(deps.storage, &cost_params.interval_operating_cost)?; + + // if the client has an active bonded [legacy] mixnode, [legacy] gateway or a nym-node, don't allow bonding + // note that this has to be done explicitly as `UniqueIndex` constraint would not protect us + // against attempting to use different node types (i.e. gateways and mixnodes) + ensure_no_existing_bond(&info.sender, deps.storage)?; + + // there's no need to explicitly check whether there already exists nymnode with the same + // identity as this is going to be done implicitly when attempting to save + // the bond information due to `UniqueIndex` constraint defined on that field. + + // check if this sender actually owns the node by checking the signature + verify_bonding_signature( + deps.as_ref(), + info.sender.clone(), + &nym_node.identity_key, + pledge.clone(), + signed_message_payload, + owner_signature, + )?; + + // update the signing nonce associated with this sender so that the future signature would be made on the new value + signing_storage::increment_signing_nonce(deps.storage, info.sender.clone())?; + + let node_identity = nym_node.identity_key.clone(); + let node_id = save_new_nymnode( + deps.storage, + env.block.height, + nym_node, + cost_params, + info.sender.clone(), + pledge.clone(), + )?; + + Ok(Response::new().add_event(new_nym_node_bonding_event( + &info.sender, + &pledge, + &node_identity, + node_id, + ))) +} + +pub(crate) fn try_remove_nym_node( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, +) -> Result { + let existing_bond = must_get_node_bond_by_owner(deps.storage, &info.sender)?; + let pending_changes = + storage::PENDING_NYMNODE_CHANGES.load(deps.storage, existing_bond.node_id)?; + + // unbonding is only allowed if the epoch is currently not in the process of being advanced + ensure_epoch_in_progress_state(deps.storage)?; + + // see if the proxy matches + existing_bond.ensure_bonded()?; + + // if there are any pending requests to change the pledge, wait for them to resolve before allowing the unbonding + pending_changes.ensure_no_pending_pledge_changes()?; + + // set `is_unbonding` field + set_unbonding(deps.storage, &existing_bond)?; + + // push the event to execute it at the end of the epoch + let epoch_event = PendingEpochEventKind::UnbondNymNode { + node_id: existing_bond.node_id, + }; + interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; + + Ok( + Response::new().add_event(new_pending_nym_node_unbonding_event( + &existing_bond.owner, + existing_bond.identity(), + existing_bond.node_id, + )), + ) +} + +pub(crate) fn try_update_node_config( + deps: DepsMut<'_>, + info: MessageInfo, + update: NodeConfigUpdate, +) -> Result { + let existing_bond = must_get_node_bond_by_owner(deps.storage, &info.sender)?; + existing_bond.ensure_bonded()?; + + let mut updated_bond = existing_bond.clone(); + + if let Some(updated_host) = update.host { + updated_bond.node.host = updated_host; + } + + if let Some(updated_custom_http_port) = update.custom_http_port { + updated_bond.node.custom_http_port = Some(updated_custom_http_port); + } + + if update.restore_default_http_port { + updated_bond.node.custom_http_port = None + } + + storage::nym_nodes().replace( + deps.storage, + existing_bond.node_id, + Some(&updated_bond), + Some(&existing_bond), + )?; + + Ok(Response::new()) +} + +pub(crate) fn try_increase_nym_node_pledge( + deps: DepsMut<'_>, + env: Env, + increase: Vec, + node_details: NymNodeDetails, +) -> Result { + let mut pending_changes = node_details.pending_changes; + let node_id = node_details.node_id(); + + ensure_can_increase_pledge(deps.storage, &node_details)?; + + let rewarding_denom = &node_details.original_pledge().denom; + let pledge_increase = validate_pledge(increase, coin(1, rewarding_denom))?; + + let cosmos_event = new_pending_pledge_increase_event(node_id, &pledge_increase); + + // push the event to execute it at the end of the epoch + let epoch_event = PendingEpochEventKind::NymNodePledgeMore { + node_id, + amount: pledge_increase, + }; + let epoch_event_id = interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; + pending_changes.pledge_change = Some(epoch_event_id); + storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; + + Ok(Response::new().add_event(cosmos_event)) +} + +pub(crate) fn try_decrease_nym_node_pledge( + deps: DepsMut<'_>, + env: Env, + decrease_by: Coin, + node_details: NymNodeDetails, +) -> Result { + let mut pending_changes = node_details.pending_changes; + let node_id = node_details.node_id(); + + ensure_can_decrease_pledge(deps.storage, &node_details, &decrease_by)?; + + let cosmos_event = new_pending_pledge_decrease_event(node_id, &decrease_by); + + // push the event to execute it at the end of the epoch + let epoch_event = PendingEpochEventKind::NymNodeDecreasePledge { + node_id, + decrease_by, + }; + let epoch_event_id = interval_storage::push_new_epoch_event(deps.storage, &env, epoch_event)?; + pending_changes.pledge_change = Some(epoch_event_id); + storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; + + Ok(Response::new().add_event(cosmos_event)) +} + +pub(crate) fn try_update_nym_node_cost_params( + deps: DepsMut, + env: Env, + new_costs: NodeCostParams, + node_details: NymNodeDetails, +) -> Result { + let mut pending_changes = node_details.pending_changes; + let node_id = node_details.node_id(); + + ensure_can_modify_cost_params(deps.storage, &node_details)?; + + let cosmos_event = new_pending_cost_params_update_event(node_id, &new_costs); + + // push the interval event + let interval_event = PendingIntervalEventKind::ChangeNymNodeCostParams { node_id, new_costs }; + let interval_event_id = push_new_interval_event(deps.storage, &env, interval_event)?; + pending_changes.cost_params_change = Some(interval_event_id); + storage::PENDING_NYMNODE_CHANGES.save(deps.storage, node_id, &pending_changes)?; + + Ok(Response::new().add_event(cosmos_event)) +} diff --git a/contracts/mixnet/src/queued_migrations.rs b/contracts/mixnet/src/queued_migrations.rs index a0273196be..007a2f376f 100644 --- a/contracts/mixnet/src/queued_migrations.rs +++ b/contracts/mixnet/src/queued_migrations.rs @@ -1,2 +1,412 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 + +mod families_purge { + use cosmwasm_std::{DepsMut, Order, StdResult}; + use cw_storage_plus::{Index, IndexList, IndexedMap, Map, UniqueIndex}; + use mixnet_contract_common::error::MixnetContractError; + use nym_contracts_common::IdentityKey; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub const FAMILIES_INDEX_NAMESPACE: &str = "faml2"; + pub const FAMILIES_MAP_NAMESPACE: &str = "fam2"; + pub const MEMBERS_MAP_NAMESPACE: &str = "memb2"; + + type FamilyHeadKey = IdentityKey; + + #[derive(Serialize, Deserialize, Clone)] + pub struct Family { + /// Owner of this family. + head: FamilyHead, + + /// Optional proxy (i.e. vesting contract address) used when creating the family. + proxy: Option, + + /// Human readable label for this family. + label: String, + } + + #[derive(Debug, Clone, Eq, PartialEq)] + pub struct FamilyHead(IdentityKey); + + impl Serialize for FamilyHead { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.0.serialize(serializer) + } + } + + impl<'de> Deserialize<'de> for FamilyHead { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let inner = IdentityKey::deserialize(deserializer)?; + Ok(FamilyHead(inner)) + } + } + + pub struct FamilyIndex<'a> { + pub label: UniqueIndex<'a, FamilyHeadKey, Family>, + } + + impl IndexList for FamilyIndex<'_> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.label]; + Box::new(v.into_iter()) + } + } + + pub fn families<'a>() -> IndexedMap<'a, FamilyHeadKey, Family, FamilyIndex<'a>> { + let indexes = FamilyIndex { + label: UniqueIndex::new(|d| d.label.to_string(), FAMILIES_INDEX_NAMESPACE), + }; + IndexedMap::new(FAMILIES_MAP_NAMESPACE, indexes) + } + + pub const MEMBERS: Map = Map::new(MEMBERS_MAP_NAMESPACE); + + pub(crate) fn families_purge(deps: DepsMut) -> Result<(), MixnetContractError> { + // we don't care about values, we are only concerned with keys + let family_keys = families() + .keys(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + for family in family_keys { + families().remove(deps.storage, family)?; + } + + let member_keys = MEMBERS + .keys(deps.storage, None, None, Order::Ascending) + .collect::>>()?; + for member in member_keys { + MEMBERS.remove(deps.storage, member); + } + + Ok(()) + } +} + +mod nym_nodes_usage { + use crate::constants::{CONTRACT_STATE_KEY, REWARDING_PARAMS_KEY}; + use crate::interval::storage::current_interval; + use crate::mixnet_contract_settings::storage::CONTRACT_STATE; + use crate::nodes::storage::helpers::RoleStorageBucket; + use crate::nodes::storage::rewarded_set::{ACTIVE_ROLES_BUCKET, ROLES, ROLES_METADATA}; + use crate::rewards::storage::RewardingStorage; + use crate::support::helpers::ensure_epoch_in_progress_state; + use cosmwasm_std::{Addr, Coin, DepsMut, Order, StdResult, Storage}; + use cw_storage_plus::{Item, Map}; + use mixnet_contract_common::error::MixnetContractError; + use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role}; + use mixnet_contract_common::reward_params::RewardedSetParams; + use mixnet_contract_common::{ + ContractState, ContractStateParams, IntervalRewardParams, MigrateMsg, NodeId, + OperatingCostRange, PendingIntervalEvent, PendingIntervalEventKind, ProfitMarginRange, + RewardingParams, + }; + use serde::{Deserialize, Serialize}; + + fn migrate_contract_state(storage: &mut dyn Storage) -> Result<(), MixnetContractError> { + #[derive(Serialize, Deserialize)] + struct OldContractState { + owner: Option, + rewarding_validator_address: Addr, + vesting_contract_address: Addr, + rewarding_denom: String, + params: OldContractStateParams, + } + + #[derive(Serialize, Deserialize)] + struct OldContractStateParams { + minimum_mixnode_delegation: Option, + minimum_mixnode_pledge: Coin, + minimum_gateway_pledge: Coin, + #[serde(default)] + profit_margin: ProfitMarginRange, + #[serde(default)] + interval_operating_cost: OperatingCostRange, + } + + let old_state_entry = Item::new(CONTRACT_STATE_KEY); + let old_state: OldContractState = old_state_entry.load(storage)?; + + #[allow(deprecated)] + CONTRACT_STATE.save( + storage, + &ContractState { + owner: old_state.owner, + rewarding_validator_address: old_state.rewarding_validator_address, + vesting_contract_address: old_state.vesting_contract_address, + rewarding_denom: old_state.rewarding_denom, + params: ContractStateParams { + minimum_delegation: old_state.params.minimum_mixnode_delegation, + // just use the same value for nym-node pledge as we have for mixnodes + minimum_pledge: old_state.params.minimum_mixnode_pledge, + profit_margin: old_state.params.profit_margin, + interval_operating_cost: old_state.params.interval_operating_cost, + }, + }, + )?; + + Ok(()) + } + + fn migrate_pending_interval_changes( + storage: &mut dyn Storage, + ) -> Result<(), MixnetContractError> { + // at the time of writing this migration there were just 15 pending interval events, + // so if we stay within this order of magnitude, it's quite safe to just grab all of them + let events = crate::interval::storage::PENDING_INTERVAL_EVENTS + .range(storage, None, None, Order::Ascending) + .map(|res| res.map(|row| row.into())) + .collect::>>()?; + + for event in events { + if let PendingIntervalEventKind::ChangeMixCostParams { mix_id, .. } = event.event.kind { + let mut pending = crate::mixnodes::storage::PENDING_MIXNODE_CHANGES + .may_load(storage, mix_id)? + .unwrap_or_default(); + pending.cost_params_change = Some(event.id); + crate::mixnodes::storage::PENDING_MIXNODE_CHANGES + .save(storage, mix_id, &pending)?; + } + } + + Ok(()) + } + + fn preassign_gateway_ids( + storage: &mut dyn Storage, + ) -> Result<(Option, Option), MixnetContractError> { + // that one is a big if. we have ~100 gateways so we **might** be able to fit it within migration. + // if not, then we'll have to do it in batches/change our approach + + let gateways = crate::gateways::storage::gateways() + .range(storage, None, None, Order::Ascending) + .map(|res| res.map(|row| row.1)) + .collect::>>()?; + + let mut start = None; + let mut end = None; + for gateway in gateways { + let id = crate::nodes::storage::next_nymnode_id_counter(storage)?; + if start.is_none() { + start = Some(id) + } + end = Some(id); + + crate::gateways::storage::PREASSIGNED_LEGACY_IDS.save( + storage, + gateway.gateway.identity_key, + &id, + )?; + } + + Ok((start, end)) + } + + fn cleanup_legacy_storage( + storage: &mut dyn Storage, + ) -> Result, MixnetContractError> { + #[derive(Copy, Clone, Default, Serialize, Deserialize)] + pub struct LayerDistribution { + pub layer1: u64, + pub layer2: u64, + pub layer3: u64, + } + pub const LAYERS: Item<'_, LayerDistribution> = Item::new("layers"); + + #[derive(Copy, Clone, Serialize, Deserialize)] + #[serde(deny_unknown_fields, rename_all = "snake_case")] + pub enum RewardedSetNodeStatus { + /// Node that is currently active, i.e. is expected to be used by clients for mixing packets. + #[serde(alias = "Active")] + Active, + + /// Node that is currently in standby, i.e. it's present in the rewarded set but is not active. + #[serde(alias = "Standby")] + Standby, + } + pub(crate) const REWARDED_SET: Map = Map::new("rs"); + + // remove explicit layer assignment -> got replaced with role assignment + LAYERS.remove(storage); + + // remove every node from the legacy rewarded set + let rewarded_ids = REWARDED_SET + .keys(storage, None, None, Order::Ascending) + .collect::, _>>()?; + + for &node_id in &rewarded_ids { + REWARDED_SET.remove(storage, node_id) + } + + Ok(rewarded_ids) + } + + fn migrate_rewarded_set_params(storage: &mut dyn Storage) -> Result<(), MixnetContractError> { + #[derive(Copy, Clone, Serialize, Deserialize)] + pub struct LegacyRewardingParams { + pub interval: IntervalRewardParams, + pub rewarded_set_size: u32, + pub active_set_size: u32, + } + pub(crate) const REWARDING_PARAMS: Item<'_, LegacyRewardingParams> = + Item::new(REWARDING_PARAMS_KEY); + + let legacy = REWARDING_PARAMS.load(storage)?; + + // our mainnet assumption. we could work around it, + // but what's the point of the extra logic if we might not need it? + if legacy.rewarded_set_size != 240 || legacy.active_set_size != 240 { + return Err(MixnetContractError::FailedMigration { + comment: "the current active or rewarded set size is not 240 (the expected value for mainnet)".to_string(), + }); + } + + let updated = RewardingParams { + interval: legacy.interval, + rewarded_set: RewardedSetParams { + entry_gateways: 50, + exit_gateways: 70, + mixnodes: 120, + standby: 0, + }, + }; + + RewardingStorage::load() + .global_rewarding_params + .save(storage, &updated)?; + + Ok(()) + } + + fn assign_temporary_rewarded_set( + storage: &mut dyn Storage, + (min_available_gateway, max_available_gateway): (Option, Option), + current_rewarded_set_mixnodes: Vec, + ) -> Result<(), MixnetContractError> { + let epoch_id = current_interval(storage)?.current_epoch_absolute_id(); + + // in the previous step we explicitly set rewarded set to 120 mixnodes and 50 entry gateways + // note: we can't assign exit gateways because the contract itself doesn't know which might support it + + let active_bucket = RoleStorageBucket::default(); + let inactive_bucket = active_bucket.other(); + ACTIVE_ROLES_BUCKET.save(storage, &active_bucket)?; + + // ACTIVE BUCKET: + let mut active_metadata = RewardedSetMetadata::new(epoch_id); + + let mut current_rewarded_set_mixnodes = current_rewarded_set_mixnodes; + // ensure it's sorted. it should have already been, but better safe than sorry.. + current_rewarded_set_mixnodes.sort(); + + let mut layer1 = Vec::new(); + let mut layer2 = Vec::new(); + let mut layer3 = Vec::new(); + let mut entry = Vec::new(); + + for (i, mix_id) in current_rewarded_set_mixnodes + .into_iter() + .take(120) + .enumerate() + { + if i % 3 == 0 { + layer1.push(mix_id); + } else if i % 3 == 1 { + layer2.push(mix_id); + } else if i % 3 == 2 { + layer3.push(mix_id); + } + } + + if let (Some(min_id), Some(max_id)) = (min_available_gateway, max_available_gateway) { + // we can assign the gateway nodes + entry = (min_id..=max_id).take(50).collect(); + } + + // ACTIVE BUCKET: + active_metadata.fully_assigned = true; + + // layer1 + ROLES.save(storage, (active_bucket as u8, Role::Layer1), &layer1)?; + active_metadata.layer1_metadata.num_nodes = layer1.len() as u32; + active_metadata.layer1_metadata.highest_id = layer1.last().copied().unwrap_or_default(); + + // layer2 + ROLES.save(storage, (active_bucket as u8, Role::Layer2), &layer2)?; + active_metadata.layer2_metadata.num_nodes = layer2.len() as u32; + active_metadata.layer2_metadata.highest_id = layer2.last().copied().unwrap_or_default(); + + // layer3 + ROLES.save(storage, (active_bucket as u8, Role::Layer3), &layer3)?; + active_metadata.layer3_metadata.num_nodes = layer3.len() as u32; + active_metadata.layer3_metadata.highest_id = layer3.last().copied().unwrap_or_default(); + + // entry + ROLES.save(storage, (active_bucket as u8, Role::EntryGateway), &entry)?; + active_metadata.entry_gateway_metadata.num_nodes = entry.len() as u32; + active_metadata.entry_gateway_metadata.highest_id = + entry.last().copied().unwrap_or_default(); + + // nothing for exit or standby + ROLES.save(storage, (active_bucket as u8, Role::ExitGateway), &vec![])?; + ROLES.save(storage, (active_bucket as u8, Role::Standby), &vec![])?; + ROLES_METADATA.save(storage, active_bucket as u8, &active_metadata)?; + + // SECONDARY BUCKET + let roles = vec![ + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::EntryGateway, + Role::ExitGateway, + Role::Standby, + ]; + for role in roles { + ROLES.save(storage, (inactive_bucket as u8, role), &vec![])? + } + + ROLES_METADATA.save(storage, inactive_bucket as u8, &Default::default())?; + + Ok(()) + } + + pub(crate) fn migrate_to_nym_nodes_usage( + deps: DepsMut<'_>, + _msg: &MigrateMsg, + ) -> Result<(), MixnetContractError> { + // ensure we're not migrating mid-epoch progression, or we're gonna have bad time + ensure_epoch_in_progress_state(deps.storage)?; + + // update the contract state structure (remove separate mixnode/gateway pledge amount) + migrate_contract_state(deps.storage)?; + + // make sure to assign pending cost params changes to mixnodes so those nodes couldn't be migrated + // to nym-nodes until the events are resolved + migrate_pending_interval_changes(deps.storage)?; + + // pre-assign NodeId to all gateways (that will be applied during nym-node migration) + // to simplify all other code during the intermediate period + let gateways = preassign_gateway_ids(deps.storage)?; + + // update the simple active/rewarded set sizes to actually contain the distribution of roles + migrate_rewarded_set_params(deps.storage)?; + + // remove all redundant storage items + let old_rewarded_set_mixnodes = cleanup_legacy_storage(deps.storage)?; + + // assign initial rewarded set + // and initialise all the storage structures required by nym-nodes + // based on the nodes that are in the contract right now + assign_temporary_rewarded_set(deps.storage, gateways, old_rewarded_set_mixnodes)?; + + Ok(()) + } +} + +pub(crate) use families_purge::families_purge; +pub(crate) use nym_nodes_usage::migrate_to_nym_nodes_usage; diff --git a/contracts/mixnet/src/rewards/helpers.rs b/contracts/mixnet/src/rewards/helpers.rs index 689c263efb..fba530e334 100644 --- a/contracts/mixnet/src/rewards/helpers.rs +++ b/contracts/mixnet/src/rewards/helpers.rs @@ -4,16 +4,18 @@ use super::storage; use crate::delegations::storage as delegations_storage; use crate::interval::storage as interval_storage; +use crate::nodes::storage::read_assigned_roles; use cosmwasm_std::{Coin, Storage}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::helpers::IntoBaseDecimal; -use mixnet_contract_common::mixnode::{MixNodeDetails, MixNodeRewarding}; -use mixnet_contract_common::{Delegation, EpochState, EpochStatus, MixId}; +use mixnet_contract_common::helpers::{IntoBaseDecimal, NodeBond, NodeDetails}; +use mixnet_contract_common::mixnode::NodeRewarding; +use mixnet_contract_common::nym_node::Role; +use mixnet_contract_common::{Delegation, EpochState, EpochStatus, NodeId}; pub(crate) fn update_and_save_last_rewarded( storage: &mut dyn Storage, mut current_epoch_status: EpochStatus, - new_last_rewarded: MixId, + new_last_rewarded: NodeId, ) -> Result<(), MixnetContractError> { let is_done = current_epoch_status.update_last_rewarded(new_last_rewarded)?; if is_done { @@ -39,7 +41,7 @@ pub(crate) fn apply_reward_pool_changes( let epoch_reward_budget = reward_pool / interval.epochs_in_interval().into_base_decimal()? * rewarding_params.interval.interval_pool_emission; let stake_saturation_point = - staking_supply / rewarding_params.rewarded_set_size.into_base_decimal()?; + staking_supply / rewarding_params.rewarded_set_size().into_base_decimal()?; rewarding_params.interval.reward_pool = reward_pool; rewarding_params.interval.staking_supply = staking_supply; @@ -52,26 +54,29 @@ pub(crate) fn apply_reward_pool_changes( Ok(()) } -pub(crate) fn withdraw_operator_reward( +pub(crate) fn withdraw_operator_reward( store: &mut dyn Storage, - mix_details: MixNodeDetails, -) -> Result { - let mix_id = mix_details.mix_id(); - let mut mix_rewarding = mix_details.rewarding_details; - let original_pledge = mix_details.bond_information.original_pledge; - let reward = mix_rewarding.withdraw_operator_reward(&original_pledge)?; + node_details: D, +) -> Result +where + D: NodeDetails, +{ + let (bond_info, mut node_rewarding, _) = node_details.split(); + let node_id = bond_info.node_id(); + let original_pledge = bond_info.original_pledge(); + let reward = node_rewarding.withdraw_operator_reward(original_pledge)?; // save updated rewarding info - storage::MIXNODE_REWARDING.save(store, mix_id, &mix_rewarding)?; + storage::NYMNODE_REWARDING.save(store, node_id, &node_rewarding)?; Ok(reward) } pub(crate) fn withdraw_delegator_reward( store: &mut dyn Storage, delegation: Delegation, - mut mix_rewarding: MixNodeRewarding, + mut mix_rewarding: NodeRewarding, ) -> Result { - let mix_id = delegation.mix_id; + let mix_id = delegation.node_id; let mut updated_delegation = delegation.clone(); let reward = mix_rewarding.withdraw_delegator_reward(&mut updated_delegation)?; @@ -86,12 +91,53 @@ pub(crate) fn withdraw_delegator_reward( Ok(reward) } +pub(crate) fn ensure_assignment( + storage: &dyn Storage, + node_id: NodeId, + role: Role, +) -> Result<(), MixnetContractError> { + // that's a bit expensive to read the whole thing each time, but I'm not sure if there's a much better way + // (creating a reverse map would be more expensive in the long run due to writes being more costly than reads) + let assignment = read_assigned_roles(storage, role)?; + if !assignment.contains(&node_id) { + return Err(MixnetContractError::IncorrectEpochRole { node_id, role }); + } + Ok(()) +} + +// this is **ONLY** to be used in queries +// unless a better way can be figured out +pub(crate) fn expensive_role_lookup( + storage: &dyn Storage, + node_id: NodeId, +) -> Result, MixnetContractError> { + if ensure_assignment(storage, node_id, Role::EntryGateway).is_ok() { + return Ok(Some(Role::EntryGateway)); + } + if ensure_assignment(storage, node_id, Role::ExitGateway).is_ok() { + return Ok(Some(Role::ExitGateway)); + } + if ensure_assignment(storage, node_id, Role::Layer1).is_ok() { + return Ok(Some(Role::Layer1)); + } + if ensure_assignment(storage, node_id, Role::Layer2).is_ok() { + return Ok(Some(Role::Layer2)); + } + if ensure_assignment(storage, node_id, Role::Layer3).is_ok() { + return Ok(Some(Role::Layer3)); + } + if ensure_assignment(storage, node_id, Role::Standby).is_ok() { + return Ok(Some(Role::Standby)); + } + Ok(None) +} + #[cfg(test)] mod tests { use super::*; use crate::mixnodes::helpers::get_mixnode_details_by_id; use crate::rewards::models::RewardPoolChange; - use crate::support::tests::test_helpers::{assert_decimals, performance, TestSetup}; + use crate::support::tests::test_helpers::{assert_decimals, TestSetup}; use cosmwasm_std::Uint128; use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; @@ -145,10 +191,7 @@ mod tests { assert_eq!( updated_rewarding_params.interval.stake_saturation_point, updated_rewarding_params.interval.staking_supply - / updated_rewarding_params - .rewarded_set_size - .into_base_decimal() - .unwrap() + / updated_rewarding_params.dec_rewarded_set_size() ); // resets changes back to 0 @@ -197,10 +240,7 @@ mod tests { assert_eq!( updated_rewarding_params2.interval.stake_saturation_point, updated_rewarding_params2.interval.staking_supply - / updated_rewarding_params2 - .rewarded_set_size - .into_base_decimal() - .unwrap() + / updated_rewarding_params2.dec_rewarded_set_size() ); // resets changes back to 0 @@ -218,7 +258,8 @@ mod tests { let pledge = Uint128::new(250_000_000); let pledge_dec = 250_000_000u32.into_base_decimal().unwrap(); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(pledge)); + let mix_id = test.add_legacy_mixnode("mix-owner", Some(pledge)); + let active_params = test.active_node_params(100.0); // no rewards let mix_details = get_mixnode_details_by_id(test.deps().storage, mix_id) @@ -228,14 +269,14 @@ mod tests { assert_eq!(res.amount, Uint128::zero()); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - let dist1 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + test.force_change_mix_rewarded_set(vec![mix_id]); + let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist2 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist3 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist3 = test.reward_with_distribution_ignore_state(mix_id, active_params); let mix_details = get_mixnode_details_by_id(test.deps().storage, mix_id) .unwrap() @@ -252,10 +293,11 @@ mod tests { #[test] fn withdrawing_delegator_reward() { let mut test = TestSetup::new(); + let active_params = test.active_node_params(100.0); let delegation_amount = Uint128::new(2_500_000_000); let delegation_dec = 2_500_000_000_u32.into_base_decimal().unwrap(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_legacy_mixnode("mix-owner", None); let delegator = "delegator"; test.add_immediate_delegation(delegator, delegation_amount, mix_id); @@ -267,14 +309,14 @@ mod tests { assert_eq!(res.amount, Uint128::zero()); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - let dist1 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + test.force_change_mix_rewarded_set(vec![mix_id]); + let dist1 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist2 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist2 = test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - let dist3 = test.reward_with_distribution_with_state_bypass(mix_id, performance(100.0)); + let dist3 = test.reward_with_distribution_ignore_state(mix_id, active_params); let delegation_pre = test.delegation(mix_id, delegator, &None); let mix_rewarding = test.mix_rewarding(mix_id); diff --git a/contracts/mixnet/src/rewards/queries.rs b/contracts/mixnet/src/rewards/queries.rs index 87df0f9fc8..64f55bcec9 100644 --- a/contracts/mixnet/src/rewards/queries.rs +++ b/contracts/mixnet/src/rewards/queries.rs @@ -1,39 +1,28 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use super::storage; +use super::{helpers, storage}; +use crate::compat; +use crate::compat::helpers::may_get_bond; use crate::delegations::storage as delegations_storage; use crate::interval::storage as interval_storage; -use crate::mixnodes; -use crate::mixnodes::storage as mixnodes_storage; use cosmwasm_std::{coin, Coin, Decimal, Deps, StdResult}; +use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::helpers::into_base_decimal; -use mixnet_contract_common::mixnode::MixNodeDetails; -use mixnet_contract_common::reward_params::{NodeRewardParams, Performance, RewardingParams}; +use mixnet_contract_common::nym_node::Role; +use mixnet_contract_common::reward_params::{ + NodeRewardingParameters, Performance, RewardingParams, WorkFactor, +}; use mixnet_contract_common::rewarding::helpers::truncate_reward; use mixnet_contract_common::rewarding::{ EstimatedCurrentEpochRewardResponse, PendingRewardResponse, }; -use mixnet_contract_common::{Delegation, MixId}; +use mixnet_contract_common::{Delegation, NodeId}; pub(crate) fn query_rewarding_params(deps: Deps<'_>) -> StdResult { storage::REWARDING_PARAMS.load(deps.storage) } -fn pending_operator_reward( - mix_details: Option, -) -> StdResult { - Ok(match mix_details { - Some(mix_details) => PendingRewardResponse { - amount_staked: Some(mix_details.original_pledge().clone()), - amount_earned: Some(mix_details.pending_operator_reward()), - amount_earned_detailed: Some(mix_details.pending_detailed_operator_reward()?), - mixnode_still_fully_bonded: !mix_details.is_unbonding(), - }, - None => PendingRewardResponse::default(), - }) -} - pub fn query_pending_operator_reward( deps: Deps, owner: String, @@ -41,24 +30,20 @@ pub fn query_pending_operator_reward( let owner_address = deps.api.addr_validate(&owner)?; // in order to determine operator's reward we need to know its original pledge and thus // we have to load the entire thing - let mix_details = mixnodes::helpers::get_mixnode_details_by_owner(deps.storage, owner_address)?; - pending_operator_reward(mix_details) + compat::queries::rewards::pending_operator_reward(deps, owner_address) } pub fn query_pending_mixnode_operator_reward( deps: Deps, - mix_id: MixId, + node_id: NodeId, ) -> StdResult { - // in order to determine operator's reward we need to know its original pledge and thus - // we have to load the entire thing - let mix_details = mixnodes::helpers::get_mixnode_details_by_id(deps.storage, mix_id)?; - pending_operator_reward(mix_details) + compat::queries::rewards::pending_operator_reward_by_id(deps, node_id) } pub fn query_pending_delegator_reward( deps: Deps, owner: String, - mix_id: MixId, + node_id: NodeId, proxy: Option, ) -> StdResult { let owner_address = deps.api.addr_validate(&owner)?; @@ -66,28 +51,32 @@ pub fn query_pending_delegator_reward( .map(|proxy| deps.api.addr_validate(&proxy)) .transpose()?; - let mix_rewarding = match storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { + let node_rewarding = match storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { Some(mix_rewarding) => mix_rewarding, None => return Ok(PendingRewardResponse::default()), }; - let storage_key = Delegation::generate_storage_key(mix_id, &owner_address, proxy.as_ref()); + let storage_key = Delegation::generate_storage_key(node_id, &owner_address, proxy.as_ref()); let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { Some(delegation) => delegation, None => return Ok(PendingRewardResponse::default()), }; - let detailed_reward = mix_rewarding.determine_delegation_reward(&delegation)?; - let delegator_reward = mix_rewarding.pending_delegator_reward(&delegation)?; + let detailed_reward = node_rewarding.determine_delegation_reward(&delegation)?; + let delegator_reward = node_rewarding.pending_delegator_reward(&delegation)?; - // check if the mixnode isnt in the process of unbonding (or has already unbonded) - let is_bonded = matches!(mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)?, Some(mix_bond) if !mix_bond.is_unbonding); + // check if the node isnt in the process of unbonding (or has already unbonded) + let is_bonded = may_get_bond(deps.storage, node_id)? + .map(|b| !b.is_unbonding()) + .unwrap_or_default(); + #[allow(deprecated)] Ok(PendingRewardResponse { amount_staked: Some(delegation.amount), amount_earned: Some(delegator_reward), amount_earned_detailed: Some(detailed_reward), mixnode_still_fully_bonded: is_bonded, + node_still_fully_bonded: is_bonded, }) } @@ -106,40 +95,57 @@ fn zero_reward( pub(crate) fn query_estimated_current_epoch_operator_reward( deps: Deps<'_>, - mix_id: MixId, + node_id: NodeId, estimated_performance: Performance, -) -> StdResult { - let mix_details = match mixnodes::helpers::get_mixnode_details_by_id(deps.storage, mix_id)? { + estimated_work: Option, +) -> Result { + let rewarding_details = match storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()), - Some(mix_details) => mix_details, + Some(info) => info, }; - let amount_staked = mix_details.original_pledge().clone(); - let mix_rewarding = mix_details.rewarding_details; - let current_value = mix_rewarding.operator; + let bond = compat::helpers::get_bond(deps.storage, node_id)?; + + let amount_staked = bond.original_pledge().clone(); + let current_value = rewarding_details.operator; // if node is currently not in the rewarded set, the performance is 0, // or the node has either unbonded or is in the process of unbonding, // the calculations are trivial - the rewards are 0 - if mix_details.bond_information.is_unbonding { + if bond.is_unbonding() { return Ok(zero_reward(amount_staked, current_value)); } - let node_status = match interval_storage::REWARDED_SET.may_load(deps.storage, mix_id)? { - None => return Ok(zero_reward(amount_staked, current_value)), - Some(node_status) => node_status, - }; - if estimated_performance.is_zero() { return Ok(zero_reward(amount_staked, current_value)); } + let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; + + let work_factor = if let Some(work_factor) = estimated_work { + work_factor + } else { + let Some(role) = helpers::expensive_role_lookup(deps.storage, node_id)? else { + return Ok(zero_reward(amount_staked, current_value)); + }; + match role { + Role::EntryGateway | Role::Layer1 | Role::Layer2 | Role::Layer3 | Role::ExitGateway => { + rewarding_params.active_node_work() + } + Role::Standby => rewarding_params.standby_node_work(), + } + }; + + let node_reward_params = NodeRewardingParameters { + performance: estimated_performance, + work_factor, + }; + let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; let interval = interval_storage::current_interval(deps.storage)?; - let node_reward_params = NodeRewardParams::new(estimated_performance, node_status.is_active()); - let node_reward = mix_rewarding.node_reward(&rewarding_params, node_reward_params); - let reward_distribution = mix_rewarding.determine_reward_split( + let node_reward = rewarding_details.node_reward(&rewarding_params, node_reward_params); + let reward_distribution = rewarding_details.determine_reward_split( node_reward, estimated_performance, interval.epochs_in_interval(), @@ -160,55 +166,69 @@ pub(crate) fn query_estimated_current_epoch_operator_reward( pub(crate) fn query_estimated_current_epoch_delegator_reward( deps: Deps<'_>, owner: String, - mix_id: MixId, - proxy: Option, + node_id: NodeId, estimated_performance: Performance, -) -> StdResult { + estimated_work: Option, +) -> Result { let owner_address = deps.api.addr_validate(&owner)?; - let proxy = proxy - .map(|proxy| deps.api.addr_validate(&proxy)) - .transpose()?; - let mix_rewarding = match storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { - Some(mix_rewarding) => mix_rewarding, + let rewarding_details = match storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()), + Some(info) => info, }; - let storage_key = Delegation::generate_storage_key(mix_id, &owner_address, proxy.as_ref()); + let storage_key = Delegation::generate_storage_key(node_id, &owner_address, None); let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { Some(delegation) => delegation, None => return Ok(EstimatedCurrentEpochRewardResponse::empty_response()), }; let staked_dec = into_base_decimal(delegation.amount.amount)?; - let current_value = staked_dec + mix_rewarding.determine_delegation_reward(&delegation)?; + let current_value = staked_dec + rewarding_details.determine_delegation_reward(&delegation)?; let amount_staked = delegation.amount; - // check if the mixnode isnt in the process of unbonding (or has already unbonded) - let is_bonded = matches!(mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)?, Some(mix_bond) if !mix_bond.is_unbonding); - - if !is_bonded { + if estimated_performance.is_zero() { return Ok(zero_reward(amount_staked, current_value)); } - // if node is currently not in the rewarded set, the performance is 0, - // or the node has either unbonded or is in the process of unbonding, - // the calculations are trivial - the rewards are 0 - let node_status = match interval_storage::REWARDED_SET.may_load(deps.storage, mix_id)? { - None => return Ok(zero_reward(amount_staked, current_value)), - Some(node_status) => node_status, + // check if the node isnt in the process of unbonding (or has already unbonded) + let Ok(bond) = compat::helpers::get_bond(deps.storage, node_id) else { + return Ok(zero_reward(amount_staked, current_value)); }; + if bond.is_unbonding() { + return Ok(zero_reward(amount_staked, current_value)); + } + if estimated_performance.is_zero() { return Ok(zero_reward(amount_staked, current_value)); } let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; + + let work_factor = if let Some(work_factor) = estimated_work { + work_factor + } else { + let Some(role) = helpers::expensive_role_lookup(deps.storage, node_id)? else { + return Ok(zero_reward(amount_staked, current_value)); + }; + match role { + Role::EntryGateway | Role::Layer1 | Role::Layer2 | Role::Layer3 | Role::ExitGateway => { + rewarding_params.active_node_work() + } + Role::Standby => rewarding_params.standby_node_work(), + } + }; + + let node_reward_params = NodeRewardingParameters { + performance: estimated_performance, + work_factor, + }; + let interval = interval_storage::current_interval(deps.storage)?; - let node_reward_params = NodeRewardParams::new(estimated_performance, node_status.is_active()); - let node_reward = mix_rewarding.node_reward(&rewarding_params, node_reward_params); - let reward_distribution = mix_rewarding.determine_reward_split( + let node_reward = rewarding_details.node_reward(&rewarding_params, node_reward_params); + let reward_distribution = rewarding_details.determine_reward_split( node_reward, estimated_performance, interval.epochs_in_interval(), @@ -218,7 +238,7 @@ pub(crate) fn query_estimated_current_epoch_delegator_reward( return Ok(zero_reward(amount_staked, current_value)); } - let reward_share = current_value / mix_rewarding.delegates * reward_distribution.delegates; + let reward_share = current_value / rewarding_details.delegates * reward_distribution.delegates; Ok(EstimatedCurrentEpochRewardResponse { estimation: Some(truncate_reward(reward_share, &amount_staked.denom)), @@ -264,7 +284,7 @@ mod tests { assert!(res.amount_earned.is_none()); assert!(res.amount_earned_detailed.is_none()); assert!(res.amount_staked.is_none()); - assert!(!res.mixnode_still_fully_bonded); + assert!(!res.node_still_fully_bonded); } #[test] @@ -273,7 +293,7 @@ mod tests { let owner = "mix-owner"; let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); let res2 = query_pending_mixnode_operator_reward(test.deps(), mix_id).unwrap(); @@ -284,7 +304,7 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), Decimal::zero()); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(res.mixnode_still_fully_bonded); + assert!(res.node_still_fully_bonded); } #[test] @@ -292,16 +312,14 @@ mod tests { let mut test = TestSetup::new(); let owner = "mix-owner"; let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); total_earned += dist.operator; let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); @@ -313,15 +331,12 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(res.mixnode_still_fully_bonded); + assert!(res.node_still_fully_bonded); // reward it few more times for good measure for _ in 0..10 { test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); total_earned += dist.operator; let res = query_pending_operator_reward(test.deps(), owner.into()).unwrap(); @@ -333,7 +348,7 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(res.mixnode_still_fully_bonded); + assert!(res.node_still_fully_bonded); } } @@ -342,16 +357,14 @@ mod tests { let mut test = TestSetup::new(); let owner = "mix-owner"; let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); total_earned += dist.operator; let sender = mock_info(owner, &[]); @@ -366,7 +379,7 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(!res.mixnode_still_fully_bonded); + assert!(!res.node_still_fully_bonded); } #[test] @@ -374,15 +387,13 @@ mod tests { let mut test = TestSetup::new(); let owner = "mix-owner"; let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); let sender = mock_info(owner, &[]); let env = test.env(); @@ -398,7 +409,7 @@ mod tests { assert!(res.amount_earned.is_none()); assert!(res.amount_earned_detailed.is_none()); assert!(res.amount_staked.is_none()); - assert!(!res.mixnode_still_fully_bonded); + assert!(!res.node_still_fully_bonded); } } @@ -422,7 +433,7 @@ mod tests { assert!(res.amount_earned.is_none()); assert!(res.amount_earned_detailed.is_none()); assert!(res.amount_staked.is_none()); - assert!(!res.mixnode_still_fully_bonded); + assert!(!res.node_still_fully_bonded); } #[test] @@ -431,7 +442,8 @@ mod tests { let owner = "delegator"; let initial_stake = Uint128::new(100_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + let mix_id = test + .add_rewarded_legacy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); test.add_immediate_delegation(owner, initial_stake, mix_id); let res = @@ -442,26 +454,25 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), Decimal::zero()); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(res.mixnode_still_fully_bonded); + assert!(res.node_still_fully_bonded); } #[test] fn for_delegator_with_pending_reward() { let mut test = TestSetup::new(); let owner = "delegator"; + let active_params = test.active_node_params(100.); let initial_stake = Uint128::new(100_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + let mix_id = test + .add_rewarded_legacy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); total_earned += dist.delegates; let res = @@ -472,15 +483,12 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(res.mixnode_still_fully_bonded); + assert!(res.node_still_fully_bonded); // reward it few more times for good measure for _ in 0..10 { test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); total_earned += dist.delegates; let res = query_pending_delegator_reward(test.deps(), owner.into(), mix_id, None) @@ -491,7 +499,7 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(res.mixnode_still_fully_bonded); + assert!(res.node_still_fully_bonded); } } @@ -499,19 +507,18 @@ mod tests { fn for_node_that_is_unbonding() { let mut test = TestSetup::new(); let owner = "delegator"; + let active_params = test.active_node_params(100.); let initial_stake = Uint128::new(100_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + let mix_id = test + .add_rewarded_legacy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); total_earned += dist.delegates; let sender = mock_info("mix-owner", &[]); @@ -525,26 +532,25 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(!res.mixnode_still_fully_bonded); + assert!(!res.node_still_fully_bonded); } #[test] fn for_node_that_has_unbonded() { let mut test = TestSetup::new(); let owner = "delegator"; + let active_params = test.active_node_params(100.); let initial_stake = Uint128::new(100_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + let mix_id = test + .add_rewarded_legacy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); let mut total_earned = Decimal::zero(); - let dist = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + let dist = test.reward_with_distribution_ignore_state(mix_id, active_params); total_earned += dist.delegates; let sender = mock_info("mix-owner", &[]); @@ -559,7 +565,7 @@ mod tests { assert_eq!(res.amount_earned.unwrap(), expected_actual); assert_eq!(res.amount_earned_detailed.unwrap(), total_earned); assert_eq!(res.amount_staked.unwrap().amount, initial_stake); - assert!(!res.mixnode_still_fully_bonded); + assert!(!res.node_still_fully_bonded); } #[test] @@ -571,64 +577,46 @@ mod tests { let del2 = "delegator2"; let del3 = "delegator3"; let del4 = "delegator4"; + let active_params = test.active_node_params(100.); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); + let mix_id = test + .add_rewarded_legacy_mixnode("mix-owner", Some(Uint128::new(1_000_000_000_000))); test.add_immediate_delegation(del1, 123_456_789u32, mix_id); test.add_immediate_delegation(del2, 150_000_000u32, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.add_immediate_delegation(del3, 500_000_000u32, mix_id); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(85.0), - ); + let params = test.active_node_params(85.0); + test.reward_with_distribution_ignore_state(mix_id, params); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass(mix_id, test_helpers::performance(5.0)); + let params = test.active_node_params(5.0); + test.reward_with_distribution_ignore_state(mix_id, params); test.add_immediate_delegation(del4, 5_000_000u32, mix_id); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.add_immediate_delegation(del2, 250_000_000u32, mix_id); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(98.0), - ); + let params = test.active_node_params(98.0); + test.reward_with_distribution_ignore_state(mix_id, params); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); test.remove_immediate_delegation(del3, mix_id); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(98.0), - ); + let params = test.active_node_params(98.0); + test.reward_with_distribution_ignore_state(mix_id, params); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.reward_with_distribution_ignore_state(mix_id, active_params); let pending1 = query_pending_delegator_reward(test.deps(), del1.into(), mix_id, None).unwrap(); @@ -671,7 +659,7 @@ mod tests { fn expected_current_operator( test: &TestSetup, - mix_id: MixId, + mix_id: NodeId, initial_stake: Uint128, ) -> EstimatedCurrentEpochRewardResponse { let mix_rewarding = test.mix_rewarding(mix_id); @@ -691,6 +679,7 @@ mod tests { test.deps(), 42, test_helpers::performance(100.0), + None, ) .unwrap(); assert_eq!(res, EstimatedCurrentEpochRewardResponse::empty_response()) @@ -701,14 +690,12 @@ mod tests { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.force_change_mix_rewarded_set(vec![mix_id]); + test.reward_with_distribution_ignore_state(mix_id, active_params); let sender = mock_info(owner, &[]); let env = test.env(); @@ -718,6 +705,7 @@ mod tests { test.deps(), mix_id, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -730,14 +718,12 @@ mod tests { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode(owner, Some(initial_stake)); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.force_change_mix_rewarded_set(vec![mix_id]); + test.reward_with_distribution_ignore_state(mix_id, active_params); let sender = mock_info(owner, &[]); let env = test.env(); @@ -748,6 +734,7 @@ mod tests { test.deps(), mix_id, test_helpers::performance(100.0), + None, ) .unwrap(); assert_eq!(res, EstimatedCurrentEpochRewardResponse::empty_response()) @@ -757,20 +744,19 @@ mod tests { fn when_node_is_not_in_the_rewarded_set() { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", Some(initial_stake)); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); - test.force_change_rewarded_set(vec![]); + test.force_change_mix_rewarded_set(vec![mix_id]); + test.reward_with_distribution_ignore_state(mix_id, active_params); + test.force_change_mix_rewarded_set(vec![]); let res = query_estimated_current_epoch_operator_reward( test.deps(), mix_id, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -782,19 +768,18 @@ mod tests { fn when_estimated_performance_is_zero() { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", Some(initial_stake)); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.force_change_mix_rewarded_set(vec![mix_id]); + test.reward_with_distribution_ignore_state(mix_id, active_params); let res = query_estimated_current_epoch_operator_reward( test.deps(), mix_id, test_helpers::performance(0.0), + None, ) .unwrap(); @@ -806,28 +791,25 @@ mod tests { fn with_correct_parameters_matches_actual_distribution() { let mut test = TestSetup::new(); let initial_stake = Uint128::new(1_000_000_000_000); - let mix_id = test.add_dummy_mixnode("mix-owner", Some(initial_stake)); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", Some(initial_stake)); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.force_change_mix_rewarded_set(vec![mix_id]); + test.reward_with_distribution_ignore_state(mix_id, active_params); let mix_rewarding = test.mix_rewarding(mix_id); let res = query_estimated_current_epoch_operator_reward( test.deps(), mix_id, test_helpers::performance(95.0), + None, ) .unwrap(); test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(95.0), - ); + let params = test.active_node_params(95.); + let dist = test.reward_with_distribution_ignore_state(mix_id, params); let expected = EstimatedCurrentEpochRewardResponse { original_stake: Some(coin(initial_stake.u128(), TEST_COIN_DENOM)), @@ -850,7 +832,7 @@ mod tests { fn expected_current_delegator( test: &TestSetup, - mix_id: MixId, + mix_id: NodeId, owner: &str, ) -> EstimatedCurrentEpochRewardResponse { let mix_rewarding = test.mix_rewarding(mix_id); @@ -875,21 +857,19 @@ mod tests { #[test] fn when_delegation_doesnt_exist() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.force_change_mix_rewarded_set(vec![mix_id]); + test.reward_with_distribution_ignore_state(mix_id, active_params); let res = query_estimated_current_epoch_delegator_reward( test.deps(), "foomper".into(), mix_id, - None, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -899,18 +879,16 @@ mod tests { #[test] fn when_node_is_unbonding() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); + let active_params = test.active_node_params(100.); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.force_change_mix_rewarded_set(vec![mix_id]); + test.reward_with_distribution_ignore_state(mix_id, active_params); let sender = mock_info("mix-owner", &[]); let env = test.env(); @@ -920,8 +898,8 @@ mod tests { test.deps(), owner.into(), mix_id, - None, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -932,18 +910,16 @@ mod tests { #[test] fn when_node_has_already_unbonded() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); + let active_params = test.active_node_params(100.); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.force_change_mix_rewarded_set(vec![mix_id]); + test.reward_with_distribution_ignore_state(mix_id, active_params); let sender = mock_info("mix-owner", &[]); let env = test.env(); @@ -954,8 +930,8 @@ mod tests { test.deps(), owner.into(), mix_id, - None, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -966,26 +942,24 @@ mod tests { #[test] fn when_node_is_not_in_the_rewarded_set() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); + let active_params = test.active_node_params(100.); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); - test.force_change_rewarded_set(vec![]); + test.force_change_mix_rewarded_set(vec![mix_id]); + test.reward_with_distribution_ignore_state(mix_id, active_params); + test.force_change_mix_rewarded_set(vec![]); let res = query_estimated_current_epoch_delegator_reward( test.deps(), owner.into(), mix_id, - None, test_helpers::performance(100.0), + None, ) .unwrap(); @@ -996,25 +970,23 @@ mod tests { #[test] fn when_estimated_performance_is_zero() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); + let active_params = test.active_node_params(100.); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(100.0), - ); + test.force_change_mix_rewarded_set(vec![mix_id]); + test.reward_with_distribution_ignore_state(mix_id, active_params); let res = query_estimated_current_epoch_delegator_reward( test.deps(), owner.into(), mix_id, - None, test_helpers::performance(0.0), + None, ) .unwrap(); @@ -1025,30 +997,28 @@ mod tests { #[test] fn with_correct_parameters_matches_actual_distribution_for_single_delegator() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let initial_stake = Uint128::new(1_000_000_000); let owner = "delegator"; test.add_immediate_delegation(owner, initial_stake, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![mix_id]); let mix_rewarding = test.mix_rewarding(mix_id); let res = query_estimated_current_epoch_delegator_reward( test.deps(), owner.into(), mix_id, - None, test_helpers::performance(95.0), + None, ) .unwrap(); test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(95.0), - ); + let params = test.active_node_params(95.); + let dist = test.reward_with_distribution_ignore_state(mix_id, params); let expected = EstimatedCurrentEpochRewardResponse { original_stake: Some(coin(initial_stake.u128(), TEST_COIN_DENOM)), @@ -1067,7 +1037,7 @@ mod tests { #[test] fn with_correct_parameters_matches_actual_distribution_for_three_delegators() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let mix_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let initial_stake1 = Uint128::new(1_000_000_000); let initial_stake2 = Uint128::new(45_000_000_000); @@ -1083,18 +1053,14 @@ mod tests { test.add_immediate_delegation(del2, initial_stake2, mix_id); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(95.0), - ); + test.force_change_mix_rewarded_set(vec![mix_id]); + let params = test.active_node_params(95.0); + test.reward_with_distribution_ignore_state(mix_id, params); test.add_immediate_delegation(del3, initial_stake3, mix_id); test.skip_to_next_epoch_end(); - test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(85.0), - ); + let params = test.active_node_params(85.0); + test.reward_with_distribution_ignore_state(mix_id, params); let mix_rewarding = test.mix_rewarding(mix_id); @@ -1105,8 +1071,8 @@ mod tests { test.deps(), owner.to_string(), mix_id, - None, test_helpers::performance(95.0), + None, ) .unwrap() }) @@ -1131,10 +1097,8 @@ mod tests { let cur3 = initial_stake3_dec + est3; test.skip_to_next_epoch_end(); - let dist = test.reward_with_distribution_with_state_bypass( - mix_id, - test_helpers::performance(95.0), - ); + let params = test.active_node_params(95.0); + let dist = test.reward_with_distribution_ignore_state(mix_id, params); let share1 = cur1 / mix_rewarding.delegates * dist.delegates; let share2 = cur2 / mix_rewarding.delegates * dist.delegates; diff --git a/contracts/mixnet/src/rewards/storage.rs b/contracts/mixnet/src/rewards/storage.rs index e773e67d5d..37574f99ee 100644 --- a/contracts/mixnet/src/rewards/storage.rs +++ b/contracts/mixnet/src/rewards/storage.rs @@ -2,38 +2,126 @@ // SPDX-License-Identifier: Apache-2.0 use crate::constants::{ - MIXNODES_REWARDING_PK_NAMESPACE, PENDING_REWARD_POOL_KEY, REWARDING_PARAMS_KEY, + CUMULATIVE_EPOCH_WORK_KEY, MIXNODES_REWARDING_PK_NAMESPACE, PENDING_REWARD_POOL_KEY, + REWARDING_PARAMS_KEY, }; use crate::rewards::models::RewardPoolChange; use cosmwasm_std::{Decimal, StdResult, Storage}; use cw_storage_plus::{Item, Map}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::mixnode::MixNodeRewarding; -use mixnet_contract_common::reward_params::RewardingParams; -use mixnet_contract_common::MixId; +use mixnet_contract_common::mixnode::NodeRewarding; +use mixnet_contract_common::reward_params::{RewardingParams, WorkFactor}; +use mixnet_contract_common::NodeId; + +// LEGACY CONSTANTS: // current parameters used for rewarding purposes pub(crate) const REWARDING_PARAMS: Item<'_, RewardingParams> = Item::new(REWARDING_PARAMS_KEY); pub(crate) const PENDING_REWARD_POOL_CHANGE: Item<'_, RewardPoolChange> = Item::new(PENDING_REWARD_POOL_KEY); -pub const MIXNODE_REWARDING: Map = - Map::new(MIXNODES_REWARDING_PK_NAMESPACE); +pub const MIXNODE_REWARDING: Map = Map::new(MIXNODES_REWARDING_PK_NAMESPACE); + +// we're using the same underlying key to allow seamless delegation migration +pub const NYMNODE_REWARDING: Map = MIXNODE_REWARDING; + +pub struct RewardingStorage<'a> { + /// Global parameters used for reward calculation, such as the current reward pool, the active set size, etc. + pub global_rewarding_params: Item<'a, RewardingParams>, -pub fn reward_accounting( - storage: &mut dyn Storage, - amount: Decimal, -) -> Result<(), MixnetContractError> { - let mut pending_changes = PENDING_REWARD_POOL_CHANGE.load(storage)?; - pending_changes.removed += amount; + /// All the changes to the rewarding pool that should get applied upon the **interval** finishing. + pub pending_reward_pool_change: Item<'a, RewardPoolChange>, - Ok(PENDING_REWARD_POOL_CHANGE.save(storage, &pending_changes)?) + /// Information associated with all nym-nodes (and legacy-mixnodes) required for reward calculation + // important note: this is using **EXACTLY** the same underlying key (and structure) as legacy mixnode rewarding + pub nym_node_rewarding_data: Map<'a, NodeId, NodeRewarding>, + + /// keeps track of total cumulative work submitted for this rewarding epoch to make sure it never goes above 1 + pub cumulative_epoch_work: Item<'a, WorkFactor>, } -pub(crate) fn initialise_storage( - storage: &mut dyn Storage, - reward_params: RewardingParams, -) -> StdResult<()> { - REWARDING_PARAMS.save(storage, &reward_params)?; - PENDING_REWARD_POOL_CHANGE.save(storage, &RewardPoolChange::default()) +impl<'a> RewardingStorage<'a> { + pub const fn new() -> RewardingStorage<'a> { + RewardingStorage { + global_rewarding_params: REWARDING_PARAMS, + pending_reward_pool_change: PENDING_REWARD_POOL_CHANGE, + nym_node_rewarding_data: NYMNODE_REWARDING, + cumulative_epoch_work: Item::new(CUMULATIVE_EPOCH_WORK_KEY), + } + } + + // an 'alias' because a `new` method might be a bit misleading since it'd suggest a brand new storage is created + // as opposed to using the same underlying data as before + pub const fn load() -> RewardingStorage<'a> { + Self::new() + } + + pub fn initialise( + &self, + storage: &mut dyn Storage, + reward_params: RewardingParams, + ) -> StdResult<()> { + self.global_rewarding_params.save(storage, &reward_params)?; + self.pending_reward_pool_change + .save(storage, &RewardPoolChange::default())?; + self.cumulative_epoch_work + .save(storage, &WorkFactor::zero())?; + + Ok(()) + } + + pub fn reset_cumulative_epoch_work( + &self, + storage: &mut dyn Storage, + ) -> Result<(), MixnetContractError> { + self.cumulative_epoch_work + .save(storage, &WorkFactor::zero())?; + Ok(()) + } + + pub fn update_cumulative_epoch_work( + &self, + storage: &mut dyn Storage, + work: Decimal, + ) -> Result<(), MixnetContractError> { + // we use a default in case this is the first run in the new contract since that value hasn't existed before + let current = self + .cumulative_epoch_work + .may_load(storage)? + .unwrap_or(WorkFactor::zero()); + let updated = current + work; + if updated > WorkFactor::one() { + return Err(MixnetContractError::TotalWorkAboveOne); + } + self.cumulative_epoch_work.save(storage, &updated)?; + Ok(()) + } + + pub fn add_pending_pool_changes( + &self, + storage: &mut dyn Storage, + amount: Decimal, + ) -> Result<(), MixnetContractError> { + let mut pending_changes = self.pending_reward_pool_change.load(storage)?; + pending_changes.removed += amount; + self.pending_reward_pool_change + .save(storage, &pending_changes)?; + Ok(()) + } + + pub fn try_persist_node_reward( + &self, + storage: &mut dyn Storage, + node: NodeId, + updated_data: NodeRewarding, + reward: Decimal, + work: WorkFactor, + ) -> Result<(), MixnetContractError> { + self.nym_node_rewarding_data + .save(storage, node, &updated_data)?; + self.add_pending_pool_changes(storage, reward)?; + self.update_cumulative_epoch_work(storage, work)?; + + Ok(()) + } } diff --git a/contracts/mixnet/src/rewards/transactions.rs b/contracts/mixnet/src/rewards/transactions.rs index f772dca800..824b918521 100644 --- a/contracts/mixnet/src/rewards/transactions.rs +++ b/contracts/mixnet/src/rewards/transactions.rs @@ -2,186 +2,200 @@ // SPDX-License-Identifier: Apache-2.0 use super::storage; +use crate::compat::helpers::ensure_can_withdraw_rewards; use crate::delegations::storage as delegations_storage; use crate::interval::storage as interval_storage; use crate::interval::storage::{push_new_epoch_event, push_new_interval_event}; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnet_contract_settings::storage::ADMIN; -use crate::mixnodes::helpers::get_mixnode_details_by_owner; -use crate::mixnodes::storage as mixnodes_storage; use crate::rewards::helpers; use crate::rewards::helpers::update_and_save_last_rewarded; +use crate::rewards::storage::RewardingStorage; use crate::support::helpers::{ - ensure_bonded, ensure_can_advance_epoch, ensure_epoch_in_progress_state, AttachSendTokens, + ensure_any_node_bonded, ensure_can_advance_epoch, ensure_epoch_in_progress_state, }; use cosmwasm_std::{DepsMut, Env, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ new_active_set_update_event, new_mix_rewarding_event, - new_not_found_mix_operator_rewarding_event, new_pending_active_set_update_event, + new_not_found_node_operator_rewarding_event, new_pending_active_set_update_event, new_pending_rewarding_params_update_event, new_rewarding_params_update_event, new_withdraw_delegator_reward_event, new_withdraw_operator_reward_event, new_zero_uptime_mix_operator_rewarding_event, }; use mixnet_contract_common::pending_events::{PendingEpochEventKind, PendingIntervalEventKind}; use mixnet_contract_common::reward_params::{ - IntervalRewardingParamsUpdate, NodeRewardParams, Performance, + ActiveSetUpdate, IntervalRewardingParamsUpdate, NodeRewardingParameters, }; -use mixnet_contract_common::{Delegation, EpochState, MixId}; +use mixnet_contract_common::{Delegation, EpochState, MixNodeDetails, NodeId, NymNodeDetails}; +use nym_contracts_common::helpers::ResponseExt; -pub(crate) fn try_reward_mixnode( +pub(crate) fn try_reward_node( deps: DepsMut<'_>, env: Env, info: MessageInfo, - mix_id: MixId, - node_performance: Performance, + node_id: NodeId, + node_rewarding_params: NodeRewardingParameters, ) -> Result { + let rewarding_storage = RewardingStorage::load(); + // check whether this `info.sender` is the same one as set in `epoch_status.being_advanced_by` // if so, return `epoch_status` so we could avoid having to perform extra read from the storage let current_epoch_status = ensure_can_advance_epoch(&info.sender, deps.storage)?; // see if the epoch has finished let interval = interval_storage::current_interval(deps.storage)?; - if !interval.is_current_epoch_over(&env) { - return Err(MixnetContractError::EpochInProgress { - current_block_time: env.block.time.seconds(), - epoch_start: interval.current_epoch_start_unix_timestamp(), - epoch_end: interval.current_epoch_end_unix_timestamp(), - }); - } + interval.ensure_current_epoch_is_over(&env)?; + let absolute_epoch_id = interval.current_epoch_absolute_id(); - if matches!(current_epoch_status.state, EpochState::Rewarding {last_rewarded, ..} if last_rewarded == mix_id) - { - return Err(MixnetContractError::MixnodeAlreadyRewarded { - mix_id, - absolute_epoch_id, - }); + if let EpochState::Rewarding { last_rewarded, .. } = current_epoch_status.state { + if last_rewarded >= node_id { + return Err(MixnetContractError::NodeAlreadyRewarded { + node_id, + absolute_epoch_id, + }); + } } // update the epoch state with this node as being rewarded most recently - // (if the transaction fails down the line, it will be reverted) - update_and_save_last_rewarded(deps.storage, current_epoch_status, mix_id)?; + // (if the transaction fails down the line, this storage write will be reverted) + update_and_save_last_rewarded(deps.storage, current_epoch_status, node_id)?; - // there's a chance of this failing to load the details if the mixnode unbonded before rewards + // there's a chance of this failing to load the details if the node unbonded before rewards // were distributed and all of its delegators are also gone - let mut mix_rewarding = match storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)? { - Some(mix_rewarding) if mix_rewarding.still_bonded() => mix_rewarding, - // don't fail if the node has unbonded as we don't want to fail the underlying transaction + + // NOTE: legacy mixnode rewarding are stored under the same storage key + // and have the same rewarding structure thus they'd also be loaded here + let mut rewarding_info = match storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)? { + Some(rewarding_info) if rewarding_info.still_bonded() => rewarding_info, + // don't fail if the node has unbonded (or it's a legacy gateway) as we don't want to fail the underlying transaction _ => { - return Ok(Response::new() - .add_event(new_not_found_mix_operator_rewarding_event(interval, mix_id))); + return Ok( + Response::new().add_event(new_not_found_node_operator_rewarding_event( + interval, node_id, + )), + ); } }; - let prior_delegates = mix_rewarding.delegates; - let prior_unit_reward = mix_rewarding.full_reward_ratio(); + let prior_delegates = rewarding_info.delegates; + let prior_unit_reward = rewarding_info.full_reward_ratio(); // check if this node has already been rewarded for the current epoch. // unlike the previous check, this one should be a hard error since this cannot be // influenced by users actions (note that previous epoch state checks should actually already guard us against it) - if absolute_epoch_id == mix_rewarding.last_rewarded_epoch { - return Err(MixnetContractError::MixnodeAlreadyRewarded { - mix_id, + if absolute_epoch_id == rewarding_info.last_rewarded_epoch { + return Err(MixnetContractError::NodeAlreadyRewarded { + node_id, absolute_epoch_id, }); } - // again a hard error since the rewarding validator should have known not to reward this node - let node_status = interval_storage::REWARDED_SET - .load(deps.storage, mix_id) - .map_err(|_| MixnetContractError::MixnodeNotInRewardedSet { - mix_id, - absolute_epoch_id, - })?; - // no need to calculate anything as rewards are going to be 0 for everything // however, we still need to update last_rewarded_epoch field - if node_performance.is_zero() { - mix_rewarding.last_rewarded_epoch = absolute_epoch_id; - storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; + if node_rewarding_params.is_zero() { + rewarding_info.last_rewarded_epoch = absolute_epoch_id; + storage::NYMNODE_REWARDING.save(deps.storage, node_id, &rewarding_info)?; return Ok( Response::new().add_event(new_zero_uptime_mix_operator_rewarding_event( - interval, mix_id, + interval, node_id, )), ); } - // make sure node's profit margin is within the allowed range, + // make sure node's cost function is within the allowed range, // if not adjust it accordingly - let params = mixnet_params_storage::CONTRACT_STATE - .load(deps.storage)? - .params; - mix_rewarding.normalise_profit_margin(params.profit_margin); - mix_rewarding.normalise_operating_cost(params.interval_operating_cost); - - let rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; - let node_reward_params = NodeRewardParams::new(node_performance, node_status.is_active()); - - // calculate each step separate for easier accounting - let node_reward = mix_rewarding.node_reward(&rewarding_params, node_reward_params); - let reward_distribution = mix_rewarding.determine_reward_split( + let params = mixnet_params_storage::state_params(deps.storage)?; + rewarding_info.normalise_cost_function(params.profit_margin, params.interval_operating_cost); + + let global_rewarding_params = rewarding_storage + .global_rewarding_params + .load(deps.storage)?; + + // calculate each step separately for easier accounting + // + // total node reward, i.e. owner + delegates + let node_reward = rewarding_info.node_reward(&global_rewarding_params, node_rewarding_params); + + // the actual split between owner and its delegates + let reward_distribution = rewarding_info.determine_reward_split( node_reward, - node_performance, + node_rewarding_params.performance, interval.epochs_in_interval(), ); - mix_rewarding.distribute_rewards(reward_distribution, absolute_epoch_id); - - // persist changes happened to the storage - storage::MIXNODE_REWARDING.save(deps.storage, mix_id, &mix_rewarding)?; - storage::reward_accounting(deps.storage, node_reward)?; + // update internal accounting with the new values + rewarding_info.distribute_rewards(reward_distribution, absolute_epoch_id); + + // persist the changes to the storage + rewarding_storage.try_persist_node_reward( + deps.storage, + node_id, + rewarding_info, + node_reward, + node_rewarding_params.work_factor, + )?; Ok(Response::new().add_event(new_mix_rewarding_event( interval, - mix_id, + node_id, reward_distribution, prior_delegates, prior_unit_reward, ))) } -pub(crate) fn try_withdraw_operator_reward( +pub(crate) fn try_withdraw_nym_node_operator_reward( deps: DepsMut<'_>, - info: MessageInfo, + node_details: NymNodeDetails, ) -> Result { - // we need to grab all of the node's details, so we'd known original pledge alongside - // all the earned rewards (and obviously to know if this node even exists and is still - // in the bonded state) - let mix_details = get_mixnode_details_by_owner(deps.storage, info.sender.clone())?.ok_or( - MixnetContractError::NoAssociatedMixNodeBond { - owner: info.sender.clone(), - }, - )?; - let mix_id = mix_details.mix_id(); + let node_id = node_details.node_id(); + let owner = node_details.bond_information.owner.clone(); + + ensure_can_withdraw_rewards(&node_details)?; + + let reward = helpers::withdraw_operator_reward(deps.storage, node_details)?; + let mut response = Response::new(); - ensure_bonded(&mix_details.bond_information)?; + // if the reward is zero, don't track or send anything - there's no point + if !reward.amount.is_zero() { + response = response.send_tokens(&owner, reward.clone()) + } + + Ok(response.add_event(new_withdraw_operator_reward_event(&owner, reward, node_id))) +} + +pub(crate) fn try_withdraw_mixnode_operator_reward( + deps: DepsMut<'_>, + mix_details: MixNodeDetails, +) -> Result { + let node_id = mix_details.mix_id(); + let owner = mix_details.bond_information.owner.clone(); + + ensure_can_withdraw_rewards(&mix_details)?; let reward = helpers::withdraw_operator_reward(deps.storage, mix_details)?; let mut response = Response::new(); // if the reward is zero, don't track or send anything - there's no point if !reward.amount.is_zero() { - response = response.send_tokens(&info.sender, reward.clone()) + response = response.send_tokens(&owner, reward.clone()) } - Ok(response.add_event(new_withdraw_operator_reward_event( - &info.sender, - reward, - mix_id, - ))) + Ok(response.add_event(new_withdraw_operator_reward_event(&owner, reward, node_id))) } pub(crate) fn try_withdraw_delegator_reward( deps: DepsMut<'_>, info: MessageInfo, - mix_id: MixId, + node_id: NodeId, ) -> Result { // see if the delegation even exists - let storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None); + let storage_key = Delegation::generate_storage_key(node_id, &info.sender, None); let delegation = match delegations_storage::delegations().may_load(deps.storage, storage_key)? { None => { - return Err(MixnetContractError::NoMixnodeDelegationFound { - mix_id, + return Err(MixnetContractError::NodeDelegationNotFound { + node_id, address: info.sender.into_string(), proxy: None, }); @@ -189,21 +203,15 @@ pub(crate) fn try_withdraw_delegator_reward( Some(delegation) => delegation, }; - // grab associated mixnode rewarding details + // grab associated node rewarding details let mix_rewarding = - storage::MIXNODE_REWARDING.may_load(deps.storage, mix_id)?.ok_or(MixnetContractError::inconsistent_state( - "mixnode rewarding got removed from the storage whilst there's still an existing delegation" + storage::NYMNODE_REWARDING.may_load(deps.storage, node_id)?.ok_or(MixnetContractError::inconsistent_state( + "nym-node/legacy mixnode rewarding got removed from the storage whilst there's still an existing delegation" ))?; // see if the mixnode is not in the process of unbonding or whether it has already unbonded // (in that case the expected path of getting your tokens back is via undelegation) - match mixnodes_storage::mixnode_bonds().may_load(deps.storage, mix_id)? { - Some(mix_bond) if mix_bond.is_unbonding => { - return Err(MixnetContractError::MixnodeIsUnbonding { mix_id }); - } - None => return Err(MixnetContractError::MixnodeHasUnbonded { mix_id }), - _ => (), - }; + ensure_any_node_bonded(deps.storage, node_id)?; let reward = helpers::withdraw_delegator_reward(deps.storage, delegation, mix_rewarding)?; let mut response = Response::new(); @@ -216,54 +224,46 @@ pub(crate) fn try_withdraw_delegator_reward( Ok(response.add_event(new_withdraw_delegator_reward_event( &info.sender, reward, - mix_id, + node_id, ))) } -pub(crate) fn try_update_active_set_size( +pub(crate) fn try_update_active_set_distribution( deps: DepsMut<'_>, env: Env, info: MessageInfo, - active_set_size: u32, + update: ActiveSetUpdate, force_immediately: bool, ) -> Result { ADMIN.assert_admin(deps.as_ref(), &info.sender)?; - let mut rewarding_params = storage::REWARDING_PARAMS.load(deps.storage)?; - if active_set_size == 0 { - return Err(MixnetContractError::ZeroActiveSet); - } + let mut rewarding_params = RewardingStorage::load() + .global_rewarding_params + .load(deps.storage)?; - if active_set_size > rewarding_params.rewarded_set_size { - return Err(MixnetContractError::InvalidActiveSetSize); - } + // make sure the values could theoretically be applied in the current context + rewarding_params.validate_active_set_update(update)?; let interval = interval_storage::current_interval(deps.storage)?; + + // perform the change immediately if force_immediately || interval.is_current_epoch_over(&env) { - rewarding_params.try_change_active_set_size(active_set_size)?; + rewarding_params.try_change_active_set(update)?; storage::REWARDING_PARAMS.save(deps.storage, &rewarding_params)?; - Ok(Response::new().add_event(new_active_set_update_event( - env.block.height, - active_set_size, - ))) - } else { - // updating active sety size is only allowed if the epoch is currently not in the process of being advanced - // (unless the force flag was used) - ensure_epoch_in_progress_state(deps.storage)?; - - // push the epoch event - let epoch_event = PendingEpochEventKind::UpdateActiveSetSize { - new_size: active_set_size, - }; - push_new_epoch_event(deps.storage, &env, epoch_event)?; - let time_left = interval.secs_until_current_interval_end(&env); - Ok( - Response::new().add_event(new_pending_active_set_update_event( - active_set_size, - time_left, - )), - ) + return Ok(Response::new().add_event(new_active_set_update_event(env.block.height, update))); } + + // otherwise push the event onto the queue to get executed when the epoch concludes + + // updating active set is only allowed if the epoch is currently not in the process of being advanced + // (unless the force flag was used) + ensure_epoch_in_progress_state(deps.storage)?; + + // push the epoch event + let epoch_event = PendingEpochEventKind::UpdateActiveSet { update }; + push_new_epoch_event(deps.storage, &env, epoch_event)?; + let time_left = interval.secs_until_current_interval_end(&env); + Ok(Response::new().add_event(new_pending_active_set_update_event(update, time_left))) } pub(crate) fn try_update_rewarding_params( @@ -311,48 +311,89 @@ pub(crate) fn try_update_rewarding_params( #[cfg(test)] pub mod tests { - use cosmwasm_std::testing::mock_info; - + use super::*; use crate::mixnodes::storage as mixnodes_storage; + use crate::support::tests::fixtures::active_set_update_fixture; use crate::support::tests::test_helpers; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::testing::mock_info; - use super::*; + // a simple wrapper to streamline checking for rewarding results + trait TestRewarding { + fn execute_rewarding( + &mut self, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, + ) -> Result; + + fn assert_rewarding( + &mut self, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, + ) -> Response; + } + + impl TestRewarding for TestSetup { + fn execute_rewarding( + &mut self, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, + ) -> Result { + let sender = self.rewarding_validator(); + self.execute_fn( + |deps, env, info| try_reward_node(deps, env, info, node_id, rewarding_params), + sender, + ) + } + + #[track_caller] + fn assert_rewarding( + &mut self, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, + ) -> Response { + let caller = std::panic::Location::caller(); + self.execute_rewarding(node_id, rewarding_params) + .unwrap_or_else(|err| panic!("{caller} failed with: '{err}' ({err:?})")) + } + } #[cfg(test)] - mod mixnode_rewarding { + mod legacy_mixnode_rewarding { + use super::*; + use crate::interval::pending_events; + use crate::support::tests::test_helpers::{find_attribute, FindAttribute, TestSetup}; use cosmwasm_std::{Decimal, Uint128}; - use mixnet_contract_common::events::{ MixnetEventType, BOND_NOT_FOUND_VALUE, DELEGATES_REWARD_KEY, NO_REWARD_REASON_KEY, OPERATOR_REWARD_KEY, PRIOR_DELEGATES_KEY, PRIOR_UNIT_REWARD_KEY, - ZERO_PERFORMANCE_VALUE, + ZERO_PERFORMANCE_OR_WORK_VALUE, }; use mixnet_contract_common::helpers::compare_decimals; - use mixnet_contract_common::{EpochStatus, RewardedSetNodeStatus}; - - use crate::interval::pending_events; - use crate::support::tests::test_helpers::{find_attribute, TestSetup}; - - use super::*; + use mixnet_contract_common::nym_node::Role; + use mixnet_contract_common::reward_params::WorkFactor; + use mixnet_contract_common::EpochStatus; #[cfg(test)] mod epoch_state_is_correctly_updated { use super::*; + use mixnet_contract_common::reward_params::WorkFactor; #[test] fn when_target_mixnode_unbonded() { let mut test = TestSetup::new(); - let mix_id_unbonded = test.add_dummy_mixnode("mix-owner-unbonded", None); - let mix_id_unbonded_leftover = - test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); - let mix_id_never_existed = 42; + let node_id_unbonded = test.add_rewarded_legacy_mixnode("mix-owner-unbonded", None); + let node_id_unbonded_leftover = + test.add_rewarded_legacy_mixnode("mix-owner-unbonded-leftover", None); + let node_id_never_existed = 42; test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![ - mix_id_unbonded, - mix_id_unbonded_leftover, - mix_id_never_existed, + test.force_change_mix_rewarded_set(vec![ + node_id_unbonded, + node_id_unbonded_leftover, + node_id_never_existed, ]); test.start_epoch_transition(); + let active_params = test.active_node_params(100.); let env = test.env(); @@ -360,77 +401,52 @@ pub mod tests { // since before performing the nym-api should clear out the event queue // manually adjust delegation info as to indicate the rewarding information shouldnt get removed - let mut rewarding_details = storage::MIXNODE_REWARDING - .load(test.deps().storage, mix_id_unbonded_leftover) + let mut rewarding_details = storage::NYMNODE_REWARDING + .load(test.deps().storage, node_id_unbonded_leftover) .unwrap(); rewarding_details.delegates = Decimal::raw(12345); rewarding_details.unique_delegations = 1; - storage::MIXNODE_REWARDING + storage::NYMNODE_REWARDING .save( test.deps_mut().storage, - mix_id_unbonded_leftover, + node_id_unbonded_leftover, &rewarding_details, ) .unwrap(); - pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded) + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, node_id_unbonded) .unwrap(); pending_events::unbond_mixnode( test.deps_mut(), &env, 123, - mix_id_unbonded_leftover, + node_id_unbonded_leftover, ) .unwrap(); - let env = test.env(); - let sender = test.rewarding_validator(); - let performance = test_helpers::performance(100.0); - - try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id_unbonded, - performance, - ) - .unwrap(); + test.assert_rewarding(node_id_unbonded, active_params); assert_eq!( EpochState::Rewarding { - last_rewarded: mix_id_unbonded, - final_node_id: mix_id_never_existed, + last_rewarded: node_id_unbonded, + final_node_id: node_id_never_existed, }, interval_storage::current_epoch_status(test.deps().storage) .unwrap() .state ); - try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id_unbonded_leftover, - performance, - ) - .unwrap(); + test.assert_rewarding(node_id_unbonded_leftover, active_params); assert_eq!( EpochState::Rewarding { - last_rewarded: mix_id_unbonded_leftover, - final_node_id: mix_id_never_existed, + last_rewarded: node_id_unbonded_leftover, + final_node_id: node_id_never_existed, }, interval_storage::current_epoch_status(test.deps().storage) .unwrap() .state ); - try_reward_mixnode( - test.deps_mut(), - env, - sender, - mix_id_never_existed, - performance, - ) - .unwrap(); + test.assert_rewarding(node_id_never_existed, active_params); assert_eq!( EpochState::ReconcilingEvents, interval_storage::current_epoch_status(test.deps().storage) @@ -442,16 +458,77 @@ pub mod tests { #[test] fn when_target_mixnode_has_zero_performance() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![node_id]); test.start_epoch_transition(); - let zero_performance = test_helpers::performance(0.); - let env = test.env(); - let sender = test.rewarding_validator(); - try_reward_mixnode(test.deps_mut(), env, sender, mix_id, zero_performance).unwrap(); + let zero_performance = test.active_node_params(0.); + test.assert_rewarding(node_id, zero_performance); + assert_eq!( + EpochState::ReconcilingEvents, + interval_storage::current_epoch_status(test.deps().storage) + .unwrap() + .state + ); + } + + #[test] + fn when_target_mixnode_has_zero_work_factor() { + let mut test = TestSetup::new(); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); + + test.skip_to_next_epoch_end(); + test.force_change_mix_rewarded_set(vec![node_id]); + test.start_epoch_transition(); + + let params = NodeRewardingParameters::new( + test_helpers::performance(100.), + WorkFactor::zero(), + ); + test.assert_rewarding(node_id, params); + assert_eq!( + EpochState::ReconcilingEvents, + interval_storage::current_epoch_status(test.deps().storage) + .unwrap() + .state + ); + } + + #[test] + fn when_target_nymnode_has_zero_performance() { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("node-owner", None); + + test.skip_to_next_epoch_end(); + test.force_change_mix_rewarded_set(vec![node_id]); + test.start_epoch_transition(); + + let zero_performance = test.active_node_params(0.); + test.assert_rewarding(node_id, zero_performance); + assert_eq!( + EpochState::ReconcilingEvents, + interval_storage::current_epoch_status(test.deps().storage) + .unwrap() + .state + ); + } + + #[test] + fn when_target_node_has_zero_workfactor() { + let mut test = TestSetup::new(); + let node_id = test.add_dummy_nymnode("mix-owner", None); + + test.skip_to_next_epoch_end(); + test.force_change_mix_rewarded_set(vec![node_id]); + test.start_epoch_transition(); + + let params = NodeRewardingParameters::new( + test_helpers::performance(100.), + WorkFactor::zero(), + ); + test.assert_rewarding(node_id, params); assert_eq!( EpochState::ReconcilingEvents, interval_storage::current_epoch_status(test.deps().storage) @@ -463,16 +540,14 @@ pub mod tests { #[test] fn when_theres_only_one_node_to_reward() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![node_id]); test.start_epoch_transition(); - let performance = test_helpers::performance(100.0); - let env = test.env(); - let sender = test.rewarding_validator(); + let active_params = test.active_node_params(100.); - try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance).unwrap(); + test.assert_rewarding(node_id, active_params); assert_eq!( EpochState::ReconcilingEvents, interval_storage::current_epoch_status(test.deps().storage) @@ -487,36 +562,27 @@ pub mod tests { let mut ids = Vec::new(); for i in 0..100 { - let mix_id = test.add_dummy_mixnode(&format!("mix-owner{i}"), None); - ids.push(mix_id); + let node_id = test.add_rewarded_legacy_mixnode(&format!("mix-owner{i}"), None); + ids.push(node_id); } test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(ids.clone()); + test.force_change_mix_rewarded_set(ids.clone()); test.start_epoch_transition(); - let performance = test_helpers::performance(100.0); - let env = test.env(); - let sender = test.rewarding_validator(); - - for mix_id in ids { - try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id, - performance, - ) - .unwrap(); + let active_params = test.active_node_params(100.); + + for node_id in ids { + test.assert_rewarding(node_id, active_params); let current_state = interval_storage::current_epoch_status(test.deps().storage) .unwrap() .state; - if mix_id == 100 { + if node_id == 100 { assert_eq!(EpochState::ReconcilingEvents, current_state) } else { assert_eq!( EpochState::Rewarding { - last_rewarded: mix_id, + last_rewarded: node_id, final_node_id: 100, }, current_state @@ -531,12 +597,14 @@ pub mod tests { let bad_states = vec![ EpochState::InProgress, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { let mut test = TestSetup::new(); - let rewarding_validator = test.rewarding_validator(); + let active_params = test.active_node_params(100.); let mut status = EpochStatus::new(test.rewarding_validator().sender); status.state = bad_state; @@ -544,16 +612,10 @@ pub mod tests { .unwrap(); test.skip_to_current_epoch_end(); - test.force_change_rewarded_set(vec![1, 2, 3]); - let env = test.env(); + test.force_change_mix_rewarded_set(vec![1, 2, 3]); + + let res = test.execute_rewarding(1, active_params); - let res = try_reward_mixnode( - test.deps_mut(), - env, - rewarding_validator, - 1, - test_helpers::performance(100.), - ); assert_eq!( res, Err(MixnetContractError::UnexpectedNonRewardingEpochState { @@ -566,39 +628,39 @@ pub mod tests { #[test] fn can_only_be_performed_by_specified_rewarding_validator() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); let some_sender = mock_info("foomper", &[]); // skip time to when the following epoch is over (since mixnodes are not eligible for rewarding // in the same epoch they're bonded and we need the rewarding epoch to be over) test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![node_id]); test.start_epoch_transition(); - let performance = test_helpers::performance(100.); + let params = test.legacy_rewarding_params(node_id, 100.); let env = test.env(); - let res = try_reward_mixnode(test.deps_mut(), env, some_sender, mix_id, performance); + let res = try_reward_node(test.deps_mut(), env, some_sender, node_id, params); assert_eq!(res, Err(MixnetContractError::Unauthorized)); // good address (sanity check) let env = test.env(); let sender = test.rewarding_validator(); - let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance); + let res = try_reward_node(test.deps_mut(), env, sender, node_id, params); assert!(res.is_ok()); } #[test] fn can_only_be_performed_if_node_is_fully_bonded() { let mut test = TestSetup::new(); - let mix_id_never_existed = 42; - let mix_id_unbonded = test.add_dummy_mixnode("mix-owner-unbonded", None); - let mix_id_unbonded_leftover = - test.add_dummy_mixnode("mix-owner-unbonded-leftover", None); + let node_id_never_existed = 42; + let node_id_unbonded = test.add_rewarded_legacy_mixnode("mix-owner-unbonded", None); + let node_id_unbonded_leftover = + test.add_rewarded_legacy_mixnode("mix-owner-unbonded-leftover", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![ - mix_id_unbonded, - mix_id_unbonded_leftover, - mix_id_never_existed, + test.force_change_mix_rewarded_set(vec![ + node_id_unbonded, + node_id_unbonded_leftover, + node_id_never_existed, ]); test.start_epoch_transition(); @@ -608,43 +670,34 @@ pub mod tests { // since before performing the nym-api should clear out the event queue // manually adjust delegation info as to indicate the rewarding information shouldnt get removed - let mut rewarding_details = storage::MIXNODE_REWARDING - .load(test.deps().storage, mix_id_unbonded_leftover) + let mut rewarding_details = storage::NYMNODE_REWARDING + .load(test.deps().storage, node_id_unbonded_leftover) .unwrap(); rewarding_details.delegates = Decimal::raw(12345); rewarding_details.unique_delegations = 1; - storage::MIXNODE_REWARDING + storage::NYMNODE_REWARDING .save( test.deps_mut().storage, - mix_id_unbonded_leftover, + node_id_unbonded_leftover, &rewarding_details, ) .unwrap(); - pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded).unwrap(); + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, node_id_unbonded).unwrap(); - pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover) + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, node_id_unbonded_leftover) .unwrap(); - let env = test.env(); - let sender = test.rewarding_validator(); - let performance = test_helpers::performance(100.0); + let active_params = test.active_node_params(100.); - for &mix_id in &[ - mix_id_unbonded, - mix_id_unbonded_leftover, - mix_id_never_existed, + for &node_id in &[ + node_id_unbonded, + node_id_unbonded_leftover, + node_id_never_existed, ] { - let res = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id, - performance, - ) - .unwrap(); + let res = test.assert_rewarding(node_id, active_params); let reason = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), + Some(MixnetEventType::NodeRewarding.to_string()), NO_REWARD_REASON_KEY, &res, ); @@ -656,16 +709,14 @@ pub mod tests { fn can_only_be_performed_once_epoch_is_over() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); - let sender = test.rewarding_validator(); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); // node is in the active set BUT the current epoch has just begun test.skip_to_next_epoch(); - test.force_change_rewarded_set(vec![mix_id]); - let performance = test_helpers::performance(100.); + test.force_change_mix_rewarded_set(vec![node_id]); - let env = test.env(); - let res = try_reward_mixnode(test.deps_mut(), env, sender.clone(), mix_id, performance); + let active_params = test.active_node_params(100.); + let res = test.execute_rewarding(node_id, active_params); assert!(matches!( res, Err(MixnetContractError::EpochInProgress { .. }) @@ -674,199 +725,133 @@ pub mod tests { // epoch is over (sanity check) test.skip_to_current_epoch_end(); test.start_epoch_transition(); - let env = test.env(); - let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance); + let res = test.execute_rewarding(node_id, active_params); assert!(res.is_ok()); } #[test] - fn can_only_be_performed_for_nodes_in_rewarded_set() { + fn can_only_be_performed_once_per_node_per_epoch() { let mut test = TestSetup::new(); - - let active_mix_id = test.add_dummy_mixnode("mix-owner-active", None); - let standby_mix_id = test.add_dummy_mixnode("mix-owner-standby", None); - let inactive_mix_id = test.add_dummy_mixnode("mix-owner-inactive", None); - let sender = test.rewarding_validator(); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - - // manually set the rewarded set so that we'd have 1 active node, 1 standby and 1 inactive - interval_storage::REWARDED_SET - .save( - test.deps_mut().storage, - active_mix_id, - &RewardedSetNodeStatus::Active, - ) - .unwrap(); - interval_storage::REWARDED_SET - .save( - test.deps_mut().storage, - standby_mix_id, - &RewardedSetNodeStatus::Standby, - ) - .unwrap(); - - // actually add one more dummy node with high id so we wouldn't go into the next state - interval_storage::REWARDED_SET - .save( - test.deps_mut().storage, - 9001, - &RewardedSetNodeStatus::Standby, - ) - .unwrap(); + test.force_change_mix_rewarded_set(vec![node_id, 42]); test.start_epoch_transition(); + let active_params = test.active_node_params(100.); - let performance = test_helpers::performance(100.); - let env = test.env(); - let res_active = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - active_mix_id, - performance, - ); - let res_standby = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - standby_mix_id, - performance, - ); - let res_inactive = - try_reward_mixnode(test.deps_mut(), env, sender, inactive_mix_id, performance); + // first rewarding + test.assert_rewarding(node_id, active_params); - assert!(res_active.is_ok()); - assert!(res_standby.is_ok()); + // second rewarding + let res = test.execute_rewarding(node_id, active_params); assert!(matches!( - res_inactive, - Err(MixnetContractError::MixnodeNotInRewardedSet { mix_id, .. }) if mix_id == inactive_mix_id + res, + Err(MixnetContractError::NodeAlreadyRewarded { node_id, .. }) if node_id == node_id )); + + // in the following epoch we're good again + test.skip_to_next_epoch_end(); + test.start_epoch_transition(); + + let res = test.execute_rewarding(node_id, active_params); + assert!(res.is_ok()); } #[test] - fn can_only_be_performed_once_per_node_per_epoch() { + fn requires_nonzero_performance_score() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id, 42]); + test.force_change_mix_rewarded_set(vec![node_id, 42]); test.start_epoch_transition(); - let performance = test_helpers::performance(100.); - let env = test.env(); - let sender = test.rewarding_validator(); + let zero_perf_params = test.active_node_params(0.); + let active_params = test.active_node_params(100.); // first rewarding - let res = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id, - performance, - ); - assert!(res.is_ok()); + let res = test.assert_rewarding(node_id, zero_perf_params); + let reason = res.attribute(MixnetEventType::NodeRewarding, NO_REWARD_REASON_KEY); + assert_eq!(ZERO_PERFORMANCE_OR_WORK_VALUE, reason); - // second rewarding - let res = try_reward_mixnode(test.deps_mut(), env, sender.clone(), mix_id, performance); + // sanity check: it's still treated as rewarding, so we can't reward the node again + // with different performance for the same epoch + let res = test.execute_rewarding(node_id, zero_perf_params); assert!(matches!( res, - Err(MixnetContractError::MixnodeAlreadyRewarded { mix_id, .. }) if mix_id == mix_id + Err(MixnetContractError::NodeAlreadyRewarded { node_id, .. }) if node_id == node_id )); - // in the following epoch we're good again + // but in the next epoch, as always, we're good again test.skip_to_next_epoch_end(); test.start_epoch_transition(); - let env = test.env(); - let res = try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance); - assert!(res.is_ok()); + let res = test.assert_rewarding(node_id, active_params); + + // rewards got distributed (in this test we don't care what they were exactly, but they must be non-zero) + let operator = res.attribute(MixnetEventType::NodeRewarding, OPERATOR_REWARD_KEY); + assert!(!operator.is_empty()); + assert_ne!("0", operator); + let delegates = res.attribute(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); + assert_eq!("0", delegates); } #[test] - fn requires_nonzero_performance_score() { + fn requires_nonzero_work_factor() { let mut test = TestSetup::new(); - let mix_id = test.add_dummy_mixnode("mix-owner", None); + let node_id = test.add_rewarded_legacy_mixnode("mix-owner", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id, 42]); + test.force_change_mix_rewarded_set(vec![node_id, 42]); test.start_epoch_transition(); - let zero_performance = test_helpers::performance(0.); - let performance = test_helpers::performance(100.0); - let env = test.env(); - let sender = test.rewarding_validator(); + + let zero_work_params = + NodeRewardingParameters::new(test_helpers::performance(100.), WorkFactor::zero()); + let active_params = test.active_node_params(100.); // first rewarding - let res = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id, - zero_performance, - ) - .unwrap(); - let reason = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), - NO_REWARD_REASON_KEY, - &res, - ); - assert_eq!(ZERO_PERFORMANCE_VALUE, reason); + let res = test.assert_rewarding(node_id, zero_work_params); + let reason = res.attribute(MixnetEventType::NodeRewarding, NO_REWARD_REASON_KEY); + assert_eq!(ZERO_PERFORMANCE_OR_WORK_VALUE, reason); - // sanity check: it's still treated as rewarding, so you we can't reward the node again + // sanity check: it's still treated as rewarding, so we can't reward the node again // with different performance for the same epoch - let res = try_reward_mixnode( - test.deps_mut(), - env, - sender.clone(), - mix_id, - zero_performance, - ); + let res = test.execute_rewarding(node_id, zero_work_params); assert!(matches!( res, - Err(MixnetContractError::MixnodeAlreadyRewarded { mix_id, .. }) if mix_id == mix_id + Err(MixnetContractError::NodeAlreadyRewarded { node_id, .. }) if node_id == node_id )); // but in the next epoch, as always, we're good again test.skip_to_next_epoch_end(); test.start_epoch_transition(); - let env = test.env(); - let res = - try_reward_mixnode(test.deps_mut(), env, sender, mix_id, performance).unwrap(); + let res = test.assert_rewarding(node_id, active_params); // rewards got distributed (in this test we don't care what they were exactly, but they must be non-zero) - let operator = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), - OPERATOR_REWARD_KEY, - &res, - ); + let operator = res.attribute(MixnetEventType::NodeRewarding, OPERATOR_REWARD_KEY); assert!(!operator.is_empty()); assert_ne!("0", operator); - let delegates = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), - DELEGATES_REWARD_KEY, - &res, - ); + let delegates = res.attribute(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); assert_eq!("0", delegates); } #[test] fn correctly_accounts_for_rewards_distributed() { let mut test = TestSetup::new(); - let mix_id1 = test.add_dummy_mixnode("mix-owner1", None); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", None); - let mix_id3 = test.add_dummy_mixnode("mix-owner3", None); + let node_id1 = test.add_rewarded_legacy_mixnode("mix-owner1", None); + let node_id2 = test.add_rewarded_legacy_mixnode("mix-owner2", None); + let node_id3 = test.add_rewarded_legacy_mixnode("mix-owner3", None); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id1, mix_id2, mix_id3]); + test.force_change_mix_rewarded_set(vec![node_id1, node_id2, node_id3]); test.start_epoch_transition(); - let performance = test_helpers::performance(98.0); - let env = test.env(); - let sender = test.rewarding_validator(); + let params = test.active_node_params(98.0); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id2); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id2); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id3); - test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), mix_id3); - test.add_immediate_delegation("delegator3", Uint128::new(9_100_000_000), mix_id3); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id3); + test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), node_id3); + test.add_immediate_delegation("delegator3", Uint128::new(9_100_000_000), node_id3); let change = storage::PENDING_REWARD_POOL_CHANGE .load(test.deps().storage) @@ -877,36 +862,17 @@ pub mod tests { let mut total_operator = Decimal::zero(); let mut total_delegates = Decimal::zero(); - for &mix_id in &[mix_id1, mix_id2, mix_id3] { - let before = storage::MIXNODE_REWARDING - .load(test.deps().storage, mix_id) + for &node_id in &[node_id1, node_id2, node_id3] { + let before = storage::NYMNODE_REWARDING + .load(test.deps().storage, node_id) .unwrap(); - let res = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id, - performance, - ) - .unwrap(); - let operator: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), - OPERATOR_REWARD_KEY, - &res, - ) - .parse() - .unwrap(); - let delegates: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), - DELEGATES_REWARD_KEY, - &res, - ) - .parse() - .unwrap(); + let res = test.assert_rewarding(node_id, params); + let operator = res.decimal(MixnetEventType::NodeRewarding, OPERATOR_REWARD_KEY); + let delegates = res.decimal(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); - let after = storage::MIXNODE_REWARDING - .load(test.deps().storage, mix_id) + let after = storage::NYMNODE_REWARDING + .load(test.deps().storage, node_id) .unwrap(); // also the values emitted via events are consistent with the actual values! @@ -936,20 +902,21 @@ pub mod tests { let operator3 = Uint128::new(12_345_000_000); let mut test = TestSetup::new(); - let mix_id1 = test.add_dummy_mixnode("mix-owner1", Some(operator1)); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", Some(operator2)); - let mix_id3 = test.add_dummy_mixnode("mix-owner3", Some(operator3)); + let global_rewarding_params = test.rewarding_params(); + let node_id1 = test.add_rewarded_legacy_mixnode("mix-owner1", Some(operator1)); + let node_id2 = test.add_rewarded_legacy_mixnode("mix-owner2", Some(operator2)); + let node_id3 = test.add_rewarded_legacy_mixnode("mix-owner3", Some(operator3)); test.skip_to_next_epoch_end(); test.start_epoch_transition(); - test.force_change_rewarded_set(vec![mix_id1, mix_id2, mix_id3]); + test.force_change_mix_rewarded_set(vec![node_id1, node_id2, node_id3]); let performance = test_helpers::performance(98.0); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id2); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id2); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id3); - test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), mix_id3); - test.add_immediate_delegation("delegator3", Uint128::new(9_100_000_000), mix_id3); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id3); + test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), node_id3); + test.add_immediate_delegation("delegator3", Uint128::new(9_100_000_000), node_id3); // bypass proper epoch progression and force change the state test.set_epoch_in_progress_state(); @@ -957,13 +924,13 @@ pub mod tests { // repeat the rewarding the same set of delegates for few epochs for _ in 0..10 { test.start_epoch_transition(); - for &mix_id in &[mix_id1, mix_id2, mix_id3] { - let mut sim = test.instantiate_simulator(mix_id); - let dist = test.reward_with_distribution(mix_id, performance); - let node_params = NodeRewardParams { + for &node_id in &[node_id1, node_id2, node_id3] { + let mut sim = test.instantiate_simulator(node_id); + let node_params = NodeRewardingParameters::new( performance, - in_active_set: true, - }; + global_rewarding_params.active_node_work(), + ); + let dist = test.reward_with_distribution(node_id, node_params); let sim_res = sim.simulate_epoch_single_node(node_params).unwrap(); assert_eq!(sim_res, dist); } @@ -975,11 +942,11 @@ pub mod tests { // add few more delegations and repeat it // (note: we're not concerned about whether particular delegation owner got the correct amount, // this is checked in other unit tests) - test.add_immediate_delegation("delegator1", Uint128::new(50_000_000), mix_id1); - test.add_immediate_delegation("delegator1", Uint128::new(200_000_000), mix_id2); + test.add_immediate_delegation("delegator1", Uint128::new(50_000_000), node_id1); + test.add_immediate_delegation("delegator1", Uint128::new(200_000_000), node_id2); - test.add_immediate_delegation("delegator5", Uint128::new(123_000_000), mix_id3); - test.add_immediate_delegation("delegator6", Uint128::new(456_000_000), mix_id3); + test.add_immediate_delegation("delegator5", Uint128::new(123_000_000), node_id3); + test.add_immediate_delegation("delegator6", Uint128::new(456_000_000), node_id3); // bypass proper epoch progression and force change the state test.set_epoch_in_progress_state(); @@ -987,13 +954,13 @@ pub mod tests { let performance = test_helpers::performance(12.3); for _ in 0..10 { test.start_epoch_transition(); - for &mix_id in &[mix_id1, mix_id2, mix_id3] { - let mut sim = test.instantiate_simulator(mix_id); - let dist = test.reward_with_distribution(mix_id, performance); - let node_params = NodeRewardParams { + for &node_id in &[node_id1, node_id2, node_id3] { + let mut sim = test.instantiate_simulator(node_id); + let node_params = NodeRewardingParameters::new( performance, - in_active_set: true, - }; + global_rewarding_params.active_node_work(), + ); + let dist = test.reward_with_distribution(node_id, node_params); let sim_res = sim.simulate_epoch_single_node(node_params).unwrap(); assert_eq!(sim_res, dist); } @@ -1009,78 +976,53 @@ pub mod tests { let operator2 = Uint128::new(12_345_000_000); let mut test = TestSetup::new(); - let sender = test.rewarding_validator(); + let global_rewarding_params = test.rewarding_params(); - let mix_id1 = test.add_dummy_mixnode("mix-owner1", Some(operator1)); - let mix_id2 = test.add_dummy_mixnode("mix-owner2", Some(operator2)); + let node_id1 = test.add_rewarded_legacy_mixnode("mix-owner1", Some(operator1)); + let node_id2 = test.add_rewarded_legacy_mixnode("mix-owner2", Some(operator2)); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id1, mix_id2]); + test.force_change_mix_rewarded_set(vec![node_id1, node_id2]); let performance = test_helpers::performance(98.0); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id1); - test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), mix_id2); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id1); + test.add_immediate_delegation("delegator1", Uint128::new(100_000_000), node_id2); - test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), mix_id1); + test.add_immediate_delegation("delegator2", Uint128::new(123_456_000), node_id1); - let del11 = test.delegation(mix_id1, "delegator1", &None); - let del12 = test.delegation(mix_id1, "delegator2", &None); - let del21 = test.delegation(mix_id2, "delegator1", &None); + let del11 = test.delegation(node_id1, "delegator1", &None); + let del12 = test.delegation(node_id1, "delegator2", &None); + let del21 = test.delegation(node_id2, "delegator1", &None); for _ in 0..10 { test.start_epoch_transition(); // we know from the previous tests that actual rewarding distribution matches the simulator - let mut sim1 = test.instantiate_simulator(mix_id1); - let mut sim2 = test.instantiate_simulator(mix_id2); + let mut sim1 = test.instantiate_simulator(node_id1); + let mut sim2 = test.instantiate_simulator(node_id2); - let node_params = NodeRewardParams { + let node_params = NodeRewardingParameters::new( performance, - in_active_set: true, - }; + global_rewarding_params.active_node_work(), + ); let dist1 = sim1.simulate_epoch_single_node(node_params).unwrap(); let dist2 = sim2.simulate_epoch_single_node(node_params).unwrap(); - let env = test.env(); + let actual_prior1 = test.mix_rewarding(node_id1); + let actual_prior2 = test.mix_rewarding(node_id2); - let actual_prior1 = test.mix_rewarding(mix_id1); - let actual_prior2 = test.mix_rewarding(mix_id2); + let res1 = test.assert_rewarding(node_id1, node_params); - let res1 = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id1, - performance, - ) - .unwrap(); + let prior_delegates1 = + res1.decimal(MixnetEventType::NodeRewarding, PRIOR_DELEGATES_KEY); + let delegates_reward1 = + res1.decimal(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); + let prior_unit_reward = + res1.decimal(MixnetEventType::NodeRewarding, PRIOR_UNIT_REWARD_KEY); - let prior_delegates1: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_DELEGATES_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(prior_delegates1, actual_prior1.delegates); - - let delegates_reward1: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - DELEGATES_REWARD_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(delegates_reward1, dist1.delegates); - - let prior_unit_reward: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_UNIT_REWARD_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(actual_prior1.full_reward_ratio(), prior_unit_reward); // either use the constant for (which for now is the same for all nodes) @@ -1111,40 +1053,17 @@ pub mod tests { None, ); - let res2 = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id2, - performance, - ) - .unwrap(); + let res2 = test.assert_rewarding(node_id2, node_params); - let prior_delegates2: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_DELEGATES_KEY, - &res2, - ) - .parse() - .unwrap(); - assert_eq!(prior_delegates2, actual_prior2.delegates); + let prior_delegates2 = + res2.decimal(MixnetEventType::NodeRewarding, PRIOR_DELEGATES_KEY); + let delegates_reward2 = + res2.decimal(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); + let prior_unit_reward = + res2.decimal(MixnetEventType::NodeRewarding, PRIOR_UNIT_REWARD_KEY); - let delegates_reward2: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - DELEGATES_REWARD_KEY, - &res2, - ) - .parse() - .unwrap(); + assert_eq!(prior_delegates2, actual_prior2.delegates); assert_eq!(delegates_reward2, dist2.delegates); - - let prior_unit_reward: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_UNIT_REWARD_KEY, - &res2, - ) - .parse() - .unwrap(); assert_eq!(actual_prior2.full_reward_ratio(), prior_unit_reward); // either use the constant for (which for now is the same for all nodes) @@ -1168,66 +1087,41 @@ pub mod tests { } // add more delegations and check few more epochs (so that the delegations would start from non-default unit delegation value) - test.add_immediate_delegation("delegator3", Uint128::new(15_850_000_000), mix_id1); - test.add_immediate_delegation("delegator3", Uint128::new(15_850_000_000), mix_id2); + test.add_immediate_delegation("delegator3", Uint128::new(15_850_000_000), node_id1); + test.add_immediate_delegation("delegator3", Uint128::new(15_850_000_000), node_id2); - let del13 = test.delegation(mix_id1, "delegator3", &None); - let del23 = test.delegation(mix_id2, "delegator3", &None); + let del13 = test.delegation(node_id1, "delegator3", &None); + let del23 = test.delegation(node_id2, "delegator3", &None); for _ in 0..10 { test.start_epoch_transition(); // we know from the previous tests that actual rewarding distribution matches the simulator - let mut sim1 = test.instantiate_simulator(mix_id1); - let mut sim2 = test.instantiate_simulator(mix_id2); + let mut sim1 = test.instantiate_simulator(node_id1); + let mut sim2 = test.instantiate_simulator(node_id2); - let node_params = NodeRewardParams { + let node_params = NodeRewardingParameters::new( performance, - in_active_set: true, - }; + global_rewarding_params.active_node_work(), + ); let dist1 = sim1.simulate_epoch_single_node(node_params).unwrap(); let dist2 = sim2.simulate_epoch_single_node(node_params).unwrap(); - let env = test.env(); + let actual_prior1 = test.mix_rewarding(node_id1); + let actual_prior2 = test.mix_rewarding(node_id2); - let actual_prior1 = test.mix_rewarding(mix_id1); - let actual_prior2 = test.mix_rewarding(mix_id2); + let res1 = test.assert_rewarding(node_id1, node_params); - let res1 = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id1, - performance, - ) - .unwrap(); + let prior_delegates1 = + res1.decimal(MixnetEventType::NodeRewarding, PRIOR_DELEGATES_KEY); + let delegates_reward1 = + res1.decimal(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); + let prior_unit_reward = + res1.decimal(MixnetEventType::NodeRewarding, PRIOR_UNIT_REWARD_KEY); - let prior_delegates1: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_DELEGATES_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(prior_delegates1, actual_prior1.delegates); - - let delegates_reward1: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - DELEGATES_REWARD_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(delegates_reward1, dist1.delegates); - - let prior_unit_reward: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_UNIT_REWARD_KEY, - &res1, - ) - .parse() - .unwrap(); assert_eq!(actual_prior1.full_reward_ratio(), prior_unit_reward); // either use the constant for (which for now is the same for all nodes) @@ -1266,40 +1160,17 @@ pub mod tests { None, ); - let res2 = try_reward_mixnode( - test.deps_mut(), - env.clone(), - sender.clone(), - mix_id2, - performance, - ) - .unwrap(); + let res2 = test.assert_rewarding(node_id2, node_params); - let prior_delegates2: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_DELEGATES_KEY, - &res2, - ) - .parse() - .unwrap(); - assert_eq!(prior_delegates2, actual_prior2.delegates); + let prior_delegates2 = + res2.decimal(MixnetEventType::NodeRewarding, PRIOR_DELEGATES_KEY); + let delegates_reward2 = + res2.decimal(MixnetEventType::NodeRewarding, DELEGATES_REWARD_KEY); + let prior_unit_reward = + res2.decimal(MixnetEventType::NodeRewarding, PRIOR_UNIT_REWARD_KEY); - let delegates_reward2: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - DELEGATES_REWARD_KEY, - &res2, - ) - .parse() - .unwrap(); + assert_eq!(prior_delegates2, actual_prior2.delegates); assert_eq!(delegates_reward2, dist2.delegates); - - let prior_unit_reward: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding), - PRIOR_UNIT_REWARD_KEY, - &res2, - ) - .parse() - .unwrap(); assert_eq!(actual_prior2.full_reward_ratio(), prior_unit_reward); // either use the constant for (which for now is the same for all nodes) @@ -1336,10 +1207,314 @@ pub mod tests { } } + #[cfg(test)] + mod legacy_gateway_rewarding { + use super::*; + use crate::support::tests::test_helpers::FindAttribute; + use mixnet_contract_common::events::{BOND_NOT_FOUND_VALUE, NO_REWARD_REASON_KEY}; + use mixnet_contract_common::nym_node::Role; + use mixnet_contract_common::RoleAssignment; + + #[test] + fn regardless_of_performance_or_work_they_get_nothing() { + let mut test = TestSetup::new(); + let (_, node_id) = test.add_legacy_gateway("owner", None); + + test.skip_to_next_epoch_end(); + test.force_assign_rewarded_set(vec![RoleAssignment::new( + Role::EntryGateway, + vec![node_id], + )]); + test.start_epoch_transition(); + + let rewarding_params = test.active_node_params(100.); + let res = test.assert_rewarding(node_id, rewarding_params); + + let reward_attr = res.any_attribute(NO_REWARD_REASON_KEY); + assert_eq!(reward_attr, BOND_NOT_FOUND_VALUE); + + // make sure the epoch actually progressed (i.e. unrewarded gateway hasn't stalled it) + let current = test.current_epoch_state(); + assert_eq!(current, EpochState::ReconcilingEvents) + } + } + + // rewarding for entry gateway, exit gateway and standby nym-nodes + #[cfg(test)] + mod non_legacy_rewarding { + use super::*; + use crate::interval::pending_events; + use crate::support::tests::test_helpers::FindAttribute; + use cosmwasm_std::{Decimal, Uint128}; + use mixnet_contract_common::events::{ + BOND_NOT_FOUND_VALUE, NO_REWARD_REASON_KEY, OPERATOR_REWARD_KEY, + ZERO_PERFORMANCE_OR_WORK_VALUE, + }; + use mixnet_contract_common::nym_node::Role; + use mixnet_contract_common::reward_params::WorkFactor; + use mixnet_contract_common::RoleAssignment; + use std::collections::HashMap; + use std::ops::{Deref, DerefMut}; + + struct RewardingSetup { + standby_node: NodeId, + entry_node: NodeId, + exit_node: NodeId, + mixing_node: NodeId, + + inner: TestSetup, + } + + impl RewardingSetup { + pub fn new_rewarding_setup() -> Self { + let mut inner = TestSetup::new(); + let mixing_node = inner.add_dummy_nymnode("mixing-owner", None); + let entry_node = inner.add_dummy_nymnode("entry-owner", None); + let exit_node = inner.add_dummy_nymnode("exit-owner", None); + let standby_node = inner.add_dummy_nymnode("standby-owner", None); + + RewardingSetup { + standby_node, + entry_node, + exit_node, + mixing_node, + inner, + } + } + + pub fn nodes(&self) -> Vec { + vec![ + self.mixing_node, + self.entry_node, + self.exit_node, + self.standby_node, + ] + } + + pub fn reset_rewarded_set(&mut self) { + self.inner.force_assign_rewarded_set(vec![ + RoleAssignment { + role: Role::Layer1, + nodes: vec![self.mixing_node], + }, + RoleAssignment { + role: Role::EntryGateway, + nodes: vec![self.entry_node], + }, + RoleAssignment { + role: Role::ExitGateway, + nodes: vec![self.exit_node], + }, + RoleAssignment { + role: Role::Standby, + nodes: vec![self.standby_node], + }, + ]); + } + + pub fn local_node_role(&self, node_id: NodeId) -> Role { + match node_id { + n if n == self.mixing_node => Role::Layer1, + n if n == self.entry_node => Role::EntryGateway, + n if n == self.exit_node => Role::ExitGateway, + n if n == self.standby_node => Role::Standby, + _ => unreachable!(), + } + } + + pub fn add_to_rewarded_set(&mut self, node_id: NodeId) { + let role = self.local_node_role(node_id); + self.inner.force_assign_rewarded_set(vec![RoleAssignment { + role, + nodes: vec![node_id], + }]) + } + + pub fn reward_all( + &mut self, + performance: f32, + ) -> HashMap> { + let mut results = HashMap::new(); + + self.skip_to_next_epoch_end(); + self.reset_rewarded_set(); + self.start_epoch_transition(); + + let active_params = self.active_node_params(performance); + let standby_params = self.standby_node_params(performance); + + let mixing_node = self.mixing_node; + let entry_node = self.entry_node; + let exit_node = self.exit_node; + let standby_node = self.standby_node; + + results.insert( + mixing_node, + self.execute_rewarding(mixing_node, active_params), + ); + results.insert( + entry_node, + self.execute_rewarding(entry_node, active_params), + ); + results.insert(exit_node, self.execute_rewarding(exit_node, active_params)); + results.insert( + standby_node, + self.execute_rewarding(standby_node, standby_params), + ); + + results + } + } + + impl Deref for RewardingSetup { + type Target = TestSetup; + fn deref(&self) -> &Self::Target { + &self.inner + } + } + + impl DerefMut for RewardingSetup { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.inner + } + } + + #[test] + fn when_target_node_has_zero_performance() { + let mut test = RewardingSetup::new_rewarding_setup(); + let results = test.reward_all(0.); + for res in results.into_values() { + let reward_attr = res.unwrap().any_attribute(NO_REWARD_REASON_KEY); + assert_eq!(reward_attr, ZERO_PERFORMANCE_OR_WORK_VALUE); + } + + let current = test.current_epoch_state(); + assert_eq!(current, EpochState::ReconcilingEvents) + } + + #[test] + fn when_target_node_has_zero_work_factor() { + let mut test = RewardingSetup::new_rewarding_setup(); + + test.skip_to_next_epoch_end(); + test.reset_rewarded_set(); + test.start_epoch_transition(); + + let params = + NodeRewardingParameters::new(test_helpers::performance(100.), WorkFactor::zero()); + + for node in test.nodes() { + let res = test.assert_rewarding(node, params); + let reward_attr = res.any_attribute(NO_REWARD_REASON_KEY); + assert_eq!(reward_attr, ZERO_PERFORMANCE_OR_WORK_VALUE); + } + + let current = test.current_epoch_state(); + assert_eq!(current, EpochState::ReconcilingEvents) + } + + #[test] + fn when_theres_only_one_node_to_reward() { + let test_lookup = RewardingSetup::new_rewarding_setup(); + + for node in test_lookup.nodes() { + let mut actual_setup = RewardingSetup::new_rewarding_setup(); + actual_setup.add_to_rewarded_set(node); + let mut res = actual_setup.reward_all(100.); + + // get the response for this particular node + let res = res.remove(&node).unwrap().unwrap(); + let reward: Decimal = res.any_parsed_attribute(OPERATOR_REWARD_KEY); + assert!(!reward.is_zero()); + + let current = actual_setup.current_epoch_state(); + assert_eq!(current, EpochState::ReconcilingEvents) + } + } + + #[test] + fn when_theres_multiple_nodes_to_reward() { + let mut test = RewardingSetup::new_rewarding_setup(); + let results = test.reward_all(100.); + for res in results.into_values() { + let reward: Decimal = res.unwrap().any_parsed_attribute(OPERATOR_REWARD_KEY); + assert!(!reward.is_zero()); + } + + let current = test.current_epoch_state(); + assert_eq!(current, EpochState::ReconcilingEvents) + } + + #[test] + fn cant_be_performed_for_unbonded_nodes() { + let test_lookup = RewardingSetup::new_rewarding_setup(); + + for node in test_lookup.nodes() { + let mut actual_setup = RewardingSetup::new_rewarding_setup(); + actual_setup.add_to_rewarded_set(node); + + let env = actual_setup.env(); + + // add some delegations to indicate the rewarding information shouldn't get removed + actual_setup.add_immediate_delegation("delegator", Uint128::new(12345678), node); + pending_events::unbond_nym_node(actual_setup.deps_mut(), &env, 123, node).unwrap(); + + let mut res = actual_setup.reward_all(100.); + + // get the response for this particular node + let res = res.remove(&node).unwrap().unwrap(); + let reward_attr = res.any_attribute(NO_REWARD_REASON_KEY); + assert_eq!(reward_attr, BOND_NOT_FOUND_VALUE); + + let current = actual_setup.current_epoch_state(); + assert_eq!(current, EpochState::ReconcilingEvents) + } + } + + #[test] + fn can_only_be_performed_once_per_node_per_epoch() { + let test_lookup = RewardingSetup::new_rewarding_setup(); + + let params = test_lookup.active_node_params(100.0); + for node in test_lookup.nodes() { + let mut actual_setup = RewardingSetup::new_rewarding_setup(); + + actual_setup.skip_to_next_epoch_end(); + + let extra = actual_setup.add_dummy_nymnode("foomp", None); + + // add extra node to the rewarded set so rewarding wouldn't immediately go into event reconciliation + let role = actual_setup.local_node_role(node); + actual_setup + .inner + .force_assign_rewarded_set(vec![RoleAssignment { + role, + nodes: vec![node, extra], + }]); + + actual_setup.start_epoch_transition(); + + // first rewarding + actual_setup.assert_rewarding(node, params); + + // second rewarding + let res = actual_setup.execute_rewarding(node, params).unwrap_err(); + assert_eq!( + res, + MixnetContractError::NodeAlreadyRewarded { + node_id: node, + absolute_epoch_id: 1, + } + ); + } + } + } + #[cfg(test)] mod withdrawing_delegator_reward { use crate::interval::pending_events; use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup}; + use cosmwasm_std::testing::mock_info; use cosmwasm_std::{BankMsg, CosmosMsg, Decimal, Uint128}; use mixnet_contract_common::rewarding::helpers::truncate_reward_amount; @@ -1349,10 +1524,11 @@ pub mod tests { fn can_only_be_done_if_delegation_exists() { let mut test = TestSetup::new(); // add relatively huge stake so that the reward would be high enough to offset operating costs - let mix_id1 = - test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); - let mix_id2 = - test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + let node_id1 = test + .add_rewarded_legacy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let node_id2 = test + .add_rewarded_legacy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + let active_params = test.active_node_params(100.); let delegator1 = "delegator1"; let delegator2 = "delegator2"; @@ -1361,36 +1537,36 @@ pub mod tests { let sender2 = mock_info(delegator2, &[]); // note that there's no delegation from delegator1 towards mix1 - test.add_immediate_delegation(delegator2, 100_000_000u128, mix_id1); + test.add_immediate_delegation(delegator2, 100_000_000u128, node_id1); - test.add_immediate_delegation(delegator1, 100_000_000u128, mix_id2); - test.add_immediate_delegation(delegator2, 100_000_000u128, mix_id2); + test.add_immediate_delegation(delegator1, 100_000_000u128, node_id2); + test.add_immediate_delegation(delegator2, 100_000_000u128, node_id2); // perform some rewarding so that we'd have non-zero rewards test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id1, mix_id2]); + test.force_change_mix_rewarded_set(vec![node_id1, node_id2]); test.start_epoch_transition(); - test.reward_with_distribution(mix_id1, test_helpers::performance(100.0)); - test.reward_with_distribution(mix_id2, test_helpers::performance(100.0)); + test.reward_with_distribution(node_id1, active_params); + test.reward_with_distribution(node_id2, active_params); - let res = try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), mix_id1); + let res = try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), node_id1); assert_eq!( res, - Err(MixnetContractError::NoMixnodeDelegationFound { - mix_id: mix_id1, + Err(MixnetContractError::NodeDelegationNotFound { + node_id: node_id1, address: delegator1.to_string(), proxy: None, }) ); // sanity check for other ones - let res = try_withdraw_delegator_reward(test.deps_mut(), sender1, mix_id2); + let res = try_withdraw_delegator_reward(test.deps_mut(), sender1, node_id2); assert!(res.is_ok()); - let res = try_withdraw_delegator_reward(test.deps_mut(), sender2.clone(), mix_id1); + let res = try_withdraw_delegator_reward(test.deps_mut(), sender2.clone(), node_id1); assert!(res.is_ok()); - let res = try_withdraw_delegator_reward(test.deps_mut(), sender2, mix_id2); + let res = try_withdraw_delegator_reward(test.deps_mut(), sender2, node_id2); assert!(res.is_ok()); } @@ -1398,38 +1574,39 @@ pub mod tests { fn tokens_are_only_sent_if_reward_is_nonzero() { let mut test = TestSetup::new(); // add relatively huge stake so that the reward would be high enough to offset operating costs - let mix_id1 = - test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); - let mix_id2 = - test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + let node_id1 = test + .add_rewarded_legacy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let node_id2 = test + .add_rewarded_legacy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + let active_params = test.active_node_params(100.); // very low stake so operating cost would be higher than total reward let low_stake_id = - test.add_dummy_mixnode("mix-owner3", Some(Uint128::new(100_000_000))); + test.add_rewarded_legacy_mixnode("mix-owner3", Some(Uint128::new(100_000_000))); let delegator = "delegator"; let sender = mock_info(delegator, &[]); - test.add_immediate_delegation(delegator, 100_000_000u128, mix_id1); - test.add_immediate_delegation(delegator, 100_000_000u128, mix_id2); + test.add_immediate_delegation(delegator, 100_000_000u128, node_id1); + test.add_immediate_delegation(delegator, 100_000_000u128, node_id2); test.add_immediate_delegation(delegator, 1_000u128, low_stake_id); // reward mix1, but don't reward mix2 test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id1, low_stake_id]); + test.force_change_mix_rewarded_set(vec![node_id1, low_stake_id]); test.start_epoch_transition(); - test.reward_with_distribution(mix_id1, test_helpers::performance(100.0)); - test.reward_with_distribution(low_stake_id, test_helpers::performance(100.0)); + test.reward_with_distribution(node_id1, active_params); + test.reward_with_distribution(low_stake_id, active_params); let res1 = - try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), mix_id1).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), node_id1).unwrap(); assert!(matches!( &res1.messages[0].msg, CosmosMsg::Bank(BankMsg::Send { to_address, amount }) if to_address == delegator && !amount[0].amount.is_zero() ),); let res2 = - try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), mix_id2).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), node_id2).unwrap(); assert!(res2.messages.is_empty()); let res3 = @@ -1442,27 +1619,27 @@ pub mod tests { // note: if node has unbonded or is in the process of unbonding, the expected // way of getting back the rewards is to completely undelegate let mut test = TestSetup::new(); - let mix_id_unbonding = - test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); - let mix_id_unbonded_leftover = - test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + let node_id_unbonding = test + .add_rewarded_legacy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let node_id_unbonded_leftover = test + .add_rewarded_legacy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); let delegator = "delegator"; let sender = mock_info(delegator, &[]); - test.add_immediate_delegation(delegator, 100_000_000u128, mix_id_unbonding); - test.add_immediate_delegation(delegator, 100_000_000u128, mix_id_unbonded_leftover); + test.add_immediate_delegation(delegator, 100_000_000u128, node_id_unbonding); + test.add_immediate_delegation(delegator, 100_000_000u128, node_id_unbonded_leftover); - let performance = test_helpers::performance(100.0); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_unbonding, mix_id_unbonded_leftover]); + test.force_change_mix_rewarded_set(vec![node_id_unbonding, node_id_unbonded_leftover]); // go through few rewarding cycles before unbonding nodes (partially or fully) for _ in 0..10 { test.start_epoch_transition(); - test.reward_with_distribution(mix_id_unbonding, performance); - test.reward_with_distribution(mix_id_unbonded_leftover, performance); + test.reward_with_distribution(node_id_unbonding, active_params); + test.reward_with_distribution(node_id_unbonded_leftover, active_params); test.skip_to_next_epoch_end(); // bypass proper epoch progression and force change the state @@ -1471,32 +1648,32 @@ pub mod tests { // start unbonding the first node and fully unbond the other let mut bond = mixnodes_storage::mixnode_bonds() - .load(test.deps().storage, mix_id_unbonding) + .load(test.deps().storage, node_id_unbonding) .unwrap(); bond.is_unbonding = true; mixnodes_storage::mixnode_bonds() - .save(test.deps_mut().storage, mix_id_unbonding, &bond) + .save(test.deps_mut().storage, node_id_unbonding, &bond) .unwrap(); let env = test.env(); - pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover) + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, node_id_unbonded_leftover) .unwrap(); let res = - try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), mix_id_unbonding); + try_withdraw_delegator_reward(test.deps_mut(), sender.clone(), node_id_unbonding); assert_eq!( res, Err(MixnetContractError::MixnodeIsUnbonding { - mix_id: mix_id_unbonding + mix_id: node_id_unbonding }) ); let res = - try_withdraw_delegator_reward(test.deps_mut(), sender, mix_id_unbonded_leftover); + try_withdraw_delegator_reward(test.deps_mut(), sender, node_id_unbonded_leftover); assert_eq!( res, - Err(MixnetContractError::MixnodeHasUnbonded { - mix_id: mix_id_unbonded_leftover + Err(MixnetContractError::NymNodeBondNotFound { + node_id: node_id_unbonded_leftover }) ); } @@ -1504,10 +1681,10 @@ pub mod tests { #[test] fn correctly_determines_earned_share_and_resets_reward_ratio() { let mut test = TestSetup::new(); - let mix_id_single = - test.add_dummy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); - let mix_id_quad = - test.add_dummy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); + let node_id_single = test + .add_rewarded_legacy_mixnode("mix-owner1", Some(Uint128::new(1_000_000_000_000))); + let node_id_quad = test + .add_rewarded_legacy_mixnode("mix-owner2", Some(Uint128::new(1_000_000_000_000))); let delegator1 = "delegator1"; let delegator2 = "delegator2"; @@ -1525,28 +1702,28 @@ pub mod tests { let amount_quad3 = 250_000_000u128; let amount_quad4 = 500_000_000u128; - test.add_immediate_delegation(delegator1, amount_single, mix_id_single); + test.add_immediate_delegation(delegator1, amount_single, node_id_single); - test.add_immediate_delegation(delegator1, amount_quad1, mix_id_quad); - test.add_immediate_delegation(delegator2, amount_quad2, mix_id_quad); - test.add_immediate_delegation(delegator3, amount_quad3, mix_id_quad); - test.add_immediate_delegation(delegator4, amount_quad4, mix_id_quad); + test.add_immediate_delegation(delegator1, amount_quad1, node_id_quad); + test.add_immediate_delegation(delegator2, amount_quad2, node_id_quad); + test.add_immediate_delegation(delegator3, amount_quad3, node_id_quad); + test.add_immediate_delegation(delegator4, amount_quad4, node_id_quad); - let performance = test_helpers::performance(100.0); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_single, mix_id_quad]); + test.force_change_mix_rewarded_set(vec![node_id_single, node_id_quad]); // accumulate some rewards let mut accumulated_single = Decimal::zero(); let mut accumulated_quad = Decimal::zero(); for _ in 0..10 { test.start_epoch_transition(); - let dist = test.reward_with_distribution(mix_id_single, performance); + let dist = test.reward_with_distribution(node_id_single, active_params); // sanity check to make sure test is actually doing what it's supposed to be doing assert!(!dist.delegates.is_zero()); accumulated_single += dist.delegates; - let dist = test.reward_with_distribution(mix_id_quad, performance); + let dist = test.reward_with_distribution(node_id_quad, active_params); accumulated_quad += dist.delegates; test.skip_to_next_epoch_end(); @@ -1554,30 +1731,32 @@ pub mod tests { test.set_epoch_in_progress_state(); } - let before = test.read_delegation(mix_id_single, delegator1, None); + let before = test.read_delegation(node_id_single, delegator1, None); assert_eq!(before.cumulative_reward_ratio, Decimal::zero()); let res1 = - try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), mix_id_single) + try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), node_id_single) .unwrap(); let (_, reward) = test_helpers::get_bank_send_msg(&res1).unwrap(); assert_eq!(truncate_reward_amount(accumulated_single), reward[0].amount); - let after = test.read_delegation(mix_id_single, delegator1, None); + let after = test.read_delegation(node_id_single, delegator1, None); assert_ne!(after.cumulative_reward_ratio, Decimal::zero()); assert_eq!( after.cumulative_reward_ratio, - test.mix_rewarding(mix_id_single).total_unit_reward + test.mix_rewarding(node_id_single).total_unit_reward ); // withdraw first two rewards. note that due to scaling we expect second reward to be 4x the first one - let before1 = test.read_delegation(mix_id_quad, delegator1, None); - let before2 = test.read_delegation(mix_id_quad, delegator2, None); + let before1 = test.read_delegation(node_id_quad, delegator1, None); + let before2 = test.read_delegation(node_id_quad, delegator2, None); assert_eq!(before1.cumulative_reward_ratio, Decimal::zero()); assert_eq!(before2.cumulative_reward_ratio, Decimal::zero()); - let res1 = try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), mix_id_quad) - .unwrap(); + let res1 = + try_withdraw_delegator_reward(test.deps_mut(), sender1.clone(), node_id_quad) + .unwrap(); let (_, reward1) = test_helpers::get_bank_send_msg(&res1).unwrap(); - let res2 = try_withdraw_delegator_reward(test.deps_mut(), sender2.clone(), mix_id_quad) - .unwrap(); + let res2 = + try_withdraw_delegator_reward(test.deps_mut(), sender2.clone(), node_id_quad) + .unwrap(); let (_, reward2) = test_helpers::get_bank_send_msg(&res2).unwrap(); // the seeming "error" comes from reward truncation, // say "actual" reward1 was `100.9`, while reward2 was 4x that, i.e. `403.6 @@ -1592,34 +1771,34 @@ pub mod tests { Uint128::new(4), ); - let after1 = test.read_delegation(mix_id_quad, delegator1, None); + let after1 = test.read_delegation(node_id_quad, delegator1, None); assert_ne!(after1.cumulative_reward_ratio, Decimal::zero()); assert_eq!( after1.cumulative_reward_ratio, - test.mix_rewarding(mix_id_quad).total_unit_reward + test.mix_rewarding(node_id_quad).total_unit_reward ); - let after2 = test.read_delegation(mix_id_quad, delegator2, None); + let after2 = test.read_delegation(node_id_quad, delegator2, None); assert_ne!(after2.cumulative_reward_ratio, Decimal::zero()); assert_eq!( after2.cumulative_reward_ratio, - test.mix_rewarding(mix_id_quad).total_unit_reward + test.mix_rewarding(node_id_quad).total_unit_reward ); // accumulate some more for _ in 0..10 { test.start_epoch_transition(); - let dist = test.reward_with_distribution(mix_id_quad, performance); + let dist = test.reward_with_distribution(node_id_quad, active_params); accumulated_quad += dist.delegates; test.skip_to_next_epoch_end(); // bypass proper epoch progression and force change the state test.set_epoch_in_progress_state(); } - let before1_new = test.read_delegation(mix_id_quad, delegator1, None); - let before2_new = test.read_delegation(mix_id_quad, delegator2, None); - let before3 = test.read_delegation(mix_id_quad, delegator3, None); - let before4 = test.read_delegation(mix_id_quad, delegator4, None); + let before1_new = test.read_delegation(node_id_quad, delegator1, None); + let before2_new = test.read_delegation(node_id_quad, delegator2, None); + let before3 = test.read_delegation(node_id_quad, delegator3, None); + let before4 = test.read_delegation(node_id_quad, delegator4, None); assert_eq!( before1_new.cumulative_reward_ratio, @@ -1633,10 +1812,10 @@ pub mod tests { assert_eq!(before4.cumulative_reward_ratio, Decimal::zero()); let res1 = - try_withdraw_delegator_reward(test.deps_mut(), sender1, mix_id_quad).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender1, node_id_quad).unwrap(); let (_, reward1_new) = test_helpers::get_bank_send_msg(&res1).unwrap(); let res2 = - try_withdraw_delegator_reward(test.deps_mut(), sender2, mix_id_quad).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender2, node_id_quad).unwrap(); let (_, reward2_new) = test_helpers::get_bank_send_msg(&res2).unwrap(); // the ratio between first and second delegator is still the same @@ -1647,10 +1826,10 @@ pub mod tests { ); let res3 = - try_withdraw_delegator_reward(test.deps_mut(), sender3, mix_id_quad).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender3, node_id_quad).unwrap(); let (_, reward3) = test_helpers::get_bank_send_msg(&res3).unwrap(); let res4 = - try_withdraw_delegator_reward(test.deps_mut(), sender4, mix_id_quad).unwrap(); + try_withdraw_delegator_reward(test.deps_mut(), sender4, node_id_quad).unwrap(); let (_, reward4) = test_helpers::get_bank_send_msg(&res4).unwrap(); // (and so is the ratio between 3rd and 4th) @@ -1677,6 +1856,7 @@ pub mod tests { #[cfg(test)] mod withdrawing_operator_reward { use super::*; + use crate::compat::transactions::try_withdraw_operator_reward; use crate::interval::pending_events; use crate::support::tests::test_helpers::TestSetup; use cosmwasm_std::{Addr, BankMsg, CosmosMsg, Uint128}; @@ -1686,18 +1866,20 @@ pub mod tests { let mut test = TestSetup::new(); let owner = "mix-owner"; - let mix_id = test.add_dummy_mixnode(owner, Some(Uint128::new(1_000_000_000_000))); + let node_id = + test.add_rewarded_legacy_mixnode(owner, Some(Uint128::new(1_000_000_000_000))); let sender = mock_info("random-guy", &[]); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id]); + test.force_change_mix_rewarded_set(vec![node_id]); test.start_epoch_transition(); - test.reward_with_distribution(mix_id, test_helpers::performance(100.0)); + test.reward_with_distribution(node_id, active_params); let res = try_withdraw_operator_reward(test.deps_mut(), sender.clone()); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: sender.sender }) ) @@ -1709,17 +1891,19 @@ pub mod tests { let owner1 = "mix-owner1"; let owner2 = "mix-owner2"; - let mix_id1 = test.add_dummy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); - test.add_dummy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); + let node_id1 = + test.add_rewarded_legacy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); + test.add_rewarded_legacy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); + let active_params = test.active_node_params(100.); let sender1 = mock_info(owner1, &[]); let sender2 = mock_info(owner2, &[]); // reward mix1, but don't reward mix2 test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id1]); + test.force_change_mix_rewarded_set(vec![node_id1]); test.start_epoch_transition(); - test.reward_with_distribution(mix_id1, test_helpers::performance(100.0)); + test.reward_with_distribution(node_id1, active_params); let res1 = try_withdraw_operator_reward(test.deps_mut(), sender1).unwrap(); assert!(matches!( @@ -1740,23 +1924,23 @@ pub mod tests { let owner2 = "mix-owner2"; let sender1 = mock_info(owner1, &[]); let sender2 = mock_info(owner2, &[]); - let mix_id_unbonding = - test.add_dummy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); - let mix_id_unbonded_leftover = - test.add_dummy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); + let node_id_unbonding = + test.add_rewarded_legacy_mixnode(owner1, Some(Uint128::new(1_000_000_000_000))); + let node_id_unbonded_leftover = + test.add_rewarded_legacy_mixnode(owner2, Some(Uint128::new(1_000_000_000_000))); // add some delegation to the second node so that it wouldn't be cleared upon unbonding - test.add_immediate_delegation("delegator", 100_000_000u128, mix_id_unbonded_leftover); + test.add_immediate_delegation("delegator", 100_000_000u128, node_id_unbonded_leftover); - let performance = test_helpers::performance(100.0); + let active_params = test.active_node_params(100.); test.skip_to_next_epoch_end(); - test.force_change_rewarded_set(vec![mix_id_unbonding, mix_id_unbonded_leftover]); + test.force_change_mix_rewarded_set(vec![node_id_unbonding, node_id_unbonded_leftover]); // go through few rewarding cycles before unbonding nodes (partially or fully) for _ in 0..10 { test.start_epoch_transition(); - test.reward_with_distribution(mix_id_unbonding, performance); - test.reward_with_distribution(mix_id_unbonded_leftover, performance); + test.reward_with_distribution(node_id_unbonding, active_params); + test.reward_with_distribution(node_id_unbonded_leftover, active_params); test.skip_to_next_epoch_end(); // bypass proper epoch progression and force change the state @@ -1765,29 +1949,29 @@ pub mod tests { // start unbonding the first node and fully unbond the other let mut bond = mixnodes_storage::mixnode_bonds() - .load(test.deps().storage, mix_id_unbonding) + .load(test.deps().storage, node_id_unbonding) .unwrap(); bond.is_unbonding = true; mixnodes_storage::mixnode_bonds() - .save(test.deps_mut().storage, mix_id_unbonding, &bond) + .save(test.deps_mut().storage, node_id_unbonding, &bond) .unwrap(); let env = test.env(); - pending_events::unbond_mixnode(test.deps_mut(), &env, 123, mix_id_unbonded_leftover) + pending_events::unbond_mixnode(test.deps_mut(), &env, 123, node_id_unbonded_leftover) .unwrap(); let res = try_withdraw_operator_reward(test.deps_mut(), sender1); assert_eq!( res, - Err(MixnetContractError::MixnodeIsUnbonding { - mix_id: mix_id_unbonding + Err(MixnetContractError::NodeIsUnbonding { + node_id: node_id_unbonding }) ); let res = try_withdraw_operator_reward(test.deps_mut(), sender2); assert_eq!( res, - Err(MixnetContractError::NoAssociatedMixNodeBond { + Err(MixnetContractError::NoAssociatedNodeBond { owner: Addr::unchecked(owner2) }) ); @@ -1797,6 +1981,7 @@ pub mod tests { #[cfg(test)] mod updating_active_set { use cw_controllers::AdminError::NotAdmin; + use mixnet_contract_common::nym_node::Role; use mixnet_contract_common::EpochStatus; use crate::support::tests::test_helpers::TestSetup; @@ -1811,7 +1996,9 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; for bad_state in bad_states { @@ -1825,11 +2012,11 @@ pub mod tests { interval_storage::save_current_epoch_status(test.deps_mut().storage, &status) .unwrap(); - let res = try_update_active_set_size( + let res = try_update_active_set_distribution( test.deps_mut(), env.clone(), owner.clone(), - 100, + active_set_update_fixture(), false, ); assert!(matches!( @@ -1837,8 +2024,13 @@ pub mod tests { Err(MixnetContractError::EpochAdvancementInProgress { .. }) )); - let res_forced = - try_update_active_set_size(test.deps_mut(), env.clone(), owner, 100, true); + let res_forced = try_update_active_set_distribution( + test.deps_mut(), + env.clone(), + owner, + active_set_update_fixture(), + true, + ); assert!(res_forced.is_ok()) } } @@ -1852,19 +2044,31 @@ pub mod tests { let random = mock_info("random-guy", &[]); let env = test.env(); - let res = try_update_active_set_size( + let res = try_update_active_set_distribution( test.deps_mut(), env.clone(), rewarding_validator, - 42, + active_set_update_fixture(), false, ); assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); - let res = try_update_active_set_size(test.deps_mut(), env.clone(), random, 42, false); + let res = try_update_active_set_distribution( + test.deps_mut(), + env.clone(), + random, + active_set_update_fixture(), + false, + ); assert_eq!(res, Err(MixnetContractError::Admin(NotAdmin {}))); - let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, false); + let res = try_update_active_set_distribution( + test.deps_mut(), + env, + owner, + active_set_update_fixture(), + false, + ); assert!(res.is_ok()) } @@ -1876,14 +2080,18 @@ pub mod tests { let rewarded_set_size = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .rewarded_set_size; + .rewarded_set_size(); let env = test.env(); - let res = try_update_active_set_size( + let res = try_update_active_set_distribution( test.deps_mut(), env, owner.clone(), - rewarded_set_size + 1, + ActiveSetUpdate { + entry_gateways: rewarded_set_size, + exit_gateways: rewarded_set_size, + mixnodes: rewarded_set_size * 3, + }, false, ); assert_eq!(res, Err(MixnetContractError::InvalidActiveSetSize)); @@ -1892,11 +2100,15 @@ pub mod tests { // (make sure we start with the fresh state) let mut test = TestSetup::new(); let env = test.env(); - let res = try_update_active_set_size( + let res = try_update_active_set_distribution( test.deps_mut(), env, owner.clone(), - rewarded_set_size, + ActiveSetUpdate { + entry_gateways: rewarded_set_size / 3, + exit_gateways: rewarded_set_size / 3, + mixnodes: rewarded_set_size / 3, + }, false, ); assert!(res.is_ok()); @@ -1904,11 +2116,15 @@ pub mod tests { // as well as if its any value lower than that let mut test = TestSetup::new(); let env = test.env(); - let res = try_update_active_set_size( + let res = try_update_active_set_distribution( test.deps_mut(), env, owner, - rewarded_set_size - 100, + ActiveSetUpdate { + entry_gateways: 1, + exit_gateways: 1, + mixnodes: 3, + }, false, ); assert!(res.is_ok()); @@ -1922,27 +2138,37 @@ pub mod tests { let old = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); test.skip_to_current_interval_end(); let env = test.env(); - let res = try_update_active_set_size(test.deps_mut(), env, owner.clone(), 42, false); + + let update = active_set_update_fixture(); + let expected = update.active_set_size(); + let res = try_update_active_set_distribution( + test.deps_mut(), + env, + owner.clone(), + update, + false, + ); assert!(res.is_ok()); let new = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); assert_ne!(old, new); - assert_eq!(new, 42); + assert_eq!(new, expected); // sanity check for "normal" case let mut test = TestSetup::new(); let env = test.env(); - let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, false); + let res = + try_update_active_set_distribution(test.deps_mut(), env, owner, update, false); assert!(res.is_ok()); let new = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); assert_eq!(old, new); } @@ -1954,16 +2180,19 @@ pub mod tests { let old = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); let env = test.env(); - let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, true); + + let update = active_set_update_fixture(); + let expected = update.active_set_size(); + let res = try_update_active_set_distribution(test.deps_mut(), env, owner, update, true); assert!(res.is_ok()); let new = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); assert_ne!(old, new); - assert_eq!(new, 42); + assert_eq!(new, expected); } #[test] @@ -1974,29 +2203,39 @@ pub mod tests { let old = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); let env = test.env(); - let res = try_update_active_set_size(test.deps_mut(), env, owner, 42, false); + + let issued_update = active_set_update_fixture(); + let expected_updated = issued_update.active_set_size(); + let res = try_update_active_set_distribution( + test.deps_mut(), + env, + owner, + issued_update, + false, + ); assert!(res.is_ok()); let new = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); assert_eq!(old, new); // make sure it's actually saved to pending events let events = test.pending_epoch_events(); - assert!( - matches!(events[0].kind, PendingEpochEventKind::UpdateActiveSetSize { new_size } if new_size == 42) - ); + let PendingEpochEventKind::UpdateActiveSet { update } = events[0].kind else { + panic!("unexpected epoch event") + }; + assert_eq!(update, issued_update); test.execute_all_pending_events(); let new = storage::REWARDING_PARAMS .load(test.deps().storage) .unwrap() - .active_set_size; + .active_set_size(); assert_ne!(old, new); - assert_eq!(new, 42); + assert_eq!(new, expected_updated); } } @@ -2005,6 +2244,8 @@ pub mod tests { use cosmwasm_std::Decimal; use cw_controllers::AdminError::NotAdmin; + use mixnet_contract_common::nym_node::Role; + use mixnet_contract_common::reward_params::RewardedSetParams; use mixnet_contract_common::EpochStatus; use crate::support::tests::test_helpers::{assert_decimals, TestSetup}; @@ -2019,7 +2260,9 @@ pub mod tests { final_node_id: 0, }, EpochState::ReconcilingEvents, - EpochState::AdvancingEpoch, + EpochState::RoleAssignment { + next: Role::first(), + }, ]; let update = IntervalRewardingParamsUpdate { @@ -2029,7 +2272,12 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: Some(123), + rewarded_set_params: Some(RewardedSetParams { + entry_gateways: 123, + exit_gateways: 123, + mixnodes: 300, + standby: 123, + }), }; for bad_state in bad_states { @@ -2076,7 +2324,12 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: Some(123), + rewarded_set_params: Some(RewardedSetParams { + entry_gateways: 123, + exit_gateways: 123, + mixnodes: 300, + standby: 123, + }), }; let env = test.env(); @@ -2109,7 +2362,7 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: None, + rewarded_set_params: None, }; let env = test.env(); @@ -2129,7 +2382,12 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: Some(123), + rewarded_set_params: Some(RewardedSetParams { + entry_gateways: 123, + exit_gateways: 123, + mixnodes: 300, + standby: 123, + }), }; let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); @@ -2141,7 +2399,7 @@ pub mod tests { assert!(res.is_ok()); let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); assert_ne!(old, new); - assert_eq!(new.rewarded_set_size, 123); + assert_eq!(new.rewarded_set_size(), 669); // sanity check for "normal" case let mut test = TestSetup::new(); @@ -2164,7 +2422,12 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: Some(123), + rewarded_set_params: Some(RewardedSetParams { + entry_gateways: 123, + exit_gateways: 123, + mixnodes: 300, + standby: 123, + }), }; let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); @@ -2173,7 +2436,7 @@ pub mod tests { assert!(res.is_ok()); let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); assert_ne!(old, new); - assert_eq!(new.rewarded_set_size, 123); + assert_eq!(new.rewarded_set_size(), 669); } #[test] @@ -2188,7 +2451,12 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: Some(123), + rewarded_set_params: Some(RewardedSetParams { + entry_gateways: 123, + exit_gateways: 123, + mixnodes: 300, + standby: 123, + }), }; let old = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); @@ -2200,14 +2468,18 @@ pub mod tests { // make sure it's actually saved to pending events let events = test.pending_interval_events(); - assert!( - matches!(events[0].kind,PendingIntervalEventKind::UpdateRewardingParams { update } if update.rewarded_set_size == Some(123)) - ); + let PendingIntervalEventKind::UpdateRewardingParams { update } = events[0].kind else { + panic!("unexpected epoch event") + }; + let Some(rewarded_set_update) = update.rewarded_set_params else { + panic!("no rewarded set updates"); + }; + assert_eq!(rewarded_set_update.rewarded_set_size(), 669); test.execute_all_pending_events(); let new = storage::REWARDING_PARAMS.load(test.deps().storage).unwrap(); assert_ne!(old, new); - assert_eq!(new.rewarded_set_size, 123); + assert_eq!(new.rewarded_set_size(), 669); } #[test] @@ -2229,7 +2501,7 @@ pub mod tests { sybil_resistance_percent: None, active_set_work_factor: None, interval_pool_emission: None, - rewarded_set_size: None, + rewarded_set_params: None, }; let env = test.env(); diff --git a/contracts/mixnet/src/support/helpers.rs b/contracts/mixnet/src/support/helpers.rs index 19ddf2bb6e..8997882462 100644 --- a/contracts/mixnet/src/support/helpers.rs +++ b/contracts/mixnet/src/support/helpers.rs @@ -3,46 +3,17 @@ use crate::gateways::storage as gateways_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; +use crate::mixnodes::helpers::must_get_mixnode_bond_by_owner; use crate::mixnodes::storage as mixnodes_storage; -use cosmwasm_std::{Addr, BankMsg, Coin, CosmosMsg, Response, Storage}; +use crate::nodes::helpers::must_get_node_bond_by_owner; +use crate::nodes::storage as nymnodes_storage; +use cosmwasm_std::{Addr, Coin, Storage}; use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::mixnode::PendingMixNodeChanges; -use mixnet_contract_common::{EpochState, EpochStatus, IdentityKeyRef, MixNodeBond}; +use mixnet_contract_common::{EpochState, EpochStatus, IdentityKeyRef, MixNodeBond, NodeId}; +use nym_contracts_common::IdentityKey; use nym_contracts_common::Percent; -// helper trait to attach `Msg` to a response if it's provided -#[allow(dead_code)] -pub(crate) trait AttachOptionalMessage { - fn add_optional_message(self, msg: Option>>) -> Self; -} - -impl AttachOptionalMessage for Response { - fn add_optional_message(self, msg: Option>>) -> Self { - if let Some(msg) = msg { - self.add_message(msg) - } else { - self - } - } -} - -pub(crate) trait AttachSendTokens { - fn send_tokens(self, to: impl AsRef, amount: Coin) -> Self; -} - -impl AttachSendTokens for Response { - fn send_tokens(self, to: impl AsRef, amount: Coin) -> Self { - self.add_message(BankMsg::Send { - to_address: to.as_ref().to_string(), - amount: vec![amount], - }) - } -} - -// pub fn debug_with_visibility>(api: &dyn Api, msg: S) { -// api.debug(&*format!("\n\n\n=========================================\n{}\n=========================================\n\n\n", msg.into())); -// } - pub(crate) fn validate_pledge( mut pledge: Vec, minimum_pledge: Coin, @@ -132,40 +103,6 @@ pub(crate) fn ensure_epoch_in_progress_state( Ok(()) } -// pub(crate) fn ensure_mix_rewarding_state(storage: &dyn Storage) -> Result<(), MixnetContractError> { -// let epoch_status = crate::interval::storage::current_epoch_status(storage)?; -// if !matches!(epoch_status.state, EpochState::Rewarding { .. }) { -// return Err(MixnetContractError::EpochNotInMixRewardingState { -// current_state: epoch_status.state, -// }); -// } -// Ok(()) -// } -// -// pub(crate) fn ensure_event_reconciliation_state( -// storage: &dyn Storage, -// ) -> Result<(), MixnetContractError> { -// let epoch_status = crate::interval::storage::current_epoch_status(storage)?; -// if !matches!(epoch_status.state, EpochState::ReconcilingEvents) { -// return Err(MixnetContractError::EpochNotInEventReconciliationState { -// current_state: epoch_status.state, -// }); -// } -// Ok(()) -// } -// -// pub(crate) fn ensure_epoch_advancement_state( -// storage: &dyn Storage, -// ) -> Result<(), MixnetContractError> { -// let epoch_status = crate::interval::storage::current_epoch_status(storage)?; -// if !matches!(epoch_status.state, EpochState::AdvancingEpoch) { -// return Err(MixnetContractError::EpochNotInAdvancementState { -// current_state: epoch_status.state, -// }); -// } -// Ok(()) -// } - pub(crate) fn ensure_is_authorized( sender: &Addr, storage: &dyn Storage, @@ -212,12 +149,66 @@ pub(crate) fn ensure_no_pending_pledge_changes( Ok(()) } +pub(crate) fn ensure_no_pending_params_changes( + pending_changes: &PendingMixNodeChanges, +) -> Result<(), MixnetContractError> { + if let Some(pending_event_id) = pending_changes.cost_params_change { + return Err(MixnetContractError::PendingParamsChange { pending_event_id }); + } + Ok(()) +} + +/// get identity key of the currently bonded legacy mixnode or nym-node +#[allow(dead_code)] +pub(crate) fn get_bond_identity( + storage: &dyn Storage, + owner: &Addr, +) -> Result { + // legacy mixnode + if let Ok(bond) = must_get_mixnode_bond_by_owner(storage, owner) { + return Ok(bond.mix_node.identity_key); + } + // current nym-node + must_get_node_bond_by_owner(storage, owner).map(|b| b.node.identity_key) +} + +/// Checks whether a nym-node or a legacy mixnode with the provided id is currently bonded +pub(crate) fn ensure_any_node_bonded( + storage: &dyn Storage, + node_id: NodeId, +) -> Result<(), MixnetContractError> { + // legacy mixnode + if let Some(mixnode_bond) = mixnodes_storage::mixnode_bonds().may_load(storage, node_id)? { + return if mixnode_bond.is_unbonding { + Err(MixnetContractError::MixnodeIsUnbonding { mix_id: node_id }) + } else { + Ok(()) + }; + } + + // current nym-node + match nymnodes_storage::nym_nodes().may_load(storage, node_id)? { + None => Err(MixnetContractError::NymNodeBondNotFound { node_id }), + Some(bond) if bond.is_unbonding => Err(MixnetContractError::NodeIsUnbonding { node_id }), + _ => Ok(()), + } +} + // check if the target address has already bonded a mixnode or gateway, // in either case, return an appropriate error pub(crate) fn ensure_no_existing_bond( sender: &Addr, storage: &dyn Storage, ) -> Result<(), MixnetContractError> { + if nymnodes_storage::nym_nodes() + .idx + .owner + .item(storage, sender.clone())? + .is_some() + { + return Err(MixnetContractError::AlreadyOwnsNymNode); + } + if mixnodes_storage::mixnode_bonds() .idx .owner @@ -275,7 +266,7 @@ pub fn ensure_operating_cost_within_range( storage: &dyn Storage, operating_cost: &Coin, ) -> Result<(), MixnetContractError> { - let range = mixnet_params_storage::interval_oprating_cost_range(storage)?; + let range = mixnet_params_storage::interval_operating_cost_range(storage)?; if !range.within_range(operating_cost.amount) { return Err(MixnetContractError::OperatingCostOutsideRange { denom: operating_cost.denom.clone(), diff --git a/contracts/mixnet/src/support/legacy.rs b/contracts/mixnet/src/support/legacy.rs new file mode 100644 index 0000000000..755fb6cc8b --- /dev/null +++ b/contracts/mixnet/src/support/legacy.rs @@ -0,0 +1,2 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 diff --git a/contracts/mixnet/src/support/mod.rs b/contracts/mixnet/src/support/mod.rs index f27f7eecd0..ecef1156f3 100644 --- a/contracts/mixnet/src/support/mod.rs +++ b/contracts/mixnet/src/support/mod.rs @@ -2,4 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 pub(crate) mod helpers; +pub(crate) mod legacy; + +#[cfg(test)] pub(crate) mod tests; diff --git a/contracts/mixnet/src/support/tests/fixtures.rs b/contracts/mixnet/src/support/tests/fixtures.rs index 5a3d5e38a6..d76b9598b8 100644 --- a/contracts/mixnet/src/support/tests/fixtures.rs +++ b/contracts/mixnet/src/support/tests/fixtures.rs @@ -1,7 +1,11 @@ -use crate::constants::{INITIAL_GATEWAY_PLEDGE_AMOUNT, INITIAL_MIXNODE_PLEDGE_AMOUNT}; +use crate::constants::INITIAL_PLEDGE_AMOUNT; use cosmwasm_std::{coin, Coin}; -use mixnet_contract_common::mixnode::MixNodeCostParams; -use mixnet_contract_common::{Gateway, MixNode, Percent}; +use mixnet_contract_common::mixnode::NodeCostParams; +use mixnet_contract_common::reward_params::ActiveSetUpdate; +use mixnet_contract_common::{ + Gateway, MixNode, Percent, DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, + DEFAULT_PROFIT_MARGIN_PERCENT, +}; pub const TEST_COIN_DENOM: &str = "unym"; @@ -17,10 +21,11 @@ pub fn mix_node_fixture() -> MixNode { } } -pub fn mix_node_cost_params_fixture() -> MixNodeCostParams { - MixNodeCostParams { - profit_margin_percent: Percent::from_percentage_value(10).unwrap(), - interval_operating_cost: coin(40_000_000, TEST_COIN_DENOM), +pub fn node_cost_params_fixture() -> NodeCostParams { + NodeCostParams { + profit_margin_percent: Percent::from_percentage_value(DEFAULT_PROFIT_MARGIN_PERCENT) + .unwrap(), + interval_operating_cost: coin(DEFAULT_INTERVAL_OPERATING_COST_AMOUNT, TEST_COIN_DENOM), } } @@ -36,16 +41,25 @@ pub fn gateway_fixture() -> Gateway { } } -pub fn good_mixnode_pledge() -> Vec { +pub fn good_node_plegge() -> Vec { vec![Coin { denom: TEST_COIN_DENOM.to_string(), - amount: INITIAL_MIXNODE_PLEDGE_AMOUNT, + amount: INITIAL_PLEDGE_AMOUNT, }] } +pub fn good_mixnode_pledge() -> Vec { + good_node_plegge() +} + pub fn good_gateway_pledge() -> Vec { - vec![Coin { - denom: TEST_COIN_DENOM.to_string(), - amount: INITIAL_GATEWAY_PLEDGE_AMOUNT, - }] + good_node_plegge() +} + +pub fn active_set_update_fixture() -> ActiveSetUpdate { + ActiveSetUpdate { + entry_gateways: 30, + exit_gateways: 30, + mixnodes: 30, + } } diff --git a/contracts/mixnet/src/support/tests/legacy.rs b/contracts/mixnet/src/support/tests/legacy.rs new file mode 100644 index 0000000000..2fc8cfe93a --- /dev/null +++ b/contracts/mixnet/src/support/tests/legacy.rs @@ -0,0 +1,65 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::gateways::storage as gateways_storage; +use crate::gateways::storage::PREASSIGNED_LEGACY_IDS; +use crate::interval::storage as interval_storage; +use crate::mixnodes::storage as mixnodes_storage; +use crate::nodes::storage::next_nymnode_id_counter; +use crate::rewards::storage as rewards_storage; +use cosmwasm_std::{Addr, Coin, Env, Storage}; +use mixnet_contract_common::error::MixnetContractError; +use mixnet_contract_common::{ + Gateway, GatewayBond, MixNode, MixNodeBond, NodeCostParams, NodeId, NodeRewarding, +}; +use nym_contracts_common::IdentityKey; + +pub(crate) fn save_new_mixnode( + storage: &mut dyn Storage, + env: Env, + mixnode: MixNode, + cost_params: NodeCostParams, + owner: Addr, + pledge: Coin, +) -> Result { + let mix_id = next_nymnode_id_counter(storage)?; + let current_epoch = interval_storage::current_interval(storage)?.current_epoch_absolute_id(); + + let mixnode_rewarding = NodeRewarding::initialise_new(cost_params, &pledge, current_epoch)?; + let mixnode_bond = MixNodeBond { + mix_id, + owner, + original_pledge: pledge, + mix_node: mixnode, + proxy: None, + bonding_height: env.block.height, + is_unbonding: false, + }; + + // save mixnode bond data + // note that this implicitly checks for uniqueness on identity key, sphinx key and owner + mixnodes_storage::mixnode_bonds().save(storage, mix_id, &mixnode_bond)?; + + // save rewarding data + rewards_storage::MIXNODE_REWARDING.save(storage, mix_id, &mixnode_rewarding)?; + + Ok(mix_id) +} + +pub(crate) fn save_new_gateway( + storage: &mut dyn Storage, + env: Env, + gateway: Gateway, + owner: Addr, + pledge: Coin, +) -> Result<(IdentityKey, NodeId), MixnetContractError> { + let gateway_identity = gateway.identity_key.clone(); + let bond = GatewayBond::new(pledge.clone(), owner.clone(), env.block.height, gateway); + + gateways_storage::gateways().save(storage, bond.identity(), &bond)?; + + let id = next_nymnode_id_counter(storage)?; + PREASSIGNED_LEGACY_IDS.save(storage, gateway_identity.clone(), &id)?; + + Ok((gateway_identity, id)) +} diff --git a/contracts/mixnet/src/support/tests/messages.rs b/contracts/mixnet/src/support/tests/messages.rs deleted file mode 100644 index 61a0815d91..0000000000 --- a/contracts/mixnet/src/support/tests/messages.rs +++ /dev/null @@ -1,36 +0,0 @@ -use cosmwasm_std::{Coin, Deps}; -use mixnet_contract_common::{ExecuteMsg, Gateway, IdentityKey}; -use nym_crypto::asymmetric::identity; -use rand_chacha::rand_core::{CryptoRng, RngCore}; - -use crate::support::tests; -use crate::support::tests::test_helpers::{ed25519_sign_message, gateway_bonding_sign_payload}; - -pub(crate) fn valid_bond_gateway_msg( - mut rng: impl RngCore + CryptoRng, - deps: Deps<'_>, - stake: Vec, - sender: &str, -) -> (ExecuteMsg, IdentityKey) { - let keypair = identity::KeyPair::new(&mut rng); - let identity_key = keypair.public_key().to_base58_string(); - let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut rng); - - let gateway = Gateway { - identity_key, - sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), - ..tests::fixtures::gateway_fixture() - }; - - let msg = gateway_bonding_sign_payload(deps, sender, gateway.clone(), stake); - let owner_signature = ed25519_sign_message(msg, keypair.private_key()); - - let identity_key = keypair.public_key().to_base58_string(); - ( - ExecuteMsg::BondGateway { - gateway, - owner_signature, - }, - identity_key, - ) -} diff --git a/contracts/mixnet/src/support/tests/mod.rs b/contracts/mixnet/src/support/tests/mod.rs index dee121b2f0..ed59e8a19c 100644 --- a/contracts/mixnet/src/support/tests/mod.rs +++ b/contracts/mixnet/src/support/tests/mod.rs @@ -3,85 +3,152 @@ #[cfg(test)] pub mod fixtures; -#[cfg(test)] -pub mod messages; +pub(crate) mod legacy; #[cfg(test)] pub mod test_helpers { use crate::constants; - use crate::contract::instantiate; - use crate::delegations::queries::query_mixnode_delegations_paged; + use crate::contract::{execute, instantiate}; + use crate::delegations::queries::query_node_delegations_paged; use crate::delegations::storage as delegations_storage; - use crate::delegations::transactions::try_delegate_to_mixnode; - use crate::families::transactions::{try_create_family, try_join_family}; - use crate::gateways::transactions::try_add_gateway; + use crate::delegations::storage::delegations; + use crate::delegations::transactions::try_delegate_to_node; use crate::interval::transactions::{ perform_pending_epoch_actions, perform_pending_interval_actions, try_begin_epoch_transition, }; use crate::interval::{pending_events, storage as interval_storage}; - use crate::mixnet_contract_settings::storage::{self as mixnet_params_storage}; + use crate::mixnet_contract_settings::queries::query_contract_settings_params; use crate::mixnet_contract_settings::storage::{ - minimum_gateway_pledge, minimum_mixnode_pledge, rewarding_denom, - rewarding_validator_address, + self as mixnet_params_storage, minimum_node_pledge, }; + use crate::mixnet_contract_settings::storage::{rewarding_denom, rewarding_validator_address}; + use crate::mixnodes::helpers::get_mixnode_details_by_id; use crate::mixnodes::storage as mixnodes_storage; use crate::mixnodes::storage::mixnode_bonds; - use crate::mixnodes::transactions::{try_add_mixnode, try_remove_mixnode}; + use crate::mixnodes::transactions::try_remove_mixnode; + use crate::nodes::helpers::{ + get_node_details_by_id, get_node_details_by_identity, must_get_node_bond_by_owner, + }; + use crate::nodes::storage as nymnodes_storage; + use crate::nodes::storage::helpers::RoleStorageBucket; + use crate::nodes::storage::rewarded_set::{ACTIVE_ROLES_BUCKET, ROLES, ROLES_METADATA}; + use crate::nodes::storage::{ + next_nymnode_id_counter, read_assigned_roles, save_assignment, swap_active_role_bucket, + }; + use crate::nodes::transactions::{try_add_nym_node, try_remove_nym_node}; + use crate::rewards::helpers::expensive_role_lookup; use crate::rewards::queries::{ query_pending_delegator_reward, query_pending_mixnode_operator_reward, }; use crate::rewards::storage as rewards_storage; - use crate::rewards::transactions::try_reward_mixnode; + use crate::rewards::storage::RewardingStorage; + use crate::rewards::transactions::try_reward_node; use crate::signing::storage as signing_storage; + use crate::support::helpers::ensure_no_existing_bond; use crate::support::tests; use crate::support::tests::fixtures::{ - good_gateway_pledge, good_mixnode_pledge, TEST_COIN_DENOM, + good_gateway_pledge, good_mixnode_pledge, good_node_plegge, TEST_COIN_DENOM, }; + use crate::support::tests::{legacy, test_helpers}; use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::testing::mock_env; use cosmwasm_std::testing::mock_info; use cosmwasm_std::testing::MockApi; use cosmwasm_std::testing::MockQuerier; - use cosmwasm_std::{coin, coins, Addr, BankMsg, CosmosMsg, Storage}; + use cosmwasm_std::{coin, coins, Addr, Api, BankMsg, CosmosMsg, Storage}; use cosmwasm_std::{Coin, Order}; use cosmwasm_std::{Decimal, Empty, MemoryStorage}; use cosmwasm_std::{Deps, OwnedDeps}; use cosmwasm_std::{DepsMut, MessageInfo}; use cosmwasm_std::{Env, Response, Timestamp, Uint128}; + use mixnet_contract_common::error::MixnetContractError; use mixnet_contract_common::events::{ may_find_attribute, MixnetEventType, DELEGATES_REWARD_KEY, OPERATOR_REWARD_KEY, }; - use mixnet_contract_common::families::FamilyHead; - use mixnet_contract_common::mixnode::{MixNodeRewarding, UnbondedMixnode}; + use mixnet_contract_common::helpers::compare_decimals; + use mixnet_contract_common::mixnode::{NodeRewarding, UnbondedMixnode}; + use mixnet_contract_common::nym_node::{RewardedSetMetadata, Role}; use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData}; - use mixnet_contract_common::reward_params::{Performance, RewardingParams}; + use mixnet_contract_common::reward_params::{ + NodeRewardingParameters, Performance, RewardedSetParams, 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::{ - construct_family_join_permit, Delegation, EpochEventId, EpochState, EpochStatus, Gateway, - GatewayBondingPayload, IdentityKey, IdentityKeyRef, InitialRewardingParams, InstantiateMsg, - Interval, MixId, MixNode, MixNodeBond, MixnodeBondingPayload, Percent, - RewardedSetNodeStatus, SignableGatewayBondingMsg, SignableMixNodeBondingMsg, + ContractStateParams, Delegation, EpochEventId, EpochState, EpochStatus, ExecuteMsg, + Gateway, GatewayBondingPayload, IdentityKey, InitialRewardingParams, InstantiateMsg, + Interval, MixNode, MixNodeBond, MixNodeDetails, MixnodeBondingPayload, NodeId, NymNode, + NymNodeBond, NymNodeBondingPayload, NymNodeDetails, OperatingCostRange, Percent, + ProfitMarginRange, RoleAssignment, SignableGatewayBondingMsg, SignableMixNodeBondingMsg, + SignableNymNodeBondingMsg, }; use nym_contracts_common::signing::{ ContractMessageContent, MessageSignature, SignableMessage, SigningAlgorithm, SigningPurpose, }; use nym_crypto::asymmetric::identity; use nym_crypto::asymmetric::identity::KeyPair; + use rand::distributions::WeightedIndex; + use rand::prelude::*; use rand_chacha::rand_core::{CryptoRng, RngCore, SeedableRng}; use rand_chacha::ChaCha20Rng; use serde::Serialize; + use std::collections::HashMap; + use std::fmt::Debug; + use std::str::FromStr; use std::time::Duration; + pub(crate) trait ExtractBankMsg { + fn unwrap_bank_msg(self) -> Option; + } + + impl ExtractBankMsg for Response { + fn unwrap_bank_msg(self) -> Option { + for msg in self.messages { + match msg.msg { + CosmosMsg::Bank(bank_msg) => return Some(bank_msg), + _ => continue, + } + } + + None + } + } + + #[allow(clippy::enum_variant_names)] + pub enum NodeQueryType { + ById(NodeId), + ByIdentity(IdentityKey), + ByOwner(Addr), + } + + impl From for NodeQueryType { + fn from(value: NodeId) -> Self { + NodeQueryType::ById(value) + } + } + + impl From for NodeQueryType { + fn from(value: IdentityKey) -> Self { + NodeQueryType::ByIdentity(value) + } + } + impl From for NodeQueryType { + fn from(value: Addr) -> Self { + NodeQueryType::ByOwner(value) + } + } + + #[track_caller] pub fn assert_eq_with_leeway(a: Uint128, b: Uint128, leeway: Uint128) { if a > b { - assert!(a - b <= leeway) + assert!(a - b <= leeway, "{} != {}", a, b) } else { - assert!(b - a <= leeway) + assert!(b - a <= leeway, "{} != {}", a, b) } } + #[track_caller] pub fn assert_decimals(a: Decimal, b: Decimal) { let epsilon = Decimal::from_ratio(1u128, 100_000_000u128); if a > b { @@ -100,6 +167,7 @@ pub mod test_helpers { pub owner: MessageInfo, } + #[allow(unused)] impl TestSetup { pub fn new() -> Self { let deps = init_contract(); @@ -120,6 +188,141 @@ pub mod test_helpers { } } + pub fn new_complex() -> Self { + let mut test = TestSetup::new(); + + let mut nodes = Vec::new(); + + let problematic_delegator = "n1foomp"; + let problematic_delegator_twin = "n1bar"; + let problematic_delegator_alt_twin = "n1whatever"; + + let choices = [true, false]; + + // every epoch there's a 2% chance of somebody bonding a node + let bonding_weights = [2, 98]; + + // and 15% of making a delegation + let delegation_weights = [15, 85]; + + // and 1% of making a VESTED delegation + let vested_delegation_weights = [1, 99]; + + let bonding_dist = WeightedIndex::new(bonding_weights).unwrap(); + let delegation_dist = WeightedIndex::new(delegation_weights).unwrap(); + let vested_delegation_dist = WeightedIndex::new(vested_delegation_weights).unwrap(); + + // make sure we have at least a single node at the beginning + let owner = test.random_address(); + let mix_id = test.add_legacy_mixnode(&owner, None); + nodes.push(mix_id); + + // create a bunch of nodes and delegations and progress through epochs + for epoch_id in 0..1000 { + // go through 1000 epochs + + let owner = test.random_address(); + let min_stake = 100_000_000; + // u32 has max value of 4B, which is ~4k nym tokens, which is a realistic amount somebody could bond/delegate + let variance = test.rng.next_u32(); + let stake = Uint128::new(min_stake as u128 + variance as u128); + + if choices[bonding_dist.sample(&mut test.rng)] { + // bond + let mix_id = test.add_legacy_mixnode(&owner, Some(stake)); + nodes.push(mix_id); + } + + if choices[delegation_dist.sample(&mut test.rng)] { + // uniformly choose a random node to delegate to + let node = nodes.choose(&mut test.rng).unwrap(); + test.add_immediate_delegation(&owner, stake, *node) + } + + if choices[vested_delegation_dist.sample(&mut test.rng)] { + // uniformly choose a random node to make vested delegation to + let node = nodes.choose(&mut test.rng).unwrap(); + test.add_immediate_delegation_with_legal_proxy(&owner, stake, *node) + } + + // make sure we cover our edge case of somebody having both liquid and vested delegation towards the same node + if epoch_id == 123 { + test.add_immediate_delegation(problematic_delegator, stake, 4); + test.add_immediate_delegation(problematic_delegator_twin, stake, 4); + } + + if epoch_id == 666 { + test.add_immediate_delegation_with_legal_proxy(problematic_delegator, stake, 4); + test.add_immediate_delegation_with_legal_proxy( + problematic_delegator_twin, + stake, + 4, + ); + } + + if epoch_id == 234 { + test.add_immediate_delegation(problematic_delegator_alt_twin, stake, 4); + } + + if epoch_id == 420 { + test.add_immediate_delegation_with_legal_proxy( + problematic_delegator_alt_twin, + stake, + 4, + ); + } + + test.skip_to_next_epoch_end(); + // it doesn't matter that they're on the same layer here, we just need to make sure they're rewarded + test.force_assign_rewarded_set(vec![RoleAssignment { + role: Role::Layer1, + nodes: nodes.clone(), + }]); + test.start_epoch_transition(); + + // reward each node + for node in &nodes { + let performance = test.rng.next_u64() % 100; + let work_factor = test.active_node_work(); + test.reward_with_distribution( + *node, + NodeRewardingParameters { + performance: Performance::from_percentage_value(performance).unwrap(), + work_factor, + }, + ); + } + + test.set_epoch_in_progress_state(); + } + + test + } + + #[track_caller] + pub fn ensure_delegation_sync(&self, mix_id: NodeId) { + let mix_info = self.mix_rewarding(mix_id); + let epsilon = "0.001".parse().unwrap(); + + let subtotal: Decimal = delegations() + .prefix(mix_id) + .range(self.deps().storage, None, None, Order::Ascending) + .filter_map(|d| { + d.map(|(_, del)| { + let pending_rewards = mix_info.determine_delegation_reward(&del).unwrap(); + pending_rewards + del.dec_amount().unwrap() + }) + .ok() + }) + .sum(); + + compare_decimals(mix_info.delegates, subtotal, Some(epsilon)) + } + + pub fn random_address(&mut self) -> String { + format!("n1foomp{}", self.rng.next_u64()) + } + pub fn deps(&self) -> Deps<'_> { self.deps.as_ref() } @@ -132,6 +335,126 @@ pub mod test_helpers { self.env.clone() } + pub fn execute( + &mut self, + info: MessageInfo, + msg: ExecuteMsg, + ) -> Result { + let env = self.env.clone(); + execute(self.deps_mut(), env, info, msg) + } + + #[allow(unused)] + pub fn execute_no_funds( + &mut self, + sender: impl Into, + msg: ExecuteMsg, + ) -> Result { + self.execute(self.mock_info(sender), msg) + } + + pub fn execute_fn( + &mut self, + exec_fn: F, + info: MessageInfo, + ) -> Result + where + F: FnOnce(DepsMut<'_>, Env, MessageInfo) -> Result, + { + let env = self.env().clone(); + exec_fn(self.deps_mut(), env, info) + } + + #[allow(unused)] + pub fn execute_fn_no_funds( + &mut self, + exec_fn: F, + sender: impl Into, + ) -> Result + where + F: FnOnce(DepsMut<'_>, Env, MessageInfo) -> Result, + { + let info = self.mock_info(sender); + self.execute_fn(exec_fn, info) + } + + #[track_caller] + pub fn assert_simple_execution(&mut self, exec_fn: F, info: MessageInfo) -> Response + where + F: FnOnce(DepsMut<'_>, Env, MessageInfo) -> Result, + { + let caller = std::panic::Location::caller(); + self.execute_fn(exec_fn, info) + .unwrap_or_else(|err| panic!("{caller} failed with: '{err}' ({err:?})")) + } + + #[allow(unused)] + #[track_caller] + pub fn assert_simple_execution_no_funds( + &mut self, + exec_fn: F, + sender: impl Into, + ) -> Response + where + F: FnOnce(DepsMut<'_>, Env, MessageInfo) -> Result, + { + let caller = std::panic::Location::caller(); + self.execute_fn_no_funds(exec_fn, sender) + .unwrap_or_else(|err| panic!("{caller} failed with: '{err}' ({err:?})")) + } + + pub fn update_profit_margin_range(&mut self, range: ProfitMarginRange) { + let current = query_contract_settings_params(self.deps()).unwrap(); + + self.execute( + self.owner(), + ExecuteMsg::UpdateContractStateParams { + updated_parameters: ContractStateParams { + profit_margin: range, + ..current + }, + }, + ) + .unwrap(); + } + + pub fn update_operating_cost_range(&mut self, range: OperatingCostRange) { + let current = query_contract_settings_params(self.deps()).unwrap(); + + self.execute( + self.owner(), + ExecuteMsg::UpdateContractStateParams { + updated_parameters: ContractStateParams { + interval_operating_cost: range, + ..current + }, + }, + ) + .unwrap(); + } + + pub fn get_node_id(&self, query_type: impl Into) -> NodeId { + match query_type.into() { + NodeQueryType::ById(id) => id, + NodeQueryType::ByIdentity(identity) => { + get_node_details_by_identity(&self.deps.storage, identity) + .unwrap() + .unwrap() + .node_id() + } + NodeQueryType::ByOwner(owner) => { + must_get_node_bond_by_owner(&self.deps.storage, &owner) + .unwrap() + .node_id + } + } + } + + #[allow(unused)] + pub fn mock_info(&self, sender: impl Into) -> MessageInfo { + mock_info(&sender.into(), &[]) + } + pub fn rewarding_validator(&self) -> MessageInfo { self.rewarding_validator.clone() } @@ -146,6 +469,20 @@ pub mod test_helpers { self.owner.clone() } + pub fn vesting_contract(&self) -> Addr { + mixnet_params_storage::CONTRACT_STATE + .load(self.deps().storage) + .unwrap() + .vesting_contract_address + } + + pub fn all_mixnodes(&self) -> Vec { + mixnode_bonds() + .range(self.deps().storage, None, None, Order::Ascending) + .filter_map(|m| m.map(|(_, node)| node.mix_id).ok()) + .collect::>() + } + pub fn coin(&self, amount: u128) -> Coin { coin(amount, rewarding_denom(self.deps().storage).unwrap()) } @@ -158,158 +495,379 @@ pub mod test_helpers { interval_storage::current_interval(self.deps().storage).unwrap() } - pub fn rewarded_set(&self) -> Vec<(MixId, RewardedSetNodeStatus)> { - interval_storage::REWARDED_SET - .range(self.deps().storage, None, None, Order::Ascending) - .map(|res| res.unwrap()) - .collect::>() + pub fn current_epoch_state(&self) -> EpochState { + interval_storage::current_epoch_status(self.deps().storage) + .unwrap() + .state } - pub fn generate_family_join_permit( - &mut self, - family_owner_keys: &identity::KeyPair, - member_node: IdentityKeyRef, - ) -> MessageSignature { - let identity = family_owner_keys.public_key().to_base58_string(); + pub fn active_roles_bucket(&self) -> RoleStorageBucket { + ACTIVE_ROLES_BUCKET.load(self.deps().storage).unwrap() + } - let head_mixnode = mixnodes_storage::mixnode_bonds() - .idx - .identity_key - .item(self.deps().storage, identity.clone()) + #[allow(unused)] + pub fn active_roles_metadata(&self) -> RewardedSetMetadata { + let bucket = self.active_roles_bucket().other(); + ROLES_METADATA + .load(self.deps().storage, bucket as u8) .unwrap() - .map(|record| record.1) - .unwrap(); + } - let family_head = FamilyHead::new(&identity); - let owner = head_mixnode.owner; + pub fn inactive_roles_metadata(&self) -> RewardedSetMetadata { + let bucket = self.active_roles_bucket().other(); + ROLES_METADATA + .load(self.deps().storage, bucket as u8) + .unwrap() + } - let nonce = signing_storage::get_signing_nonce(self.deps().storage, owner).unwrap(); + #[allow(unused)] + pub fn active_roles(&self, role: Role) -> Vec { + let bucket = self.active_roles_bucket().other(); + ROLES + .load(self.deps().storage, (bucket as u8, role)) + .unwrap() + } - let msg = construct_family_join_permit(nonce, family_head, member_node.to_owned()); + pub fn inactive_roles(&self, role: Role) -> Vec { + let bucket = self.active_roles_bucket().other(); + ROLES + .load(self.deps().storage, (bucket as u8, role)) + .unwrap() + } - let sig_bytes = family_owner_keys - .private_key() - .sign(msg.to_plaintext().unwrap()) - .to_bytes(); - MessageSignature::from(sig_bytes.as_ref()) + pub fn max_role_count(&self, role: Role) -> u32 { + RewardingStorage::load() + .global_rewarding_params + .load(self.deps().storage) + .unwrap() + .rewarded_set + .maximum_role_count(role) } - #[allow(unused)] - pub fn join_family( + pub fn set_pending_pledge_change( &mut self, - member: &str, - member_keys: &identity::KeyPair, - head_keys: &identity::KeyPair, + mix_id: NodeId, + event_id: Option, ) { - let member_identity = member_keys.public_key().to_base58_string(); - let head_identity = head_keys.public_key().to_base58_string(); + let mut changes = mixnodes_storage::PENDING_MIXNODE_CHANGES + .load(self.deps().storage, mix_id) + .unwrap_or_default(); + changes.pledge_change = Some(event_id.unwrap_or(12345)); + + mixnodes_storage::PENDING_MIXNODE_CHANGES + .save(self.deps_mut().storage, mix_id, &changes) + .unwrap(); + } + + pub fn lowest_mix_layer(&mut self) -> Role { + let layer1 = read_assigned_roles(&self.deps.storage, Role::Layer1).unwrap(); + let layer2 = read_assigned_roles(&self.deps.storage, Role::Layer2).unwrap(); + let layer3 = read_assigned_roles(&self.deps.storage, Role::Layer3).unwrap(); + let l1 = layer1.len(); + let l2 = layer2.len(); + let l3 = layer3.len(); + + if l1 <= l2 && l1 <= l3 { + Role::Layer1 + } else if l2 <= l3 && l2 <= l1 { + Role::Layer2 + } else { + Role::Layer3 + } + } + + pub fn immediately_assign_lowest_mix_layer(&mut self, node_id: NodeId) -> Role { + let layer = self.lowest_mix_layer(); + self.immediately_add_to_role(node_id, layer); + layer + } + + pub fn immediately_add_to_role(&mut self, node_id: NodeId, role: Role) { + let active_bucket = ACTIVE_ROLES_BUCKET.load(&self.deps.storage).unwrap(); + let mut current = read_assigned_roles(self.deps().storage, role).unwrap(); + current.push(node_id); + ROLES + .save( + &mut self.deps.storage, + (active_bucket as u8, role), + ¤t, + ) + .unwrap(); + } + + pub fn immediately_assign_standby_role(&mut self, node_id: NodeId) { + self.immediately_add_to_role(node_id, Role::Standby) + } + + pub fn immediately_assign_exit_gateway_role(&mut self, node_id: NodeId) { + self.immediately_add_to_role(node_id, Role::ExitGateway) + } + + pub fn immediately_assign_entry_gateway_role(&mut self, node_id: NodeId) { + self.immediately_add_to_role(node_id, Role::EntryGateway) + } + + pub fn add_rewarded_set_nymnode( + &mut self, + owner: &str, + stake: Option, + ) -> (NymNodeBond, MessageSignature, KeyPair) { + let res = self.add_nymnode(owner, stake); + let id = res.0.node_id; + self.immediately_assign_lowest_mix_layer(id); + + res + } + + pub fn add_rewarded_set_nymnode_id( + &mut self, + owner: &str, + stake: Option, + ) -> NodeId { + self.add_rewarded_set_nymnode(owner, stake).0.node_id + } + + pub fn add_nymnode( + &mut self, + owner: &str, + stake: Option, + ) -> (NymNodeBond, MessageSignature, KeyPair) { + let stake = self.make_node_pledge(stake); + let (node, owner_signature, keypair) = + self.node_with_signature(owner, Some(stake.clone())); - let join_permit = self.generate_family_join_permit(head_keys, &member_identity); - let family_head = FamilyHead::new(head_identity); + let info = mock_info(owner, stake.as_ref()); + let env = self.env(); - try_join_family( + try_add_nym_node( self.deps_mut(), - mock_info(member, &[]), - join_permit, - family_head, + env, + info.clone(), + node, + tests::fixtures::node_cost_params_fixture(), + owner_signature.clone(), ) .unwrap(); - } - #[allow(dead_code)] - pub fn create_dummy_mixnode_with_new_family( - &mut self, - head: &str, - label: &str, - ) -> (MixId, identity::KeyPair) { - let (mix_id, keys) = self.add_dummy_mixnode_with_keypair(head, None); + let bond = must_get_node_bond_by_owner(&self.deps.storage, &info.sender).unwrap(); - try_create_family(self.deps_mut(), mock_info(head, &[]), label.to_string()).unwrap(); - (mix_id, keys) + (bond, owner_signature, keypair) } - pub fn set_pending_pledge_change(&mut self, mix_id: MixId, event_id: Option) { - let mut changes = mixnodes_storage::PENDING_MIXNODE_CHANGES - .load(self.deps().storage, mix_id) - .unwrap_or_default(); - changes.pledge_change = Some(event_id.unwrap_or(12345)); + pub fn add_dummy_nymnode(&mut self, owner: &str, stake: Option) -> NodeId { + self.add_nymnode(owner, stake).0.node_id + } - mixnodes_storage::PENDING_MIXNODE_CHANGES - .save(self.deps_mut().storage, mix_id, &changes) - .unwrap(); + #[allow(unused)] + pub fn add_dummy_nym_node_with_keypair( + &mut self, + owner: &str, + stake: Option, + ) -> (NodeId, identity::KeyPair) { + let (bond, _, keypair) = self.add_nymnode(owner, stake); + (bond.node_id, keypair) } - pub fn add_dummy_mixnode(&mut self, owner: &str, stake: Option) -> MixId { + #[track_caller] + pub fn add_legacy_mixnode(&mut self, owner: &str, stake: Option) -> NodeId { let stake = self.make_mix_pledge(stake); - let (mixnode, owner_signature, _) = - self.mixnode_with_signature(owner, Some(stake.clone())); + let (mixnode, _, _) = self.mixnode_with_signature(owner, Some(stake.clone())); let info = mock_info(owner, stake.as_ref()); - let current_id_counter = mixnodes_storage::MIXNODE_ID_COUNTER - .may_load(self.deps().storage) - .unwrap() - .unwrap_or_default(); - let env = self.env(); - try_add_mixnode( - self.deps_mut(), + ensure_no_existing_bond(&info.sender, &self.deps.storage).unwrap(); + signing_storage::increment_signing_nonce(&mut self.deps.storage, info.sender.clone()) + .unwrap(); + legacy::save_new_mixnode( + &mut self.deps.storage, env, - info, mixnode, - tests::fixtures::mix_node_cost_params_fixture(), - owner_signature, + tests::fixtures::node_cost_params_fixture(), + info.sender, + info.funds[0].clone(), + ) + .unwrap() + } + + pub fn add_rewarded_mixing_node(&mut self, owner: &str, stake: Option) -> NodeId { + let node_id = self.add_dummy_nymnode(owner, stake); + self.immediately_assign_lowest_mix_layer(node_id); + node_id + } + + pub fn add_rewarded_entry_gateway_node( + &mut self, + owner: &str, + stake: Option, + ) -> NodeId { + let node_id = self.add_dummy_nymnode(owner, stake); + self.immediately_assign_entry_gateway_role(node_id); + node_id + } + + pub fn add_rewarded_exit_gateway_node( + &mut self, + owner: &str, + stake: Option, + ) -> NodeId { + let node_id = self.add_dummy_nymnode(owner, stake); + self.immediately_assign_exit_gateway_role(node_id); + node_id + } + + pub fn add_standby_node(&mut self, owner: &str, stake: Option) -> NodeId { + let node_id = self.add_dummy_nymnode(owner, stake); + self.immediately_assign_standby_role(node_id); + node_id + } + + pub fn add_legacy_mixnode_with_proxy_and_keypair( + &mut self, + owner: &str, + stake: Option, + ) -> (NodeId, identity::KeyPair) { + let pledge = self.make_mix_pledge(stake).pop().unwrap(); + + let proxy = self.vesting_contract(); + + let keypair = identity::KeyPair::new(&mut self.rng); + let identity_key = keypair.public_key().to_base58_string(); + let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng); + + let mixnode = MixNode { + identity_key, + sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), + ..tests::fixtures::mix_node_fixture() + }; + + let height = self.env.block.height; + let storage = self.deps_mut().storage; + + // manually unroll `save_new_mixnode` to allow for proxy usage + let mix_id = next_nymnode_id_counter(storage).unwrap(); + + let current_epoch = interval_storage::current_interval(storage) + .unwrap() + .current_epoch_absolute_id(); + + let mixnode_rewarding = NodeRewarding::initialise_new( + tests::fixtures::node_cost_params_fixture(), + &pledge, + current_epoch, ) .unwrap(); + let mixnode_bond = MixNodeBond { + mix_id, + owner: Addr::unchecked(owner), + original_pledge: pledge, + mix_node: mixnode, + proxy: Some(proxy), + bonding_height: height, + is_unbonding: false, + }; + + mixnode_bonds() + .save(storage, mix_id, &mixnode_bond) + .unwrap(); + rewards_storage::MIXNODE_REWARDING + .save(storage, mix_id, &mixnode_rewarding) + .unwrap(); + + (mix_id, keypair) + } - // newly added mixnode gets assigned the current counter + 1 - current_id_counter + 1 + pub fn add_legacy_mixnode_with_legal_proxy( + &mut self, + owner: &str, + stake: Option, + ) -> NodeId { + self.add_legacy_mixnode_with_proxy_and_keypair(owner, stake) + .0 } - pub fn add_dummy_gateway(&mut self, sender: &str, stake: Option) -> IdentityKey { + pub fn add_rewarded_legacy_mixnode( + &mut self, + owner: &str, + stake: Option, + ) -> NodeId { + let node_id = self.add_legacy_mixnode(owner, stake); + self.immediately_assign_lowest_mix_layer(node_id); + + node_id + } + + pub fn add_legacy_gateway( + &mut self, + sender: &str, + stake: Option, + ) -> (IdentityKey, NodeId) { let stake = self.make_gateway_pledge(stake); - let (gateway, owner_signature) = - self.gateway_with_signature(sender, Some(stake.clone())); + let (gateway, _) = self.gateway_with_signature(sender, Some(stake.clone())); + let env = self.env(); let info = mock_info(sender, &stake); - let key = gateway.identity_key.clone(); + + legacy::save_new_gateway( + &mut self.deps.storage, + env, + gateway, + info.sender, + info.funds[0].clone(), + ) + .unwrap() + } + + pub fn save_legacy_gateway(&mut self, gateway: Gateway, info: &MessageInfo) { let env = self.env(); - try_add_gateway(self.deps_mut(), env, info, gateway, owner_signature).unwrap(); - key + + legacy::save_new_gateway( + &mut self.deps.storage, + env, + gateway, + info.sender.clone(), + info.funds[0].clone(), + ) + .unwrap(); } - pub fn add_dummy_mixnodes(&mut self, n: usize) { + pub fn add_legacy_mixnodes(&mut self, n: usize) { for i in 0..n { - self.add_dummy_mixnode(&format!("owner{i}"), None); + self.add_legacy_mixnode(&format!("owner{i}"), None); } } pub fn add_dummy_gateways(&mut self, n: usize) { for i in 0..n { - self.add_dummy_gateway(&format!("owner{i}"), None); + self.add_legacy_gateway(&format!("owner{i}"), None); } } - pub fn make_mix_pledge(&self, stake: Option) -> Vec { + pub fn make_node_pledge(&self, stake: Option) -> Vec { let stake = match stake { Some(amount) => { let denom = rewarding_denom(self.deps().storage).unwrap(); Coin { denom, amount } } - None => minimum_mixnode_pledge(self.deps.as_ref().storage).unwrap(), + None => minimum_node_pledge(self.deps.as_ref().storage).unwrap(), }; vec![stake] } + pub fn make_mix_pledge(&self, stake: Option) -> Vec { + self.make_node_pledge(stake) + } + pub fn make_gateway_pledge(&self, stake: Option) -> Vec { - let stake = match stake { - Some(amount) => { - let denom = rewarding_denom(self.deps().storage).unwrap(); - Coin { denom, amount } - } - None => minimum_gateway_pledge(self.deps.as_ref().storage).unwrap(), - }; - vec![stake] + self.make_node_pledge(stake) + } + + pub fn mixnode_by_id(&self, node_id: NodeId) -> Option { + get_mixnode_details_by_id(self.deps().storage, node_id).unwrap() + } + + pub fn nymnode_by_id(&self, node_id: NodeId) -> Option { + get_node_details_by_id(self.deps().storage, node_id).unwrap() } pub fn mixnode_bonding_signature( @@ -324,46 +882,52 @@ pub mod test_helpers { ed25519_sign_message(msg, key) } - pub fn add_dummy_mixnode_with_keypair( + pub fn add_legacy_mixnode_with_keypair( &mut self, owner: &str, stake: Option, - ) -> (MixId, identity::KeyPair) { + ) -> (NodeId, identity::KeyPair) { let stake = self.make_mix_pledge(stake); + let (mixnode, _, keypair) = self.mixnode_with_signature(owner, Some(stake.clone())); + + let info = mock_info(owner, stake.as_ref()); + let env = self.env(); + + ensure_no_existing_bond(&info.sender, &self.deps.storage).unwrap(); + signing_storage::increment_signing_nonce(&mut self.deps.storage, info.sender.clone()) + .unwrap(); + let node_id = legacy::save_new_mixnode( + &mut self.deps.storage, + env, + mixnode, + tests::fixtures::node_cost_params_fixture(), + info.sender, + info.funds[0].clone(), + ) + .unwrap(); + + (node_id, keypair) + } + + pub fn node_with_signature( + &mut self, + sender: &str, + stake: Option>, + ) -> (NymNode, MessageSignature, KeyPair) { + let stake = stake.unwrap_or(good_node_plegge()); let keypair = identity::KeyPair::new(&mut self.rng); let identity_key = keypair.public_key().to_base58_string(); - let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut self.rng); - let mixnode = MixNode { + let node = NymNode { + host: "1.2.3.4".to_string(), + custom_http_port: None, identity_key, - sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), - ..tests::fixtures::mix_node_fixture() }; - - let msg = - mixnode_bonding_sign_payload(self.deps(), owner, mixnode.clone(), stake.clone()); + let msg = nymnode_bonding_sign_payload(self.deps(), sender, node.clone(), stake); let owner_signature = ed25519_sign_message(msg, keypair.private_key()); - let info = mock_info(owner, &stake); - let current_id_counter = mixnodes_storage::MIXNODE_ID_COUNTER - .may_load(self.deps().storage) - .unwrap() - .unwrap_or_default(); - - let env = self.env(); - try_add_mixnode( - self.deps_mut(), - env, - info, - mixnode, - tests::fixtures::mix_node_cost_params_fixture(), - owner_signature, - ) - .unwrap(); - - // newly added mixnode gets assigned the current counter + 1 - (current_id_counter + 1, keypair) + (node, owner_signature, keypair) } pub fn mixnode_with_signature( @@ -390,7 +954,7 @@ pub mod test_helpers { pub fn gateway_with_signature( &mut self, - sender: &str, + sender: impl Into, stake: Option>, ) -> (Gateway, MessageSignature) { let stake = stake.unwrap_or(good_gateway_pledge()); @@ -405,13 +969,19 @@ pub mod test_helpers { ..tests::fixtures::gateway_fixture() }; - let msg = gateway_bonding_sign_payload(self.deps(), sender, gateway.clone(), stake); + let msg = gateway_bonding_sign_payload( + self.deps(), + sender.into().as_str(), + gateway.clone(), + stake, + ); let owner_signature = ed25519_sign_message(msg, keypair.private_key()); (gateway, owner_signature) } - pub fn start_unbonding_mixnode(&mut self, mix_id: MixId) { + #[track_caller] + pub fn start_unbonding_mixnode(&mut self, mix_id: NodeId) { let bond_details = mixnodes_storage::mixnode_bonds() .load(self.deps().storage, mix_id) .unwrap(); @@ -425,17 +995,46 @@ pub mod test_helpers { .unwrap(); } - pub fn immediately_unbond_mixnode(&mut self, mix_id: MixId) { + #[track_caller] + pub fn start_unbonding_nymnode(&mut self, node_id: NodeId) { + let bond_details = nymnodes_storage::nym_nodes() + .load(self.deps().storage, node_id) + .unwrap(); + + let env = self.env(); + try_remove_nym_node( + self.deps_mut(), + env, + mock_info(bond_details.owner.as_str(), &[]), + ) + .unwrap(); + } + + #[track_caller] + pub fn immediately_unbond_node(&mut self, node: impl Into) { + let node_id = self.get_node_id(node); + let env = self.env(); + pending_events::unbond_nym_node(self.deps_mut(), &env, env.block.height, node_id) + .unwrap(); + } + + pub fn immediately_unbond_mixnode(&mut self, mix_id: NodeId) { let env = self.env(); pending_events::unbond_mixnode(self.deps_mut(), &env, env.block.height, mix_id) .unwrap(); } + pub fn immediately_unbond_nymnode(&mut self, node_id: NodeId) { + let env = self.env(); + pending_events::unbond_nym_node(self.deps_mut(), &env, env.block.height, node_id) + .unwrap(); + } + pub fn add_immediate_delegation( &mut self, delegator: &str, amount: impl Into, - target: MixId, + target: NodeId, ) { let denom = rewarding_denom(self.deps().storage).unwrap(); let amount = Coin { @@ -454,12 +1053,61 @@ pub mod test_helpers { .unwrap(); } + pub fn add_immediate_delegation_with_legal_proxy( + &mut self, + delegator: &str, + amount: impl Into, + target: NodeId, + ) { + let denom = rewarding_denom(self.deps().storage).unwrap(); + let amount = Coin { + denom, + amount: amount.into(), + }; + let proxy = self.vesting_contract(); + + let owner = self.deps.api.addr_validate(delegator).unwrap(); + let storage_key = Delegation::generate_storage_key(target, &owner, Some(&proxy)); + + let mut mix_rewarding = self.mix_rewarding(target); + + let mut stored_delegation_amount = amount; + + if let Some(existing_delegation) = delegations_storage::delegations() + .may_load(&self.deps.storage, storage_key.clone()) + .unwrap() + { + let og_with_reward = mix_rewarding.undelegate(&existing_delegation).unwrap(); + stored_delegation_amount.amount += og_with_reward.amount; + } + + mix_rewarding + .add_base_delegation(stored_delegation_amount.amount) + .unwrap(); + + let delegation = Delegation { + owner, + node_id: target, + cumulative_reward_ratio: mix_rewarding.total_unit_reward, + amount: stored_delegation_amount, + height: self.env.block.height, + proxy: Some(proxy), + }; + + delegations_storage::delegations() + .save(&mut self.deps.storage, storage_key, &delegation) + .unwrap(); + rewards_storage::MIXNODE_REWARDING + .save(&mut self.deps.storage, target, &mix_rewarding) + .unwrap(); + } + #[allow(unused)] pub fn add_delegation( &mut self, delegator: &str, amount: impl Into, - target: MixId, + target: NodeId, ) { let denom = rewarding_denom(self.deps().storage).unwrap(); let amount = Coin { @@ -470,7 +1118,7 @@ pub mod test_helpers { delegate(self.deps_mut(), env, delegator, vec![amount], target) } - pub fn remove_immediate_delegation(&mut self, delegator: &str, target: MixId) { + pub fn remove_immediate_delegation(&mut self, delegator: &str, target: NodeId) { let height = self.env.block.height; pending_events::undelegate(self.deps_mut(), height, Addr::unchecked(delegator), target) .unwrap(); @@ -482,6 +1130,12 @@ pub mod test_helpers { try_begin_epoch_transition(self.deps_mut(), env, sender).unwrap(); } + pub fn epoch_state(&self) -> EpochState { + interval_storage::current_epoch_status(self.deps().storage) + .unwrap() + .state + } + pub fn set_epoch_in_progress_state(&mut self) { let being_advanced_by = self.rewarding_validator.sender.clone(); interval_storage::save_current_epoch_status( @@ -506,20 +1160,22 @@ pub mod test_helpers { .unwrap(); } - pub fn set_epoch_advancement_state(&mut self) { + pub fn set_epoch_role_assignment_state(&mut self) { let being_advanced_by = self.rewarding_validator.sender.clone(); interval_storage::save_current_epoch_status( self.deps_mut().storage, &EpochStatus { being_advanced_by, - state: EpochState::AdvancingEpoch, + state: EpochState::RoleAssignment { + next: Role::first(), + }, }, ) .unwrap(); } #[allow(unused)] - pub fn pending_operator_reward(&mut self, mix: MixId) -> Decimal { + pub fn pending_operator_reward(&mut self, mix: NodeId) -> Decimal { query_pending_mixnode_operator_reward(self.deps(), mix) .unwrap() .amount_earned_detailed @@ -527,7 +1183,7 @@ pub mod test_helpers { } #[allow(unused)] - pub fn pending_delegator_reward(&mut self, delegator: &str, target: MixId) -> Decimal { + pub fn pending_delegator_reward(&mut self, delegator: &str, target: NodeId) -> Decimal { query_pending_delegator_reward(self.deps(), delegator.into(), target, None) .unwrap() .amount_earned_detailed @@ -583,16 +1239,55 @@ pub mod test_helpers { self.set_epoch_in_progress_state(); } - pub fn force_change_rewarded_set(&mut self, nodes: Vec) { - let active_set_size = rewards_storage::REWARDING_PARAMS - .load(self.deps().storage) - .unwrap() - .active_set_size; - interval_storage::update_rewarded_set(self.deps_mut().storage, active_set_size, nodes) - .unwrap(); + pub fn reset_role_assignment(&mut self) { + let active_bucket = ACTIVE_ROLES_BUCKET.load(&self.deps.storage).unwrap(); + + for role in [ + Role::EntryGateway, + Role::ExitGateway, + Role::Layer1, + Role::Layer2, + Role::Layer3, + Role::Standby, + ] { + ROLES + .save(&mut self.deps.storage, (active_bucket as u8, role), &vec![]) + .unwrap(); + } + } + + pub fn force_assign_rewarded_set(&mut self, assignment: Vec) { + self.reset_role_assignment(); + + // we cheat a bit to write to the 'active' bucket instead + swap_active_role_bucket(self.deps_mut().storage).unwrap(); + for role_assignment in assignment { + let mut sorted_assignment = role_assignment.clone(); + sorted_assignment.nodes.sort(); + + save_assignment(self.deps_mut().storage, sorted_assignment).unwrap(); + } + swap_active_role_bucket(self.deps_mut().storage).unwrap(); } - pub fn instantiate_simulator(&self, node: MixId) -> Simulator { + // note: this does NOT assign gateway role + pub fn force_change_mix_rewarded_set(&mut self, nodes: Vec) { + let mut roles = HashMap::new(); + for node in nodes { + let layer = self.lowest_mix_layer(); + let assigned = roles.entry(layer).or_insert(Vec::new()); + assigned.push(node) + } + + let roles = roles + .into_iter() + .map(|(role, nodes)| RoleAssignment { role, nodes }) + .collect(); + + self.force_assign_rewarded_set(roles) + } + + pub fn instantiate_simulator(&self, node: NodeId) -> Simulator { simulator_from_single_node_state(self.deps(), node) } @@ -615,39 +1310,152 @@ pub mod test_helpers { .collect::>() } + pub fn active_node_work(&self) -> WorkFactor { + self.rewarding_params().active_node_work() + } + + #[allow(dead_code)] + pub fn standby_node_work(&self) -> WorkFactor { + self.rewarding_params().standby_node_work() + } + + pub fn active_node_params(&self, performance: f32) -> NodeRewardingParameters { + NodeRewardingParameters { + performance: test_helpers::performance(performance), + work_factor: self.active_node_work(), + } + } + + #[allow(dead_code)] + pub fn standby_node_params(&self, performance: f32) -> NodeRewardingParameters { + NodeRewardingParameters { + performance: test_helpers::performance(performance), + work_factor: self.standby_node_work(), + } + } + + #[track_caller] + pub fn reward_with_distribution_ignore_state( + &mut self, + node_id: NodeId, + params: NodeRewardingParameters, + ) -> RewardDistribution { + self.reward_with_distribution_with_state_bypass( + node_id, + params.performance, + params.work_factor, + ) + } + + #[track_caller] pub fn reward_with_distribution_with_state_bypass( &mut self, - mix_id: MixId, + node_id: NodeId, performance: Performance, + work_factor: WorkFactor, ) -> RewardDistribution { let initial_status = interval_storage::current_epoch_status(self.deps().storage).unwrap(); self.start_epoch_transition(); - let res = self.reward_with_distribution(mix_id, performance); + let res = self.reward_with_distribution( + node_id, + NodeRewardingParameters::new(performance, work_factor), + ); interval_storage::save_current_epoch_status(self.deps_mut().storage, &initial_status) .unwrap(); res } + #[allow(dead_code)] + #[track_caller] + pub fn node_role(&self, node_id: NodeId) -> Role { + if read_assigned_roles(&self.deps.storage, Role::EntryGateway) + .unwrap() + .contains(&node_id) + { + Role::EntryGateway + } else if read_assigned_roles(&self.deps.storage, Role::ExitGateway) + .unwrap() + .contains(&node_id) + { + Role::ExitGateway + } else if read_assigned_roles(&self.deps.storage, Role::Layer1) + .unwrap() + .contains(&node_id) + { + Role::Layer1 + } else if read_assigned_roles(&self.deps.storage, Role::Layer2) + .unwrap() + .contains(&node_id) + { + Role::Layer2 + } else if read_assigned_roles(&self.deps.storage, Role::Layer3) + .unwrap() + .contains(&node_id) + { + Role::Layer3 + } else if read_assigned_roles(&self.deps.storage, Role::Standby) + .unwrap() + .contains(&node_id) + { + Role::Standby + } else { + let caller = std::panic::Location::caller(); + panic!("{caller}: no assigned roles") + } + } + + pub fn legacy_rewarding_params( + &self, + node_id: NodeId, + performance: f32, + ) -> NodeRewardingParameters { + let performance = test_helpers::performance(performance); + let work_factor = self.get_legacy_rewarding_node_work_factor(node_id); + NodeRewardingParameters { + performance, + work_factor, + } + } + + pub fn get_legacy_rewarding_node_work_factor(&self, node_id: NodeId) -> Decimal { + let global_rewarding_params = self.rewarding_params(); + let work_factor = + match expensive_role_lookup(self.deps.as_ref().storage, node_id).unwrap() { + None => Decimal::zero(), + Some(Role::Standby) => global_rewarding_params.standby_node_work(), + _ => global_rewarding_params.active_node_work(), + }; + work_factor + } + + #[track_caller] pub fn reward_with_distribution( &mut self, - mix_id: MixId, - performance: Performance, + node_id: NodeId, + rewarding_params: NodeRewardingParameters, ) -> RewardDistribution { let env = self.env(); let sender = self.rewarding_validator(); let res = - try_reward_mixnode(self.deps_mut(), env, sender, mix_id, performance).unwrap(); + try_reward_node(self.deps_mut(), env, sender, node_id, rewarding_params).unwrap(); + + if rewarding_params.is_zero() { + return RewardDistribution { + operator: Decimal::zero(), + delegates: Decimal::zero(), + }; + } let operator: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), + Some(MixnetEventType::NodeRewarding.to_string()), OPERATOR_REWARD_KEY, &res, ) .parse() .unwrap(); let delegates: Decimal = find_attribute( - Some(MixnetEventType::MixnodeRewarding.to_string()), + Some(MixnetEventType::NodeRewarding.to_string()), DELEGATES_REWARD_KEY, &res, ) @@ -662,7 +1470,7 @@ pub mod test_helpers { pub fn read_delegation( &mut self, - mix: MixId, + mix: NodeId, owner: &str, proxy: Option<&str>, ) -> Delegation { @@ -675,19 +1483,25 @@ pub mod test_helpers { .unwrap() } - pub fn mix_rewarding(&self, node: MixId) -> MixNodeRewarding { + pub fn mix_rewarding(&self, node: NodeId) -> NodeRewarding { rewards_storage::MIXNODE_REWARDING .load(self.deps().storage, node) .unwrap() } #[allow(unused)] - pub fn mix_bond(&self, mix_id: MixId) -> MixNodeBond { + pub fn mix_bond(&self, mix_id: NodeId) -> MixNodeBond { mixnode_bonds().load(self.deps().storage, mix_id).unwrap() } - pub fn delegation(&self, mix: MixId, owner: &str, proxy: &Option) -> Delegation { - read_delegation(self.deps().storage, mix, &Addr::unchecked(owner), proxy).unwrap() + #[track_caller] + pub fn delegation(&self, mix: NodeId, owner: &str, proxy: &Option) -> Delegation { + let caller = std::panic::Location::caller(); + + read_delegation(self.deps().storage, mix, &Addr::unchecked(owner), proxy) + .unwrap_or_else(|| { + panic!("{caller} failed with: delegation for {mix}/{owner} doesn't exist") + }) } } @@ -707,11 +1521,11 @@ pub mod test_helpers { } } - pub fn simulator_from_single_node_state(deps: Deps<'_>, node: MixId) -> Simulator { + pub fn simulator_from_single_node_state(deps: Deps<'_>, node: NodeId) -> Simulator { let mix_rewarding = rewards_storage::MIXNODE_REWARDING .load(deps.storage, node) .unwrap(); - let delegations = query_mixnode_delegations_paged(deps, node, None, None).unwrap(); + let delegations = query_node_delegations_paged(deps, node, None, None).unwrap(); if delegations.delegations.len() as u32 == constants::DELEGATION_PAGE_DEFAULT_RETRIEVAL_LIMIT { @@ -749,6 +1563,7 @@ pub mod test_helpers { None } + #[track_caller] pub fn find_attribute>( event_type: Option, attribute: &str, @@ -769,6 +1584,62 @@ pub mod test_helpers { panic!("did not find the attribute") } + pub(crate) trait FindAttribute { + fn attribute(&self, event_type: E, attribute: &str) -> String + where + E: Into>, + S: Into; + + fn any_attribute(&self, attribute: &str) -> String { + self.attribute::<_, String>(None, attribute) + } + + fn any_parsed_attribute(&self, attribute: &str) -> T + where + T: FromStr, + ::Err: Debug, + { + self.parsed_attribute::<_, String, T>(None, attribute) + } + + fn parsed_attribute(&self, event_type: E, attribute: &str) -> T + where + E: Into>, + S: Into, + T: FromStr, + ::Err: Debug; + + fn decimal(&self, event_type: E, attribute: &str) -> Decimal + where + E: Into>, + S: Into, + { + self.parsed_attribute(event_type, attribute) + } + } + + impl FindAttribute for Response { + fn attribute(&self, event_type: E, attribute: &str) -> String + where + E: Into>, + S: Into, + { + find_attribute(event_type.into(), attribute, self) + } + + fn parsed_attribute(&self, event_type: E, attribute: &str) -> T + where + E: Into>, + S: Into, + T: FromStr, + ::Err: Debug, + { + find_attribute(event_type.into(), attribute, self) + .parse() + .unwrap() + } + } + // using floats in tests is fine // (what it does is converting % value, like 12.34 into `Performance` (`Percent`) // which internally is represented by decimal `0.1234` @@ -793,56 +1664,7 @@ pub mod test_helpers { perform_pending_interval_actions(deps.branch(), &env, None).unwrap(); } - // pub fn mixnode_with_signature( - // mut rng: impl RngCore + CryptoRng, - // deps: Deps<'_>, - // sender: &str, - // stake: Option>, - // ) -> (MixNode, MessageSignature, KeyPair) { - // // hehe stupid workaround for bypassing the immutable borrow and removing duplicate code - // - // let stake = stake.unwrap_or(good_mixnode_pledge()); - // - // let keypair = identity::KeyPair::new(&mut rng); - // let identity_key = keypair.public_key().to_base58_string(); - // let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut rng); - // - // let mixnode = MixNode { - // identity_key, - // sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), - // ..tests::fixtures::mix_node_fixture() - // }; - // let msg = mixnode_bonding_sign_payload(deps, sender, None, mixnode.clone(), stake.clone()); - // let owner_signature = ed25519_sign_message(msg, keypair.private_key()); - // - // (mixnode, owner_signature, keypair) - // } - - // pub fn gateway_with_signature( - // mut rng: impl RngCore + CryptoRng, - // deps: Deps<'_>, - // sender: &str, - // stake: Option>, - // ) -> (Gateway, MessageSignature) { - // let stake = stake.unwrap_or(good_gateway_pledge()); - // - // let keypair = identity::KeyPair::new(&mut rng); - // let identity_key = keypair.public_key().to_base58_string(); - // let legit_sphinx_keys = nym_crypto::asymmetric::encryption::KeyPair::new(&mut rng); - // - // let gateway = Gateway { - // identity_key, - // sphinx_key: legit_sphinx_keys.public_key().to_base58_string(), - // ..tests::fixtures::gateway_fixture() - // }; - // - // let msg = gateway_bonding_sign_payload(deps, sender, None, gateway.clone(), stake.clone()); - // let owner_signature = ed25519_sign_message(msg, keypair.private_key()); - // - // (gateway, owner_signature) - // } - - pub fn add_dummy_delegations(mut deps: DepsMut<'_>, env: Env, mix_id: MixId, n: usize) { + pub fn add_dummy_delegations(mut deps: DepsMut<'_>, env: Env, mix_id: NodeId, n: usize) { for i in 0..n { pending_events::delegate( deps.branch(), @@ -856,23 +1678,6 @@ pub mod test_helpers { } } - // pub fn add_dummy_mixnodes( - // mut rng: impl RngCore + CryptoRng, - // mut deps: DepsMut<'_>, - // env: Env, - // n: usize, - // ) { - // for i in 0..n { - // add_mixnode( - // &mut rng, - // deps.branch(), - // env.clone(), - // &format!("owner{}", i), - // tests::fixtures::good_mixnode_pledge(), - // ); - // } - // } - pub fn add_dummy_unbonded_mixnodes( mut rng: impl RngCore + CryptoRng, mut deps: DepsMut<'_>, @@ -916,7 +1721,7 @@ pub mod test_helpers { deps: DepsMut<'_>, identity_key: Option<&str>, owner: &str, - ) -> MixId { + ) -> NodeId { let id = loop { let candidate = rng.next_u32(); if !mixnodes_storage::unbonded_mixnodes().has(deps.storage, candidate) { @@ -943,13 +1748,28 @@ pub mod test_helpers { id } + pub fn nymnode_bonding_sign_payload( + deps: Deps<'_>, + owner: &str, + node: NymNode, + stake: Vec, + ) -> SignableNymNodeBondingMsg { + let cost_params = tests::fixtures::node_cost_params_fixture(); + let nonce = + signing_storage::get_signing_nonce(deps.storage, Addr::unchecked(owner)).unwrap(); + + let payload = NymNodeBondingPayload::new(node, cost_params); + let content = ContractMessageContent::new(Addr::unchecked(owner), stake, payload); + SignableNymNodeBondingMsg::new(nonce, content) + } + pub fn mixnode_bonding_sign_payload( deps: Deps<'_>, owner: &str, mixnode: MixNode, stake: Vec, ) -> SignableMixNodeBondingMsg { - let cost_params = tests::fixtures::mix_node_cost_params_fixture(); + let cost_params = tests::fixtures::node_cost_params_fixture(); let nonce = signing_storage::get_signing_nonce(deps.storage, Addr::unchecked(owner)).unwrap(); @@ -972,6 +1792,15 @@ 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; @@ -983,8 +1812,7 @@ pub mod test_helpers { 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_size: 240, - active_set_size: 100, + rewarded_set_params: intial_rewarded_set_params(), } } @@ -1006,14 +1834,14 @@ pub mod test_helpers { deps } - pub fn delegate(deps: DepsMut<'_>, env: Env, sender: &str, stake: Vec, mix_id: MixId) { + pub fn delegate(deps: DepsMut<'_>, env: Env, sender: &str, stake: Vec, mix_id: NodeId) { let info = mock_info(sender, &stake); - try_delegate_to_mixnode(deps, env, info, mix_id).unwrap(); + try_delegate_to_node(deps, env, info, mix_id).unwrap(); } pub(crate) fn read_delegation( storage: &dyn Storage, - mix: MixId, + mix: NodeId, owner: &Addr, proxy: &Option, ) -> Option { diff --git a/contracts/mixnet/src/testing/legacy.rs b/contracts/mixnet/src/testing/legacy.rs new file mode 100644 index 0000000000..38a919fd33 --- /dev/null +++ b/contracts/mixnet/src/testing/legacy.rs @@ -0,0 +1,12 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn legacy_mixnode_bonding() { + todo!() + } +} diff --git a/contracts/mixnet/src/testing/mod.rs b/contracts/mixnet/src/testing/mod.rs index 30de4b3136..ac99770b50 100644 --- a/contracts/mixnet/src/testing/mod.rs +++ b/contracts/mixnet/src/testing/mod.rs @@ -2,3 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 pub(crate) mod transactions; + +// the purpose of that module is to keep track of tests of legacy features that will eventually be phased out +// such as standalone mixnode/gateway bonding +pub(crate) mod legacy; diff --git a/contracts/mixnet/src/vesting_migration.rs b/contracts/mixnet/src/vesting_migration.rs index 3faf826ec0..c67a87518e 100644 --- a/contracts/mixnet/src/vesting_migration.rs +++ b/contracts/mixnet/src/vesting_migration.rs @@ -5,12 +5,13 @@ use crate::delegations::storage as delegations_storage; use crate::mixnet_contract_settings::storage as mixnet_params_storage; use crate::mixnodes::helpers::get_mixnode_details_by_owner; use crate::mixnodes::storage as mixnodes_storage; +use crate::rewards::storage as rewards_storage; use crate::support::helpers::{ ensure_bonded, ensure_epoch_in_progress_state, ensure_no_pending_pledge_changes, }; -use cosmwasm_std::{wasm_execute, DepsMut, MessageInfo, Response}; +use cosmwasm_std::{wasm_execute, DepsMut, Env, Event, MessageInfo, Response}; use mixnet_contract_common::error::MixnetContractError; -use mixnet_contract_common::{Delegation, MixId}; +use mixnet_contract_common::{Delegation, NodeId}; use vesting_contract_common::messages::ExecuteMsg as VestingExecuteMsg; pub(crate) fn try_migrate_vested_mixnode( @@ -49,42 +50,159 @@ pub(crate) fn try_migrate_vested_mixnode( Some(&mix_details.bond_information), )?; - Ok(Response::new().add_message(wasm_execute( - vesting_contract, - &VestingExecuteMsg::TrackMigratedMixnode { - owner: info.sender.into_string(), - }, - vec![], - )?)) + Ok(Response::new() + .add_event(Event::new("migrate-vested-mixnode").add_attribute("mix_id", mix_id.to_string())) + .add_message(wasm_execute( + vesting_contract, + &VestingExecuteMsg::TrackMigratedMixnode { + owner: info.sender.into_string(), + }, + vec![], + )?)) } pub(crate) fn try_migrate_vested_delegation( deps: DepsMut<'_>, + env: Env, info: MessageInfo, - mix_id: MixId, + mix_id: NodeId, ) -> Result { + let mut response = Response::new(); + ensure_epoch_in_progress_state(deps.storage)?; let vesting_contract = mixnet_params_storage::vesting_contract_address(deps.storage)?; let storage_key = Delegation::generate_storage_key(mix_id, &info.sender, Some(&vesting_contract)); - let Some(mut delegation) = + let Some(vested_delegation) = delegations_storage::delegations().may_load(deps.storage, storage_key.clone())? else { return Err(MixnetContractError::NotAVestingDelegation); }; // sanity check that's meant to blow up the contract - assert_eq!(delegation.proxy, Some(vesting_contract.clone())); + assert_eq!(vested_delegation.proxy, Some(vesting_contract.clone())); // update the delegation and save it under the correct storage key - delegation.proxy = None; - let updated_storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None); + let mut updated_delegation = vested_delegation.clone(); + updated_delegation.proxy = None; + + let new_storage_key = Delegation::generate_storage_key(mix_id, &info.sender, None); + + // remove the old (vested) delegation delegations_storage::delegations().remove(deps.storage, storage_key)?; - delegations_storage::delegations().save(deps.storage, updated_storage_key, &delegation)?; - Ok(Response::new().add_message(wasm_execute( + // check if there was already a delegation present under that key (i.e. an old liquid one) + if let Some(existing_liquid_delegation) = + delegations_storage::delegations().may_load(deps.storage, new_storage_key.clone())? + { + // treat it as adding extra stake to the existing delegation, so we need to update the unit reward value + // as well as retrieve any pending rewards + // it replicates part of code from `pending_events::delegate`, + // but without some checks that'd be redundant in this instance + let mut mix_rewarding = + rewards_storage::MIXNODE_REWARDING.load(deps.storage, vested_delegation.node_id)?; + + // calculate rewards separately for the purposes of emitting those in events + let pending_liquid_reward = + mix_rewarding.determine_delegation_reward(&existing_liquid_delegation)?; + let pending_vested_reward = + mix_rewarding.determine_delegation_reward(&vested_delegation)?; + + // the calls to 'undelegate' followed by artificial delegate are performed + // to keep the internal `.delegates` field in sync + // (this is due to the fact delegation only holds values up in `Uint128` and lacks the precision of a `Decimal` + // which has to be used for reward accounting) + let liquid_delegation_with_reward = + mix_rewarding.undelegate(&existing_liquid_delegation)?; + let vested_delegation_with_reward = mix_rewarding.undelegate(&vested_delegation)?; + + // updated delegation amount consists of: + // - delegated vested tokens + // - delegated liquid tokens + // - pending rewards earned by the delegated vested tokens + // - pending rewards earned by the delegated liquid tokens + let mut updated_total = liquid_delegation_with_reward.clone(); + updated_total.amount += vested_delegation_with_reward.amount; + mix_rewarding.add_base_delegation(updated_total.amount)?; + + updated_delegation.amount = updated_total; + updated_delegation.height = env.block.height; + updated_delegation.cumulative_reward_ratio = mix_rewarding.total_unit_reward; + + rewards_storage::MIXNODE_REWARDING.save( + deps.storage, + vested_delegation.node_id, + &mix_rewarding, + )?; + + // replace the old delegation with the new one + delegations_storage::delegations().replace( + deps.storage, + new_storage_key, + Some(&updated_delegation), + Some(&existing_liquid_delegation), + )?; + + // just emit EVERYTHING we can. just in case + response.events.push( + Event::new("migrate-vested-delegation") + .add_attribute("mix_id", mix_id.to_string()) + .add_attribute("existing_liquid", "true") + .add_attribute( + "old_vested_unit_reward", + vested_delegation.cumulative_reward_ratio.to_string(), + ) + .add_attribute( + "old_vested_delegation_amount", + vested_delegation.amount.to_string(), + ) + .add_attribute( + "old_liquid_unit_reward", + existing_liquid_delegation + .cumulative_reward_ratio + .to_string(), + ) + .add_attribute( + "old_liquid_delegation_amount", + existing_liquid_delegation.amount.to_string(), + ) + .add_attribute( + "new_unit_reward", + updated_delegation.cumulative_reward_ratio.to_string(), + ) + .add_attribute( + "new_delegation_amount", + updated_delegation.amount.to_string(), + ) + .add_attribute("applied_liquid_reward", pending_liquid_reward.to_string()) + .add_attribute("applied_vested_reward", pending_vested_reward.to_string()), + ) + } else { + // otherwise, this is as simple as resaving the updated value under the new key + delegations_storage::delegations().save( + deps.storage, + new_storage_key, + &updated_delegation, + )?; + + response.events.push( + Event::new("migrate-vested-delegation") + .add_attribute("mix_id", mix_id.to_string()) + .add_attribute("existing_liquid", "false") + .add_attribute( + "old_vested_unit_reward", + vested_delegation.cumulative_reward_ratio.to_string(), + ) + .add_attribute( + "old_vested_delegation_amount", + vested_delegation.amount.to_string(), + ), + ) + } + + Ok(response.add_message(wasm_execute( vesting_contract, &VestingExecuteMsg::TrackMigratedDelegation { owner: info.sender.into_string(), @@ -93,3 +211,364 @@ pub(crate) fn try_migrate_vested_delegation( vec![], )?)) } + +#[cfg(test)] +mod tests { + use super::*; + + #[cfg(test)] + mod migrating_vested_mixnode { + use super::*; + use crate::mixnodes::helpers::get_mixnode_details_by_id; + use crate::support::tests::test_helpers::TestSetup; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{from_binary, Addr, CosmosMsg, WasmMsg}; + + #[test] + fn with_no_bonded_nodes() { + let mut test = TestSetup::new(); + + let sender = mock_info("owner", &[]); + let deps = test.deps_mut(); + + // nothing happens + let res = try_migrate_vested_mixnode(deps, sender).unwrap_err(); + assert_eq!( + res, + MixnetContractError::NoAssociatedMixNodeBond { + owner: Addr::unchecked("owner") + } + ) + } + + #[test] + fn with_liquid_node_bonded() { + let mut test = TestSetup::new(); + test.add_legacy_mixnode("owner", None); + + let sender = mock_info("owner", &[]); + let deps = test.deps_mut(); + + // nothing happens + let res = try_migrate_vested_mixnode(deps, sender).unwrap_err(); + assert_eq!(res, MixnetContractError::NotAVestingMixnode) + } + + #[test] + fn with_vested_node_bonded() { + let mut test = TestSetup::new(); + let mix_id = test.add_legacy_mixnode_with_legal_proxy("owner", None); + + let sender = mock_info("owner", &[]); + let deps = test.deps_mut(); + + let existing_node = get_mixnode_details_by_id(deps.storage, mix_id) + .unwrap() + .unwrap(); + assert!(existing_node.bond_information.proxy.is_some()); + + let mut expected = existing_node.clone(); + expected.bond_information.proxy = None; + + // node is simply resaved with proxy data removed and a track message is sent into the vesting contract + let res = try_migrate_vested_mixnode(deps, sender).unwrap(); + let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = &res.messages[0].msg else { + panic!("no track message present") + }; + + assert_eq!( + from_binary::(msg).unwrap(), + VestingExecuteMsg::TrackMigratedMixnode { + owner: "owner".to_string() + } + ); + } + } + + #[cfg(test)] + mod migrating_vested_delegation { + use super::*; + use crate::delegations::storage::delegations; + use crate::support::tests::test_helpers::{assert_eq_with_leeway, TestSetup}; + use cosmwasm_std::testing::mock_info; + use cosmwasm_std::{from_binary, Addr, CosmosMsg, Order, Uint128, WasmMsg}; + use mixnet_contract_common::helpers::compare_decimals; + use mixnet_contract_common::nym_node::Role; + use mixnet_contract_common::reward_params::{NodeRewardingParameters, Performance}; + use mixnet_contract_common::rewarding::helpers::truncate_reward; + use mixnet_contract_common::RoleAssignment; + use rand::RngCore; + + #[test] + fn with_no_delegation() { + let mut test = TestSetup::new_complex(); + let env = test.env(); + + let sender = mock_info("owner-without-any-delegations", &[]); + + // it simply fails for there is nothing to migrate + let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, 42).unwrap_err(); + assert_eq!(res, MixnetContractError::NotAVestingDelegation); + } + + #[test] + fn with_just_liquid_delegation() { + let mut test = TestSetup::new_complex(); + let env = test.env(); + + // find a valid delegation + let delegation = delegations() + .range(test.deps().storage, None, None, Order::Ascending) + .filter_map(|d| d.map(|(_, del)| del).ok()) + .find(|d| d.proxy.is_none()) + .unwrap(); + + // make sure we haven't chosen somebody that also has a vested delegation because that would have invalidated the test + assert!(!delegations() + .range(test.deps().storage, None, None, Order::Ascending) + .filter_map(|d| d.map(|(_, del)| del).ok()) + .any(|d| d.proxy.is_some() && d.owner.as_str() == delegation.owner.as_str())); + + let sender = mock_info(delegation.owner.as_str(), &[]); + let mix_id = delegation.node_id; + + // it also fails because the method is only allowed for vested delegations + let res = + try_migrate_vested_delegation(test.deps_mut(), env, sender, mix_id).unwrap_err(); + assert_eq!(res, MixnetContractError::NotAVestingDelegation); + } + + #[test] + fn with_just_vested_delegation() { + let mut test = TestSetup::new_complex(); + let env = test.env(); + + // find a valid delegation + let delegation = delegations() + .range(test.deps().storage, None, None, Order::Ascending) + .filter_map(|d| d.map(|(_, del)| del).ok()) + .find(|d| d.proxy.is_some()) + .unwrap(); + + // make sure we haven't chosen somebody that also has a liquid delegation because that would have invalidated the test + assert!(!delegations() + .range(test.deps().storage, None, None, Order::Ascending) + .filter_map(|d| d.map(|(_, del)| del).ok()) + .any(|d| d.proxy.is_none() && d.owner.as_str() == delegation.owner.as_str())); + + let storage_key = delegation.storage_key(); + let mut expected_liquid = delegation.clone(); + expected_liquid.proxy = None; + let expected_new_storage_key = expected_liquid.storage_key(); + + let sender = mock_info(delegation.owner.as_str(), &[]); + let mix_id = delegation.node_id; + + // a track message is sent into the vesting contract + let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, mix_id).unwrap(); + let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = &res.messages[0].msg else { + panic!("no track message present") + }; + + assert_eq!( + from_binary::(msg).unwrap(), + VestingExecuteMsg::TrackMigratedDelegation { + owner: delegation.owner.to_string(), + mix_id, + } + ); + + // the entry is gone from the old storage key + assert!(delegations() + .may_load(test.deps().storage, storage_key) + .unwrap() + .is_none()); + + // and is resaved (without proxy) under the new key + assert_eq!( + expected_liquid, + delegations() + .load(test.deps().storage, expected_new_storage_key) + .unwrap() + ); + } + + #[test] + fn with_both_liquid_and_vested_delegation() { + let mut test = TestSetup::new_complex(); + let env = test.env(); + + let problematic_delegator = "n1foomp"; + let problematic_delegator_twin = "n1bar"; + let mix_id = 4; + + let liquid_storage_key = Delegation::generate_storage_key( + mix_id, + &Addr::unchecked(problematic_delegator), + None, + ); + let vested_storage_key = Delegation::generate_storage_key( + mix_id, + &Addr::unchecked(problematic_delegator), + Some(&test.vesting_contract()), + ); + + let liquid_delegation = delegations() + .load(test.deps().storage, liquid_storage_key.clone()) + .unwrap(); + let vested_delegation = delegations() + .load(test.deps().storage, vested_storage_key.clone()) + .unwrap(); + let mix_info = test.mix_rewarding(mix_id); + let unclaimed_liquid_reward = mix_info + .determine_delegation_reward(&liquid_delegation) + .unwrap(); + let unclaimed_vested_reward = mix_info + .determine_delegation_reward(&vested_delegation) + .unwrap(); + + // sanity check before doing anything + test.ensure_delegation_sync(mix_id); + + // a track message is sent into the vesting contract + let sender = mock_info(problematic_delegator, &[]); + let res = try_migrate_vested_delegation(test.deps_mut(), env, sender, mix_id).unwrap(); + let CosmosMsg::Wasm(WasmMsg::Execute { msg, .. }) = &res.messages[0].msg else { + panic!("no track message present") + }; + + assert_eq!( + from_binary::(msg).unwrap(), + VestingExecuteMsg::TrackMigratedDelegation { + owner: problematic_delegator.to_string(), + mix_id, + } + ); + + let updated_mix_info = test.mix_rewarding(mix_id); + assert_eq!( + mix_info.unique_delegations - 1, + updated_mix_info.unique_delegations + ); + + // the vested delegation is gone + assert!(delegations() + .may_load(test.deps().storage, vested_storage_key) + .unwrap() + .is_none()); + + let updated_liquid_delegation = delegations() + .load(test.deps().storage, liquid_storage_key.clone()) + .unwrap(); + + assert!(updated_liquid_delegation.proxy.is_none()); + assert_eq!( + updated_liquid_delegation.cumulative_reward_ratio, + updated_mix_info.total_unit_reward + ); + + let expected_amount = truncate_reward( + vested_delegation.dec_amount().unwrap() + + liquid_delegation.dec_amount().unwrap() + + unclaimed_liquid_reward + + unclaimed_vested_reward, + "unym", + ); + // due to rounding we can expect and tolerate a single token of difference + assert_eq_with_leeway( + updated_liquid_delegation.amount.amount, + expected_amount.amount, + Uint128::one(), + ); + + // this assertion must still hold + test.ensure_delegation_sync(mix_id); + + // go through few more rewarding epochs to make sure the rewards and accounting + // would be the same as if the delegations remained separate + let all_nodes = test.all_mixnodes(); + + let twin_liquid_storage_key = Delegation::generate_storage_key( + mix_id, + &Addr::unchecked(problematic_delegator_twin), + None, + ); + let twin_vested_storage_key = Delegation::generate_storage_key( + mix_id, + &Addr::unchecked(problematic_delegator_twin), + Some(&test.vesting_contract()), + ); + + let twin_liquid_delegation = delegations() + .load(test.deps().storage, twin_liquid_storage_key.clone()) + .unwrap(); + let twin_vested_delegation = delegations() + .load(test.deps().storage, twin_vested_storage_key.clone()) + .unwrap(); + + let info = test.mix_rewarding(mix_id); + + let unclaimed_rewards_twin_liquid = info + .determine_delegation_reward(&twin_liquid_delegation) + .unwrap(); + let unclaimed_rewards_twin_vested = info + .determine_delegation_reward(&twin_vested_delegation) + .unwrap(); + + for _ in 0..100 { + test.skip_to_next_epoch_end(); + // it doesn't matter that they're on the same layer here, we just need to make sure they're rewarded + test.force_assign_rewarded_set(vec![RoleAssignment { + role: Role::Layer1, + nodes: all_nodes.clone(), + }]); + test.start_epoch_transition(); + + // reward each node + for node in &all_nodes { + let performance = test.rng.next_u64() % 100; + let work_factor = test.active_node_work(); + test.reward_with_distribution( + *node, + NodeRewardingParameters { + performance: Performance::from_percentage_value(performance).unwrap(), + work_factor, + }, + ); + } + + test.set_epoch_in_progress_state(); + } + + // this assertion must still hold + test.ensure_delegation_sync(mix_id); + + let info = test.mix_rewarding(mix_id); + + let current_liquid = delegations() + .load(test.deps().storage, liquid_storage_key) + .unwrap(); + let rewards = info.determine_delegation_reward(¤t_liquid).unwrap(); + + let twin_liquid_delegation = delegations() + .load(test.deps().storage, twin_liquid_storage_key.clone()) + .unwrap(); + let twin_vested_delegation = delegations() + .load(test.deps().storage, twin_vested_storage_key.clone()) + .unwrap(); + + let rewards_twin_liquid = info + .determine_delegation_reward(&twin_liquid_delegation) + .unwrap(); + let rewards_twin_vested = info + .determine_delegation_reward(&twin_vested_delegation) + .unwrap(); + + let new_rewards_twin = rewards_twin_liquid + rewards_twin_vested + - unclaimed_rewards_twin_liquid + - unclaimed_rewards_twin_vested; + + compare_decimals(rewards, new_rewards_twin, Some("0.01".parse().unwrap())) + } + } +} diff --git a/contracts/vesting/schema/nym-vesting-contract.json b/contracts/vesting/schema/nym-vesting-contract.json index 1a7b292784..26069a16c4 100644 --- a/contracts/vesting/schema/nym-vesting-contract.json +++ b/contracts/vesting/schema/nym-vesting-contract.json @@ -24,96 +24,6 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "ExecuteMsg", "oneOf": [ - { - "description": "Only owner of the node can crate the family with node as head", - "type": "object", - "required": [ - "create_family" - ], - "properties": { - "create_family": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Family head needs to sign the joining node IdentityKey, the Node provides its signature signaling consent to join the family", - "type": "object", - "required": [ - "join_family" - ], - "properties": { - "join_family": { - "type": "object", - "required": [ - "family_head", - "join_permit" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "join_permit": { - "$ref": "#/definitions/MessageSignature" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "leave_family" - ], - "properties": { - "leave_family": { - "type": "object", - "required": [ - "family_head" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "kick_family_member" - ], - "properties": { - "kick_family_member": { - "type": "object", - "required": [ - "member" - ], - "properties": { - "member": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -188,7 +98,7 @@ ], "properties": { "new_costs": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } }, "additionalProperties": false @@ -418,7 +328,7 @@ "$ref": "#/definitions/Coin" }, "cost_params": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "mix_node": { "$ref": "#/definitions/MixNode" @@ -761,10 +671,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - }, "Gateway": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -937,7 +843,7 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -946,7 +852,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -954,7 +860,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" diff --git a/contracts/vesting/schema/raw/execute.json b/contracts/vesting/schema/raw/execute.json index 9723294d67..81565897d4 100644 --- a/contracts/vesting/schema/raw/execute.json +++ b/contracts/vesting/schema/raw/execute.json @@ -2,96 +2,6 @@ "$schema": "http://json-schema.org/draft-07/schema#", "title": "ExecuteMsg", "oneOf": [ - { - "description": "Only owner of the node can crate the family with node as head", - "type": "object", - "required": [ - "create_family" - ], - "properties": { - "create_family": { - "type": "object", - "required": [ - "label" - ], - "properties": { - "label": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "description": "Family head needs to sign the joining node IdentityKey, the Node provides its signature signaling consent to join the family", - "type": "object", - "required": [ - "join_family" - ], - "properties": { - "join_family": { - "type": "object", - "required": [ - "family_head", - "join_permit" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - }, - "join_permit": { - "$ref": "#/definitions/MessageSignature" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "leave_family" - ], - "properties": { - "leave_family": { - "type": "object", - "required": [ - "family_head" - ], - "properties": { - "family_head": { - "$ref": "#/definitions/FamilyHead" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, - { - "type": "object", - "required": [ - "kick_family_member" - ], - "properties": { - "kick_family_member": { - "type": "object", - "required": [ - "member" - ], - "properties": { - "member": { - "type": "string" - } - }, - "additionalProperties": false - } - }, - "additionalProperties": false - }, { "type": "object", "required": [ @@ -166,7 +76,7 @@ ], "properties": { "new_costs": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" } }, "additionalProperties": false @@ -396,7 +306,7 @@ "$ref": "#/definitions/Coin" }, "cost_params": { - "$ref": "#/definitions/MixNodeCostParams" + "$ref": "#/definitions/NodeCostParams" }, "mix_node": { "$ref": "#/definitions/MixNode" @@ -739,10 +649,6 @@ "description": "A fixed-point decimal value with 18 fractional digits, i.e. Decimal(1_000_000_000_000_000_000) == 1.0\n\nThe greatest possible value that can be represented is 340282366920938463463.374607431768211455 (which is (2^128 - 1) / 10^18)", "type": "string" }, - "FamilyHead": { - "description": "Head of particular family as identified by its identity key (i.e. public component of its ed25519 keypair stringified into base58).", - "type": "string" - }, "Gateway": { "description": "Information provided by the node operator during bonding that are used to allow other entities to use the services of this node.", "type": "object", @@ -915,7 +821,7 @@ }, "additionalProperties": false }, - "MixNodeCostParams": { + "NodeCostParams": { "description": "The cost parameters, or the cost function, defined for the particular mixnode that influences how the rewards should be split between the node operator and its delegators.", "type": "object", "required": [ @@ -924,7 +830,7 @@ ], "properties": { "interval_operating_cost": { - "description": "Operating cost of the associated mixnode per the entire interval.", + "description": "Operating cost of the associated node per the entire interval.", "allOf": [ { "$ref": "#/definitions/Coin" @@ -932,7 +838,7 @@ ] }, "profit_margin_percent": { - "description": "The profit margin of the associated mixnode, i.e. the desired percent of the reward to be distributed to the operator.", + "description": "The profit margin of the associated node, i.e. the desired percent of the reward to be distributed to the operator.", "allOf": [ { "$ref": "#/definitions/Percent" diff --git a/contracts/vesting/src/queries.rs b/contracts/vesting/src/queries.rs index 17330c6dbe..a744db56ad 100644 --- a/contracts/vesting/src/queries.rs +++ b/contracts/vesting/src/queries.rs @@ -8,7 +8,7 @@ use crate::vesting::StorableVestingAccountExt; use contracts_common::{get_build_information, ContractBuildInformation}; use cosmwasm_std::{Coin, Deps, Env, Order, StdResult, Timestamp, Uint128}; use cw_storage_plus::Bound; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use vesting_contract_common::{ Account, AccountVestingCoins, AccountsResponse, AllDelegationsResponse, BaseVestingAccountInfo, DelegationTimesResponse, OriginalVestingResponse, Period, PledgeData, VestingCoinsResponse, @@ -259,7 +259,7 @@ pub fn try_get_withdrawn_coins( pub fn try_get_delegation_times( deps: Deps<'_>, vesting_account_address: &str, - mix_id: MixId, + mix_id: NodeId, ) -> Result { let owner = deps.api.addr_validate(vesting_account_address)?; let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; @@ -277,7 +277,7 @@ pub fn try_get_delegation_times( pub fn try_get_all_delegations( deps: Deps<'_>, - start_after: Option<(u32, MixId, BlockTimestampSecs)>, + start_after: Option<(u32, NodeId, BlockTimestampSecs)>, limit: Option, ) -> Result { let limit = limit.unwrap_or(100).min(200) as usize; @@ -314,7 +314,7 @@ pub fn try_get_all_delegations( pub fn try_get_delegation( deps: Deps<'_>, vesting_account_address: &str, - mix_id: MixId, + mix_id: NodeId, block_timestamp_secs: BlockTimestampSecs, ) -> Result { let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; @@ -333,7 +333,7 @@ pub fn try_get_delegation( pub fn try_get_delegation_amount( deps: Deps<'_>, vesting_account_address: &str, - mix_id: MixId, + mix_id: NodeId, ) -> Result { let account = account_from_address(vesting_account_address, deps.storage, deps.api)?; diff --git a/contracts/vesting/src/storage.rs b/contracts/vesting/src/storage.rs index d6d363d36f..a2122de58f 100644 --- a/contracts/vesting/src/storage.rs +++ b/contracts/vesting/src/storage.rs @@ -1,7 +1,7 @@ use cosmwasm_std::{Addr, Api, Storage, Uint128}; use cosmwasm_std::{Coin, Order}; use cw_storage_plus::{Item, Map}; -use mixnet_contract_common::{IdentityKey, MixId}; +use mixnet_contract_common::{IdentityKey, NodeId}; use vesting_contract_common::account::VestingAccountStorageKey; use vesting_contract_common::{Account, PledgeData, VestingContractError}; @@ -40,7 +40,7 @@ pub const _OLD_DELEGATIONS: Map< /// Storage map containing information about tokens delegated towards particular mixnodes /// in the mixnet contract with given vesting account. -pub const DELEGATIONS: Map<'_, (VestingAccountStorageKey, MixId, BlockTimestampSecs), Uint128> = +pub const DELEGATIONS: Map<'_, (VestingAccountStorageKey, NodeId, BlockTimestampSecs), Uint128> = Map::new("dlg_v2"); /// Explicit contract admin that is allowed, among other things, to create new vesting accounts. @@ -53,7 +53,7 @@ pub const MIXNET_CONTRACT_ADDRESS: Item<'_, Addr> = Item::new("mix"); pub const MIX_DENOM: Item<'_, String> = Item::new("den"); pub fn save_delegation( - key: (VestingAccountStorageKey, MixId, BlockTimestampSecs), + key: (VestingAccountStorageKey, NodeId, BlockTimestampSecs), amount: Uint128, storage: &mut dyn Storage, ) -> Result<(), VestingContractError> { @@ -68,7 +68,7 @@ pub fn save_delegation( } pub fn remove_delegation( - key: (VestingAccountStorageKey, MixId, BlockTimestampSecs), + key: (VestingAccountStorageKey, NodeId, BlockTimestampSecs), storage: &mut dyn Storage, ) -> Result<(), VestingContractError> { DELEGATIONS.remove(storage, key); @@ -76,7 +76,7 @@ pub fn remove_delegation( } pub fn load_delegation_timestamps( - prefix: (VestingAccountStorageKey, MixId), + prefix: (VestingAccountStorageKey, NodeId), storage: &dyn Storage, ) -> Result, VestingContractError> { let block_timestamps = DELEGATIONS @@ -87,7 +87,7 @@ pub fn load_delegation_timestamps( } pub fn count_subdelegations_for_mix( - prefix: (VestingAccountStorageKey, MixId), + prefix: (VestingAccountStorageKey, NodeId), storage: &dyn Storage, ) -> u32 { DELEGATIONS diff --git a/contracts/vesting/src/traits/bonding_account.rs b/contracts/vesting/src/traits/bonding_account.rs index 8e4c7c6a62..d4fb13e330 100644 --- a/contracts/vesting/src/traits/bonding_account.rs +++ b/contracts/vesting/src/traits/bonding_account.rs @@ -2,7 +2,7 @@ use contracts_common::signing::MessageSignature; use cosmwasm_std::{Coin, Env, Response, Storage}; use mixnet_contract_common::{ gateway::GatewayConfigUpdate, - mixnode::{MixNodeConfigUpdate, MixNodeCostParams}, + mixnode::{MixNodeConfigUpdate, NodeCostParams}, Gateway, MixNode, }; use vesting_contract_common::VestingContractError; @@ -16,7 +16,7 @@ pub trait MixnodeBondingAccount { fn try_bond_mixnode( &self, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, pledge: Coin, env: &Env, @@ -58,7 +58,7 @@ pub trait MixnodeBondingAccount { fn try_update_mixnode_cost_params( &self, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, storage: &mut dyn Storage, ) -> Result; fn try_track_migrated_mixnode( diff --git a/contracts/vesting/src/traits/delegating_account.rs b/contracts/vesting/src/traits/delegating_account.rs index 2e7b5a65d3..b0d2154537 100644 --- a/contracts/vesting/src/traits/delegating_account.rs +++ b/contracts/vesting/src/traits/delegating_account.rs @@ -1,17 +1,17 @@ use cosmwasm_std::{Coin, Env, Response, Storage, Uint128}; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use vesting_contract_common::VestingContractError; pub trait DelegatingAccount { fn try_claim_delegator_reward( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result; fn try_delegate_to_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, amount: Coin, env: &Env, storage: &mut dyn Storage, @@ -19,7 +19,7 @@ pub trait DelegatingAccount { fn try_undelegate_from_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result; @@ -30,7 +30,7 @@ pub trait DelegatingAccount { fn track_delegation( &self, block_height: u64, - mix_id: MixId, + mix_id: NodeId, // Save some gas by passing it in current_balance: Uint128, delegation: Coin, @@ -40,13 +40,13 @@ pub trait DelegatingAccount { // vesting account performs an undelegation. fn track_undelegation( &self, - mix_id: MixId, + mix_id: NodeId, amount: Coin, storage: &mut dyn Storage, ) -> Result<(), VestingContractError>; fn track_migrated_delegation( &self, - mix_id: MixId, + mix_id: NodeId, storage: &mut dyn Storage, ) -> Result<(), VestingContractError>; } diff --git a/contracts/vesting/src/traits/mod.rs b/contracts/vesting/src/traits/mod.rs index d1126bb92c..60cff028d4 100644 --- a/contracts/vesting/src/traits/mod.rs +++ b/contracts/vesting/src/traits/mod.rs @@ -1,9 +1,7 @@ pub mod bonding_account; pub mod delegating_account; -pub mod node_families; pub mod vesting_account; pub use self::bonding_account::{GatewayBondingAccount, MixnodeBondingAccount}; pub use self::delegating_account::DelegatingAccount; -pub use self::node_families::NodeFamilies; pub use self::vesting_account::VestingAccount; diff --git a/contracts/vesting/src/traits/node_families.rs b/contracts/vesting/src/traits/node_families.rs deleted file mode 100644 index f5af460d17..0000000000 --- a/contracts/vesting/src/traits/node_families.rs +++ /dev/null @@ -1,32 +0,0 @@ -use contracts_common::signing::MessageSignature; -use cosmwasm_std::{Response, Storage}; -use mixnet_contract_common::families::FamilyHead; -use mixnet_contract_common::IdentityKeyRef; -use vesting_contract_common::VestingContractError; - -pub trait NodeFamilies { - fn try_create_family( - &self, - storage: &dyn Storage, - label: String, - ) -> Result; - - fn try_join_family( - &self, - storage: &dyn Storage, - join_permit: MessageSignature, - family_head: FamilyHead, - ) -> Result; - - fn try_leave_family( - &self, - storage: &dyn Storage, - family_head: FamilyHead, - ) -> Result; - - fn try_head_kick_member( - &self, - storage: &dyn Storage, - member: IdentityKeyRef<'_>, - ) -> Result; -} diff --git a/contracts/vesting/src/transactions.rs b/contracts/vesting/src/transactions.rs index a51f084862..09e3ac9a8a 100644 --- a/contracts/vesting/src/transactions.rs +++ b/contracts/vesting/src/transactions.rs @@ -6,14 +6,13 @@ use crate::storage::{ account_from_address, save_account, ADMIN, MIXNET_CONTRACT_ADDRESS, MIX_DENOM, }; use crate::traits::{ - DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, NodeFamilies, VestingAccount, + DelegatingAccount, GatewayBondingAccount, MixnodeBondingAccount, VestingAccount, }; use crate::vesting::{populate_vesting_periods, StorableVestingAccountExt}; use contracts_common::signing::MessageSignature; use cosmwasm_std::{coin, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Timestamp}; -use mixnet_contract_common::families::FamilyHead; use mixnet_contract_common::{ - Gateway, GatewayConfigUpdate, MixId, MixNode, MixNodeConfigUpdate, MixNodeCostParams, + Gateway, GatewayConfigUpdate, MixNode, MixNodeConfigUpdate, NodeCostParams, NodeId, }; use vesting_contract_common::events::{ new_ownership_transfer_event, new_periodic_vesting_account_event, @@ -24,40 +23,6 @@ use vesting_contract_common::events::{ }; use vesting_contract_common::{Account, PledgeCap, VestingContractError, VestingSpecification}; -pub fn try_create_family( - info: MessageInfo, - deps: DepsMut, - label: String, -) -> Result { - let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_create_family(deps.storage, label) -} -pub fn try_join_family( - info: MessageInfo, - deps: DepsMut, - join_permit: MessageSignature, - family_head: FamilyHead, -) -> Result { - let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_join_family(deps.storage, join_permit, family_head) -} -pub fn try_leave_family( - info: MessageInfo, - deps: DepsMut, - family_head: FamilyHead, -) -> Result { - let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_leave_family(deps.storage, family_head) -} -pub fn try_kick_family_member( - info: MessageInfo, - deps: DepsMut, - member: String, -) -> Result { - let account = account_from_address(info.sender.as_ref(), deps.storage, deps.api)?; - account.try_head_kick_member(deps.storage, &member) -} - /// Update locked_pledge_cap, the hard cap for staking/bonding with unvested tokens. /// /// Callable by ADMIN only, see [instantiate]. @@ -99,7 +64,7 @@ pub fn try_update_gateway_config( } pub fn try_update_mixnode_cost_params( - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, info: MessageInfo, deps: DepsMut, ) -> Result { @@ -273,7 +238,7 @@ pub fn try_track_migrate_mixnode( /// Track vesting delegation being converted into the usage of liquid tokens. invoked by the mixnet contract after successful migration. pub fn try_track_migrate_delegation( owner: &str, - mix_id: MixId, + mix_id: NodeId, info: MessageInfo, deps: DepsMut<'_>, ) -> Result { @@ -288,7 +253,7 @@ pub fn try_track_migrate_delegation( /// Bond a mixnode, sends [mixnet_contract_common::ExecuteMsg::BondMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_bond_mixnode( mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, amount: Coin, info: MessageInfo, @@ -392,7 +357,7 @@ pub fn try_track_reward( /// Track undelegation, invoked by the mixnet contract after sucessful undelegation, message contains coins returned with any accrued rewards. pub fn try_track_undelegation( address: &str, - mix_id: MixId, + mix_id: NodeId, amount: Coin, info: MessageInfo, deps: DepsMut<'_>, @@ -408,7 +373,7 @@ pub fn try_track_undelegation( /// Delegate to mixnode, sends [mixnet_contract_common::ExecuteMsg::DelegateToMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS].. pub fn try_delegate_to_mixnode( - mix_id: MixId, + mix_id: NodeId, amount: Coin, on_behalf_of: Option, info: MessageInfo, @@ -452,7 +417,7 @@ pub fn try_claim_operator_reward( pub fn try_claim_delegator_reward( deps: DepsMut<'_>, info: MessageInfo, - mix_id: MixId, + mix_id: NodeId, ) -> Result { let account = account_from_address(info.sender.as_str(), deps.storage, deps.api)?; @@ -461,7 +426,7 @@ pub fn try_claim_delegator_reward( /// Undelegates from a mixnode, sends [mixnet_contract_common::ExecuteMsg::UndelegateFromMixnodeOnBehalf] to [crate::storage::MIXNET_CONTRACT_ADDRESS]. pub fn try_undelegate_from_mixnode( - mix_id: MixId, + mix_id: NodeId, on_behalf_of: Option, info: MessageInfo, deps: DepsMut<'_>, diff --git a/contracts/vesting/src/vesting/account/delegating_account.rs b/contracts/vesting/src/vesting/account/delegating_account.rs index d01bea6789..167707401a 100644 --- a/contracts/vesting/src/vesting/account/delegating_account.rs +++ b/contracts/vesting/src/vesting/account/delegating_account.rs @@ -5,7 +5,7 @@ use crate::traits::DelegatingAccount; use crate::vesting::account::StorableVestingAccountExt; use cosmwasm_std::{wasm_execute, Coin, Env, Response, Storage, Uint128}; use mixnet_contract_common::ExecuteMsg as MixnetExecuteMsg; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use vesting_contract_common::events::{ new_vesting_delegation_event, new_vesting_undelegation_event, }; @@ -16,7 +16,7 @@ use super::Account; impl DelegatingAccount for Account { fn try_claim_delegator_reward( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result { let msg = MixnetExecuteMsg::WithdrawDelegatorRewardOnBehalf { @@ -32,7 +32,7 @@ impl DelegatingAccount for Account { fn try_delegate_to_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, coin: Coin, env: &Env, storage: &mut dyn Storage, @@ -74,7 +74,7 @@ impl DelegatingAccount for Account { fn try_undelegate_from_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result { if !self.any_delegation_for_mix(mix_id, storage) { @@ -99,7 +99,7 @@ impl DelegatingAccount for Account { fn track_delegation( &self, block_timestamp_secs: u64, - mix_id: MixId, + mix_id: NodeId, current_balance: Uint128, delegation: Coin, storage: &mut dyn Storage, @@ -116,7 +116,7 @@ impl DelegatingAccount for Account { fn track_undelegation( &self, - mix_id: MixId, + mix_id: NodeId, amount: Coin, storage: &mut dyn Storage, ) -> Result<(), VestingContractError> { @@ -128,7 +128,7 @@ impl DelegatingAccount for Account { fn track_migrated_delegation( &self, - mix_id: MixId, + mix_id: NodeId, storage: &mut dyn Storage, ) -> Result<(), VestingContractError> { let delegation = self.total_delegations_for_mix(mix_id, storage)?; diff --git a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs index ee14207d3e..fb3ede091f 100644 --- a/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs +++ b/contracts/vesting/src/vesting/account/mixnode_bonding_account.rs @@ -8,7 +8,7 @@ use crate::vesting::account::StorableVestingAccountExt; use contracts_common::signing::MessageSignature; use cosmwasm_std::{wasm_execute, Coin, Env, Response, Storage, Uint128}; use mixnet_contract_common::mixnode::MixNodeConfigUpdate; -use mixnet_contract_common::mixnode::MixNodeCostParams; +use mixnet_contract_common::mixnode::NodeCostParams; use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, MixNode}; use vesting_contract_common::events::{ new_vesting_decrease_pledge_event, new_vesting_mixnode_bonding_event, @@ -35,7 +35,7 @@ impl MixnodeBondingAccount for Account { fn try_bond_mixnode( &self, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, owner_signature: MessageSignature, pledge: Coin, env: &Env, @@ -206,7 +206,7 @@ impl MixnodeBondingAccount for Account { fn try_update_mixnode_cost_params( &self, - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, storage: &mut dyn Storage, ) -> Result { let msg = MixnetExecuteMsg::UpdateMixnodeCostParamsOnBehalf { diff --git a/contracts/vesting/src/vesting/account/mod.rs b/contracts/vesting/src/vesting/account/mod.rs index c09b6057b5..4353b748eb 100644 --- a/contracts/vesting/src/vesting/account/mod.rs +++ b/contracts/vesting/src/vesting/account/mod.rs @@ -10,14 +10,13 @@ use crate::storage::{ }; use crate::traits::VestingAccount; use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128}; -use mixnet_contract_common::MixId; +use mixnet_contract_common::NodeId; use vesting_contract_common::account::VestingAccountStorageKey; use vesting_contract_common::{Account, PledgeCap, PledgeData, VestingContractError}; mod delegating_account; mod gateway_bonding_account; mod mixnode_bonding_account; -mod node_families; mod vesting_account; fn generate_storage_key( @@ -152,20 +151,20 @@ pub(crate) trait StorableVestingAccountExt: VestingAccount { fn remove_gateway_pledge(&self, storage: &mut dyn Storage) -> Result<(), VestingContractError>; - fn any_delegation_for_mix(&self, mix_id: MixId, storage: &dyn Storage) -> bool; + fn any_delegation_for_mix(&self, mix_id: NodeId, storage: &dyn Storage) -> bool; - fn num_subdelegations_for_mix(&self, mix_id: MixId, storage: &dyn Storage) -> u32; + fn num_subdelegations_for_mix(&self, mix_id: NodeId, storage: &dyn Storage) -> u32; fn remove_delegations_for_mix( &self, - mix_id: MixId, + mix_id: NodeId, storage: &mut dyn Storage, ) -> Result<(), VestingContractError>; #[allow(dead_code)] fn total_delegations_for_mix( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result; @@ -273,7 +272,7 @@ impl StorableVestingAccountExt for Account { remove_gateway_pledge(self.storage_key(), storage) } - fn any_delegation_for_mix(&self, mix_id: MixId, storage: &dyn Storage) -> bool { + fn any_delegation_for_mix(&self, mix_id: NodeId, storage: &dyn Storage) -> bool { DELEGATIONS .prefix((self.storage_key(), mix_id)) .range(storage, None, None, Order::Ascending) @@ -281,13 +280,13 @@ impl StorableVestingAccountExt for Account { .is_some() } - fn num_subdelegations_for_mix(&self, mix_id: MixId, storage: &dyn Storage) -> u32 { + fn num_subdelegations_for_mix(&self, mix_id: NodeId, storage: &dyn Storage) -> u32 { count_subdelegations_for_mix((self.storage_key(), mix_id), storage) } fn remove_delegations_for_mix( &self, - mix_id: MixId, + mix_id: NodeId, storage: &mut dyn Storage, ) -> Result<(), VestingContractError> { // note that the limit is implicitly set to `MAX_PER_MIX_DELEGATIONS` @@ -302,7 +301,7 @@ impl StorableVestingAccountExt for Account { fn total_delegations_for_mix( &self, - mix_id: MixId, + mix_id: NodeId, storage: &dyn Storage, ) -> Result { Ok(DELEGATIONS diff --git a/contracts/vesting/src/vesting/account/node_families.rs b/contracts/vesting/src/vesting/account/node_families.rs deleted file mode 100644 index 649531688b..0000000000 --- a/contracts/vesting/src/vesting/account/node_families.rs +++ /dev/null @@ -1,71 +0,0 @@ -use super::Account; -use crate::{storage::MIXNET_CONTRACT_ADDRESS, traits::NodeFamilies}; -use contracts_common::signing::MessageSignature; -use cosmwasm_std::{wasm_execute, Response, Storage}; -use mixnet_contract_common::families::FamilyHead; -use mixnet_contract_common::{ExecuteMsg as MixnetExecuteMsg, IdentityKeyRef}; -use vesting_contract_common::VestingContractError; - -impl NodeFamilies for Account { - fn try_create_family( - &self, - storage: &dyn Storage, - label: String, - ) -> Result { - let msg = MixnetExecuteMsg::CreateFamilyOnBehalf { - owner_address: self.owner_address().into_string(), - label, - }; - - let msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; - - Ok(Response::new().add_message(msg)) - } - - fn try_join_family( - &self, - storage: &dyn Storage, - join_permit: MessageSignature, - family_head: FamilyHead, - ) -> Result { - let msg = MixnetExecuteMsg::JoinFamilyOnBehalf { - member_address: self.owner_address().to_string(), - join_permit, - family_head, - }; - - let msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; - - Ok(Response::new().add_message(msg)) - } - - fn try_leave_family( - &self, - storage: &dyn Storage, - family_head: FamilyHead, - ) -> Result { - let msg = MixnetExecuteMsg::LeaveFamilyOnBehalf { - member_address: self.owner_address().to_string(), - family_head, - }; - - let msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; - - Ok(Response::new().add_message(msg)) - } - - fn try_head_kick_member( - &self, - storage: &dyn Storage, - member: IdentityKeyRef<'_>, - ) -> Result { - let msg = MixnetExecuteMsg::KickFamilyMemberOnBehalf { - head_address: self.owner_address().to_string(), - member: member.to_string(), - }; - - let msg = wasm_execute(MIXNET_CONTRACT_ADDRESS.load(storage)?, &msg, vec![])?; - - Ok(Response::new().add_message(msg)) - } -} diff --git a/contracts/vesting/src/vesting/account/vesting_account.rs b/contracts/vesting/src/vesting/account/vesting_account.rs index 6406df460d..3c2beb142e 100644 --- a/contracts/vesting/src/vesting/account/vesting_account.rs +++ b/contracts/vesting/src/vesting/account/vesting_account.rs @@ -14,7 +14,6 @@ impl VestingAccount for Account { /// See [VestingAccount::locked_coins] for documentation. /// Returns 0 in case of underflow. Which is fine, as the amount of pledged and delegated tokens can be larger then vesting_coins due to rewards and vesting periods expiring - // TODO: rename. it's no longer 'locked'... or is it? fn locked_coins( &self, diff --git a/contracts/vesting/src/vesting/mod.rs b/contracts/vesting/src/vesting/mod.rs index d8da405008..33cfc2baec 100644 --- a/contracts/vesting/src/vesting/mod.rs +++ b/contracts/vesting/src/vesting/mod.rs @@ -26,7 +26,7 @@ mod tests { use contracts_common::signing::MessageSignature; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{coin, coins, Addr, Coin, Timestamp, Uint128}; - use mixnet_contract_common::mixnode::MixNodeCostParams; + use mixnet_contract_common::mixnode::NodeCostParams; use mixnet_contract_common::{Gateway, MixNode, Percent}; use vesting_contract_common::messages::ExecuteMsg; use vesting_contract_common::{Account, PledgeCap, VestingSpecification}; @@ -428,7 +428,7 @@ mod tests { version: "0.10.0".to_string(), }; - let cost_params = MixNodeCostParams { + let cost_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(10).unwrap(), interval_operating_cost: Coin { denom: "NYM".to_string(), diff --git a/documentation/operators/src/changelog.md b/documentation/operators/src/changelog.md index 81dafbb6c2..bd57c8173c 100644 --- a/documentation/operators/src/changelog.md +++ b/documentation/operators/src/changelog.md @@ -2,6 +2,466 @@ This page displays a full list of all the changes during our release cycle from [`v2024.3-eclipse`](https://github.com/nymtech/nym/blob/nym-binaries-v2024.3-eclipse/CHANGELOG.md) onwards. Operators can find here the newest updates together with links to relevant documentation. The list is sorted so that the newest changes appear first. +## `v2024.12-aero` + +- [Release binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2024.12-aero) +- [Release CHANGELOG.md](https://github.com/nymtech/nym/blob/nym-binaries-v2024.12-aero/CHANGELOG.md) +- [`nym-node`](nodes/nym-node.md) version `1.1.9` + +```sh +nym-node +Binary Name: nym-node +Build Timestamp: 2024-10-17T08:57:52.525093253Z +Build Version: 1.1.9 +Commit SHA: d75c7eaaaf3bb7350720cf9c7657ce3f7ee6ec2e +Commit Date: 2024-10-17T08:51:39.000000000+02:00 +Commit Branch: HEAD +rustc Version: 1.81.0 +rustc Channel: stable +cargo Profile: release +``` + +### Features + +- [Rust sdk stream abstraction](https://github.com/nymtech/nym/pull/4743): Starting to move this from being standalone binaries (as seen [here](https://github.com/nymtech/nym-zcash-grpc-demo)) into the sdk. EDIT this has sort of expanded a bit to include a few things: + - [x] simple example + - [x] example doc to `src/tcp_proxy.rs` + - [x] simple echo server in `tools/` + - [x] multithread example + - [x] example to sdk for using different network + - [x] go ffi for proxies + +- [Build(deps): bump `toml` from `0.5.11` to `0.8.14`](https://github.com/nymtech/nym/pull/4805): [`toml`](https://github.com/toml-rs/toml) version update +~~~admonish example collapsible=true title='Testing steps performed' +- Ensured that the `cargo.toml` is legible in various places; tested it on `nym-node`, `nym-api` and `nymvisor`. +- Ensured that updating the cargo.toml file and restarting the given binary continues to behave as normal. +~~~ + +- [Use `serde` from workspace](https://github.com/nymtech/nym/pull/4833): cargo autoinherit for `serde` - cargo autoinherit for `bs58` and `vergen` in `cosmwasm-smart-contracts` + +- [Gateway database modifications for different modes](https://github.com/nymtech/nym/pull/4868): As gateway clients will not be solely from the mixnet, we need to split the table that handles shared keys from the client ids that are referenced from other tables. That way, the bandwidth table can be shared between different client types (entry mixnet, entry gateway, exit gateway), using the same `client_id` referencing. + +- [Remove the push trigger for `ci-nym-wallet-rust`](https://github.com/nymtech/nym/pull/4869) + +- [Chore: remove queued migration for adding explicit admin](https://github.com/nymtech/nym/pull/4871) + +- [Allow clients to send stateless gateway requests without prior registration](https://github.com/nymtech/nym/pull/4873): in order to make changes to the registration/authentication procedure we needed a way of extracting protocol information before undergoing the handshake. + +- [Fix sql `serde` with `enum`](https://github.com/nymtech/nym/pull/4875) + +- [Few fixes to NNM pre deploy](https://github.com/nymtech/nym/pull/4883) + +- [Feature/updated gateway registration](https://github.com/nymtech/nym/pull/4885): This PR introduces support for aes256-gcm-siv shared keys between clients and gateways. + - Those changes should be fully backwards compatible. if they're not, there's a bug. +~~~admonish example collapsible=true title='Testing steps performed' +- For the following combinations I inited the client, ran the client, stopped the client, and ran the client again: +- Fresh client on new binary && gateway on old binary +- Fresh client on old binary && gateway on new binary +- Fresh client on new binary && gateway new binary +- Existing old client on old binary & new gateway +~~~ + +- [Build and Push CI](https://github.com/nymtech/nym/pull/4887) + +- [Entry wireguard tickets](https://github.com/nymtech/nym/pull/4888): Note: The behaviour of the nodes and vpn client (as a test) has not changed, it still works as it used to. Obtaining ticketbooks also is unchanged + +- [Update `nym-vpn` metapackage and replace `nymvpn-x` with `nym-vpn-app`](https://github.com/nymtech/nym/pull/4889): Change dependency from `nymvpn-x` to `nym-vpn-app` to reflect the new package name of the tauri client + +- [Update network monitor entry point](https://github.com/nymtech/nym/pull/4893) + +- [Remove clippy github PR annotations](https://github.com/nymtech/nym/pull/4896): It eats up CI resources and time to run the clippy annotation checks that likely no one uses anyway. We keep the clippy checks of course. + +- [Fix clippy for beta toolchain](https://github.com/nymtech/nym/pull/4897): + +- [Update cargo deny](https://github.com/nymtech/nym/pull/4901): Update to use latest `cargo-deny`. Here are the steps done: + - Regenerate `deny.toml` + - Backport old settings to `deny.toml` + - Explicitly allow GPL-3 only on our own specific crates + - Update `deny.toml` for latest changes + - Fix `cargo-deny` warnings for duplicate crates + - Update `cargo-deny-action` to v2 + +- [Data Observatory stub](https://github.com/nymtech/nym/pull/4905): You need Postgres up for `sqlx` compile-time checked queries to work +~~~admonish example collapsible=true title='Try yourself' + +- Get [`page_up.sh` script](https://github.com/nymtech/nym/blob/develop/nym-data-observatory/pg_up.sh) + +```bash +./pg_up.sh +``` + +Play with the database: +```bash +docker exec -it nym-data-observatory-pg /bin/bash +psql -U youruser -d yourdb +``` +~~~ + +- [Proxy ffi](https://github.com/nymtech/nym/pull/4906): Updates Go & CPP FFI with the proxy code from [\#4743](https://github.com/nymtech/nym/pull/4743) + +- [Bump `http-api-client` default timeout to 30 sec](https://github.com/nymtech/nym/pull/4917) + +- [Check both version and type in message header](https://github.com/nymtech/nym/pull/4918) + +- [Fix argument to `cargo-deny` action](https://github.com/nymtech/nym/pull/4922) + +- [Expose error type](https://github.com/nymtech/nym/pull/4924) + +- [Make ip-packet-request VERSION pub](https://github.com/nymtech/nym/pull/4925) + +- [Assume offline mode](https://github.com/nymtech/nym/pull/4926) + +- [`nym-node`: don't use bloomfilters for double spending checks](https://github.com/nymtech/nym/pull/4960): this PR disables gateways polling for double spending bloomfilters and also `nym-apis` from providing this data. + +### Bugfix + +- [Fix `apt install` in `ci-build-upload-binaries.yml`](https://github.com/nymtech/nym/pull/4894) + +- [Fix missing duplication of modified tables](https://github.com/nymtech/nym/pull/4904) + +- [Fix nymvpn.com url in mainnet defaults](https://github.com/nymtech/nym/pull/4920): The old URL (nympvn.net) works since it is redirected to nymvpn.com, but the extra round-trip adds latency to all the API calls the vpn client does. So this PR should help speed things up, in particular when these API calls happen across the mixnet. + +- [Fix handle drop](https://github.com/nymtech/nym/pull/4934) + +- [Replace unreachable macro with an error return](https://github.com/nymtech/nym/pull/4958) + +### Operators Guide, Tooling & Updates + +#### Documentation Updates + +- [Update FAQ sphinx size](https://github.com/nymtech/nym/pull/4946): This PR upgrades url to our code base sphinx creation from an outdated branch to develop. + +#### Fast & Furious - WireGuard edition + +Nym team started another round of load and speed testing. This time the tests are limited to Wireguard mode Gateways - to find out any weak spots for needed improvement. The load testing is happening directly on mainnet as it simulates a real user traffic which the network components must be able to handle in order. + +Over past week we ran a total of three tests, with 450 clients at most. We've managed to push around 300 GB in total. Around 50% of requests failed. Over the course of those three tests, we did about 5000 requests, and bandwidth per client varies between 50Mb/s and 150Mb/s. + +We already caught two bugs and [fixed](https://github.com/nymtech/nym/pull/4885) it in this release. + +**The faster the operators upgrade to this [latest release](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2024.12-aero), the better**. A that will allow us to do more precise testing through the nodes without the registry bug, leading to more precise specs for `nym-node`. + +Here are the aims of these tests: + +1. Understanding of the wireguard network behavior under full load + - How many client users can all entry gateways and exit gateways handle simultaneously? + - How much sustained IP traffic can a subset of mainnet nodes sustain? +2. Needed improvements of Nym Node binaries to improve the throughput on mainnet +3. Measurement of required machine specs + - Releasing a new spec requirements +4. Raw data record +5. Increase quality of Nym Nodes + +Meanwhile we started to research pricing of stronger servers with unlimited bandwidth and higher (and stable) port speed, to arrive to a better understanding of needed rewards and grants to bootstrap the network before NymVPN launch. + +More info about testing and tools for performance monitoring can be found in [this chapter](testing/performance.md). + +> We would like to call out to operators to join the efforts and reach out to us if they know of solid ISPs who offer reliable dedicated services for good price or may even be interested in partnership. + +#### Delegation Program + +In October we again proceeded with our Delegation Program. 22 nodes didn't meet the program rules and got their delegation removed and 25 nodes from the que received delegation. Below is a complete list. + +~~~admonish example collapsible=true title='List of all delegation changes' +Delegated: +``` +Ce6kcPckNfQsga2z645VFQYadtoTjqXrS1YXMTtNNv98 +2XSCWy1vAoJRaYBJXx4KWwjU1cfoS2wNBXVQZvi8Jtdr +Bu4sUGjJqkje4vSncTH2KgrnojmfESdaYwamC6DbpJGZ +7TWEw9qQxsc8w4WhPAX6zjZ8vuNBdtP21zUVN8K26RkD +HejyqervmGTCEwi1JbRBXV5My463336huBn8ZgSpuhc3 +CXcCVGiamYSwgVwaxW3mEkXkZh1sKY2TXnWjjTjxDxzA +FScLfnKUPv9wSef3R4N2pQ9ft7DiwdivLW1i65Dqfc9L +2vuZZJjyYN27fvDbhyqeGosewGWaRh6iVsFtqbJoYAR7 +B9QiBsSAx7MRcTpYMs1fu9AFJurAZTPWMispHZXPbaVW +E3e2a9kXZjQXsKAfvmCf2WqwmVkiGR2LbjCwoadZgEJt +Dk4fCLM7idHPqfsUucLQtSMtYaYCLhi4T7vwvw88jG3P +9xZUp4sYWUNJesWy3MPVjh5kTorNqj3RxcFgBmYjV1xV +HK9QxPpdJfNtNpLJZHTN5M113jeBbFzTkMtPt9eouimx +ECkzyHfoiNGKyDTtbbH5HDCWa8KMGh92mtGbGHLZ3Y9n +9jQQV9vQ2mFFXywwVhACCKefjUFpyBoCU6KXNfjAEi45 +6QguhCfnDPKJe8bQXg9myuPB89yYFk6R77vMhLTbipK7 +4hAJJQhLTFve8FZGd28ksjavbch8STMax2rytzKmDPCV +EZLFq5HGXFKRpxu78nVjf7kuuUaKPLAbezR6mXbZrP6y +FtAAA5GMxY1Ge9wKYDrQgaSfJEUp4XvBLptBwy3GU8ap +tUiLPjz5nkPn5ZJT5ZXLPGDcZ3caQsfkMAp1epoAuSQ +4ScsM6AVowhKTMWaH98NLntKDwbu2ZMEycUk4mZiZppG +Hb34PTth6CeFziPAAEUMEjJFHWJg1dDex5QxUXKNqRBE +9ek1PMvLhpbwZe7kTMyCVY5VNqrdSPPoruFPQtbxnZyf +``` + +Undelegated due to the use of an outdated binary: +``` +9UHXFYuMLhuugndt8xCFRydmDPFyEEUHYc72tNANEtHp +5Y86A7fUX3LYVDDeoujtAiZFudYcHJq6gw8nsp71wN7U +HYWjn6yL8y7TBPFL9bTgDm6tHgyoEQupgJuBhLLoA5EY +4JCpbdhiQFKWwhrbkNDbwcwBGZnvU4WQrF2vqQLfmZvW +2f7JaYmmrMQQMczLX32ogfP7PBHeyPKbAVNjjEsExZVd +9TW55JrsFhsMoe3Tf8LBR4bPSCX86VXyvioMmCw9tWB +AyN34XqUi5XxgjmivWG2z6TftkqAFjVV5C9zCbx8Fvp5 +skNS4zNsKdbbUR9wFTJoPdmReW4NdrDEpp8512TNG4f +DztUnMKM545sdipgqhCsPNhK3YVmBbS2fp9HZgM5Jpw9 +GnLmx1s7g9nH3uLRhGpaXTbQEhCSKB6YenBQWQhthSx9 +GoJjAkH5hpcPYeW7JDUVfHdqgcufjwdhY2PLwBGJV3Ar +EdHVMTXpLiBbvCUnEoSPQ86pBNY1h9HtL34Q7cpNPWCy +``` + +Undelegated due to increased operation costs or profit margin: +``` +Erw9AQ4UJCgCiAWisUWbFk9Yedm8qvW4YQqmJRrBrE5p +BVDVtmNbZRgPKU81uBkrgfj5TnhtZqQcPAwxD48jcfMd +36nmH3kawhAsNA6sxFva2HgTnQHQDbcrRefvWWbmhHvY +2831fyXRAJ88x1Pd5aW7utw7WH1XkHZEfoWhLk2foLxJ +AMDS4cib433iRstwP9mWnZ4zPqb6hm6uPF7PpvhSkpYC +DE9eEeVsuiKeVfwebg5HYsebqRUvxd7LWsT9hQUtVrTQ +FAKhiQ8nW5sAWAxks1WB8u1MAWsapToCSE3KmF9LuGRQ +``` + +Undelegated due to being blacklisted for extensive period +``` +sjL9n9ymxfWWwkQJxXdsMkdwamXfh3AJ3vCe7rJ8RrT +E2HAJrHnk56QZDUCkcjc4i4pVEqtyuPYL5bNFYtweQuL +4PytR3tmodsvqGTKdY47yie8kmrkARQdb5Ht3Ro3ChH4 +``` +~~~ + +--- + +## `v2024.11-wedel` + +- [Release binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2024.11-wedel) +- [Release CHANGELOG.md](https://github.com/nymtech/nym/blob/nym-binaries-v2024.11-wedel/CHANGELOG.md) +- [`nym-node`](nodes/nym-node.md) version `1.1.8` + +```sh +Binary Name: nym-node +Build Timestamp: 2024-09-27T11:02:37.073944654Z +Build Version: 1.1.8 +Commit SHA: c3ec970a377adb25d57be5428551fada2ec55128 +Commit Date: 2024-09-26T08:24:53.000000000+02:00 +Commit Branch: master +rustc Version: 1.80.1 +rustc Channel: stable +cargo Profile: release +``` + +### Features + +- [New Network Monitor](https://github.com/nymtech/nym/pull/4610): Monitors the Nym network by sending itself packages across the mixnet. Network monitor is running two tokio tasks, one manages mixnet clients and another manages monitoring itself. Monitor is designed to be driven externally, via an `HTTP api`. This means that it does not do any monitoring unless driven by something like [`locust`](https://locust.io/). This allows us to tailor the load externally, potentially distributing it across multiple monitors. Includes a dockerised setup for automatically spinning up monitor and driving it with locust. + - *Note: NNM is not deployed on mainnet yet!* + +- [Add get_mixnodes_described to validator_client](https://github.com/nymtech/nym/pull/4725) + +- [Remove deprecated mark_as_success and use new disarm](https://github.com/nymtech/nym/pull/4751): Update function name to keep terminology consistent with tokio `CancellationToken DropGuard`. + +- [Update peer refresh value](https://github.com/nymtech/nym/pull/4754): `lso` expose the value by moving it to wireguard types, and separate the refresh time to the database sync time, so that more probable and needed actions happen faster (refresh) and more improbable ones don't overload the system (peer suspended or stale) +~~~admonish example collapsible=true title='Testing steps performed' +- **Noted** that the constants `DEFAULT_PEER_TIMEOUT` and `DEFAULT_PEER_TIMEOUT_CHECK` have been moved to `common/wireguard-types/src/lib.rs` and are now being used across modules for consistency +- **Observed** that the `peer_controller.rs` now separates the in-memory updates from the storage sync operations to reduce system load +- **Identified** that in-memory updates of peer bandwidth usage happen every `DEFAULT_PEER_TIMEOUT_CHECK` (every 5 seconds), while storage updates occur every 5 * `DEFAULT_PEER_TIMEOUT_CHECK` (every 25 seconds) + +**Checked System Load and Performance:** + +- **Monitored** system resource usage (CPU, memory, I/O) during the test to assess the impact of the changes +- **Confirmed** that the separation of in-memory updates and storage syncs resulted in reduced system load, particularly I/O operations, compared to previous versions where storage updates occurred more frequently +- **Ensured** that the system remained responsive and no performance bottlenecks were introduced + +- **Efficiency Improvement:** The separation of in-memory updates and storage syncs effectively reduced unnecessary database writes, improving system efficiency without compromising data accuracy +~~~ + +- [Remove duplicate stat count for retransmissions](https://github.com/nymtech/nym/pull/4756) + +- [Make gateway latency check generic](https://github.com/nymtech/nym/pull/4759): Replace concrete gateway type with trait in latency check, so we can make use of it in the vpn client. +~~~admonish example collapsible=true title='Testing steps performed' +- Initialised new `nym-client` with the `--latency-based-selection` flag and ensured it still works as normal. +~~~ + +- [chore: remove repetitive words](https://github.com/nymtech/nym/pull/4763) + +- [Avoid race on ip and registration structures](https://github.com/nymtech/nym/pull/4766): To avoid a state where the ip is being cleared out before the registration is also cleared out, couple the two structures under the same lock, since they are anyway very inter-dependent. +~~~admonish example collapsible=true title='Testing steps performed' +1. - **Checked out** the release/2024.10-wedel branch containing the fix for the race condition on IP and registration structures + - **Deployed** the on a controlled test environment to prevent interference + +2. **Monitored Logs:** + + - **Enabled** debug logging to capture all events + - **Monitored** logs in real-time to observe the handling of concurrent registration requests + - **Checked** for any error messages, warnings, or indications of race conditions + +3. **Verified Client Responses:** + + - Ensured that all clients received appropriate responses: + - Successful registration with assigned IP and registration data + - Appropriate error messages if no IPs were available or if other issues occurred + - Confirmed that no clients were left in an inconsistent state (e.g., assigned an IP but not fully registered) + +4. **Validated Normal Operation:** + - **Conducted standard registration processes** with individual clients to confirm that regular functionality is unaffected via `nym-vpn-cli` + - Ensured that authenticated clients could communicate over the network as expected +~~~ + +- [Persist used wireguard private IPs](https://github.com/nymtech/nym/pull/4771) + +- [Enable dependabot version upgrades for root rust workspace](https://github.com/nymtech/nym/pull/4778) + +- [Fix clippy for `unwrap_or_default`](https://github.com/nymtech/nym/pull/4783): Fix nightly build for [beta toolchain](https://github.com/nymtech/nym/actions/runs/10552082396/job/29230401668) + +- [Update dependabot](https://github.com/nymtech/nym/pull/4796): Bump max number of dependabot rust PRs to 10. Add readme entry to workspace package. + +- [Run `cargo-autoinherit` for a few new crates](https://github.com/nymtech/nym/pull/4801): Run cargo-autoinherit for a few new crates - Sort crates list. + +- [Add `axum` server to `nym-api`](https://github.com/nymtech/nym/pull/4803): Summary PR to add axum functionality behind a feature flag `axum`, alongside rocket. + +- [Remove unused wireguard flag from SDK](https://github.com/nymtech/nym/pull/4823) + +- [Expose wireguard details on self described endpoint](https://github.com/nymtech/nym/pull/4825) +~~~admonish example collapsible=true title='Testing steps performed' +Wireguard details are now visible at the nym-node endpoint `/api/v1/gateway/client-interfaces` as well as on the nym-api self-described endpoint `/api/v1/gateways/described`, above the existing data displaying mixnet_websocket information. + +An example of what will be shown is: +```json + "wireguard": { + "port": 51822, + "public_key": "" + } +``` +~~~ + +- [Revamped ticketbook serialisation and exposed additional cli methods](https://github.com/nymtech/nym/pull/4827): `wip` branch that includes changes needed for `vpn-api` alongside additional `ecash utils` +~~~admonish example collapsible=true title='Testing steps performed' +Checked the following commands: +```sh +show-ticket-books # which displays the information about all ticketbooks associated to the client +import-ticket-book # which imports a normal ticketbook to the client alongside `--full` flag +``` + +On the cli, the following were added: `import-coin-index-signatures`, `import-expiration-date-signatures` and `import-master-verification-key`. +~~~ + +- [Run cargo autoinherit following last weeks dependabot updates](https://github.com/nymtech/nym/pull/4831) + +- [Remove serde_crate named import](https://github.com/nymtech/nym/pull/4832) + +- [Create nym-repo-setup debian package and nym-vpn meta package](https://github.com/nymtech/nym/pull/4837): Create nym-repo-setup debian package that sets up the nymtech debian repo on the system it's installed on. It does 2 things: + + 1. Copy the keyring to `/usr/share/keyrings/nymtech.gpg` + 2. Copy the repo spec to `/etc/apt/sources.list.d/nymtech.list` + - Also create a meta package `nym-vpn` which only purpose is to depend on the daemon and UI. + +~~~admonish example collapsible=true title='Usage' +1. Install with +```sh +sudo dpkg -i ./nym-repo-setup.deb +``` +2. Once it's installed, it should be possible to install the vpn client with +```sh +sudo apt install nym-vpnc +``` +3. To reemove the repo, use +```sh +sudo apt remove nym-repo-setup +``` + +NOTE: removing the repo will not remove any installed nym-vpn packages +~~~ + +~~~admonish example collapsible=true title='Testing steps performed' + +1. **Downloaded** the `nym-repo-setup.deb` package to a Debian-based test system + +2. **Installed** the repository setup package using the command: +```bash +sudo dpkg -i ./nym-repo-setup.deb +``` + +3. **Verified** that the GPG keyring was copied to `/usr/share/keyrings/nymtech.gpg`: +```bash +ls -l /usr/share/keyrings/nymtech.gpg +``` + +4. **Checked** that the repository specification was added to `/etc/apt/sources.list.d/nymtech.list`: +```bash +cat /etc/apt/sources.list.d/nymtech.list +``` + + 5. **Updated** the package list: +```bash +sudo apt update +``` + +6. **Installed** the VPN client meta-package: +```bash +sudo apt install nym-vpnc +``` + +7. **Confirmed** that the `nym-vpnc` package and its dependencies (daemon and UI) were installed successfully + +8. **Tested** the VPN client to ensure it operates as expected + +9. **Removed** the repository setup package: +```bash +sudo apt remove nym-repo-setup +``` + +10. **Verified** that the repository specification file `/etc/apt/sources.list.d/nymtech.list` was removed + +11. **Ensured** that the installed `nym-vpnc` packages remained installed and functional after removing the repo setup package +~~~ + +- [Use ecash credential type for bandwidth value](https://github.com/nymtech/nym/pull/4840) + +- [Start switching over jobs to arc-ubuntu-20.04](https://github.com/nymtech/nym/pull/4843) + +~~~admonish example collapsible=true title='`ci-binary-config-checker`' +``` + - ci-build-upload-binaries + - ci-build + - ci-cargo-deny + - ci-contracts-schema + - ci-contracts-upload-binaries + - ci-contracts + - ci-docs + - ci-nym-wallet-rust + - ci-sdk-wasm +``` +~~~ + +- [Move credential verification into common crate](https://github.com/nymtech/nym/pull/4853) + +- [Revert runner for `ci-docs`](https://github.com/nymtech/nym/pull/4855) + +- [Remove `golang` workaround in `ci-sdk-wasm`](https://github.com/nymtech/nym/pull/4858) + +- [Fix linux conditional in `ci-build.yml`](https://github.com/nymtech/nym/pull/4863) + +- [Disable push trigger and add missing paths in `ci-build`](https://github.com/nymtech/nym/pull/4864) + +- [chore: removed completed queued mixnet migration](https://github.com/nymtech/nym/pull/4865) + +- [Bump defguard to github latest version](https://github.com/nymtech/nym/pull/4872) + +- [Backport #4894 to fix ci](https://github.com/nymtech/nym/pull/4899) + +### Bugfix + +- [Fix test failure in ipr request size](https://github.com/nymtech/nym/pull/4844): Nightly build started failing due to a unit test using `now()`, changing the serialized size. Fixed to use a fixed date. + +- [Fix clippy for nym-wallet and latest rustc](https://github.com/nymtech/nym/pull/4845) + +- [Allow updating globally stored signatures](https://github.com/nymtech/nym/pull/4891) + +- [Bugfix/ticketbook false double spending](https://github.com/nymtech/nym/pull/4892) +~~~admonish example collapsible=true title='Testing steps performed' +Tested running a client in mixnet mode, with a standard ticketbook, as well as a client using an imported ticketbook. The double spending bug is no longer an issue, bandwidth is consumed properly, and upon consumption of one ticket another ticket is properly obtained. +~~~ + +### Operators Guide, Tooling & Updates + +- [WSS setup guide updates](https://github.com/nymtech/nym/commit/05d6652177fb77324f8c38b3d8a547d07e729fec): Operators setting up WSS and reverse proxy on Gateways have now cleaner and simpler guide to configure their VPS. + +- [Updat hostname instruction for WSS](https://github.com/nymtech/nym/commit/7146c4c012ba7012dc74edc8510bbf377dc32fba): Adding a hostname instruction for clarity + ## `nym-node` patch from `release/2024.10-caramello` - [Patch release binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2024.10-caramello-patch) diff --git a/documentation/operators/src/faq/general-faq.md b/documentation/operators/src/faq/general-faq.md index df8587872b..8692d7a9b4 100644 --- a/documentation/operators/src/faq/general-faq.md +++ b/documentation/operators/src/faq/general-faq.md @@ -25,7 +25,7 @@ We don't recommend this setup because it's really difficult to get a static IP a ### What's the Sphinx packet size? -The sizes are shown in the configs [here](https://github.com/nymtech/nym/blob/1ba6444e722e7757f1175a296bed6e31e25b8db8/common/nymsphinx/params/src/packet_sizes.rs#L12) (default is the one clients use, the others are for research purposes, not to be used in production as this would fragment the anonymity set). More info can be found [here](https://github.com/nymtech/nym/blob/4844ac953a12b29fa27688609ec193f1d560c996/common/nymsphinx/anonymous-replies/src/reply_surb.rs#L80). +The sizes are shown in the configs [here](https://github.com/nymtech/nym/blob/develop/common/nymsphinx/params/src/packet_sizes.rs#L32) (default is the one clients use, the others are for research purposes, not to be used in production as this would fragment the anonymity set). More info can be found [here](https://github.com/nymtech/nym/blob/develop/common/nymsphinx/anonymous-replies/src/reply_surb.rs#L80). ### Why a Mix Node and a Gateway cannot be bonded with the same wallet? diff --git a/documentation/operators/src/faq/nym-nodes-faq.md b/documentation/operators/src/faq/nym-nodes-faq.md index d0a65b2ec0..325782a32e 100644 --- a/documentation/operators/src/faq/nym-nodes-faq.md +++ b/documentation/operators/src/faq/nym-nodes-faq.md @@ -2,6 +2,8 @@ ### What determines the rewards when running a `nym-node --mode mixnode`? +> **Visit [nymtech.net/about/token](https://nymtech.net/about/token) to find live information, graphs and dashboards about NYM token.** + The stake required for a Mix Node to achieve maximum rewards is called Mix Node saturation point. This is calculated from the staking supply (all circulating supply + part of unlocked tokens). The target level of staking is to have 40% of the staking supply locked in Mix Nodes. The node stake saturation point, which we denote by Nsat, is given by the stake supply, target level of staking divided between the rewarded nodes. @@ -20,13 +22,3 @@ The rewarded nodes are the nodes which will receive some rewards by the end of t For more detailed calculation, read our blog post [Nym Token Economics update](https://blog.nymtech.net/nym-token-economics-update-fedff0ed5267). More info on staking can be found [here](https://blog.nymtech.net/staking-in-nym-introducing-mainnet-mixmining-f9bb1cbc7c36). And [here](https://blog.nymtech.net/want-to-stake-in-nym-here-is-how-to-choose-a-mix-node-to-delegate-nym-to-c3b862add165) is more info on how to choose a Mix Node for delegation. And finally an [update](https://blog.nymtech.net/quarterly-token-economic-parameter-update-b2862948710f) on token economics from July 2023. - - - - - -*More graphs and stats at [stats.notrustverify.ch](https://status.notrustverify.ch/d/CW3L7dVVk/nym-mixnet?orgId=1&from=1703074861988&to=1705666862004).* - - diff --git a/envs/canary.env b/envs/canary.env index c4aac15082..84aa65eefa 100644 --- a/envs/canary.env +++ b/envs/canary.env @@ -18,6 +18,7 @@ GROUP_CONTRACT_ADDRESS=n1qg5ega6dykkxc307y25pecuufrjkxkaggkkxh7nad0vhyhtuhw3sa07 MULTISIG_CONTRACT_ADDRESS=n1zwv6feuzhy6a9wekh96cd57lsarmqlwxdypdsplw6zhfncqw6ftqx5a364 COCONUT_DKG_CONTRACT_ADDRESS=n1aakfpghcanxtc45gpqlx8j3rq0zcpyf49qmhm9mdjrfx036h4z5sy2vfh9 -EXPLORER_API=https://canary-explorer.performance.nymte.ch/api -NYXD="https://canary-validator.performance.nymte.ch" -NYM_API="https://canary-api.performance.nymte.ch/api" +EXPLORER_API=https://canary-explorer.performance.nymte.ch/api/ +NYXD=https://canary-validator.performance.nymte.ch +NYM_API=https://canary-api.performance.nymte.ch/api/ +NYM_VPN_API=https://nym-vpn-api-git-deploy-canary-nyx-network-staging.vercel.app/api/ diff --git a/envs/mainnet.env b/envs/mainnet.env index e40e54a471..8a2b1f61bf 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -21,8 +21,8 @@ COCONUT_DKG_CONTRACT_ADDRESS=n19604yflqggs9mk2z26mqygq43q2kr3n932egxx630svywd5mp REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" -NYXD="https://rpc.nymtech.net" -NYM_API="https://validator.nymtech.net/api/" +NYXD=https://rpc.nymtech.net +NYM_API=https://validator.nymtech.net/api/ NYXD_WS="wss://rpc.nymtech.net/websocket" -EXPLORER_API="https://explorer.nymtech.net/api/" -NYM_VPN_API="https://nymvpn.com/api" +EXPLORER_API=https://explorer.nymtech.net/api/ +NYM_VPN_API="https://nymvpn.com/api/" diff --git a/envs/qa.env b/envs/qa.env index e88f845416..81adaf299c 100644 --- a/envs/qa.env +++ b/envs/qa.env @@ -18,6 +18,7 @@ COCONUT_DKG_CONTRACT_ADDRESS=n1pk8jgr6y4c5k93gz7qf3xc0hvygmp7csk88c2tf8l39tkq683 VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8nnnvj REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39 -EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api -NYXD="https://qa-validator.qa.nymte.ch" -NYM_API="https://qa-nym-api.qa.nymte.ch/api" +EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api/ +NYXD=https://qa-validator.qa.nymte.ch +NYM_API=https://qa-nym-api.qa.nymte.ch/api/ +NYM_VPN_API=https://nym-vpn-api-git-deploy-qa-nyx-network-staging.vercel.app/api/ diff --git a/envs/sandbox.env b/envs/sandbox.env index 4763269a6f..3033312368 100644 --- a/envs/sandbox.env +++ b/envs/sandbox.env @@ -19,7 +19,8 @@ COCONUT_DKG_CONTRACT_ADDRESS=n1v3n2ly2dp3a9ng3ff6rh26yfkn0pc5hed7w2shc5u9ca5c865 ECASH_CONTRACT_ADDRESS=n1v3vydvs2ued84yv3khqwtgldmgwn0elljsdh08dr5s2j9x4rc5fs9jlwz9 STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" -EXPLORER_API=https://sandbox-explorer.nymtech.net/api -NYXD="https://rpc.sandbox.nymtech.net" -NYXD_WS="wss://rpc.sandbox.nymtech.net/websocket" -NYM_API="https://sandbox-nym-api1.nymtech.net/api" +EXPLORER_API=https://sandbox-explorer.nymtech.net/api/ +NYXD=https://rpc.sandbox.nymtech.net +NYXD_WS=wss://rpc.sandbox.nymtech.net/websocket/ +NYM_API=https://sandbox-nym-api1.nymtech.net/api/ +NYM_VPN_API=https://nym-vpn-api-git-deploy-sandbox-nyx-network-staging.vercel.app/api/ diff --git a/explorer-api/Cargo.toml b/explorer-api/Cargo.toml index 8e59935476..11f8672f98 100644 --- a/explorer-api/Cargo.toml +++ b/explorer-api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "explorer-api" -version = "1.1.41" +version = "1.1.42" edition = "2021" license.workspace = true diff --git a/explorer-api/explorer-api-requests/src/lib.rs b/explorer-api/explorer-api-requests/src/lib.rs index 2109348899..3ea6ed7200 100644 --- a/explorer-api/explorer-api-requests/src/lib.rs +++ b/explorer-api/explorer-api-requests/src/lib.rs @@ -1,6 +1,6 @@ use nym_api_requests::models::NodePerformance; use nym_contracts_common::Percent; -use nym_mixnet_contract_common::{Addr, Coin, Gateway, Layer, MixId, MixNode}; +use nym_mixnet_contract_common::{Addr, Coin, Gateway, LegacyMixLayer, MixNode, NodeId}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -14,13 +14,13 @@ pub enum MixnodeStatus { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] pub struct PrettyDetailedMixNodeBond { - pub mix_id: MixId, + pub mix_id: NodeId, pub location: Option, pub status: MixnodeStatus, pub pledge_amount: Coin, pub total_delegation: Coin, pub owner: Addr, - pub layer: Layer, + pub layer: LegacyMixLayer, pub mix_node: MixNode, pub stake_saturation: f32, pub uncapped_saturation: f32, diff --git a/explorer-api/explorer-client/Cargo.toml b/explorer-api/explorer-client/Cargo.toml index 2397c83f7b..d32429e9a1 100644 --- a/explorer-api/explorer-client/Cargo.toml +++ b/explorer-api/explorer-client/Cargo.toml @@ -7,12 +7,12 @@ license.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -log.workspace = true nym-explorer-api-requests = { path = "../explorer-api-requests" } reqwest = { workspace = true, features = ["json"] } serde.workspace = true thiserror.workspace = true url.workspace = true +tracing = {workspace = true, features = ["attributes"]} [dev-dependencies] tokio = { workspace = true, features = ["full"] } diff --git a/explorer-api/explorer-client/src/lib.rs b/explorer-api/explorer-client/src/lib.rs index 3ff4807b2b..999f23466b 100644 --- a/explorer-api/explorer-client/src/lib.rs +++ b/explorer-api/explorer-client/src/lib.rs @@ -3,6 +3,7 @@ use std::time::Duration; use reqwest::StatusCode; use thiserror::Error; +use tracing::instrument; use url::Url; // Re-export request types @@ -12,6 +13,8 @@ pub use nym_explorer_api_requests::{ // Paths const API_VERSION: &str = "v1"; +const TMP: &str = "tmp"; +const UNSTABLE: &str = "unstable"; const MIXNODES: &str = "mix-nodes"; const GATEWAYS: &str = "gateways"; @@ -47,6 +50,12 @@ impl ExplorerClient { Ok(Self { client, url }) } + #[cfg(not(target_arch = "wasm32"))] + pub fn new_with_timeout(url: url::Url, timeout: Duration) -> Result { + let client = reqwest::Client::builder().timeout(timeout).build()?; + Ok(Self { client, url }) + } + #[cfg(target_arch = "wasm32")] pub fn new(url: url::Url) -> Result { let client = reqwest::Client::builder().build()?; @@ -58,10 +67,11 @@ impl ExplorerClient { paths: &[&str], ) -> Result { let url = combine_url(self.url.clone(), paths)?; - log::trace!("Sending GET request {url:?}"); + tracing::debug!("Sending GET request"); Ok(self.client.get(url).send().await?) } + #[instrument(level = "trace", skip_all, fields(paths=?paths))] async fn query_explorer_api(&self, paths: &[&str]) -> Result where T: std::fmt::Debug, @@ -70,12 +80,14 @@ impl ExplorerClient { let response = self.send_get_request(paths).await?; if response.status().is_success() { let res = response.json::().await?; - log::trace!("Got response: {res:?}"); + tracing::trace!("Got response: {res:?}"); Ok(res) } else if response.status() == StatusCode::NOT_FOUND { Err(ExplorerApiError::NotFound) } else { - Err(ExplorerApiError::RequestFailure(response.text().await?)) + let status = response.status(); + let err_msg = format!("{}: {}", response.text().await?, status); + Err(ExplorerApiError::RequestFailure(err_msg)) } } @@ -86,6 +98,13 @@ impl ExplorerClient { pub async fn get_gateways(&self) -> Result, ExplorerApiError> { self.query_explorer_api(&[API_VERSION, GATEWAYS]).await } + + pub async fn unstable_get_gateways( + &self, + ) -> Result, ExplorerApiError> { + self.query_explorer_api(&[API_VERSION, TMP, UNSTABLE, GATEWAYS]) + .await + } } fn combine_url(mut base_url: Url, paths: &[&str]) -> Result { diff --git a/explorer-api/src/country_statistics/geolocate.rs b/explorer-api/src/country_statistics/geolocate.rs index 858f593986..18442fc6d7 100644 --- a/explorer-api/src/country_statistics/geolocate.rs +++ b/explorer-api/src/country_statistics/geolocate.rs @@ -4,6 +4,7 @@ use crate::state::ExplorerApiStateContext; use log::{info, warn}; use nym_explorer_api_requests::Location; +use nym_network_defaults::DEFAULT_NYM_NODE_HTTP_PORT; use nym_task::TaskClient; pub(crate) struct GeoLocateTask { @@ -25,6 +26,7 @@ impl GeoLocateTask { _ = interval_timer.tick() => { self.locate_mix_nodes().await; self.locate_gateways().await; + self.locate_nym_nodes().await; } _ = self.shutdown.recv() => { trace!("Listener: Received shutdown"); @@ -109,6 +111,83 @@ impl GeoLocateTask { trace!("All mix nodes located"); } + async fn locate_nym_nodes(&mut self) { + // I'm unwrapping to the default value to get rid of an extra indentation level from the `if let Some(...) = ...` + // If the value is None, we'll unwrap to an empty hashmap and the `values()` loop won't do any work anyway + let nym_nodes = self.state.inner.nymnodes.get_bonded_nymnodes().await; + + let geo_ip = self.state.inner.geo_ip.0.clone(); + + for (i, cache_item) in nym_nodes.values().enumerate() { + if self + .state + .inner + .nymnodes + .is_location_valid(cache_item.node_id()) + .await + { + // when the cached location is valid, don't locate and continue to next mix node + continue; + } + + let bonded_host = &cache_item.bond_information.node.host; + + match geo_ip.query( + bonded_host, + Some( + cache_item + .bond_information + .node + .custom_http_port + .unwrap_or(DEFAULT_NYM_NODE_HTTP_PORT), + ), + ) { + Ok(opt) => match opt { + Some(location) => { + let location: Location = location.into(); + + trace!( + "{} mix nodes already located. host {} is located in {:#?}", + i, + bonded_host, + location.three_letter_iso_country_code, + ); + + if i > 0 && (i % 100) == 0 { + info!("Located {} nym-nodes...", i + 1,); + } + + self.state + .inner + .nymnodes + .set_location(cache_item.node_id(), Some(location)) + .await; + + // one node has been located, so return out of the loop + return; + } + None => { + warn!("❌ Location for {bonded_host} not found."); + self.state + .inner + .nymnodes + .set_location(cache_item.node_id(), None) + .await; + } + }, + Err(_e) => { + // warn!( + // "❌ Oh no! Location for {} failed. Error: {:#?}", + // cache_item.mix_node().host, + // e + // ); + } + }; + } + + trace!("All nym-nodes nodes located"); + } + async fn locate_gateways(&mut self) { let gateways = self.state.inner.gateways.get_gateways().await; diff --git a/explorer-api/src/http/mod.rs b/explorer-api/src/http/mod.rs index 559093671e..2d4e2d9fab 100644 --- a/explorer-api/src/http/mod.rs +++ b/explorer-api/src/http/mod.rs @@ -10,6 +10,7 @@ use crate::gateways::http::gateways_make_default_routes; use crate::http::swagger::get_docs; use crate::mix_node::http::mix_node_make_default_routes; use crate::mix_nodes::http::mix_nodes_make_default_routes; +use crate::nym_nodes::http::unstable_temp_nymnodes_make_default_routes; use crate::overview::http::overview_make_default_routes; use crate::ping::http::ping_make_default_routes; use crate::service_providers::http::service_providers_make_default_routes; @@ -58,6 +59,7 @@ fn configure_rocket(state: ExplorerApiStateContext) -> Rocket { "/ping" => ping_make_default_routes(&openapi_settings), "/validators" => validators_make_default_routes(&openapi_settings), "/service-providers" => service_providers_make_default_routes(&openapi_settings), + "/tmp/unstable" => unstable_temp_nymnodes_make_default_routes(&openapi_settings), }; building_rocket diff --git a/explorer-api/src/main.rs b/explorer-api/src/main.rs index 8cb98280af..ecaae3f492 100644 --- a/explorer-api/src/main.rs +++ b/explorer-api/src/main.rs @@ -22,6 +22,7 @@ mod http; mod location; mod mix_node; pub(crate) mod mix_nodes; +mod nym_nodes; mod overview; mod ping; pub(crate) mod service_providers; diff --git a/explorer-api/src/mix_node/delegations.rs b/explorer-api/src/mix_node/delegations.rs index feaefcf5c0..fc9420c88c 100644 --- a/explorer-api/src/mix_node/delegations.rs +++ b/explorer-api/src/mix_node/delegations.rs @@ -4,12 +4,12 @@ use super::models::SummedDelegations; use crate::client::ThreadsafeValidatorClient; use itertools::Itertools; -use nym_mixnet_contract_common::{Delegation, MixId}; +use nym_mixnet_contract_common::{Delegation, NodeId}; use nym_validator_client::nyxd::contract_traits::PagedMixnetQueryClient; pub(crate) async fn get_single_mixnode_delegations( client: &ThreadsafeValidatorClient, - mix_id: MixId, + mix_id: NodeId, ) -> Vec { match client .0 @@ -27,7 +27,7 @@ pub(crate) async fn get_single_mixnode_delegations( pub(crate) async fn get_single_mixnode_delegations_summed( client: &ThreadsafeValidatorClient, - mix_id: MixId, + mix_id: NodeId, ) -> Vec { let delegations_by_owner = get_single_mixnode_delegations(client, mix_id) .await diff --git a/explorer-api/src/mix_node/econ_stats.rs b/explorer-api/src/mix_node/econ_stats.rs index 0b1c901a40..3fe054bb5d 100644 --- a/explorer-api/src/mix_node/econ_stats.rs +++ b/explorer-api/src/mix_node/econ_stats.rs @@ -5,12 +5,15 @@ use crate::client::ThreadsafeValidatorClient; use crate::helpers::best_effort_small_dec_to_f64; use crate::mix_node::models::EconomicDynamicsStats; use nym_contracts_common::truncate_decimal; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_validator_client::client::NymApiClientExt; +// use deprecated method as hopefully this whole API will be sunset soon-enough... +// and we're only getting info for legacy node so the relevant data should still exist +#[allow(deprecated)] pub(crate) async fn retrieve_mixnode_econ_stats( client: &ThreadsafeValidatorClient, - mix_id: MixId, + mix_id: NodeId, ) -> Option { let stake_saturation = client .0 diff --git a/explorer-api/src/mix_node/http.rs b/explorer-api/src/mix_node/http.rs index e00ed8ff82..2a44097275 100644 --- a/explorer-api/src/mix_node/http.rs +++ b/explorer-api/src/mix_node/http.rs @@ -10,7 +10,7 @@ use crate::mix_node::models::{ }; use crate::state::ExplorerApiStateContext; use nym_explorer_api_requests::PrettyDetailedMixNodeBond; -use nym_mixnet_contract_common::{Delegation, MixId}; +use nym_mixnet_contract_common::{Delegation, NodeId}; use reqwest::Error as ReqwestError; use rocket::response::status::NotFound; use rocket::serde::json::Json; @@ -47,7 +47,7 @@ async fn get_mix_node_stats(host: &str, port: u16) -> Result")] pub(crate) async fn get_by_id( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Result, NotFound> { match state.inner.mixnodes.get_detailed_mixnode(mix_id).await { @@ -59,7 +59,7 @@ pub(crate) async fn get_by_id( #[openapi(tag = "mix_node")] #[get("//delegations")] pub(crate) async fn get_delegations( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Json> { Json(get_single_mixnode_delegations(&state.inner.validator_client, mix_id).await) @@ -68,7 +68,7 @@ pub(crate) async fn get_delegations( #[openapi(tag = "mix_node")] #[get("//delegations/summed")] pub(crate) async fn get_delegations_summed( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Json> { Json(get_single_mixnode_delegations_summed(&state.inner.validator_client, mix_id).await) @@ -77,7 +77,7 @@ pub(crate) async fn get_delegations_summed( #[openapi(tag = "mix_node")] #[get("//description")] pub(crate) async fn get_description( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Option> { match state.inner.mixnode.clone().get_description(mix_id).await { @@ -125,7 +125,7 @@ pub(crate) async fn get_description( #[openapi(tag = "mix_node")] #[get("//stats")] pub(crate) async fn get_stats( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Option> { match state.inner.mixnode.get_node_stats(mix_id).await { @@ -170,7 +170,7 @@ pub(crate) async fn get_stats( #[openapi(tag = "mix_node")] #[get("//economic-dynamics-stats")] pub(crate) async fn get_economic_dynamics_stats( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Option> { match state.inner.mixnode.get_econ_stats(mix_id).await { diff --git a/explorer-api/src/mix_node/models.rs b/explorer-api/src/mix_node/models.rs index 4adfa6dd86..d7b79ad71a 100644 --- a/explorer-api/src/mix_node/models.rs +++ b/explorer-api/src/mix_node/models.rs @@ -3,7 +3,7 @@ use crate::cache::Cache; use nym_mixnet_contract_common::Delegation; -use nym_mixnet_contract_common::{Addr, Coin, MixId}; +use nym_mixnet_contract_common::{Addr, Coin, NodeId}; use nym_validator_client::models::SelectionChance; use serde::Deserialize; use serde::Serialize; @@ -14,7 +14,7 @@ use tokio::sync::RwLock; #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, JsonSchema)] pub struct SummedDelegations { pub owner: Addr, - pub mix_id: MixId, + pub mix_id: NodeId, pub amount: Coin, } @@ -40,9 +40,9 @@ impl SummedDelegations { } pub(crate) struct MixNodeCache { - pub(crate) descriptions: Cache, - pub(crate) node_stats: Cache, - pub(crate) econ_stats: Cache, + pub(crate) descriptions: Cache, + pub(crate) node_stats: Cache, + pub(crate) econ_stats: Cache, } #[derive(Clone)] @@ -61,19 +61,19 @@ impl ThreadsafeMixNodeCache { } } - pub(crate) async fn get_description(&self, mix_id: MixId) -> Option { + pub(crate) async fn get_description(&self, mix_id: NodeId) -> Option { self.inner.read().await.descriptions.get(&mix_id) } - pub(crate) async fn get_node_stats(&self, mix_id: MixId) -> Option { + pub(crate) async fn get_node_stats(&self, mix_id: NodeId) -> Option { self.inner.read().await.node_stats.get(&mix_id) } - pub(crate) async fn get_econ_stats(&self, mix_id: MixId) -> Option { + pub(crate) async fn get_econ_stats(&self, mix_id: NodeId) -> Option { self.inner.read().await.econ_stats.get(&mix_id) } - pub(crate) async fn set_description(&self, mix_id: MixId, description: NodeDescription) { + pub(crate) async fn set_description(&self, mix_id: NodeId, description: NodeDescription) { self.inner .write() .await @@ -81,11 +81,11 @@ impl ThreadsafeMixNodeCache { .set(mix_id, description); } - pub(crate) async fn set_node_stats(&self, mix_id: MixId, node_stats: NodeStats) { + pub(crate) async fn set_node_stats(&self, mix_id: NodeId, node_stats: NodeStats) { self.inner.write().await.node_stats.set(mix_id, node_stats); } - pub(crate) async fn set_econ_stats(&self, mix_id: MixId, econ_stats: EconomicDynamicsStats) { + pub(crate) async fn set_econ_stats(&self, mix_id: NodeId, econ_stats: EconomicDynamicsStats) { self.inner.write().await.econ_stats.set(mix_id, econ_stats); } } @@ -147,11 +147,11 @@ fn get_common_owner(delegations: &[Delegation]) -> Option { Some(owner) } -fn get_common_mix_id(delegations: &[Delegation]) -> Option { - let mix_id = delegations.iter().next()?.mix_id; +fn get_common_mix_id(delegations: &[Delegation]) -> Option { + let mix_id = delegations.iter().next()?.node_id; if delegations .iter() - .any(|delegation| delegation.mix_id != mix_id) + .any(|delegation| delegation.node_id != mix_id) { log::warn!("Unexpected different node identities when summing delegations"); return None; diff --git a/explorer-api/src/mix_nodes/location.rs b/explorer-api/src/mix_nodes/location.rs index de3c2e2fd6..4fdd1714d4 100644 --- a/explorer-api/src/mix_nodes/location.rs +++ b/explorer-api/src/mix_nodes/location.rs @@ -1,8 +1,8 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use crate::location::LocationCache; -pub(crate) type MixnodeLocationCache = LocationCache; +pub(crate) type MixnodeLocationCache = LocationCache; diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs index fe5ae7fde8..4ffd99304f 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -1,24 +1,20 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::collections::{HashMap, HashSet}; -use std::sync::Arc; -use std::time::{Duration, SystemTime}; - +use super::location::MixnodeLocationCache; +use crate::helpers::best_effort_small_dec_to_f64; +use crate::location::LocationCacheItem; +use crate::mix_nodes::CACHE_ENTRY_TTL; use nym_explorer_api_requests::{Location, MixnodeStatus, PrettyDetailedMixNodeBond}; use nym_mixnet_contract_common::rewarding::helpers::truncate_reward; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; +use nym_validator_client::models::MixNodeBondAnnotated; use serde::Serialize; +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; +use std::time::{Duration, SystemTime}; use tokio::sync::{RwLock, RwLockReadGuard}; -use crate::helpers::best_effort_small_dec_to_f64; -use crate::location::LocationCacheItem; -use nym_validator_client::models::MixNodeBondAnnotated; - -use super::location::MixnodeLocationCache; -use super::utils::family_numerical_id; -use crate::mix_nodes::CACHE_ENTRY_TTL; - #[derive(Clone, Debug, Serialize, JsonSchema)] pub(crate) struct MixNodeActiveSetSummary { pub active: usize, @@ -35,9 +31,9 @@ pub(crate) struct MixNodeSummary { #[derive(Clone, Debug)] pub(crate) struct MixNodesResult { pub(crate) valid_until: SystemTime, - pub(crate) all_mixnodes: HashMap, - active_mixnodes: HashSet, - rewarded_mixnodes: HashSet, + pub(crate) all_mixnodes: HashMap, + active_mixnodes: HashSet, + rewarded_mixnodes: HashSet, } impl MixNodesResult { @@ -50,7 +46,7 @@ impl MixNodesResult { } } - fn determine_node_status(&self, mix_id: MixId) -> MixnodeStatus { + fn determine_node_status(&self, mix_id: NodeId) -> MixnodeStatus { if self.active_mixnodes.contains(&mix_id) { MixnodeStatus::Active } else if self.rewarded_mixnodes.contains(&mix_id) { @@ -64,7 +60,7 @@ impl MixNodesResult { self.valid_until >= SystemTime::now() } - fn get_mixnode(&self, mix_id: MixId) -> Option { + fn get_mixnode(&self, mix_id: NodeId) -> Option { if self.is_valid() { self.all_mixnodes.get(&mix_id).cloned() } else { @@ -72,7 +68,7 @@ impl MixNodesResult { } } - fn get_mixnodes(&self) -> Option> { + fn get_mixnodes(&self) -> Option> { if self.is_valid() { Some(self.all_mixnodes.clone()) } else { @@ -102,7 +98,7 @@ impl ThreadsafeMixNodesCache { } } - pub(crate) async fn is_location_valid(&self, mix_id: MixId) -> bool { + pub(crate) async fn is_location_valid(&self, mix_id: NodeId) -> bool { self.locations .read() .await @@ -116,7 +112,7 @@ impl ThreadsafeMixNodesCache { self.locations.read().await.clone() } - pub(crate) async fn set_location(&self, mix_id: MixId, location: Option) { + pub(crate) async fn set_location(&self, mix_id: NodeId, location: Option) { // cache the location for this mix node so that it can be used when the mix node list is refreshed self.locations .write() @@ -124,25 +120,23 @@ impl ThreadsafeMixNodesCache { .insert(mix_id, LocationCacheItem::new_from_location(location)); } - pub(crate) async fn get_mixnode(&self, mix_id: MixId) -> Option { + pub(crate) async fn get_mixnode(&self, mix_id: NodeId) -> Option { self.mixnodes.read().await.get_mixnode(mix_id) } - pub(crate) async fn get_mixnodes(&self) -> Option> { + pub(crate) async fn get_mixnodes(&self) -> Option> { self.mixnodes.read().await.get_mixnodes() } fn create_detailed_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, mixnodes_guard: &RwLockReadGuard<'_, MixNodesResult>, location: Option<&LocationCacheItem>, node: &MixNodeBondAnnotated, ) -> PrettyDetailedMixNodeBond { - let denom = &node.mixnode_details.original_pledge().denom; let rewarding_info = &node.mixnode_details.rewarding_details; - - let family_id = node.family.as_ref().map(family_numerical_id); + let denom = &rewarding_info.cost_params.interval_operating_cost.denom; PrettyDetailedMixNodeBond { mix_id, @@ -162,14 +156,14 @@ impl ThreadsafeMixNodesCache { estimated_delegators_apy: best_effort_small_dec_to_f64(node.estimated_delegators_apy), operating_cost: rewarding_info.cost_params.interval_operating_cost.clone(), profit_margin_percent: rewarding_info.cost_params.profit_margin_percent, - family_id, + family_id: None, blacklisted: node.blacklisted, } } pub(crate) async fn get_detailed_mixnode( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Option { let mixnodes_guard = self.mixnodes.read().await; let location_guard = self.locations.read().await; @@ -197,8 +191,8 @@ impl ThreadsafeMixNodesCache { pub(crate) async fn update_cache( &self, all_bonds: Vec, - rewarded_nodes: HashSet, - active_nodes: HashSet, + rewarded_nodes: HashSet, + active_nodes: HashSet, ) { let mut guard = self.mixnodes.write().await; guard.all_mixnodes = all_bonds diff --git a/explorer-api/src/mix_nodes/utils.rs b/explorer-api/src/mix_nodes/utils.rs index 5273d66f55..2742fd2415 100644 --- a/explorer-api/src/mix_nodes/utils.rs +++ b/explorer-api/src/mix_nodes/utils.rs @@ -1,13 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use isocountry::CountryCode; -use nym_mixnet_contract_common::families::FamilyHead; -use rand::Rng; -use rand_pcg::Pcg64; -use rand_seeder::Seeder; - use crate::location::GeoLocation; +use isocountry::CountryCode; #[allow(dead_code)] pub(crate) fn map_2_letter_to_3_letter_country_code(geo: &GeoLocation) -> String { @@ -22,10 +17,3 @@ pub(crate) fn map_2_letter_to_3_letter_country_code(geo: &GeoLocation) -> String } } } - -// We don't need numerical IDs anywhere, so to avoid modifying the contract storage again and -// since this is for explorer ergonomics, it will generate a deterministic random u16 based on the family Identity. -pub(crate) fn family_numerical_id(fh: &FamilyHead) -> u16 { - let mut rng: Pcg64 = Seeder::from(fh.identity()).make_rng(); - rng.gen() -} diff --git a/explorer-api/src/nym_nodes/http.rs b/explorer-api/src/nym_nodes/http.rs new file mode 100644 index 0000000000..378fcc78d7 --- /dev/null +++ b/explorer-api/src/nym_nodes/http.rs @@ -0,0 +1,26 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::state::ExplorerApiStateContext; +use nym_explorer_api_requests::PrettyDetailedGatewayBond; +use okapi::openapi3::OpenApi; +use rocket::serde::json::Json; +use rocket::{Route, State}; +use rocket_okapi::settings::OpenApiSettings; + +pub fn unstable_temp_nymnodes_make_default_routes( + settings: &OpenApiSettings, +) -> (Vec, OpenApi) { + openapi_get_routes_spec![settings: all_gateways] +} + +#[openapi(tag = "UNSTABLE")] +#[get("/gateways")] +pub(crate) async fn all_gateways( + state: &State, +) -> Json> { + let mut gateways = state.inner.gateways.get_detailed_gateways().await; + gateways.append(&mut state.inner.nymnodes.pretty_gateways().await); + + Json(gateways) +} diff --git a/explorer-api/src/nym_nodes/location.rs b/explorer-api/src/nym_nodes/location.rs new file mode 100644 index 0000000000..134023f3ca --- /dev/null +++ b/explorer-api/src/nym_nodes/location.rs @@ -0,0 +1,8 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use nym_mixnet_contract_common::NodeId; + +use crate::location::LocationCache; + +pub(crate) type NymNodeLocationCache = LocationCache; diff --git a/explorer-api/src/nym_nodes/mod.rs b/explorer-api/src/nym_nodes/mod.rs new file mode 100644 index 0000000000..2ceb85885a --- /dev/null +++ b/explorer-api/src/nym_nodes/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::time::Duration; + +pub(crate) mod http; +pub(crate) mod location; +pub(crate) mod models; + +pub(crate) const CACHE_ENTRY_TTL: Duration = Duration::from_secs(1200); diff --git a/explorer-api/src/nym_nodes/models.rs b/explorer-api/src/nym_nodes/models.rs new file mode 100644 index 0000000000..7cdbbbdee3 --- /dev/null +++ b/explorer-api/src/nym_nodes/models.rs @@ -0,0 +1,154 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::location::{LocationCache, LocationCacheItem}; +use crate::nym_nodes::location::NymNodeLocationCache; +use crate::nym_nodes::CACHE_ENTRY_TTL; +use nym_explorer_api_requests::{Location, PrettyDetailedGatewayBond}; +use nym_mixnet_contract_common::{Gateway, NodeId, NymNodeDetails}; +use nym_validator_client::models::NymNodeDescription; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::{Duration, SystemTime}; +use tokio::sync::{RwLock, RwLockReadGuard}; + +pub(crate) struct NymNodesCache { + pub(crate) valid_until: SystemTime, + pub(crate) bonded_nym_nodes: HashMap, + pub(crate) described_nodes: HashMap, +} + +impl NymNodesCache { + fn new() -> Self { + NymNodesCache { + valid_until: SystemTime::now() - Duration::from_secs(60), // in the past + bonded_nym_nodes: Default::default(), + described_nodes: Default::default(), + } + } + + // fn is_valid(&self) -> bool { + // self.valid_until >= SystemTime::now() + // } +} + +#[derive(Clone)] +pub(crate) struct ThreadSafeNymNodesCache { + nymnodes: Arc>, + locations: Arc>>, +} + +impl ThreadSafeNymNodesCache { + pub(crate) fn new() -> Self { + ThreadSafeNymNodesCache { + nymnodes: Arc::new(RwLock::new(NymNodesCache::new())), + locations: Arc::new(RwLock::new(NymNodeLocationCache::new())), + } + } + + pub(crate) fn new_with_location_cache(locations: NymNodeLocationCache) -> Self { + ThreadSafeNymNodesCache { + nymnodes: Arc::new(RwLock::new(NymNodesCache::new())), + locations: Arc::new(RwLock::new(locations)), + } + } + + pub(crate) async fn is_location_valid(&self, node_id: NodeId) -> bool { + self.locations + .read() + .await + .get(&node_id) + .map_or(false, |cache_item| { + cache_item.valid_until > SystemTime::now() + }) + } + + pub(crate) async fn get_bonded_nymnodes( + &self, + ) -> RwLockReadGuard> { + let guard = self.nymnodes.read().await; + RwLockReadGuard::map(guard, |n| &n.bonded_nym_nodes) + } + + pub(crate) async fn get_locations(&self) -> NymNodeLocationCache { + self.locations.read().await.clone() + } + + pub(crate) async fn set_location(&self, node_id: NodeId, location: Option) { + // cache the location for this mix node so that it can be used when the mix node list is refreshed + self.locations + .write() + .await + .insert(node_id, LocationCacheItem::new_from_location(location)); + } + + pub(crate) async fn update_cache( + &self, + all_bonds: Vec, + descriptions: Vec, + ) { + let mut guard = self.nymnodes.write().await; + guard.bonded_nym_nodes = all_bonds + .into_iter() + .map(|details| (details.node_id(), details)) + .collect(); + guard.described_nodes = descriptions + .into_iter() + .map(|description| (description.node_id, description)) + .collect(); + + guard.valid_until = SystemTime::now() + CACHE_ENTRY_TTL; + } + + pub(crate) async fn pretty_gateways(&self) -> Vec { + let nodes_guard = self.nymnodes.read().await; + let location_guard = self.locations.read().await; + + let mut pretty_gateways = vec![]; + + for (node_id, native_nymnode) in &nodes_guard.bonded_nym_nodes { + let Some(description) = nodes_guard.described_nodes.get(node_id) else { + continue; + }; + + if description.description.declared_role.entry { + let location = location_guard.get(node_id); + let bond = &native_nymnode.bond_information; + + pretty_gateways.push(PrettyDetailedGatewayBond { + pledge_amount: bond.original_pledge.clone(), + owner: bond.owner.clone(), + block_height: bond.bonding_height, + gateway: Gateway { + host: bond.node.host.clone(), + mix_port: description.description.mix_port(), + clients_port: description.description.mixnet_websockets.ws_port, + location: description + .description + .auxiliary_details + .location + .as_ref() + .map(|l| l.to_string()) + .unwrap_or_default(), + sphinx_key: description + .description + .host_information + .keys + .x25519 + .to_base58_string(), + identity_key: bond.node.identity_key.clone(), + version: description + .description + .build_information + .build_version + .clone(), + }, + proxy: None, + location: location.and_then(|l| l.location.clone()), + }) + } + } + + pretty_gateways + } +} diff --git a/explorer-api/src/ping/http.rs b/explorer-api/src/ping/http.rs index c3cd4107f1..22486f6608 100644 --- a/explorer-api/src/ping/http.rs +++ b/explorer-api/src/ping/http.rs @@ -1,7 +1,7 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_mixnet_contract_common::{MixId, MixNode}; +use nym_mixnet_contract_common::{MixNode, NodeId}; use rocket::serde::json::Json; use rocket::{Route, State}; use rocket_okapi::okapi::openapi3::OpenApi; @@ -23,7 +23,7 @@ pub fn ping_make_default_routes(settings: &OpenApiSettings) -> (Vec, Open #[openapi(tag = "ping")] #[get("/")] pub(crate) async fn index( - mix_id: MixId, + mix_id: NodeId, state: &State, ) -> Option> { match state.inner.ping.clone().get(mix_id).await { diff --git a/explorer-api/src/ping/models.rs b/explorer-api/src/ping/models.rs index 9aebf7d5fd..10d6d832ce 100644 --- a/explorer-api/src/ping/models.rs +++ b/explorer-api/src/ping/models.rs @@ -2,12 +2,12 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, SystemTime}; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use serde::Deserialize; use serde::Serialize; use tokio::sync::RwLock; -pub(crate) type PingCache = HashMap; +pub(crate) type PingCache = HashMap; const PING_TTL: Duration = Duration::from_secs(60 * 5); // 5 mins, before port check will be re-tried (only while pending) const CACHE_TTL: Duration = Duration::from_secs(60 * 60); // 1 hour, to cache result from port check @@ -24,7 +24,7 @@ impl ThreadsafePingCache { } } - pub(crate) async fn get(&self, mix_id: MixId) -> Option { + pub(crate) async fn get(&self, mix_id: NodeId) -> Option { self.inner .read() .await @@ -44,7 +44,7 @@ impl ThreadsafePingCache { }) } - pub(crate) async fn set_pending(&self, mix_id: MixId) { + pub(crate) async fn set_pending(&self, mix_id: NodeId) { self.inner.write().await.insert( mix_id, PingCacheItem { @@ -55,7 +55,7 @@ impl ThreadsafePingCache { ); } - pub(crate) async fn set(&self, mix_id: MixId, item: PingResponse) { + pub(crate) async fn set(&self, mix_id: NodeId, item: PingResponse) { self.inner.write().await.insert( mix_id, PingCacheItem { diff --git a/explorer-api/src/state.rs b/explorer-api/src/state.rs index 8ecee92ae2..28373aef80 100644 --- a/explorer-api/src/state.rs +++ b/explorer-api/src/state.rs @@ -3,7 +3,7 @@ use std::path::Path; use chrono::{DateTime, Utc}; use log::info; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use serde::{Deserialize, Serialize}; use crate::client::ThreadsafeValidatorClient; @@ -18,6 +18,8 @@ use crate::gateways::models::ThreadsafeGatewayCache; use crate::mix_node::models::ThreadsafeMixNodeCache; use crate::mix_nodes::location::MixnodeLocationCache; use crate::mix_nodes::models::ThreadsafeMixNodesCache; +use crate::nym_nodes::location::NymNodeLocationCache; +use crate::nym_nodes::models::ThreadSafeNymNodesCache; use crate::ping::models::ThreadsafePingCache; use crate::validators::models::ThreadsafeValidatorCache; @@ -30,6 +32,7 @@ pub struct ExplorerApiState { pub(crate) gateways: ThreadsafeGatewayCache, pub(crate) mixnode: ThreadsafeMixNodeCache, pub(crate) mixnodes: ThreadsafeMixNodesCache, + pub(crate) nymnodes: ThreadSafeNymNodesCache, pub(crate) ping: ThreadsafePingCache, pub(crate) validators: ThreadsafeValidatorCache, pub(crate) geo_ip: ThreadsafeGeoIp, @@ -39,7 +42,7 @@ pub struct ExplorerApiState { } impl ExplorerApiState { - pub(crate) async fn get_mix_node(&self, mix_id: MixId) -> Option { + pub(crate) async fn get_mix_node(&self, mix_id: NodeId) -> Option { self.mixnodes.get_mixnode(mix_id).await } } @@ -49,6 +52,7 @@ pub struct ExplorerApiStateOnDisk { pub(crate) country_node_distribution: CountryNodesDistribution, pub(crate) mixnode_location_cache: MixnodeLocationCache, pub(crate) gateway_location_cache: GatewayLocationCache, + pub(crate) nymnode_location_cache: NymNodeLocationCache, pub(crate) as_at: DateTime, } @@ -85,6 +89,9 @@ impl ExplorerApiStateContext { mixnodes: ThreadsafeMixNodesCache::new_with_location_cache( state.mixnode_location_cache, ), + nymnodes: ThreadSafeNymNodesCache::new_with_location_cache( + state.nymnode_location_cache, + ), ping: ThreadsafePingCache::new(), validators: ThreadsafeValidatorCache::new(), validator_client: ThreadsafeValidatorClient::new(), @@ -101,6 +108,7 @@ impl ExplorerApiStateContext { gateways: ThreadsafeGatewayCache::new(), mixnode: ThreadsafeMixNodeCache::new(), mixnodes: ThreadsafeMixNodesCache::new(), + nymnodes: ThreadSafeNymNodesCache::new(), ping: ThreadsafePingCache::new(), validators: ThreadsafeValidatorCache::new(), validator_client: ThreadsafeValidatorClient::new(), @@ -117,6 +125,7 @@ impl ExplorerApiStateContext { country_node_distribution: self.inner.country_node_distribution.get_all().await, mixnode_location_cache: self.inner.mixnodes.get_locations().await, gateway_location_cache: self.inner.gateways.get_locations().await, + nymnode_location_cache: self.inner.nymnodes.get_locations().await, as_at: Utc::now(), }; serde_json::to_writer(file, &state).expect("error writing state to disk"); diff --git a/explorer-api/src/tasks.rs b/explorer-api/src/tasks.rs index b0b9a9701b..fa43bfc9e4 100644 --- a/explorer-api/src/tasks.rs +++ b/explorer-api/src/tasks.rs @@ -1,22 +1,24 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_mixnet_contract_common::GatewayBond; +use crate::mix_nodes::CACHE_REFRESH_RATE; +use crate::state::ExplorerApiStateContext; +use nym_mixnet_contract_common::{GatewayBond, NymNodeDetails}; use nym_task::TaskClient; -use nym_validator_client::models::MixNodeBondAnnotated; +use nym_validator_client::models::{MixNodeBondAnnotated, NymNodeDescription}; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::{Paging, TendermintRpcClient, ValidatorResponse}; use nym_validator_client::{QueryHttpRpcValidatorClient, ValidatorClientError}; use std::future::Future; - -use crate::mix_nodes::CACHE_REFRESH_RATE; -use crate::state::ExplorerApiStateContext; +use tokio::time::MissedTickBehavior; pub(crate) struct ExplorerApiTasks { state: ExplorerApiStateContext, shutdown: TaskClient, } +// allow usage of deprecated methods here as we actually want to be explicitly querying for legacy data +#[allow(deprecated)] impl ExplorerApiTasks { pub(crate) fn new(state: ExplorerApiStateContext, shutdown: TaskClient) -> Self { ExplorerApiTasks { state, shutdown } @@ -28,18 +30,39 @@ impl ExplorerApiTasks { F: FnOnce(&'a QueryHttpRpcValidatorClient) -> Fut, Fut: Future, ValidatorClientError>>, { - let bonds = match f(&self.state.inner.validator_client.0).await { - Ok(result) => result, - Err(err) => { + let bonds = f(&self.state.inner.validator_client.0) + .await + .unwrap_or_else(|err| { error!("Unable to retrieve mixnode bonds: {err}"); vec![] - } - }; + }); info!("Fetched {} mixnode bonds", bonds.len()); bonds } + async fn retrieve_bonded_nymnodes(&self) -> Result, ValidatorClientError> { + info!("About to retrieve all nymnode bonds..."); + self.state + .inner + .validator_client + .0 + .get_all_cached_bonded_nym_nodes() + .await + } + + async fn retrieve_node_descriptions( + &self, + ) -> Result, ValidatorClientError> { + info!("About to retrieve node descriptions..."); + self.state + .inner + .validator_client + .0 + .get_all_cached_described_nodes() + .await + } + async fn retrieve_all_mixnodes(&self) -> Vec { info!("About to retrieve all mixnode bonds..."); self.retrieve_mixnodes( @@ -131,10 +154,33 @@ impl ExplorerApiTasks { } } + async fn update_nymnodes_cache(&self) { + let nym_node_bonds = self.retrieve_bonded_nymnodes().await.unwrap_or_else(|err| { + error!("failed to retrieve nym node bonds: {err}"); + Vec::new() + }); + + let all_descriptions = self + .retrieve_node_descriptions() + .await + .unwrap_or_else(|err| { + error!("failed to retrieve node descriptions: {err}"); + Vec::new() + }); + + self.state + .inner + .nymnodes + .update_cache(nym_node_bonds, all_descriptions) + .await + } + pub(crate) fn start(mut self) { info!("Spawning mix nodes task runner..."); tokio::spawn(async move { let mut interval_timer = tokio::time::interval(CACHE_REFRESH_RATE); + interval_timer.set_missed_tick_behavior(MissedTickBehavior::Skip); + while !self.shutdown.is_shutdown() { tokio::select! { _ = interval_timer.tick() => { @@ -148,6 +194,10 @@ impl ExplorerApiTasks { info!("Updating mix node cache..."); self.update_mixnode_cache().await; + + info!("Updating nymnode cache..."); + self.update_nymnodes_cache().await; + info!("Done"); } _ = self.shutdown.recv() => { trace!("Listener: Received shutdown"); diff --git a/explorer/src/utils/index.ts b/explorer/src/utils/index.ts index 5375377b5a..91b082c5ca 100644 --- a/explorer/src/utils/index.ts +++ b/explorer/src/utils/index.ts @@ -102,7 +102,6 @@ export const isLessThan = (a: number, b: number) => a < b; */ export const isBalanceEnough = (fee: string, tx: string = '0', balance: string = '0') => { - console.log('balance', balance, fee, tx); try { return Big(balance).gte(Big(fee).plus(Big(tx))); } catch (e) { diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index cbdcfc1ad2..8c6014aef6 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -41,15 +41,9 @@ rand = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } si-scale = { workspace = true } -sqlx = { workspace = true, features = [ - "runtime-tokio-rustls", - "sqlite", - "macros", - "migrate", - "time", -] } subtle-encoding = { workspace = true, features = ["bech32-preview"] } thiserror = { workspace = true } +time = { workspace = true } tokio = { workspace = true, features = [ "rt-multi-thread", "net", @@ -82,8 +76,11 @@ nym-network-defaults = { path = "../common/network-defaults" } nym-network-requester = { path = "../service-providers/network-requester" } nym-node-http-api = { path = "../nym-node/nym-node-http-api" } nym-pemstore = { path = "../common/pemstore" } +nym-sdk = { path = "../sdk/rust/nym-sdk" } nym-sphinx = { path = "../common/nymsphinx" } +nym-statistics-common = { path = "../common/statistics" } nym-task = { path = "../common/task" } +nym-topology = { path = "../common/topology" } nym-types = { path = "../common/types" } nym-validator-client = { path = "../common/client-libs/validator-client" } nym-ip-packet-router = { path = "../service-providers/ip-packet-router" } diff --git a/gateway/src/http/mod.rs b/gateway/src/http/mod.rs index 72d0aa499d..a3a697d56f 100644 --- a/gateway/src/http/mod.rs +++ b/gateway/src/http/mod.rs @@ -40,9 +40,9 @@ fn load_host_details( ip_address: config.host.public_ips.clone(), hostname: config.host.hostname.clone(), keys: api_requests::v1::node::models::HostKeys { - ed25519_identity: identity_keypair.public_key().to_base58_string(), - x25519_sphinx: sphinx_key.to_base58_string(), - x25519_noise: "".to_string(), + ed25519_identity: *identity_keypair.public_key(), + x25519_sphinx: *sphinx_key, + x25519_noise: None, }, }; diff --git a/gateway/src/node/client_handling/active_clients.rs b/gateway/src/node/client_handling/active_clients.rs index 962765067e..82cdf98dc3 100644 --- a/gateway/src/node/client_handling/active_clients.rs +++ b/gateway/src/node/client_handling/active_clients.rs @@ -5,6 +5,8 @@ use super::websocket::message_receiver::{IsActiveRequestSender, MixMessageSender use crate::node::client_handling::embedded_clients::LocalEmbeddedClientHandle; use dashmap::DashMap; use nym_sphinx::DestinationAddressBytes; +use nym_statistics_common::events; +use nym_statistics_common::events::StatsEventSender; use std::sync::Arc; use tracing::warn; @@ -35,6 +37,7 @@ impl ActiveClient { #[derive(Clone)] pub(crate) struct ActiveClientsStore { inner: Arc>, + stats_event_sender: StatsEventSender, } #[derive(Clone)] @@ -48,9 +51,10 @@ pub(crate) struct ClientIncomingChannels { impl ActiveClientsStore { /// Creates new instance of `ActiveClientsStore` to store in-memory handles to all currently connected clients. - pub(crate) fn new() -> Self { + pub(crate) fn new(stats_event_sender: StatsEventSender) -> Self { ActiveClientsStore { inner: Arc::new(DashMap::new()), + stats_event_sender, } } @@ -126,6 +130,12 @@ impl ActiveClientsStore { /// * `client`: address of the client for which to remove the handle. pub(crate) fn disconnect(&self, client: DestinationAddressBytes) { self.inner.remove(&client); + if let Err(e) = self + .stats_event_sender + .unbounded_send(events::StatsEvent::new_session_stop(client)) + { + warn!("Failed to send session stop event to collector : {e}") + }; } /// Insert new client handle into the store. @@ -147,6 +157,12 @@ impl ActiveClientsStore { if self.inner.insert(client, entry).is_some() { panic!("inserted a duplicate remote client") } + if let Err(e) = self + .stats_event_sender + .unbounded_send(events::StatsEvent::new_session_start(client)) + { + warn!("Failed to send session start event to collector : {e}") + }; } /// Inserts a handle to the embedded client diff --git a/gateway/src/node/client_handling/websocket/common_state.rs b/gateway/src/node/client_handling/websocket/common_state.rs index 2120b33422..a07f0e768d 100644 --- a/gateway/src/node/client_handling/websocket/common_state.rs +++ b/gateway/src/node/client_handling/websocket/common_state.rs @@ -3,6 +3,7 @@ use nym_credential_verification::{ecash::EcashManager, BandwidthFlushingBehaviourConfig}; use nym_crypto::asymmetric::identity; +use nym_statistics_common::events::StatsEventSender; use std::sync::Arc; // I can see this being possible expanded with say storage or client store @@ -13,4 +14,5 @@ pub(crate) struct CommonHandlerState { pub(crate) local_identity: Arc, pub(crate) only_coconut_credentials: bool, pub(crate) bandwidth_cfg: BandwidthFlushingBehaviourConfig, + pub(crate) stats_event_sender: StatsEventSender, } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs index c15a273022..80d99a8890 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/authenticated.rs @@ -18,6 +18,7 @@ use nym_credential_verification::CredentialVerifier; use nym_credential_verification::{ bandwidth_storage_manager::BandwidthStorageManager, ClientBandwidth, }; +use nym_credentials_interface::TicketType; use nym_gateway_requests::{ types::{BinaryRequest, ServerResponse}, ClientControlRequest, ClientRequest, GatewayRequestsError, SensitiveServerResponse, @@ -25,6 +26,7 @@ use nym_gateway_requests::{ }; use nym_gateway_storage::{error::StorageError, Storage}; use nym_sphinx::forwarding::packet::MixPacket; +use nym_statistics_common::events; use nym_task::TaskClient; use nym_validator_client::coconut::EcashApiError; use rand::{random, CryptoRng, Rng}; @@ -243,6 +245,7 @@ where &self.client.shared_keys, iv, )?; + let maybe_ticket_type = TicketType::try_from_encoded(credential.data.payment.t_type); let mut verifier = CredentialVerifier::new( credential, self.inner.shared_state.ecash_verifier.clone(), @@ -255,6 +258,16 @@ where .inspect_err(|verification_failure| debug!("{verification_failure}"))?; trace!("available total bandwidth: {available_total}"); + if let Ok(ticket_type) = maybe_ticket_type { + if let Err(e) = self.inner.shared_state.stats_event_sender.unbounded_send( + events::StatsEvent::new_ecash_ticket(self.client.address, ticket_type), + ) { + warn!("Failed to send session stop event to collector : {e}") + }; + } else { + error!("Somehow verified a ticket with an unknown ticket type"); + } + Ok(ServerResponse::Bandwidth { available_total }) } diff --git a/gateway/src/node/helpers.rs b/gateway/src/node/helpers.rs index 854254e215..8823a8e931 100644 --- a/gateway/src/node/helpers.rs +++ b/gateway/src/node/helpers.rs @@ -3,13 +3,18 @@ use crate::config::Config; use crate::error::GatewayError; - +use async_trait::async_trait; use nym_crypto::asymmetric::encryption; use nym_gateway_storage::PersistentStorage; use nym_pemstore::traits::PemStorableKeyPair; use nym_pemstore::KeyPairPath; - +use nym_sdk::{NymApiTopologyProvider, NymApiTopologyProviderConfig, UserAgent}; +use nym_topology::{gateway, NymTopology, TopologyProvider}; use std::path::Path; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::debug; +use url::Url; pub async fn load_network_requester_config>( id: &str, @@ -93,3 +98,56 @@ pub(crate) fn load_sphinx_keys(config: &Config) -> Result>, +} + +impl GatewayTopologyProvider { + pub fn new( + gateway_node: gateway::LegacyNode, + user_agent: UserAgent, + nym_api_url: Vec, + ) -> GatewayTopologyProvider { + GatewayTopologyProvider { + inner: Arc::new(Mutex::new(GatewayTopologyProviderInner { + inner: NymApiTopologyProvider::new( + NymApiTopologyProviderConfig { + min_mixnode_performance: 50, + min_gateway_performance: 0, + }, + nym_api_url, + env!("CARGO_PKG_VERSION").to_string(), + Some(user_agent), + ), + gateway_node, + })), + } + } +} + +struct GatewayTopologyProviderInner { + inner: NymApiTopologyProvider, + gateway_node: gateway::LegacyNode, +} + +#[async_trait] +impl TopologyProvider for GatewayTopologyProvider { + async fn get_new_topology(&mut self) -> Option { + let mut guard = self.inner.lock().await; + match guard.inner.get_new_topology().await { + None => None, + Some(mut base) => { + if !base.gateway_exists(&guard.gateway_node.identity_key) { + debug!( + "{} didn't exist in topology. inserting it.", + guard.gateway_node.identity_key + ); + base.insert_gateway(guard.gateway_node.clone()); + } + Some(base) + } + } + } +} diff --git a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs index 4ee18c77d5..5eb0b0c136 100644 --- a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs +++ b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs @@ -8,10 +8,10 @@ use futures::channel::mpsc::SendError; use futures::StreamExt; use nym_gateway_storage::{error::StorageError, Storage}; use nym_mixnet_client::forwarder::MixForwardingSender; -use nym_mixnode_common::packet_processor::processor::ProcessedFinalHop; use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::framing::codec::NymCodec; use nym_sphinx::framing::packet::FramedNymPacket; +use nym_sphinx::framing::processing::ProcessedFinalHop; use nym_sphinx::DestinationAddressBytes; use nym_task::TaskClient; use std::collections::HashMap; @@ -21,6 +21,8 @@ use tokio::net::TcpStream; use tokio_util::codec::Framed; use tracing::*; +use super::packet_processing::process_packet; + // defines errors that warrant a panic if not thrown in the context of a shutdown #[derive(Debug, Error)] enum CriticalPacketProcessingError { @@ -184,14 +186,14 @@ impl ConnectionHandler { // question: can it also be per connection vs global? // - let processed_final_hop = match self.packet_processor.process_received(framed_sphinx_packet) - { - Err(err) => { - debug!("We failed to process received sphinx packet - {err}"); - return Ok(()); - } - Ok(processed_final_hop) => processed_final_hop, - }; + let processed_final_hop = + match process_packet(framed_sphinx_packet, self.packet_processor.sphinx_key()) { + Err(err) => { + debug!("We failed to process received sphinx packet - {err}"); + return Ok(()); + } + Ok(processed_final_hop) => processed_final_hop, + }; self.handle_processed_packet(processed_final_hop).await } diff --git a/gateway/src/node/mixnet_handling/receiver/packet_processing.rs b/gateway/src/node/mixnet_handling/receiver/packet_processing.rs index 7ac90bb174..4f16c2451f 100644 --- a/gateway/src/node/mixnet_handling/receiver/packet_processing.rs +++ b/gateway/src/node/mixnet_handling/receiver/packet_processing.rs @@ -3,18 +3,24 @@ use nym_crypto::asymmetric::encryption; use nym_mixnode_common::packet_processor::error::MixProcessingError; -pub use nym_mixnode_common::packet_processor::processor::MixProcessingResult; -use nym_mixnode_common::packet_processor::processor::{ProcessedFinalHop, SphinxPacketProcessor}; +use nym_mixnode_common::packet_processor::processor::SphinxPacketProcessor; use nym_sphinx::framing::packet::FramedNymPacket; +use nym_sphinx::framing::processing::{ + process_framed_packet, MixProcessingResult, PacketProcessingError, ProcessedFinalHop, +}; +use nym_sphinx::PrivateKey; use thiserror::Error; #[derive(Error, Debug)] pub enum GatewayProcessingError { #[error("failed to process received mix packet - {0}")] - PacketProcessingError(#[from] MixProcessingError), + PacketProcessing(#[from] MixProcessingError), #[error("received a forward hop mix packet")] - ForwardHopReceivedError, + ForwardHopReceived, + + #[error("failed to process received sphinx packet: {0}")] + NymPacketProcessing(#[from] PacketProcessingError), } // PacketProcessor contains all data required to correctly unwrap and store sphinx packets @@ -24,21 +30,23 @@ pub struct PacketProcessor { } impl PacketProcessor { + pub fn sphinx_key(&self) -> &PrivateKey { + self.inner_processor.sphinx_key() + } + pub(crate) fn new(encryption_key: &encryption::PrivateKey) -> Self { PacketProcessor { inner_processor: SphinxPacketProcessor::new(encryption_key.into()), } } +} - pub(crate) fn process_received( - &self, - received: FramedNymPacket, - ) -> Result { - match self.inner_processor.process_received(received)? { - MixProcessingResult::ForwardHop(..) => { - Err(GatewayProcessingError::ForwardHopReceivedError) - } - MixProcessingResult::FinalHop(processed_final) => Ok(processed_final), - } +pub(crate) fn process_packet( + received: FramedNymPacket, + sphinx_key: &nym_sphinx::PrivateKey, +) -> Result { + match process_framed_packet(received, sphinx_key)? { + MixProcessingResult::ForwardHop(..) => Err(GatewayProcessingError::ForwardHopReceived), + MixProcessingResult::FinalHop(processed_final) => Ok(processed_final), } } diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index bf5c3f0ab0..86c07a84cb 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -12,9 +12,12 @@ use crate::http::HttpApiBuilder; use crate::node::client_handling::active_clients::ActiveClientsStore; use crate::node::client_handling::embedded_clients::{LocalEmbeddedClientHandle, MessageRouter}; use crate::node::client_handling::websocket; -use crate::node::helpers::{initialise_main_storage, load_network_requester_config}; +use crate::node::helpers::{ + initialise_main_storage, load_network_requester_config, GatewayTopologyProvider, +}; use crate::node::mixnet_handling::receiver::connection_handler::ConnectionHandler; use futures::channel::{mpsc, oneshot}; +use nym_bin_common::bin_info; use nym_credential_verification::ecash::{ credential_sender::CredentialHandlerConfig, EcashManager, }; @@ -22,13 +25,18 @@ use nym_crypto::asymmetric::{encryption, identity}; use nym_mixnet_client::forwarder::{MixForwardingSender, PacketForwarder}; use nym_network_defaults::NymNetworkDetails; use nym_network_requester::{LocalGateway, NRServiceProviderBuilder, RequestFilter}; +use nym_node_http_api::state::metrics::SharedSessionStats; +use nym_statistics_common::events::{self, StatsEventSender}; use nym_task::{TaskClient, TaskHandle, TaskManager}; +use nym_topology::NetworkAddress; use nym_types::gateway::GatewayNodeDetailsResponse; +use nym_validator_client::client::NodeId; use nym_validator_client::nyxd::{Coin, CosmWasmClient}; use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient}; use rand::seq::SliceRandom; use rand::thread_rng; -use std::net::SocketAddr; +use statistics::GatewayStatisticsCollector; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; use std::sync::Arc; use tracing::*; @@ -36,6 +44,7 @@ use tracing::*; pub(crate) mod client_handling; pub(crate) mod helpers; pub(crate) mod mixnet_handling; +pub(crate) mod statistics; pub use nym_gateway_storage::{PersistentStorage, Storage}; @@ -147,6 +156,8 @@ pub struct Gateway { wireguard_data: Option, + session_stats: Option, + run_http_server: bool, task_client: Option, } @@ -168,6 +179,7 @@ impl Gateway { ip_packet_router_opts, authenticator_opts: None, wireguard_data: None, + session_stats: None, run_http_server: true, task_client: None, }) @@ -191,6 +203,7 @@ impl Gateway { sphinx_keypair, storage, wireguard_data: None, + session_stats: None, run_http_server: true, task_client: None, } @@ -204,6 +217,10 @@ impl Gateway { self.task_client = Some(task_client) } + pub fn set_session_stats(&mut self, session_stats: SharedSessionStats) { + self.session_stats = Some(session_stats); + } + pub fn set_wireguard_data(&mut self, wireguard_data: nym_wireguard::WireguardData) { self.wireguard_data = Some(wireguard_data) } @@ -213,6 +230,39 @@ impl Gateway { crate::helpers::node_details(&self.config).await } + fn gateway_topology_provider(&self) -> GatewayTopologyProvider { + GatewayTopologyProvider::new( + self.as_topology_node(), + bin_info!().into(), + self.config.gateway.nym_api_urls.clone(), + ) + } + + fn as_topology_node(&self) -> nym_topology::gateway::LegacyNode { + let ip = self + .config + .host + .public_ips + .first() + .copied() + .unwrap_or(IpAddr::V4(Ipv4Addr::LOCALHOST)); + let mix_host = SocketAddr::new(ip, self.config.gateway.mix_port); + + nym_topology::gateway::LegacyNode { + // those fields are irrelevant for the purposes of routing so it's fine if they're inaccurate. + // the only thing that matters is the identity key (and maybe version) + node_id: NodeId::MAX, + mix_host, + host: NetworkAddress::IpAddr(ip), + clients_ws_port: self.config.gateway.clients_port, + clients_wss_port: self.config.gateway.clients_wss_port, + sphinx_key: *self.sphinx_keypair.public_key(), + + identity_key: *self.identity_keypair.public_key(), + version: env!("CARGO_PKG_VERSION").into(), + } + } + fn start_mix_socket_listener( &self, ack_sender: MixForwardingSender, @@ -245,6 +295,7 @@ impl Gateway { async fn start_authenticator( &mut self, forwarding_channel: MixForwardingSender, + topology_provider: GatewayTopologyProvider, shutdown: TaskClient, ecash_verifier: Arc>, ) -> Result> @@ -292,6 +343,7 @@ impl Gateway { .with_shutdown(shutdown.fork("authenticator")) .with_wait_for_gateway(true) .with_minimum_gateway_performance(0) + .with_custom_topology_provider(Box::new(topology_provider)) .with_on_start(on_start_tx); if let Some(custom_mixnet) = &opts.custom_mixnet_path { @@ -340,6 +392,7 @@ impl Gateway { async fn start_authenticator( &self, _forwarding_channel: MixForwardingSender, + _topology_provider: GatewayTopologyProvider, _shutdown: TaskClient, _ecash_verifier: Arc>, ) -> Result> { @@ -352,6 +405,7 @@ impl Gateway { active_clients_store: ActiveClientsStore, shutdown: TaskClient, ecash_verifier: Arc>, + stats_event_sender: StatsEventSender, ) where St: Storage + Send + Sync + Clone + 'static, { @@ -368,6 +422,7 @@ impl Gateway { local_identity: Arc::clone(&self.identity_keypair), only_coconut_credentials: self.config.gateway.only_coconut_credentials, bandwidth_cfg: (&self.config).into(), + stats_event_sender, }; websocket::Listener::new(listening_address, shared_state).start( @@ -393,10 +448,24 @@ impl Gateway { packet_sender } + fn start_stats_collector( + &self, + shared_session_stats: SharedSessionStats, + shutdown: TaskClient, + ) -> events::StatsEventSender { + info!("Starting gateway stats collector..."); + + let (mut stats_collector, stats_event_sender) = + GatewayStatisticsCollector::new(shared_session_stats); + tokio::spawn(async move { stats_collector.run(shutdown).await }); + stats_event_sender + } + // TODO: rethink the logic in this function... async fn start_network_requester( &self, forwarding_channel: MixForwardingSender, + topology_provider: GatewayTopologyProvider, shutdown: TaskClient, ) -> Result { info!("Starting network requester..."); @@ -424,6 +493,7 @@ impl Gateway { .with_custom_gateway_transceiver(Box::new(transceiver)) .with_wait_for_gateway(true) .with_minimum_gateway_performance(0) + .with_custom_topology_provider(Box::new(topology_provider)) .with_on_start(on_start_tx); if let Some(custom_mixnet) = &nr_opts.custom_mixnet_path { @@ -461,6 +531,7 @@ impl Gateway { async fn start_ip_packet_router( &self, forwarding_channel: MixForwardingSender, + topology_provider: GatewayTopologyProvider, shutdown: TaskClient, ) -> Result { info!("Starting IP packet provider..."); @@ -489,6 +560,7 @@ impl Gateway { .with_custom_gateway_transceiver(Box::new(transceiver)) .with_wait_for_gateway(true) .with_minimum_gateway_performance(0) + .with_custom_topology_provider(Box::new(topology_provider)) .with_on_start(on_start_tx); if let Some(custom_mixnet) = &ip_opts.custom_mixnet_path { @@ -550,7 +622,7 @@ impl Gateway { // TODO: if anything, this should be getting data directly from the contract // as opposed to the validator API let validator_client = self.random_api_client()?; - let existing_nodes = match validator_client.get_cached_gateways().await { + let existing_nodes = match validator_client.get_all_basic_nodes(None).await { Ok(nodes) => nodes, Err(err) => { error!("failed to grab initial network gateways - {err}\n Please try to startup again in few minutes"); @@ -558,9 +630,9 @@ impl Gateway { } }; - Ok(existing_nodes.iter().any(|node| { - node.gateway.identity_key == self.identity_keypair.public_key().to_base58_string() - })) + Ok(existing_nodes + .iter() + .any(|node| &node.ed25519_identity_pubkey == self.identity_keypair.public_key())) } pub async fn run(mut self) -> Result<(), GatewayError> @@ -599,6 +671,13 @@ impl Gateway { return Err(GatewayError::InsufficientNodeBalance { account, balance }); } } + let shared_session_stats = self.session_stats.take().unwrap_or_default(); + let stats_event_sender = self.start_stats_collector( + shared_session_stats, + shutdown.fork("statistics::GatewayStatisticsCollector"), + ); + + let topology_provider = self.gateway_topology_provider(); let handler_config = CredentialHandlerConfig { revocation_bandwidth_penalty: self @@ -629,7 +708,7 @@ impl Gateway { let mix_forwarding_channel = self.start_packet_forwarder(shutdown.fork("PacketForwarder")); - let active_clients_store = ActiveClientsStore::new(); + let active_clients_store = ActiveClientsStore::new(stats_event_sender.clone()); self.start_mix_socket_listener( mix_forwarding_channel.clone(), active_clients_store.clone(), @@ -641,12 +720,14 @@ impl Gateway { active_clients_store.clone(), shutdown.fork("websocket::Listener"), ecash_verifier.clone(), + stats_event_sender.clone(), ); let nr_request_filter = if self.config.network_requester.enabled { let embedded_nr = self .start_network_requester( mix_forwarding_channel.clone(), + topology_provider.clone(), shutdown.fork("NetworkRequester"), ) .await?; @@ -662,6 +743,7 @@ impl Gateway { let embedded_ip_sp = self .start_ip_packet_router( mix_forwarding_channel.clone(), + topology_provider.clone(), shutdown.fork("ip_service_provider"), ) .await?; @@ -674,6 +756,7 @@ impl Gateway { let embedded_auth = self .start_authenticator( mix_forwarding_channel, + topology_provider, shutdown.fork("authenticator"), ecash_verifier, ) diff --git a/gateway/src/node/statistics/mod.rs b/gateway/src/node/statistics/mod.rs new file mode 100644 index 0000000000..53ea277db5 --- /dev/null +++ b/gateway/src/node/statistics/mod.rs @@ -0,0 +1,63 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use futures::{channel::mpsc, StreamExt}; +use nym_node_http_api::state::metrics::SharedSessionStats; +use nym_statistics_common::events::{StatsEvent, StatsEventReceiver, StatsEventSender}; +use nym_task::TaskClient; +use sessions::SessionStatsHandler; +use std::time::Duration; +use time::OffsetDateTime; +use tracing::trace; + +pub mod sessions; + +const STATISTICS_UPDATE_TIMER_INTERVAL: Duration = Duration::from_secs(3600); //update timer, no need to check everytime + +pub(crate) struct GatewayStatisticsCollector { + stats_event_rx: StatsEventReceiver, + session_stats: SessionStatsHandler, + //here goes additionnal stats handler +} + +impl GatewayStatisticsCollector { + pub fn new( + shared_session_stats: SharedSessionStats, + ) -> (GatewayStatisticsCollector, StatsEventSender) { + let (stats_event_tx, stats_event_rx) = mpsc::unbounded(); + let collector = GatewayStatisticsCollector { + stats_event_rx, + session_stats: SessionStatsHandler::new(shared_session_stats), + }; + (collector, stats_event_tx) + } + + async fn update_shared_state(&mut self, update_time: OffsetDateTime) { + self.session_stats.update_shared_state(update_time).await; + //here goes additionnal stats handler update + } + + pub async fn run(&mut self, mut shutdown: TaskClient) { + let mut update_interval = tokio::time::interval(STATISTICS_UPDATE_TIMER_INTERVAL); + while !shutdown.is_shutdown() { + tokio::select! { + biased; + _ = shutdown.recv() => { + trace!("StatisticsCollector: Received shutdown"); + }, + _ = update_interval.tick() => { + let now = OffsetDateTime::now_utc(); + self.update_shared_state(now).await; + }, + + Some(stat_event) = self.stats_event_rx.next() => { + //dispatching event to proper handler + match stat_event { + StatsEvent::SessionStatsEvent(event) => self.session_stats.handle_event(event), + } + }, + + } + } + } +} diff --git a/gateway/src/node/statistics/sessions.rs b/gateway/src/node/statistics/sessions.rs new file mode 100644 index 0000000000..2853f75ad5 --- /dev/null +++ b/gateway/src/node/statistics/sessions.rs @@ -0,0 +1,177 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_credentials_interface::TicketType; +use nym_node_http_api::state::metrics::SharedSessionStats; +use nym_sphinx::DestinationAddressBytes; +use std::collections::{HashMap, HashSet}; +use time::{Date, Duration, OffsetDateTime}; + +use nym_statistics_common::events::SessionEvent; + +const FINISHED_SESSIONS_CAP: usize = 1_000_000; //to be on the safe side of memory blowups until persistent storage + +#[derive(PartialEq)] +enum SessionType { + Vpn, + Mixnet, + Unknown, +} + +impl SessionType { + fn to_string(&self) -> &str { + match self { + Self::Vpn => "vpn", + Self::Mixnet => "mixnet", + Self::Unknown => "unknown", + } + } +} + +impl From for SessionType { + fn from(value: TicketType) -> Self { + match value { + TicketType::V1MixnetEntry => Self::Mixnet, + TicketType::V1MixnetExit => Self::Mixnet, + TicketType::V1WireguardEntry => Self::Vpn, + TicketType::V1WireguardExit => Self::Vpn, + } + } +} + +struct FinishedSession { + duration: Duration, + typ: SessionType, +} + +impl FinishedSession { + fn serialize(&self) -> (u64, String) { + ( + self.duration.whole_milliseconds() as u64, //we are sure that it fits in a u64, see `fn end_at` + self.typ.to_string().into(), + ) + } +} + +struct ActiveSession { + start: OffsetDateTime, + typ: SessionType, +} + +impl ActiveSession { + fn new(start_time: OffsetDateTime) -> Self { + ActiveSession { + start: start_time, + typ: SessionType::Unknown, + } + } + + fn set_type(&mut self, ticket_type: TicketType) { + self.typ = ticket_type.into(); + } + + fn end_at(self, stop_time: OffsetDateTime) -> Option { + let session_duration = stop_time - self.start; + //ensure duration is positive to fit in a u64 + //u64::max milliseconds is 500k millenia so no overflow issue + if session_duration > Duration::ZERO { + Some(FinishedSession { + duration: session_duration, + typ: self.typ, + }) + } else { + None + } + } +} + +pub(crate) struct SessionStatsHandler { + last_update_day: Date, + + shared_session_stats: SharedSessionStats, + active_sessions: HashMap, + unique_users: HashSet, + sessions_started: u32, + finished_sessions: Vec, +} + +impl SessionStatsHandler { + pub fn new(shared_session_stats: SharedSessionStats) -> Self { + SessionStatsHandler { + last_update_day: OffsetDateTime::now_utc().date(), + shared_session_stats, + active_sessions: Default::default(), + unique_users: Default::default(), + sessions_started: 0, + finished_sessions: Default::default(), + } + } + + pub(crate) fn handle_event(&mut self, event: SessionEvent) { + match event { + SessionEvent::SessionStart { start_time, client } => { + self.handle_session_start(start_time, client); + } + SessionEvent::SessionStop { stop_time, client } => { + self.handle_session_stop(stop_time, client); + } + SessionEvent::EcashTicket { + ticket_type, + client, + } => self.handle_ecash_ticket(ticket_type, client), + } + } + fn handle_session_start( + &mut self, + start_time: OffsetDateTime, + client: DestinationAddressBytes, + ) { + self.sessions_started += 1; + self.unique_users.insert(client); + self.active_sessions + .insert(client, ActiveSession::new(start_time)); + } + fn handle_session_stop(&mut self, stop_time: OffsetDateTime, client: DestinationAddressBytes) { + if let Some(session) = self.active_sessions.remove(&client) { + if let Some(finished_session) = session.end_at(stop_time) { + if self.finished_sessions.len() < FINISHED_SESSIONS_CAP { + self.finished_sessions.push(finished_session); + } + } + } + } + + fn handle_ecash_ticket(&mut self, ticket_type: TicketType, client: DestinationAddressBytes) { + if let Some(active_session) = self.active_sessions.get_mut(&client) { + if active_session.typ == SessionType::Unknown { + active_session.set_type(ticket_type); + } + } + } + + //update shared state once a day has passed, with data from the previous day + pub(crate) async fn update_shared_state(&mut self, update_time: OffsetDateTime) { + let update_date = update_time.date(); + if update_date != self.last_update_day { + { + let mut shared_state = self.shared_session_stats.write().await; + shared_state.update_time = self.last_update_day; + shared_state.unique_active_users = self.unique_users.len() as u32; + shared_state.session_started = self.sessions_started; + shared_state.sessions = self + .finished_sessions + .iter() + .map(|s| s.serialize()) + .collect(); + } + self.reset_stats(update_date); + } + } + + fn reset_stats(&mut self, reset_day: Date) { + self.last_update_day = reset_day; + self.unique_users = self.active_sessions.keys().copied().collect(); + self.finished_sessions = Default::default(); + self.sessions_started = 0; + } +} diff --git a/mixnode/src/node/http/mod.rs b/mixnode/src/node/http/mod.rs index 3784aab759..658c2ce980 100644 --- a/mixnode/src/node/http/mod.rs +++ b/mixnode/src/node/http/mod.rs @@ -24,9 +24,9 @@ fn load_host_details( ip_address: config.host.public_ips.clone(), hostname: config.host.hostname.clone(), keys: api_requests::v1::node::models::HostKeys { - ed25519_identity: identity_keypair.public_key().to_base58_string(), - x25519_sphinx: sphinx_key.to_base58_string(), - x25519_noise: "".to_string(), + ed25519_identity: *identity_keypair.public_key(), + x25519_sphinx: *sphinx_key, + x25519_noise: None, }, }; diff --git a/mixnode/src/node/listener/connection_handler/mod.rs b/mixnode/src/node/listener/connection_handler/mod.rs index f4144c4f67..0b76762667 100644 --- a/mixnode/src/node/listener/connection_handler/mod.rs +++ b/mixnode/src/node/listener/connection_handler/mod.rs @@ -1,9 +1,7 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node::listener::connection_handler::packet_processing::{ - MixProcessingResult, PacketProcessor, -}; +use crate::node::listener::connection_handler::packet_processing::PacketProcessor; use crate::node::packet_delayforwarder::PacketDelayForwardSender; use crate::node::TaskClient; use futures::StreamExt; @@ -13,7 +11,9 @@ use nym_metrics::nanos; use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::framing::codec::NymCodec; use nym_sphinx::framing::packet::FramedNymPacket; +use nym_sphinx::framing::processing::MixProcessingResult; use nym_sphinx::Delay as SphinxDelay; +use packet_processing::process_received_packet; use std::net::SocketAddr; use tokio::net::TcpStream; use tokio::time::Instant; @@ -38,6 +38,10 @@ impl ConnectionHandler { } } + pub fn packet_processor(&self) -> &PacketProcessor { + &self.packet_processor + } + fn delay_and_forward_packet(&self, mix_packet: MixPacket, delay: Option) { // determine instant at which packet should get forwarded. this way we minimise effect of // being stuck in the queue [of the channel] to get inserted into the delay queue @@ -60,7 +64,10 @@ impl ConnectionHandler { // all processing such, key caching, etc. was done. // however, if it was a forward hop, we still need to delay it nanos!("handle_received_packet", { - match self.packet_processor.process_received(framed_sphinx_packet) { + self.packet_processor + .node_stats_update_sender() + .report_received(); + match process_received_packet(framed_sphinx_packet, self.packet_processor().inner()) { Err(err) => debug!("We failed to process received sphinx packet - {err}"), Ok(res) => match res { MixProcessingResult::ForwardHop(forward_packet, delay) => { diff --git a/mixnode/src/node/listener/connection_handler/packet_processing.rs b/mixnode/src/node/listener/connection_handler/packet_processing.rs index f849632f0f..9e6742a6b8 100644 --- a/mixnode/src/node/listener/connection_handler/packet_processing.rs +++ b/mixnode/src/node/listener/connection_handler/packet_processing.rs @@ -4,13 +4,13 @@ use crate::node::node_statistics; use nym_crypto::asymmetric::encryption; use nym_mixnode_common::packet_processor::error::MixProcessingError; -pub use nym_mixnode_common::packet_processor::processor::MixProcessingResult; use nym_mixnode_common::packet_processor::processor::SphinxPacketProcessor; use nym_sphinx::framing::packet::FramedNymPacket; +use nym_sphinx::framing::processing::{process_framed_packet, MixProcessingResult}; // PacketProcessor contains all data required to correctly unwrap and forward sphinx packets #[derive(Clone)] -pub struct PacketProcessor { +pub(crate) struct PacketProcessor { /// Responsible for performing unwrapping inner_processor: SphinxPacketProcessor, @@ -29,11 +29,18 @@ impl PacketProcessor { } } - pub(crate) fn process_received( - &self, - received: FramedNymPacket, - ) -> Result { - self.node_stats_update_sender.report_received(); - self.inner_processor.process_received(received) + pub fn inner(&self) -> &SphinxPacketProcessor { + &self.inner_processor } + + pub fn node_stats_update_sender(&self) -> &node_statistics::UpdateSender { + &self.node_stats_update_sender + } +} + +pub fn process_received_packet( + packet: FramedNymPacket, + inner_processor: &SphinxPacketProcessor, +) -> Result { + Ok(process_framed_packet(packet, inner_processor.sphinx_key())?) } diff --git a/mixnode/src/node/mod.rs b/mixnode/src/node/mod.rs index 0043113658..e630007454 100644 --- a/mixnode/src/node/mod.rs +++ b/mixnode/src/node/mod.rs @@ -234,7 +234,7 @@ impl MixNode { // TODO: if anything, this should be getting data directly from the contract // as opposed to the validator API let validator_client = self.random_api_client(); - let existing_nodes = match validator_client.get_cached_mixnodes().await { + let existing_nodes = match validator_client.get_all_basic_nodes(None).await { Ok(nodes) => nodes, Err(err) => { error!( @@ -245,10 +245,9 @@ impl MixNode { } }; - existing_nodes.iter().any(|node| { - node.bond_information.mix_node.identity_key - == self.identity_keypair.public_key().to_base58_string() - }) + existing_nodes + .iter() + .any(|node| &node.ed25519_identity_pubkey == self.identity_keypair.public_key()) } async fn wait_for_interrupt(&self, shutdown: TaskHandle) { diff --git a/nym-api.dockerfile b/nym-api.dockerfile new file mode 100644 index 0000000000..7ee95379dc --- /dev/null +++ b/nym-api.dockerfile @@ -0,0 +1,7 @@ +FROM rust:latest AS builder + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-api +RUN cargo build --release + +ENTRYPOINT ["/usr/src/nym/nym-api/entrypoint.sh"] diff --git a/nym-api/.sqlx/query-01c1251e3dba4db47ffbb83d5545fd75ead425f2ee10dbe28ace13a58cadd2e5.json b/nym-api/.sqlx/query-01c1251e3dba4db47ffbb83d5545fd75ead425f2ee10dbe28ace13a58cadd2e5.json new file mode 100644 index 0000000000..e01b4b406f --- /dev/null +++ b/nym-api/.sqlx/query-01c1251e3dba4db47ffbb83d5545fd75ead425f2ee10dbe28ace13a58cadd2e5.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE epoch_credentials\n SET total_issued = 1, start_id = ?\n WHERE epoch_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "01c1251e3dba4db47ffbb83d5545fd75ead425f2ee10dbe28ace13a58cadd2e5" +} diff --git a/nym-api/.sqlx/query-09b6a36cf4455e53188f0ddd2f664855ee541cf7e262d5bba8ccd1df8088762e.json b/nym-api/.sqlx/query-09b6a36cf4455e53188f0ddd2f664855ee541cf7e262d5bba8ccd1df8088762e.json new file mode 100644 index 0000000000..ba84fb0761 --- /dev/null +++ b/nym-api/.sqlx/query-09b6a36cf4455e53188f0ddd2f664855ee541cf7e262d5bba8ccd1df8088762e.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "SELECT * FROM mixnode_details WHERE id = ?", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "mix_id", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "identity_key", + "ordinal": 2, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "09b6a36cf4455e53188f0ddd2f664855ee541cf7e262d5bba8ccd1df8088762e" +} diff --git a/nym-api/.sqlx/query-0a2587e2c72175caa89823675c4f2b6437c700eb7cdc41215e6dcde9754920db.json b/nym-api/.sqlx/query-0a2587e2c72175caa89823675c4f2b6437c700eb7cdc41215e6dcde9754920db.json new file mode 100644 index 0000000000..156de4067f --- /dev/null +++ b/nym-api/.sqlx/query-0a2587e2c72175caa89823675c4f2b6437c700eb7cdc41215e6dcde9754920db.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT COUNT(*) as count\n FROM mixnode_status\n JOIN monitor_run ON mixnode_status.timestamp = monitor_run.timestamp\n JOIN testing_route ON monitor_run.id = testing_route.monitor_run_id\n WHERE mixnode_details_id = ?\n ", + "describe": { + "columns": [ + { + "name": "count", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "0a2587e2c72175caa89823675c4f2b6437c700eb7cdc41215e6dcde9754920db" +} diff --git a/nym-api/.sqlx/query-0ac0747bc2988ccc5ae95a78e626fcf79e6d090f667b150b101d0ca90e6ba9ea.json b/nym-api/.sqlx/query-0ac0747bc2988ccc5ae95a78e626fcf79e6d090f667b150b101d0ca90e6ba9ea.json new file mode 100644 index 0000000000..3d3f1c50ea --- /dev/null +++ b/nym-api/.sqlx/query-0ac0747bc2988ccc5ae95a78e626fcf79e6d090f667b150b101d0ca90e6ba9ea.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM mixnode_status WHERE timestamp < ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "0ac0747bc2988ccc5ae95a78e626fcf79e6d090f667b150b101d0ca90e6ba9ea" +} diff --git a/nym-api/.sqlx/query-158b6cf296627527806ca5cae7375bcfc73533bc24acc837211ff35bd00b9abc.json b/nym-api/.sqlx/query-158b6cf296627527806ca5cae7375bcfc73533bc24acc837211ff35bd00b9abc.json new file mode 100644 index 0000000000..9ad6f6f437 --- /dev/null +++ b/nym-api/.sqlx/query-158b6cf296627527806ca5cae7375bcfc73533bc24acc837211ff35bd00b9abc.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT AVG(reliability) as \"reliability: f32\" FROM mixnode_status\n WHERE mixnode_details_id= ? AND timestamp >= ? AND timestamp <= ?\n ", + "describe": { + "columns": [ + { + "name": "reliability: f32", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + true + ] + }, + "hash": "158b6cf296627527806ca5cae7375bcfc73533bc24acc837211ff35bd00b9abc" +} diff --git a/nym-api/.sqlx/query-16ebee4ef4d74e6fa3aaf611d2396a6445c9c69612515aeb51c519115a2064fd.json b/nym-api/.sqlx/query-16ebee4ef4d74e6fa3aaf611d2396a6445c9c69612515aeb51c519115a2064fd.json new file mode 100644 index 0000000000..4402e35940 --- /dev/null +++ b/nym-api/.sqlx/query-16ebee4ef4d74e6fa3aaf611d2396a6445c9c69612515aeb51c519115a2064fd.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO testing_route\n (gateway_id, layer1_mix_id, layer2_mix_id, layer3_mix_id, monitor_run_id)\n VALUES (?, ?, ?, ?, ?);\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "16ebee4ef4d74e6fa3aaf611d2396a6445c9c69612515aeb51c519115a2064fd" +} diff --git a/nym-api/.sqlx/query-19e3f9bcf0adfea2b12fc50ab5bb7aa84ba3edc90ebcc49644ad52ebdc71e162.json b/nym-api/.sqlx/query-19e3f9bcf0adfea2b12fc50ab5bb7aa84ba3edc90ebcc49644ad52ebdc71e162.json new file mode 100644 index 0000000000..0709ae6d6a --- /dev/null +++ b/nym-api/.sqlx/query-19e3f9bcf0adfea2b12fc50ab5bb7aa84ba3edc90ebcc49644ad52ebdc71e162.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO v3_migration_info(id) VALUES (0)", + "describe": { + "columns": [], + "parameters": { + "Right": 0 + }, + "nullable": [] + }, + "hash": "19e3f9bcf0adfea2b12fc50ab5bb7aa84ba3edc90ebcc49644ad52ebdc71e162" +} diff --git a/nym-api/.sqlx/query-1d4535b58abdefaaca96bc7312fe14f63ccb56fa62976f7ce3d3b4f6eca8b711.json b/nym-api/.sqlx/query-1d4535b58abdefaaca96bc7312fe14f63ccb56fa62976f7ce3d3b4f6eca8b711.json new file mode 100644 index 0000000000..2a2b8976de --- /dev/null +++ b/nym-api/.sqlx/query-1d4535b58abdefaaca96bc7312fe14f63ccb56fa62976f7ce3d3b4f6eca8b711.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT timestamp, reliability as \"reliability: u8\"\n FROM gateway_status\n JOIN gateway_details\n ON gateway_status.gateway_details_id = gateway_details.id\n WHERE gateway_details.node_id=? AND gateway_status.timestamp > ?;\n ", + "describe": { + "columns": [ + { + "name": "timestamp", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "reliability: u8", + "ordinal": 1, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + true, + true + ] + }, + "hash": "1d4535b58abdefaaca96bc7312fe14f63ccb56fa62976f7ce3d3b4f6eca8b711" +} diff --git a/nym-api/.sqlx/query-1f3fcc8fde2b9345ec08905eda07c142c49524eaecd846cdf4b84827efdc3f01.json b/nym-api/.sqlx/query-1f3fcc8fde2b9345ec08905eda07c142c49524eaecd846cdf4b84827efdc3f01.json new file mode 100644 index 0000000000..c5b2c975f2 --- /dev/null +++ b/nym-api/.sqlx/query-1f3fcc8fde2b9345ec08905eda07c142c49524eaecd846cdf4b84827efdc3f01.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT mix_id as \"mix_id: NodeId\" FROM mixnode_details WHERE identity_key = ?", + "describe": { + "columns": [ + { + "name": "mix_id: NodeId", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "1f3fcc8fde2b9345ec08905eda07c142c49524eaecd846cdf4b84827efdc3f01" +} diff --git a/nym-api/.sqlx/query-1f72d6f538a3655a031a3a8706794559c4c0df6defdfd179c84d02d3b8a6c055.json b/nym-api/.sqlx/query-1f72d6f538a3655a031a3a8706794559c4c0df6defdfd179c84d02d3b8a6c055.json new file mode 100644 index 0000000000..d1252da321 --- /dev/null +++ b/nym-api/.sqlx/query-1f72d6f538a3655a031a3a8706794559c4c0df6defdfd179c84d02d3b8a6c055.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures\n FROM partial_expiration_date_signatures\n WHERE expiration_date = ?\n ", + "describe": { + "columns": [ + { + "name": "epoch_id: u32", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "serialised_signatures", + "ordinal": 1, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + }, + "hash": "1f72d6f538a3655a031a3a8706794559c4c0df6defdfd179c84d02d3b8a6c055" +} diff --git a/nym-api/.sqlx/query-22e69e450e12f72e2cf06b2e296913f1e22d90e63a57e6256a9c6669e7c27724.json b/nym-api/.sqlx/query-22e69e450e12f72e2cf06b2e296913f1e22d90e63a57e6256a9c6669e7c27724.json new file mode 100644 index 0000000000..a7e8cbc31e --- /dev/null +++ b/nym-api/.sqlx/query-22e69e450e12f72e2cf06b2e296913f1e22d90e63a57e6256a9c6669e7c27724.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO mixnode_details(mix_id, identity_key) VALUES (?, ?);\n SELECT id FROM mixnode_details WHERE mix_id = ?;\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false + ] + }, + "hash": "22e69e450e12f72e2cf06b2e296913f1e22d90e63a57e6256a9c6669e7c27724" +} diff --git a/nym-api/.sqlx/query-22f87c049f002e4f5c9c1f00ddf4c1fc80c2cbb1662b576b0273af382dd7dbe2.json b/nym-api/.sqlx/query-22f87c049f002e4f5c9c1f00ddf4c1fc80c2cbb1662b576b0273af382dd7dbe2.json new file mode 100644 index 0000000000..cd7a5aef10 --- /dev/null +++ b/nym-api/.sqlx/query-22f87c049f002e4f5c9c1f00ddf4c1fc80c2cbb1662b576b0273af382dd7dbe2.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM partial_bloomfilter WHERE date > ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "22f87c049f002e4f5c9c1f00ddf4c1fc80c2cbb1662b576b0273af382dd7dbe2" +} diff --git a/nym-api/.sqlx/query-24a3d608e8a727f863129371be6087976b741fb59d09382cbb09597c6cc44c43.json b/nym-api/.sqlx/query-24a3d608e8a727f863129371be6087976b741fb59d09382cbb09597c6cc44c43.json new file mode 100644 index 0000000000..0c35e15cc9 --- /dev/null +++ b/nym-api/.sqlx/query-24a3d608e8a727f863129371be6087976b741fb59d09382cbb09597c6cc44c43.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO issued_ticketbook\n (epoch_id, deposit_id, partial_credential, signature, joined_private_commitments, expiration_date, ticketbook_type_repr)\n VALUES\n (?, ?, ?, ?, ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "24a3d608e8a727f863129371be6087976b741fb59d09382cbb09597c6cc44c43" +} diff --git a/nym-api/.sqlx/query-25c7b3e68bc759093312045a3cb0c344e5a09224da60f015b48618f88d1a10e6.json b/nym-api/.sqlx/query-25c7b3e68bc759093312045a3cb0c344e5a09224da60f015b48618f88d1a10e6.json new file mode 100644 index 0000000000..4bf3e5e1c6 --- /dev/null +++ b/nym-api/.sqlx/query-25c7b3e68bc759093312045a3cb0c344e5a09224da60f015b48618f88d1a10e6.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO gateway_details(node_id, identity) VALUES (?, ?);\n SELECT id FROM gateway_details WHERE identity = ?;\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false + ] + }, + "hash": "25c7b3e68bc759093312045a3cb0c344e5a09224da60f015b48618f88d1a10e6" +} diff --git a/nym-api/.sqlx/query-299e04e840beab437f84a0a9b82958f158751af79987d1cf6c992ff8384d2a06.json b/nym-api/.sqlx/query-299e04e840beab437f84a0a9b82958f158751af79987d1cf6c992ff8384d2a06.json new file mode 100644 index 0000000000..2d34d111eb --- /dev/null +++ b/nym-api/.sqlx/query-299e04e840beab437f84a0a9b82958f158751af79987d1cf6c992ff8384d2a06.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO partial_coin_index_signatures(epoch_id, serialised_signatures) VALUES (?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "299e04e840beab437f84a0a9b82958f158751af79987d1cf6c992ff8384d2a06" +} diff --git a/nym-api/.sqlx/query-2db414dfc769ae63a63656500d574cb3eea7cd88eaf829643d71eff410ca5bd2.json b/nym-api/.sqlx/query-2db414dfc769ae63a63656500d574cb3eea7cd88eaf829643d71eff410ca5bd2.json new file mode 100644 index 0000000000..72aabf9250 --- /dev/null +++ b/nym-api/.sqlx/query-2db414dfc769ae63a63656500d574cb3eea7cd88eaf829643d71eff410ca5bd2.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT COUNT(*) as count FROM monitor_run WHERE timestamp > ? AND timestamp < ?", + "describe": { + "columns": [ + { + "name": "count", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "2db414dfc769ae63a63656500d574cb3eea7cd88eaf829643d71eff410ca5bd2" +} diff --git a/nym-api/.sqlx/query-2e1eecad52ef13bba5ab914be6d27a27d480d9f3f2269e42d5c4008e6e7ece2f.json b/nym-api/.sqlx/query-2e1eecad52ef13bba5ab914be6d27a27d480d9f3f2269e42d5c4008e6e7ece2f.json new file mode 100644 index 0000000000..cc74ae3e4b --- /dev/null +++ b/nym-api/.sqlx/query-2e1eecad52ef13bba5ab914be6d27a27d480d9f3f2269e42d5c4008e6e7ece2f.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT date, uptime\n FROM gateway_historical_uptime\n JOIN gateway_details\n ON gateway_historical_uptime.gateway_details_id = gateway_details.id\n WHERE gateway_details.node_id = ?\n ORDER BY date ASC\n ", + "describe": { + "columns": [ + { + "name": "date", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "uptime", + "ordinal": 1, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + true + ] + }, + "hash": "2e1eecad52ef13bba5ab914be6d27a27d480d9f3f2269e42d5c4008e6e7ece2f" +} diff --git a/nym-api/.sqlx/query-3053010ee5c10dc98932c307ab5a8dc0ecf367793c602ed3622c699c54ca0445.json b/nym-api/.sqlx/query-3053010ee5c10dc98932c307ab5a8dc0ecf367793c602ed3622c699c54ca0445.json new file mode 100644 index 0000000000..d6e7ed3184 --- /dev/null +++ b/nym-api/.sqlx/query-3053010ee5c10dc98932c307ab5a8dc0ecf367793c602ed3622c699c54ca0445.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT identity_key FROM mixnode_details WHERE mix_id = ?", + "describe": { + "columns": [ + { + "name": "identity_key", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "3053010ee5c10dc98932c307ab5a8dc0ecf367793c602ed3622c699c54ca0445" +} diff --git a/nym-api/.sqlx/query-30ff12a3e6b5d0344108bbd1f1634f92cca223ee334d546c5d91b1bb210b3e49.json b/nym-api/.sqlx/query-30ff12a3e6b5d0344108bbd1f1634f92cca223ee334d546c5d91b1bb210b3e49.json new file mode 100644 index 0000000000..13e8e84384 --- /dev/null +++ b/nym-api/.sqlx/query-30ff12a3e6b5d0344108bbd1f1634f92cca223ee334d546c5d91b1bb210b3e49.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM gateway_status WHERE timestamp < ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "30ff12a3e6b5d0344108bbd1f1634f92cca223ee334d546c5d91b1bb210b3e49" +} diff --git a/nym-api/.sqlx/query-31d1bd7418ae15c30e2ce16eff253d43823f7ebd53429ccb0598f5bca16aa3c9.json b/nym-api/.sqlx/query-31d1bd7418ae15c30e2ce16eff253d43823f7ebd53429ccb0598f5bca16aa3c9.json new file mode 100644 index 0000000000..67dc8f42f0 --- /dev/null +++ b/nym-api/.sqlx/query-31d1bd7418ae15c30e2ce16eff253d43823f7ebd53429ccb0598f5bca16aa3c9.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT id FROM gateway_details WHERE identity = ?", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "31d1bd7418ae15c30e2ce16eff253d43823f7ebd53429ccb0598f5bca16aa3c9" +} diff --git a/nym-api/.sqlx/query-361f939f40c230052b063ababe582d08bfddfebc24dc4e809f97a5dba57dfcf9.json b/nym-api/.sqlx/query-361f939f40c230052b063ababe582d08bfddfebc24dc4e809f97a5dba57dfcf9.json new file mode 100644 index 0000000000..0f02120676 --- /dev/null +++ b/nym-api/.sqlx/query-361f939f40c230052b063ababe582d08bfddfebc24dc4e809f97a5dba57dfcf9.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT timestamp, reliability as \"reliability: u8\"\n FROM gateway_status\n WHERE gateway_details_id=? AND timestamp > ? AND timestamp < ?;\n ", + "describe": { + "columns": [ + { + "name": "timestamp", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "reliability: u8", + "ordinal": 1, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + true, + true + ] + }, + "hash": "361f939f40c230052b063ababe582d08bfddfebc24dc4e809f97a5dba57dfcf9" +} diff --git a/nym-api/.sqlx/query-387504dabdb660afb2890a9686dec225f877ee0e370d81ca65c7ed20c009b0ec.json b/nym-api/.sqlx/query-387504dabdb660afb2890a9686dec225f877ee0e370d81ca65c7ed20c009b0ec.json new file mode 100644 index 0000000000..3661c1a248 --- /dev/null +++ b/nym-api/.sqlx/query-387504dabdb660afb2890a9686dec225f877ee0e370d81ca65c7ed20c009b0ec.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO gateway_status (gateway_details_id, reliability, timestamp) VALUES (?, ?, ?);\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "387504dabdb660afb2890a9686dec225f877ee0e370d81ca65c7ed20c009b0ec" +} diff --git a/nym-api/.sqlx/query-3f51747b87e7dca5c4da75a99edf04ea6df2b6a8dbc99845be2287fb9b2cec2b.json b/nym-api/.sqlx/query-3f51747b87e7dca5c4da75a99edf04ea6df2b6a8dbc99845be2287fb9b2cec2b.json new file mode 100644 index 0000000000..74ac1ae7d4 --- /dev/null +++ b/nym-api/.sqlx/query-3f51747b87e7dca5c4da75a99edf04ea6df2b6a8dbc99845be2287fb9b2cec2b.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO monitor_run(timestamp) VALUES (?)", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "3f51747b87e7dca5c4da75a99edf04ea6df2b6a8dbc99845be2287fb9b2cec2b" +} diff --git a/nym-api/.sqlx/query-4077371f0dccf1b389da97b3f8b290a4095c807eaefd68f3863b0fcec67019e9.json b/nym-api/.sqlx/query-4077371f0dccf1b389da97b3f8b290a4095c807eaefd68f3863b0fcec67019e9.json new file mode 100644 index 0000000000..cc6b558567 --- /dev/null +++ b/nym-api/.sqlx/query-4077371f0dccf1b389da97b3f8b290a4095c807eaefd68f3863b0fcec67019e9.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO gateway_historical_uptime(gateway_details_id, date, uptime) VALUES (?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "4077371f0dccf1b389da97b3f8b290a4095c807eaefd68f3863b0fcec67019e9" +} diff --git a/nym-api/.sqlx/query-462afe192f5f9bb068a557aa1c5c5f49299eb403049ddef168fc25e3d59edf6f.json b/nym-api/.sqlx/query-462afe192f5f9bb068a557aa1c5c5f49299eb403049ddef168fc25e3d59edf6f.json new file mode 100644 index 0000000000..6d6254d3ba --- /dev/null +++ b/nym-api/.sqlx/query-462afe192f5f9bb068a557aa1c5c5f49299eb403049ddef168fc25e3d59edf6f.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT id FROM mixnode_details WHERE mix_id = ?", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "462afe192f5f9bb068a557aa1c5c5f49299eb403049ddef168fc25e3d59edf6f" +} diff --git a/nym-api/.sqlx/query-4892b8ef3683e015a3f6fc5df9bcd96fd3fbe10e94393f61e4ed8af4d5700da6.json b/nym-api/.sqlx/query-4892b8ef3683e015a3f6fc5df9bcd96fd3fbe10e94393f61e4ed8af4d5700da6.json new file mode 100644 index 0000000000..c99e1af9d9 --- /dev/null +++ b/nym-api/.sqlx/query-4892b8ef3683e015a3f6fc5df9bcd96fd3fbe10e94393f61e4ed8af4d5700da6.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n absolute_epoch_id as \"absolute_epoch_id: u32\",\n eligible_mixnodes as \"eligible_mixnodes: u32\"\n FROM rewarding_report\n WHERE absolute_epoch_id = ?\n ", + "describe": { + "columns": [ + { + "name": "absolute_epoch_id: u32", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "eligible_mixnodes: u32", + "ordinal": 1, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + }, + "hash": "4892b8ef3683e015a3f6fc5df9bcd96fd3fbe10e94393f61e4ed8af4d5700da6" +} diff --git a/nym-api/.sqlx/query-498accd22c035ea1b0b887cdaab5d9b9e6c13bce4a2e093439fbfab72bcffc40.json b/nym-api/.sqlx/query-498accd22c035ea1b0b887cdaab5d9b9e6c13bce4a2e093439fbfab72bcffc40.json new file mode 100644 index 0000000000..6fcebdf6be --- /dev/null +++ b/nym-api/.sqlx/query-498accd22c035ea1b0b887cdaab5d9b9e6c13bce4a2e093439fbfab72bcffc40.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO mixnode_historical_uptime(mixnode_details_id, date, uptime) VALUES (?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "498accd22c035ea1b0b887cdaab5d9b9e6c13bce4a2e093439fbfab72bcffc40" +} diff --git a/nym-api/.sqlx/query-4acb6507c571daff7c4eed6550976bbcd28c6113618a364c009ee92ea72e06aa.json b/nym-api/.sqlx/query-4acb6507c571daff7c4eed6550976bbcd28c6113618a364c009ee92ea72e06aa.json new file mode 100644 index 0000000000..440b559125 --- /dev/null +++ b/nym-api/.sqlx/query-4acb6507c571daff7c4eed6550976bbcd28c6113618a364c009ee92ea72e06aa.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO partial_bloomfilter(date, parameters, bitmap) VALUES (?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "4acb6507c571daff7c4eed6550976bbcd28c6113618a364c009ee92ea72e06aa" +} diff --git a/nym-api/.sqlx/query-4b3ddffe1cb55032ef7d12bcf462d96b414e44e314d077baba11082d973342cf.json b/nym-api/.sqlx/query-4b3ddffe1cb55032ef7d12bcf462d96b414e44e314d077baba11082d973342cf.json new file mode 100644 index 0000000000..5d5f3d1cbf --- /dev/null +++ b/nym-api/.sqlx/query-4b3ddffe1cb55032ef7d12bcf462d96b414e44e314d077baba11082d973342cf.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT epoch_id as \"epoch_id: u32\", start_id, total_issued as \"total_issued: u32\"\n FROM epoch_credentials\n WHERE epoch_id = ?\n ", + "describe": { + "columns": [ + { + "name": "epoch_id: u32", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "start_id", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "total_issued: u32", + "ordinal": 2, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "4b3ddffe1cb55032ef7d12bcf462d96b414e44e314d077baba11082d973342cf" +} diff --git a/nym-api/.sqlx/query-4ebee7c6043a84afeb4425abefe58f8182edff025a38e25106e00d97659c522a.json b/nym-api/.sqlx/query-4ebee7c6043a84afeb4425abefe58f8182edff025a38e25106e00d97659c522a.json new file mode 100644 index 0000000000..2c65e43f17 --- /dev/null +++ b/nym-api/.sqlx/query-4ebee7c6043a84afeb4425abefe58f8182edff025a38e25106e00d97659c522a.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT bitmap FROM partial_bloomfilter WHERE date = ? AND parameters = ?", + "describe": { + "columns": [ + { + "name": "bitmap", + "ordinal": 0, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "4ebee7c6043a84afeb4425abefe58f8182edff025a38e25106e00d97659c522a" +} diff --git a/nym-api/.sqlx/query-50f0fdb88474c1cddd70cda9457b62992859d4f585f2efe6c19894ec79ec3cbb.json b/nym-api/.sqlx/query-50f0fdb88474c1cddd70cda9457b62992859d4f585f2efe6c19894ec79ec3cbb.json new file mode 100644 index 0000000000..858733eecb --- /dev/null +++ b/nym-api/.sqlx/query-50f0fdb88474c1cddd70cda9457b62992859d4f585f2efe6c19894ec79ec3cbb.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n UPDATE epoch_credentials\n SET total_issued = total_issued + 1\n WHERE epoch_id = ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "50f0fdb88474c1cddd70cda9457b62992859d4f585f2efe6c19894ec79ec3cbb" +} diff --git a/nym-api/.sqlx/query-5125387e3d7383aec751904efe8325750974fe1274729c6981b05cd872121fe6.json b/nym-api/.sqlx/query-5125387e3d7383aec751904efe8325750974fe1274729c6981b05cd872121fe6.json new file mode 100644 index 0000000000..13ee3ffc8f --- /dev/null +++ b/nym-api/.sqlx/query-5125387e3d7383aec751904efe8325750974fe1274729c6981b05cd872121fe6.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n DELETE FROM gateway_historical_uptime WHERE gateway_details_id = ?;\n DELETE FROM gateway_status WHERE gateway_details_id = ?;\n DELETE FROM testing_route WHERE gateway_id = ?;\n DELETE FROM gateway_details WHERE id = ?;\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "5125387e3d7383aec751904efe8325750974fe1274729c6981b05cd872121fe6" +} diff --git a/nym-api/.sqlx/query-59daa1d46c0e24e474cf68a9c31c0689b6becc82c779c838ed608bbb61fcbe97.json b/nym-api/.sqlx/query-59daa1d46c0e24e474cf68a9c31c0689b6becc82c779c838ed608bbb61fcbe97.json new file mode 100644 index 0000000000..bc73184005 --- /dev/null +++ b/nym-api/.sqlx/query-59daa1d46c0e24e474cf68a9c31c0689b6becc82c779c838ed608bbb61fcbe97.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO partial_expiration_date_signatures(expiration_date, epoch_id, serialised_signatures) VALUES (?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "59daa1d46c0e24e474cf68a9c31c0689b6becc82c779c838ed608bbb61fcbe97" +} diff --git a/nym-api/.sqlx/query-5c0481bd633f56e154a5cd81e3f691dd587208b876b3fa7d37b26f0c60461aa2.json b/nym-api/.sqlx/query-5c0481bd633f56e154a5cd81e3f691dd587208b876b3fa7d37b26f0c60461aa2.json new file mode 100644 index 0000000000..a69ea376f9 --- /dev/null +++ b/nym-api/.sqlx/query-5c0481bd633f56e154a5cd81e3f691dd587208b876b3fa7d37b26f0c60461aa2.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO epoch_credentials\n (epoch_id, start_id, total_issued)\n VALUES (?, ?, ?);\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "5c0481bd633f56e154a5cd81e3f691dd587208b876b3fa7d37b26f0c60461aa2" +} diff --git a/nym-api/.sqlx/query-5ce6e1f02b4018fe587ae15bba8a2e051c87d6d9bdd228a2ab0604d4ab6d252c.json b/nym-api/.sqlx/query-5ce6e1f02b4018fe587ae15bba8a2e051c87d6d9bdd228a2ab0604d4ab6d252c.json new file mode 100644 index 0000000000..b1fcad92ae --- /dev/null +++ b/nym-api/.sqlx/query-5ce6e1f02b4018fe587ae15bba8a2e051c87d6d9bdd228a2ab0604d4ab6d252c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO global_expiration_date_signatures(expiration_date, epoch_id, serialised_signatures) VALUES (?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "5ce6e1f02b4018fe587ae15bba8a2e051c87d6d9bdd228a2ab0604d4ab6d252c" +} diff --git a/nym-api/.sqlx/query-5e3824763c4ef13194876e3cdd65c2da97193c60e8d1a1640c48ba65e18a9faa.json b/nym-api/.sqlx/query-5e3824763c4ef13194876e3cdd65c2da97193c60e8d1a1640c48ba65e18a9faa.json new file mode 100644 index 0000000000..9ff0d9cea2 --- /dev/null +++ b/nym-api/.sqlx/query-5e3824763c4ef13194876e3cdd65c2da97193c60e8d1a1640c48ba65e18a9faa.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS (SELECT 1 FROM v3_migration_info) AS 'exists'", + "describe": { + "columns": [ + { + "name": "exists", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + null + ] + }, + "hash": "5e3824763c4ef13194876e3cdd65c2da97193c60e8d1a1640c48ba65e18a9faa" +} diff --git a/nym-api/.sqlx/query-5eb13bfbee53b50641f69d4d6b62383c7f43864bffe98642bb8d1cf7c259d7be.json b/nym-api/.sqlx/query-5eb13bfbee53b50641f69d4d6b62383c7f43864bffe98642bb8d1cf7c259d7be.json new file mode 100644 index 0000000000..0f79148335 --- /dev/null +++ b/nym-api/.sqlx/query-5eb13bfbee53b50641f69d4d6b62383c7f43864bffe98642bb8d1cf7c259d7be.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT epoch_id as \"epoch_id: u32\", serialised_signatures\n FROM global_expiration_date_signatures\n WHERE expiration_date = ?\n ", + "describe": { + "columns": [ + { + "name": "epoch_id: u32", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "serialised_signatures", + "ordinal": 1, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + }, + "hash": "5eb13bfbee53b50641f69d4d6b62383c7f43864bffe98642bb8d1cf7c259d7be" +} diff --git a/nym-api/.sqlx/query-66c50aba8c76ef7d3798a3188f81bb16d3ae867f3f1689a6c63c9f75f8ed91ee.json b/nym-api/.sqlx/query-66c50aba8c76ef7d3798a3188f81bb16d3ae867f3f1689a6c63c9f75f8ed91ee.json new file mode 100644 index 0000000000..606a35be1c --- /dev/null +++ b/nym-api/.sqlx/query-66c50aba8c76ef7d3798a3188f81bb16d3ae867f3f1689a6c63c9f75f8ed91ee.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO epoch_credentials\n (epoch_id, start_id, total_issued)\n VALUES (?, ?, ?);\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "66c50aba8c76ef7d3798a3188f81bb16d3ae867f3f1689a6c63c9f75f8ed91ee" +} diff --git a/nym-api/.sqlx/query-6932664a29aa1ea7ce79b5f584dcf143e2b6ef1831d1f2032e80124cc3986905.json b/nym-api/.sqlx/query-6932664a29aa1ea7ce79b5f584dcf143e2b6ef1831d1f2032e80124cc3986905.json new file mode 100644 index 0000000000..79c84d2924 --- /dev/null +++ b/nym-api/.sqlx/query-6932664a29aa1ea7ce79b5f584dcf143e2b6ef1831d1f2032e80124cc3986905.json @@ -0,0 +1,62 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id,\n epoch_id as \"epoch_id: u32\",\n deposit_id as \"deposit_id: DepositId\",\n partial_credential,\n signature,\n joined_private_commitments,\n expiration_date as \"expiration_date: Date\",\n ticketbook_type_repr as \"ticketbook_type_repr: u8\"\n FROM issued_ticketbook\n WHERE id > ?\n ORDER BY id\n LIMIT ?\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "epoch_id: u32", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "deposit_id: DepositId", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "partial_credential", + "ordinal": 3, + "type_info": "Blob" + }, + { + "name": "signature", + "ordinal": 4, + "type_info": "Blob" + }, + { + "name": "joined_private_commitments", + "ordinal": 5, + "type_info": "Blob" + }, + { + "name": "expiration_date: Date", + "ordinal": 6, + "type_info": "Date" + }, + { + "name": "ticketbook_type_repr: u8", + "ordinal": 7, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "6932664a29aa1ea7ce79b5f584dcf143e2b6ef1831d1f2032e80124cc3986905" +} diff --git a/nym-api/.sqlx/query-6b88e7f40bba38053e968d2a7198a0c9646120f24c07134ffb0a33cf2fb6b6ed.json b/nym-api/.sqlx/query-6b88e7f40bba38053e968d2a7198a0c9646120f24c07134ffb0a33cf2fb6b6ed.json new file mode 100644 index 0000000000..e063997beb --- /dev/null +++ b/nym-api/.sqlx/query-6b88e7f40bba38053e968d2a7198a0c9646120f24c07134ffb0a33cf2fb6b6ed.json @@ -0,0 +1,68 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n gateway_details.id as \"db_id\",\n identity as \"identity_key\",\n reliability as \"reliability: u8\",\n monitor_run.timestamp as \"timestamp!\",\n gateway_id as \"gateway_id!\",\n layer1_mix_id as \"layer1_mix_id!\",\n layer2_mix_id as \"layer2_mix_id!\",\n layer3_mix_id as \"layer3_mix_id!\",\n monitor_run_id as \"monitor_run_id!\"\n FROM gateway_status\n JOIN gateway_details ON gateway_status.gateway_details_id = gateway_details.id\n JOIN monitor_run ON gateway_status.timestamp = monitor_run.timestamp\n JOIN testing_route ON monitor_run.id = testing_route.monitor_run_id\n WHERE identity = ?\n ORDER BY gateway_status.timestamp DESC\n LIMIT ? OFFSET ?\n ", + "describe": { + "columns": [ + { + "name": "db_id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "identity_key", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "reliability: u8", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "timestamp!", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "gateway_id!", + "ordinal": 4, + "type_info": "Int64" + }, + { + "name": "layer1_mix_id!", + "ordinal": 5, + "type_info": "Int64" + }, + { + "name": "layer2_mix_id!", + "ordinal": 6, + "type_info": "Int64" + }, + { + "name": "layer3_mix_id!", + "ordinal": 7, + "type_info": "Int64" + }, + { + "name": "monitor_run_id!", + "ordinal": 8, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false, + false, + true, + false, + true, + true, + true, + true, + true + ] + }, + "hash": "6b88e7f40bba38053e968d2a7198a0c9646120f24c07134ffb0a33cf2fb6b6ed" +} diff --git a/nym-api/.sqlx/query-6c894d98a2aaf5bec2261502ec3abf571fd6ae01fa2c1efff6e5ff786dd443f6.json b/nym-api/.sqlx/query-6c894d98a2aaf5bec2261502ec3abf571fd6ae01fa2c1efff6e5ff786dd443f6.json new file mode 100644 index 0000000000..61c9dd9cd5 --- /dev/null +++ b/nym-api/.sqlx/query-6c894d98a2aaf5bec2261502ec3abf571fd6ae01fa2c1efff6e5ff786dd443f6.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT id FROM gateway_details WHERE node_id = ?", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "6c894d98a2aaf5bec2261502ec3abf571fd6ae01fa2c1efff6e5ff786dd443f6" +} diff --git a/nym-api/.sqlx/query-6fb96e0c0229f51fd77fc118841e1f2835a0ad378493b4fc006b7b47bea3bebe.json b/nym-api/.sqlx/query-6fb96e0c0229f51fd77fc118841e1f2835a0ad378493b4fc006b7b47bea3bebe.json new file mode 100644 index 0000000000..b4a035f070 --- /dev/null +++ b/nym-api/.sqlx/query-6fb96e0c0229f51fd77fc118841e1f2835a0ad378493b4fc006b7b47bea3bebe.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO mixnode_status_v2 (mixnode_details_id, reliability, timestamp) VALUES (?, ?, ?);\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "6fb96e0c0229f51fd77fc118841e1f2835a0ad378493b4fc006b7b47bea3bebe" +} diff --git a/nym-api/.sqlx/query-7276658a125d298043bb2204272bbc68f6bf0e2cee7de52a10c430ea51469589.json b/nym-api/.sqlx/query-7276658a125d298043bb2204272bbc68f6bf0e2cee7de52a10c430ea51469589.json new file mode 100644 index 0000000000..e441e4e642 --- /dev/null +++ b/nym-api/.sqlx/query-7276658a125d298043bb2204272bbc68f6bf0e2cee7de52a10c430ea51469589.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO rewarding_report\n (absolute_epoch_id, eligible_mixnodes)\n VALUES (?, ?);\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "7276658a125d298043bb2204272bbc68f6bf0e2cee7de52a10c430ea51469589" +} diff --git a/nym-api/.sqlx/query-73bb892f19060693d122774b66bfaa8059135fadc3632a3ba6201cfc5d96482e.json b/nym-api/.sqlx/query-73bb892f19060693d122774b66bfaa8059135fadc3632a3ba6201cfc5d96482e.json new file mode 100644 index 0000000000..e02d48f56e --- /dev/null +++ b/nym-api/.sqlx/query-73bb892f19060693d122774b66bfaa8059135fadc3632a3ba6201cfc5d96482e.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT date, uptime\n FROM mixnode_historical_uptime\n JOIN mixnode_details\n ON mixnode_historical_uptime.mixnode_details_id = mixnode_details.id\n WHERE mixnode_details.mix_id = ?\n ORDER BY date ASC\n ", + "describe": { + "columns": [ + { + "name": "date", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "uptime", + "ordinal": 1, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + true + ] + }, + "hash": "73bb892f19060693d122774b66bfaa8059135fadc3632a3ba6201cfc5d96482e" +} diff --git a/nym-api/.sqlx/query-73ca856950a0157acfd3e2ed07b11aca3d875f67c77e2e7c75653c3f337d594e.json b/nym-api/.sqlx/query-73ca856950a0157acfd3e2ed07b11aca3d875f67c77e2e7c75653c3f337d594e.json new file mode 100644 index 0000000000..aa72f67202 --- /dev/null +++ b/nym-api/.sqlx/query-73ca856950a0157acfd3e2ed07b11aca3d875f67c77e2e7c75653c3f337d594e.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT timestamp, reliability as \"reliability: u8\"\n FROM mixnode_status\n JOIN mixnode_details\n ON mixnode_status.mixnode_details_id = mixnode_details.id\n WHERE mixnode_details.mix_id=? AND mixnode_status.timestamp > ?;\n ", + "describe": { + "columns": [ + { + "name": "timestamp", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "reliability: u8", + "ordinal": 1, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + true, + true + ] + }, + "hash": "73ca856950a0157acfd3e2ed07b11aca3d875f67c77e2e7c75653c3f337d594e" +} diff --git a/nym-api/.sqlx/query-76f3b7ac8537b204607175fd75a63fafad22d5b00caca9cadf9b72f9068760a7.json b/nym-api/.sqlx/query-76f3b7ac8537b204607175fd75a63fafad22d5b00caca9cadf9b72f9068760a7.json new file mode 100644 index 0000000000..1ec491b658 --- /dev/null +++ b/nym-api/.sqlx/query-76f3b7ac8537b204607175fd75a63fafad22d5b00caca9cadf9b72f9068760a7.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT serialised_signatures FROM partial_coin_index_signatures WHERE epoch_id = ?", + "describe": { + "columns": [ + { + "name": "serialised_signatures", + "ordinal": 0, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "76f3b7ac8537b204607175fd75a63fafad22d5b00caca9cadf9b72f9068760a7" +} diff --git a/nym-api/.sqlx/query-89e5b5b5c1df6609301189e245d93b8498ca8c9960a964fc0ae1d0b07ef2f20f.json b/nym-api/.sqlx/query-89e5b5b5c1df6609301189e245d93b8498ca8c9960a964fc0ae1d0b07ef2f20f.json new file mode 100644 index 0000000000..326c19bc53 --- /dev/null +++ b/nym-api/.sqlx/query-89e5b5b5c1df6609301189e245d93b8498ca8c9960a964fc0ae1d0b07ef2f20f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO verified_tickets(ticket_data, serial_number, spending_date, verified_at, gateway_id)\n VALUES (?, ?, ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "89e5b5b5c1df6609301189e245d93b8498ca8c9960a964fc0ae1d0b07ef2f20f" +} diff --git a/nym-api/.sqlx/query-8b1978f7cd1a6281cc0d6528f8ea004e1047fe42b7d74d53c617d20b886e54c1.json b/nym-api/.sqlx/query-8b1978f7cd1a6281cc0d6528f8ea004e1047fe42b7d74d53c617d20b886e54c1.json new file mode 100644 index 0000000000..ecdcbeb97b --- /dev/null +++ b/nym-api/.sqlx/query-8b1978f7cd1a6281cc0d6528f8ea004e1047fe42b7d74d53c617d20b886e54c1.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT timestamp, reliability as \"reliability: u8\"\n FROM mixnode_status\n WHERE mixnode_details_id=? AND timestamp > ? AND timestamp < ?;\n ", + "describe": { + "columns": [ + { + "name": "timestamp", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "reliability: u8", + "ordinal": 1, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + true, + true + ] + }, + "hash": "8b1978f7cd1a6281cc0d6528f8ea004e1047fe42b7d74d53c617d20b886e54c1" +} diff --git a/nym-api/.sqlx/query-924f8eb10c6cbb7f35da6c1bb77e1025442a594dcb5c6401b3dfac7df9c25073.json b/nym-api/.sqlx/query-924f8eb10c6cbb7f35da6c1bb77e1025442a594dcb5c6401b3dfac7df9c25073.json new file mode 100644 index 0000000000..c9cf3e5e09 --- /dev/null +++ b/nym-api/.sqlx/query-924f8eb10c6cbb7f35da6c1bb77e1025442a594dcb5c6401b3dfac7df9c25073.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT identity FROM gateway_details WHERE node_id = ?", + "describe": { + "columns": [ + { + "name": "identity", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "924f8eb10c6cbb7f35da6c1bb77e1025442a594dcb5c6401b3dfac7df9c25073" +} diff --git a/nym-api/.sqlx/query-93db1709eb08a8badc95ce94e1c28ba3da889468e4b12807aaad117a741d3f11.json b/nym-api/.sqlx/query-93db1709eb08a8badc95ce94e1c28ba3da889468e4b12807aaad117a741d3f11.json new file mode 100644 index 0000000000..6ef9cdf7d2 --- /dev/null +++ b/nym-api/.sqlx/query-93db1709eb08a8badc95ce94e1c28ba3da889468e4b12807aaad117a741d3f11.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT COUNT(*) as count FROM\n (\n SELECT monitor_run_id\n FROM testing_route\n WHERE testing_route.gateway_id = ?\n ) testing_route\n JOIN\n (\n SELECT id\n FROM monitor_run\n WHERE monitor_run.timestamp > ?\n ) monitor_run\n ON monitor_run.id = testing_route.monitor_run_id;\n ", + "describe": { + "columns": [ + { + "name": "count", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "93db1709eb08a8badc95ce94e1c28ba3da889468e4b12807aaad117a741d3f11" +} diff --git a/nym-api/.sqlx/query-975fb3c6e38d29f1c9a7d2d67a218bb48f577b228a6053a55b7ac8049993f4f3.json b/nym-api/.sqlx/query-975fb3c6e38d29f1c9a7d2d67a218bb48f577b228a6053a55b7ac8049993f4f3.json new file mode 100644 index 0000000000..6e21adcd90 --- /dev/null +++ b/nym-api/.sqlx/query-975fb3c6e38d29f1c9a7d2d67a218bb48f577b228a6053a55b7ac8049993f4f3.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO bloomfilter_parameters(num_hashes, bitmap_size,sip0_key0, sip0_key1, sip1_key0, sip1_key1)\n VALUES (?, ?, ?, ?, ?, ?)\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 6 + }, + "nullable": [] + }, + "hash": "975fb3c6e38d29f1c9a7d2d67a218bb48f577b228a6053a55b7ac8049993f4f3" +} diff --git a/nym-api/.sqlx/query-9e14089dbc02ea0f64545be769608c4d1ab1e01c3a0f7903cd2667738e0b77e7.json b/nym-api/.sqlx/query-9e14089dbc02ea0f64545be769608c4d1ab1e01c3a0f7903cd2667738e0b77e7.json new file mode 100644 index 0000000000..9b45447a08 --- /dev/null +++ b/nym-api/.sqlx/query-9e14089dbc02ea0f64545be769608c4d1ab1e01c3a0f7903cd2667738e0b77e7.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT serialised_signatures FROM global_coin_index_signatures WHERE epoch_id = ?", + "describe": { + "columns": [ + { + "name": "serialised_signatures", + "ordinal": 0, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "9e14089dbc02ea0f64545be769608c4d1ab1e01c3a0f7903cd2667738e0b77e7" +} diff --git a/nym-api/.sqlx/query-9f65b370360ff2e0891fdf89233932212254708fec2973eb4d621179d6b975f4.json b/nym-api/.sqlx/query-9f65b370360ff2e0891fdf89233932212254708fec2973eb4d621179d6b975f4.json new file mode 100644 index 0000000000..648001157d --- /dev/null +++ b/nym-api/.sqlx/query-9f65b370360ff2e0891fdf89233932212254708fec2973eb4d621179d6b975f4.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT date as \"date!: Date\", uptime as \"uptime!\"\n FROM gateway_historical_uptime\n JOIN gateway_details\n ON gateway_historical_uptime.gateway_details_id = gateway_details.id\n WHERE\n gateway_details.node_id = ?\n AND\n gateway_historical_uptime.date = ?\n ", + "describe": { + "columns": [ + { + "name": "date!: Date", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "uptime!", + "ordinal": 1, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + true, + true + ] + }, + "hash": "9f65b370360ff2e0891fdf89233932212254708fec2973eb4d621179d6b975f4" +} diff --git a/nym-api/.sqlx/query-a100a49b9f24231e915c9f8237183a52545bf6abaa75c2af40a5d63d3b599b97.json b/nym-api/.sqlx/query-a100a49b9f24231e915c9f8237183a52545bf6abaa75c2af40a5d63d3b599b97.json new file mode 100644 index 0000000000..591877b4bd --- /dev/null +++ b/nym-api/.sqlx/query-a100a49b9f24231e915c9f8237183a52545bf6abaa75c2af40a5d63d3b599b97.json @@ -0,0 +1,62 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id,\n epoch_id as \"epoch_id: u32\",\n deposit_id as \"deposit_id: DepositId\",\n partial_credential,\n signature,\n joined_private_commitments,\n expiration_date as \"expiration_date: Date\",\n ticketbook_type_repr as \"ticketbook_type_repr: u8\"\n FROM issued_ticketbook\n WHERE deposit_id = ?\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "epoch_id: u32", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "deposit_id: DepositId", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "partial_credential", + "ordinal": 3, + "type_info": "Blob" + }, + { + "name": "signature", + "ordinal": 4, + "type_info": "Blob" + }, + { + "name": "joined_private_commitments", + "ordinal": 5, + "type_info": "Blob" + }, + { + "name": "expiration_date: Date", + "ordinal": 6, + "type_info": "Date" + }, + { + "name": "ticketbook_type_repr: u8", + "ordinal": 7, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "a100a49b9f24231e915c9f8237183a52545bf6abaa75c2af40a5d63d3b599b97" +} diff --git a/nym-api/.sqlx/query-a4054504b9b6088af3221fc04ec79c547933f2fc022d818dc8498396025666f3.json b/nym-api/.sqlx/query-a4054504b9b6088af3221fc04ec79c547933f2fc022d818dc8498396025666f3.json new file mode 100644 index 0000000000..9e5582fd3b --- /dev/null +++ b/nym-api/.sqlx/query-a4054504b9b6088af3221fc04ec79c547933f2fc022d818dc8498396025666f3.json @@ -0,0 +1,62 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id,\n epoch_id as \"epoch_id: u32\",\n deposit_id as \"deposit_id: DepositId\",\n partial_credential,\n signature,\n joined_private_commitments,\n expiration_date as \"expiration_date: Date\",\n ticketbook_type_repr as \"ticketbook_type_repr: u8\"\n FROM issued_ticketbook\n WHERE id = ?\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "epoch_id: u32", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "deposit_id: DepositId", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "partial_credential", + "ordinal": 3, + "type_info": "Blob" + }, + { + "name": "signature", + "ordinal": 4, + "type_info": "Blob" + }, + { + "name": "joined_private_commitments", + "ordinal": 5, + "type_info": "Blob" + }, + { + "name": "expiration_date: Date", + "ordinal": 6, + "type_info": "Date" + }, + { + "name": "ticketbook_type_repr: u8", + "ordinal": 7, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "a4054504b9b6088af3221fc04ec79c547933f2fc022d818dc8498396025666f3" +} diff --git a/nym-api/.sqlx/query-a4558a100c6fa20855b3475acb88f9d48a84c539c779057da0d044430e3835f3.json b/nym-api/.sqlx/query-a4558a100c6fa20855b3475acb88f9d48a84c539c779057da0d044430e3835f3.json new file mode 100644 index 0000000000..191109e1fc --- /dev/null +++ b/nym-api/.sqlx/query-a4558a100c6fa20855b3475acb88f9d48a84c539c779057da0d044430e3835f3.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO ticket_providers(gateway_address) VALUES (?)", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "a4558a100c6fa20855b3475acb88f9d48a84c539c779057da0d044430e3835f3" +} diff --git a/nym-api/.sqlx/query-a59d870f936ab133f35bb0a49f8bc2406802120ef5135e86936d6f24155ff0b3.json b/nym-api/.sqlx/query-a59d870f936ab133f35bb0a49f8bc2406802120ef5135e86936d6f24155ff0b3.json new file mode 100644 index 0000000000..a17555d965 --- /dev/null +++ b/nym-api/.sqlx/query-a59d870f936ab133f35bb0a49f8bc2406802120ef5135e86936d6f24155ff0b3.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE partial_bloomfilter SET bitmap = ? WHERE date = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "a59d870f936ab133f35bb0a49f8bc2406802120ef5135e86936d6f24155ff0b3" +} diff --git a/nym-api/.sqlx/query-af45b384ee5caa4f9bac1fa314239a8455e023dcdea1b0b2674cd1ea48d65919.json b/nym-api/.sqlx/query-af45b384ee5caa4f9bac1fa314239a8455e023dcdea1b0b2674cd1ea48d65919.json new file mode 100644 index 0000000000..0e5185d864 --- /dev/null +++ b/nym-api/.sqlx/query-af45b384ee5caa4f9bac1fa314239a8455e023dcdea1b0b2674cd1ea48d65919.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO mixnode_details_v2(node_id, identity_key) VALUES (?, ?);\n SELECT id FROM mixnode_details_v2 WHERE node_id = ?;\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false + ] + }, + "hash": "af45b384ee5caa4f9bac1fa314239a8455e023dcdea1b0b2674cd1ea48d65919" +} diff --git a/nym-api/.sqlx/query-af7b333e919cf139670c9c6436531a6bd450652e2a4e09e8be910b58ad61ee14.json b/nym-api/.sqlx/query-af7b333e919cf139670c9c6436531a6bd450652e2a4e09e8be910b58ad61ee14.json new file mode 100644 index 0000000000..58eb2b5b63 --- /dev/null +++ b/nym-api/.sqlx/query-af7b333e919cf139670c9c6436531a6bd450652e2a4e09e8be910b58ad61ee14.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT AVG(reliability) as \"reliability: f32\" FROM gateway_status\n WHERE gateway_details_id= ? AND timestamp >= ? AND timestamp <= ?\n ", + "describe": { + "columns": [ + { + "name": "reliability: f32", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + true + ] + }, + "hash": "af7b333e919cf139670c9c6436531a6bd450652e2a4e09e8be910b58ad61ee14" +} diff --git a/nym-api/.sqlx/query-b657ab34d60dcda8a4bb1803db65d57002331d709068feac5ccad6632dbd046f.json b/nym-api/.sqlx/query-b657ab34d60dcda8a4bb1803db65d57002331d709068feac5ccad6632dbd046f.json new file mode 100644 index 0000000000..c7305db1cd --- /dev/null +++ b/nym-api/.sqlx/query-b657ab34d60dcda8a4bb1803db65d57002331d709068feac5ccad6632dbd046f.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT node_id as \"node_id: NodeId\" FROM gateway_details WHERE identity = ?", + "describe": { + "columns": [ + { + "name": "node_id: NodeId", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "b657ab34d60dcda8a4bb1803db65d57002331d709068feac5ccad6632dbd046f" +} diff --git a/nym-api/.sqlx/query-b72420d03ee03ee3506e7b2a97667f1481269877ef2eea32a673f4ba2fbdb498.json b/nym-api/.sqlx/query-b72420d03ee03ee3506e7b2a97667f1481269877ef2eea32a673f4ba2fbdb498.json new file mode 100644 index 0000000000..eb8e65e763 --- /dev/null +++ b/nym-api/.sqlx/query-b72420d03ee03ee3506e7b2a97667f1481269877ef2eea32a673f4ba2fbdb498.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT DISTINCT identity_key, mix_id as \"mix_id: NodeId\", id\n FROM mixnode_details\n JOIN mixnode_status\n ON mixnode_details.id = mixnode_status.mixnode_details_id\n WHERE EXISTS (\n SELECT 1 FROM mixnode_status WHERE timestamp > ? AND timestamp < ?\n )\n ", + "describe": { + "columns": [ + { + "name": "identity_key", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "mix_id: NodeId", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "id", + "ordinal": 2, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "b72420d03ee03ee3506e7b2a97667f1481269877ef2eea32a673f4ba2fbdb498" +} diff --git a/nym-api/.sqlx/query-c19e1b3768bf2929407599e6e8783ead09f4d7319b7997fa2a9bb628f9404166.json b/nym-api/.sqlx/query-c19e1b3768bf2929407599e6e8783ead09f4d7319b7997fa2a9bb628f9404166.json new file mode 100644 index 0000000000..0c6d2f6072 --- /dev/null +++ b/nym-api/.sqlx/query-c19e1b3768bf2929407599e6e8783ead09f4d7319b7997fa2a9bb628f9404166.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n d.mix_id as \"mix_id: NodeId\",\n AVG(s.reliability) as \"value: f32\"\n FROM\n mixnode_details d\n JOIN\n mixnode_status s on d.id = s.mixnode_details_id\n WHERE\n timestamp >= ? AND\n timestamp <= ?\n GROUP BY 1\n ", + "describe": { + "columns": [ + { + "name": "mix_id: NodeId", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "value: f32", + "ordinal": 1, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + true + ] + }, + "hash": "c19e1b3768bf2929407599e6e8783ead09f4d7319b7997fa2a9bb628f9404166" +} diff --git a/nym-api/.sqlx/query-c8927b6e408f470c2fc5d971e9a6a63bf5797c5d133414407b42bacbc75885db.json b/nym-api/.sqlx/query-c8927b6e408f470c2fc5d971e9a6a63bf5797c5d133414407b42bacbc75885db.json new file mode 100644 index 0000000000..fefa1d5f4e --- /dev/null +++ b/nym-api/.sqlx/query-c8927b6e408f470c2fc5d971e9a6a63bf5797c5d133414407b42bacbc75885db.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT serial_number\n FROM verified_tickets\n WHERE gateway_id = ?\n AND verified_at > ?\n ORDER BY verified_at ASC\n LIMIT 65535\n ", + "describe": { + "columns": [ + { + "name": "serial_number", + "ordinal": 0, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false + ] + }, + "hash": "c8927b6e408f470c2fc5d971e9a6a63bf5797c5d133414407b42bacbc75885db" +} diff --git a/nym-api/.sqlx/query-c89cfd911d0a2406e988bfcf95c5a6d398c23b20b6b91575f75fef228701c171.json b/nym-api/.sqlx/query-c89cfd911d0a2406e988bfcf95c5a6d398c23b20b6b91575f75fef228701c171.json new file mode 100644 index 0000000000..e1e680e7bf --- /dev/null +++ b/nym-api/.sqlx/query-c89cfd911d0a2406e988bfcf95c5a6d398c23b20b6b91575f75fef228701c171.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT OR IGNORE INTO gateway_details_v2(identity, node_id) VALUES (?, ?);\n SELECT id FROM gateway_details_v2 WHERE identity = ?;\n ", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false + ] + }, + "hash": "c89cfd911d0a2406e988bfcf95c5a6d398c23b20b6b91575f75fef228701c171" +} diff --git a/nym-api/.sqlx/query-cf202aa0360254ef961b98070970713387eb2b87d211827cc3e48c51e1104083.json b/nym-api/.sqlx/query-cf202aa0360254ef961b98070970713387eb2b87d211827cc3e48c51e1104083.json new file mode 100644 index 0000000000..a31cc32c92 --- /dev/null +++ b/nym-api/.sqlx/query-cf202aa0360254ef961b98070970713387eb2b87d211827cc3e48c51e1104083.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT AVG(reliability) as \"reliability: f32\" FROM mixnode_status_v2\n WHERE mixnode_details_id= ? AND timestamp >= ? AND timestamp <= ?\n ", + "describe": { + "columns": [ + { + "name": "reliability: f32", + "ordinal": 0, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + true + ] + }, + "hash": "cf202aa0360254ef961b98070970713387eb2b87d211827cc3e48c51e1104083" +} diff --git a/nym-api/.sqlx/query-d8d720912f74c3daf0d46dc4b5407e73d2584cc8a5a54e5dff82a0dbc1c9d908.json b/nym-api/.sqlx/query-d8d720912f74c3daf0d46dc4b5407e73d2584cc8a5a54e5dff82a0dbc1c9d908.json new file mode 100644 index 0000000000..7ef2c1c01d --- /dev/null +++ b/nym-api/.sqlx/query-d8d720912f74c3daf0d46dc4b5407e73d2584cc8a5a54e5dff82a0dbc1c9d908.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO gateway_status_v2 (gateway_details_id, reliability, timestamp) VALUES (?, ?, ?);\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "d8d720912f74c3daf0d46dc4b5407e73d2584cc8a5a54e5dff82a0dbc1c9d908" +} diff --git a/nym-api/.sqlx/query-d95a42f50209863e51537e98f0e74f6ca036890aa01c26d01167642ea2d8ff3d.json b/nym-api/.sqlx/query-d95a42f50209863e51537e98f0e74f6ca036890aa01c26d01167642ea2d8ff3d.json new file mode 100644 index 0000000000..3b80faa506 --- /dev/null +++ b/nym-api/.sqlx/query-d95a42f50209863e51537e98f0e74f6ca036890aa01c26d01167642ea2d8ff3d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE gateway_details SET node_id = ? WHERE identity = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "d95a42f50209863e51537e98f0e74f6ca036890aa01c26d01167642ea2d8ff3d" +} diff --git a/nym-api/.sqlx/query-dbf79ace08b8e6fe97df0899fd0177ba591e38b513ad5aa9ed4df70e6bf7afd7.json b/nym-api/.sqlx/query-dbf79ace08b8e6fe97df0899fd0177ba591e38b513ad5aa9ed4df70e6bf7afd7.json new file mode 100644 index 0000000000..a9160fa522 --- /dev/null +++ b/nym-api/.sqlx/query-dbf79ace08b8e6fe97df0899fd0177ba591e38b513ad5aa9ed4df70e6bf7afd7.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO master_verification_key(epoch_id, serialised_key) VALUES (?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "dbf79ace08b8e6fe97df0899fd0177ba591e38b513ad5aa9ed4df70e6bf7afd7" +} diff --git a/nym-api/.sqlx/query-dc78354f374f0fb71feef0b219cefb12a0b84ef905ab08ef0adcbc2535b3bbca.json b/nym-api/.sqlx/query-dc78354f374f0fb71feef0b219cefb12a0b84ef905ab08ef0adcbc2535b3bbca.json new file mode 100644 index 0000000000..ea3c140049 --- /dev/null +++ b/nym-api/.sqlx/query-dc78354f374f0fb71feef0b219cefb12a0b84ef905ab08ef0adcbc2535b3bbca.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO global_coin_index_signatures(epoch_id, serialised_signatures) VALUES (?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "dc78354f374f0fb71feef0b219cefb12a0b84ef905ab08ef0adcbc2535b3bbca" +} diff --git a/nym-api/.sqlx/query-dcc526c3855fea0ffbd73a0fb563cf10c707e356c767f8399452b56f044a1f6e.json b/nym-api/.sqlx/query-dcc526c3855fea0ffbd73a0fb563cf10c707e356c767f8399452b56f044a1f6e.json new file mode 100644 index 0000000000..3a8afcec25 --- /dev/null +++ b/nym-api/.sqlx/query-dcc526c3855fea0ffbd73a0fb563cf10c707e356c767f8399452b56f044a1f6e.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT COUNT(*) as count\n FROM gateway_status\n JOIN monitor_run ON gateway_status.timestamp = monitor_run.timestamp\n JOIN testing_route ON monitor_run.id = testing_route.monitor_run_id\n WHERE gateway_details_id = ?\n ", + "describe": { + "columns": [ + { + "name": "count", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "dcc526c3855fea0ffbd73a0fb563cf10c707e356c767f8399452b56f044a1f6e" +} diff --git a/nym-api/.sqlx/query-e55db4def70689c061d0e07115a21068431575afd2be8afafce1a7fb13507e7e.json b/nym-api/.sqlx/query-e55db4def70689c061d0e07115a21068431575afd2be8afafce1a7fb13507e7e.json new file mode 100644 index 0000000000..8d314d07f8 --- /dev/null +++ b/nym-api/.sqlx/query-e55db4def70689c061d0e07115a21068431575afd2be8afafce1a7fb13507e7e.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT EXISTS (SELECT 1 FROM mixnode_historical_uptime WHERE date = ?) AS 'exists'", + "describe": { + "columns": [ + { + "name": "exists", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + null + ] + }, + "hash": "e55db4def70689c061d0e07115a21068431575afd2be8afafce1a7fb13507e7e" +} diff --git a/nym-api/.sqlx/query-f055560483e843929fd93dc269f9650c707bc10db50aacc7988763320318bf2c.json b/nym-api/.sqlx/query-f055560483e843929fd93dc269f9650c707bc10db50aacc7988763320318bf2c.json new file mode 100644 index 0000000000..661d52138e --- /dev/null +++ b/nym-api/.sqlx/query-f055560483e843929fd93dc269f9650c707bc10db50aacc7988763320318bf2c.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT COUNT(*) as count FROM\n (\n SELECT monitor_run_id\n FROM testing_route\n WHERE testing_route.layer1_mix_id = ? OR testing_route.layer2_mix_id = ? OR testing_route.layer3_mix_id = ?\n ) testing_route\n JOIN\n (\n SELECT id\n FROM monitor_run\n WHERE monitor_run.timestamp > ?\n ) monitor_run\n ON monitor_run.id = testing_route.monitor_run_id;\n ", + "describe": { + "columns": [ + { + "name": "count", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 4 + }, + "nullable": [ + false + ] + }, + "hash": "f055560483e843929fd93dc269f9650c707bc10db50aacc7988763320318bf2c" +} diff --git a/nym-api/.sqlx/query-f19865730e1406df9bab136bab47c9bba85c3a07e7f5cd689a6926dcf269429d.json b/nym-api/.sqlx/query-f19865730e1406df9bab136bab47c9bba85c3a07e7f5cd689a6926dcf269429d.json new file mode 100644 index 0000000000..2409d0d31d --- /dev/null +++ b/nym-api/.sqlx/query-f19865730e1406df9bab136bab47c9bba85c3a07e7f5cd689a6926dcf269429d.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT serialised_key FROM master_verification_key WHERE epoch_id = ?", + "describe": { + "columns": [ + { + "name": "serialised_key", + "ordinal": 0, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "f19865730e1406df9bab136bab47c9bba85c3a07e7f5cd689a6926dcf269429d" +} diff --git a/nym-api/.sqlx/query-f777c5a8b0a4c2b516b00c4dc582a86e9c93587a7acab0ac5149579bb1914569.json b/nym-api/.sqlx/query-f777c5a8b0a4c2b516b00c4dc582a86e9c93587a7acab0ac5149579bb1914569.json new file mode 100644 index 0000000000..987eee80a8 --- /dev/null +++ b/nym-api/.sqlx/query-f777c5a8b0a4c2b516b00c4dc582a86e9c93587a7acab0ac5149579bb1914569.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT date as \"date!: Date\", uptime as \"uptime!\"\n FROM mixnode_historical_uptime\n JOIN mixnode_details\n ON mixnode_historical_uptime.mixnode_details_id = mixnode_details.id\n WHERE\n mixnode_details.mix_id = ?\n AND\n mixnode_historical_uptime.date = ?\n ", + "describe": { + "columns": [ + { + "name": "date!: Date", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "uptime!", + "ordinal": 1, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + true, + true + ] + }, + "hash": "f777c5a8b0a4c2b516b00c4dc582a86e9c93587a7acab0ac5149579bb1914569" +} diff --git a/nym-api/.sqlx/query-f853af3ddd1e48f9805d9bf685b0b624bfee5089e0681208aa4ef0d9ae9a896e.json b/nym-api/.sqlx/query-f853af3ddd1e48f9805d9bf685b0b624bfee5089e0681208aa4ef0d9ae9a896e.json new file mode 100644 index 0000000000..72064216a0 --- /dev/null +++ b/nym-api/.sqlx/query-f853af3ddd1e48f9805d9bf685b0b624bfee5089e0681208aa4ef0d9ae9a896e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "\n INSERT INTO mixnode_status (mixnode_details_id, reliability, timestamp) VALUES (?, ?, ?);\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "f853af3ddd1e48f9805d9bf685b0b624bfee5089e0681208aa4ef0d9ae9a896e" +} diff --git a/nym-api/.sqlx/query-f94d1b21ee833f0619ba1c26b747bed54392c9064ac3eff35ae32933221aa33f.json b/nym-api/.sqlx/query-f94d1b21ee833f0619ba1c26b747bed54392c9064ac3eff35ae32933221aa33f.json new file mode 100644 index 0000000000..3ef0dd10a6 --- /dev/null +++ b/nym-api/.sqlx/query-f94d1b21ee833f0619ba1c26b747bed54392c9064ac3eff35ae32933221aa33f.json @@ -0,0 +1,74 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n mixnode_details.id as \"db_id\",\n mix_id as \"mix_id!\",\n identity_key,\n reliability as \"reliability: u8\",\n monitor_run.timestamp as \"timestamp!\",\n gateway_id as \"gateway_id!\",\n layer1_mix_id as \"layer1_mix_id!\",\n layer2_mix_id as \"layer2_mix_id!\",\n layer3_mix_id as \"layer3_mix_id!\",\n monitor_run_id as \"monitor_run_id!\"\n FROM mixnode_status\n JOIN mixnode_details ON mixnode_status.mixnode_details_id = mixnode_details.id\n JOIN monitor_run ON mixnode_status.timestamp = monitor_run.timestamp\n JOIN testing_route ON monitor_run.id = testing_route.monitor_run_id\n WHERE mix_id = ?\n ORDER BY mixnode_status.timestamp DESC\n LIMIT ? OFFSET ?\n ", + "describe": { + "columns": [ + { + "name": "db_id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "mix_id!", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "identity_key", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "reliability: u8", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "timestamp!", + "ordinal": 4, + "type_info": "Int64" + }, + { + "name": "gateway_id!", + "ordinal": 5, + "type_info": "Int64" + }, + { + "name": "layer1_mix_id!", + "ordinal": 6, + "type_info": "Int64" + }, + { + "name": "layer2_mix_id!", + "ordinal": 7, + "type_info": "Int64" + }, + { + "name": "layer3_mix_id!", + "ordinal": 8, + "type_info": "Int64" + }, + { + "name": "monitor_run_id!", + "ordinal": 9, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 3 + }, + "nullable": [ + false, + false, + false, + true, + false, + true, + true, + true, + true, + true + ] + }, + "hash": "f94d1b21ee833f0619ba1c26b747bed54392c9064ac3eff35ae32933221aa33f" +} diff --git a/nym-api/.sqlx/query-fbf83213ff8ccd0b8c7de24a066842fdd1b57d9082e56d4c488cc12e415e5db7.json b/nym-api/.sqlx/query-fbf83213ff8ccd0b8c7de24a066842fdd1b57d9082e56d4c488cc12e415e5db7.json new file mode 100644 index 0000000000..632962c8c7 --- /dev/null +++ b/nym-api/.sqlx/query-fbf83213ff8ccd0b8c7de24a066842fdd1b57d9082e56d4c488cc12e415e5db7.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT serial_number\n FROM verified_tickets\n WHERE spending_date = ?\n ", + "describe": { + "columns": [ + { + "name": "serial_number", + "ordinal": 0, + "type_info": "Blob" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "fbf83213ff8ccd0b8c7de24a066842fdd1b57d9082e56d4c488cc12e415e5db7" +} diff --git a/nym-api/.sqlx/query-fe9c8ec99f8a9c6a41087c870fb9e27a24aada3a8ac681f4f8c7f58941e35502.json b/nym-api/.sqlx/query-fe9c8ec99f8a9c6a41087c870fb9e27a24aada3a8ac681f4f8c7f58941e35502.json new file mode 100644 index 0000000000..802a97ff5f --- /dev/null +++ b/nym-api/.sqlx/query-fe9c8ec99f8a9c6a41087c870fb9e27a24aada3a8ac681f4f8c7f58941e35502.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "DELETE FROM verified_tickets WHERE spending_date > ?", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "fe9c8ec99f8a9c6a41087c870fb9e27a24aada3a8ac681f4f8c7f58941e35502" +} diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 908ab7ef9c..fb8c58c9f1 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -4,14 +4,10 @@ [package] name = "nym-api" license = "GPL-3.0" -version = "1.1.45" -authors = [ - "Dave Hrycyszyn ", - "Jędrzej Stuczyński ", - "Drazen Urch ", -] +version = "1.1.46" +authors.workspace = true edition = "2021" -rust-version = "1.76.0" +rust-version.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -22,8 +18,9 @@ bip39 = { workspace = true } bincode.workspace = true bloomfilter = { workspace = true } cfg-if = { workspace = true } -clap = { workspace = true, features = ["cargo", "derive"] } +clap = { workspace = true, features = ["cargo", "derive", "env"] } console-subscriber = { workspace = true, optional = true } # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable" +dashmap = { workspace = true } dirs = { workspace = true } futures = { workspace = true } itertools = { workspace = true } @@ -31,16 +28,12 @@ humantime-serde = { workspace = true } k256 = { workspace = true, features = [ "ecdsa-core", ] } # needed for the Verifier trait; pull whatever version is used by other dependencies -log = { workspace = true } pin-project = { workspace = true } rand = { workspace = true } rand_chacha = { workspace = true } reqwest = { workspace = true, features = ["json"] } -rocket = { workspace = true, features = ["json"] } -rocket_cors = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -tap = { workspace = true } thiserror = { workspace = true } time = { workspace = true, features = ["serde-human-readable", "parsing"] } tokio = { workspace = true, features = [ @@ -66,20 +59,17 @@ sqlx = { workspace = true, features = [ "time", ] } -okapi = { workspace = true, features = ["impl_json_schema"] } -rocket_okapi = { workspace = true, features = ["swagger"] } schemars = { workspace = true, features = ["preserve_order"] } zeroize = { workspace = true } # for axum server -axum = { workspace = true, features = ["tokio"], optional = true } -axum-extra = { workspace = true, features = ["typed-header"], optional = true } -tower-http = { workspace = true, features = ["cors", "trace"], optional = true } -utoipa = { workspace = true, features = ["axum_extras", "time"], optional = true } -utoipa-swagger-ui = { workspace = true, features = ["axum"], optional = true} -utoipauto = { workspace = true, optional = true } -tracing-subscriber = { workspace = true, features = ["env-filter"], optional = true } -tracing = { workspace = true, optional = true } +axum = { workspace = true, features = ["tokio"] } +axum-extra = { workspace = true, features = ["typed-header"] } +tower-http = { workspace = true, features = ["cors", "trace"] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } +utoipauto = { workspace = true } +utoipa-swagger-ui = { workspace = true, features = ["axum"] } +tracing = { workspace = true } ## ephemera-specific #actix-web = "4" @@ -112,7 +102,7 @@ cw4 = { workspace = true } nym-dkg = { path = "../common/dkg", features = ["cw-types"] } nym-gateway-client = { path = "../common/client-libs/gateway-client" } nym-inclusion-probability = { path = "../common/inclusion-probability" } -nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"]} +nym-mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract", features = ["utoipa"] } nym-vesting-contract-common = { path = "../common/cosmwasm-smart-contracts/vesting-contract" } nym-contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" } nym-multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" } @@ -121,28 +111,19 @@ nym-sphinx = { path = "../common/nymsphinx" } nym-pemstore = { path = "../common/pemstore" } nym-task = { path = "../common/task" } nym-topology = { path = "../common/topology" } -nym-api-requests = { path = "nym-api-requests", features = ["rocket-traits"] } +nym-api-requests = { path = "nym-api-requests" } nym-validator-client = { path = "../common/client-libs/validator-client" } -nym-bin-common = { path = "../common/bin-common", features = ["output_format", "openapi"] } +nym-bin-common = { path = "../common/bin-common", features = ["output_format", "openapi", "basic_tracing"] } nym-node-tester-utils = { path = "../common/node-tester-utils" } nym-node-requests = { path = "../nym-node/nym-node-requests" } nym-types = { path = "../common/types" } nym-http-api-common = { path = "../common/http-api-common", features = ["utoipa"] } +nym-serde-helpers = { path = "../common/serde-helpers", features = ["date"] } [features] no-reward = [] +v2-performance = [] generate-ts = ["ts-rs"] -axum = ["dep:axum", - "axum-extra", - "tower-http", - "utoipa", - "utoipauto", - "tracing-subscriber", - "tracing", - "utoipa-swagger-ui", - "nym-http-api-common/utoipa", - "nym-mixnet-contract-common/utoipa" -] [build-dependencies] tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } @@ -154,6 +135,7 @@ sqlx = { workspace = true, features = [ ] } [dev-dependencies] +axum-test = "16.2.0" tempfile = { workspace = true } cw3 = { workspace = true } cw-utils = { workspace = true } diff --git a/nym-api/Rocket.toml b/nym-api/Rocket.toml deleted file mode 100644 index a094d2157a..0000000000 --- a/nym-api/Rocket.toml +++ /dev/null @@ -1,7 +0,0 @@ -[default] -limits = { forms = "64 kB", json = "1 MiB" } -port = 8080 -address = "127.0.0.1" - -[release] -address = "0.0.0.0" \ No newline at end of file diff --git a/nym-api/build.rs b/nym-api/build.rs index cdd97f9505..d6e0c45770 100644 --- a/nym-api/build.rs +++ b/nym-api/build.rs @@ -1,4 +1,4 @@ -use sqlx::{Connection, SqliteConnection}; +use sqlx::{Connection, FromRow, SqliteConnection}; use std::env; #[tokio::main] @@ -15,6 +15,42 @@ async fn main() { .await .expect("Failed to perform SQLx migrations"); + #[derive(FromRow)] + struct Exists { + exists: bool, + } + + // check if it was already run + let res: Exists = sqlx::query_as("SELECT EXISTS (SELECT 1 FROM v3_migration_info) AS 'exists'") + .fetch_one(&mut conn) + .await + .unwrap(); + + let already_run = res.exists; + + // execute the manual v3 migration + // it's performed on an empty storage, so we don't need to actually make any network queries + if !already_run { + sqlx::query( + r#" + CREATE TABLE gateway_details_temp + ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + node_id INTEGER NOT NULL UNIQUE, + identity VARCHAR NOT NULL UNIQUE + ); + + DROP TABLE gateway_details; + ALTER TABLE gateway_details_temp RENAME TO gateway_details; + + INSERT INTO v3_migration_info(id) VALUES (0); + "#, + ) + .execute(&mut conn) + .await + .expect("failed to update post v3 migration tables"); + } + #[cfg(target_family = "unix")] println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); diff --git a/nym-api/entrypoint.sh b/nym-api/entrypoint.sh new file mode 100755 index 0000000000..bf4c1942c3 --- /dev/null +++ b/nym-api/entrypoint.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +set -e + +/usr/src/nym/target/release/nym-api init && /usr/src/nym/target/release/nym-api run \ No newline at end of file diff --git a/nym-api/migrations/20240726120000_v3_changes.sql b/nym-api/migrations/20240726120000_v3_changes.sql new file mode 100644 index 0000000000..652729d215 --- /dev/null +++ b/nym-api/migrations/20240726120000_v3_changes.sql @@ -0,0 +1,24 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +-- we don't have to be keeping track of the gateway owner; it will make things easier in code +ALTER TABLE gateway_details DROP COLUMN owner; +ALTER TABLE mixnode_details DROP COLUMN owner; + +-- NOTE: this column is made `NOT NULL UNIQUE` in code during `migrate_v3_database` call! +ALTER TABLE gateway_details ADD node_id INTEGER; + +-- a hacky table-flag to indicate whether the v3 migration has been run +CREATE TABLE v3_migration_info ( + id INTEGER PRIMARY KEY CHECK (id = 0) +); + +--CREATE TABLE node_historical_performance ( +-- contract_node_id INTEGER NOT NULL, +-- date DATE NOT NULL, +-- performance FLOAT NOT NULL, +-- +-- UNIQUE(contract_node_id, date); +--) \ No newline at end of file diff --git a/nym-api/nym-api-requests/Cargo.toml b/nym-api/nym-api-requests/Cargo.toml index 70943d6fba..f2873b9173 100644 --- a/nym-api/nym-api-requests/Cargo.toml +++ b/nym-api/nym-api-requests/Cargo.toml @@ -11,9 +11,9 @@ bs58 = { workspace = true } cosmrs = { workspace = true } cosmwasm-std = { workspace = true } getset = { workspace = true } -rocket = { workspace = true, optional = true } schemars = { workspace = true, features = ["preserve_order"] } serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } sha2.workspace = true tendermint = { workspace = true } thiserror.workspace = true @@ -32,12 +32,9 @@ nym-ecash-time = { path = "../../common/ecash-time" } nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } nym-mixnet-contract-common = { path = "../../common/cosmwasm-smart-contracts/mixnet-contract" } nym-node-requests = { path = "../../nym-node/nym-node-requests", default-features = false, features = ["openapi"] } +nym-network-defaults = { path = "../../common/network-defaults" } -[dev-dependencies] -serde_json.workspace = true - [features] default = [] -rocket-traits = ["rocket"] generate-ts = ["ts-rs", "nym-mixnet-contract-common/generate-ts"] diff --git a/nym-api/nym-api-requests/src/ecash/models.rs b/nym-api/nym-api-requests/src/ecash/models.rs index 64c1a2dccb..35d6df7a07 100644 --- a/nym-api/nym-api-requests/src/ecash/models.rs +++ b/nym-api/nym-api-requests/src/ecash/models.rs @@ -383,7 +383,7 @@ pub struct EpochCredentialsResponse { #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema, ToSchema)] #[serde(rename_all = "camelCase")] pub struct IssuedCredentialsResponse { - // note: BTreeMap returns ordered results so it's fine to use it with pagination + // note: BTreeMap returns ordered results, so it's fine to use it with pagination pub credentials: BTreeMap, } diff --git a/nym-api/nym-api-requests/src/legacy.rs b/nym-api/nym-api-requests/src/legacy.rs new file mode 100644 index 0000000000..f0810ffa8e --- /dev/null +++ b/nym-api/nym-api-requests/src/legacy.rs @@ -0,0 +1,80 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use cosmwasm_std::Decimal; +use nym_mixnet_contract_common::mixnode::LegacyPendingMixNodeChanges; +use nym_mixnet_contract_common::{GatewayBond, LegacyMixLayer, MixNodeBond, NodeId, NodeRewarding}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::ops::Deref; +use utoipa::ToSchema; + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct LegacyGatewayBondWithId { + // we need to flatten it so that consumers of endpoints that returned `GatewayBond` wouldn't break + #[serde(flatten)] + pub bond: GatewayBond, + pub node_id: NodeId, +} + +impl Deref for LegacyGatewayBondWithId { + type Target = GatewayBond; + fn deref(&self) -> &Self::Target { + &self.bond + } +} + +impl From for GatewayBond { + fn from(value: LegacyGatewayBondWithId) -> Self { + value.bond + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, ToSchema)] +pub struct LegacyMixNodeBondWithLayer { + // we need to flatten it so that consumers of endpoints that returned `MixNodeBond` wouldn't break + #[serde(flatten)] + pub bond: MixNodeBond, + + pub layer: LegacyMixLayer, +} + +impl Deref for LegacyMixNodeBondWithLayer { + type Target = MixNodeBond; + fn deref(&self) -> &Self::Target { + &self.bond + } +} + +impl From for MixNodeBond { + fn from(value: LegacyMixNodeBondWithLayer) -> Self { + value.bond + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +pub struct LegacyMixNodeDetailsWithLayer { + /// Basic bond information of this mixnode, such as owner address, original pledge, etc. + pub bond_information: LegacyMixNodeBondWithLayer, + + /// Details used for computation of rewarding related data. + pub rewarding_details: NodeRewarding, + + /// Adjustments to the mixnode that are ought to happen during future epoch transitions. + #[serde(default)] + pub pending_changes: LegacyPendingMixNodeChanges, +} + +impl LegacyMixNodeDetailsWithLayer { + pub fn mix_id(&self) -> NodeId { + self.bond_information.mix_id + } + + pub fn total_stake(&self) -> Decimal { + self.rewarding_details.node_bond() + } + + pub fn is_unbonding(&self) -> bool { + self.bond_information.is_unbonding + } +} diff --git a/nym-api/nym-api-requests/src/lib.rs b/nym-api/nym-api-requests/src/lib.rs index 7ea5db1f6d..664eb59fff 100644 --- a/nym-api/nym-api-requests/src/lib.rs +++ b/nym-api/nym-api-requests/src/lib.rs @@ -7,6 +7,7 @@ use serde::{Deserialize, Serialize}; pub mod constants; pub mod ecash; mod helpers; +pub mod legacy; pub mod models; pub mod nym_nodes; pub mod pagination; diff --git a/nym-api/nym-api-requests/src/models.rs b/nym-api/nym-api-requests/src/models.rs index f42c8cb764..c21fe448c7 100644 --- a/nym-api/nym-api-requests/src/models.rs +++ b/nym-api/nym-api-requests/src/models.rs @@ -2,17 +2,29 @@ // SPDX-License-Identifier: Apache-2.0 use crate::helpers::unix_epoch; -use crate::nym_nodes::NodeRole; +use crate::helpers::PlaceholderJsonSchemaImpl; +use crate::legacy::{ + LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, +}; +use crate::nym_nodes::{BasicEntryInformation, NodeRole, SkimmedNode}; use crate::pagination::PaginatedResponse; use cosmwasm_std::{Addr, Coin, Decimal, Uint128}; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_mixnet_contract_common::mixnode::MixNodeDetails; +use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey}; +use nym_crypto::asymmetric::x25519::{ + self, + serde_helpers::{bs58_x25519_pubkey, option_bs58_x25519_pubkey}, +}; +use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::{Performance, RewardingParams}; use nym_mixnet_contract_common::rewarding::RewardEstimate; -use nym_mixnet_contract_common::{ - GatewayBond, IdentityKey, Interval, MixId, MixNode, MixNodeBond, Percent, RewardedSetNodeStatus, +use nym_mixnet_contract_common::{GatewayBond, IdentityKey, Interval, MixNode, NodeId, Percent}; +use nym_network_defaults::{DEFAULT_MIX_LISTENING_PORT, DEFAULT_VERLOC_LISTENING_PORT}; +use nym_node_requests::api::v1::authenticator::models::Authenticator; +use nym_node_requests::api::v1::gateway::models::Wireguard; +use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; +use nym_node_requests::api::v1::node::models::{ + AuxiliaryDetails, BinaryBuildInformationOwned, NodeRoles, }; -use nym_node_requests::api::v1::node::models::{AuxiliaryDetails, BinaryBuildInformationOwned}; use schemars::gen::SchemaGenerator; use schemars::schema::{InstanceType, Schema, SchemaObject}; use schemars::JsonSchema; @@ -21,7 +33,8 @@ use std::fmt::{Debug, Display, Formatter}; use std::net::IpAddr; use std::ops::{Deref, DerefMut}; use std::{fmt, time::Duration}; -use time::OffsetDateTime; +use thiserror::Error; +use time::{Date, OffsetDateTime}; use utoipa::{IntoParams, ToResponse, ToSchema}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] @@ -57,7 +70,10 @@ impl Display for RequestError { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixnodeStatus.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/MixnodeStatus.ts" + ) )] #[serde(rename_all = "snake_case")] pub enum MixnodeStatus { @@ -66,18 +82,6 @@ pub enum MixnodeStatus { Inactive, // in neither the rewarded set nor the active set, but is bonded NotFound, // doesn't even exist in the bonded set } - -impl From for Option { - fn from(status: MixnodeStatus) -> Self { - match status { - MixnodeStatus::Active => Some(RewardedSetNodeStatus::Active), - MixnodeStatus::Standby => Some(RewardedSetNodeStatus::Standby), - MixnodeStatus::Inactive => None, - MixnodeStatus::NotFound => None, - } - } -} - impl MixnodeStatus { pub fn is_active(&self) -> bool { *self == MixnodeStatus::Active @@ -88,10 +92,13 @@ impl MixnodeStatus { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/MixnodeCoreStatusResponse.ts" + ) )] pub struct MixnodeCoreStatusResponse { - pub mix_id: MixId, + pub mix_id: NodeId, pub count: i32, } @@ -99,7 +106,10 @@ pub struct MixnodeCoreStatusResponse { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/GatewayCoreStatusResponse.ts" + ) )] pub struct GatewayCoreStatusResponse { pub identity: String, @@ -110,7 +120,10 @@ pub struct GatewayCoreStatusResponse { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/MixnodeStatusResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/MixnodeStatusResponse.ts" + ) )] pub struct MixnodeStatusResponse { pub status: MixnodeStatus, @@ -126,23 +139,135 @@ pub struct NodePerformance { pub last_24h: Performance, } -#[derive(ToSchema)] -#[schema(title = "MixNodeDetails")] -pub struct MixNodeDetailsSchema { +#[derive(Serialize, Deserialize, Debug, Clone, Copy, Hash, JsonSchema)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export, export_to = "ts-packages/types/src/types/rust/DisplayRole.ts") +)] +pub enum DisplayRole { + EntryGateway, + Layer1, + Layer2, + Layer3, + ExitGateway, + Standby, +} + +impl From for DisplayRole { + fn from(role: Role) -> Self { + match role { + Role::EntryGateway => DisplayRole::EntryGateway, + Role::Layer1 => DisplayRole::Layer1, + Role::Layer2 => DisplayRole::Layer2, + Role::Layer3 => DisplayRole::Layer3, + Role::ExitGateway => DisplayRole::ExitGateway, + Role::Standby => DisplayRole::Standby, + } + } +} + +impl From for Role { + fn from(role: DisplayRole) -> Self { + match role { + DisplayRole::EntryGateway => Role::EntryGateway, + DisplayRole::Layer1 => Role::Layer1, + DisplayRole::Layer2 => Role::Layer2, + DisplayRole::Layer3 => Role::Layer3, + DisplayRole::ExitGateway => Role::ExitGateway, + DisplayRole::Standby => Role::Standby, + } + } +} + +// imo for now there's no point in exposing more than that, +// nym-api shouldn't be calculating apy or stake saturation for you. +// it should just return its own metrics (performance) and then you can do with it as you wish +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/NodeAnnotation.ts" + ) +)] +pub struct NodeAnnotation { + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub last_24h_performance: Performance, + pub current_role: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/AnnotationResponse.ts" + ) +)] +pub struct AnnotationResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub annotation: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/NodePerformanceResponse.ts" + ) +)] +pub struct NodePerformanceResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub performance: Option, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/NodeDatePerformanceResponse.ts" + ) +)] +pub struct NodeDatePerformanceResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub date: Date, + pub performance: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[schema(title = "LegacyMixNodeDetailsWithLayer")] +pub struct LegacyMixNodeDetailsWithLayerSchema { /// Basic bond information of this mixnode, such as owner address, original pledge, etc. + #[schema(example = "unimplemented schema")] pub bond_information: String, /// Details used for computation of rewarding related data. + #[schema(example = "unimplemented schema")] pub rewarding_details: String, /// Adjustments to the mixnode that are ought to happen during future epoch transitions. + #[schema(example = "unimplemented schema")] pub pending_changes: String, } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct MixNodeBondAnnotated { - #[schema(value_type = MixNodeDetailsSchema)] - pub mixnode_details: MixNodeDetails, + #[schema(value_type = LegacyMixNodeDetailsWithLayerSchema)] + pub mixnode_details: LegacyMixNodeDetailsWithLayer, #[schema(value_type = String)] pub stake_saturation: StakeSaturation, #[schema(value_type = String)] @@ -153,7 +278,6 @@ pub struct MixNodeBondAnnotated { pub node_performance: NodePerformance, pub estimated_operator_apy: Decimal, pub estimated_delegators_apy: Decimal, - pub family: Option, pub blacklisted: bool, // a rather temporary thing until we query self-described endpoints of mixnodes @@ -161,12 +285,21 @@ pub struct MixNodeBondAnnotated { pub ip_addresses: Vec, } +#[derive(Debug, Error)] +pub enum MalformedNodeBond { + #[error("the associated ed25519 identity key is malformed")] + InvalidEd25519Key, + + #[error("the associated x25519 sphinx key is malformed")] + InvalidX25519Key, +} + impl MixNodeBondAnnotated { pub fn mix_node(&self) -> &MixNode { &self.mixnode_details.bond_information.mix_node } - pub fn mix_id(&self) -> MixId { + pub fn mix_id(&self) -> NodeId { self.mixnode_details.mix_id() } @@ -177,11 +310,41 @@ impl MixNodeBondAnnotated { pub fn owner(&self) -> &Addr { self.mixnode_details.bond_information.owner() } + + pub fn version(&self) -> &str { + &self.mixnode_details.bond_information.mix_node.version + } + + pub fn try_to_skimmed_node(&self, role: NodeRole) -> Result { + Ok(SkimmedNode { + node_id: self.mix_id(), + ed25519_identity_pubkey: self + .identity_key() + .parse() + .map_err(|_| MalformedNodeBond::InvalidEd25519Key)?, + ip_addresses: self.ip_addresses.clone(), + mix_port: self.mix_node().mix_port, + x25519_sphinx_pubkey: self + .mix_node() + .sphinx_key + .parse() + .map_err(|_| MalformedNodeBond::InvalidX25519Key)?, + role, + supported_roles: DeclaredRoles { + mixnode: true, + entry: false, + exit_nr: false, + exit_ipr: false, + }, + entry: None, + performance: self.node_performance.last_24h, + }) + } } #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct GatewayBondAnnotated { - pub gateway_bond: GatewayBond, + pub gateway_bond: LegacyGatewayBondWithId, #[serde(default)] pub self_described: Option, @@ -196,12 +359,49 @@ pub struct GatewayBondAnnotated { } impl GatewayBondAnnotated { + pub fn version(&self) -> &str { + &self.gateway_bond.gateway.version + } + pub fn identity(&self) -> &String { - self.gateway_bond.identity() + self.gateway_bond.bond.identity() } pub fn owner(&self) -> &Addr { - self.gateway_bond.owner() + self.gateway_bond.bond.owner() + } + + pub fn try_to_skimmed_node(&self, role: NodeRole) -> Result { + Ok(SkimmedNode { + node_id: self.gateway_bond.node_id, + ip_addresses: self.ip_addresses.clone(), + ed25519_identity_pubkey: self + .gateway_bond + .gateway + .identity_key + .parse() + .map_err(|_| MalformedNodeBond::InvalidEd25519Key)?, + mix_port: self.gateway_bond.bond.gateway.mix_port, + x25519_sphinx_pubkey: self + .gateway_bond + .gateway + .sphinx_key + .parse() + .map_err(|_| MalformedNodeBond::InvalidX25519Key)?, + role, + supported_roles: DeclaredRoles { + mixnode: false, + entry: true, + exit_nr: false, + exit_ipr: false, + }, + entry: Some(BasicEntryInformation { + hostname: None, + ws_port: self.gateway_bond.bond.gateway.clients_port, + wss_port: None, + }), + performance: self.node_performance.last_24h, + }) } } @@ -226,7 +426,10 @@ pub struct ComputeRewardEstParam { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/RewardEstimationResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/RewardEstimationResponse.ts" + ) )] #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct RewardEstimationResponse { @@ -240,7 +443,7 @@ pub struct RewardEstimationResponse { #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] pub struct UptimeResponse { #[schema(value_type = u32)] - pub mix_id: MixId, + pub mix_id: NodeId, // The same as node_performance.last_24h. Legacy pub avg_uptime: u8, pub node_performance: NodePerformance, @@ -258,7 +461,10 @@ pub struct GatewayUptimeResponse { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/StakeSaturationResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/StakeSaturationResponse.ts" + ) )] pub struct StakeSaturationResponse { #[cfg_attr(feature = "generate-ts", ts(type = "string"))] @@ -277,7 +483,10 @@ pub type StakeSaturation = Decimal; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/SelectionChance.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/SelectionChance.ts" + ) )] pub enum SelectionChance { High, @@ -319,7 +528,10 @@ impl fmt::Display for SelectionChance { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "ts-packages/types/src/types/rust/InclusionProbabilityResponse.ts") + ts( + export, + export_to = "ts-packages/types/src/types/rust/InclusionProbabilityResponse.ts" + ) )] pub struct InclusionProbabilityResponse { pub in_active: SelectionChance, @@ -349,7 +561,7 @@ pub struct AllInclusionProbabilitiesResponse { #[derive(Clone, Serialize, schemars::JsonSchema, ToSchema)] pub struct InclusionProbability { #[schema(value_type = u32)] - pub mix_id: MixId, + pub mix_id: NodeId, pub in_active: f64, pub in_reserve: f64, } @@ -358,7 +570,7 @@ type Uptime = u8; #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct MixnodeStatusReportResponse { - pub mix_id: MixId, + pub mix_id: NodeId, pub identity: IdentityKey, pub owner: String, pub most_recent: Uptime, @@ -378,8 +590,74 @@ pub struct GatewayStatusReportResponse { pub last_day: Uptime, } +#[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/PerformanceHistoryResponse.ts" + ) +)] +pub struct PerformanceHistoryResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub history: PaginatedResponse, +} + +#[derive(Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/UptimeHistoryResponse.ts" + ) +)] +pub struct UptimeHistoryResponse { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub history: PaginatedResponse, +} + #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/HistoricalUptimeResponse.ts" + ) +)] pub struct HistoricalUptimeResponse { + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub date: Date, + + pub uptime: Uptime, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/HistoricalPerformanceResponse.ts" + ) +)] +pub struct HistoricalPerformanceResponse { + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + #[cfg_attr(feature = "generate-ts", ts(type = "string"))] + pub date: Date, + + pub performance: f64, +} + +#[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct OldHistoricalUptimeResponse { pub date: String, #[schema(value_type = u8)] pub uptime: Uptime, @@ -387,17 +665,17 @@ pub struct HistoricalUptimeResponse { #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct MixnodeUptimeHistoryResponse { - pub mix_id: MixId, + pub mix_id: NodeId, pub identity: String, pub owner: String, - pub history: Vec, + pub history: Vec, } #[derive(Clone, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct GatewayUptimeHistoryResponse { pub identity: String, pub owner: String, - pub history: Vec, + pub history: Vec, } #[derive(ToSchema)] @@ -439,8 +717,18 @@ impl From for HostInf #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct HostKeys { - pub ed25519: String, - pub x25519: String, + #[serde(with = "bs58_ed25519_pubkey")] + #[schemars(with = "String")] + pub ed25519: ed25519::PublicKey, + + #[serde(with = "bs58_x25519_pubkey")] + #[schemars(with = "String")] + pub x25519: x25519::PublicKey, + + #[serde(default)] + #[serde(with = "option_bs58_x25519_pubkey")] + #[schemars(with = "Option")] + pub x25519_noise: Option, } impl From for HostKeys { @@ -448,6 +736,7 @@ impl From for HostKeys { HostKeys { ed25519: value.ed25519_identity, x25519: value.x25519_sphinx, + x25519_noise: value.x25519_noise, } } } @@ -543,14 +832,122 @@ impl JsonSchema for OffsetDateTimeJsonSchemaWrapper { } } -// this struct is getting quite bloated... #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct NymNodeDescription { + #[schema(value_type = u32)] + pub node_id: NodeId, + pub contract_node_type: DescribedNodeType, + pub description: NymNodeData, +} + +impl NymNodeDescription { + pub fn version(&self) -> &str { + &self.description.build_information.build_version + } + + pub fn entry_information(&self) -> BasicEntryInformation { + BasicEntryInformation { + hostname: self.description.host_information.hostname.clone(), + ws_port: self.description.mixnet_websockets.ws_port, + wss_port: self.description.mixnet_websockets.wss_port, + } + } + + pub fn ed25519_identity_key(&self) -> ed25519::PublicKey { + self.description.host_information.keys.ed25519 + } + + pub fn to_skimmed_node(&self, role: NodeRole, performance: Performance) -> SkimmedNode { + let keys = &self.description.host_information.keys; + let entry = if self.description.declared_role.entry { + Some(self.entry_information()) + } else { + None + }; + + SkimmedNode { + node_id: self.node_id, + ed25519_identity_pubkey: keys.ed25519, + ip_addresses: self.description.host_information.ip_address.clone(), + mix_port: self.description.mix_port(), + x25519_sphinx_pubkey: keys.x25519, + // we can't use the declared roles, we have to take whatever was provided in the contract. + // why? say this node COULD operate as an exit, but it might be the case the contract decided + // to assign it an ENTRY role only. we have to use that one instead. + role, + supported_roles: self.description.declared_role, + entry, + performance, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[serde(rename_all = "snake_case")] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/DescribedNodeType.ts" + ) +)] +pub enum DescribedNodeType { + LegacyMixnode, + LegacyGateway, + NymNode, +} + +impl DescribedNodeType { + pub fn is_nym_node(&self) -> bool { + matches!(self, DescribedNodeType::NymNode) + } +} + +#[derive(Clone, Copy, Debug, Default, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/DeclaredRoles.ts" + ) +)] +pub struct DeclaredRoles { + pub mixnode: bool, + pub entry: bool, + pub exit_nr: bool, + pub exit_ipr: bool, +} + +impl DeclaredRoles { + pub fn can_operate_exit_gateway(&self) -> bool { + self.exit_ipr && self.exit_nr + } +} + +impl From for DeclaredRoles { + fn from(value: NodeRoles) -> Self { + DeclaredRoles { + mixnode: value.mixnode_enabled, + entry: value.gateway_enabled, + exit_nr: value.gateway_enabled && value.network_requester_enabled, + exit_ipr: value.gateway_enabled && value.ip_packet_router_enabled, + } + } +} + +// this struct is getting quite bloated... +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct NymNodeData { #[serde(default)] pub last_polled: OffsetDateTimeJsonSchemaWrapper, pub host_information: HostInformation, + #[serde(default)] + pub declared_role: DeclaredRoles, + #[serde(default)] pub auxiliary_details: AuxiliaryDetails, @@ -571,40 +968,48 @@ pub struct NymNodeDescription { // for now we only care about their ws/wss situation, nothing more pub mixnet_websockets: WebSockets, - - #[serde(default = "default_node_role")] - pub role: NodeRole, } -// For backwards compatibility, set a slightly artificial default -fn default_node_role() -> NodeRole { - NodeRole::Inactive +impl NymNodeData { + pub fn mix_port(&self) -> u16 { + self.auxiliary_details + .announce_ports + .mix_port + .unwrap_or(DEFAULT_MIX_LISTENING_PORT) + } + + pub fn verloc_port(&self) -> u16 { + self.auxiliary_details + .announce_ports + .verloc_port + .unwrap_or(DEFAULT_VERLOC_LISTENING_PORT) + } } #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct DescribedGateway { +pub struct LegacyDescribedGateway { pub bond: GatewayBond, - pub self_described: Option, + pub self_described: Option, } -impl From for DescribedGateway { - fn from(bond: GatewayBond) -> Self { - DescribedGateway { - bond, +impl From for LegacyDescribedGateway { + fn from(bond: LegacyGatewayBondWithId) -> Self { + LegacyDescribedGateway { + bond: bond.bond, self_described: None, } } } #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] -pub struct DescribedMixNode { - pub bond: MixNodeBond, - pub self_described: Option, +pub struct LegacyDescribedMixNode { + pub bond: LegacyMixNodeBondWithLayer, + pub self_described: Option, } -impl From for DescribedMixNode { - fn from(bond: MixNodeBond) -> Self { - DescribedMixNode { +impl From for LegacyDescribedMixNode { + fn from(bond: LegacyMixNodeBondWithLayer) -> Self { + LegacyDescribedMixNode { bond, self_described: None, } @@ -626,18 +1031,45 @@ pub struct IpPacketRouterDetails { pub address: String, } +// works for current simple case. +impl From for IpPacketRouterDetails { + fn from(value: IpPacketRouter) -> Self { + IpPacketRouterDetails { + address: value.address, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct AuthenticatorDetails { /// address of the embedded authenticator pub address: String, } +// works for current simple case. +impl From for AuthenticatorDetails { + fn from(value: Authenticator) -> Self { + AuthenticatorDetails { + address: value.address, + } + } +} #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct WireguardDetails { pub port: u16, pub public_key: String, } +// works for current simple case. +impl From for WireguardDetails { + fn from(value: Wireguard) -> Self { + WireguardDetails { + port: value.port, + public_key: value.public_key, + } + } +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct ApiHealthResponse { pub status: ApiStatus, @@ -701,6 +1133,78 @@ pub struct PartialTestResult { pub type MixnodeTestResultResponse = PaginatedResponse; pub type GatewayTestResultResponse = PaginatedResponse; +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct NoiseDetails { + #[schemars(with = "String")] + #[serde(with = "bs58_x25519_pubkey")] + pub x25119_pubkey: x25519::PublicKey, + + pub mixnet_port: u16, + + pub ip_addresses: Vec, +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] +pub struct NodeRefreshBody { + #[serde(with = "bs58_ed25519_pubkey")] + #[schemars(with = "String")] + pub node_identity: ed25519::PublicKey, + + // a poor man's nonce + pub request_timestamp: i64, + + #[schemars(with = "PlaceholderJsonSchemaImpl")] + pub signature: ed25519::Signature, +} + +impl NodeRefreshBody { + pub fn plaintext(node_identity: ed25519::PublicKey, request_timestamp: i64) -> Vec { + node_identity + .to_bytes() + .into_iter() + .chain(request_timestamp.to_be_bytes()) + .chain(b"describe-cache-refresh-request".iter().copied()) + .collect() + } + + pub fn new(private_key: &ed25519::PrivateKey) -> Self { + let node_identity = private_key.public_key(); + let request_timestamp = OffsetDateTime::now_utc().unix_timestamp(); + let signature = private_key.sign(Self::plaintext(node_identity, request_timestamp)); + NodeRefreshBody { + node_identity, + request_timestamp, + signature, + } + } + + pub fn verify_signature(&self) -> bool { + self.node_identity + .verify( + Self::plaintext(self.node_identity, self.request_timestamp), + &self.signature, + ) + .is_ok() + } + + pub fn is_stale(&self) -> bool { + let Ok(encoded) = OffsetDateTime::from_unix_timestamp(self.request_timestamp) else { + return true; + }; + let now = OffsetDateTime::now_utc(); + + if encoded > now { + return true; + } + + if (encoded + Duration::from_secs(30)) < now { + return true; + } + + false + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/nym-api/nym-api-requests/src/nym_nodes.rs b/nym-api/nym-api-requests/src/nym_nodes.rs index 3a4ea40ca9..fcfd09c2be 100644 --- a/nym-api/nym-api-requests/src/nym_nodes.rs +++ b/nym-api/nym-api-requests/src/nym_nodes.rs @@ -1,13 +1,17 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::models::{ - GatewayBondAnnotated, MixNodeBondAnnotated, NymNodeDescription, OffsetDateTimeJsonSchemaWrapper, -}; +use crate::models::{DeclaredRoles, NymNodeData, OffsetDateTimeJsonSchemaWrapper}; +use crate::pagination::{PaginatedResponse, Pagination}; +use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey; +use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey; +use nym_crypto::asymmetric::{ed25519, x25519}; +use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use serde::{Deserialize, Serialize}; use std::net::IpAddr; +use time::OffsetDateTime; use utoipa::ToSchema; #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] @@ -16,9 +20,48 @@ pub struct CachedNodesResponse { pub nodes: Vec, } -#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] +impl From> for CachedNodesResponse { + fn from(nodes: Vec) -> Self { + CachedNodesResponse::new(nodes) + } +} + +impl CachedNodesResponse { + pub fn new(nodes: Vec) -> Self { + CachedNodesResponse { + refreshed_at: OffsetDateTime::now_utc().into(), + nodes, + } + } +} + +#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema)] +pub struct PaginatedCachedNodesResponse { + pub refreshed_at: OffsetDateTimeJsonSchemaWrapper, + pub nodes: PaginatedResponse, +} + +impl PaginatedCachedNodesResponse { + pub fn new_full( + refreshed_at: impl Into, + nodes: Vec, + ) -> Self { + PaginatedCachedNodesResponse { + refreshed_at: refreshed_at.into(), + nodes: PaginatedResponse { + pagination: Pagination { + total: nodes.len(), + page: 0, + size: nodes.len(), + }, + data: nodes, + }, + } + } +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)] #[serde(rename_all = "kebab-case")] -#[cfg_attr(feature = "rocket-traits", derive(rocket::form::FromFormField))] pub enum NodeRoleQueryParam { ActiveMixnode, @@ -48,6 +91,26 @@ pub enum NodeRole { Inactive, } +impl NodeRole { + pub fn is_inactive(&self) -> bool { + matches!(self, NodeRole::Inactive) + } +} + +impl From> for NodeRole { + fn from(role: Option) -> Self { + match role { + Some(Role::EntryGateway) => NodeRole::EntryGateway, + Some(Role::Layer1) => NodeRole::Mixnode { layer: 1 }, + Some(Role::Layer2) => NodeRole::Mixnode { layer: 2 }, + Some(Role::Layer3) => NodeRole::Mixnode { layer: 3 }, + Some(Role::ExitGateway) => NodeRole::ExitGateway, + Some(Role::Standby) => NodeRole::Standby, + None => NodeRole::Inactive, + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct BasicEntryInformation { pub hostname: Option, @@ -56,8 +119,6 @@ pub struct BasicEntryInformation { pub wss_port: Option, } -type NodeId = MixId; - // the bare minimum information needed to construct sphinx packets #[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)] pub struct SkimmedNode { @@ -66,14 +127,27 @@ pub struct SkimmedNode { #[schema(value_type = u32)] pub node_id: NodeId, - pub ed25519_identity_pubkey: String, + #[serde(with = "bs58_ed25519_pubkey")] + #[schemars(with = "String")] + pub ed25519_identity_pubkey: ed25519::PublicKey, + #[schema(value_type = Vec)] pub ip_addresses: Vec, // TODO: to be deprecated in favour of well-known hardcoded port for everyone pub mix_port: u16, - pub x25519_sphinx_pubkey: String, + + #[serde(with = "bs58_x25519_pubkey")] + #[schemars(with = "String")] + pub x25519_sphinx_pubkey: x25519::PublicKey, + + #[serde(alias = "epoch_role")] pub role: NodeRole, + + // needed for the purposes of sending appropriate test packets + #[serde(default)] + pub supported_roles: DeclaredRoles, + pub entry: Option, /// Average node performance in last 24h period @@ -82,65 +156,10 @@ pub struct SkimmedNode { } impl SkimmedNode { - pub fn from_described_gateway( - annotated: &GatewayBondAnnotated, - description: Option<&NymNodeDescription>, - ) -> Self { - let mut base: SkimmedNode = annotated.into(); - let Some(description) = description else { - return base; - }; - - // safety: the conversion always set the entry field - let entry = base.entry.as_mut().unwrap(); - entry - .hostname - .clone_from(&description.host_information.hostname); - entry.ws_port = description.mixnet_websockets.ws_port; - entry.wss_port = description.mixnet_websockets.wss_port; - - // always prefer self-described data - if !description.host_information.ip_address.is_empty() { - base.ip_addresses - .clone_from(&description.host_information.ip_address) - } - - base - } -} - -impl<'a> From<&'a MixNodeBondAnnotated> for SkimmedNode { - fn from(value: &'a MixNodeBondAnnotated) -> Self { - SkimmedNode { - node_id: value.mix_id(), - ed25519_identity_pubkey: value.identity_key().to_string(), - ip_addresses: value.ip_addresses.clone(), - mix_port: value.mix_node().mix_port, - x25519_sphinx_pubkey: value.mix_node().sphinx_key.clone(), - role: NodeRole::Mixnode { - layer: value.mixnode_details.bond_information.layer.into(), - }, - entry: None, - performance: value.node_performance.last_24h, - } - } -} - -impl<'a> From<&'a GatewayBondAnnotated> for SkimmedNode { - fn from(value: &'a GatewayBondAnnotated) -> Self { - SkimmedNode { - node_id: MixId::MAX, - ip_addresses: value.ip_addresses.clone(), - ed25519_identity_pubkey: value.gateway_bond.identity().clone(), - mix_port: value.gateway_bond.gateway.mix_port, - x25519_sphinx_pubkey: value.gateway_bond.gateway.sphinx_key.clone(), - role: NodeRole::EntryGateway, - entry: Some(BasicEntryInformation { - hostname: None, - ws_port: value.gateway_bond.gateway.clients_port, - wss_port: None, - }), - performance: value.node_performance.last_24h, + pub fn get_mix_layer(&self) -> Option { + match self.role { + NodeRole::Mixnode { layer } => Some(layer), + _ => None, } } } @@ -159,5 +178,5 @@ pub struct FullFatNode { pub expanded: SemiSkimmedNode, // kinda temporary for now to make as few changes as possible for now - pub self_described: Option, + pub self_described: Option, } diff --git a/nym-api/nym-api-requests/src/pagination.rs b/nym-api/nym-api-requests/src/pagination.rs index c17d5a7bee..43a3458db7 100644 --- a/nym-api/nym-api-requests/src/pagination.rs +++ b/nym-api/nym-api-requests/src/pagination.rs @@ -5,14 +5,27 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -#[derive(Serialize, Deserialize, JsonSchema, ToSchema)] +#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts(export, export_to = "ts-packages/types/src/types/rust/Pagination.ts") +)] pub struct Pagination { pub total: usize, pub page: u32, pub size: usize, } -#[derive(Serialize, Deserialize, JsonSchema, ToSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)] +#[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] +#[cfg_attr( + feature = "generate-ts", + ts( + export, + export_to = "ts-packages/types/src/types/rust/PaginatedResponse.ts" + ) +)] pub struct PaginatedResponse { pub pagination: Pagination, pub data: Vec, diff --git a/nym-api/src/circulating_supply_api/cache/mod.rs b/nym-api/src/circulating_supply_api/cache/mod.rs index 7a2537ebee..87e401da9d 100644 --- a/nym-api/src/circulating_supply_api/cache/mod.rs +++ b/nym-api/src/circulating_supply_api/cache/mod.rs @@ -6,7 +6,6 @@ use cosmwasm_std::Addr; use nym_api_requests::models::CirculatingSupplyResponse; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::Coin; -use rocket::fairing::AdHoc; use std::ops::Deref; use std::{ sync::{atomic::AtomicBool, Arc}, @@ -15,6 +14,7 @@ use std::{ use thiserror::Error; use tokio::sync::RwLock; use tokio::time; +use tracing::{error, info}; mod data; pub(crate) mod refresher; @@ -67,13 +67,6 @@ impl CirculatingSupplyCache { } } - #[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")] - pub(crate) fn stage(mix_denom: String) -> AdHoc { - AdHoc::on_ignite("Circulating Supply Cache Stage", |rocket| async { - rocket.manage(Self::new(mix_denom)) - }) - } - pub(crate) async fn update(&self, mixmining_reserve: Coin, vesting_tokens: Coin) { let mut cache = self.data.write().await; @@ -81,10 +74,10 @@ impl CirculatingSupplyCache { circulating_supply.amount -= mixmining_reserve.amount; circulating_supply.amount -= vesting_tokens.amount; - log::info!("Updating circulating supply cache"); - log::info!("the mixmining reserve is now {mixmining_reserve}"); - log::info!("the number of tokens still vesting is now {vesting_tokens}"); - log::info!("the circulating supply is now {circulating_supply}"); + info!("Updating circulating supply cache"); + info!("the mixmining reserve is now {mixmining_reserve}"); + info!("the number of tokens still vesting is now {vesting_tokens}"); + info!("the circulating supply is now {circulating_supply}"); cache.mixmining_reserve.unchecked_update(mixmining_reserve); cache.vesting_tokens.unchecked_update(vesting_tokens); diff --git a/nym-api/src/circulating_supply_api/cache/refresher.rs b/nym-api/src/circulating_supply_api/cache/refresher.rs index f983390356..60d7c2bafc 100644 --- a/nym-api/src/circulating_supply_api/cache/refresher.rs +++ b/nym-api/src/circulating_supply_api/cache/refresher.rs @@ -11,6 +11,7 @@ use std::collections::HashSet; use std::sync::atomic::Ordering; use std::time::Duration; use tokio::time; +use tracing::{error, trace}; pub(crate) struct CirculatingSupplyCacheRefresher { nyxd_client: Client, diff --git a/nym-api/src/circulating_supply_api/handlers.rs b/nym-api/src/circulating_supply_api/handlers.rs index 973d1fc64e..3e49659b55 100644 --- a/nym-api/src/circulating_supply_api/handlers.rs +++ b/nym-api/src/circulating_supply_api/handlers.rs @@ -1,15 +1,13 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::{ - node_status_api::models::{AxumErrorResponse, AxumResult}, - v2::AxumAppState, -}; +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::support::http::state::AppState; use axum::{extract, Router}; use nym_api_requests::models::CirculatingSupplyResponse; use nym_validator_client::nyxd::Coin; -pub(crate) fn circulating_supply_routes() -> Router { +pub(crate) fn circulating_supply_routes() -> Router { Router::new() .route("/", axum::routing::get(get_full_circulating_supply)) .route( @@ -31,7 +29,7 @@ pub(crate) fn circulating_supply_routes() -> Router { ) )] async fn get_full_circulating_supply( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> AxumResult> { match state .circulating_supply_cache() @@ -52,7 +50,7 @@ async fn get_full_circulating_supply( ) )] async fn get_total_supply( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> AxumResult> { let full_circulating_supply = match state .circulating_supply_cache() @@ -75,7 +73,7 @@ async fn get_total_supply( ) )] async fn get_circulating_supply( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> AxumResult> { let full_circulating_supply = match state .circulating_supply_cache() diff --git a/nym-api/src/circulating_supply_api/mod.rs b/nym-api/src/circulating_supply_api/mod.rs index 2bfefcc35f..3253ccaaab 100644 --- a/nym-api/src/circulating_supply_api/mod.rs +++ b/nym-api/src/circulating_supply_api/mod.rs @@ -1,28 +1,12 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_task::TaskManager; -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::{openapi_get_routes_spec, settings::OpenApiSettings}; - -use crate::support::{config, nyxd}; - use self::cache::refresher::CirculatingSupplyCacheRefresher; +use crate::support::{config, nyxd}; +use nym_task::TaskManager; pub(crate) mod cache; -#[cfg(feature = "axum")] pub(crate) mod handlers; -pub(crate) mod routes; - -/// Merges the routes with http information and returns it to Rocket for serving -pub(crate) fn circulating_supply_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: routes::get_full_circulating_supply, - routes::get_total_supply, - routes::get_circulating_supply - ] -} /// Spawn the circulating supply cache refresher. pub(crate) fn start_cache_refresh( diff --git a/nym-api/src/circulating_supply_api/routes.rs b/nym-api/src/circulating_supply_api/routes.rs deleted file mode 100644 index b8b00cb82e..0000000000 --- a/nym-api/src/circulating_supply_api/routes.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2022-2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::circulating_supply_api::cache::CirculatingSupplyCache; -use crate::node_status_api::models::RocketErrorResponse; -use nym_api_requests::models::CirculatingSupplyResponse; -use nym_validator_client::nyxd::Coin; -use rocket::http::Status; -use rocket::serde::json::Json; -use rocket::State; -use rocket_okapi::openapi; - -// TODO: this is not the best place to put it, it should be more centralised, -// but for a quick fix, that's good enough for now... -// (for proper solution we should be managing `NymNetworkDetails` via rocket and grabbing display exponent -// value from the mix denom here. -const UNYM_RATIO: f64 = 1000000.; - -fn unym_coin_to_float_unym(coin: Coin) -> f64 { - // our total supply can't exceed 1B so an overflow here is impossible - // (if it happened, then we SHOULD crash) - coin.amount as f64 / UNYM_RATIO -} - -#[openapi(tag = "circulating-supply")] -#[get("/circulating-supply")] -pub(crate) async fn get_full_circulating_supply( - cache: &State, -) -> Result, RocketErrorResponse> { - match cache.get_circulating_supply().await { - Some(value) => Ok(Json(value)), - None => Err(RocketErrorResponse::new( - "unavailable", - Status::InternalServerError, - )), - } -} - -#[openapi(tag = "circulating-supply")] -#[get("/circulating-supply/total-supply-value")] -pub(crate) async fn get_total_supply( - cache: &State, -) -> Result, RocketErrorResponse> { - let full_circulating_supply = match cache.get_circulating_supply().await { - Some(res) => res, - None => { - return Err(RocketErrorResponse::new( - "unavailable", - Status::InternalServerError, - )) - } - }; - - Ok(Json(unym_coin_to_float_unym( - full_circulating_supply.total_supply.into(), - ))) -} - -#[openapi(tag = "circulating-supply")] -#[get("/circulating-supply/circulating-supply-value")] -pub(crate) async fn get_circulating_supply( - cache: &State, -) -> Result, RocketErrorResponse> { - let full_circulating_supply = match cache.get_circulating_supply().await { - Some(res) => res, - None => { - return Err(RocketErrorResponse::new( - "unavailable", - Status::InternalServerError, - )) - } - }; - - Ok(Json(unym_coin_to_float_unym( - full_circulating_supply.circulating_supply.into(), - ))) -} diff --git a/nym-api/src/ecash/api_routes/aggregation.rs b/nym-api/src/ecash/api_routes/aggregation.rs index f552698d86..89478fbd09 100644 --- a/nym-api/src/ecash/api_routes/aggregation.rs +++ b/nym-api/src/ecash/api_routes/aggregation.rs @@ -1,28 +1,66 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::ecash::error::{EcashError, Result}; +use crate::ecash::api_routes::helpers::EpochIdParam; +use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; -use log::trace; +use crate::node_status_api::models::AxumResult; +use crate::support::http::state::AppState; +use axum::extract::Path; +use axum::{Json, Router}; use nym_api_requests::ecash::models::{ AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, }; use nym_api_requests::ecash::VerificationKeyResponse; use nym_ecash_time::{cred_exp_date, EcashTime}; use nym_validator_client::nym_api::rfc_3339_date; -use rocket::serde::json::Json; -use rocket::State as RocketState; -use rocket_okapi::openapi; +use serde::Deserialize; +use std::sync::Arc; use time::Date; +use tracing::trace; +use utoipa::IntoParams; -// routes with globally aggregated keys, signatures, etc. +/// routes with globally aggregated keys, signatures, etc. +pub(crate) fn aggregation_routes(ecash_state: Arc) -> Router { + Router::new() + .route( + "/master-verification-key", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |epoch_id| master_verification_key(epoch_id, ecash_state) + }), + ) + .route( + "/aggregated-expiration-date-signatures", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |expiration_date| expiration_date_signatures(expiration_date, ecash_state) + }), + ) + .route( + "/aggregated-coin-indices-signatures", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |epoch_id| coin_indices_signatures(epoch_id, ecash_state) + }), + ) +} -#[openapi(tag = "Ecash Global Data")] -#[get("/master-verification-key?")] -pub async fn master_verification_key( - epoch_id: Option, - state: &RocketState, -) -> Result> { +#[utoipa::path( + tag = "Ecash Global Data", + get, + params( + EpochIdParam + ), + path = "/v1/ecash/master-verification-key", + responses( + (status = 200, body = VerificationKeyResponse) + ) +)] +async fn master_verification_key( + Path(EpochIdParam { epoch_id }): Path, + state: Arc, +) -> AxumResult> { trace!("aggregated_verification_key request"); // see if we're not in the middle of new dkg @@ -33,12 +71,27 @@ pub async fn master_verification_key( Ok(Json(VerificationKeyResponse::new(key.clone()))) } -#[openapi(tag = "Ecash Global Data")] -#[get("/aggregated-expiration-date-signatures?")] -pub async fn expiration_date_signatures( +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct ExpirationDateParam { expiration_date: Option, - state: &RocketState, -) -> Result> { +} + +#[utoipa::path( + tag = "Ecash Global Data", + get, + params( + ExpirationDateParam + ), + path = "/v1/ecash/aggregated-expiration-date-signatures", + responses( + (status = 200, body = AggregatedExpirationDateSignatureResponse) + ) +)] +async fn expiration_date_signatures( + Path(ExpirationDateParam { expiration_date }): Path, + state: Arc, +) -> AxumResult> { trace!("aggregated_expiration_date_signatures request"); let expiration_date = match expiration_date { @@ -61,12 +114,21 @@ pub async fn expiration_date_signatures( })) } -#[openapi(tag = "Ecash Global Data")] -#[get("/aggregated-coin-indices-signatures?")] -pub async fn coin_indices_signatures( - epoch_id: Option, - state: &RocketState, -) -> Result> { +#[utoipa::path( + tag = "Ecash Global Data", + get, + params( + EpochIdParam + ), + path = "/v1/ecash/aggregated-coin-indices-signatures", + responses( + (status = 200, body = AggregatedCoinIndicesSignatureResponse) + ) +)] +async fn coin_indices_signatures( + Path(EpochIdParam { epoch_id }): Path, + state: Arc, +) -> AxumResult> { trace!("aggregated_coin_indices_signatures request"); // see if we're not in the middle of new dkg diff --git a/nym-api/src/ecash/api_routes/aggregation_axum.rs b/nym-api/src/ecash/api_routes/aggregation_axum.rs deleted file mode 100644 index 4a03313580..0000000000 --- a/nym-api/src/ecash/api_routes/aggregation_axum.rs +++ /dev/null @@ -1,143 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::ecash::api_routes::helpers::EpochIdParam; -use crate::ecash::error::EcashError; -use crate::ecash::state::EcashState; -use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; -use axum::extract::Path; -use axum::{Json, Router}; -use log::trace; -use nym_api_requests::ecash::models::{ - AggregatedCoinIndicesSignatureResponse, AggregatedExpirationDateSignatureResponse, -}; -use nym_api_requests::ecash::VerificationKeyResponse; -use nym_ecash_time::{cred_exp_date, EcashTime}; -use nym_validator_client::nym_api::rfc_3339_date; -use serde::Deserialize; -use std::sync::Arc; -use time::Date; -use utoipa::IntoParams; - -/// routes with globally aggregated keys, signatures, etc. -pub(crate) fn aggregation_routes(ecash_state: Arc) -> Router { - Router::new() - .route( - "/master-verification-key:epoch_id", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |epoch_id| master_verification_key(epoch_id, ecash_state) - }), - ) - .route( - "/aggregated-expiration-date-signatures:expiration_date", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |expiration_date| expiration_date_signatures(expiration_date, ecash_state) - }), - ) - .route( - "/aggregated-coin-indices-signatures:epoch_id", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |epoch_id| coin_indices_signatures(epoch_id, ecash_state) - }), - ) -} - -#[utoipa::path( - tag = "Ecash Global Data", - get, - params( - EpochIdParam - ), - path = "/v1/ecash/master-verification-key/{epoch_id}", - responses( - (status = 200, body = VerificationKeyResponse) - ) -)] -async fn master_verification_key( - Path(EpochIdParam { epoch_id }): Path, - state: Arc, -) -> AxumResult> { - trace!("aggregated_verification_key request"); - - // see if we're not in the middle of new dkg - state.ensure_dkg_not_in_progress().await?; - - let key = state.master_verification_key(epoch_id).await?; - - Ok(Json(VerificationKeyResponse::new(key.clone()))) -} - -#[derive(Deserialize, IntoParams)] -#[into_params(parameter_in = Path)] -struct ExpirationDateParam { - expiration_date: Option, -} - -#[utoipa::path( - tag = "Ecash Global Data", - get, - params( - ExpirationDateParam - ), - path = "/v1/ecash/aggregated-expiration-date-signatures/{epoch_id}", - responses( - (status = 200, body = AggregatedExpirationDateSignatureResponse) - ) -)] -async fn expiration_date_signatures( - Path(ExpirationDateParam { expiration_date }): Path, - state: Arc, -) -> AxumResult> { - trace!("aggregated_expiration_date_signatures request"); - - let expiration_date = match expiration_date { - None => cred_exp_date().ecash_date(), - Some(raw) => Date::parse(&raw, &rfc_3339_date()) - .map_err(|_| EcashError::MalformedExpirationDate { raw })?, - }; - - // see if we're not in the middle of new dkg - state.ensure_dkg_not_in_progress().await?; - - let expiration_date_signatures = state - .master_expiration_date_signatures(expiration_date) - .await?; - - Ok(Json(AggregatedExpirationDateSignatureResponse { - epoch_id: expiration_date_signatures.epoch_id, - expiration_date, - signatures: expiration_date_signatures.signatures.clone(), - })) -} - -#[utoipa::path( - tag = "Ecash Global Data", - get, - params( - EpochIdParam - ), - path = "/v1/ecash/aggregated-coin-indices-signatures/{epoch_id}", - responses( - (status = 200, body = AggregatedCoinIndicesSignatureResponse) - ) -)] -async fn coin_indices_signatures( - Path(EpochIdParam { epoch_id }): Path, - state: Arc, -) -> AxumResult> { - trace!("aggregated_coin_indices_signatures request"); - - // see if we're not in the middle of new dkg - state.ensure_dkg_not_in_progress().await?; - - let coin_indices_signatures = state.master_coin_index_signatures(epoch_id).await?; - - Ok(Json(AggregatedCoinIndicesSignatureResponse { - epoch_id: coin_indices_signatures.epoch_id, - signatures: coin_indices_signatures.signatures.clone(), - })) -} diff --git a/nym-api/src/ecash/api_routes/handlers.rs b/nym-api/src/ecash/api_routes/handlers.rs index b40ccf9773..e9f97c48b0 100644 --- a/nym-api/src/ecash/api_routes/handlers.rs +++ b/nym-api/src/ecash/api_routes/handlers.rs @@ -1,16 +1,16 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::ecash::api_routes::aggregation_axum::aggregation_routes; -use crate::ecash::api_routes::issued_axum::issued_routes; -use crate::ecash::api_routes::partial_signing_axum::partial_signing_routes; -use crate::ecash::api_routes::spending_axum::spending_routes; +use crate::ecash::api_routes::aggregation::aggregation_routes; +use crate::ecash::api_routes::issued::issued_routes; +use crate::ecash::api_routes::partial_signing::partial_signing_routes; +use crate::ecash::api_routes::spending::spending_routes; use crate::ecash::state::EcashState; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::Router; use std::sync::Arc; -pub(crate) fn ecash_routes(ecash_state: Arc) -> Router { +pub(crate) fn ecash_routes(ecash_state: Arc) -> Router { Router::new() .merge(aggregation_routes(Arc::clone(&ecash_state))) .merge(issued_routes(Arc::clone(&ecash_state))) diff --git a/nym-api/src/ecash/api_routes/helpers.rs b/nym-api/src/ecash/api_routes/helpers.rs index 4469bf11d0..6fba7d43dd 100644 --- a/nym-api/src/ecash/api_routes/helpers.rs +++ b/nym-api/src/ecash/api_routes/helpers.rs @@ -27,7 +27,6 @@ pub(crate) fn build_credentials_response( Ok(IssuedCredentialsResponse { credentials }) } -#[cfg(feature = "axum")] #[derive(serde::Deserialize, utoipa::IntoParams)] #[into_params(parameter_in = Path)] pub(super) struct EpochIdParam { diff --git a/nym-api/src/ecash/api_routes/issued.rs b/nym-api/src/ecash/api_routes/issued.rs index 5b85e6b0cf..1593e5e9e1 100644 --- a/nym-api/src/ecash/api_routes/issued.rs +++ b/nym-api/src/ecash/api_routes/issued.rs @@ -5,21 +5,67 @@ use crate::ecash::api_routes::helpers::build_credentials_response; use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; use crate::ecash::storage::EcashStorageExt; +use crate::node_status_api::models::AxumResult; +use crate::support::http::state::AppState; +use axum::extract::Path; +use axum::{Json, Router}; use nym_api_requests::ecash::models::{ EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, }; use nym_api_requests::ecash::CredentialsRequestBody; -use nym_coconut_dkg_common::types::EpochId; -use rocket::serde::json::Json; -use rocket::State as RocketState; -use rocket_okapi::openapi; - -#[openapi(tag = "Ecash")] -#[get("/epoch-credentials/")] -pub async fn epoch_credentials( - epoch: EpochId, - state: &RocketState, -) -> crate::ecash::error::Result> { +use serde::Deserialize; +use std::sync::Arc; +use utoipa::IntoParams; + +pub(crate) fn issued_routes(ecash_state: Arc) -> Router { + Router::new() + .route( + "/epoch-credentials/:epoch", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |epoch| epoch_credentials(epoch, ecash_state) + }), + ) + .route( + "/issued-credential/:id", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |id| issued_credential(id, ecash_state) + }), + ) + .route( + "/issued-credentials", + axum::routing::post({ + let ecash_state = Arc::clone(&ecash_state); + |body| issued_credentials(body, ecash_state) + }), + ) +} + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct EpochParam { + epoch: u64, +} + +#[utoipa::path( + tag = "Ecash", + get, + params( + EpochParam + ), + path = "/v1/ecash/epoch-credentials/{epoch}", + responses( + (status = 200, body = EpochCredentialsResponse), + (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), + ) +)] +async fn epoch_credentials( + Path(EpochParam { epoch }): Path, + state: Arc, +) -> AxumResult> { + state.ensure_signer().await?; + let issued = state.aux.storage.get_epoch_credentials(epoch).await?; let response = if let Some(issued) = issued { @@ -35,12 +81,30 @@ pub async fn epoch_credentials( Ok(Json(response)) } -#[openapi(tag = "Ecash")] -#[get("/issued-credential/")] -pub async fn issued_credential( +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct IdParam { id: i64, - state: &RocketState, -) -> crate::ecash::error::Result> { +} + +#[utoipa::path( + tag = "Ecash", + get, + params( + IdParam + ), + path = "/v1/ecash/issued-credential/{id}", + responses( + (status = 200, body = IssuedCredentialResponse), + (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), + ) +)] +async fn issued_credential( + Path(IdParam { id }): Path, + state: Arc, +) -> AxumResult> { + state.ensure_signer().await?; + let issued = state.aux.storage.get_issued_credential(id).await?; let credential = if let Some(issued) = issued { @@ -52,16 +116,24 @@ pub async fn issued_credential( Ok(Json(IssuedCredentialResponse { credential })) } -#[openapi(tag = "Ecash")] -#[post("/issued-credentials", data = "")] -pub async fn issued_credentials( - params: Json, - state: &RocketState, -) -> crate::ecash::error::Result> { - let params = params.into_inner(); +#[utoipa::path( + tag = "Ecash", + post, + request_body = CredentialsRequestBody, + path = "/v1/ecash/issued-credentials", + responses( + (status = 200, body = IssuedCredentialsResponse), + (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), + ) +)] +async fn issued_credentials( + Json(params): Json, + state: Arc, +) -> AxumResult> { + state.ensure_signer().await?; if params.pagination.is_some() && !params.credential_ids.is_empty() { - return Err(EcashError::InvalidQueryArguments); + return Err(EcashError::InvalidQueryArguments.into()); } let credentials = if let Some(pagination) = params.pagination { @@ -78,5 +150,7 @@ pub async fn issued_credentials( .await? }; - build_credentials_response(credentials).map(Json) + build_credentials_response(credentials) + .map(Json) + .map_err(From::from) } diff --git a/nym-api/src/ecash/api_routes/issued_axum.rs b/nym-api/src/ecash/api_routes/issued_axum.rs deleted file mode 100644 index 72fa55e39f..0000000000 --- a/nym-api/src/ecash/api_routes/issued_axum.rs +++ /dev/null @@ -1,147 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::ecash::api_routes::helpers::build_credentials_response; -use crate::ecash::error::EcashError; -use crate::ecash::state::EcashState; -use crate::ecash::storage::EcashStorageExt; -use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; -use axum::extract::Path; -use axum::{Json, Router}; -use nym_api_requests::ecash::models::{ - EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, -}; -use nym_api_requests::ecash::CredentialsRequestBody; -use serde::Deserialize; -use std::sync::Arc; -use utoipa::IntoParams; - -pub(crate) fn issued_routes(ecash_state: Arc) -> Router { - Router::new() - .route( - "/epoch-credentials/:epoch", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |epoch| epoch_credentials(epoch, ecash_state) - }), - ) - .route( - "/issued-credential/:id", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |id| issued_credential(id, ecash_state) - }), - ) - .route( - "/issued-credentials", - axum::routing::post({ - let ecash_state = Arc::clone(&ecash_state); - |body| issued_credentials(body, ecash_state) - }), - ) -} - -#[derive(Deserialize, IntoParams)] -#[into_params(parameter_in = Path)] -struct EpochParam { - epoch: u64, -} - -#[utoipa::path( - tag = "Ecash", - get, - params( - EpochParam - ), - path = "/v1/ecash/epoch-credentials/{epoch}", - responses( - (status = 200, body = EpochCredentialsResponse) - ) -)] -async fn epoch_credentials( - Path(EpochParam { epoch }): Path, - state: Arc, -) -> AxumResult> { - let issued = state.aux.storage.get_epoch_credentials(epoch).await?; - - let response = if let Some(issued) = issued { - issued.into() - } else { - EpochCredentialsResponse { - epoch_id: epoch, - first_epoch_credential_id: None, - total_issued: 0, - } - }; - - Ok(Json(response)) -} - -#[derive(Deserialize, IntoParams)] -#[into_params(parameter_in = Path)] -struct IdParam { - id: i64, -} - -#[utoipa::path( - tag = "Ecash", - get, - params( - IdParam - ), - path = "/v1/ecash/issued-credential/{id}", - responses( - (status = 200, body = IssuedCredentialResponse) - ) -)] -async fn issued_credential( - Path(IdParam { id }): Path, - state: Arc, -) -> AxumResult> { - let issued = state.aux.storage.get_issued_credential(id).await?; - - let credential = if let Some(issued) = issued { - Some(issued.try_into()?) - } else { - None - }; - - Ok(Json(IssuedCredentialResponse { credential })) -} - -#[utoipa::path( - tag = "Ecash", - post, - request_body = CredentialsRequestBody, - path = "/v1/ecash/issued-credentials", - responses( - (status = 200, body = IssuedCredentialsResponse) - ) -)] -async fn issued_credentials( - Json(params): Json, - state: Arc, -) -> AxumResult> { - if params.pagination.is_some() && !params.credential_ids.is_empty() { - return Err(EcashError::InvalidQueryArguments.into()); - } - - let credentials = if let Some(pagination) = params.pagination { - state - .aux - .storage - .get_issued_credentials_paged(pagination) - .await? - } else { - state - .aux - .storage - .get_issued_credentials(params.credential_ids) - .await? - }; - - build_credentials_response(credentials) - .map(Json) - .map_err(From::from) -} diff --git a/nym-api/src/ecash/api_routes/mod.rs b/nym-api/src/ecash/api_routes/mod.rs index 910e253288..6e1db9ffca 100644 --- a/nym-api/src/ecash/api_routes/mod.rs +++ b/nym-api/src/ecash/api_routes/mod.rs @@ -1,18 +1,14 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -pub(crate) mod aggregation; +// pub(crate) mod aggregation; mod helpers; +// pub(crate) mod issued; +// pub(crate) mod partial_signing; +// pub(crate) mod spending; + +pub(crate) mod aggregation; +pub(crate) mod handlers; pub(crate) mod issued; pub(crate) mod partial_signing; pub(crate) mod spending; - -cfg_if::cfg_if! { - if #[cfg(feature = "axum")] { - pub(crate) mod aggregation_axum; - pub(crate) mod handlers; - pub(crate) mod issued_axum; - pub(crate) mod partial_signing_axum; - pub(crate) mod spending_axum; - } -} diff --git a/nym-api/src/ecash/api_routes/partial_signing.rs b/nym-api/src/ecash/api_routes/partial_signing.rs index a0958a2a6b..70749b1454 100644 --- a/nym-api/src/ecash/api_routes/partial_signing.rs +++ b/nym-api/src/ecash/api_routes/partial_signing.rs @@ -1,27 +1,69 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::ecash::api_routes::helpers::EpochIdParam; use crate::ecash::error::EcashError; use crate::ecash::helpers::blind_sign; use crate::ecash::state::EcashState; +use crate::node_status_api::models::AxumResult; +use crate::support::http::state::AppState; +use axum::extract::Query; +use axum::{Json, Router}; use nym_api_requests::ecash::{ BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, PartialExpirationDateSignatureResponse, }; use nym_ecash_time::{cred_exp_date, EcashTime}; use nym_validator_client::nym_api::rfc_3339_date; -use rocket::serde::json::Json; -use rocket::State as RocketState; -use rocket_okapi::openapi; +use serde::Deserialize; use std::ops::Deref; +use std::sync::Arc; use time::Date; +use tracing::{debug, trace}; +use utoipa::IntoParams; + +pub(crate) fn partial_signing_routes(ecash_state: Arc) -> Router { + Router::new() + .route( + "/blind-sign", + axum::routing::post({ + let ecash_state = Arc::clone(&ecash_state); + |body| post_blind_sign(body, ecash_state) + }), + ) + .route( + "/partial-expiration-date-signatures", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |expiration_date| partial_expiration_date_signatures(expiration_date, ecash_state) + }), + ) + .route( + "/partial-coin-indices-signatures", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + |epoch_id| partial_coin_indices_signatures(epoch_id, ecash_state) + }), + ) +} + +#[utoipa::path( + tag = "Ecash", + post, + request_body = BlindSignRequestBody, + path = "/v1/ecash/blind-sign", + responses( + (status = 200, body = BlindedSignatureResponse), + (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), + + ) +)] +async fn post_blind_sign( + Json(blind_sign_request_body): Json, + state: Arc, +) -> AxumResult> { + state.ensure_signer().await?; -#[openapi(tag = "Ecash")] -#[post("/blind-sign", data = "")] -pub async fn post_blind_sign( - blind_sign_request_body: Json, - state: &RocketState, -) -> crate::ecash::error::Result> { debug!("Received blind sign request"); trace!("body: {:?}", blind_sign_request_body); @@ -31,7 +73,7 @@ pub async fn post_blind_sign( // basic check of expiration date validity if blind_sign_request_body.expiration_date > cred_exp_date().ecash_date() { - return Err(EcashError::ExpirationDateTooLate); + return Err(EcashError::ExpirationDateTooLate.into()); } // see if we're not in the middle of new dkg @@ -62,24 +104,41 @@ pub async fn post_blind_sign( // produce the partial signature debug!("producing the partial credential"); - let blinded_signature = blind_sign(blind_sign_request_body.deref(), signing_key.deref())?; + let blinded_signature = blind_sign(&blind_sign_request_body, signing_key.deref())?; // store the information locally debug!("storing the issued credential in the database"); state - .store_issued_credential(blind_sign_request_body.into_inner(), &blinded_signature) + .store_issued_credential(blind_sign_request_body, &blinded_signature) .await?; // finally return the credential to the client Ok(Json(BlindedSignatureResponse { blinded_signature })) } -#[openapi(tag = "Ecash")] -#[get("/partial-expiration-date-signatures?")] -pub async fn partial_expiration_date_signatures( +#[derive(Deserialize, IntoParams)] +struct ExpirationDateParam { expiration_date: Option, - state: &RocketState, -) -> crate::ecash::error::Result> { +} + +#[utoipa::path( + tag = "Ecash", + get, + params( + ExpirationDateParam + ), + path = "/v1/ecash/partial-expiration-date-signatures", + responses( + (status = 200, body = PartialExpirationDateSignatureResponse), + (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), + ) +)] +async fn partial_expiration_date_signatures( + Query(ExpirationDateParam { expiration_date }): Query, + state: Arc, +) -> AxumResult> { + state.ensure_signer().await?; + let expiration_date = match expiration_date { None => cred_exp_date().ecash_date(), Some(raw) => Date::parse(&raw, &rfc_3339_date()) @@ -100,12 +159,24 @@ pub async fn partial_expiration_date_signatures( })) } -#[openapi(tag = "Ecash")] -#[get("/partial-coin-indices-signatures?")] -pub async fn partial_coin_indices_signatures( - epoch_id: Option, - state: &RocketState, -) -> crate::ecash::error::Result> { +#[utoipa::path( + tag = "Ecash", + get, + params( + EpochIdParam + ), + path = "/v1/ecash/partial-coin-indices-signatures", + responses( + (status = 200, body = PartialExpirationDateSignatureResponse), + (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), + ) +)] +async fn partial_coin_indices_signatures( + Query(EpochIdParam { epoch_id }): Query, + state: Arc, +) -> AxumResult> { + state.ensure_signer().await?; + // see if we're not in the middle of new dkg state.ensure_dkg_not_in_progress().await?; diff --git a/nym-api/src/ecash/api_routes/partial_signing_axum.rs b/nym-api/src/ecash/api_routes/partial_signing_axum.rs deleted file mode 100644 index fe93ddc921..0000000000 --- a/nym-api/src/ecash/api_routes/partial_signing_axum.rs +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::ecash::api_routes::helpers::EpochIdParam; -use crate::ecash::error::EcashError; -use crate::ecash::helpers::blind_sign; -use crate::ecash::state::EcashState; -use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; -use axum::extract::Path; -use axum::{Json, Router}; -use nym_api_requests::ecash::{ - BlindSignRequestBody, BlindedSignatureResponse, PartialCoinIndicesSignatureResponse, - PartialExpirationDateSignatureResponse, -}; -use nym_ecash_time::{cred_exp_date, EcashTime}; -use nym_validator_client::nym_api::rfc_3339_date; -use serde::Deserialize; -use std::ops::Deref; -use std::sync::Arc; -use time::Date; -use utoipa::IntoParams; - -pub(crate) fn partial_signing_routes(ecash_state: Arc) -> Router { - Router::new() - .route( - "/blind-sign", - axum::routing::post({ - let ecash_state = Arc::clone(&ecash_state); - |body| post_blind_sign(body, ecash_state) - }), - ) - .route( - "/partial-expiration-date-signatures:expiration_date", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |expiration_date| partial_expiration_date_signatures(expiration_date, ecash_state) - }), - ) - .route( - "/partial-coin-indices-signatures:epoch_id", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - |epoch_id| partial_coin_indices_signatures(epoch_id, ecash_state) - }), - ) -} - -#[utoipa::path( - tag = "Ecash", - post, - request_body = BlindSignRequestBody, - path = "/v1/ecash/blind-sign", - responses( - (status = 200, body = BlindedSignatureResponse) - ) -)] -async fn post_blind_sign( - Json(blind_sign_request_body): Json, - state: Arc, -) -> AxumResult> { - debug!("Received blind sign request"); - trace!("body: {:?}", blind_sign_request_body); - - // check if we have the signing key available - debug!("checking if we actually have ecash keys derived..."); - let signing_key = state.ecash_signing_key().await?; - - // basic check of expiration date validity - if blind_sign_request_body.expiration_date > cred_exp_date().ecash_date() { - return Err(EcashError::ExpirationDateTooLate.into()); - } - - // see if we're not in the middle of new dkg - state.ensure_dkg_not_in_progress().await?; - - // check if we already issued a credential for this deposit - let deposit_id = blind_sign_request_body.deposit_id; - debug!( - "checking if we have already issued credential for this deposit (deposit_id: {deposit_id})", - ); - if let Some(blinded_signature) = state.already_issued(deposit_id).await? { - return Ok(Json(BlindedSignatureResponse { blinded_signature })); - } - - //check if account was blacklisted - let pub_key_bs58 = blind_sign_request_body.ecash_pubkey.to_base58_string(); - state.aux.ensure_not_blacklisted(&pub_key_bs58).await?; - - // get the deposit details of the claimed id - debug!("getting deposit details from the chain"); - let deposit = state.get_deposit(deposit_id).await?; - - // check validity of the request - debug!("fully validating received request"); - state - .validate_request(&blind_sign_request_body, deposit) - .await?; - - // produce the partial signature - debug!("producing the partial credential"); - let blinded_signature = blind_sign(&blind_sign_request_body, signing_key.deref())?; - - // store the information locally - debug!("storing the issued credential in the database"); - state - .store_issued_credential(blind_sign_request_body, &blinded_signature) - .await?; - - // finally return the credential to the client - Ok(Json(BlindedSignatureResponse { blinded_signature })) -} - -#[derive(Deserialize, IntoParams)] -struct ExpirationDateParam { - expiration_date: Option, -} - -#[utoipa::path( - tag = "Ecash", - get, - params( - ExpirationDateParam - ), - path = "/v1/ecash/partial-expiration-date-signatures/{expiration_date}", - responses( - (status = 200, body = PartialExpirationDateSignatureResponse) - ) -)] -async fn partial_expiration_date_signatures( - Path(ExpirationDateParam { expiration_date }): Path, - state: Arc, -) -> AxumResult> { - let expiration_date = match expiration_date { - None => cred_exp_date().ecash_date(), - Some(raw) => Date::parse(&raw, &rfc_3339_date()) - .map_err(|_| EcashError::MalformedExpirationDate { raw })?, - }; - - // see if we're not in the middle of new dkg - state.ensure_dkg_not_in_progress().await?; - - let expiration_date_signatures = state - .partial_expiration_date_signatures(expiration_date) - .await?; - - Ok(Json(PartialExpirationDateSignatureResponse { - epoch_id: expiration_date_signatures.epoch_id, - expiration_date, - signatures: expiration_date_signatures.signatures.clone(), - })) -} - -#[utoipa::path( - tag = "Ecash", - get, - params( - EpochIdParam - ), - path = "/v1/ecash/partial-coin-indices-signatures/{epoch_id}", - responses( - (status = 200, body = PartialExpirationDateSignatureResponse) - ) -)] -async fn partial_coin_indices_signatures( - Path(EpochIdParam { epoch_id }): Path, - state: Arc, -) -> AxumResult> { - // see if we're not in the middle of new dkg - state.ensure_dkg_not_in_progress().await?; - - let coin_indices_signatures = state.partial_coin_index_signatures(epoch_id).await?; - - Ok(Json(PartialCoinIndicesSignatureResponse { - epoch_id: coin_indices_signatures.epoch_id, - signatures: coin_indices_signatures.signatures.clone(), - })) -} diff --git a/nym-api/src/ecash/api_routes/spending.rs b/nym-api/src/ecash/api_routes/spending.rs index a45d6f62ad..b9788a4585 100644 --- a/nym-api/src/ecash/api_routes/spending.rs +++ b/nym-api/src/ecash/api_routes/spending.rs @@ -3,6 +3,9 @@ use crate::ecash::error::EcashError; use crate::ecash::state::EcashState; +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::support::http::state::AppState; +use axum::{Json, Router}; use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; use nym_api_requests::ecash::models::{ BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationRejection, @@ -10,31 +13,66 @@ use nym_api_requests::ecash::models::{ }; use nym_compact_ecash::identify::IdentifyResult; use nym_ecash_time::EcashTime; -use rocket::serde::json::Json; -use rocket::State as RocketState; -use rocket_okapi::openapi; use std::collections::HashSet; use std::ops::Deref; +use std::sync::Arc; use time::macros::time; use time::{OffsetDateTime, Time}; +use tracing::{error, warn}; + +#[allow(deprecated)] +pub(crate) fn spending_routes(ecash_state: Arc) -> Router { + Router::new() + .route( + "/verify-ecash-ticket", + axum::routing::post({ + let ecash_state = Arc::clone(&ecash_state); + |body| verify_ticket(body, ecash_state) + }), + ) + .route( + "/batch-redeem-ecash-tickets", + axum::routing::post({ + let ecash_state = Arc::clone(&ecash_state); + |body| batch_redeem_tickets(body, ecash_state) + }), + ) + .route( + "/double-spending-filter-v1", + axum::routing::get({ + let ecash_state = Arc::clone(&ecash_state); + || double_spending_filter_v1(ecash_state) + }), + ) +} const ONE_AM: Time = time!(1:00); fn reject_ticket( reason: EcashTicketVerificationRejection, -) -> crate::ecash::error::Result> { +) -> AxumResult> { Ok(Json(EcashTicketVerificationResponse::reject(reason))) } // TODO: optimise it; for now it's just dummy split of the original `verify_offline_credential` // introduce bloomfilter checks without touching storage first, etc. -#[openapi(tag = "Ecash")] -#[post("/verify-ecash-ticket", data = "")] -pub async fn verify_ticket( +#[utoipa::path( + tag = "Ecash", + post, + request_body = VerifyEcashTicketBody, + path = "/v1/ecash/verify-ecash-ticket", + responses( + (status = 200, body = EcashTicketVerificationResponse), + (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), + ) +)] +async fn verify_ticket( // TODO in the future: make it send binary data rather than json - verify_ticket_body: Json, - state: &RocketState, -) -> crate::ecash::error::Result> { + Json(verify_ticket_body): Json, + state: Arc, +) -> AxumResult> { + state.ensure_signer().await?; + let credential_data = &verify_ticket_body.credential; let gateway_cosmos_addr = &verify_ticket_body.gateway_cosmos_addr; @@ -78,16 +116,16 @@ pub async fn verify_ticket( ) { IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message? IdentifyResult::DuplicatePayInfo(_) => { - log::warn!("Identical payInfo"); + warn!("Identical payInfo"); return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); } IdentifyResult::DoubleSpendingPublicKeys(pub_key) => { //Actual double spending - log::warn!( + warn!( "Double spending attempt for key {}", pub_key.to_base58_string() ); - log::error!("UNIMPLEMENTED: blacklisting the double spend key"); + error!("UNIMPLEMENTED: blacklisting the double spend key"); return reject_ticket(EcashTicketVerificationRejection::DoubleSpend); } } @@ -117,26 +155,33 @@ pub async fn verify_ticket( } // // for particular SN returns what gateway has submitted it and whether it has been verified correctly -// pub async fn credential_status() -> ! { +// async fn credential_status() -> ! { // todo!() // } -#[openapi(tag = "Ecash")] -#[post( - "/batch-redeem-ecash-tickets", - data = "" +#[utoipa::path( + tag = "Ecash", + post, + request_body = BatchRedeemTicketsBody, + path = "/v1/ecash/batch-redeem-ecash-tickets", + responses( + (status = 200, body = EcashBatchTicketRedemptionResponse), + (status = 400, body = ErrorResponse, description = "this nym-api is not an ecash signer in the current epoch"), + ) )] -pub async fn batch_redeem_tickets( +async fn batch_redeem_tickets( // TODO in the future: make it send binary data rather than json - batch_redeem_credentials_body: Json, - state: &RocketState, -) -> crate::ecash::error::Result> { + Json(batch_redeem_credentials_body): Json, + state: Arc, +) -> AxumResult> { + state.ensure_signer().await?; + // 1. see if that gateway has even submitted any tickets let Some(provider_info) = state .get_ticket_provider(batch_redeem_credentials_body.gateway_cosmos_addr.as_ref()) .await? else { - return Err(EcashError::NotTicketsProvided); + return Err(EcashError::NotTicketsProvided.into()); }; // 2. check if the gateway is not trying to spam the redemption requests @@ -149,13 +194,14 @@ pub async fn batch_redeem_tickets( return Err(EcashError::TooFrequentRedemption { last_redemption, next_allowed, - }); + } + .into()); } } // 3. verify the request digest if !batch_redeem_credentials_body.verify_digest() { - return Err(EcashError::MismatchedRequestDigest); + return Err(EcashError::MismatchedRequestDigest.into()); } // 4. verify the associated on-chain proposal (whether it's made by correct sender, has valid messages, etc.) @@ -164,9 +210,7 @@ pub async fn batch_redeem_tickets( .await?; let proposal_id = batch_redeem_credentials_body.proposal_id; - let received = batch_redeem_credentials_body - .into_inner() - .included_serial_numbers; + let received = batch_redeem_credentials_body.included_serial_numbers; // 5. check if **every** serial number included in the request has been verified by us // if we have more than requested, tough luck, they're going to lose them @@ -177,7 +221,8 @@ pub async fn batch_redeem_tickets( if !verified_tickets.contains(sn.deref()) { return Err(EcashError::TicketNotVerified { serial_number_bs58: bs58::encode(sn).into_string(), - }); + } + .into()); } } @@ -190,10 +235,17 @@ pub async fn batch_redeem_tickets( // explicitly mark it as v1 in the URL because the response type WILL change; // it will probably be compressed bincode or something -#[openapi(tag = "Ecash")] -#[get("/double-spending-filter-v1")] -pub async fn double_spending_filter_v1( - _state: &RocketState, -) -> crate::ecash::error::Result> { - Err(EcashError::Restricted) +#[utoipa::path( + tag = "Ecash", + get, + path = "/v1/ecash/double-spending-filter-v1", + responses( + (status = 500, body = ErrorResponse, description = "bloomfilters got disabled"), + ) +)] +#[deprecated] +async fn double_spending_filter_v1( + _state: Arc, +) -> AxumResult> { + AxumResult::Err(AxumErrorResponse::internal_msg("permanently restricted")) } diff --git a/nym-api/src/ecash/api_routes/spending_axum.rs b/nym-api/src/ecash/api_routes/spending_axum.rs deleted file mode 100644 index afd0da2bc0..0000000000 --- a/nym-api/src/ecash/api_routes/spending_axum.rs +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::ecash::error::EcashError; -use crate::ecash::state::EcashState; -use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; -use crate::v2::AxumAppState; -use axum::{Json, Router}; -use nym_api_requests::constants::MIN_BATCH_REDEMPTION_DELAY; -use nym_api_requests::ecash::models::{ - BatchRedeemTicketsBody, EcashBatchTicketRedemptionResponse, EcashTicketVerificationRejection, - EcashTicketVerificationResponse, SpentCredentialsResponse, VerifyEcashTicketBody, -}; -use nym_compact_ecash::identify::IdentifyResult; -use nym_ecash_time::EcashTime; -use std::collections::HashSet; -use std::ops::Deref; -use std::sync::Arc; -use time::macros::time; -use time::{OffsetDateTime, Time}; - -pub(crate) fn spending_routes(ecash_state: Arc) -> Router { - Router::new() - .route( - "/verify-ecash-ticket", - axum::routing::post({ - let ecash_state = Arc::clone(&ecash_state); - |body| verify_ticket(body, ecash_state) - }), - ) - .route( - "/batch-redeem-ecash-tickets", - axum::routing::post({ - let ecash_state = Arc::clone(&ecash_state); - |body| batch_redeem_tickets(body, ecash_state) - }), - ) - .route( - "/double-spending-filter-v1", - axum::routing::get({ - let ecash_state = Arc::clone(&ecash_state); - || double_spending_filter_v1(ecash_state) - }), - ) -} - -const ONE_AM: Time = time!(1:00); - -fn reject_ticket( - reason: EcashTicketVerificationRejection, -) -> AxumResult> { - Ok(Json(EcashTicketVerificationResponse::reject(reason))) -} - -// TODO: optimise it; for now it's just dummy split of the original `verify_offline_credential` -// introduce bloomfilter checks without touching storage first, etc. -#[utoipa::path( - tag = "Ecash", - post, - request_body = VerifyEcashTicketBody, - path = "/v1/ecash/verify-ecash-ticket", - responses( - (status = 200, body = EcashTicketVerificationResponse) - ) -)] -async fn verify_ticket( - // TODO in the future: make it send binary data rather than json - Json(verify_ticket_body): Json, - state: Arc, -) -> AxumResult> { - let credential_data = &verify_ticket_body.credential; - let gateway_cosmos_addr = &verify_ticket_body.gateway_cosmos_addr; - - // easy check: is there only a single payment attached? - if credential_data.payment.spend_value != 1 { - return reject_ticket(EcashTicketVerificationRejection::MultipleTickets); - } - - let sn = &credential_data.encoded_serial_number(); - let spend_date = credential_data.spend_date; - let epoch_id = credential_data.epoch_id; - - let now = OffsetDateTime::now_utc(); - let today_ecash = now.ecash_date(); - - #[allow(clippy::unwrap_used)] - let yesterday_ecash = today_ecash.previous_day().unwrap(); - - // only accept yesterday date if we're near the day transition, i.e. before 1AM UTC - if spend_date != today_ecash && now.time() > ONE_AM && spend_date != yesterday_ecash { - return reject_ticket(EcashTicketVerificationRejection::InvalidSpentDate { - today: today_ecash, - yesterday: yesterday_ecash, - received: spend_date, - }); - } - - // check the bloomfilter for obvious double-spending so that we wouldn't need to waste time on crypto verification - // TODO: when blacklisting is implemented, this should get removed - if state.check_bloomfilter(sn).await { - return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); - } - - // actual double spend detection with storage - if let Some(previous_payment) = state.get_ticket_data_by_serial_number(sn).await? { - match nym_compact_ecash::identify::identify( - &credential_data.payment, - &previous_payment.payment, - credential_data.pay_info, - previous_payment.pay_info, - ) { - IdentifyResult::NotADuplicatePayment => {} //SW NOTE This should never happen, quick message? - IdentifyResult::DuplicatePayInfo(_) => { - log::warn!("Identical payInfo"); - return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); - } - IdentifyResult::DoubleSpendingPublicKeys(pub_key) => { - //Actual double spending - log::warn!( - "Double spending attempt for key {}", - pub_key.to_base58_string() - ); - log::error!("UNIMPLEMENTED: blacklisting the double spend key"); - return reject_ticket(EcashTicketVerificationRejection::DoubleSpend); - } - } - } - - let verification_key = state.master_verification_key(Some(epoch_id)).await?; - - // perform actual crypto verification - if credential_data.verify(&verification_key).is_err() { - return reject_ticket(EcashTicketVerificationRejection::InvalidTicket); - } - - // finally get EXCLUSIVE lock on the bloomfilter, check if for the final time and insert the SN - let was_present = state - .update_bloomfilter(sn, spend_date, today_ecash) - .await?; - if was_present { - return reject_ticket(EcashTicketVerificationRejection::ReplayedTicket); - } - - //store credential - state - .store_verified_ticket(credential_data, gateway_cosmos_addr) - .await?; - - Ok(Json(EcashTicketVerificationResponse { verified: Ok(()) })) -} - -// // for particular SN returns what gateway has submitted it and whether it has been verified correctly -// async fn credential_status() -> ! { -// todo!() -// } - -#[utoipa::path( - tag = "Ecash", - post, - request_body = BatchRedeemTicketsBody, - path = "/v1/ecash/batch-redeem-ecash-tickets", - responses( - (status = 200, body = EcashBatchTicketRedemptionResponse) - ) -)] -async fn batch_redeem_tickets( - // TODO in the future: make it send binary data rather than json - Json(batch_redeem_credentials_body): Json, - state: Arc, -) -> AxumResult> { - // 1. see if that gateway has even submitted any tickets - let Some(provider_info) = state - .get_ticket_provider(batch_redeem_credentials_body.gateway_cosmos_addr.as_ref()) - .await? - else { - return Err(EcashError::NotTicketsProvided.into()); - }; - - // 2. check if the gateway is not trying to spam the redemption requests - // (we have to protect our poor chain) - if let Some(last_redemption) = provider_info.last_batch_verification { - let now = OffsetDateTime::now_utc(); - let next_allowed = last_redemption + MIN_BATCH_REDEMPTION_DELAY; - - if next_allowed > now { - return Err(EcashError::TooFrequentRedemption { - last_redemption, - next_allowed, - } - .into()); - } - } - - // 3. verify the request digest - if !batch_redeem_credentials_body.verify_digest() { - return Err(EcashError::MismatchedRequestDigest.into()); - } - - // 4. verify the associated on-chain proposal (whether it's made by correct sender, has valid messages, etc.) - state - .validate_redemption_proposal(&batch_redeem_credentials_body) - .await?; - - let proposal_id = batch_redeem_credentials_body.proposal_id; - let received = batch_redeem_credentials_body.included_serial_numbers; - - // 5. check if **every** serial number included in the request has been verified by us - // if we have more than requested, tough luck, they're going to lose them - let verified = state.get_redeemable_tickets(provider_info).await?; - let verified_tickets: HashSet<_> = verified.iter().map(|sn| sn.deref()).collect(); - - for sn in &received { - if !verified_tickets.contains(sn.deref()) { - return Err(EcashError::TicketNotVerified { - serial_number_bs58: bs58::encode(sn).into_string(), - } - .into()); - } - } - - // TODO: offload it to separate task with work queue and batching (of tx messages) to vote for multiple proposals in the same tx - state.accept_proposal(proposal_id).await?; - Ok(Json(EcashBatchTicketRedemptionResponse { - proposal_accepted: true, - })) -} - -// explicitly mark it as v1 in the URL because the response type WILL change; -// it will probably be compressed bincode or something -#[utoipa::path( - tag = "Ecash", - get, - path = "/v1/ecash/double-spending-filter-v1", - responses( - (status = 200, body = SpentCredentialsResponse) - ) -)] -async fn double_spending_filter_v1( - _state: Arc, -) -> AxumResult> { - AxumResult::Err(AxumErrorResponse::internal_msg("permanently restricted")) -} diff --git a/nym-api/src/ecash/client.rs b/nym-api/src/ecash/client.rs index e0970f2155..805252808a 100644 --- a/nym-api/src/ecash/client.rs +++ b/nym-api/src/ecash/client.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::ecash::error::Result; +use async_trait::async_trait; use cw3::{ProposalResponse, VoteResponse}; use cw4::MemberResponse; use nym_coconut_dkg_common::dealer::{ diff --git a/nym-api/src/ecash/comm.rs b/nym-api/src/ecash/comm.rs index a539cb12c2..fd70c592eb 100644 --- a/nym-api/src/ecash/comm.rs +++ b/nym-api/src/ecash/comm.rs @@ -5,6 +5,7 @@ use crate::ecash::client::Client; use crate::ecash::error::{EcashError, Result}; use crate::ecash::helpers::CachedImmutableEpochItem; use crate::{ecash, nyxd}; +use async_trait::async_trait; use nym_coconut_dkg_common::types::{Epoch, EpochId}; use nym_dkg::Threshold; use nym_validator_client::EcashApiClient; diff --git a/nym-api/src/ecash/dkg/controller/keys.rs b/nym-api/src/ecash/dkg/controller/keys.rs index 6b806bf792..474d6ab0b1 100644 --- a/nym-api/src/ecash/dkg/controller/keys.rs +++ b/nym-api/src/ecash/dkg/controller/keys.rs @@ -10,10 +10,11 @@ use nym_dkg::bte::keys::KeyPair as DkgKeyPair; use rand::{CryptoRng, RngCore}; use std::path::Path; use thiserror::__private::AsDisplay; +use tracing::warn; pub(crate) fn init_bte_keypair( rng: &mut R, - config: &config::CoconutSigner, + config: &config::EcashSigner, ) -> anyhow::Result<()> { let dkg_params = nym_dkg::bte::setup(); let kp = DkgKeyPair::new(&dkg_params, rng); @@ -27,7 +28,7 @@ pub(crate) fn init_bte_keypair( .context("DKG BTE keypair store failure") } -pub(crate) fn load_bte_keypair(config: &config::CoconutSigner) -> anyhow::Result { +pub(crate) fn load_bte_keypair(config: &config::EcashSigner) -> anyhow::Result { nym_pemstore::load_keypair(&nym_pemstore::KeyPairPath::new( &config.storage_paths.decryption_key_path, &config.storage_paths.public_key_with_proof_path, @@ -36,25 +37,25 @@ pub(crate) fn load_bte_keypair(config: &config::CoconutSigner) -> anyhow::Result } pub(crate) fn load_ecash_keypair_if_exists( - config: &config::CoconutSigner, + config: &config::EcashSigner, ) -> anyhow::Result> { - if !config.storage_paths.coconut_key_path.exists() { + if !config.storage_paths.ecash_key_path.exists() { return Ok(None); } // first attempt to load ecash keys directly, // if that fails fallback to coconut keys and perform migration if let Ok(ecash_key) = - nym_pemstore::load_key::(&config.storage_paths.coconut_key_path) + nym_pemstore::load_key::(&config.storage_paths.ecash_key_path) { return Ok(Some(ecash_key)); } - if let Ok(legacy_coconut_key) = nym_pemstore::load_key::( - &config.storage_paths.coconut_key_path, - ) { + if let Ok(legacy_coconut_key) = + nym_pemstore::load_key::(&config.storage_paths.ecash_key_path) + { let migrated_key: KeyPairWithEpoch = legacy_coconut_key.into(); - nym_pemstore::store_key(&migrated_key, &config.storage_paths.coconut_key_path) + nym_pemstore::store_key(&migrated_key, &config.storage_paths.ecash_key_path) .context("migrated key storage failure")?; return Ok(Some(migrated_key)); diff --git a/nym-api/src/ecash/dkg/controller/mod.rs b/nym-api/src/ecash/dkg/controller/mod.rs index 6dd454f10d..25cba6b48d 100644 --- a/nym-api/src/ecash/dkg/controller/mod.rs +++ b/nym-api/src/ecash/dkg/controller/mod.rs @@ -18,6 +18,7 @@ use std::path::PathBuf; use std::time::Duration; use time::OffsetDateTime; use tokio::time::{interval, MissedTickBehavior}; +use tracing::{debug, error, info, trace, warn}; mod error; pub(crate) mod keys; @@ -32,7 +33,7 @@ pub(crate) struct DkgController { impl DkgController { pub(crate) fn new( - config: &config::CoconutSigner, + config: &config::EcashSigner, nyxd_client: nyxd::Client, coconut_keypair: CoconutKeyPair, dkg_keypair: DkgKeyPair, @@ -52,7 +53,7 @@ impl DkgController { Ok(DkgController { dkg_client: DkgClient::new(nyxd_client), - coconut_key_path: config.storage_paths.coconut_key_path.clone(), + coconut_key_path: config.storage_paths.ecash_key_path.clone(), state: State::new( config.storage_paths.dkg_persistent_state_path.clone(), persistent_state, @@ -304,7 +305,7 @@ impl DkgController { } pub(crate) fn start( - config: &config::CoconutSigner, + config: &config::EcashSigner, nyxd_client: nyxd::Client, coconut_keypair: CoconutKeyPair, dkg_bte_keypair: DkgKeyPair, diff --git a/nym-api/src/ecash/dkg/dealing.rs b/nym-api/src/ecash/dkg/dealing.rs index bb8c17d92b..a0ae4ac840 100644 --- a/nym-api/src/ecash/dkg/dealing.rs +++ b/nym-api/src/ecash/dkg/dealing.rs @@ -6,7 +6,6 @@ use crate::ecash::dkg::controller::keys::archive_coconut_keypair; use crate::ecash::dkg::controller::DkgController; use crate::ecash::error::EcashError; use crate::ecash::keys::KeyPairWithEpoch; -use log::debug; use nym_coconut_dkg_common::dealing::{chunk_dealing, DealingChunkInfo, MAX_DEALING_CHUNK_SIZE}; use nym_coconut_dkg_common::types::{DealingIndex, EpochId}; use nym_dkg::{Dealing, Scalar}; @@ -15,6 +14,7 @@ use std::collections::{HashMap, HashSet}; use std::fmt::{Debug, Formatter}; use std::path::PathBuf; use thiserror::Error; +use tracing::{debug, error, info, warn}; enum DealingGeneration { Fresh { number: u32 }, diff --git a/nym-api/src/ecash/dkg/key_derivation.rs b/nym-api/src/ecash/dkg/key_derivation.rs index 23291806b8..f3e69e1fe6 100644 --- a/nym-api/src/ecash/dkg/key_derivation.rs +++ b/nym-api/src/ecash/dkg/key_derivation.rs @@ -8,7 +8,6 @@ use crate::ecash::dkg::state::key_derivation::{DealerRejectionReason, Derivation use crate::ecash::error::EcashError; use crate::ecash::keys::KeyPairWithEpoch; use cosmwasm_std::Addr; -use log::debug; use nym_coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID; use nym_coconut_dkg_common::types::{DealingIndex, EpochId, NodeIndex}; use nym_compact_ecash::scheme::keygen::SecretKeyAuth; @@ -25,6 +24,8 @@ use rand::{CryptoRng, RngCore}; use std::collections::{BTreeMap, HashMap}; use std::ops::Deref; use thiserror::Error; +use tracing::debug; +use tracing::{error, info, warn}; #[derive(Debug, Error)] pub enum KeyDerivationError { diff --git a/nym-api/src/ecash/dkg/key_finalization.rs b/nym-api/src/ecash/dkg/key_finalization.rs index 9ba5ff958a..b23e2312ba 100644 --- a/nym-api/src/ecash/dkg/key_finalization.rs +++ b/nym-api/src/ecash/dkg/key_finalization.rs @@ -7,6 +7,7 @@ use cw3::Status; use nym_coconut_dkg_common::types::EpochId; use rand::{CryptoRng, RngCore}; use thiserror::Error; +use tracing::{debug, error, info, warn}; #[derive(Debug, Error)] pub enum KeyFinalizationError { diff --git a/nym-api/src/ecash/dkg/key_validation.rs b/nym-api/src/ecash/dkg/key_validation.rs index b99bacd962..aff0ea90ac 100644 --- a/nym-api/src/ecash/dkg/key_validation.rs +++ b/nym-api/src/ecash/dkg/key_validation.rs @@ -13,6 +13,7 @@ use nym_compact_ecash::{ use rand::{CryptoRng, RngCore}; use std::collections::HashMap; use thiserror::Error; +use tracing::{debug, error, info, warn}; fn vote_matches(voted_yes: bool, chain_vote: Vote) -> bool { if voted_yes && chain_vote == Vote::Yes { diff --git a/nym-api/src/ecash/dkg/public_key.rs b/nym-api/src/ecash/dkg/public_key.rs index b7f7d3e6ac..afbdf145dc 100644 --- a/nym-api/src/ecash/dkg/public_key.rs +++ b/nym-api/src/ecash/dkg/public_key.rs @@ -3,10 +3,11 @@ use crate::ecash::dkg::controller::DkgController; use crate::ecash::error::EcashError; -use log::debug; use nym_coconut_dkg_common::types::EpochId; use rand::{CryptoRng, RngCore}; use thiserror::Error; +use tracing::debug; +use tracing::info; #[derive(Debug, Error)] pub enum PublicKeySubmissionError { diff --git a/nym-api/src/ecash/dkg/state/mod.rs b/nym-api/src/ecash/dkg/state/mod.rs index eb1cca637e..065cdb3f24 100644 --- a/nym-api/src/ecash/dkg/state/mod.rs +++ b/nym-api/src/ecash/dkg/state/mod.rs @@ -10,7 +10,6 @@ use crate::ecash::dkg::state::registration::{DkgParticipant, ParticipantState, R use crate::ecash::error::EcashError; use crate::ecash::keys::{KeyPair as CoconutKeyPair, KeyPairWithEpoch}; use cosmwasm_std::Addr; -use log::debug; use nym_coconut_dkg_common::dealer::DealerDetails; use nym_coconut_dkg_common::types::EpochId; use nym_crypto::asymmetric::identity; @@ -20,6 +19,7 @@ use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap}; use std::path::{Path, PathBuf}; use time::OffsetDateTime; +use tracing::{debug, warn}; use url::Url; pub(crate) mod dealing_exchange; diff --git a/nym-api/src/ecash/error.rs b/nym-api/src/ecash/error.rs index 3590f235a5..3e17fcc6ac 100644 --- a/nym-api/src/ecash/error.rs +++ b/nym-api/src/ecash/error.rs @@ -15,14 +15,6 @@ use nym_ecash_contract_common::redeem_credential::BATCH_REDEMPTION_PROPOSAL_TITL use nym_validator_client::coconut::EcashApiError; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::AccountId; -use okapi::openapi3::Responses; -use rocket::http::{ContentType, Status}; -use rocket::response::Responder; -use rocket::{response, Request, Response}; -use rocket_okapi::gen::OpenApiGenerator; -use rocket_okapi::response::OpenApiResponderInner; -use rocket_okapi::util::ensure_status_code_exists; -use std::io::Cursor; use std::num::ParseIntError; use thiserror::Error; use time::error::ComponentRange; @@ -38,6 +30,9 @@ pub enum EcashError { #[error(transparent)] IOError(#[from] std::io::Error), + #[error("this operation couldn't be completed as this nym-api is not an active ecash signer")] + NotASigner, + #[error("the address of the bandwidth contract hasn't been set")] MissingBandwidthContractAddress, @@ -222,24 +217,24 @@ pub enum EcashError { UnknownTicketBookType(#[from] UnknownTicketType), } -impl<'r, 'o: 'r> Responder<'r, 'o> for EcashError { - fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { - let err_msg = self.to_string(); - Response::build() - .header(ContentType::Plain) - .sized_body(err_msg.len(), Cursor::new(err_msg)) - .status(Status::BadRequest) - .ok() - } -} - -impl OpenApiResponderInner for EcashError { - fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { - let mut responses = Responses::default(); - ensure_status_code_exists(&mut responses, 400); - Ok(responses) - } -} +// impl<'r, 'o: 'r> Responder<'r, 'o> for EcashError { +// fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { +// let err_msg = self.to_string(); +// Response::build() +// .header(ContentType::Plain) +// .sized_body(err_msg.len(), Cursor::new(err_msg)) +// .status(Status::BadRequest) +// .ok() +// } +// } +// +// impl OpenApiResponderInner for EcashError { +// fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { +// let mut responses = Responses::default(); +// ensure_status_code_exists(&mut responses, 400); +// Ok(responses) +// } +// } #[derive(Debug, Error)] pub enum RedemptionError { diff --git a/nym-api/src/ecash/mod.rs b/nym-api/src/ecash/mod.rs index 662822114f..6a1d04564a 100644 --- a/nym-api/src/ecash/mod.rs +++ b/nym-api/src/ecash/mod.rs @@ -1,11 +1,6 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; - pub(crate) mod api_routes; pub(crate) mod client; pub(crate) mod comm; @@ -21,27 +16,3 @@ pub(crate) mod tests; // equivalent of 100nym pub(crate) const MINIMUM_BALANCE: u128 = 100_000000; - -pub(crate) fn routes_open_api(settings: &OpenApiSettings, enabled: bool) -> (Vec, OpenApi) { - if enabled { - openapi_get_routes_spec![ - settings: - api_routes::partial_signing::post_blind_sign, - api_routes::partial_signing::partial_expiration_date_signatures, - api_routes::partial_signing::partial_coin_indices_signatures, - api_routes::spending::verify_ticket, - api_routes::spending::batch_redeem_tickets, - api_routes::spending::double_spending_filter_v1, - api_routes::issued::epoch_credentials, - api_routes::issued::issued_credential, - api_routes::issued::issued_credentials, - api_routes::aggregation::master_verification_key, - api_routes::aggregation::coin_indices_signatures, - api_routes::aggregation::expiration_date_signatures - ] - } else { - openapi_get_routes_spec![ - settings: - ] - } -} diff --git a/nym-api/src/ecash/state/helpers.rs b/nym-api/src/ecash/state/helpers.rs index 2fcfd18a31..b96ab53099 100644 --- a/nym-api/src/ecash/state/helpers.rs +++ b/nym-api/src/ecash/state/helpers.rs @@ -16,6 +16,7 @@ use std::future::Future; use time::ext::NumericalDuration; use time::Date; use tokio::sync::Mutex; +use tracing::{debug, error, info, warn}; // attempt to completely rebuild the bloomfilter data for given day async fn try_rebuild_today_bloomfilter( @@ -23,10 +24,10 @@ async fn try_rebuild_today_bloomfilter( params: BloomfilterParameters, storage: &NymApiStorage, ) -> Result { - log::info!("rebuilding bloomfilter for {today}"); + info!("rebuilding bloomfilter for {today}"); let tickets = storage.get_all_spent_tickets_on(today).await?; - log::debug!( + debug!( "there are {} tickets to insert into the filter", tickets.len() ); @@ -45,7 +46,7 @@ pub(crate) async fn prepare_partial_bloomfilter_builder( start: Date, days: i64, ) -> Result { - log::info!( + info!( "attempting to rebuild partial bloomfilter starting at {start} which includes {days} days" ); @@ -56,11 +57,11 @@ pub(crate) async fn prepare_partial_bloomfilter_builder( .try_load_partial_bloomfilter_bitmap(date, params_id) .await? else { - log::warn!("missing double spending bloomfilter bitmap for {date} (if this API hasn't been running for at least {days} day(s) since 'ecash'-based zk-nyms were introduced this is expected)"); + warn!("missing double spending bloomfilter bitmap for {date} (if this API hasn't been running for at least {days} day(s) since 'ecash'-based zk-nyms were introduced this is expected)"); continue; }; if !filter_builder.add_bytes(&bitmap) { - log::error!( + error!( "failed to add bitmap from {date} to the global bloomfilter. it may be malformed!" ); } @@ -71,16 +72,16 @@ pub(crate) async fn prepare_partial_bloomfilter_builder( pub(super) async fn try_rebuild_bloomfilter( storage: &NymApiStorage, ) -> Result { - log::info!("attempting to rebuild the double spending bloomfilter..."); + info!("attempting to rebuild the double spending bloomfilter..."); let today = ecash_today().date(); let (params_id, params) = storage.get_double_spending_filter_params().await?; - log::info!("will use the following parameters: {params:?}"); + info!("will use the following parameters: {params:?}"); // we're never going to have persisted data for 'today'. we need to rebuild it from scratch let today_filter = try_rebuild_today_bloomfilter(today, params, storage).await?; - log::info!("attempting to rebuild the global filter"); + info!("attempting to rebuild the global filter"); let mut global_filter = prepare_partial_bloomfilter_builder( storage, params, @@ -91,9 +92,7 @@ pub(super) async fn try_rebuild_bloomfilter( .await?; if !global_filter.add_bytes(&today_filter.dump_bitmap()) { - log::error!( - "failed to add bitmap from {today} to the global bloomfilter. it may be malformed!" - ); + error!("failed to add bitmap from {today} to the global bloomfilter. it may be malformed!"); } Ok(TicketDoubleSpendingFilter::new( @@ -141,7 +140,7 @@ where match f(api).await { Ok(partial_share) => shares.lock().await.push(partial_share), Err(err) => { - log::warn!("failed to obtain partial threshold data from API: {disp}: {err}") + warn!("failed to obtain partial threshold data from API: {disp}: {err}") } } }) diff --git a/nym-api/src/ecash/state/local.rs b/nym-api/src/ecash/state/local.rs index d18935dd0e..b840bab394 100644 --- a/nym-api/src/ecash/state/local.rs +++ b/nym-api/src/ecash/state/local.rs @@ -80,6 +80,11 @@ pub(crate) struct LocalEcashState { pub(crate) ecash_keypair: KeyPair, pub(crate) identity_keypair: identity::KeyPair, + pub(crate) explicitly_disabled: bool, + + /// Specifies whether this api is a signer in given epoch + pub(crate) active_signer: CachedImmutableEpochItem, + pub(crate) partial_coin_index_signatures: CachedImmutableEpochItem, pub(crate) partial_expiration_date_signatures: CachedImmutableItems, @@ -93,10 +98,13 @@ impl LocalEcashState { ecash_keypair: KeyPair, identity_keypair: identity::KeyPair, double_spending_filter: TicketDoubleSpendingFilter, + explicitly_disabled: bool, ) -> Self { LocalEcashState { ecash_keypair, identity_keypair, + explicitly_disabled, + active_signer: Default::default(), partial_coin_index_signatures: Default::default(), partial_expiration_date_signatures: Default::default(), double_spending_filter: Arc::new(RwLock::new(double_spending_filter)), diff --git a/nym-api/src/ecash/state/mod.rs b/nym-api/src/ecash/state/mod.rs index 8676dba935..11ed0d3ff7 100644 --- a/nym-api/src/ecash/state/mod.rs +++ b/nym-api/src/ecash/state/mod.rs @@ -44,9 +44,11 @@ use nym_ecash_double_spending::DoubleSpendingFilter; use nym_ecash_time::cred_exp_date; use nym_validator_client::nyxd::AccountId; use nym_validator_client::EcashApiClient; +use std::ops::Deref; use time::ext::NumericalDuration; use time::{Date, OffsetDateTime}; use tokio::sync::RwLockReadGuard; +use tracing::{debug, error, info, warn}; pub(crate) mod auxiliary; pub(crate) mod bloom; @@ -73,6 +75,7 @@ impl EcashState { key_pair: KeyPair, comm_channel: D, storage: NymApiStorage, + signer_disabled: bool, ) -> Result where C: LocalClient + Send + Sync + 'static, @@ -82,11 +85,43 @@ impl EcashState { Ok(Self { global: GlobalEcachState::new(contract_address), - local: LocalEcashState::new(key_pair, identity_keypair, double_spending_filter), + local: LocalEcashState::new( + key_pair, + identity_keypair, + double_spending_filter, + signer_disabled, + ), aux: AuxiliaryEcashState::new(client, comm_channel, storage), }) } + /// Ensures that this nym-api is one of ecash signers for the current epoch + pub(crate) async fn ensure_signer(&self) -> Result<()> { + if self.local.explicitly_disabled { + return Err(EcashError::NotASigner); + } + + let epoch_id = self.aux.current_epoch().await?; + + let is_epoch_signer = self + .local + .active_signer + .get_or_init(epoch_id, || async { + let address = self.aux.client.address().await; + let ecash_signers = self.aux.comm_channel.ecash_clients(epoch_id).await?; + + // check if any ecash signers for this epoch has the same cosmos address as this api + Ok(ecash_signers.iter().any(|c| c.cosmos_address == address)) + }) + .await?; + + if !is_epoch_signer.deref() { + return Err(EcashError::NotASigner); + } + + Ok(()) + } + pub(crate) async fn ecash_signing_key(&self) -> Result> { self.local.ecash_keypair.signing_key().await } @@ -169,7 +204,7 @@ impl EcashState { }); } - log::info!( + info!( "attempting to establish master coin index signatures for epoch {epoch_id}..." ); @@ -263,7 +298,7 @@ impl EcashState { // because if it was a past epoch we **do** have those keys. // they're just archived - log::error!("received partial coin index signature request for an invalid epoch ({epoch_id}). our key was derived for epoch {}", signing_keys.issued_for_epoch); + error!("received partial coin index signature request for an invalid epoch ({epoch_id}). our key was derived for epoch {}", signing_keys.issued_for_epoch); return Err(EcashError::InvalidSigningKeyEpoch { requested: epoch_id, available: signing_keys.issued_for_epoch, @@ -550,7 +585,7 @@ impl EcashState { pub(crate) async fn accept_proposal(&self, proposal_id: u64) -> Result<()> { //SW NOTE: What to do if this fails if let Err(err) = self.aux.client.vote_proposal(proposal_id, true, None).await { - log::debug!("failed to vote on proposal {proposal_id}: {err}"); + debug!("failed to vote on proposal {proposal_id}: {err}"); } Ok(()) @@ -755,7 +790,7 @@ impl EcashState { // sanity check because this should NEVER happen, // but when it inevitably does, we don't want to crash if spending_date != yesterday { - log::error!("attempted to insert a ticket with spending date of {spending_date} while it's {today} today!!"); + error!("attempted to insert a ticket with spending date of {spending_date} while it's {today} today!!"); } // this shouldn't be happening too often, so it's fine to interact with the storage @@ -771,7 +806,7 @@ impl EcashState { return Ok(guard.insert_global_only(serial_number)); } - log::info!("we need to advance our bloomfilter"); + info!("we need to advance our bloomfilter"); let previous_bitmap = guard.export_today_bitmap(); // archive the BF for today's date diff --git a/nym-api/src/ecash/storage/manager.rs b/nym-api/src/ecash/storage/manager.rs index a77b4dbb20..68d39a6a92 100644 --- a/nym-api/src/ecash/storage/manager.rs +++ b/nym-api/src/ecash/storage/manager.rs @@ -6,6 +6,7 @@ use crate::ecash::storage::models::{ StoredBloomfilterParams, TicketProvider, VerifiedTicket, }; use crate::support::storage::manager::StorageManager; +use async_trait::async_trait; use nym_coconut_dkg_common::types::EpochId; use nym_ecash_contract_common::deposit::DepositId; use time::{Date, OffsetDateTime}; @@ -42,7 +43,7 @@ pub trait EcashStorageManagerExt { credential_id: i64, ) -> Result<(), sqlx::Error>; - /// Attempts to retrieve an issued credential from the data store. + /// Attempts to retrieve an issued credential from the data store. /// /// # Arguments /// @@ -75,7 +76,7 @@ pub trait EcashStorageManagerExt { ticketbook_type_repr: u8, ) -> Result; - /// Attempts to retrieve issued credentials from the data store using provided ids. + /// Attempts to retrieve issued credentials from the data store using provided ids. /// /// # Arguments /// @@ -85,7 +86,7 @@ pub trait EcashStorageManagerExt { credential_ids: Vec, ) -> Result, sqlx::Error>; - /// Attempts to retrieve issued credentials from the data store using pagination specification. + /// Attempts to retrieve issued credentials from the data store using pagination specification. /// /// # Arguments /// @@ -259,7 +260,7 @@ impl EcashStorageManagerExt for StorageManager { sqlx::query!( r#" - INSERT INTO epoch_credentials + INSERT INTO epoch_credentials (epoch_id, start_id, total_issued) VALUES (?, ?, ?); "#, @@ -305,7 +306,7 @@ impl EcashStorageManagerExt for StorageManager { "#, epoch_id_downcasted ) - .fetch_optional(&mut tx) + .fetch_optional(&mut *tx) .await? { // the entry has existed before -> update it @@ -313,33 +314,33 @@ impl EcashStorageManagerExt for StorageManager { // no credentials has been issued -> we have to set the `start_id` sqlx::query!( r#" - UPDATE epoch_credentials + UPDATE epoch_credentials SET total_issued = 1, start_id = ? WHERE epoch_id = ? "#, credential_id, epoch_id_downcasted ) - .execute(&mut tx) + .execute(&mut *tx) .await?; } else { // we have issued credentials in this epoch before -> just increment `total_issued` sqlx::query!( r#" - UPDATE epoch_credentials - SET total_issued = total_issued + 1 + UPDATE epoch_credentials + SET total_issued = total_issued + 1 WHERE epoch_id = ? "#, epoch_id_downcasted ) - .execute(&mut tx) + .execute(&mut *tx) .await?; } } else { // the entry has never been created -> probably some race condition; create it instead sqlx::query!( r#" - INSERT INTO epoch_credentials + INSERT INTO epoch_credentials (epoch_id, start_id, total_issued) VALUES (?, ?, ?); "#, @@ -347,7 +348,7 @@ impl EcashStorageManagerExt for StorageManager { credential_id, 1 ) - .execute(&mut tx) + .execute(&mut *tx) .await?; } @@ -355,7 +356,7 @@ impl EcashStorageManagerExt for StorageManager { tx.commit().await } - /// Attempts to retrieve an issued credential from the data store. + /// Attempts to retrieve an issued credential from the data store. /// /// # Arguments /// @@ -367,7 +368,7 @@ impl EcashStorageManagerExt for StorageManager { sqlx::query_as!( IssuedTicketbook, r#" - SELECT + SELECT id, epoch_id as "epoch_id: u32", deposit_id as "deposit_id: DepositId", @@ -397,7 +398,7 @@ impl EcashStorageManagerExt for StorageManager { sqlx::query_as!( IssuedTicketbook, r#" - SELECT + SELECT id, epoch_id as "epoch_id: u32", deposit_id as "deposit_id: DepositId", @@ -439,7 +440,7 @@ impl EcashStorageManagerExt for StorageManager { Ok(row_id) } - /// Attempts to retrieve issued credentials from the data store using provided ids. + /// Attempts to retrieve issued credentials from the data store using provided ids. /// /// # Arguments /// @@ -460,7 +461,7 @@ impl EcashStorageManagerExt for StorageManager { query.fetch_all(&self.connection_pool).await } - /// Attempts to retrieve issued credentials from the data store using pagination specification. + /// Attempts to retrieve issued credentials from the data store using pagination specification. /// /// # Arguments /// @@ -474,7 +475,7 @@ impl EcashStorageManagerExt for StorageManager { sqlx::query_as!( IssuedTicketbook, r#" - SELECT + SELECT id, epoch_id as "epoch_id: u32", deposit_id as "deposit_id: DepositId", @@ -563,7 +564,7 @@ impl EcashStorageManagerExt for StorageManager { WHERE gateway_id = ? AND verified_at > ? ORDER BY verified_at ASC - LIMIT 65535 + LIMIT 65535 "#, provider_id, since @@ -581,7 +582,7 @@ impl EcashStorageManagerExt for StorageManager { r#" SELECT serial_number FROM verified_tickets - WHERE spending_date = ? + WHERE spending_date = ? "#, date ) diff --git a/nym-api/src/ecash/storage/mod.rs b/nym-api/src/ecash/storage/mod.rs index cd7fc26f40..5c15f2b2f6 100644 --- a/nym-api/src/ecash/storage/mod.rs +++ b/nym-api/src/ecash/storage/mod.rs @@ -12,6 +12,7 @@ use crate::ecash::storage::models::{ }; use crate::node_status_api::models::NymApiStorageError; use crate::support::storage::NymApiStorage; +use async_trait::async_trait; use nym_api_requests::ecash::models::Pagination; use nym_coconut_dkg_common::types::EpochId; use nym_compact_ecash::scheme::coin_indices_signatures::AnnotatedCoinIndexSignature; @@ -24,6 +25,7 @@ use nym_crypto::asymmetric::identity; use nym_ecash_contract_common::deposit::DepositId; use nym_validator_client::nyxd::AccountId; use time::{Date, OffsetDateTime}; +use tracing::info; mod helpers; pub(crate) mod manager; diff --git a/nym-api/src/ecash/tests/issued_credentials.rs b/nym-api/src/ecash/tests/issued_credentials.rs index 5a86d70923..00dac0d525 100644 --- a/nym-api/src/ecash/tests/issued_credentials.rs +++ b/nym-api/src/ecash/tests/issued_credentials.rs @@ -2,12 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::ecash::tests::{voucher_fixture, TestFixture}; +use axum::http::StatusCode; use nym_api_requests::ecash::models::{ EpochCredentialsResponse, IssuedCredentialResponse, IssuedCredentialsResponse, Pagination, }; use nym_api_requests::ecash::CredentialsRequestBody; use nym_validator_client::nym_api::routes::{API_VERSION, ECASH_ROUTES}; -use rocket::http::Status; use std::collections::BTreeMap; #[tokio::test] @@ -19,11 +19,10 @@ async fn epoch_credentials() { let test_fixture = TestFixture::new().await; // initially we expect 0 issued - let response = test_fixture.rocket.get(&route_epoch1).dispatch().await; + let response = test_fixture.axum.get(&route_epoch1).await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: EpochCredentialsResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: EpochCredentialsResponse = response.json(); assert_eq!(parsed_response.epoch_id, 1); assert_eq!(parsed_response.total_issued, 0); @@ -33,10 +32,9 @@ async fn epoch_credentials() { test_fixture.issue_dummy_credential().await; // now there should be one - let response = test_fixture.rocket.get(&route_epoch1).dispatch().await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: EpochCredentialsResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + let response = test_fixture.axum.get(&route_epoch1).await; + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: EpochCredentialsResponse = response.json(); assert_eq!(parsed_response.epoch_id, 1); assert_eq!(parsed_response.total_issued, 1); @@ -45,23 +43,21 @@ async fn epoch_credentials() { // and another test_fixture.issue_dummy_credential().await; - let response = test_fixture.rocket.get(&route_epoch1).dispatch().await; + let response = test_fixture.axum.get(&route_epoch1).await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: EpochCredentialsResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: EpochCredentialsResponse = response.json(); // note that first epoch credential didn't change assert_eq!(parsed_response.epoch_id, 1); assert_eq!(parsed_response.total_issued, 2); assert_eq!(parsed_response.first_epoch_credential_id, Some(1)); - test_fixture.set_epoch(2); + test_fixture.set_epoch(2).await; - let response = test_fixture.rocket.get(&route_epoch2).dispatch().await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: EpochCredentialsResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + let response = test_fixture.axum.get(&route_epoch2).await; + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: EpochCredentialsResponse = response.json(); // note the epoch change assert_eq!(parsed_response.epoch_id, 2); @@ -70,10 +66,9 @@ async fn epoch_credentials() { test_fixture.issue_dummy_credential().await; - let response = test_fixture.rocket.get(&route_epoch2).dispatch().await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: EpochCredentialsResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + let response = test_fixture.axum.get(&route_epoch2).await; + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: EpochCredentialsResponse = response.json(); // note the epoch change assert_eq!(parsed_response.epoch_id, 2); @@ -81,10 +76,9 @@ async fn epoch_credentials() { assert_eq!(parsed_response.first_epoch_credential_id, Some(3)); // random epoch in the future - let response = test_fixture.rocket.get(&route_epoch42).dispatch().await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: EpochCredentialsResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + let response = test_fixture.axum.get(&route_epoch42).await; + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: EpochCredentialsResponse = response.json(); assert_eq!(parsed_response.epoch_id, 42); assert_eq!(parsed_response.total_issued, 0); assert_eq!(parsed_response.first_epoch_credential_id, None); @@ -114,27 +108,24 @@ async fn issued_credential() { test_fixture.add_deposit(&voucher2); // random credential that was never issued - let response = test_fixture.rocket.get(route(42)).dispatch().await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: IssuedCredentialResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + let response = test_fixture.axum.get(&route(42)).await; + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: IssuedCredentialResponse = response.json(); assert!(parsed_response.credential.is_none()); let cred1 = test_fixture.issue_credential(request1.clone()).await; - test_fixture.set_epoch(3); + test_fixture.set_epoch(3).await; let cred2 = test_fixture.issue_credential(request2.clone()).await; - let response = test_fixture.rocket.get(route(1)).dispatch().await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: IssuedCredentialResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + let response = test_fixture.axum.get(&route(1)).await; + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: IssuedCredentialResponse = response.json(); let issued1 = parsed_response.credential.unwrap(); - let response = test_fixture.rocket.get(route(2)).dispatch().await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: IssuedCredentialResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + let response = test_fixture.axum.get(&route(2)).await; + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: IssuedCredentialResponse = response.json(); let issued2 = parsed_response.credential.unwrap(); // TODO: currently we have no signature checks @@ -192,37 +183,33 @@ async fn issued_credentials() { let issued13 = test_fixture.issued_unchecked(13).await; let response = test_fixture - .rocket + .axum .post(&route) .json(&CredentialsRequestBody { credential_ids: vec![5], pagination: None, }) - .dispatch() .await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: IssuedCredentialsResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: IssuedCredentialsResponse = response.json(); assert_eq!(parsed_response.credentials[&5], issued5); assert!(!parsed_response.credentials.contains_key(&13)); let response = test_fixture - .rocket + .axum .post(&route) .json(&CredentialsRequestBody { credential_ids: vec![5, 13], pagination: None, }) - .dispatch() .await; - assert_eq!(response.status(), Status::Ok); - let parsed_response: IssuedCredentialsResponse = - serde_json::from_str(&response.into_string().await.unwrap()).unwrap(); + assert_eq!(response.status_code(), StatusCode::OK); + let parsed_response: IssuedCredentialsResponse = response.json(); assert_eq!(parsed_response.credentials[&5], issued5); assert_eq!(parsed_response.credentials[&13], issued13); let response_paginated = test_fixture - .rocket + .axum .post(&route) .json(&CredentialsRequestBody { credential_ids: vec![], @@ -231,11 +218,9 @@ async fn issued_credentials() { limit: Some(2), }), }) - .dispatch() .await; - assert_eq!(response_paginated.status(), Status::Ok); - let parsed_response: IssuedCredentialsResponse = - serde_json::from_str(&response_paginated.into_string().await.unwrap()).unwrap(); + assert_eq!(response_paginated.status_code(), StatusCode::OK); + let parsed_response: IssuedCredentialsResponse = response_paginated.json(); let mut expected = BTreeMap::new(); expected.insert(1, issued1); @@ -243,7 +228,7 @@ async fn issued_credentials() { assert_eq!(expected, parsed_response.credentials); let response_paginated = test_fixture - .rocket + .axum .post(&route) .json(&CredentialsRequestBody { credential_ids: vec![], @@ -252,11 +237,9 @@ async fn issued_credentials() { limit: Some(3), }), }) - .dispatch() .await; - assert_eq!(response_paginated.status(), Status::Ok); - let parsed_response: IssuedCredentialsResponse = - serde_json::from_str(&response_paginated.into_string().await.unwrap()).unwrap(); + assert_eq!(response_paginated.status_code(), StatusCode::OK); + let parsed_response: IssuedCredentialsResponse = response_paginated.json(); let mut expected = BTreeMap::new(); expected.insert(3, issued3); diff --git a/nym-api/src/ecash/tests/mod.rs b/nym-api/src/ecash/tests/mod.rs index fb037a85f0..4ffe0b4e5f 100644 --- a/nym-api/src/ecash/tests/mod.rs +++ b/nym-api/src/ecash/tests/mod.rs @@ -1,13 +1,24 @@ // Copyright 2022-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::ecash; +use crate::circulating_supply_api::cache::CirculatingSupplyCache; +use crate::ecash::api_routes::handlers::ecash_routes; use crate::ecash::error::{EcashError, Result}; use crate::ecash::keys::KeyPairWithEpoch; use crate::ecash::state::EcashState; use crate::ecash::storage::EcashStorageExt; +use crate::network::models::NetworkDetails; +use crate::node_describe_cache::DescribedNodes; +use crate::node_status_api::handlers::unstable; +use crate::node_status_api::NodeStatusCache; +use crate::nym_contract_cache::cache::NymContractCache; +use crate::support::caching::cache::SharedCache; +use crate::support::http::state::{AppState, ForcedRefresh}; use crate::support::storage::NymApiStorage; use async_trait::async_trait; +use axum::Router; +use axum_test::http::StatusCode; +use axum_test::TestServer; use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{ from_binary, to_binary, Addr, Binary, BlockInfo, CosmosMsg, Decimal, MessageInfo, WasmMsg, @@ -31,6 +42,7 @@ use nym_coconut_dkg_common::types::{ use nym_coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; use nym_compact_ecash::BlindedSignature; use nym_compact_ecash::{ttp_keygen, VerificationKeyAuth}; +use nym_config::defaults::NymNetworkDetails; use nym_contracts_common::IdentityKey; use nym_credentials::IssuanceTicketBook; use nym_credentials_interface::TicketType; @@ -45,8 +57,6 @@ use nym_validator_client::nyxd::{AccountId, ExecTxResult, Fee, Hash, TxResponse} use nym_validator_client::{EcashApiClient, NymApiClient}; use rand::rngs::OsRng; use rand::RngCore; -use rocket::http::Status; -use rocket::local::asynchronous::Client; use std::collections::{BTreeMap, HashMap}; use std::ops::Deref; use std::str::FromStr; @@ -863,7 +873,7 @@ impl super::client::Client for DummyClient { .votes .contains_key(&(voter.clone(), proposal_id)) { - todo!("already voted"); + panic!("unhandled case: already voted"); } chain.multisig_contract.votes.insert( (voter.clone(), proposal_id), @@ -1123,18 +1133,23 @@ impl DummyCommunicationChannel { } } - pub fn new_single_dummy(aggregated_verification_key: VerificationKeyAuth) -> Self { + pub fn new_single_dummy( + aggregated_verification_key: VerificationKeyAuth, + cosmos_address: AccountId, + ) -> Self { let client = EcashApiClient { api_client: NymApiClient::new("http://localhost:1234".parse().unwrap()), verification_key: aggregated_verification_key, node_id: 1, - cosmos_address: "n16a32stm6kknhq5cc8rx77elr66pygf2hfszw7wvpq746x3uffylqkjar4l" - .parse() - .unwrap(), + cosmos_address, }; Self::new(vec![client]) } + pub fn clients_arc(&self) -> Arc>>> { + Arc::clone(&self.ecash_clients) + } + pub fn with_epoch(mut self, current_epoch: Arc) -> Self { self.current_epoch = current_epoch; self @@ -1234,23 +1249,44 @@ fn dummy_signature() -> identity::Signature { } struct TestFixture { - rocket: Client, + axum: TestServer, storage: NymApiStorage, chain_state: SharedFakeChain, epoch: Arc, + ecash_clients: Arc>>>, _tmp_dir: TempDir, } impl TestFixture { + fn build_app_state(storage: NymApiStorage) -> AppState { + AppState { + forced_refresh: ForcedRefresh::new(true), + nym_contract_cache: NymContractCache::new(), + node_status_cache: NodeStatusCache::new(), + circulating_supply_cache: CirculatingSupplyCache::new("unym".to_owned()), + storage, + described_nodes_cache: SharedCache::::new(), + network_details: NetworkDetails::new( + "localhost".to_string(), + NymNetworkDetails::new_empty(), + ), + node_info_cache: unstable::NodeInfoCache::default(), + } + } + async fn new() -> Self { let mut rng = crate::ecash::tests::fixtures::test_rng([69u8; 32]); let coconut_keypair = ttp_keygen(1, 1).unwrap().remove(0); let identity = identity::KeyPair::new(&mut rng); let epoch = Arc::new(AtomicU64::new(1)); - let comm_channel = - DummyCommunicationChannel::new_single_dummy(coconut_keypair.verification_key().clone()) - .with_epoch(epoch.clone()); + let address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(); + let comm_channel = DummyCommunicationChannel::new_single_dummy( + coconut_keypair.verification_key().clone(), + address.clone(), + ) + .with_epoch(epoch.clone()); + let ecash_clients = comm_channel.clients_arc(); // TODO: it's AWFUL to test with actual storage, we should somehow abstract it away let tmp_dir = tempdir().unwrap(); @@ -1268,10 +1304,7 @@ impl TestFixture { staged_key_pair.validate(); let chain_state = SharedFakeChain::default(); - let nyxd_client = DummyClient::new( - AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), - chain_state.clone(), - ); + let nyxd_client = DummyClient::new(address, chain_state.clone()); let ecash_contract = chain_state .lock() @@ -1283,37 +1316,42 @@ impl TestFixture { .parse() .unwrap(); - let rocket = rocket::build() - .manage( - EcashState::new( - ecash_contract, - nyxd_client, - identity, - staged_key_pair, - comm_channel, - storage.clone(), - ) - .await - .unwrap(), - ) - .mount( - "/v1/ecash", - ecash::routes_open_api(&Default::default(), true).0, - ); + let ecash_state = EcashState::new( + ecash_contract, + nyxd_client, + identity, + staged_key_pair, + comm_channel, + storage.clone(), + false, + ) + .await + .unwrap(); TestFixture { - rocket: Client::tracked(rocket) - .await - .expect("valid rocket instance"), + axum: TestServer::new( + Router::new() + .nest("/v1/ecash", ecash_routes(Arc::new(ecash_state))) + .with_state(Self::build_app_state(storage.clone())), + ) + .unwrap(), storage, chain_state, epoch, + ecash_clients, _tmp_dir: tmp_dir, } } - fn set_epoch(&self, epoch: u64) { - self.epoch.store(epoch, Ordering::Relaxed) + async fn set_epoch(&self, epoch: u64) { + let current_epoch = self.epoch.load(Ordering::Relaxed); + self.epoch.store(epoch, Ordering::Relaxed); + + // copy the same epoch_signers as we had initially + let existing = self.ecash_clients.read().await.get(¤t_epoch).cloned(); + if let Some(clients) = existing { + self.ecash_clients.write().await.insert(epoch, clients); + } } #[allow(dead_code)] @@ -1351,26 +1389,24 @@ impl TestFixture { async fn issue_credential(&self, req: BlindSignRequestBody) -> BlindedSignatureResponse { let response = self - .rocket - .post(format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}",)) + .axum + .post(&format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}",)) .json(&req) - .dispatch() .await; - assert_eq!(response.status(), Status::Ok); - serde_json::from_str(&response.into_string().await.unwrap()).unwrap() + assert_eq!(response.status_code(), StatusCode::OK); + response.json() } async fn issued_credential(&self, id: i64) -> Option { let response = self - .rocket - .get(format!( + .axum + .get(&format!( "/{API_VERSION}/{ECASH_ROUTES}/issued-credential/{id}" )) - .dispatch() .await; - assert_eq!(response.status(), Status::Ok); - serde_json::from_str(&response.into_string().await.unwrap()).unwrap() + assert_eq!(response.status_code(), StatusCode::OK); + response.json() } async fn issued_unchecked(&self, id: i64) -> IssuedTicketbookBody { @@ -1385,6 +1421,7 @@ impl TestFixture { #[cfg(test)] mod credential_tests { use super::*; + use axum::http::StatusCode; use nym_compact_ecash::ttp_keygen; #[tokio::test] @@ -1416,23 +1453,15 @@ mod credential_tests { .unwrap(); let response = test_fixture - .rocket - .post(format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}",)) + .axum + .post(&format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}",)) .json(&request_body) - .dispatch() .await; - assert_eq!(response.status(), Status::Ok); + + assert_eq!(response.status_code(), StatusCode::OK); let expected_response = BlindedSignatureResponse::new(sig); + let blinded_signature_response = response.json::(); - // This is a more direct way, but there's a bug which makes it hang https://github.com/SergioBenitez/Rocket/issues/1893 - // let blinded_signature_response = response - // .into_json::() - // .await - // .unwrap(); - let blinded_signature_response = serde_json::from_str::( - &response.into_string().await.unwrap(), - ) - .unwrap(); assert_eq!( blinded_signature_response.to_bytes(), expected_response.to_bytes() @@ -1443,19 +1472,19 @@ mod credential_tests { async fn state_functions() { let mut rng = OsRng; let identity = identity::KeyPair::new(&mut rng); + let address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(); - let nyxd_client = DummyClient::new( - AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), - Default::default(), - ); + let nyxd_client = DummyClient::new(address.clone(), Default::default()); let key_pair = ttp_keygen(1, 1).unwrap().remove(0); let tmp_dir = tempdir().unwrap(); let storage = NymApiStorage::init(tmp_dir.path().join("storage.db")) .await .unwrap(); - let comm_channel = - DummyCommunicationChannel::new_single_dummy(key_pair.verification_key().clone()); + let comm_channel = DummyCommunicationChannel::new_single_dummy( + key_pair.verification_key().clone(), + address, + ); let staged_key_pair = crate::ecash::keys::KeyPair::new(); staged_key_pair .set(KeyPairWithEpoch { @@ -1474,6 +1503,7 @@ mod credential_tests { staged_key_pair, comm_channel, storage.clone(), + false, ) .await .unwrap(); @@ -1596,19 +1626,13 @@ mod credential_tests { let request_body = voucher.create_blind_sign_request_body(&signing_data); let response = test - .rocket - .post(format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}")) + .axum + .post(&format!("/{API_VERSION}/{ECASH_ROUTES}/{ECASH_BLIND_SIGN}")) .json(&request_body) - .dispatch() .await; - assert_eq!(response.status(), Status::Ok); - // This is a more direct way, but there's a bug which makes it hang https://github.com/SergioBenitez/Rocket/issues/1893 - // assert!(response.into_json::().is_some()); - let blinded_signature_response = serde_json::from_str::( - &response.into_string().await.unwrap(), - ); - assert!(blinded_signature_response.is_ok()); + assert_eq!(response.status_code(), StatusCode::OK); + let _ = response.json::(); } #[test] diff --git a/nym-api/src/epoch_operations/error.rs b/nym-api/src/epoch_operations/error.rs index a9f0c8ad99..65fc822dff 100644 --- a/nym-api/src/epoch_operations/error.rs +++ b/nym-api/src/epoch_operations/error.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::node_status_api::models::NymApiStorageError; -use nym_mixnet_contract_common::{EpochState, MixId}; +use nym_mixnet_contract_common::nym_node::Role; +use nym_mixnet_contract_common::{EpochState, NodeId}; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::AccountId; use nym_validator_client::ValidatorClientError; @@ -23,7 +24,10 @@ pub enum RewardingError { }, #[error("it seems the current epoch is in mid-rewarding state (last rewarded is {last_rewarded}). With our current nym-api this shouldn't have been possible. Manual intervention is required.")] - MidMixRewarding { last_rewarded: MixId }, + MidNodeRewarding { last_rewarded: NodeId }, + + #[error("it seems the current epoch is in mid-role assignment state (next role to assign is {next}). With our current nym-api this shouldn't have been possible. Manual intervention is required.")] + MidRoleAssignment { next: Role }, // #[error("There were no mixnodes to reward (network is dead)")] // NoMixnodesToReward, @@ -42,12 +46,16 @@ pub enum RewardingError { #[from] source: std::num::TryFromIntError, }, + #[error("{source}")] WeightedError { #[from] source: rand::distributions::WeightedError, }, + #[error("could not obtain the current interval rewarding parameters")] + RewardingParamsRetrievalFailure, + #[error("{0}")] GenericError(#[from] anyhow::Error), } diff --git a/nym-api/src/epoch_operations/event_reconciliation.rs b/nym-api/src/epoch_operations/event_reconciliation.rs index 47230463c9..302763e68b 100644 --- a/nym-api/src/epoch_operations/event_reconciliation.rs +++ b/nym-api/src/epoch_operations/event_reconciliation.rs @@ -2,11 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::epoch_operations::error::RewardingError; -use crate::RewardedSetUpdater; +use crate::EpochAdvancer; use nym_mixnet_contract_common::EpochState; use std::cmp::max; +use tracing::{error, info, warn}; -impl RewardedSetUpdater { +impl EpochAdvancer { pub(super) async fn reconcile_epoch_events(&self) -> Result<(), RewardingError> { let epoch_status = self.nyxd_client.get_current_epoch_status().await?; match epoch_status.state { @@ -18,17 +19,17 @@ impl RewardedSetUpdater { operation: "reconciling epoch events".to_string(), }) } - EpochState::AdvancingEpoch => { + EpochState::RoleAssignment { .. } => { warn!("we seem to have crashed mid epoch operations... no need to reconcile events as we've already done that!"); Ok(()) } EpochState::ReconcilingEvents => { - log::info!("Reconciling all pending epoch events..."); + info!("Reconciling all pending epoch events..."); if let Err(err) = self._reconcile_epoch_events().await { - log::error!("FAILED to reconcile epoch events... - {err}"); + error!("FAILED to reconcile epoch events... - {err}"); Err(err) } else { - log::info!("Reconciled all pending epoch events... SUCCESS"); + info!("Reconciled all pending epoch events... SUCCESS"); Ok(()) } } diff --git a/nym-api/src/epoch_operations/helpers.rs b/nym-api/src/epoch_operations/helpers.rs index 61ea0defeb..a4f5966615 100644 --- a/nym-api/src/epoch_operations/helpers.rs +++ b/nym-api/src/epoch_operations/helpers.rs @@ -1,24 +1,41 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::epoch_operations::RewardedSetUpdater; +use crate::epoch_operations::EpochAdvancer; use cosmwasm_std::{Decimal, Fraction}; -use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::{ExecuteMsg, Interval, MixId}; +use nym_mixnet_contract_common::reward_params::{NodeRewardingParameters, Performance, WorkFactor}; +use nym_mixnet_contract_common::{ExecuteMsg, Interval, NodeId, RewardedSet, RewardingParams}; use serde::{Deserialize, Serialize}; #[derive(Debug, Clone, Copy, Serialize, Deserialize)] -pub(crate) struct MixnodeWithPerformance { - pub(crate) mix_id: MixId, - +pub(crate) struct NodeWithPerformance { + pub(crate) node_id: NodeId, pub(crate) performance: Performance, } -impl From for ExecuteMsg { - fn from(mix_reward: MixnodeWithPerformance) -> Self { - ExecuteMsg::RewardMixnode { - mix_id: mix_reward.mix_id, - performance: mix_reward.performance, +impl NodeWithPerformance { + pub fn with_work(self, work_factor: WorkFactor) -> RewardedNodeWithParams { + RewardedNodeWithParams { + node_id: self.node_id, + params: NodeRewardingParameters { + performance: self.performance, + work_factor, + }, + } + } +} + +#[derive(Debug, Clone, Copy)] +pub struct RewardedNodeWithParams { + pub(crate) node_id: NodeId, + pub(crate) params: NodeRewardingParameters, +} + +impl From for ExecuteMsg { + fn from(node_reward: RewardedNodeWithParams) -> Self { + ExecuteMsg::RewardNode { + node_id: node_reward.node_id, + params: node_reward.params, } } } @@ -37,36 +54,122 @@ pub(super) fn stake_to_f64(stake: Decimal) -> f64 { } } -impl RewardedSetUpdater { - pub(crate) async fn load_performance( +impl EpochAdvancer { + pub(crate) async fn load_mixnode_performance( &self, interval: &Interval, - mix_id: MixId, - ) -> MixnodeWithPerformance { + node_id: NodeId, + ) -> NodeWithPerformance { let uptime = self .storage .get_average_mixnode_uptime_in_the_last_24hrs( - mix_id, + node_id, + interval.current_epoch_end_unix_timestamp(), + ) + .await + .unwrap_or_default(); + + NodeWithPerformance { + node_id, + performance: uptime.into(), + } + } + + pub(crate) async fn load_gateway_performance( + &self, + interval: &Interval, + node_id: NodeId, + ) -> NodeWithPerformance { + let uptime = self + .storage + .get_average_gateway_uptime_in_the_last_24hrs( + node_id, interval.current_epoch_end_unix_timestamp(), ) .await .unwrap_or_default(); - MixnodeWithPerformance { - mix_id, + NodeWithPerformance { + node_id, performance: uptime.into(), } } - pub(crate) async fn load_nodes_performance( + pub(crate) async fn load_any_performance( &self, interval: &Interval, - nodes: &[MixId], - ) -> Vec { - let mut with_performance = Vec::with_capacity(nodes.len()); - for mix_id in nodes { - with_performance.push(self.load_performance(interval, *mix_id).await) + node_id: NodeId, + ) -> NodeWithPerformance { + // currently we can't do much better without new network monitor + let mix_performance = self.load_mixnode_performance(interval, node_id).await; + if !mix_performance.performance.is_zero() { + return mix_performance; } + + self.load_gateway_performance(interval, node_id).await + } + + pub(crate) async fn load_nodes_for_rewarding( + &self, + interval: &Interval, + nodes: &RewardedSet, + // we only need reward parameters for active set work factor and rewarded/active set sizes; + // we do not need exact values of reward pool, staking supply, etc., so it's fine if it's slightly out of sync + global_rewarding_params: RewardingParams, + ) -> Vec { + // currently we are using constant omega for nodes, but that will change with tickets + // or different reward split between entry, exit, etc. at that point this will have to be calculated elsewhere + let active_node_work_factor = global_rewarding_params.active_node_work(); + let standby_node_work_factor = global_rewarding_params.standby_node_work(); + + // SANITY CHECK: + let standby_share = Decimal::from_atomics(nodes.standby.len() as u128, 0).unwrap() + * standby_node_work_factor; + let active_share = Decimal::from_atomics(nodes.active_set_size() as u128, 0).unwrap() + * active_node_work_factor; + let total_work = standby_share + active_share; + + // this HAS TO blow up. there's no recovery + assert!(total_work <= Decimal::one(), "work calculation logic is flawed! somehow the total work in the system is greater than 1!"); + + let mut with_performance = Vec::with_capacity(nodes.rewarded_set_size()); + + // all the active set mixnodes + for node_id in nodes + .layer1 + .iter() + .chain(nodes.layer2.iter()) + .chain(nodes.layer3.iter()) + { + with_performance.push( + self.load_mixnode_performance(interval, *node_id) + .await + .with_work(active_node_work_factor), + ) + } + + // all the active set gateways + for node_id in nodes + .entry_gateways + .iter() + .chain(nodes.exit_gateways.iter()) + { + with_performance.push( + self.load_gateway_performance(interval, *node_id) + .await + .with_work(active_node_work_factor), + ) + } + + // all the standby nodes + for node_id in &nodes.standby { + with_performance.push( + self.load_any_performance(interval, *node_id) + .await + .with_work(standby_node_work_factor), + ) + } + with_performance } } diff --git a/nym-api/src/epoch_operations/mod.rs b/nym-api/src/epoch_operations/mod.rs index 952bb5ca1f..aeae8977fe 100644 --- a/nym-api/src/epoch_operations/mod.rs +++ b/nym-api/src/epoch_operations/mod.rs @@ -1,7 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -// there is couple of reasons for putting this in a separate module: +// there is a couple of reasons for putting this in a separate module: // 1. I didn't feel it fit well in nym contract "cache". It seems like purpose of cache is to just keep updating local data // rather than attempting to change global view (i.e. the active set) // @@ -12,17 +12,20 @@ // 3. Eventually this whole procedure is going to get expanded to allow for distribution of rewarded set generation // and hence this might be a good place for it. +use crate::node_describe_cache::DescribedNodes; use crate::node_status_api::ONE_DAY; use crate::nym_contract_cache::cache::NymContractCache; +use crate::support::caching::cache::SharedCache; use crate::support::nyxd::Client; use crate::support::storage::NymApiStorage; use error::RewardingError; -pub(crate) use helpers::MixnodeWithPerformance; +pub(crate) use helpers::RewardedNodeWithParams; use nym_mixnet_contract_common::{CurrentIntervalResponse, Interval}; use nym_task::{TaskClient, TaskManager}; use std::collections::HashSet; use std::time::Duration; use tokio::time::sleep; +use tracing::{error, info, trace, warn}; pub(crate) mod error; mod event_reconciliation; @@ -31,13 +34,16 @@ mod rewarded_set_assignment; mod rewarding; mod transition_beginning; -pub struct RewardedSetUpdater { +// naming things is difficult, ok? +// this is struct responsible for advancing an epoch +pub struct EpochAdvancer { nyxd_client: Client, nym_contract_cache: NymContractCache, + described_cache: SharedCache, storage: NymApiStorage, } -impl RewardedSetUpdater { +impl EpochAdvancer { pub(crate) async fn current_interval_details( &self, ) -> Result { @@ -47,71 +53,80 @@ impl RewardedSetUpdater { pub(crate) fn new( nyxd_client: Client, nym_contract_cache: NymContractCache, + described_cache: SharedCache, storage: NymApiStorage, ) -> Self { - RewardedSetUpdater { + EpochAdvancer { nyxd_client, nym_contract_cache, + described_cache, storage, } } #[allow(clippy::doc_lazy_continuation)] // This is where the epoch gets advanced, and all epoch related transactions originate - /// Upon each epoch having finished the following actions are executed by this nym-api: - /// 1. it computes the rewards for each node using the ephemera channel for the epoch that - /// ended - /// 2. it queries the mixnet contract to check the current `EpochState` in order to figure out whether - /// a different nym-api has already started epoch transition (not yet applicable) - /// 3. it sends a `BeginEpochTransition` message to the mixnet contract causing the following to happen: - /// - if successful, the address of this validator is going to be saved as being responsible for progressing this epoch. - /// What it means in practice is that once we have multiple instances of nym-api running, - /// only this one will try to perform the rest of the actions. It will also allow it to - /// more easily recover in case of crashes. - /// - the `EpochState` changes to `Rewarding`, meaning the nym-api will now be allowed to send - /// `RewardMixnode` transactions. However, it's not going to be able anything else like `ReconcileEpochEvents` - /// until that is done. - /// - ability to send transactions (by other users) that get resolved once given epoch/interval rolls over, - /// such as `BondMixnode` or `DelegateToMixnode` will temporarily be frozen until the entire procedure is finished. - /// 4. it obtains the current rewarded set and for each node in there (**SORTED BY MIX_ID!!**), - /// it sends (in a single batch) `RewardMixnode` message with the measured performance. - /// Once the final message gets executed, the mixnet contract automatically transitions - /// the state to `ReconcilingEvents`. - /// 5. it obtains the number of pending epoch and interval events and repeatedly sends - /// `ReconcileEpochEvents` transaction until all of them are resolved. - /// At this point the mixnet contract automatically transitions the state to `AdvancingEpoch`. - /// 6. it obtains the list of all nodes on the network and pseudorandomly (but weighted by total stake) - /// determines the new rewarded set. It then assigns layers to the provided nodes taking - /// family information into consideration. Finally it sends `AdvanceCurrentEpoch` message - /// containing the set and layer information thus rolling over the epoch and changing the state - /// to `InProgress`. - /// 7. it purges old (older than 48h) measurement data - /// 8. the whole process repeats once the new epoch finishes + // TODO: make sure this is still up to date + // /// Upon each epoch having finished the following actions are executed by this nym-api: + // /// 1. it computes the rewards for each node using the ephemera channel for the epoch that + // /// ended + // /// 2. it queries the mixnet contract to check the current `EpochState` in order to figure out whether + // /// a different nym-api has already started epoch transition (not yet applicable) + // /// 3. it sends a `BeginEpochTransition` message to the mixnet contract causing the following to happen: + // /// - if successful, the address of this validator is going to be saved as being responsible for progressing this epoch. + // /// What it means in practice is that once we have multiple instances of nym-api running, + // /// only this one will try to perform the rest of the actions. It will also allow it to + // /// more easily recover in case of crashes. + // /// - the `EpochState` changes to `Rewarding`, meaning the nym-api will now be allowed to send + // /// `RewardNode` transactions. However, it's not going to be able anything else like `ReconcileEpochEvents` + // /// until that is done. + // /// - ability to send transactions (by other users) that get resolved once given epoch/interval rolls over, + // /// such as `BondMixnode` or `DelegateToMixnode` will temporarily be frozen until the entire procedure is finished. + // /// 4. it obtains the current rewarded set and for each node in there (**SORTED BY NODE_ID!!**), + // /// it sends (in a single batch) `RewardMixnode` message with the measured performance. + // /// Once the final message gets executed, the mixnet contract automatically transitions + // /// the state to `ReconcilingEvents`. + // /// 5. it obtains the number of pending epoch and interval events and repeatedly sends + // /// `ReconcileEpochEvents` transaction until all of them are resolved. + // /// At this point the mixnet contract automatically transitions the state to `AdvancingEpoch`. + // /// 6. it obtains the list of all nodes on the network and pseudorandomly (but weighted by total stake) + // /// determines the new rewarded set. It then assigns roles to the provided nodes taking + // /// family information into consideration. Finally, it sends `AssignRole` message + // /// containing the role assignment information thus (after each role has been assigned) + // /// rolling over the epoch and changing the state to `InProgress`. + // /// 7. it purges old (older than 48h) measurement data + // /// 8. the whole process repeats once the new epoch finishes async fn perform_epoch_operations(&mut self, interval: Interval) -> Result<(), RewardingError> { - let mut rewards = self.nodes_to_reward(interval).await; - rewards.sort_by_key(|a| a.mix_id); + let mut rewards = self.nodes_to_reward(interval).await?; + rewards.sort_by_key(|a| a.node_id); - log::info!("The current epoch has finished."); - log::info!( + info!("The current epoch has finished."); + info!( "Interval id: {}, epoch id: {} (absolute epoch id: {})", interval.current_interval_id(), interval.current_epoch_id(), interval.current_epoch_absolute_id() ); - log::info!( + info!( "The current epoch has lasted from {} until {}", interval.current_epoch_start(), interval.current_epoch_end() ); - log::info!("Performing all epoch operations..."); + info!("Performing all epoch operations..."); let epoch_end = interval.current_epoch_end(); - let all_mixnodes = self.nym_contract_cache.mixnodes_filtered().await; - if all_mixnodes.is_empty() { - // that's a bit weird, but - log::warn!("there don't seem to be any mixnodes on the network!") + let legacy_mixnodes = self.nym_contract_cache.legacy_mixnodes_filtered().await; + let legacy_gateways = self.nym_contract_cache.legacy_gateways_filtered().await; + + // TODO: for the purposes of rewarding, this might have to grab some pre-filtered nodes instead, + // such as ones that use up to date version or have correct 'peanut' score + let nym_nodes = self.nym_contract_cache.nym_nodes().await; + + if legacy_mixnodes.is_empty() && legacy_gateways.is_empty() && nym_nodes.is_empty() { + // that's a bit weird, but ok + warn!("there don't seem to be any nodes on the network!") } let epoch_status = self.nyxd_client.get_current_epoch_status().await?; @@ -133,23 +148,33 @@ impl RewardedSetUpdater { } // Reward all the nodes in the still current, soon to be previous rewarded set - log::info!("Rewarding the current rewarded set..."); - self.reward_current_rewarded_set(&rewards, interval).await?; + info!("Rewarding the current rewarded set..."); + self.reward_current_rewarded_set(rewards, interval).await?; // note: those operations don't really have to be atomic, so it's fine to send them // as separate transactions self.reconcile_epoch_events().await?; - self.update_rewarded_set_and_advance_epoch(interval, &all_mixnodes) - .await?; - - log::info!("Purging old node statuses from the storage..."); + self.update_rewarded_set_and_advance_epoch( + interval, + &legacy_mixnodes, + &legacy_gateways, + &nym_nodes, + ) + .await?; + + info!("Purging old node statuses from the storage..."); let cutoff = (epoch_end - 2 * ONE_DAY).unix_timestamp(); self.storage.purge_old_statuses(cutoff).await?; Ok(()) } - async fn update_blacklist(&mut self, interval: &Interval) -> Result<(), RewardingError> { + // this purposely does not deal with nym-nodes as they don't have a concept of a blacklist. + // instead clients are meant to be filtering out them themselves based on the provided scores. + async fn update_legacy_node_blacklist( + &mut self, + interval: &Interval, + ) -> Result<(), RewardingError> { info!("Updating blacklists"); let mut mix_blacklist_add = HashSet::new(); @@ -183,15 +208,16 @@ impl RewardedSetUpdater { for gateway in gateways { if gateway.value() <= 50.0 { - gate_blacklist_add.insert(gateway.identity().to_string()); + gate_blacklist_add.insert(gateway.node_id()); } else { - gate_blacklist_remove.insert(gateway.identity().to_string()); + gate_blacklist_remove.insert(gateway.node_id()); } } self.nym_contract_cache .update_gateways_blacklist(gate_blacklist_add, gate_blacklist_remove) .await; + Ok(()) } @@ -219,7 +245,7 @@ impl RewardedSetUpdater { return Some(current_interval.interval); } else { let time_left = current_interval.time_until_current_epoch_end(); - log::info!( + info!( "Waiting for epoch change, it should take approximately {}s", time_left.as_secs() ); @@ -244,15 +270,19 @@ impl RewardedSetUpdater { } pub(crate) async fn run(&mut self, mut shutdown: TaskClient) -> Result<(), RewardingError> { + info!("waiting for initial contract cache values before we can start rewarding"); self.nym_contract_cache.wait_for_initial_values().await; + info!("waiting for initial self-described cache values before we can start rewarding"); + self.described_cache.naive_wait_for_initial_values().await; + while !shutdown.is_shutdown() { let interval_details = match self.wait_until_epoch_end(&mut shutdown).await { // received a shutdown None => return Ok(()), Some(interval) => interval, }; - if let Err(err) = self.update_blacklist(&interval_details).await { + if let Err(err) = self.update_legacy_node_blacklist(&interval_details).await { error!("failed to update the node blacklist - {err}"); continue; } @@ -268,11 +298,16 @@ impl RewardedSetUpdater { pub(crate) fn start( nyxd_client: Client, nym_contract_cache: &NymContractCache, - storage: NymApiStorage, + described_cache: SharedCache, + storage: &NymApiStorage, shutdown: &TaskManager, ) { - let mut rewarded_set_updater = - RewardedSetUpdater::new(nyxd_client, nym_contract_cache.to_owned(), storage); + let mut rewarded_set_updater = EpochAdvancer::new( + nyxd_client, + nym_contract_cache.to_owned(), + described_cache, + storage.to_owned(), + ); let shutdown_listener = shutdown.subscribe(); tokio::spawn(async move { rewarded_set_updater.run(shutdown_listener).await }); } diff --git a/nym-api/src/epoch_operations/rewarded_set_assignment.rs b/nym-api/src/epoch_operations/rewarded_set_assignment.rs index 08a794768c..d92cd1d7f5 100644 --- a/nym-api/src/epoch_operations/rewarded_set_assignment.rs +++ b/nym-api/src/epoch_operations/rewarded_set_assignment.rs @@ -3,26 +3,38 @@ use crate::epoch_operations::error::RewardingError; use crate::epoch_operations::helpers::stake_to_f64; -use crate::RewardedSetUpdater; +use crate::EpochAdvancer; use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::{ - EpochState, IdentityKey, Interval, Layer, LayerAssignment, MixId, MixNodeDetails, -}; +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; +use nym_mixnet_contract_common::helpers::IntoBaseDecimal; +use nym_mixnet_contract_common::reward_params::{Performance, RewardedSetParams}; +use nym_mixnet_contract_common::{EpochState, Interval, NodeId, NymNodeDetails, RewardedSet}; use rand::prelude::SliceRandom; use rand::rngs::OsRng; -use std::collections::HashMap; +use std::collections::HashSet; +use tracing::{debug, error, info, warn}; + +#[derive(Debug, Clone, PartialEq)] +enum AvailableRole { + // legacy mixnodes + nym-nodes in mixing mode + Mix, + + // legacy gateways + nym-nodes in entry or exit mode + EntryGateway, + + // nym-nodes in exit mode + ExitGateway, +} #[derive(Debug, Clone)] -struct MixnodeWithStakeAndPerformance { - mix_id: MixId, - identity: IdentityKey, +struct NodeWithStakeAndPerformance { + node_id: NodeId, + available_roles: Vec, total_stake: Decimal, performance: Performance, } -impl MixnodeWithStakeAndPerformance { +impl NodeWithStakeAndPerformance { fn to_selection_weight(&self) -> f64 { let scaled_performance = match self.performance.checked_pow(20) { Ok(perf) => perf, @@ -35,150 +47,419 @@ impl MixnodeWithStakeAndPerformance { let scaled_stake = self.total_stake * scaled_performance; stake_to_f64(scaled_stake) } -} - -impl RewardedSetUpdater { - // Needs to run for active and reserve sets separatley, as it does not preserve order - async fn determine_layers( - &self, - set: &[MixnodeWithStakeAndPerformance], - ) -> Result, RewardingError> { - let mut assignments = Vec::with_capacity(set.len()); - let target_layer_count = set.len() / 3; - let mix_to_family = self.nym_contract_cache.mix_to_family().await.to_vec(); + fn can_operate_mixnode(&self) -> bool { + self.available_roles.contains(&AvailableRole::Mix) + } - let mix_to_family = mix_to_family - .into_iter() - .collect::>(); + fn can_operate_entry_gateway(&self) -> bool { + self.available_roles.contains(&AvailableRole::EntryGateway) + } - let mut regular_nodes = Vec::with_capacity(set.len()); + fn can_operate_exit_gateway(&self) -> bool { + self.available_roles.contains(&AvailableRole::ExitGateway) + } +} - let mut families = HashMap::new(); +struct IgnoredNodes { + typ: &'static str, + no_self_described: usize, + not_nym_node_binary: usize, + no_terms_and_conditions: usize, + use_vested_tokens: usize, +} - for node in set.iter() { - if let Some(fh) = mix_to_family.get(&node.identity) { - let family: &mut Vec = families.entry(fh.identity()).or_default(); - family.push(node.mix_id) - } else { - regular_nodes.push(node.mix_id) - } +impl IgnoredNodes { + fn new(typ: &'static str) -> Self { + IgnoredNodes { + typ, + no_self_described: 0, + not_nym_node_binary: 0, + no_terms_and_conditions: 0, + use_vested_tokens: 0, } + } - let mut layers = HashMap::new(); - layers.insert(Layer::One, Vec::with_capacity(target_layer_count)); - layers.insert(Layer::Two, Vec::with_capacity(target_layer_count)); - layers.insert(Layer::Three, Vec::with_capacity(target_layer_count)); - - // Assign all members of a family to same layer - for (_head, members) in families.iter_mut() { - let smallest_layer = layers - .iter() - .min_by_key(|(_layer, members)| members.len()) - .map(|(layer, _members)| *layer) - .unwrap_or(Layer::One); + fn is_empty(&self) -> bool { + self.no_self_described == 0 + && self.not_nym_node_binary == 0 + && self.no_terms_and_conditions == 0 + && self.use_vested_tokens == 0 + } - let entry = layers.entry(smallest_layer).or_default(); - if entry.len() + members.len() <= target_layer_count { - entry.extend_from_slice(members) - } + fn maybe_log_summary(&self) { + if self.no_self_described != 0 { + warn!( + "{} {} don't expose their self-described API", + self.no_self_described, self.typ + ) } - // Assign nodes with no families into layers - for mix_id in regular_nodes.drain(..) { - let smallest_layer = layers - .iter() - .min_by_key(|(_layer, members)| members.len()) - .map(|(layer, _members)| *layer) - .unwrap_or(Layer::One); - - let entry = layers.entry(smallest_layer).or_default(); - if entry.len() < target_layer_count { - entry.push(mix_id) - } + if self.not_nym_node_binary != 0 { + warn!( + "{} {} are not running the 'nym-node' binary", + self.not_nym_node_binary, self.typ + ) } - - for (layer, members) in layers { - let layer_assignments = members - .into_iter() - .map(|mix_id| LayerAssignment::new(mix_id, layer)); - assignments.extend(layer_assignments); + if self.no_terms_and_conditions != 0 { + warn!( + "{} {} operators have not accepted the terms and conditions", + self.no_terms_and_conditions, self.typ + ) + } + if self.use_vested_tokens != 0 { + warn!( + "{} {} operators bonded using vested tokens", + self.use_vested_tokens, self.typ + ) } - Ok(assignments) } +} +impl EpochAdvancer { fn determine_rewarded_set( &self, - mixnodes: Vec, - nodes_to_select: u32, - ) -> Result, RewardingError> { - if mixnodes.is_empty() { - return Ok(Vec::new()); + nodes: Vec, + spec: RewardedSetParams, + ) -> Result { + if nodes.is_empty() { + warn!("there are no nodes for assignment!"); + return Ok(RewardedSet::default()); } let mut rng = OsRng; - // generate list of mixnodes and their relatively weight (by total stake) - let choices = mixnodes + // generate list of nodes and their relatively weight (by total stake scaled by performance) + let all_choices = nodes .into_iter() - .map(|mix| { - let weight = mix.to_selection_weight(); - (mix, weight) + .map(|node| { + let weight = node.to_selection_weight(); + (node, weight) }) .collect::>(); - // the unwrap here is fine as an error can only be thrown under one of the following conditions: - // - our mixnode list is empty - we have already checked for that - // - we have invalid weights, i.e. less than zero or NaNs - it shouldn't happen in our case as we safely cast down from u128 - // - all weights are zero - it's impossible in our case as the list of nodes is not empty and weight is proportional to stake. You must have non-zero stake in order to bond - // - we have more than u32::MAX values (which is incredibly unrealistic to have 4B mixnodes bonded... literally every other person on the planet would need one) - Ok(choices - .choose_multiple_weighted(&mut rng, nodes_to_select as usize, |item| item.1)? - .map(|(mix, _weight)| mix.clone()) - .collect()) + // 1. determine entry gateways + let entry_eligible = all_choices + .iter() + .filter(|node| node.0.can_operate_entry_gateway()) + .collect::>(); + let entry_gateways = entry_eligible + .choose_multiple_weighted(&mut rng, spec.entry_gateways as usize, |item| item.1)? + .map(|node| node.0.node_id) + .collect::>(); + + // 2. determine exit gateways + let exit_eligible = all_choices + .iter() + .filter(|node| { + node.0.can_operate_exit_gateway() && !entry_gateways.contains(&node.0.node_id) + }) + .collect::>(); + let exit_gateways = exit_eligible + .choose_multiple_weighted(&mut rng, spec.exit_gateways as usize, |item| item.1)? + .map(|node| node.0.node_id) + .collect::>(); + + // 3. determine mixnodes + let mix_eligible = all_choices + .iter() + .filter(|node| { + node.0.can_operate_mixnode() + && !exit_gateways.contains(&node.0.node_id) + && !entry_gateways.contains(&node.0.node_id) + }) + .collect::>(); + let mixnodes = mix_eligible + .choose_multiple_weighted(&mut rng, spec.mixnodes as usize, |item| item.1)? + .map(|node| node.0.node_id) + .collect::>(); + + // 4. determine standby + let standby_eligible = all_choices + .iter() + .filter(|node| { + !exit_gateways.contains(&node.0.node_id) + && !entry_gateways.contains(&node.0.node_id) + && !mixnodes.contains(&node.0.node_id) + }) + .collect::>(); + let standby = standby_eligible + .choose_multiple_weighted(&mut rng, spec.standby as usize, |item| item.1)? + .map(|node| node.0.node_id) + .collect::>(); + + // 5. split mixnodes into the layers: just shuffle the selected nodes and select every 3rd into each layer + let mut mixnodes_vec = mixnodes.into_iter().collect::>(); + mixnodes_vec.shuffle(&mut rng); + + let mut layer1 = Vec::new(); + let mut layer2 = Vec::new(); + let mut layer3 = Vec::new(); + + for (i, mix) in mixnodes_vec.iter().enumerate() { + match i % 3 { + 0 => layer1.push(*mix), + 1 => layer2.push(*mix), + 2 => layer3.push(*mix), + n => panic!("we have broken maths! somehow {i} % 3 == {n}!"), + } + } + + if entry_gateways.len() != spec.entry_gateways as usize { + warn!( + "we didn't manage to select {} entry gateways. we only got {}", + spec.entry_gateways, + entry_gateways.len() + ) + } + + if exit_gateways.len() != spec.exit_gateways as usize { + warn!( + "we didn't manage to select {} exit gateways. we only got {}", + spec.exit_gateways, + exit_gateways.len() + ) + } + + if mixnodes_vec.len() != spec.mixnodes as usize { + warn!( + "we didn't manage to select {} mixnodes. we only got {}", + spec.mixnodes, + mixnodes_vec.len() + ) + } + + if standby.len() != spec.standby as usize { + warn!( + "we didn't manage to select {} standby nodes. we only got {}", + spec.standby, + standby.len() + ) + } + + let mut rewarded_set = RewardedSet { + entry_gateways: entry_gateways.into_iter().collect(), + exit_gateways: exit_gateways.into_iter().collect(), + layer1, + layer2, + layer3, + standby, + }; + + // make sure to sort the rewarded set values + rewarded_set.entry_gateways.sort(); + rewarded_set.exit_gateways.sort(); + rewarded_set.layer1.sort(); + rewarded_set.layer2.sort(); + rewarded_set.layer3.sort(); + rewarded_set.standby.sort(); + + Ok(rewarded_set) } - async fn attach_performance( + async fn attach_performance_to_eligible_nodes( &self, interval: Interval, - mixnodes: &[MixNodeDetails], - ) -> Vec { - let mut with_performance = Vec::with_capacity(mixnodes.len()); - for mix in mixnodes { - with_performance.push(MixnodeWithStakeAndPerformance { - mix_id: mix.mix_id(), - identity: mix.bond_information.identity().to_owned(), - total_stake: mix.total_stake(), - performance: self - .load_performance(&interval, mix.mix_id()) - .await - .performance, + legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], + legacy_gateways: &[LegacyGatewayBondWithId], + nym_nodes: &[NymNodeDetails], + ) -> Vec { + let mut with_performance = Vec::new(); + + // SAFETY: the cache MUST HAVE been initialised before now + let described_cache = self.described_cache.get().await.unwrap(); + + let mut legacy_mixnodes_info = IgnoredNodes::new("legacy mixnodes"); + let mut legacy_gateways_info = IgnoredNodes::new("legacy gateways"); + let mut nym_nodes_info = IgnoredNodes::new("nym nodes"); + + for mix in legacy_mixnodes { + let node_id = mix.mix_id(); + let total_stake = mix.total_stake(); + + let Some(self_described) = described_cache.get_description(&node_id) else { + legacy_mixnodes_info.no_self_described += 1; + continue; + }; + + if self_described.build_information.binary_name != "nym-node" { + legacy_mixnodes_info.not_nym_node_binary += 1; + continue; + } + + if !self_described + .auxiliary_details + .accepted_operator_terms_and_conditions + { + legacy_mixnodes_info.no_terms_and_conditions += 1; + continue; + } + + if mix.bond_information.proxy.is_some() { + legacy_mixnodes_info.use_vested_tokens += 1; + continue; + } + + let performance = self + .load_mixnode_performance(&interval, mix.mix_id()) + .await + .performance; + debug!( + "legacy mixnode {}: stake: {total_stake}, performance: {performance}", + mix.mix_id() + ); + + with_performance.push(NodeWithStakeAndPerformance { + node_id: mix.mix_id(), + available_roles: vec![AvailableRole::Mix], + total_stake, + performance, + }) + } + for gateway in legacy_gateways { + let node_id = gateway.node_id; + let total_stake = gateway + .bond + .pledge_amount + .amount + .into_base_decimal() + .unwrap_or_default(); + + let Some(self_described) = described_cache.get_description(&node_id) else { + legacy_gateways_info.no_self_described += 1; + continue; + }; + + if self_described.build_information.binary_name != "nym-node" { + legacy_gateways_info.not_nym_node_binary += 1; + continue; + } + + if !self_described + .auxiliary_details + .accepted_operator_terms_and_conditions + { + legacy_gateways_info.no_terms_and_conditions += 1; + continue; + } + + let performance = self + .load_gateway_performance(&interval, gateway.node_id) + .await + .performance; + debug!( + "legacy gateway {}: stake: {total_stake}, performance: {performance}", + gateway.node_id + ); + + with_performance.push(NodeWithStakeAndPerformance { + node_id: gateway.node_id, + available_roles: vec![AvailableRole::EntryGateway], + total_stake, + performance, + }) + } + + for nym_node in nym_nodes { + let node_id = nym_node.node_id(); + let total_stake = nym_node.total_stake(); + + let Some(self_described) = described_cache.get_description(&node_id) else { + nym_nodes_info.no_self_described += 1; + continue; + }; + + if self_described.build_information.binary_name != "nym-node" { + nym_nodes_info.not_nym_node_binary += 1; + continue; + } + + if !self_described + .auxiliary_details + .accepted_operator_terms_and_conditions + { + nym_nodes_info.no_terms_and_conditions += 1; + continue; + } + + let performance = self + .load_any_performance(&interval, nym_node.node_id()) + .await + .performance; + debug!("nym-node {node_id}: stake: {total_stake}, performance: {performance}",); + + let mut available_roles = Vec::new(); + if self_described.declared_role.mixnode { + available_roles.push(AvailableRole::Mix) + } + if self_described.declared_role.entry { + available_roles.push(AvailableRole::EntryGateway) + } + if self_described.declared_role.can_operate_exit_gateway() { + available_roles.push(AvailableRole::ExitGateway) + } + + if available_roles.is_empty() { + warn!("nym-node {node_id} can't operate under any mode!"); + continue; + } + + with_performance.push(NodeWithStakeAndPerformance { + node_id: nym_node.node_id(), + available_roles, + total_stake, + performance, }) } + + if !legacy_mixnodes_info.is_empty() + || !legacy_gateways_info.is_empty() + || !nym_nodes_info.is_empty() + { + warn!("not every bonded node is being considered for rewarded set selection") + } + + legacy_mixnodes_info.maybe_log_summary(); + legacy_gateways_info.maybe_log_summary(); + nym_nodes_info.maybe_log_summary(); + with_performance } pub(super) async fn update_rewarded_set_and_advance_epoch( &self, current_interval: Interval, - all_mixnodes: &[MixNodeDetails], + legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], + legacy_gateways: &[LegacyGatewayBondWithId], + nym_nodes: &[NymNodeDetails], ) -> Result<(), RewardingError> { let epoch_status = self.nyxd_client.get_current_epoch_status().await?; match epoch_status.state { - EpochState::AdvancingEpoch => { - log::info!("Advancing epoch and updating the rewarded set..."); + EpochState::RoleAssignment { next } => { + // with how the nym-api is currently coded, this should never happen as we're always + // assigning roles to ALL nodes at once, but who knows what we might decide to do in the future... + if !next.is_first() { + return Err(RewardingError::MidRoleAssignment { next }); + } + + info!("attempting to assign the rewarded set for the upcoming epoch..."); let nodes_with_performance = self - .attach_performance(current_interval, all_mixnodes) + .attach_performance_to_eligible_nodes( + current_interval, + legacy_mixnodes, + legacy_gateways, + nym_nodes, + ) .await; if let Err(err) = self ._update_rewarded_set_and_advance_epoch(nodes_with_performance) .await { - log::error!("FAILED to advance the current epoch... - {err}"); + error!("FAILED to assign the rewarded set... - {err}"); Err(err) } else { - log::info!("Advanced the epoch and updated the rewarded set... SUCCESS"); + info!("Advanced the epoch and updated the rewarded set... SUCCESS"); Ok(()) } } @@ -195,54 +476,21 @@ impl RewardedSetUpdater { async fn _update_rewarded_set_and_advance_epoch( &self, - all_mixnodes: Vec, + all_nodes: Vec, ) -> Result<(), RewardingError> { // we grab rewarding parameters here as they might have gotten updated when performing epoch actions let rewarding_parameters = self.nyxd_client.get_current_rewarding_parameters().await?; - debug!("Rewarding paremeters: {:?}", rewarding_parameters); + debug!("Rewarding parameters: {rewarding_parameters:?}"); let new_rewarded_set = - self.determine_rewarded_set(all_mixnodes, rewarding_parameters.rewarded_set_size)?; + self.determine_rewarded_set(all_nodes, rewarding_parameters.rewarded_set)?; debug!("New rewarded set: {:?}", new_rewarded_set); - let empty = vec![]; - - let (active_set, reserve_set) = if new_rewarded_set.len() - <= rewarding_parameters.active_set_size as usize - { - warn!("Active set size ({}) is greater then rewarded set len ({}), there will be no reserve set", rewarding_parameters.active_set_size, new_rewarded_set.len()); - (new_rewarded_set.as_slice(), empty.as_slice()) - } else { - new_rewarded_set.split_at(rewarding_parameters.active_set_size as usize) - }; - - let mut active_set_layer_assignments = self.determine_layers(active_set).await?; - debug!( - "Active set layer assignments: {:?}", - active_set_layer_assignments - ); - let reserve_set_layer_assignments = self.determine_layers(reserve_set).await?; - debug!( - "Reserve set layer assignments: {:?}", - reserve_set_layer_assignments - ); - - active_set_layer_assignments.extend(reserve_set_layer_assignments); - - debug!( - "Rewarded set layer assignments: {:?}", - active_set_layer_assignments - ); - self.nyxd_client - .advance_current_epoch( - active_set_layer_assignments, - rewarding_parameters.active_set_size, - ) + .send_role_assignment_messages(new_rewarded_set) .await?; - Ok(()) } } diff --git a/nym-api/src/epoch_operations/rewarding.rs b/nym-api/src/epoch_operations/rewarding.rs index 2f8ae6160b..af4d29db4a 100644 --- a/nym-api/src/epoch_operations/rewarding.rs +++ b/nym-api/src/epoch_operations/rewarding.rs @@ -2,14 +2,15 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::epoch_operations::error::RewardingError; -use crate::epoch_operations::helpers::MixnodeWithPerformance; -use crate::RewardedSetUpdater; -use nym_mixnet_contract_common::{EpochState, Interval, MixId}; +use crate::epoch_operations::helpers::RewardedNodeWithParams; +use crate::EpochAdvancer; +use nym_mixnet_contract_common::{EpochState, Interval}; +use tracing::{error, info, warn}; -impl RewardedSetUpdater { +impl EpochAdvancer { pub(super) async fn reward_current_rewarded_set( &self, - to_reward: &[MixnodeWithPerformance], + to_reward: Vec, current_interval: Interval, ) -> Result<(), RewardingError> { let epoch_status = self.nyxd_client.get_current_epoch_status().await?; @@ -22,27 +23,27 @@ impl RewardedSetUpdater { operation: "mix rewarding".to_string(), }) } - EpochState::ReconcilingEvents | EpochState::AdvancingEpoch => { - warn!("we seem to have crashed mid epoch operations... no need to reward mixnodes as we've already done that! (or this could be a false positive if there were no nodes to reward - to fix this warning later)"); + EpochState::ReconcilingEvents | EpochState::RoleAssignment { .. } => { + warn!("we seem to have crashed mid epoch operations... no need to reward nodes as we've already done that! (or this could be a false positive if there were no nodes to reward - to fix this warning later)"); Ok(()) } EpochState::Rewarding { last_rewarded, .. } => { - log::info!("Rewarding the current rewarded set..."); + info!("Rewarding the current rewarded set..."); // with how the nym-api is currently coded, this should never happen as we're always - // rewarding ALL mixnodes at once, but who knows what we might decide to do in the future... + // rewarding ALL nodes at once, but who knows what we might decide to do in the future... if last_rewarded != 0 { - return Err(RewardingError::MidMixRewarding { last_rewarded }); + return Err(RewardingError::MidNodeRewarding { last_rewarded }); } if let Err(err) = self ._reward_current_rewarded_set(to_reward, current_interval) .await { - log::error!("FAILED to reward rewarded set - {err}"); + error!("FAILED to reward rewarded set: {err}"); Err(err) } else { - log::info!("Rewarded current rewarded set... SUCCESS"); + info!("Rewarded current rewarded set... SUCCESS"); Ok(()) } } @@ -51,41 +52,56 @@ impl RewardedSetUpdater { async fn _reward_current_rewarded_set( &self, - to_reward: &[MixnodeWithPerformance], + to_reward: Vec, current_interval: Interval, ) -> Result<(), RewardingError> { if to_reward.is_empty() { error!("There are no nodes to reward in this epoch - we shouldn't have been in the 'Rewarding' state!"); - } else if let Err(err) = self.nyxd_client.send_rewarding_messages(to_reward).await { + } else if let Err(err) = self.nyxd_client.send_rewarding_messages(&to_reward).await { error!( - "failed to perform mixnode rewarding for epoch {}! Error encountered: {err}", + "failed to perform node rewarding for epoch {}! Error encountered: {err}", current_interval.current_epoch_absolute_id(), ); return Err(err.into()); } - log::info!("rewarded {} mixnodes...", to_reward.len()); + info!("rewarded {} nodes...", to_reward.len()); Ok(()) } - pub(crate) async fn nodes_to_reward(&self, interval: Interval) -> Vec { - // try to get current up to date view of the network bypassing the cache + pub(crate) async fn nodes_to_reward( + &self, + interval: Interval, + ) -> Result, RewardingError> { + // try to get current up-to-date view of the network bypassing the cache // in case the epochs were significantly shortened for the purposes of testing - let rewarded_set: Vec = match self.nyxd_client.get_rewarded_set_mixnodes().await { - Ok(nodes) => nodes.into_iter().map(|(id, _)| id).collect::>(), + let rewarded_set = match self.nyxd_client.get_rewarded_set_nodes().await { + Ok(rewarded_set) => rewarded_set, Err(err) => { - warn!("failed to obtain the current rewarded set - {err}. falling back to the cached version"); + warn!("failed to obtain the current rewarded set: {err}. falling back to the cached version"); self.nym_contract_cache - .rewarded_set() + .rewarded_set_owned() .await .into_inner() - .into_iter() - .map(|node| node.mix_id()) - .collect::>() + .into() } }; - self.load_nodes_performance(&interval, &rewarded_set).await + // we only need reward parameters for active set work factor and rewarded/active set sizes; + // we do not need exact values of reward pool, staking supply, etc., so it's fine if it's slightly out of sync + let Some(reward_params) = self + .nym_contract_cache + .interval_reward_params() + .await + .into_inner() + else { + error!("failed to obtain the current interval rewarding parameters. can't determine rewards without them"); + return Err(RewardingError::RewardingParamsRetrievalFailure); + }; + + Ok(self + .load_nodes_for_rewarding(&interval, &rewarded_set, reward_params) + .await) } } diff --git a/nym-api/src/epoch_operations/transition_beginning.rs b/nym-api/src/epoch_operations/transition_beginning.rs index f6e8f87b6a..b57779dfca 100644 --- a/nym-api/src/epoch_operations/transition_beginning.rs +++ b/nym-api/src/epoch_operations/transition_beginning.rs @@ -2,9 +2,10 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::epoch_operations::error::RewardingError; -use crate::epoch_operations::RewardedSetUpdater; +use crate::epoch_operations::EpochAdvancer; +use tracing::{error, info}; -impl RewardedSetUpdater { +impl EpochAdvancer { // returns boolean indicating whether we should bother continuing pub(super) async fn begin_epoch_transition(&self) -> Result { info!("starting the epoch transition..."); @@ -13,14 +14,14 @@ impl RewardedSetUpdater { // wasn't faster than us let epoch_status = self.nyxd_client.get_current_epoch_status().await?; if !epoch_status.is_in_progress() { - log::error!("FAILED to begin epoch progression: {err}"); + error!("FAILED to begin epoch progression: {err}"); Err(err) } else { error!("another nym-api ({}) is already advancing the epoch... but we shouldn't have other nym-apis yet!", epoch_status.being_advanced_by); Ok(false) } } else { - log::info!("Begun epoch transition... SUCCESS"); + info!("Begun epoch transition... SUCCESS"); Ok(true) } } diff --git a/nym-api/src/main.rs b/nym-api/src/main.rs index 3121ce18f2..4ee4205ea2 100644 --- a/nym-api/src/main.rs +++ b/nym-api/src/main.rs @@ -1,36 +1,19 @@ // Copyright 2020-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -// TODO rocket remove -#![allow(deprecated)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] -#[macro_use] -extern crate rocket; - -use crate::ecash::dkg::controller::keys::{ - can_validate_coconut_keys, load_bte_keypair, load_ecash_keypair_if_exists, -}; -use crate::epoch_operations::RewardedSetUpdater; -use crate::network::models::NetworkDetails; -use crate::node_describe_cache::DescribedNodes; -use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; -use crate::support::caching::cache::SharedCache; +use crate::epoch_operations::EpochAdvancer; use crate::support::cli; -use crate::support::config::Config; use crate::support::storage; -use crate::support::storage::NymApiStorage; use ::nym_config::defaults::setup_env; -use circulating_supply_api::cache::CirculatingSupplyCache; use clap::Parser; -use ecash::dkg::controller::DkgController; use node_status_api::NodeStatusCache; -use nym_bin_common::logging::setup_logging; -use nym_config::defaults::NymNetworkDetails; +use nym_bin_common::logging::setup_tracing_logger; use nym_contract_cache::cache::NymContractCache; -use nym_sphinx::receiver::SphinxMessageReceiver; -use nym_task::TaskManager; -use rand::rngs::OsRng; -use support::{http, nyxd}; +use support::nyxd; +use tracing::{info, trace}; mod circulating_supply_api; mod ecash; @@ -43,14 +26,7 @@ pub(crate) mod nym_contract_cache; pub(crate) mod nym_nodes; mod status; pub(crate) mod support; - -#[cfg(feature = "axum")] -mod v2; - -struct ShutdownHandles { - task_manager_handle: TaskManager, - rocket_handle: rocket::Shutdown, -} +mod v3_migration; // TODO rocket: remove all such Todos once rocket is phased out completely #[tokio::main] @@ -60,143 +36,13 @@ async fn main() -> Result<(), anyhow::Error> { console_subscriber::init(); }} - setup_logging(); - // TODO rocket: replace with tracing logger once rocket is eliminated from code + setup_tracing_logger(); info!("Starting nym api..."); let args = cli::Cli::parse(); - trace!("{:#?}", args); + trace!("args: {:#?}", args); setup_env(args.config_env_file.as_ref()); args.execute().await } - -async fn start_nym_api_tasks(config: Config) -> anyhow::Result { - let nyxd_client = nyxd::Client::new(&config); - let connected_nyxd = config.get_nyxd_url(); - let nym_network_details = NymNetworkDetails::new_from_env(); - let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details); - - let coconut_keypair_wrapper = ecash::keys::KeyPair::new(); - - // if the keypair doesnt exist (because say this API is running in the caching mode), nothing will happen - if let Some(loaded_keys) = load_ecash_keypair_if_exists(&config.coconut_signer)? { - let issued_for = loaded_keys.issued_for_epoch; - coconut_keypair_wrapper.set(loaded_keys).await; - - if can_validate_coconut_keys(&nyxd_client, issued_for).await? { - coconut_keypair_wrapper.validate() - } - } - - let identity_keypair = config.base.storage_paths.load_identity()?; - let identity_public_key = *identity_keypair.public_key(); - - // let's build our rocket! - let rocket = http::setup_rocket( - &config, - network_details, - nyxd_client.clone(), - identity_keypair, - coconut_keypair_wrapper.clone(), - ) - .await?; - - // setup shutdowns - let shutdown = TaskManager::new(10); - - // Rocket handles shutdown on its own, but its shutdown handling should be incorporated - // with that of the rest of the tasks. Currently its runtime is forcefully terminated once - // nym-api exits. - let rocket_shutdown_handle = rocket.shutdown(); - - // get references to the managed state - let nym_contract_cache_state = rocket.state::().unwrap(); - let node_status_cache_state = rocket.state::().unwrap(); - let circulating_supply_cache_state = rocket.state::().unwrap(); - let storage = if let Some(storage) = rocket.state::() { - storage.to_owned() - } else { - storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await? - }; - let described_nodes_state = rocket.state::>().unwrap(); - - // start note describe cache refresher - // we should be doing the below, but can't due to our current startup structure - // let refresher = node_describe_cache::new_refresher(&config.topology_cacher); - // let cache = refresher.get_shared_cache(); - node_describe_cache::new_refresher_with_initial_value( - &config.topology_cacher, - nym_contract_cache_state.clone(), - described_nodes_state.to_owned(), - ) - .named("node-self-described-data-refresher") - .start(shutdown.subscribe_named("node-self-described-data-refresher")); - - // start all the caches first - let nym_contract_cache_listener = nym_contract_cache::start_refresher( - &config.node_status_api, - nym_contract_cache_state, - nyxd_client.clone(), - &shutdown, - ); - - node_status_api::start_cache_refresh( - &config.node_status_api, - nym_contract_cache_state, - node_status_cache_state, - storage.to_owned(), - nym_contract_cache_listener, - &shutdown, - ); - circulating_supply_api::start_cache_refresh( - &config.circulating_supply_cacher, - nyxd_client.clone(), - circulating_supply_cache_state, - &shutdown, - ); - - // start dkg task - if config.coconut_signer.enabled { - let dkg_bte_keypair = load_bte_keypair(&config.coconut_signer)?; - - DkgController::start( - &config.coconut_signer, - nyxd_client.clone(), - coconut_keypair_wrapper, - dkg_bte_keypair, - identity_public_key, - OsRng, - &shutdown, - )?; - } - - // and then only start the uptime updater (and the monitor itself, duh) - // if the monitoring if it's enabled - if config.network_monitor.enabled { - network_monitor::start::( - &config.network_monitor, - nym_contract_cache_state, - &storage, - nyxd_client.clone(), - &shutdown, - ) - .await; - - HistoricalUptimeUpdater::start(storage.to_owned(), &shutdown); - - // start 'rewarding' if its enabled - if config.rewarding.enabled { - epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; - RewardedSetUpdater::start(nyxd_client, nym_contract_cache_state, storage, &shutdown); - } - } - // Launch the rocket, serve http endpoints and finish the startup - tokio::spawn(rocket.launch()); - - Ok(ShutdownHandles { - task_manager_handle: shutdown, - rocket_handle: rocket_shutdown_handle, - }) -} diff --git a/nym-api/src/network/handlers.rs b/nym-api/src/network/handlers.rs index 0cb27ca92d..7bb77bc3fc 100644 --- a/nym-api/src/network/handlers.rs +++ b/nym-api/src/network/handlers.rs @@ -2,13 +2,13 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::network::models::{ContractInformation, NetworkDetails}; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::{extract, Router}; use nym_contracts_common::ContractBuildInformation; use std::collections::HashMap; use utoipa::ToSchema; -pub(crate) fn nym_network_routes() -> Router { +pub(crate) fn nym_network_routes() -> Router { Router::new() .route("/details", axum::routing::get(network_details)) .route("/nym-contracts", axum::routing::get(nym_contracts)) @@ -27,7 +27,7 @@ pub(crate) fn nym_network_routes() -> Router { ) )] async fn network_details( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> axum::Json { state.network_details().to_owned().into() } @@ -56,7 +56,7 @@ pub(crate) struct ContractVersionSchemaResponse { ) )] async fn nym_contracts( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> axum::Json>> { let info = state.nym_contract_cache().contract_details().await; info.iter() @@ -82,7 +82,7 @@ async fn nym_contracts( ) )] async fn nym_contracts_detailed( - extract::State(state): extract::State, + extract::State(state): extract::State, ) -> axum::Json>> { let info = state.nym_contract_cache().contract_details().await; info.iter() diff --git a/nym-api/src/network/mod.rs b/nym-api/src/network/mod.rs index 1f91dea3fd..016fc441b5 100644 --- a/nym-api/src/network/mod.rs +++ b/nym-api/src/network/mod.rs @@ -1,20 +1,5 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; - -#[cfg(feature = "axum")] pub(crate) mod handlers; pub(crate) mod models; -mod routes; - -pub(crate) fn network_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: routes::network_details, - routes::nym_contracts, - routes::nym_contracts_detailed - ] -} diff --git a/nym-api/src/network/models.rs b/nym-api/src/network/models.rs index 7cdaa1d052..66d0324c26 100644 --- a/nym-api/src/network/models.rs +++ b/nym-api/src/network/models.rs @@ -5,8 +5,7 @@ use nym_config::defaults::NymNetworkDetails; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -#[derive(Clone, Serialize, Deserialize, JsonSchema)] -#[cfg_attr(feature = "axum", derive(utoipa::ToSchema))] +#[derive(Clone, Serialize, Deserialize, JsonSchema, utoipa::ToSchema)] pub struct NetworkDetails { pub(crate) connected_nyxd: String, pub(crate) network: NymNetworkDetails, @@ -21,8 +20,7 @@ impl NetworkDetails { } } -#[cfg_attr(feature = "axum", derive(utoipa::ToSchema))] -#[derive(Serialize, Deserialize, Clone, JsonSchema)] +#[derive(Clone, Serialize, Deserialize, JsonSchema, utoipa::ToSchema)] #[serde(rename_all = "snake_case")] pub struct ContractInformation { pub(crate) address: Option, diff --git a/nym-api/src/network/routes.rs b/nym-api/src/network/routes.rs deleted file mode 100644 index 73625f8b88..0000000000 --- a/nym-api/src/network/routes.rs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::network::models::{ContractInformation, NetworkDetails}; -use crate::nym_contract_cache::cache::NymContractCache; -use nym_contracts_common::ContractBuildInformation; -use rocket::serde::json::Json; -use rocket::State; -use rocket_okapi::openapi; -use std::collections::HashMap; -use std::ops::Deref; - -#[openapi(tag = "network")] -#[get("/details")] -pub(crate) fn network_details(details: &State) -> Json { - Json(details.deref().clone()) -} - -// I agree, it feels weird to be pulling contract cache here, but I feel like it makes -// more sense to return this information here rather than in the generic cache route -#[openapi(tag = "network")] -#[get("/nym-contracts")] -pub(crate) async fn nym_contracts( - cache: &State, -) -> Json>> { - let info = cache.contract_details().await; - Json( - info.iter() - .map(|(contract, info)| { - ( - contract.to_owned(), - ContractInformation { - address: info.address.as_ref().map(|a| a.to_string()), - details: info.base.clone(), - }, - ) - }) - .collect(), - ) -} - -#[openapi(tag = "network")] -#[get("/nym-contracts-detailed")] -pub(crate) async fn nym_contracts_detailed( - cache: &State, -) -> Json>> { - let info = cache.contract_details().await; - Json( - info.iter() - .map(|(contract, info)| { - ( - contract.to_owned(), - ContractInformation { - address: info.address.as_ref().map(|a| a.to_string()), - details: info.detailed.clone(), - }, - ) - }) - .collect(), - ) -} diff --git a/nym-api/src/network_monitor/mod.rs b/nym-api/src/network_monitor/mod.rs index cc308a627f..699498b287 100644 --- a/nym-api/src/network_monitor/mod.rs +++ b/nym-api/src/network_monitor/mod.rs @@ -11,8 +11,10 @@ use crate::network_monitor::monitor::receiver::{ use crate::network_monitor::monitor::sender::PacketSender; use crate::network_monitor::monitor::summary_producer::SummaryProducer; use crate::network_monitor::monitor::Monitor; +use crate::node_describe_cache::DescribedNodes; use crate::nym_contract_cache::cache::NymContractCache; use crate::storage::NymApiStorage; +use crate::support::caching::cache::SharedCache; use crate::support::{config, nyxd}; use futures::channel::mpsc; use nym_bandwidth_controller::BandwidthController; @@ -23,6 +25,7 @@ use nym_sphinx::params::PacketType; use nym_sphinx::receiver::MessageReceiver; use nym_task::TaskManager; use std::sync::Arc; +use tracing::info; pub(crate) mod gateways_reader; pub(crate) mod monitor; @@ -33,7 +36,8 @@ pub(crate) const ROUTE_TESTING_TEST_NONCE: u64 = 0; pub(crate) fn setup<'a>( config: &'a config::NetworkMonitor, - nym_contract_cache_state: &NymContractCache, + nym_contract_cache: &NymContractCache, + described_cache: SharedCache, storage: &NymApiStorage, nyxd_client: nyxd::Client, ) -> NetworkMonitorBuilder<'a> { @@ -41,7 +45,8 @@ pub(crate) fn setup<'a>( config, nyxd_client, storage.to_owned(), - nym_contract_cache_state.to_owned(), + nym_contract_cache.clone(), + described_cache, ) } @@ -49,7 +54,8 @@ pub(crate) struct NetworkMonitorBuilder<'a> { config: &'a config::NetworkMonitor, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, - validator_cache: NymContractCache, + contract_cache: NymContractCache, + described_cache: SharedCache, } impl<'a> NetworkMonitorBuilder<'a> { @@ -57,13 +63,15 @@ impl<'a> NetworkMonitorBuilder<'a> { config: &'a config::NetworkMonitor, nyxd_client: nyxd::Client, node_status_storage: NymApiStorage, - validator_cache: NymContractCache, + contract_cache: NymContractCache, + described_cache: SharedCache, ) -> Self { NetworkMonitorBuilder { config, nyxd_client, node_status_storage, - validator_cache, + contract_cache, + described_cache, } } @@ -84,7 +92,8 @@ impl<'a> NetworkMonitorBuilder<'a> { mpsc::unbounded(); let packet_preparer = new_packet_preparer( - self.validator_cache, + self.contract_cache, + self.described_cache, self.config.debug.per_node_test_packets, Arc::clone(&ack_key), *identity_keypair.public_key(), @@ -158,14 +167,16 @@ impl NetworkMonitorRunnables { } fn new_packet_preparer( - validator_cache: NymContractCache, + contract_cache: NymContractCache, + described_cache: SharedCache, per_node_test_packets: usize, ack_key: Arc, self_public_identity: identity::PublicKey, self_public_encryption: encryption::PublicKey, ) -> PacketPreparer { PacketPreparer::new( - validator_cache, + contract_cache, + described_cache, per_node_test_packets, ack_key, self_public_identity, @@ -218,12 +229,19 @@ fn new_packet_receiver( // TODO: 2) how do we make it non-async as other 'start' methods? pub(crate) async fn start( config: &config::NetworkMonitor, - nym_contract_cache_state: &NymContractCache, + nym_contract_cache: &NymContractCache, + described_cache: SharedCache, storage: &NymApiStorage, nyxd_client: nyxd::Client, shutdown: &TaskManager, ) { - let monitor_builder = setup(config, nym_contract_cache_state, storage, nyxd_client); + let monitor_builder = setup( + config, + nym_contract_cache, + described_cache, + storage, + nyxd_client, + ); info!("Starting network monitor..."); let runnables: NetworkMonitorRunnables = monitor_builder.build().await; runnables.spawn_tasks(shutdown); diff --git a/nym-api/src/network_monitor/monitor/gateways_pinger.rs b/nym-api/src/network_monitor/monitor/gateways_pinger.rs index a114088024..ed09c2d090 100644 --- a/nym-api/src/network_monitor/monitor/gateways_pinger.rs +++ b/nym-api/src/network_monitor/monitor/gateways_pinger.rs @@ -3,12 +3,12 @@ use crate::network_monitor::monitor::gateway_clients_cache::ActiveGatewayClients; use crate::network_monitor::monitor::receiver::{GatewayClientUpdate, GatewayClientUpdateSender}; -use log::{debug, info, trace, warn}; use nym_crypto::asymmetric::identity; use nym_crypto::asymmetric::identity::PUBLIC_KEY_LENGTH; use nym_task::TaskClient; use std::time::Duration; use tokio::time::{sleep, Instant}; +use tracing::{debug, info, trace, warn}; // TODO: should it perhaps be moved to config along other timeout values? const PING_TIMEOUT: Duration = Duration::from_secs(3); diff --git a/nym-api/src/network_monitor/monitor/mod.rs b/nym-api/src/network_monitor/monitor/mod.rs index e4c35d8f4f..4d24cb0504 100644 --- a/nym-api/src/network_monitor/monitor/mod.rs +++ b/nym-api/src/network_monitor/monitor/mod.rs @@ -9,13 +9,13 @@ use crate::network_monitor::test_packet::NodeTestMessage; use crate::network_monitor::test_route::TestRoute; use crate::storage::NymApiStorage; use crate::support::config; -use log::{debug, error, info}; +use nym_mixnet_contract_common::NodeId; use nym_sphinx::params::PacketType; use nym_sphinx::receiver::MessageReceiver; use nym_task::TaskClient; use std::collections::{HashMap, HashSet}; -use std::process; use tokio::time::{sleep, Duration, Instant}; +use tracing::{debug, error, info, trace}; pub(crate) mod gateway_clients_cache; pub(crate) mod gateways_pinger; @@ -94,10 +94,7 @@ impl Monitor { ) .await { - error!("Failed to submit monitor run information to the database - {err}",); - - // TODO: slightly more graceful shutdown here - process::exit(1); + error!("Failed to submit monitor run information to the database: {err}",); } } @@ -169,11 +166,11 @@ impl Monitor { .collect() } - fn blacklist_route_nodes(&self, route: &TestRoute, blacklist: &mut HashSet) { + fn blacklist_route_nodes(&self, route: &TestRoute, blacklist: &mut HashSet) { for mix in route.topology().mixes_as_vec() { - blacklist.insert(mix.identity_key.to_base58_string()); + blacklist.insert(mix.mix_id); } - blacklist.insert(route.gateway_identity().to_base58_string()); + blacklist.insert(route.gateway().node_id); } async fn prepare_test_routes(&mut self) -> Option> { @@ -273,8 +270,8 @@ impl Monitor { info!("Received {}/{} packets", total_received, total_sent); let summary = self.summary_producer.produce_summary( - prepared_packets.tested_mixnodes, - prepared_packets.tested_gateways, + prepared_packets.mixnodes_under_test, + prepared_packets.gateways_under_test, received, prepared_packets.invalid_mixnodes, prepared_packets.invalid_gateways, diff --git a/nym-api/src/network_monitor/monitor/preparer.rs b/nym-api/src/network_monitor/monitor/preparer.rs index 73a11b4d60..04fdceebef 100644 --- a/nym-api/src/network_monitor/monitor/preparer.rs +++ b/nym-api/src/network_monitor/monitor/preparer.rs @@ -3,23 +3,29 @@ use crate::network_monitor::monitor::sender::GatewayPackets; use crate::network_monitor::test_route::TestRoute; -use crate::nym_contract_cache::cache::NymContractCache; -use log::info; +use crate::node_describe_cache::{DescribedNodes, NodeDescriptionTopologyExt}; +use crate::nym_contract_cache::cache::{CachedRewardedSet, NymContractCache}; +use crate::support::caching::cache::SharedCache; +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer}; +use nym_api_requests::models::NymNodeDescription; use nym_crypto::asymmetric::{encryption, identity}; -use nym_mixnet_contract_common::{GatewayBond, Layer, MixNodeBond}; +use nym_mixnet_contract_common::{LegacyMixLayer, NodeId}; use nym_node_tester_utils::node::TestableNode; use nym_node_tester_utils::NodeTester; use nym_sphinx::acknowledgements::AckKey; use nym_sphinx::addressing::clients::Recipient; use nym_sphinx::forwarding::packet::MixPacket; use nym_sphinx::params::{PacketSize, PacketType}; +use nym_topology::gateway::GatewayConversionError; +use nym_topology::mix::MixnodeConversionError; use nym_topology::{gateway, mix}; -use rand::{rngs::ThreadRng, seq::SliceRandom, thread_rng, Rng}; +use rand::prelude::SliceRandom; +use rand::{rngs::ThreadRng, thread_rng, Rng}; use std::collections::{HashMap, HashSet}; - use std::fmt::{self, Display, Formatter}; use std::sync::Arc; use std::time::Duration; +use tracing::{debug, error, info, trace}; const DEFAULT_AVERAGE_PACKET_DELAY: Duration = Duration::from_millis(200); const DEFAULT_AVERAGE_ACK_DELAY: Duration = Duration::from_millis(200); @@ -53,10 +59,10 @@ pub(crate) struct PreparedPackets { pub(super) packets: Vec, /// Vector containing list of public keys and owners of all nodes mixnodes being tested. - pub(super) tested_mixnodes: Vec, + pub(super) mixnodes_under_test: Vec, /// Vector containing list of public keys and owners of all gateways being tested. - pub(super) tested_gateways: Vec, + pub(super) gateways_under_test: Vec, /// All mixnodes that failed to get parsed correctly or were not version compatible. /// They will be marked to the validator as being down for the test. @@ -69,7 +75,8 @@ pub(crate) struct PreparedPackets { #[derive(Clone)] pub(crate) struct PacketPreparer { - validator_cache: NymContractCache, + contract_cache: NymContractCache, + described_cache: SharedCache, /// Number of test packets sent to each node per_node_test_packets: usize, @@ -85,14 +92,16 @@ pub(crate) struct PacketPreparer { impl PacketPreparer { pub(crate) fn new( - validator_cache: NymContractCache, + contract_cache: NymContractCache, + described_cache: SharedCache, per_node_test_packets: usize, ack_key: Arc, self_public_identity: identity::PublicKey, self_public_encryption: encryption::PublicKey, ) -> Self { PacketPreparer { - validator_cache, + contract_cache, + described_cache, per_node_test_packets, ack_key, self_public_identity, @@ -112,6 +121,7 @@ impl PacketPreparer { test_route.topology().clone(), self_address, PacketSize::RegularPacket, + false, DEFAULT_AVERAGE_PACKET_DELAY, DEFAULT_AVERAGE_ACK_DELAY, self.ack_key.clone(), @@ -138,37 +148,42 @@ impl PacketPreparer { } pub(crate) async fn wait_for_validator_cache_initial_values(&self, minimum_full_routes: usize) { - // wait for the cache to get initialised - self.validator_cache.wait_for_initial_values().await; + // wait for the caches to get initialised + self.contract_cache.wait_for_initial_values().await; + self.described_cache.naive_wait_for_initial_values().await; + + let described_nodes = self + .described_cache + .get() + .await + .expect("the self-describe cache should have been initialised!"); // now wait for at least `minimum_full_routes` mixnodes per layer and `minimum_full_routes` gateway to be online info!("Waiting for minimal topology to be online"); let initialisation_backoff = Duration::from_secs(30); loop { - let gateways = self.validator_cache.gateways_all().await; - let mixnodes = self.validator_cache.mixnodes_all_basic().await; - - if gateways.len() < minimum_full_routes { - self.topology_wait_backoff(initialisation_backoff).await; - continue; - } - - let mut layer1_count = 0; - let mut layer2_count = 0; - let mut layer3_count = 0; - - for mix in mixnodes { - match mix.layer { - Layer::One => layer1_count += 1, - Layer::Two => layer2_count += 1, - Layer::Three => layer3_count += 1, + let gateways = self.contract_cache.legacy_gateways_all().await; + let mixnodes = self.contract_cache.legacy_mixnodes_all_basic().await; + let nym_nodes = self.contract_cache.nym_nodes().await; + + let mut gateways_count = gateways.len(); + let mut mixnodes_count = mixnodes.len(); + + for nym_node in nym_nodes { + if let Some(described) = described_nodes.get_description(&nym_node.node_id()) { + if described.declared_role.mixnode { + mixnodes_count += 1; + } else if described.declared_role.entry { + gateways_count += 1; + } } } - if layer1_count >= minimum_full_routes - && layer2_count >= minimum_full_routes - && layer3_count >= minimum_full_routes - { + debug!( + "we have {mixnodes_count} possible mixnodes and {gateways_count} possible gateways" + ); + + if gateways_count >= minimum_full_routes && mixnodes_count * 3 >= minimum_full_routes { break; } @@ -176,64 +191,184 @@ impl PacketPreparer { } } - async fn all_mixnodes_and_gateways(&self) -> (Vec, Vec) { + async fn all_legacy_mixnodes_and_gateways( + &self, + ) -> ( + Vec, + Vec, + ) { info!("Obtaining network topology..."); - let mixnodes = self.validator_cache.mixnodes_all_basic().await; - let gateways = self.validator_cache.gateways_all().await; + let mixnodes = self.contract_cache.legacy_mixnodes_all_basic().await; + let gateways = self.contract_cache.legacy_gateways_all().await; (mixnodes, gateways) } - async fn filtered_mixnodes_and_gateways(&self) -> (Vec, Vec) { + async fn filtered_legacy_mixnodes_and_gateways( + &self, + ) -> ( + Vec, + Vec, + ) { info!("Obtaining network topology..."); - let mixnodes = self.validator_cache.mixnodes_filtered_basic().await; - let gateways = self.validator_cache.gateways_filtered().await; + let mixnodes = self.contract_cache.legacy_mixnodes_filtered_basic().await; + let gateways = self.contract_cache.legacy_gateways_filtered().await; (mixnodes, gateways) } - pub(crate) fn try_parse_mix_bond(&self, mix: &MixNodeBond) -> Result { - let identity = mix.mix_node.identity_key.clone(); - mix.try_into().map_err(|_| identity) + pub(crate) fn try_parse_mix_bond( + &self, + bond: &LegacyMixNodeBondWithLayer, + ) -> Result { + fn parse_bond( + bond: &LegacyMixNodeBondWithLayer, + ) -> Result { + let host = mix::LegacyNode::parse_host(&bond.mix_node.host)?; + + // try to completely resolve the host in the mix situation to avoid doing it every + // single time we want to construct a path + let mix_host = mix::LegacyNode::extract_mix_host(&host, bond.mix_node.mix_port)?; + + Ok(mix::LegacyNode { + mix_id: bond.mix_id, + host, + mix_host, + identity_key: identity::PublicKey::from_base58_string(&bond.mix_node.identity_key)?, + sphinx_key: encryption::PublicKey::from_base58_string(&bond.mix_node.sphinx_key)?, + layer: bond.layer, + version: bond.mix_node.version.as_str().into(), + }) + } + + let identity = bond.mix_node.identity_key.clone(); + parse_bond(bond).map_err(|_| identity) } pub(crate) fn try_parse_gateway_bond( &self, - gateway: &GatewayBond, - ) -> Result { + gateway: &LegacyGatewayBondWithId, + ) -> Result { + fn parse_bond( + bond: &LegacyGatewayBondWithId, + ) -> Result { + let host = gateway::LegacyNode::parse_host(&bond.gateway.host)?; + + // try to completely resolve the host in the mix situation to avoid doing it every + // single time we want to construct a path + let mix_host = gateway::LegacyNode::extract_mix_host(&host, bond.gateway.mix_port)?; + + Ok(gateway::LegacyNode { + node_id: bond.node_id, + host, + mix_host, + clients_ws_port: bond.gateway.clients_port, + clients_wss_port: None, + identity_key: identity::PublicKey::from_base58_string(&bond.gateway.identity_key)?, + sphinx_key: encryption::PublicKey::from_base58_string(&bond.gateway.sphinx_key)?, + version: bond.gateway.version.as_str().into(), + }) + } + let identity = gateway.gateway.identity_key.clone(); - gateway.try_into().map_err(|_| identity) + parse_bond(gateway).map_err(|_| identity) + } + + fn layered_mixes<'a, R: Rng>( + &self, + rng: &mut R, + blacklist: &mut HashSet, + rewarded_set: &CachedRewardedSet, + legacy_mixnodes: Vec, + mixing_nym_nodes: impl Iterator + 'a, + ) -> HashMap> { + let mut layered_mixes = HashMap::new(); + for mix in legacy_mixnodes { + let layer = mix.layer; + let layer_mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); + let Ok(parsed_node) = self.try_parse_mix_bond(&mix) else { + blacklist.insert(mix.mix_id); + continue; + }; + layer_mixes.push(parsed_node) + } + + for mixing_nym_node in mixing_nym_nodes { + let Some(parsed_node) = self.nym_node_to_legacy_mix(rng, rewarded_set, mixing_nym_node) + else { + continue; + }; + let layer = parsed_node.layer; + let layer_mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); + layer_mixes.push(parsed_node) + } + + layered_mixes + } + + fn all_gateways<'a>( + &self, + blacklist: &mut HashSet, + legacy_gateways: Vec, + gateway_capable_nym_nodes: impl Iterator + 'a, + ) -> Vec { + let mut gateways = Vec::new(); + for gateway in legacy_gateways { + let Ok(parsed_node) = self.try_parse_gateway_bond(&gateway) else { + blacklist.insert(gateway.node_id); + continue; + }; + gateways.push(parsed_node) + } + + for gateway_capable_node in gateway_capable_nym_nodes { + let Some(parsed_node) = self.nym_node_to_legacy_gateway(gateway_capable_node) else { + continue; + }; + gateways.push(parsed_node) + } + + gateways } - // gets rewarded nodes // chooses n random nodes from each layer (and gateway) such that they are not on the blacklist - // if failed to parsed => onto the blacklist they go + // if failed to get parsed => onto the blacklist they go // if generated fewer than n, blacklist will be updated by external function with correctly generated // routes so that they wouldn't be reused pub(crate) async fn prepare_test_routes( &self, n: usize, - blacklist: &mut HashSet, + blacklist: &mut HashSet, ) -> Option> { - let (mixnodes, gateways) = self.filtered_mixnodes_and_gateways().await; + let (legacy_mixnodes, legacy_gateways) = self.filtered_legacy_mixnodes_and_gateways().await; + let rewarded_set = self.contract_cache.rewarded_set().await?; + + let descriptions = self.described_cache.get().await.ok()?; + + let mixing_nym_nodes = descriptions.mixing_nym_nodes(); + // last I checked `gatewaying` wasn't a word : ) + let gateway_capable_nym_nodes = descriptions.entry_capable_nym_nodes(); + + let mut rng = thread_rng(); + // separate mixes into layers for easier selection - let mut layered_mixes = HashMap::new(); - for mix in mixnodes { - let layer = mix.layer; - let mixes = layered_mixes.entry(layer).or_insert_with(Vec::new); - mixes.push(mix) - } + let layered_mixes = self.layered_mixes( + &mut rng, + blacklist, + &rewarded_set, + legacy_mixnodes, + mixing_nym_nodes, + ); + let gateways = self.all_gateways(blacklist, legacy_gateways, gateway_capable_nym_nodes); // get all nodes from each layer... - let l1 = layered_mixes.get(&Layer::One)?; - let l2 = layered_mixes.get(&Layer::Two)?; - let l3 = layered_mixes.get(&Layer::Three)?; + let l1 = layered_mixes.get(&LegacyMixLayer::One)?; + let l2 = layered_mixes.get(&LegacyMixLayer::Two)?; + let l3 = layered_mixes.get(&LegacyMixLayer::Three)?; // try to choose n nodes from each of them (+ gateways)... - let mut rng = thread_rng(); - let rand_l1 = l1.choose_multiple(&mut rng, n).collect::>(); let rand_l2 = l2.choose_multiple(&mut rng, n).collect::>(); let rand_l3 = l3.choose_multiple(&mut rng, n).collect::>(); @@ -258,36 +393,18 @@ impl PacketPreparer { trace!("Generating test routes..."); let mut routes = Vec::new(); for i in 0..most_available { - let Ok(node_1) = self.try_parse_mix_bond(rand_l1[i]) else { - blacklist.insert(rand_l1[i].identity().to_owned()); - continue; - }; - - let Ok(node_2) = self.try_parse_mix_bond(rand_l2[i]) else { - blacklist.insert(rand_l2[i].identity().to_owned()); - continue; - }; - - let Ok(node_3) = self.try_parse_mix_bond(rand_l3[i]) else { - blacklist.insert(rand_l3[i].identity().to_owned()); - continue; - }; - - let Ok(gateway) = self.try_parse_gateway_bond(rand_gateways[i]) else { - blacklist.insert(rand_gateways[i].identity().to_owned()); - continue; - }; + let node_1 = rand_l1[i].clone(); + let node_2 = rand_l2[i].clone(); + let node_3 = rand_l3[i].clone(); + let gateway = rand_gateways[i].clone(); routes.push(TestRoute::new(rng.gen(), node_1, node_2, node_3, gateway)) } - info!( - "The following routes will be used for testing: {:#?}", - routes - ); + info!("The following routes will be used for testing: {routes:#?}"); Some(routes) } - fn create_packet_sender(&self, gateway: &gateway::Node) -> Recipient { + fn create_packet_sender(&self, gateway: &gateway::LegacyNode) -> Recipient { Recipient::new( self.self_public_identity, self.self_public_encryption, @@ -325,20 +442,16 @@ impl PacketPreparer { fn filter_outdated_and_malformed_mixnodes( &self, - nodes: Vec, - ) -> (Vec, Vec) { + nodes: Vec, + ) -> (Vec, Vec) { let mut parsed_nodes = Vec::new(); let mut invalid_nodes = Vec::new(); for mixnode in nodes { - if let Ok(parsed_node) = (&mixnode).try_into() { + if let Ok(parsed_node) = self.try_parse_mix_bond(&mixnode) { parsed_nodes.push(parsed_node) } else { invalid_nodes.push(InvalidNode::Malformed { - node: TestableNode::new_mixnode( - mixnode.identity().to_owned(), - mixnode.owner.clone().into_string(), - mixnode.mix_id, - ), + node: TestableNode::new_mixnode(mixnode.identity().to_owned(), mixnode.mix_id), }); } } @@ -347,18 +460,18 @@ impl PacketPreparer { fn filter_outdated_and_malformed_gateways( &self, - nodes: Vec, - ) -> (Vec, Vec) { + nodes: Vec, + ) -> (Vec<(gateway::LegacyNode, NodeId)>, Vec) { let mut parsed_nodes = Vec::new(); let mut invalid_nodes = Vec::new(); for gateway in nodes { - if let Ok(parsed_node) = (&gateway).try_into() { - parsed_nodes.push(parsed_node) + if let Ok(parsed_node) = self.try_parse_gateway_bond(&gateway) { + parsed_nodes.push((parsed_node, gateway.node_id)) } else { invalid_nodes.push(InvalidNode::Malformed { node: TestableNode::new_gateway( - gateway.identity().to_owned(), - gateway.owner.clone().into_string(), + gateway.bond.identity().to_owned(), + gateway.node_id, ), }); } @@ -366,6 +479,43 @@ impl PacketPreparer { (parsed_nodes, invalid_nodes) } + fn nym_node_to_legacy_mix( + &self, + rng: &mut R, + rewarded_set: &CachedRewardedSet, + mixing_nym_node: &NymNodeDescription, + ) -> Option { + let maybe_explicit_layer = rewarded_set + .try_get_mix_layer(&mixing_nym_node.node_id) + .and_then(|layer| LegacyMixLayer::try_from(layer).ok()); + + let layer = match maybe_explicit_layer { + Some(layer) => layer, + None => { + let layer_choices = [ + LegacyMixLayer::One, + LegacyMixLayer::Two, + LegacyMixLayer::Three, + ]; + + // if nym-node doesn't have a layer assigned, since it's either standby or inactive, + // we have to choose one randomly for the testing purposes + // SAFETY: the slice is not empty so the unwrap is fine + #[allow(clippy::unwrap_used)] + layer_choices.choose(rng).copied().unwrap() + } + }; + + mixing_nym_node.try_to_topology_mix_node(layer).ok() + } + + fn nym_node_to_legacy_gateway( + &self, + gateway_capable_node: &NymNodeDescription, + ) -> Option { + gateway_capable_node.try_to_topology_gateway().ok() + } + pub(super) async fn prepare_test_packets( &mut self, test_nonce: u64, @@ -373,20 +523,52 @@ impl PacketPreparer { // TODO: Maybe do this _packet_type: PacketType, ) -> PreparedPackets { - // only test mixnodes that are rewarded, i.e. that will be rewarded in this interval. - // (remember that "idle" nodes are still part of that set) - // we don't care about other nodes, i.e. nodes that are bonded but will not get - // any reward during the current rewarding interval - let (mixnodes, gateways) = self.all_mixnodes_and_gateways().await; - - let (mixnodes, invalid_mixnodes) = self.filter_outdated_and_malformed_mixnodes(mixnodes); - let (gateways, invalid_gateways) = self.filter_outdated_and_malformed_gateways(gateways); + let (mixnodes, gateways) = self.all_legacy_mixnodes_and_gateways().await; + let rewarded_set = self.contract_cache.rewarded_set().await; + + let descriptions = self + .described_cache + .get() + .await + .expect("the cache must have been initialised!"); + let mixing_nym_nodes = descriptions.mixing_nym_nodes(); + let gateway_capable_nym_nodes = descriptions.entry_capable_nym_nodes(); + + let (mut mixnodes_to_test_details, invalid_mixnodes) = + self.filter_outdated_and_malformed_mixnodes(mixnodes); + let (mut gateways_to_test_details, invalid_gateways) = + self.filter_outdated_and_malformed_gateways(gateways); + + // summary of nodes that got tested + let mut mixnodes_under_test = mixnodes_to_test_details + .iter() + .map(|node| node.into()) + .collect::>(); + let mut gateways_under_test = gateways_to_test_details + .iter() + .map(|node| node.into()) + .collect::>(); + + // try to add nym-nodes into the fold + if let Some(rewarded_set) = rewarded_set { + let mut rng = thread_rng(); + for mix in mixing_nym_nodes { + if let Some(parsed) = self.nym_node_to_legacy_mix(&mut rng, &rewarded_set, mix) { + mixnodes_under_test.push(TestableNode::from(&parsed)); + mixnodes_to_test_details.push(parsed); + } + } + } - let tested_mixnodes = mixnodes.iter().map(|node| node.into()).collect::>(); - let tested_gateways = gateways.iter().map(|node| node.into()).collect::>(); + for gateway in gateway_capable_nym_nodes { + if let Some(parsed) = self.nym_node_to_legacy_gateway(gateway) { + gateways_under_test.push((&parsed, gateway.node_id).into()); + gateways_to_test_details.push((parsed, gateway.node_id)); + } + } let packets_to_create = (test_routes.len() * self.per_node_test_packets) - * (tested_mixnodes.len() + tested_gateways.len()); + * (mixnodes_under_test.len() + gateways_under_test.len()); info!("Need to create {} mix packets", packets_to_create); let mut all_gateway_packets = HashMap::new(); @@ -405,9 +587,10 @@ impl PacketPreparer { // 1. the topology is definitely valid (otherwise we wouldn't be here) // 2. the recipient is specified (by calling **mix**_tester) // 3. the test message is not too long, i.e. when serialized it will fit in a single sphinx packet + #[allow(clippy::unwrap_used)] let mixnode_test_packets = mix_tester .mixnodes_test_packets( - &mixnodes, + &mixnodes_to_test_details, route_ext, self.per_node_test_packets as u32, None, @@ -421,7 +604,7 @@ impl PacketPreparer { gateway_packets.push_packets(mix_packets); // and generate test packets for gateways (note the variable recipient) - for gateway in &gateways { + for (gateway, node_id) in &gateways_to_test_details { let recipient = self.create_packet_sender(gateway); let gateway_identity = gateway.identity_key; let gateway_address = gateway.clients_address(); @@ -430,9 +613,11 @@ impl PacketPreparer { // 1. the topology is definitely valid (otherwise we wouldn't be here) // 2. the recipient is specified // 3. the test message is not too long, i.e. when serialized it will fit in a single sphinx packet + #[allow(clippy::unwrap_used)] let gateway_test_packets = mix_tester - .gateway_test_packets( + .legacy_gateway_test_packets( gateway, + *node_id, route_ext, self.per_node_test_packets as u32, Some(recipient), @@ -455,8 +640,8 @@ impl PacketPreparer { PreparedPackets { packets, - tested_mixnodes, - tested_gateways, + mixnodes_under_test, + gateways_under_test, invalid_mixnodes, invalid_gateways, } diff --git a/nym-api/src/network_monitor/monitor/processor.rs b/nym-api/src/network_monitor/monitor/processor.rs index 84ac4b3aeb..0008bfa901 100644 --- a/nym-api/src/network_monitor/monitor/processor.rs +++ b/nym-api/src/network_monitor/monitor/processor.rs @@ -7,7 +7,6 @@ use crate::network_monitor::ROUTE_TESTING_TEST_NONCE; use futures::channel::mpsc; use futures::lock::{Mutex, MutexGuard}; use futures::{SinkExt, StreamExt}; -use log::warn; use nym_crypto::asymmetric::encryption; use nym_node_tester_utils::error::NetworkTestingError; use nym_node_tester_utils::processor::TestPacketProcessor; @@ -16,6 +15,7 @@ use nym_sphinx::receiver::{MessageReceiver, MessageRecoveryError}; use std::mem; use std::sync::Arc; use thiserror::Error; +use tracing::{debug, error, trace, warn}; pub(crate) type ReceivedProcessorSender = mpsc::UnboundedSender; pub(crate) type ReceivedProcessorReceiver = mpsc::UnboundedReceiver; diff --git a/nym-api/src/network_monitor/monitor/receiver.rs b/nym-api/src/network_monitor/monitor/receiver.rs index be835fcc41..d6dcbf4b85 100644 --- a/nym-api/src/network_monitor/monitor/receiver.rs +++ b/nym-api/src/network_monitor/monitor/receiver.rs @@ -8,6 +8,7 @@ use futures::StreamExt; use nym_crypto::asymmetric::identity; use nym_gateway_client::{AcknowledgementReceiver, MixnetMessageReceiver}; use nym_task::TaskClient; +use tracing::trace; pub(crate) type GatewayClientUpdateSender = mpsc::UnboundedSender; pub(crate) type GatewayClientUpdateReceiver = mpsc::UnboundedReceiver; diff --git a/nym-api/src/network_monitor/monitor/sender.rs b/nym-api/src/network_monitor/monitor/sender.rs index e2c5bf8e4d..0b371f1003 100644 --- a/nym-api/src/network_monitor/monitor/sender.rs +++ b/nym-api/src/network_monitor/monitor/sender.rs @@ -11,7 +11,6 @@ use futures::channel::mpsc; use futures::stream::{self, FuturesUnordered, StreamExt}; use futures::task::Context; use futures::{Future, Stream}; -use log::{debug, info, trace, warn}; use nym_bandwidth_controller::BandwidthController; use nym_credential_storage::persistent_storage::PersistentStorage; use nym_crypto::asymmetric::identity::{self, PUBLIC_KEY_LENGTH}; @@ -30,6 +29,7 @@ use std::pin::Pin; use std::sync::Arc; use std::task::Poll; use std::time::Duration; +use tracing::{debug, info, trace, warn}; const TIME_CHUNK_SIZE: Duration = Duration::from_millis(50); diff --git a/nym-api/src/network_monitor/monitor/summary_producer.rs b/nym-api/src/network_monitor/monitor/summary_producer.rs index 0c09e196f6..5f8449f75f 100644 --- a/nym-api/src/network_monitor/monitor/summary_producer.rs +++ b/nym-api/src/network_monitor/monitor/summary_producer.rs @@ -5,7 +5,7 @@ use crate::network_monitor::monitor::preparer::InvalidNode; use crate::network_monitor::test_packet::NodeTestMessage; use crate::network_monitor::test_route::TestRoute; use nym_node_tester_utils::node::{NodeType, TestableNode}; -use nym_types::monitoring::{GatewayResult, MixnodeResult}; +use nym_types::monitoring::NodeResult; use std::collections::HashMap; use std::fmt::{Display, Formatter}; @@ -60,8 +60,8 @@ impl TestReport { fn new( total_sent: usize, total_received: usize, - mixnode_results: &[MixnodeResult], - gateway_results: &[GatewayResult], + mixnode_results: &[NodeResult], + gateway_results: &[NodeResult], route_results: &[RouteResult], ) -> Self { let mut exceptional_mixnodes = 0; @@ -206,8 +206,8 @@ impl Display for TestReport { } pub(crate) struct TestSummary { - pub(crate) mixnode_results: Vec, - pub(crate) gateway_results: Vec, + pub(crate) mixnode_results: Vec, + pub(crate) gateway_results: Vec, pub(crate) route_results: Vec, } @@ -291,16 +291,10 @@ impl SummaryProducer { let performance = received as f32 / per_node_expected as f32 * 100.0; let reliability = performance.round() as u8; + let result = NodeResult::new(node.node_id, node.encoded_identity, reliability); match node.typ { - NodeType::Mixnode { mix_id } => { - let res = - MixnodeResult::new(mix_id, node.encoded_identity, node.owner, reliability); - mixnode_results.push(res) - } - NodeType::Gateway => { - let res = GatewayResult::new(node.encoded_identity, node.owner, reliability); - gateway_results.push(res) - } + NodeType::Mixnode => mixnode_results.push(result), + NodeType::Gateway => gateway_results.push(result), } } diff --git a/nym-api/src/network_monitor/test_packet.rs b/nym-api/src/network_monitor/test_packet.rs index 6e5c433ff8..1b1bfbbeda 100644 --- a/nym-api/src/network_monitor/test_packet.rs +++ b/nym-api/src/network_monitor/test_packet.rs @@ -24,7 +24,7 @@ impl NymApiTestMessageExt { pub fn mix_plaintexts( &self, - node: &mix::Node, + node: &mix::LegacyNode, test_packets: u32, ) -> Result>, NetworkTestingError> { NodeTestMessage::mix_plaintexts(node, test_packets, *self) diff --git a/nym-api/src/network_monitor/test_route/mod.rs b/nym-api/src/network_monitor/test_route/mod.rs index 477251f80d..224f751357 100644 --- a/nym-api/src/network_monitor/test_route/mod.rs +++ b/nym-api/src/network_monitor/test_route/mod.rs @@ -16,10 +16,10 @@ pub(crate) struct TestRoute { impl TestRoute { pub(crate) fn new( id: u64, - l1_mix: mix::Node, - l2_mix: mix::Node, - l3_mix: mix::Node, - gateway: gateway::Node, + l1_mix: mix::LegacyNode, + l2_mix: mix::LegacyNode, + l3_mix: mix::LegacyNode, + gateway: gateway::LegacyNode, ) -> Self { let layered_mixes = [ (1u8, vec![l1_mix]), @@ -39,19 +39,19 @@ impl TestRoute { self.id } - pub(crate) fn gateway(&self) -> &gateway::Node { + pub(crate) fn gateway(&self) -> &gateway::LegacyNode { &self.nodes.gateways()[0] } - pub(crate) fn layer_one_mix(&self) -> &mix::Node { + pub(crate) fn layer_one_mix(&self) -> &mix::LegacyNode { &self.nodes.mixes().get(&1).unwrap()[0] } - pub(crate) fn layer_two_mix(&self) -> &mix::Node { + pub(crate) fn layer_two_mix(&self) -> &mix::LegacyNode { &self.nodes.mixes().get(&2).unwrap()[0] } - pub(crate) fn layer_three_mix(&self) -> &mix::Node { + pub(crate) fn layer_three_mix(&self) -> &mix::LegacyNode { &self.nodes.mixes().get(&3).unwrap()[0] } diff --git a/nym-api/src/node_describe_cache/mod.rs b/nym-api/src/node_describe_cache/mod.rs index f56646e6fc..adf25c29f1 100644 --- a/nym-api/src/node_describe_cache/mod.rs +++ b/nym-api/src/node_describe_cache/mod.rs @@ -1,26 +1,29 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node_describe_cache::query_helpers::query_for_described_data; use crate::nym_contract_cache::cache::NymContractCache; use crate::support::caching::cache::{SharedCache, UninitialisedCache}; use crate::support::caching::refresher::{CacheItemProvider, CacheRefresher}; use crate::support::config; use crate::support::config::DEFAULT_NODE_DESCRIBE_BATCH_SIZE; +use async_trait::async_trait; use futures::{stream, StreamExt}; -use nym_api_requests::models::{ - AuthenticatorDetails, IpPacketRouterDetails, NetworkRequesterDetails, NymNodeDescription, - WireguardDetails, -}; -use nym_api_requests::nym_nodes::NodeRole; -use nym_config::defaults::{mainnet, DEFAULT_NYM_NODE_HTTP_PORT}; -use nym_contracts_common::IdentityKey; +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; +use nym_api_requests::models::{DescribedNodeType, NymNodeData, NymNodeDescription}; +use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; +use nym_mixnet_contract_common::{LegacyMixLayer, NodeId, NymNodeDetails}; use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt}; +use nym_topology::gateway::GatewayConversionError; +use nym_topology::mix::MixnodeConversionError; +use nym_topology::{gateway, mix, NetworkAddress}; use std::collections::HashMap; +use std::net::SocketAddr; +use std::time::Duration; use thiserror::Error; -use time::OffsetDateTime; +use tracing::{debug, error, info}; -// type alias for ease of use -pub type DescribedNodes = HashMap; +mod query_helpers; #[derive(Debug, Error)] pub enum NodeDescribeCacheError { @@ -30,42 +33,187 @@ pub enum NodeDescribeCacheError { source: UninitialisedCache, }, - #[error("gateway {gateway} has provided malformed host information ({host}: {source}")] + #[error("node {node_id} has provided malformed host information ({host}: {source}")] MalformedHost { host: String, - gateway: IdentityKey, + node_id: NodeId, #[source] source: NymNodeApiClientError, }, - #[error("gateway '{gateway}' with host '{host}' doesn't seem to expose any of the standard API ports, i.e.: 80, 443 or {}", DEFAULT_NYM_NODE_HTTP_PORT)] - NoHttpPortsAvailable { host: String, gateway: IdentityKey }, + #[error("node {node_id} with host '{host}' doesn't seem to expose its declared http port nor any of the standard API ports, i.e.: 80, 443 or {}", DEFAULT_NYM_NODE_HTTP_PORT)] + NoHttpPortsAvailable { host: String, node_id: NodeId }, - #[error("failed to query gateway '{gateway}': {source}")] + #[error("failed to query node {node_id}: {source}")] ApiFailure { - gateway: IdentityKey, + node_id: NodeId, #[source] source: NymNodeApiClientError, }, // TODO: perhaps include more details here like whether key/signature/payload was malformed - #[error("could not verify signed host information for gateway '{gateway}'")] - MissignedHostInformation { gateway: IdentityKey }, + #[error("could not verify signed host information for node {node_id}")] + MissignedHostInformation { node_id: NodeId }, + + #[error("node {node_id} is announcing an illegal ip address")] + IllegalIpAddress { node_id: NodeId }, +} + +// this exists because I've been moving things around quite a lot and now the place that holds the type +// doesn't have relevant dependencies for proper impl +pub(crate) trait NodeDescriptionTopologyExt { + fn try_to_topology_mix_node( + &self, + layer: LegacyMixLayer, + ) -> Result; + + fn try_to_topology_gateway(&self) -> Result; +} + +impl NodeDescriptionTopologyExt for NymNodeDescription { + // TODO: this might have to be moved around + fn try_to_topology_mix_node( + &self, + layer: LegacyMixLayer, + ) -> Result { + let keys = &self.description.host_information.keys; + let ips = &self.description.host_information.ip_address; + if ips.is_empty() { + return Err(MixnodeConversionError::NoIpAddressesProvided { + mixnode: keys.ed25519.to_base58_string(), + }); + } + + let host = match &self.description.host_information.hostname { + None => NetworkAddress::IpAddr(ips[0]), + Some(hostname) => NetworkAddress::Hostname(hostname.clone()), + }; + + // get ip from the self-reported values so we wouldn't need to do any hostname resolution + // (which doesn't really work in wasm) + let mix_host = SocketAddr::new(ips[0], self.description.mix_port()); + + Ok(mix::LegacyNode { + mix_id: self.node_id, + host, + mix_host, + identity_key: keys.ed25519, + sphinx_key: keys.x25519, + layer, + version: self + .description + .build_information + .build_version + .as_str() + .into(), + }) + } + + fn try_to_topology_gateway(&self) -> Result { + let keys = &self.description.host_information.keys; + + let ips = &self.description.host_information.ip_address; + if ips.is_empty() { + return Err(GatewayConversionError::NoIpAddressesProvided { + gateway: keys.ed25519.to_base58_string(), + }); + } + + let host = match &self.description.host_information.hostname { + None => NetworkAddress::IpAddr(ips[0]), + Some(hostname) => NetworkAddress::Hostname(hostname.clone()), + }; + + // get ip from the self-reported values so we wouldn't need to do any hostname resolution + // (which doesn't really work in wasm) + let mix_host = SocketAddr::new(ips[0], self.description.mix_port()); + + Ok(gateway::LegacyNode { + node_id: self.node_id, + host, + mix_host, + clients_ws_port: self.description.mixnet_websockets.ws_port, + clients_wss_port: self.description.mixnet_websockets.wss_port, + identity_key: self.description.host_information.keys.ed25519, + sphinx_key: self.description.host_information.keys.x25519, + version: self + .description + .build_information + .build_version + .as_str() + .into(), + }) + } +} + +#[derive(Debug, Clone)] +pub struct DescribedNodes { + nodes: HashMap, +} + +impl DescribedNodes { + pub fn force_update(&mut self, node: NymNodeDescription) { + self.nodes.insert(node.node_id, node); + } + + pub fn get_description(&self, node_id: &NodeId) -> Option<&NymNodeData> { + self.nodes.get(node_id).map(|n| &n.description) + } + + pub fn get_node(&self, node_id: &NodeId) -> Option<&NymNodeDescription> { + self.nodes.get(node_id) + } + + pub fn all_nodes(&self) -> impl Iterator { + self.nodes.values() + } + + pub fn all_nym_nodes(&self) -> impl Iterator { + self.nodes + .values() + .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) + } + + pub fn mixing_nym_nodes(&self) -> impl Iterator { + self.nodes + .values() + .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) + .filter(|n| n.description.declared_role.mixnode) + } + + pub fn entry_capable_nym_nodes(&self) -> impl Iterator { + self.nodes + .values() + .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) + .filter(|n| n.description.declared_role.entry) + } + + pub fn exit_capable_nym_nodes(&self) -> impl Iterator { + self.nodes + .values() + .filter(|n| n.contract_node_type == DescribedNodeType::NymNode) + .filter(|n| n.description.declared_role.can_operate_exit_gateway()) + } } pub struct NodeDescriptionProvider { contract_cache: NymContractCache, + allow_all_ips: bool, batch_size: usize, } impl NodeDescriptionProvider { - pub(crate) fn new(contract_cache: NymContractCache) -> NodeDescriptionProvider { + pub(crate) fn new( + contract_cache: NymContractCache, + allow_all_ips: bool, + ) -> NodeDescriptionProvider { NodeDescriptionProvider { contract_cache, + allow_all_ips, batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, } } @@ -79,35 +227,42 @@ impl NodeDescriptionProvider { async fn try_get_client( host: &str, - identity_key: &IdentityKey, - port: Option, + node_id: NodeId, + custom_port: Option, ) -> Result { // first try the standard port in case the operator didn't put the node behind the proxy, // then default https (443) // finally default http (80) let mut addresses_to_try = vec![ - format!("http://{host}:{DEFAULT_NYM_NODE_HTTP_PORT}"), - format!("http://{host}:8000"), - format!("https://{host}"), - format!("http://{host}"), + format!("http://{host}:{DEFAULT_NYM_NODE_HTTP_PORT}"), // 'standard' nym-node + format!("https://{host}"), // node behind https proxy (443) + format!("http://{host}"), // node behind http proxy (80) ]; - if let Some(port) = port { + // note: I removed 'standard' legacy mixnode port because it should now be automatically pulled via + // the 'custom_port' since it should have been present in the contract. + + if let Some(port) = custom_port { addresses_to_try.insert(0, format!("http://{host}:{port}")); } for address in addresses_to_try { // if provided host was malformed, no point in continuing - let client = match nym_node_requests::api::Client::new_url(address, None) { + let client = match nym_node_requests::api::Client::builder(address).and_then(|b| { + b.with_timeout(Duration::from_secs(5)) + .with_user_agent("nym-api-describe-cache") + .build() + }) { Ok(client) => client, Err(err) => { return Err(NodeDescribeCacheError::MalformedHost { host: host.to_string(), - gateway: identity_key.clone(), + node_id, source: err, }); } }; + if let Ok(health) = client.get_health().await { if health.status.is_up() { return Ok(client); @@ -117,149 +272,120 @@ async fn try_get_client( Err(NodeDescribeCacheError::NoHttpPortsAvailable { host: host.to_string(), - gateway: identity_key.to_string(), + node_id, }) } async fn try_get_description( data: RefreshData, -) -> Result<(IdentityKey, NymNodeDescription), NodeDescribeCacheError> { - let client = try_get_client(&data.host(), &data.identity_key(), data.port()).await?; - - let host_info = - client - .get_host_information() - .await - .map_err(|err| NodeDescribeCacheError::ApiFailure { - gateway: data.identity_key().to_string(), - source: err, - })?; + allow_all_ips: bool, +) -> Result { + let client = try_get_client(&data.host, data.node_id, data.port).await?; + + let map_query_err = |err| NodeDescribeCacheError::ApiFailure { + node_id: data.node_id, + source: err, + }; + + let host_info = client.get_host_information().await.map_err(map_query_err)?; if !host_info.verify_host_information() { return Err(NodeDescribeCacheError::MissignedHostInformation { - gateway: data.identity_key().clone(), + node_id: data.node_id, }); } - let build_info = - client - .get_build_information() - .await - .map_err(|err| NodeDescribeCacheError::ApiFailure { - gateway: data.identity_key().clone(), - source: err, - })?; - - // this can be an old node that hasn't yet exposed this - let auxiliary_details = client.get_auxiliary_details().await.inspect_err(|err| { - debug!("could not obtain auxiliary details of node {}: {err} is it running an old version?", data.identity_key()); - }).unwrap_or_default(); - - let websockets = - client - .get_mixnet_websockets() - .await - .map_err(|err| NodeDescribeCacheError::ApiFailure { - gateway: data.identity_key().clone(), - source: err, - })?; - - let network_requester = - if let Ok(nr) = client.get_network_requester().await { - let exit_policy = client.get_exit_policy().await.map_err(|err| { - NodeDescribeCacheError::ApiFailure { - gateway: data.identity_key().clone(), - source: err, - } - })?; - let uses_nym_exit_policy = exit_policy.upstream_source == mainnet::EXIT_POLICY_URL; - - Some(NetworkRequesterDetails { - address: nr.address, - uses_exit_policy: exit_policy.enabled && uses_nym_exit_policy, - }) - } else { - None - }; + if !allow_all_ips && !host_info.data.check_ips() { + return Err(NodeDescribeCacheError::IllegalIpAddress { + node_id: data.node_id, + }); + } - let ip_packet_router = if let Ok(ipr) = client.get_ip_packet_router().await { - Some(IpPacketRouterDetails { - address: ipr.address, - }) - } else { - None - }; + let node_info = query_for_described_data(&client, data.node_id).await?; + let description = node_info.into_node_description(host_info.data); - let authenticator = if let Ok(auth) = client.get_authenticator().await { - Some(AuthenticatorDetails { - address: auth.address, - }) - } else { - None - }; + Ok(NymNodeDescription { + node_id: data.node_id, + contract_node_type: data.node_type, + description, + }) +} - let wireguard = if let Ok(wg) = client.get_wireguard().await { - Some(WireguardDetails { - port: wg.port, - public_key: wg.public_key, - }) - } else { - None - }; +#[derive(Debug)] +pub(crate) struct RefreshData { + host: String, + node_id: NodeId, + node_type: DescribedNodeType, - let description = NymNodeDescription { - host_information: host_info.data.into(), - last_polled: OffsetDateTime::now_utc().into(), - build_information: build_info, - network_requester, - ip_packet_router, - authenticator, - wireguard, - mixnet_websockets: websockets.into(), - auxiliary_details, - role: data.role(), - }; + port: Option, +} - Ok((data.identity_key().clone(), description)) +impl<'a> From<&'a LegacyMixNodeDetailsWithLayer> for RefreshData { + fn from(node: &'a LegacyMixNodeDetailsWithLayer) -> Self { + RefreshData::new( + &node.bond_information.mix_node.host, + DescribedNodeType::LegacyMixnode, + node.mix_id(), + Some(node.bond_information.mix_node.http_api_port), + ) + } } -struct RefreshData { - host: String, - identity_key: IdentityKey, - role: NodeRole, - port: Option, +impl<'a> From<&'a LegacyGatewayBondWithId> for RefreshData { + fn from(node: &'a LegacyGatewayBondWithId) -> Self { + RefreshData::new( + &node.bond.gateway.host, + DescribedNodeType::LegacyGateway, + node.node_id, + None, + ) + } +} + +impl<'a> From<&'a NymNodeDetails> for RefreshData { + fn from(node: &'a NymNodeDetails) -> Self { + RefreshData::new( + &node.bond_information.node.host, + DescribedNodeType::NymNode, + node.node_id(), + node.bond_information.node.custom_http_port, + ) + } } impl RefreshData { - pub fn new(host: String, identity_key: IdentityKey, role: NodeRole, port: Option) -> Self { + pub fn new( + host: impl Into, + node_type: DescribedNodeType, + node_id: NodeId, + port: Option, + ) -> Self { RefreshData { - host, - identity_key, - role, + host: host.into(), + node_id, + node_type, port, } } - pub fn host(&self) -> String { - self.host.clone() - } - - pub fn identity_key(&self) -> IdentityKey { - self.identity_key.clone() + pub(crate) fn node_id(&self) -> NodeId { + self.node_id } - pub fn port(&self) -> Option { - self.port - } - - pub fn role(&self) -> NodeRole { - self.role.clone() + pub(crate) async fn try_refresh(self, allow_all_ips: bool) -> Option { + match try_get_description(self, allow_all_ips).await { + Ok(description) => Some(description), + Err(err) => { + debug!("failed to obtain node self-described data: {err}"); + None + } + } } } #[async_trait] impl CacheItemProvider for NodeDescriptionProvider { - type Item = HashMap; + type Item = DescribedNodes; type Error = NodeDescribeCacheError; async fn wait_until_ready(&self) { @@ -267,58 +393,53 @@ impl CacheItemProvider for NodeDescriptionProvider { } async fn try_refresh(&self) -> Result { - let mut host_id_pairs = self - .contract_cache - .gateways_all() - .await - .into_iter() - .map(|full| { - RefreshData::new( - full.gateway.host, - full.gateway.identity_key, - NodeRole::EntryGateway, - None, - ) - }) - .collect::>(); - - host_id_pairs.extend( - self.contract_cache - .mixnodes_all() - .await - .into_iter() - .map(|full| { - RefreshData::new( - full.bond_information.mix_node.host, - full.bond_information.mix_node.identity_key, - NodeRole::Mixnode { - layer: full.bond_information.layer.into(), - }, - Some(full.bond_information.mix_node.mix_port), - ) - }) - .collect::>(), - ); - - if host_id_pairs.is_empty() { - return Ok(HashMap::new()); + // we need to query: + // - legacy mixnodes (because they might already be running nym-nodes, but haven't updated contract info) + // - legacy gateways (because they might already be running nym-nodes, but haven't updated contract info) + // - nym-nodes + + let mut nodes_to_query: Vec = Vec::new(); + + match self.contract_cache.all_cached_legacy_mixnodes().await { + None => error!("failed to obtain mixnodes information from the cache"), + Some(legacy_mixnodes) => { + for node in &**legacy_mixnodes { + nodes_to_query.push(node.into()) + } + } + } + + match self.contract_cache.all_cached_legacy_gateways().await { + None => error!("failed to obtain gateways information from the cache"), + Some(legacy_gateways) => { + for node in &**legacy_gateways { + nodes_to_query.push(node.into()) + } + } } - let node_description = stream::iter(host_id_pairs.into_iter().map(try_get_description)) - .buffer_unordered(self.batch_size) - .filter_map(|res| async move { - match res { - Ok((identity, description)) => Some((identity, description)), - Err(err) => { - debug!("failed to obtain gateway self-described data: {err}"); - None - } + match self.contract_cache.all_cached_nym_nodes().await { + None => error!("failed to obtain nym-nodes information from the cache"), + Some(nym_nodes) => { + for node in &**nym_nodes { + nodes_to_query.push(node.into()) } - }) - .collect::>() - .await; + } + } + + let nodes = stream::iter( + nodes_to_query + .into_iter() + .map(|n| n.try_refresh(self.allow_all_ips)), + ) + .buffer_unordered(self.batch_size) + .filter_map(|x| async move { x.map(|d| (d.node_id, d)) }) + .collect::>() + .await; + + info!("refreshed self described data for {} nodes", nodes.len()); - Ok(node_description) + Ok(DescribedNodes { nodes }) } } @@ -327,13 +448,14 @@ impl CacheItemProvider for NodeDescriptionProvider { pub(crate) fn new_refresher( config: &config::TopologyCacher, contract_cache: NymContractCache, - // hehe. we can't do that yet - // network_gateways: SharedCache>, ) -> CacheRefresher { CacheRefresher::new( Box::new( - NodeDescriptionProvider::new(contract_cache) - .with_batch_size(config.debug.node_describe_batch_size), + NodeDescriptionProvider::new( + contract_cache, + config.debug.node_describe_allow_illegal_ips, + ) + .with_batch_size(config.debug.node_describe_batch_size), ), config.debug.node_describe_caching_interval, ) @@ -342,14 +464,15 @@ pub(crate) fn new_refresher( pub(crate) fn new_refresher_with_initial_value( config: &config::TopologyCacher, contract_cache: NymContractCache, - // hehe. we can't do that yet - // network_gateways: SharedCache>, initial: SharedCache, ) -> CacheRefresher { CacheRefresher::new_with_initial_value( Box::new( - NodeDescriptionProvider::new(contract_cache) - .with_batch_size(config.debug.node_describe_batch_size), + NodeDescriptionProvider::new( + contract_cache, + config.debug.node_describe_allow_illegal_ips, + ) + .with_batch_size(config.debug.node_describe_batch_size), ), config.debug.node_describe_caching_interval, initial, diff --git a/nym-api/src/node_describe_cache/query_helpers.rs b/nym-api/src/node_describe_cache/query_helpers.rs new file mode 100644 index 0000000000..78611ca624 --- /dev/null +++ b/nym-api/src/node_describe_cache/query_helpers.rs @@ -0,0 +1,243 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::node_describe_cache::NodeDescribeCacheError; +use futures::future::{maybe_done, MaybeDone}; +use futures::{FutureExt, TryFutureExt}; +use nym_api_requests::models::{ + AuthenticatorDetails, DeclaredRoles, HostInformation, IpPacketRouterDetails, + NetworkRequesterDetails, NymNodeData, WebSockets, WireguardDetails, +}; +use nym_bin_common::build_information::BinaryBuildInformationOwned; +use nym_config::defaults::mainnet; +use nym_mixnet_contract_common::NodeId; +use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt}; +use nym_node_requests::api::v1::node::models::AuxiliaryDetails; +use nym_node_requests::api::Client; +use pin_project::pin_project; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; +use time::OffsetDateTime; +use tracing::debug; + +async fn network_requester_future( + client: &Client, +) -> Result, NymNodeApiClientError> { + let Ok(nr) = client.get_network_requester().await else { + return Ok(None); + }; + + client.get_exit_policy().await.map(|exit_policy| { + let uses_nym_exit_policy = exit_policy.upstream_source == mainnet::EXIT_POLICY_URL; + Some(NetworkRequesterDetails { + address: nr.address, + uses_exit_policy: exit_policy.enabled && uses_nym_exit_policy, + }) + }) +} + +pub(crate) async fn query_for_described_data( + client: &Client, + node_id: NodeId, +) -> Result { + let map_query_err = |source| NodeDescribeCacheError::ApiFailure { node_id, source }; + + // all of those should be happening concurrently. + NodeDescribedInfoMegaFuture::new( + client.get_build_information().map_err(map_query_err), + client.get_roles().ok_into().map_err(map_query_err), + client.get_auxiliary_details() + .inspect_err(|err| { + // old nym-nodes will not have this field, so use the default instead + debug!("could not obtain auxiliary details of node {node_id}: {err} is it running an old version?") + }) + .unwrap_or_else(|_| AuxiliaryDetails::default()), + client.get_mixnet_websockets().ok_into().map_err(map_query_err), + network_requester_future(client).map_err(map_query_err), + // `ok_into` ultimately calls `IpPacketRouter::into` to transform it into `IpPacketRouterDetails` + client.get_ip_packet_router().ok_into().map(Result::ok), + client.get_authenticator().ok_into().map(Result::ok), + client.get_wireguard().ok_into().map(Result::ok) + ) + .await +} + +// just a helper to have named fields as opposed to a mega tuple +// could I have used something more sophisticated? sure. +// is this code disgusting? yes. does it work? also yes +// (note: I've just mostly copied code from `futures-util::generate` macro where +// they derive code for `join2`, `join3`, etc.) +#[pin_project] +struct NodeDescribedInfoMegaFuture +where + F1: Future, + F2: Future, + F3: Future, + F4: Future, + F5: Future, + F6: Future, + F7: Future, + F8: Future, +{ + #[pin] + build_info: MaybeDone, + #[pin] + roles: MaybeDone, + #[pin] + auxiliary_details: MaybeDone, + #[pin] + websockets: MaybeDone, + #[pin] + network_requester: MaybeDone, + #[pin] + ipr: MaybeDone, + #[pin] + authenticator: MaybeDone, + #[pin] + wireguard: MaybeDone, +} + +impl Future + for NodeDescribedInfoMegaFuture +where + F1: Future>, + F2: Future>, + F3: Future, + F4: Future>, + F5: Future, NodeDescribeCacheError>>, + F6: Future>, + F7: Future>, + F8: Future>, +{ + type Output = Result; + + // SAFETY: we've explicitly checked all futures have completed thus the unwraps are fine + #[allow(clippy::unwrap_used)] + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut all_done = true; + let mut futures = self.project(); + + all_done &= futures.build_info.as_mut().poll(cx).is_ready(); + all_done &= futures.roles.as_mut().poll(cx).is_ready(); + all_done &= futures.auxiliary_details.as_mut().poll(cx).is_ready(); + all_done &= futures.websockets.as_mut().poll(cx).is_ready(); + all_done &= futures.network_requester.as_mut().poll(cx).is_ready(); + all_done &= futures.ipr.as_mut().poll(cx).is_ready(); + all_done &= futures.authenticator.as_mut().poll(cx).is_ready(); + all_done &= futures.wireguard.as_mut().poll(cx).is_ready(); + + if all_done { + Poll::Ready( + ResolvedNodeDescribedInfo { + build_info: futures.build_info.take_output().unwrap(), + roles: futures.roles.take_output().unwrap(), + auxiliary_details: futures.auxiliary_details.take_output().unwrap(), + websockets: futures.websockets.take_output().unwrap(), + network_requester: futures.network_requester.take_output().unwrap(), + ipr: futures.ipr.take_output().unwrap(), + authenticator: futures.authenticator.take_output().unwrap(), + wireguard: futures.wireguard.take_output().unwrap(), + } + .try_unwrap(), + ) + } else { + Poll::Pending + } + } +} + +impl NodeDescribedInfoMegaFuture +where + F1: Future, + F2: Future, + F3: Future, + F4: Future, + F5: Future, + F6: Future, + F7: Future, + F8: Future, +{ + // okay. the fact I have to bypass clippy here means it wasn't a good idea to create this abomination after all + #[allow(clippy::too_many_arguments)] + fn new( + build_info: F1, + roles: F2, + auxiliary_details: F3, + websockets: F4, + network_requester: F5, + ipr: F6, + authenticator: F7, + wireguard: F8, + ) -> Self { + NodeDescribedInfoMegaFuture { + build_info: maybe_done(build_info), + roles: maybe_done(roles), + auxiliary_details: maybe_done(auxiliary_details), + websockets: maybe_done(websockets), + network_requester: maybe_done(network_requester), + ipr: maybe_done(ipr), + authenticator: maybe_done(authenticator), + wireguard: maybe_done(wireguard), + } + } +} + +struct ResolvedNodeDescribedInfo { + build_info: Result, + roles: Result, + // TODO: in the future make it return a Result as well. + auxiliary_details: AuxiliaryDetails, + websockets: Result, + network_requester: Result, NodeDescribeCacheError>, + ipr: Option, + authenticator: Option, + wireguard: Option, +} + +impl ResolvedNodeDescribedInfo { + fn try_unwrap(self) -> Result { + Ok(UnwrappedResolvedNodeDescribedInfo { + build_info: self.build_info?, + roles: self.roles?, + auxiliary_details: self.auxiliary_details, + websockets: self.websockets?, + network_requester: self.network_requester?, + ipr: self.ipr, + authenticator: self.authenticator, + wireguard: self.wireguard, + }) + } +} + +#[derive(Debug)] +pub(crate) struct UnwrappedResolvedNodeDescribedInfo { + pub(crate) build_info: BinaryBuildInformationOwned, + pub(crate) roles: DeclaredRoles, + pub(crate) auxiliary_details: AuxiliaryDetails, + pub(crate) websockets: WebSockets, + pub(crate) network_requester: Option, + pub(crate) ipr: Option, + pub(crate) authenticator: Option, + pub(crate) wireguard: Option, +} + +impl UnwrappedResolvedNodeDescribedInfo { + pub(crate) fn into_node_description( + self, + host_info: impl Into, + ) -> NymNodeData { + NymNodeData { + host_information: host_info.into(), + last_polled: OffsetDateTime::now_utc().into(), + build_information: self.build_info, + network_requester: self.network_requester, + ip_packet_router: self.ipr, + authenticator: self.authenticator, + wireguard: self.wireguard, + mixnet_websockets: self.websockets, + auxiliary_details: self.auxiliary_details, + declared_role: self.roles, + } + } +} diff --git a/nym-api/src/node_status_api/cache/data.rs b/nym-api/src/node_status_api/cache/data.rs index b2e6512417..d8b2cda51e 100644 --- a/nym-api/src/node_status_api/cache/data.rs +++ b/nym-api/src/node_status_api/cache/data.rs @@ -1,9 +1,9 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated}; +use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; use nym_contracts_common::IdentityKey; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use std::collections::HashMap; use crate::support::caching::Cache; @@ -12,11 +12,14 @@ use super::inclusion_probabilities::InclusionProbabilities; #[derive(Default)] pub(crate) struct NodeStatusCacheData { - pub(crate) mixnodes_annotated: Cache>, - pub(crate) rewarded_set_annotated: Cache>, - pub(crate) active_set_annotated: Cache>, + pub(crate) legacy_gateway_mapping: Cache>, - pub(crate) gateways_annotated: Cache>, + /// Basic annotation for **all** nodes, i.e. legacy + nym-nodes + pub(crate) node_annotations: Cache>, + + /// Annotations as before, just for legacy things + pub(crate) mixnodes_annotated: Cache>, + pub(crate) gateways_annotated: Cache>, // Estimated active set inclusion probabilities from Monte Carlo simulation pub(crate) inclusion_probabilities: Cache, diff --git a/nym-api/src/node_status_api/cache/inclusion_probabilities.rs b/nym-api/src/node_status_api/cache/inclusion_probabilities.rs index 4d53793d36..28bde0e1e2 100644 --- a/nym-api/src/node_status_api/cache/inclusion_probabilities.rs +++ b/nym-api/src/node_status_api/cache/inclusion_probabilities.rs @@ -1,12 +1,13 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; use nym_api_requests::models::InclusionProbability; use nym_contracts_common::truncate_decimal; -use nym_mixnet_contract_common::{MixId, MixNodeDetails, RewardingParams}; +use nym_mixnet_contract_common::{NodeId, RewardingParams}; use serde::Serialize; use std::time::Duration; -use tap::TapFallible; +use tracing::error; const MAX_SIMULATION_SAMPLES: u64 = 5000; const MAX_SIMULATION_TIME_SEC: u64 = 15; @@ -22,13 +23,13 @@ pub(crate) struct InclusionProbabilities { impl InclusionProbabilities { pub(crate) fn compute( - mixnodes: &[MixNodeDetails], + mixnodes: &[LegacyMixNodeDetailsWithLayer], params: RewardingParams, ) -> Option { compute_inclusion_probabilities(mixnodes, params) } - pub(crate) fn node(&self, mix_id: MixId) -> Option<&InclusionProbability> { + pub(crate) fn node(&self, mix_id: NodeId) -> Option<&InclusionProbability> { self.inclusion_probabilities .iter() .find(|x| x.mix_id == mix_id) @@ -36,11 +37,11 @@ impl InclusionProbabilities { } fn compute_inclusion_probabilities( - mixnodes: &[MixNodeDetails], + mixnodes: &[LegacyMixNodeDetailsWithLayer], params: RewardingParams, ) -> Option { - let active_set_size = params.active_set_size; - let standby_set_size = params.rewarded_set_size - active_set_size; + let active_set_size = params.active_set_size(); + let standby_set_size = params.rewarded_set.standby; // Unzip list of total bonds into ids and bonds. // We need to go through this zip/unzip procedure to make sure we have matching identities @@ -57,7 +58,7 @@ fn compute_inclusion_probabilities( Duration::from_secs(MAX_SIMULATION_TIME_SEC), &mut rng, ) - .tap_err(|err| error!("{err}")) + .inspect_err(|err| error!("{err}")) .ok()?; Some(InclusionProbabilities { @@ -69,7 +70,9 @@ fn compute_inclusion_probabilities( }) } -fn unzip_into_mixnode_ids_and_total_bonds(mixnodes: &[MixNodeDetails]) -> (Vec, Vec) { +fn unzip_into_mixnode_ids_and_total_bonds( + mixnodes: &[LegacyMixNodeDetailsWithLayer], +) -> (Vec, Vec) { mixnodes .iter() .map(|m| (m.mix_id(), truncate_decimal(m.total_stake()).u128())) @@ -77,7 +80,7 @@ fn unzip_into_mixnode_ids_and_total_bonds(mixnodes: &[MixNodeDetails]) -> (Vec Vec { ids.iter() diff --git a/nym-api/src/node_status_api/cache/mod.rs b/nym-api/src/node_status_api/cache/mod.rs index 99d4358802..262755ef3b 100644 --- a/nym-api/src/node_status_api/cache/mod.rs +++ b/nym-api/src/node_status_api/cache/mod.rs @@ -4,15 +4,15 @@ use self::data::NodeStatusCacheData; use self::inclusion_probabilities::InclusionProbabilities; use crate::support::caching::Cache; -use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, MixnodeStatus}; -use nym_contracts_common::{IdentityKey, IdentityKeyRef}; -use nym_mixnet_contract_common::MixId; -use rocket::fairing::AdHoc; +use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; +use nym_contracts_common::IdentityKey; +use nym_mixnet_contract_common::NodeId; use std::collections::HashMap; use std::{sync::Arc, time::Duration}; use thiserror::Error; use tokio::sync::RwLockReadGuard; use tokio::{sync::RwLock, time}; +use tracing::error; const CACHE_TIMEOUT_MS: u64 = 100; @@ -47,27 +47,22 @@ impl NodeStatusCache { } } - #[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")] - pub fn stage() -> AdHoc { - AdHoc::on_ignite("Node Status Cache", |rocket| async { - rocket.manage(Self::new()) - }) - } - /// Updates the cache with the latest data. async fn update( &self, - mixnodes: HashMap, - rewarded_set: Vec, - active_set: Vec, - gateways: HashMap, + legacy_gateway_mapping: HashMap, + node_annotations: HashMap, + mixnodes: HashMap, + gateways: HashMap, inclusion_probabilities: InclusionProbabilities, ) { match time::timeout(Duration::from_millis(CACHE_TIMEOUT_MS), self.inner.write()).await { Ok(mut cache) => { cache.mixnodes_annotated.unchecked_update(mixnodes); - cache.rewarded_set_annotated.unchecked_update(rewarded_set); - cache.active_set_annotated.unchecked_update(active_set); + cache + .legacy_gateway_mapping + .unchecked_update(legacy_gateway_mapping); + cache.node_annotations.unchecked_update(node_annotations); cache.gateways_annotated.unchecked_update(gateways); cache .inclusion_probabilities @@ -104,10 +99,25 @@ impl NodeStatusCache { } } - pub(crate) async fn active_mixnodes_cache( + pub(crate) async fn node_annotations( + &self, + ) -> Option>>> { + self.get(|c| &c.node_annotations).await + } + + pub(crate) async fn map_identity_to_node_id(&self, identity: &str) -> Option { + self.inner + .read() + .await + .legacy_gateway_mapping + .get(identity) + .copied() + } + + pub(crate) async fn annotated_legacy_mixnodes( &self, - ) -> Option>>> { - self.get(|c| &c.active_set_annotated).await + ) -> Option>>> { + self.get(|c| &c.mixnodes_annotated).await } pub(crate) async fn mixnodes_annotated_full(&self) -> Option> { @@ -122,24 +132,14 @@ impl NodeStatusCache { Some(full.iter().filter(|m| !m.blacklisted).cloned().collect()) } - pub(crate) async fn mixnode_annotated(&self, mix_id: MixId) -> Option { + pub(crate) async fn mixnode_annotated(&self, mix_id: NodeId) -> Option { let mixnodes = self.get(|c| &c.mixnodes_annotated).await?; mixnodes.get(&mix_id).cloned() } - pub(crate) async fn rewarded_set_annotated(&self) -> Option>> { - self.get_owned(|c| c.rewarded_set_annotated.clone_cache()) - .await - } - - pub(crate) async fn active_set_annotated(&self) -> Option>> { - self.get_owned(|c| c.active_set_annotated.clone_cache()) - .await - } - - pub(crate) async fn gateways_cache( + pub(crate) async fn annotated_legacy_gateways( &self, - ) -> Option>>> { + ) -> Option>>> { self.get(|c| &c.gateways_annotated).await } @@ -155,41 +155,13 @@ impl NodeStatusCache { Some(full.iter().filter(|m| !m.blacklisted).cloned().collect()) } - pub(crate) async fn gateway_annotated( - &self, - gateway_id: IdentityKeyRef<'_>, - ) -> Option { + pub(crate) async fn gateway_annotated(&self, node_id: NodeId) -> Option { let gateways = self.get(|c| &c.gateways_annotated).await?; - gateways.get(gateway_id).cloned() + gateways.get(&node_id).cloned() } pub(crate) async fn inclusion_probabilities(&self) -> Option> { self.get_owned(|c| c.inclusion_probabilities.clone_cache()) .await } - - pub async fn mixnode_details( - &self, - mix_id: MixId, - ) -> (Option, MixnodeStatus) { - // it might not be the most optimal to possibly iterate the entire vector to find (or not) - // the relevant value. However, the vectors are relatively small (< 10_000 elements, < 1000 for active set) - - let active_set = &self.active_set_annotated().await.unwrap().into_inner(); - if let Some(bond) = active_set.iter().find(|mix| mix.mix_id() == mix_id) { - return (Some(bond.clone()), MixnodeStatus::Active); - } - - let rewarded_set = &self.rewarded_set_annotated().await.unwrap().into_inner(); - if let Some(bond) = rewarded_set.iter().find(|mix| mix.mix_id() == mix_id) { - return (Some(bond.clone()), MixnodeStatus::Standby); - } - - let all_bonded = &self.mixnodes_annotated_filtered().await.unwrap(); - if let Some(bond) = all_bonded.iter().find(|mix| mix.mix_id() == mix_id) { - (Some(bond.clone()), MixnodeStatus::Inactive) - } else { - (None, MixnodeStatus::NotFound) - } - } } diff --git a/nym-api/src/node_status_api/cache/node_sets.rs b/nym-api/src/node_status_api/cache/node_sets.rs index d20f3329b4..a13508b9df 100644 --- a/nym-api/src/node_status_api/cache/node_sets.rs +++ b/nym-api/src/node_status_api/cache/node_sets.rs @@ -1,59 +1,24 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node_status_api::helpers::RewardedSetStatus; use crate::node_status_api::reward_estimate::{compute_apy_from_reward, compute_reward_estimate}; +use crate::nym_contract_cache::cache::CachedRewardedSet; use crate::support::storage::NymApiStorage; -use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodePerformance}; -use nym_mixnet_contract_common::families::FamilyHead; -use nym_mixnet_contract_common::{reward_params::Performance, Interval, MixId}; -use nym_mixnet_contract_common::{ - GatewayBond, IdentityKey, MixNodeDetails, RewardedSetNodeStatus, RewardingParams, +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; +use nym_api_requests::models::{ + GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation, NodePerformance, }; +use nym_mixnet_contract_common::{reward_params::Performance, Interval, NodeId}; +use nym_mixnet_contract_common::{NymNodeDetails, RewardingParams}; use nym_topology::NetworkAddress; use std::collections::{HashMap, HashSet}; use std::net::ToSocketAddrs; use std::str::FromStr; -pub(super) fn to_rewarded_set_node_status( - rewarded_set: &[MixNodeDetails], - active_set: &[MixNodeDetails], -) -> HashMap { - let mut rewarded_set_node_status: HashMap = rewarded_set - .iter() - .map(|m| (m.mix_id(), RewardedSetNodeStatus::Standby)) - .collect(); - for mixnode in active_set { - *rewarded_set_node_status - .get_mut(&mixnode.mix_id()) - .expect("All active nodes are rewarded nodes") = RewardedSetNodeStatus::Active; - } - rewarded_set_node_status -} - -pub(super) fn split_into_active_and_rewarded_set( - mixnodes_annotated: &HashMap, - rewarded_set_node_status: &HashMap, -) -> (Vec, Vec) { - let rewarded_set: Vec<_> = mixnodes_annotated - .values() - .filter(|mixnode| rewarded_set_node_status.get(&mixnode.mix_id()).is_some()) - .cloned() - .collect(); - let active_set: Vec<_> = rewarded_set - .iter() - .filter(|mixnode| { - rewarded_set_node_status - .get(&mixnode.mix_id()) - .map_or(false, RewardedSetNodeStatus::is_active) - }) - .cloned() - .collect(); - (rewarded_set, active_set) -} - pub(super) async fn get_mixnode_performance_from_storage( storage: &NymApiStorage, - mix_id: MixId, + mix_id: NodeId, epoch: Interval, ) -> Option { storage @@ -68,12 +33,12 @@ pub(super) async fn get_mixnode_performance_from_storage( pub(super) async fn get_gateway_performance_from_storage( storage: &NymApiStorage, - gateway_id: &str, + node_id: NodeId, epoch: Interval, ) -> Option { storage .get_average_gateway_uptime_in_the_last_24hrs( - gateway_id, + node_id, epoch.current_epoch_end_unix_timestamp(), ) .await @@ -81,19 +46,25 @@ pub(super) async fn get_gateway_performance_from_storage( .map(Into::into) } -pub(super) async fn annotate_nodes_with_details( +// TODO: this might have to be moved to a different file if other places also rely on this functionality +fn get_rewarded_set_status(rewarded_set: &CachedRewardedSet, node_id: NodeId) -> RewardedSetStatus { + if rewarded_set.is_standby(&node_id) { + RewardedSetStatus::Standby + } else if rewarded_set.is_active_mixnode(&node_id) { + RewardedSetStatus::Active + } else { + RewardedSetStatus::Inactive + } +} + +pub(super) async fn annotate_legacy_mixnodes_nodes_with_details( storage: &NymApiStorage, - mixnodes: Vec, + mixnodes: Vec, interval_reward_params: RewardingParams, current_interval: Interval, - rewarded_set: &HashMap, - mix_to_family: Vec<(IdentityKey, FamilyHead)>, - blacklist: &HashSet, -) -> HashMap { - let mix_to_family = mix_to_family - .into_iter() - .collect::>(); - + rewarded_set: &CachedRewardedSet, + blacklist: &HashSet, +) -> HashMap { let mut annotated = HashMap::new(); for mixnode in mixnodes { let stake_saturation = mixnode @@ -104,7 +75,7 @@ pub(super) async fn annotate_nodes_with_details( .rewarding_details .uncapped_bond_saturation(&interval_reward_params); - let rewarded_set_status = rewarded_set.get(&mixnode.mix_id()).copied(); + let rewarded_set_status = get_rewarded_set_status(rewarded_set, mixnode.mix_id()); // If the performance can't be obtained, because the nym-api was not started with // the monitoring (and hence, storage), then reward estimates will be all zero @@ -147,10 +118,6 @@ pub(super) async fn annotate_nodes_with_details( let (estimated_operator_apy, estimated_delegators_apy) = compute_apy_from_reward(&mixnode, reward_estimate, current_interval); - let family = mix_to_family - .get(mixnode.bond_information.identity()) - .cloned(); - annotated.insert( mixnode.mix_id(), MixNodeBondAnnotated { @@ -162,7 +129,6 @@ pub(super) async fn annotate_nodes_with_details( node_performance, estimated_operator_apy, estimated_delegators_apy, - family, ip_addresses, }, ); @@ -170,35 +136,33 @@ pub(super) async fn annotate_nodes_with_details( annotated } -pub(crate) async fn annotate_gateways_with_details( +pub(crate) async fn annotate_legacy_gateways_with_details( storage: &NymApiStorage, - gateway_bonds: Vec, + gateway_bonds: Vec, current_interval: Interval, - blacklist: &HashSet, -) -> HashMap { + blacklist: &HashSet, +) -> HashMap { let mut annotated = HashMap::new(); for gateway_bond in gateway_bonds { - let performance = get_gateway_performance_from_storage( - storage, - gateway_bond.identity(), - current_interval, - ) - .await - .unwrap_or_default(); + let performance = + get_gateway_performance_from_storage(storage, gateway_bond.node_id, current_interval) + .await + .unwrap_or_default(); let node_performance = storage - .construct_gateway_report(gateway_bond.identity()) + .construct_gateway_report(gateway_bond.node_id) .await .map(NodePerformance::from) .ok() .unwrap_or_default(); // safety: this conversion is infallible - let ip_addresses = match NetworkAddress::from_str(&gateway_bond.gateway.host).unwrap() { + let ip_addresses = match NetworkAddress::from_str(&gateway_bond.bond.gateway.host).unwrap() + { NetworkAddress::IpAddr(ip) => vec![ip], NetworkAddress::Hostname(hostname) => { // try to resolve it - (hostname.as_str(), gateway_bond.gateway.mix_port) + (hostname.as_str(), gateway_bond.bond.gateway.mix_port) .to_socket_addrs() .map(|iter| iter.map(|s| s.ip()).collect::>()) .unwrap_or_default() @@ -206,9 +170,9 @@ pub(crate) async fn annotate_gateways_with_details( }; annotated.insert( - gateway_bond.identity().to_string(), + gateway_bond.node_id, GatewayBondAnnotated { - blacklisted: blacklist.contains(&gateway_bond.gateway.identity_key), + blacklisted: blacklist.contains(&gateway_bond.node_id), gateway_bond, self_described: None, performance, @@ -219,3 +183,76 @@ pub(crate) async fn annotate_gateways_with_details( } annotated } + +pub(crate) async fn produce_node_annotations( + storage: &NymApiStorage, + legacy_mixnodes: &[LegacyMixNodeDetailsWithLayer], + legacy_gateways: &[LegacyGatewayBondWithId], + nym_nodes: &[NymNodeDetails], + rewarded_set: &CachedRewardedSet, + current_interval: Interval, +) -> HashMap { + let mut annotations = HashMap::new(); + + for legacy_mix in legacy_mixnodes { + let perf = storage + .get_average_mixnode_uptime_in_the_last_24hrs( + legacy_mix.mix_id(), + current_interval.current_epoch_end_unix_timestamp(), + ) + .await + .ok() + .unwrap_or_default() + .into(); + + annotations.insert( + legacy_mix.mix_id(), + NodeAnnotation { + last_24h_performance: perf, + current_role: rewarded_set.role(legacy_mix.mix_id()).map(|r| r.into()), + }, + ); + } + + for legacy_gateway in legacy_gateways { + let perf = storage + .get_average_gateway_uptime_in_the_last_24hrs( + legacy_gateway.node_id, + current_interval.current_epoch_end_unix_timestamp(), + ) + .await + .ok() + .unwrap_or_default() + .into(); + + annotations.insert( + legacy_gateway.node_id, + NodeAnnotation { + last_24h_performance: perf, + current_role: rewarded_set.role(legacy_gateway.node_id).map(|r| r.into()), + }, + ); + } + + for nym_node in nym_nodes { + let perf = storage + .get_average_node_uptime_in_the_last_24hrs( + nym_node.node_id(), + current_interval.current_epoch_end_unix_timestamp(), + ) + .await + .ok() + .unwrap_or_default() + .into(); + + annotations.insert( + nym_node.node_id(), + NodeAnnotation { + last_24h_performance: perf, + current_role: rewarded_set.role(nym_node.node_id()).map(|r| r.into()), + }, + ); + } + + annotations +} diff --git a/nym-api/src/node_status_api/cache/refresher.rs b/nym-api/src/node_status_api/cache/refresher.rs index ad07919e62..900051f911 100644 --- a/nym-api/src/node_status_api/cache/refresher.rs +++ b/nym-api/src/node_status_api/cache/refresher.rs @@ -2,12 +2,12 @@ // SPDX-License-Identifier: GPL-3.0-only use super::NodeStatusCache; +use crate::node_status_api::cache::node_sets::produce_node_annotations; use crate::{ node_status_api::cache::{ inclusion_probabilities::InclusionProbabilities, node_sets::{ - annotate_gateways_with_details, annotate_nodes_with_details, - split_into_active_and_rewarded_set, to_rewarded_set_node_status, + annotate_legacy_gateways_with_details, annotate_legacy_mixnodes_nodes_with_details, }, NodeStatusCacheError, }, @@ -16,9 +16,11 @@ use crate::{ support::caching::CacheNotification, }; use nym_task::TaskClient; +use std::collections::HashMap; use std::time::Duration; use tokio::sync::watch; use tokio::time; +use tracing::{debug, error, info, trace}; // Long running task responsible for keeping the node status cache up-to-date. pub struct NodeStatusCacheRefresher { @@ -56,14 +58,14 @@ impl NodeStatusCacheRefresher { tokio::select! { biased; _ = shutdown.recv() => { - log::trace!("NodeStatusCacheRefresher: Received shutdown"); + trace!("NodeStatusCacheRefresher: Received shutdown"); } // Update node status cache when the contract cache / validator cache is updated Ok(_) = self.contract_cache_listener.changed() => { tokio::select! { _ = self.update_on_notify(&mut fallback_interval) => (), _ = shutdown.recv() => { - log::trace!("NodeStatusCacheRefresher: Received shutdown"); + trace!("NodeStatusCacheRefresher: Received shutdown"); } } } @@ -73,18 +75,18 @@ impl NodeStatusCacheRefresher { tokio::select! { _ = self.update_on_timer() => (), _ = shutdown.recv() => { - log::trace!("NodeStatusCacheRefresher: Received shutdown"); + trace!("NodeStatusCacheRefresher: Received shutdown"); } } } } } - log::info!("NodeStatusCacheRefresher: Exiting"); + info!("NodeStatusCacheRefresher: Exiting"); } /// Updates the node status cache when the contract cache / validator cache is updated async fn update_on_notify(&self, fallback_interval: &mut time::Interval) { - log::debug!( + debug!( "Validator cache event detected: {:?}", &*self.contract_cache_listener.borrow(), ); @@ -94,31 +96,28 @@ impl NodeStatusCacheRefresher { /// Updates the node status cache when the fallback interval is reached async fn update_on_timer(&self) { - log::debug!("Timed trigger for the node status cache"); + debug!("Timed trigger for the node status cache"); let have_contract_cache_data = *self.contract_cache_listener.borrow() != CacheNotification::Start; if have_contract_cache_data { let _ = self.refresh().await; } else { - log::trace!( - "Skipping updating node status cache, is the contract cache not yet available?" - ); + trace!("Skipping updating node status cache, is the contract cache not yet available?"); } } /// Refreshes the node status cache by fetching the latest data from the contract cache async fn refresh(&self) -> Result<(), NodeStatusCacheError> { - log::info!("Updating node status cache"); + info!("Updating node status cache"); // Fetch contract cache data to work with - let mixnode_details = self.contract_cache.mixnodes_all().await; + let mixnode_details = self.contract_cache.legacy_mixnodes_all().await; let interval_reward_params = self.contract_cache.interval_reward_params().await; let current_interval = self.contract_cache.current_interval().await; - let rewarded_set = self.contract_cache.rewarded_set().await; - let active_set = self.contract_cache.active_set().await; - let mix_to_family = self.contract_cache.mix_to_family().await; - let gateway_bonds = self.contract_cache.gateways_all().await; + let rewarded_set = self.contract_cache.rewarded_set_owned().await; + let gateway_bonds = self.contract_cache.legacy_gateways_all().await; + let nym_nodes = self.contract_cache.nym_nodes().await; // get blacklists let mixnodes_blacklist = self.contract_cache.mixnodes_blacklist().await; @@ -138,24 +137,34 @@ impl NodeStatusCacheRefresher { NodeStatusCacheError::SimulationFailed })?; + let mut legacy_gateway_mapping = HashMap::new(); + for gateway in &gateway_bonds { + legacy_gateway_mapping.insert(gateway.identity().clone(), gateway.node_id); + } + // Create annotated data - let rewarded_set_node_status = to_rewarded_set_node_status(&rewarded_set, &active_set); - let mixnodes_annotated = annotate_nodes_with_details( + + let node_annotations = produce_node_annotations( + &self.storage, + &mixnode_details, + &gateway_bonds, + &nym_nodes, + &rewarded_set, + current_interval, + ) + .await; + + let mixnodes_annotated = annotate_legacy_mixnodes_nodes_with_details( &self.storage, mixnode_details, interval_reward_params, current_interval, - &rewarded_set_node_status, - mix_to_family.to_vec(), + &rewarded_set, &mixnodes_blacklist, ) .await; - // Create the annotated rewarded and active sets - let (rewarded_set, active_set) = - split_into_active_and_rewarded_set(&mixnodes_annotated, &rewarded_set_node_status); - - let gateways_annotated = annotate_gateways_with_details( + let gateways_annotated = annotate_legacy_gateways_with_details( &self.storage, gateway_bonds, current_interval, @@ -166,9 +175,9 @@ impl NodeStatusCacheRefresher { // Update the cache self.cache .update( + legacy_gateway_mapping, + node_annotations, mixnodes_annotated, - rewarded_set, - active_set, gateways_annotated, inclusion_probabilities, ) diff --git a/nym-api/src/node_status_api/handlers/mod.rs b/nym-api/src/node_status_api/handlers/mod.rs index 2a385b2812..f42cd59d84 100644 --- a/nym-api/src/node_status_api/handlers/mod.rs +++ b/nym-api/src/node_status_api/handlers/mod.rs @@ -1,9 +1,9 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::Router; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use serde::Deserialize; use utoipa::IntoParams; @@ -11,7 +11,7 @@ pub(crate) mod network_monitor; pub(crate) mod unstable; pub(crate) mod without_monitor; -pub(crate) fn node_status_routes(network_monitor: bool) -> Router { +pub(crate) fn node_status_routes(network_monitor: bool) -> Router { // in the minimal variant we would not have access to endpoints relying on existence // of the network monitor and the associated storage let without_network_monitor = without_monitor::mandatory_routes(); @@ -28,5 +28,5 @@ pub(crate) fn node_status_routes(network_monitor: bool) -> Router #[derive(Deserialize, IntoParams)] #[into_params(parameter_in = Path)] struct MixIdParam { - mix_id: MixId, + mix_id: NodeId, } diff --git a/nym-api/src/node_status_api/handlers/network_monitor.rs b/nym-api/src/node_status_api/handlers/network_monitor.rs index ea38422c17..5f71eac079 100644 --- a/nym-api/src/node_status_api/handlers/network_monitor.rs +++ b/nym-api/src/node_status_api/handlers/network_monitor.rs @@ -4,13 +4,13 @@ use crate::node_status_api::handlers::MixIdParam; use crate::node_status_api::helpers::{ _compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report, - _gateway_uptime_history, _get_gateway_avg_uptime, _get_gateways_detailed, - _get_gateways_detailed_unfiltered, _get_mixnode_avg_uptime, _get_mixnode_reward_estimation, - _get_mixnodes_detailed_unfiltered, _mixnode_core_status_count, _mixnode_report, - _mixnode_uptime_history, + _gateway_uptime_history, _get_gateway_avg_uptime, _get_legacy_gateways_detailed, + _get_legacy_gateways_detailed_unfiltered, _get_mixnode_avg_uptime, + _get_mixnode_reward_estimation, _get_mixnodes_detailed_unfiltered, _mixnode_core_status_count, + _mixnode_report, _mixnode_uptime_history, }; use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::extract::{Path, Query, State}; use axum::Json; use axum::Router; @@ -25,7 +25,9 @@ use utoipa::IntoParams; use super::unstable; -pub(super) fn network_monitor_routes() -> Router { +// we want to mark the routes as deprecated in swagger, but still expose them +#[allow(deprecated)] +pub(super) fn network_monitor_routes() -> Router { Router::new() .nest( "/gateway/:identity", @@ -92,9 +94,10 @@ pub(super) fn network_monitor_routes() -> Router { (status = 200, body = GatewayStatusReportResponse) ) )] +#[deprecated] async fn gateway_report( Path(identity): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _gateway_report(state.node_status_cache(), &identity).await?, @@ -109,12 +112,13 @@ async fn gateway_report( (status = 200, body = GatewayUptimeHistoryResponse) ) )] +#[deprecated] async fn gateway_uptime_history( Path(identity): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( - _gateway_uptime_history(state.storage(), &identity).await?, + _gateway_uptime_history(state.storage(), state.nym_contract_cache(), &identity).await?, )) } @@ -135,10 +139,11 @@ struct SinceQueryParams { (status = 200, body = GatewayCoreStatusResponse) ) )] +#[deprecated] async fn gateway_core_status_count( Path(identity): Path, Query(SinceQueryParams { since }): Query, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _gateway_core_status_count(state.storage(), &identity, since).await?, @@ -153,9 +158,10 @@ async fn gateway_core_status_count( (status = 200, body = GatewayUptimeResponse) ) )] +#[deprecated] async fn get_gateway_avg_uptime( Path(identity): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _get_gateway_avg_uptime(state.node_status_cache(), &identity).await?, @@ -173,9 +179,10 @@ async fn get_gateway_avg_uptime( (status = 200, body = MixnodeStatusReportResponse) ) )] +#[deprecated] async fn mixnode_report( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _mixnode_report(state.node_status_cache(), mix_id).await?, @@ -193,12 +200,13 @@ async fn mixnode_report( (status = 200, body = MixnodeUptimeHistoryResponse) ) )] +#[deprecated] async fn mixnode_uptime_history( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( - _mixnode_uptime_history(state.storage(), mix_id).await?, + _mixnode_uptime_history(state.storage(), state.nym_contract_cache(), mix_id).await?, )) } @@ -213,10 +221,11 @@ async fn mixnode_uptime_history( (status = 200, body = MixnodeCoreStatusResponse) ) )] +#[deprecated] async fn mixnode_core_status_count( Path(MixIdParam { mix_id }): Path, Query(SinceQueryParams { since }): Query, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _mixnode_core_status_count(state.storage(), mix_id, since).await?, @@ -234,9 +243,10 @@ async fn mixnode_core_status_count( (status = 200, body = RewardEstimationResponse) ) )] +#[deprecated] async fn get_mixnode_reward_estimation( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _get_mixnode_reward_estimation( @@ -260,9 +270,10 @@ async fn get_mixnode_reward_estimation( (status = 200, body = RewardEstimationResponse) ) )] +#[deprecated] async fn compute_mixnode_reward_estimation( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, Json(user_reward_param): Json, ) -> AxumResult> { Ok(Json( @@ -287,9 +298,10 @@ async fn compute_mixnode_reward_estimation( (status = 200, body = UptimeResponse) ) )] +#[deprecated] async fn get_mixnode_avg_uptime( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _get_mixnode_avg_uptime(state.node_status_cache(), mix_id).await?, @@ -304,8 +316,9 @@ async fn get_mixnode_avg_uptime( (status = 200, body = MixNodeBondAnnotated) ) )] +#[deprecated] pub async fn get_mixnodes_detailed_unfiltered( - State(state): State, + State(state): State, ) -> Json> { Json(_get_mixnodes_detailed_unfiltered(state.node_status_cache()).await) } @@ -318,10 +331,11 @@ pub async fn get_mixnodes_detailed_unfiltered( (status = 200, body = GatewayBondAnnotated) ) )] +#[deprecated] pub async fn get_gateways_detailed( - State(state): State, + State(state): State, ) -> Json> { - Json(_get_gateways_detailed(state.node_status_cache()).await) + Json(_get_legacy_gateways_detailed(state.node_status_cache()).await) } #[utoipa::path( @@ -332,8 +346,9 @@ pub async fn get_gateways_detailed( (status = 200, body = GatewayBondAnnotated) ) )] +#[deprecated] pub async fn get_gateways_detailed_unfiltered( - State(state): State, + State(state): State, ) -> Json> { - Json(_get_gateways_detailed_unfiltered(state.node_status_cache()).await) + Json(_get_legacy_gateways_detailed_unfiltered(state.node_status_cache()).await) } diff --git a/nym-api/src/node_status_api/handlers/unstable.rs b/nym-api/src/node_status_api/handlers/unstable.rs index b8a30639b4..2b72f72908 100644 --- a/nym-api/src/node_status_api/handlers/unstable.rs +++ b/nym-api/src/node_status_api/handlers/unstable.rs @@ -1,21 +1,23 @@ // Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node_status_api::handlers::MixIdParam; use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::support::http::helpers::PaginationRequest; +use crate::support::http::state::AppState; use crate::support::storage::NymApiStorage; -use crate::v2::AxumAppState; use axum::extract::{Path, Query, State}; use axum::Json; use nym_api_requests::models::{ GatewayTestResultResponse, MixnodeTestResultResponse, PartialTestResult, TestNode, TestRoute, }; use nym_api_requests::pagination::Pagination; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use std::cmp::min; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::RwLock; +use tracing::{error, trace}; pub type DbId = i64; @@ -103,7 +105,7 @@ const MAX_TEST_RESULTS_PAGE_SIZE: u32 = 100; const DEFAULT_TEST_RESULTS_PAGE_SIZE: u32 = 50; async fn _mixnode_test_results( - mix_id: MixId, + mix_id: NodeId, page: u32, per_page: u32, info_cache: &NodeInfoCache, @@ -161,10 +163,21 @@ async fn _mixnode_test_results( }) } +#[utoipa::path( + tag = "UNSTABLE - DO **NOT** USE", + get, + params( + MixIdParam, PaginationRequest + ), + path = "/v1/status/mixnodes/unstable/{mix_id}/test-results", + responses( + (status = 200, body = MixnodeTestResultResponse) + ) +)] pub async fn mixnode_test_results( - Path(mix_id): Path, + Path(mix_id): Path, Query(pagination): Query, - State(state): State, + State(state): State, ) -> AxumResult> { let page = pagination.page.unwrap_or_default(); let per_page = min( @@ -249,10 +262,21 @@ async fn _gateway_test_results( }) } +#[utoipa::path( + tag = "UNSTABLE - DO **NOT** USE", + get, + params( + PaginationRequest + ), + path = "/v1/status/gateways/unstable/{identity}/test-results", + responses( + (status = 200, body = GatewayTestResultResponse) + ) +)] pub async fn gateway_test_results( Path(gateway_identity): Path, Query(pagination): Query, - State(state): State, + State(state): State, ) -> AxumResult> { let page = pagination.page.unwrap_or_default(); let per_page = min( diff --git a/nym-api/src/node_status_api/handlers/without_monitor.rs b/nym-api/src/node_status_api/handlers/without_monitor.rs index b431d38913..ef87fa025c 100644 --- a/nym-api/src/node_status_api/handlers/without_monitor.rs +++ b/nym-api/src/node_status_api/handlers/without_monitor.rs @@ -3,34 +3,44 @@ use crate::node_status_api::handlers::MixIdParam; use crate::node_status_api::helpers::{ - _get_active_set_detailed, _get_mixnode_inclusion_probabilities, - _get_mixnode_inclusion_probability, _get_mixnode_stake_saturation, _get_mixnode_status, - _get_mixnodes_detailed, _get_rewarded_set_detailed, + _get_active_set_legacy_mixnodes_detailed, _get_legacy_mixnodes_detailed, + _get_mixnode_inclusion_probabilities, _get_mixnode_inclusion_probability, + _get_mixnode_stake_saturation, _get_mixnode_status, _get_rewarded_set_legacy_mixnodes_detailed, }; -use crate::node_status_api::models::AxumResult; -use crate::v2::AxumAppState; +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::support::http::state::AppState; use axum::extract::{Path, State}; +use axum::routing::{get, post}; use axum::Json; use axum::Router; use nym_api_requests::models::{ AllInclusionProbabilitiesResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, MixnodeStatusResponse, StakeSaturationResponse, }; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; +use nym_types::monitoring::MonitorMessage; +use tracing::error; -pub(super) fn mandatory_routes() -> Router { +// we want to mark the routes as deprecated in swagger, but still expose them +#[allow(deprecated)] +pub(super) fn mandatory_routes() -> Router { Router::new() + .route( + "/submit-gateway-monitoring-results", + post(submit_gateway_monitoring_results), + ) + .route( + "/submit-node-monitoring-results", + post(submit_node_monitoring_results), + ) .nest( "/mixnode/:mix_id", Router::new() - .route("/status", axum::routing::get(get_mixnode_status)) - .route( - "/stake-saturation", - axum::routing::get(get_mixnode_stake_saturation), - ) + .route("/status", get(get_mixnode_status)) + .route("/stake-saturation", get(get_mixnode_stake_saturation)) .route( "/inclusion-probability", - axum::routing::get(get_mixnode_inclusion_probability), + get(get_mixnode_inclusion_probability), ), ) .merge( @@ -39,21 +49,103 @@ pub(super) fn mandatory_routes() -> Router { Router::new() .route( "/inclusion-probability", - axum::routing::get(get_mixnode_inclusion_probabilities), - ) - .route("/detailed", axum::routing::get(get_mixnodes_detailed)) - .route( - "/rewarded/detailed", - axum::routing::get(get_rewarded_set_detailed), + get(get_mixnode_inclusion_probabilities), ) - .route( - "/active/detailed", - axum::routing::get(get_active_set_detailed), - ), + .route("/detailed", get(get_mixnodes_detailed)) + .route("/rewarded/detailed", get(get_rewarded_set_detailed)) + .route("/active/detailed", get(get_active_set_detailed)), ), ) } +#[utoipa::path( + tag = "status", + post, + path = "/v1/status/submit-gateway-monitoring-results", + responses( + (status = 200), + (status = 400, body = ErrorResponse, description = "TBD"), + (status = 403, body = ErrorResponse, description = "TBD"), + (status = 500, body = ErrorResponse, description = "TBD"), + ), +)] +pub(crate) async fn submit_gateway_monitoring_results( + State(state): State, + Json(message): Json, +) -> AxumResult<()> { + if !message.is_in_allowed() { + return Err(AxumErrorResponse::forbidden( + "Monitor not registered to submit results", + )); + } + + if !message.timely() { + return Err(AxumErrorResponse::bad_request("Message is too old")); + } + + if !message.verify() { + return Err(AxumErrorResponse::bad_request("invalid signature")); + } + + match state + .storage + .submit_gateway_statuses_v2(message.results()) + .await + { + Ok(_) => Ok(()), + Err(err) => { + error!("failed to submit gateway monitoring results: {err}"); + Err(AxumErrorResponse::internal_msg( + "failed to submit gateway monitoring results", + )) + } + } +} + +#[utoipa::path( + tag = "status", + post, + path = "/v1/status/submit-node-monitoring-results", + responses( + (status = 200), + (status = 400, body = ErrorResponse, description = "TBD"), + (status = 403, body = ErrorResponse, description = "TBD"), + (status = 500, body = ErrorResponse, description = "TBD"), + ), +)] +pub(crate) async fn submit_node_monitoring_results( + State(state): State, + Json(message): Json, +) -> AxumResult<()> { + if !message.is_in_allowed() { + return Err(AxumErrorResponse::forbidden( + "Monitor not registered to submit results", + )); + } + + if !message.timely() { + return Err(AxumErrorResponse::bad_request("Message is too old")); + } + + if !message.verify() { + return Err(AxumErrorResponse::bad_request("invalid signature")); + } + + match state + .storage + .submit_mixnode_statuses_v2(message.results()) + .await + { + Ok(_) => Ok(()), + Err(err) => { + error!("failed to submit node monitoring results: {err}"); + Err(AxumErrorResponse::internal_msg( + "failed to submit node monitoring results", + )) + } + } +} + #[utoipa::path( tag = "status", get, @@ -65,9 +157,10 @@ pub(super) fn mandatory_routes() -> Router { (status = 200, body = MixnodeStatusResponse) ) )] +#[deprecated] async fn get_mixnode_status( Path(MixIdParam { mix_id }): Path, - State(state): State, + State(state): State, ) -> Json { Json(_get_mixnode_status(state.nym_contract_cache(), mix_id).await) } @@ -83,9 +176,10 @@ async fn get_mixnode_status( (status = 200, body = StakeSaturationResponse) ) )] +#[deprecated] async fn get_mixnode_stake_saturation( - Path(mix_id): Path, - State(state): State, + Path(mix_id): Path, + State(state): State, ) -> AxumResult> { Ok(Json( _get_mixnode_stake_saturation( @@ -108,9 +202,10 @@ async fn get_mixnode_stake_saturation( (status = 200, body = InclusionProbabilityResponse) ) )] +#[deprecated] async fn get_mixnode_inclusion_probability( - Path(mix_id): Path, - State(state): State, + Path(mix_id): Path, + State(state): State, ) -> AxumResult> { Ok(Json( _get_mixnode_inclusion_probability(state.node_status_cache(), mix_id).await?, @@ -125,8 +220,9 @@ async fn get_mixnode_inclusion_probability( (status = 200, body = AllInclusionProbabilitiesResponse) ) )] +#[deprecated] async fn get_mixnode_inclusion_probabilities( - State(state): State, + State(state): State, ) -> AxumResult> { Ok(Json( _get_mixnode_inclusion_probabilities(state.node_status_cache()).await?, @@ -141,10 +237,11 @@ async fn get_mixnode_inclusion_probabilities( (status = 200, body = MixNodeBondAnnotated) ) )] +#[deprecated] pub async fn get_mixnodes_detailed( - State(state): State, + State(state): State, ) -> Json> { - Json(_get_mixnodes_detailed(state.node_status_cache()).await) + Json(_get_legacy_mixnodes_detailed(state.node_status_cache()).await) } #[utoipa::path( @@ -155,10 +252,17 @@ pub async fn get_mixnodes_detailed( (status = 200, body = MixNodeBondAnnotated) ) )] +#[deprecated] pub async fn get_rewarded_set_detailed( - State(state): State, + State(state): State, ) -> Json> { - Json(_get_rewarded_set_detailed(state.node_status_cache()).await) + Json( + _get_rewarded_set_legacy_mixnodes_detailed( + state.node_status_cache(), + state.nym_contract_cache(), + ) + .await, + ) } #[utoipa::path( @@ -169,8 +273,15 @@ pub async fn get_rewarded_set_detailed( (status = 200, body = MixNodeBondAnnotated) ) )] +#[deprecated] pub async fn get_active_set_detailed( - State(state): State, + State(state): State, ) -> Json> { - Json(_get_active_set_detailed(state.node_status_cache()).await) + Json( + _get_active_set_legacy_mixnodes_detailed( + state.node_status_cache(), + state.nym_contract_cache(), + ) + .await, + ) } diff --git a/nym-api/src/node_status_api/helpers.rs b/nym-api/src/node_status_api/helpers.rs index 0d706b1156..c3308eaee4 100644 --- a/nym-api/src/node_status_api/helpers.rs +++ b/nym-api/src/node_status_api/helpers.rs @@ -11,25 +11,62 @@ use nym_api_requests::models::{ AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated, GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, GatewayUptimeResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, - MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse, + MixnodeCoreStatusResponse, MixnodeStatus, MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; -use nym_mixnet_contract_common::{MixId, RewardedSetNodeStatus}; +use nym_mixnet_contract_common::NodeId; -async fn get_gateway_bond_annotated( +pub(crate) enum RewardedSetStatus { + Active, + Standby, + Inactive, +} + +impl From for RewardedSetStatus { + fn from(value: MixnodeStatus) -> Self { + match value { + MixnodeStatus::Active => RewardedSetStatus::Active, + MixnodeStatus::Standby => RewardedSetStatus::Standby, + // for all intents and purposes, missing node is treated as inactive for rewarding (since it wouldn't get anything + MixnodeStatus::Inactive => RewardedSetStatus::Inactive, + MixnodeStatus::NotFound => RewardedSetStatus::Inactive, + } + } +} + +async fn gateway_identity_to_node_id( cache: &NodeStatusCache, identity: &str, +) -> AxumResult { + let node_id = cache + .map_identity_to_node_id(identity) + .await + .ok_or(AxumErrorResponse::not_found("gateway bond not found"))?; + Ok(node_id) +} + +async fn get_gateway_bond_annotated( + cache: &NodeStatusCache, + node_id: NodeId, ) -> AxumResult { cache - .gateway_annotated(identity) + .gateway_annotated(node_id) .await .ok_or(AxumErrorResponse::not_found("gateway bond not found")) } +async fn get_gateway_bond_annotated_by_identity( + cache: &NodeStatusCache, + identity: &str, +) -> AxumResult { + let node_id = gateway_identity_to_node_id(cache, identity).await?; + get_gateway_bond_annotated(cache, node_id).await +} + async fn get_mixnode_bond_annotated( cache: &NodeStatusCache, - mix_id: MixId, + mix_id: NodeId, ) -> AxumResult { cache .mixnode_annotated(mix_id) @@ -41,7 +78,7 @@ pub(crate) async fn _gateway_report( cache: &NodeStatusCache, identity: &str, ) -> AxumResult { - let gateway = get_gateway_bond_annotated(cache, identity).await?; + let gateway = get_gateway_bond_annotated_by_identity(cache, identity).await?; Ok(GatewayStatusReportResponse { identity: gateway.identity().to_owned(), @@ -54,13 +91,24 @@ pub(crate) async fn _gateway_report( pub(crate) async fn _gateway_uptime_history( storage: &NymApiStorage, + nym_contract_cache: &NymContractCache, identity: &str, ) -> AxumResult { - storage - .get_gateway_uptime_history(identity) + let history = storage + .get_gateway_uptime_history_by_identity(identity) .await - .map(GatewayUptimeHistoryResponse::from) - .map_err(AxumErrorResponse::not_found) + .map_err(AxumErrorResponse::not_found)?; + + let owner = nym_contract_cache + .legacy_gateway_owner(history.node_id) + .await + .ok_or_else(|| AxumErrorResponse::not_found("could not determine gateway owner"))?; + + Ok(GatewayUptimeHistoryResponse { + identity: history.identity, + owner, + history: history.history.into_iter().map(Into::into).collect(), + }) } pub(crate) async fn _gateway_core_status_count( @@ -69,7 +117,7 @@ pub(crate) async fn _gateway_core_status_count( since: Option, ) -> AxumResult { let count = storage - .get_core_gateway_status_count(identity, since) + .get_core_gateway_status_count_by_identity(identity, since) .await .map_err(AxumErrorResponse::not_found)?; @@ -81,7 +129,7 @@ pub(crate) async fn _gateway_core_status_count( pub(crate) async fn _mixnode_report( cache: &NodeStatusCache, - mix_id: MixId, + mix_id: NodeId, ) -> AxumResult { let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; @@ -97,18 +145,30 @@ pub(crate) async fn _mixnode_report( pub(crate) async fn _mixnode_uptime_history( storage: &NymApiStorage, - mix_id: MixId, + nym_contract_cache: &NymContractCache, + mix_id: NodeId, ) -> AxumResult { - storage + let history = storage .get_mixnode_uptime_history(mix_id) .await - .map(MixnodeUptimeHistoryResponse::from) - .map_err(AxumErrorResponse::not_found) + .map_err(AxumErrorResponse::not_found)?; + + let owner = nym_contract_cache + .legacy_gateway_owner(mix_id) + .await + .ok_or_else(|| AxumErrorResponse::not_found("could not determine mixnode owner"))?; + + Ok(MixnodeUptimeHistoryResponse { + mix_id, + identity: history.identity, + owner, + history: history.history.into_iter().map(Into::into).collect(), + }) } pub(crate) async fn _mixnode_core_status_count( storage: &NymApiStorage, - mix_id: MixId, + mix_id: NodeId, since: Option, ) -> AxumResult { let count = storage @@ -121,7 +181,7 @@ pub(crate) async fn _mixnode_core_status_count( pub(crate) async fn _get_mixnode_status( cache: &NymContractCache, - mix_id: MixId, + mix_id: NodeId, ) -> MixnodeStatusResponse { MixnodeStatusResponse { status: cache.mixnode_status(mix_id).await, @@ -129,157 +189,161 @@ pub(crate) async fn _get_mixnode_status( } pub(crate) async fn _get_mixnode_reward_estimation( - cache: &NodeStatusCache, - validator_cache: &NymContractCache, - mix_id: MixId, + status_cache: &NodeStatusCache, + contract_cache: &NymContractCache, + mix_id: NodeId, ) -> AxumResult { - let (mixnode, status) = cache.mixnode_details(mix_id).await; - if let Some(mixnode) = mixnode { - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let reward_params = reward_params - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; - let current_interval = validator_cache - .current_interval() - .await - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; - - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - mixnode.performance, - status.into(), - reward_params, - current_interval, - ); - - Ok(RewardEstimationResponse { - estimation: reward_estimation, - reward_params, - epoch: current_interval, - as_at: as_at.unix_timestamp(), - }) - } else { - Err(AxumErrorResponse::not_found("mixnode bond not found")) - } + let status = contract_cache.mixnode_status(mix_id).await; + let mixnode = status_cache + .mixnode_annotated(mix_id) + .await + .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; + + let reward_params = contract_cache.interval_reward_params().await; + let as_at = reward_params.timestamp(); + let reward_params = reward_params + .into_inner() + .ok_or_else(AxumErrorResponse::internal)?; + let current_interval = contract_cache + .current_interval() + .await + .into_inner() + .ok_or_else(AxumErrorResponse::internal)?; + + let reward_estimation = compute_reward_estimate( + &mixnode.mixnode_details, + mixnode.performance, + status.into(), + reward_params, + current_interval, + ); + + Ok(RewardEstimationResponse { + estimation: reward_estimation, + reward_params, + epoch: current_interval, + as_at: as_at.unix_timestamp(), + }) } pub(crate) async fn _compute_mixnode_reward_estimation( user_reward_param: &ComputeRewardEstParam, - cache: &NodeStatusCache, - validator_cache: &NymContractCache, - mix_id: MixId, + status_cache: &NodeStatusCache, + contract_cache: &NymContractCache, + mix_id: NodeId, ) -> AxumResult { - let (mixnode, actual_status) = cache.mixnode_details(mix_id).await; - if let Some(mut mixnode) = mixnode { - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let reward_params = reward_params - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; - let current_interval = validator_cache - .current_interval() - .await - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; - - // For these parameters we either use the provided ones, or fall back to the system ones - let performance = user_reward_param.performance.unwrap_or(mixnode.performance); - - let status = match user_reward_param.active_in_rewarded_set { - Some(true) => Some(RewardedSetNodeStatus::Active), - Some(false) => Some(RewardedSetNodeStatus::Standby), - None => actual_status.into(), - }; - - if let Some(pledge_amount) = user_reward_param.pledge_amount { - mixnode.mixnode_details.rewarding_details.operator = - Decimal::from_ratio(pledge_amount, 1u64); - } - if let Some(total_delegation) = user_reward_param.total_delegation { - mixnode.mixnode_details.rewarding_details.delegates = - Decimal::from_ratio(total_delegation, 1u64); - } + let mut mixnode = status_cache + .mixnode_annotated(mix_id) + .await + .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; - if let Some(profit_margin_percent) = user_reward_param.profit_margin_percent { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .profit_margin_percent = profit_margin_percent; - } + let reward_params = contract_cache.interval_reward_params().await; + let as_at = reward_params.timestamp(); + let reward_params = reward_params + .into_inner() + .ok_or_else(AxumErrorResponse::internal)?; + let current_interval = contract_cache + .current_interval() + .await + .into_inner() + .ok_or_else(AxumErrorResponse::internal)?; - if let Some(interval_operating_cost) = &user_reward_param.interval_operating_cost { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .interval_operating_cost = interval_operating_cost.clone(); - } + // For these parameters we either use the provided ones, or fall back to the system ones + let performance = user_reward_param.performance.unwrap_or(mixnode.performance); - if mixnode.mixnode_details.rewarding_details.operator - + mixnode.mixnode_details.rewarding_details.delegates - > reward_params.interval.staking_supply - { - return Err(AxumErrorResponse::unprocessable_entity( - "Pledge plus delegation too large", - )); + let status = match user_reward_param.active_in_rewarded_set { + Some(true) => RewardedSetStatus::Active, + Some(false) => RewardedSetStatus::Standby, + None => { + let actual_status = contract_cache.mixnode_status(mix_id).await; + actual_status.into() } + }; - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - performance, - status, - reward_params, - current_interval, - ); - - Ok(RewardEstimationResponse { - estimation: reward_estimation, - reward_params, - epoch: current_interval, - as_at: as_at.unix_timestamp(), - }) - } else { - Err(AxumErrorResponse::not_found("mixnode bond not found")) + if let Some(pledge_amount) = user_reward_param.pledge_amount { + mixnode.mixnode_details.rewarding_details.operator = + Decimal::from_ratio(pledge_amount, 1u64); + } + if let Some(total_delegation) = user_reward_param.total_delegation { + mixnode.mixnode_details.rewarding_details.delegates = + Decimal::from_ratio(total_delegation, 1u64); + } + + if let Some(profit_margin_percent) = user_reward_param.profit_margin_percent { + mixnode + .mixnode_details + .rewarding_details + .cost_params + .profit_margin_percent = profit_margin_percent; + } + + if let Some(interval_operating_cost) = &user_reward_param.interval_operating_cost { + mixnode + .mixnode_details + .rewarding_details + .cost_params + .interval_operating_cost = interval_operating_cost.clone(); } + + if mixnode.mixnode_details.rewarding_details.operator + + mixnode.mixnode_details.rewarding_details.delegates + > reward_params.interval.staking_supply + { + return Err(AxumErrorResponse::unprocessable_entity( + "Pledge plus delegation too large", + )); + } + + let reward_estimation = compute_reward_estimate( + &mixnode.mixnode_details, + performance, + status, + reward_params, + current_interval, + ); + + Ok(RewardEstimationResponse { + estimation: reward_estimation, + reward_params, + epoch: current_interval, + as_at: as_at.unix_timestamp(), + }) } pub(crate) async fn _get_mixnode_stake_saturation( - cache: &NodeStatusCache, - validator_cache: &NymContractCache, - mix_id: MixId, + status_cache: &NodeStatusCache, + contract_cache: &NymContractCache, + mix_id: NodeId, ) -> AxumResult { - let (mixnode, _) = cache.mixnode_details(mix_id).await; - if let Some(mixnode) = mixnode { - // Recompute the stake saturation just so that we can confidently state that the `as_at` - // field is consistent and correct. Luckily this is very cheap. - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let rewarding_params = reward_params - .into_inner() - .ok_or_else(AxumErrorResponse::internal)?; - - Ok(StakeSaturationResponse { - saturation: mixnode - .mixnode_details - .rewarding_details - .bond_saturation(&rewarding_params), - uncapped_saturation: mixnode - .mixnode_details - .rewarding_details - .uncapped_bond_saturation(&rewarding_params), - as_at: as_at.unix_timestamp(), - }) - } else { - Err(AxumErrorResponse::not_found("mixnode bond not found")) - } + let mixnode = status_cache + .mixnode_annotated(mix_id) + .await + .ok_or_else(|| AxumErrorResponse::not_found("mixnode bond not found"))?; + + // Recompute the stake saturation just so that we can confidently state that the `as_at` + // field is consistent and correct. Luckily this is very cheap. + let reward_params = contract_cache.interval_reward_params().await; + let as_at = reward_params.timestamp(); + let rewarding_params = reward_params + .into_inner() + .ok_or_else(AxumErrorResponse::internal)?; + + Ok(StakeSaturationResponse { + saturation: mixnode + .mixnode_details + .rewarding_details + .bond_saturation(&rewarding_params), + uncapped_saturation: mixnode + .mixnode_details + .rewarding_details + .uncapped_bond_saturation(&rewarding_params), + as_at: as_at.unix_timestamp(), + }) } pub(crate) async fn _get_mixnode_inclusion_probability( cache: &NodeStatusCache, - mix_id: MixId, + mix_id: NodeId, ) -> AxumResult { cache .inclusion_probabilities() @@ -295,7 +359,7 @@ pub(crate) async fn _get_mixnode_inclusion_probability( pub(crate) async fn _get_mixnode_avg_uptime( cache: &NodeStatusCache, - mix_id: MixId, + mix_id: NodeId, ) -> AxumResult { let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; @@ -310,7 +374,7 @@ pub(crate) async fn _get_gateway_avg_uptime( cache: &NodeStatusCache, identity: &str, ) -> AxumResult { - let gateway = get_gateway_bond_annotated(cache, identity).await?; + let gateway = get_gateway_bond_annotated_by_identity(cache, identity).await?; Ok(GatewayUptimeResponse { identity: identity.to_string(), @@ -338,7 +402,9 @@ pub(crate) async fn _get_mixnode_inclusion_probabilities( } } -pub(crate) async fn _get_mixnodes_detailed(cache: &NodeStatusCache) -> Vec { +pub(crate) async fn _get_legacy_mixnodes_detailed( + cache: &NodeStatusCache, +) -> Vec { cache .mixnodes_annotated_filtered() .await @@ -351,32 +417,50 @@ pub(crate) async fn _get_mixnodes_detailed_unfiltered( cache.mixnodes_annotated_full().await.unwrap_or_default() } -pub(crate) async fn _get_rewarded_set_detailed( - cache: &NodeStatusCache, +pub(crate) async fn _get_rewarded_set_legacy_mixnodes_detailed( + status_cache: &NodeStatusCache, + contract_cache: &NymContractCache, ) -> Vec { - cache - .rewarded_set_annotated() - .await - .unwrap_or_default() - .into_inner() + let Some(rewarded_set) = contract_cache.rewarded_set().await else { + return Vec::new(); + }; + let Some(mixnodes) = status_cache.mixnodes_annotated_full().await else { + return Vec::new(); + }; + mixnodes + .into_iter() + .filter(|m| { + rewarded_set.is_active_mixnode(&m.mix_id()) || rewarded_set.is_standby(&m.mix_id()) + }) + .collect() } -pub(crate) async fn _get_active_set_detailed(cache: &NodeStatusCache) -> Vec { - cache - .active_set_annotated() - .await - .unwrap_or_default() - .into_inner() +pub(crate) async fn _get_active_set_legacy_mixnodes_detailed( + status_cache: &NodeStatusCache, + contract_cache: &NymContractCache, +) -> Vec { + let Some(rewarded_set) = contract_cache.rewarded_set().await else { + return Vec::new(); + }; + let Some(mixnodes) = status_cache.mixnodes_annotated_full().await else { + return Vec::new(); + }; + mixnodes + .into_iter() + .filter(|m| rewarded_set.is_active_mixnode(&m.mix_id())) + .collect() } -pub(crate) async fn _get_gateways_detailed(cache: &NodeStatusCache) -> Vec { +pub(crate) async fn _get_legacy_gateways_detailed( + cache: &NodeStatusCache, +) -> Vec { cache .gateways_annotated_filtered() .await .unwrap_or_default() } -pub(crate) async fn _get_gateways_detailed_unfiltered( +pub(crate) async fn _get_legacy_gateways_detailed_unfiltered( cache: &NodeStatusCache, ) -> Vec { cache.gateways_annotated_full().await.unwrap_or_default() diff --git a/nym-api/src/node_status_api/helpers_deprecated.rs b/nym-api/src/node_status_api/helpers_deprecated.rs deleted file mode 100644 index 0788cd2cb3..0000000000 --- a/nym-api/src/node_status_api/helpers_deprecated.rs +++ /dev/null @@ -1,405 +0,0 @@ -// Copyright 2021-2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_status_api::models::RocketErrorResponse; -use crate::storage::NymApiStorage; -use crate::support::caching::Cache; -use crate::{NodeStatusCache, NymContractCache}; -use cosmwasm_std::Decimal; -use nym_api_requests::models::{ - AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated, - GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, - GatewayUptimeResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, - MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse, - MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, - UptimeResponse, -}; -use nym_mixnet_contract_common::{MixId, RewardedSetNodeStatus}; -use rocket::http::Status; -use rocket::State; - -use super::reward_estimate::compute_reward_estimate; - -async fn get_gateway_bond_annotated( - cache: &NodeStatusCache, - identity: &str, -) -> Result { - cache - .gateway_annotated(identity) - .await - .ok_or(RocketErrorResponse::new( - "gateway bond not found", - Status::NotFound, - )) -} - -async fn get_mixnode_bond_annotated( - cache: &NodeStatusCache, - mix_id: MixId, -) -> Result { - cache - .mixnode_annotated(mix_id) - .await - .ok_or(RocketErrorResponse::new( - "mixnode bond not found", - Status::NotFound, - )) -} - -pub(crate) async fn _gateway_report( - cache: &NodeStatusCache, - identity: &str, -) -> Result { - let gateway = get_gateway_bond_annotated(cache, identity).await?; - - Ok(GatewayStatusReportResponse { - identity: gateway.identity().to_owned(), - owner: gateway.owner().to_string(), - most_recent: gateway.node_performance.most_recent.round_to_integer(), - last_hour: gateway.node_performance.last_hour.round_to_integer(), - last_day: gateway.node_performance.last_24h.round_to_integer(), - }) -} - -pub(crate) async fn _gateway_uptime_history( - storage: &NymApiStorage, - identity: &str, -) -> Result { - storage - .get_gateway_uptime_history(identity) - .await - .map(GatewayUptimeHistoryResponse::from) - .map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound)) -} - -pub(crate) async fn _gateway_core_status_count( - storage: &State, - identity: &str, - since: Option, -) -> Result { - let count = storage - .get_core_gateway_status_count(identity, since) - .await - .map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound))?; - - Ok(GatewayCoreStatusResponse { - identity: identity.to_string(), - count, - }) -} - -pub(crate) async fn _mixnode_report( - cache: &NodeStatusCache, - mix_id: MixId, -) -> Result { - let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; - - Ok(MixnodeStatusReportResponse { - mix_id, - identity: mixnode.identity_key().to_owned(), - owner: mixnode.owner().to_string(), - most_recent: mixnode.node_performance.most_recent.round_to_integer(), - last_hour: mixnode.node_performance.last_hour.round_to_integer(), - last_day: mixnode.node_performance.last_24h.round_to_integer(), - }) -} - -pub(crate) async fn _mixnode_uptime_history( - storage: &NymApiStorage, - mix_id: MixId, -) -> Result { - storage - .get_mixnode_uptime_history(mix_id) - .await - .map(MixnodeUptimeHistoryResponse::from) - .map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound)) -} - -pub(crate) async fn _mixnode_core_status_count( - storage: &State, - mix_id: MixId, - since: Option, -) -> Result { - let count = storage - .get_core_mixnode_status_count(mix_id, since) - .await - .map_err(|err| RocketErrorResponse::new(err.to_string(), Status::NotFound))?; - - Ok(MixnodeCoreStatusResponse { mix_id, count }) -} - -pub(crate) async fn _get_mixnode_status( - cache: &NymContractCache, - mix_id: MixId, -) -> MixnodeStatusResponse { - MixnodeStatusResponse { - status: cache.mixnode_status(mix_id).await, - } -} - -pub(crate) async fn _get_mixnode_reward_estimation( - cache: &State, - validator_cache: &State, - mix_id: MixId, -) -> Result { - let (mixnode, status) = cache.mixnode_details(mix_id).await; - if let Some(mixnode) = mixnode { - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let reward_params = reward_params - .into_inner() - .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; - let current_interval = validator_cache - .current_interval() - .await - .into_inner() - .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; - - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - mixnode.performance, - status.into(), - reward_params, - current_interval, - ); - - Ok(RewardEstimationResponse { - estimation: reward_estimation, - reward_params, - epoch: current_interval, - as_at: as_at.unix_timestamp(), - }) - } else { - Err(RocketErrorResponse::new( - "mixnode bond not found", - Status::NotFound, - )) - } -} - -pub(crate) async fn _compute_mixnode_reward_estimation( - user_reward_param: ComputeRewardEstParam, - cache: &NodeStatusCache, - validator_cache: &NymContractCache, - mix_id: MixId, -) -> Result { - let (mixnode, actual_status) = cache.mixnode_details(mix_id).await; - if let Some(mut mixnode) = mixnode { - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let reward_params = reward_params - .into_inner() - .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; - let current_interval = validator_cache - .current_interval() - .await - .into_inner() - .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; - - // For these parameters we either use the provided ones, or fall back to the system ones - let performance = user_reward_param.performance.unwrap_or(mixnode.performance); - - let status = match user_reward_param.active_in_rewarded_set { - Some(true) => Some(RewardedSetNodeStatus::Active), - Some(false) => Some(RewardedSetNodeStatus::Standby), - None => actual_status.into(), - }; - - if let Some(pledge_amount) = user_reward_param.pledge_amount { - mixnode.mixnode_details.rewarding_details.operator = - Decimal::from_ratio(pledge_amount, 1u64); - } - if let Some(total_delegation) = user_reward_param.total_delegation { - mixnode.mixnode_details.rewarding_details.delegates = - Decimal::from_ratio(total_delegation, 1u64); - } - - if let Some(profit_margin_percent) = user_reward_param.profit_margin_percent { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .profit_margin_percent = profit_margin_percent; - } - - if let Some(interval_operating_cost) = user_reward_param.interval_operating_cost { - mixnode - .mixnode_details - .rewarding_details - .cost_params - .interval_operating_cost = interval_operating_cost; - } - - if mixnode.mixnode_details.rewarding_details.operator - + mixnode.mixnode_details.rewarding_details.delegates - > reward_params.interval.staking_supply - { - return Err(RocketErrorResponse::new( - "Pledge plus delegation too large", - Status::UnprocessableEntity, - )); - } - - let reward_estimation = compute_reward_estimate( - &mixnode.mixnode_details, - performance, - status, - reward_params, - current_interval, - ); - - Ok(RewardEstimationResponse { - estimation: reward_estimation, - reward_params, - epoch: current_interval, - as_at: as_at.unix_timestamp(), - }) - } else { - Err(RocketErrorResponse::new( - "mixnode bond not found", - Status::NotFound, - )) - } -} - -pub(crate) async fn _get_mixnode_stake_saturation( - cache: &NodeStatusCache, - validator_cache: &NymContractCache, - mix_id: MixId, -) -> Result { - let (mixnode, _) = cache.mixnode_details(mix_id).await; - if let Some(mixnode) = mixnode { - // Recompute the stake saturation just so that we can confidently state that the `as_at` - // field is consistent and correct. Luckily this is very cheap. - let reward_params = validator_cache.interval_reward_params().await; - let as_at = reward_params.timestamp(); - let rewarding_params = reward_params - .into_inner() - .ok_or_else(|| RocketErrorResponse::new("server error", Status::InternalServerError))?; - - Ok(StakeSaturationResponse { - saturation: mixnode - .mixnode_details - .rewarding_details - .bond_saturation(&rewarding_params), - uncapped_saturation: mixnode - .mixnode_details - .rewarding_details - .uncapped_bond_saturation(&rewarding_params), - as_at: as_at.unix_timestamp(), - }) - } else { - Err(RocketErrorResponse::new( - "mixnode bond not found", - Status::NotFound, - )) - } -} - -pub(crate) async fn _get_mixnode_inclusion_probability( - cache: &NodeStatusCache, - mix_id: MixId, -) -> Result { - cache - .inclusion_probabilities() - .await - .map(Cache::into_inner) - .and_then(|p| p.node(mix_id).cloned()) - .map(|p| InclusionProbabilityResponse { - in_active: p.in_active.into(), - in_reserve: p.in_reserve.into(), - }) - .ok_or_else(|| RocketErrorResponse::new("mixnode bond not found", Status::NotFound)) -} - -pub(crate) async fn _get_mixnode_avg_uptime( - cache: &NodeStatusCache, - mix_id: MixId, -) -> Result { - let mixnode = get_mixnode_bond_annotated(cache, mix_id).await?; - - Ok(UptimeResponse { - mix_id, - avg_uptime: mixnode.node_performance.last_24h.round_to_integer(), - node_performance: mixnode.node_performance, - }) -} - -pub(crate) async fn _get_gateway_avg_uptime( - cache: &NodeStatusCache, - identity: &str, -) -> Result { - let gateway = get_gateway_bond_annotated(cache, identity).await?; - - Ok(GatewayUptimeResponse { - identity: identity.to_string(), - avg_uptime: gateway.node_performance.last_24h.round_to_integer(), - node_performance: gateway.node_performance, - }) -} - -pub(crate) async fn _get_mixnode_inclusion_probabilities( - cache: &NodeStatusCache, -) -> Result { - if let Some(prob) = cache.inclusion_probabilities().await { - let as_at = prob.timestamp(); - let prob = prob.into_inner(); - Ok(AllInclusionProbabilitiesResponse { - inclusion_probabilities: prob.inclusion_probabilities, - samples: prob.samples, - elapsed: prob.elapsed, - delta_max: prob.delta_max, - delta_l2: prob.delta_l2, - as_at: as_at.unix_timestamp(), - }) - } else { - Err(RocketErrorResponse::new( - "No data available", - Status::ServiceUnavailable, - )) - } -} - -pub(crate) async fn _get_mixnodes_detailed(cache: &NodeStatusCache) -> Vec { - cache - .mixnodes_annotated_filtered() - .await - .unwrap_or_default() -} - -pub(crate) async fn _get_mixnodes_detailed_unfiltered( - cache: &NodeStatusCache, -) -> Vec { - cache.mixnodes_annotated_full().await.unwrap_or_default() -} - -pub(crate) async fn _get_rewarded_set_detailed( - cache: &NodeStatusCache, -) -> Vec { - cache - .rewarded_set_annotated() - .await - .unwrap_or_default() - .into_inner() -} - -pub(crate) async fn _get_active_set_detailed(cache: &NodeStatusCache) -> Vec { - cache - .active_set_annotated() - .await - .unwrap_or_default() - .into_inner() -} - -pub(crate) async fn _get_gateways_detailed(cache: &NodeStatusCache) -> Vec { - cache - .gateways_annotated_filtered() - .await - .unwrap_or_default() -} - -pub(crate) async fn _get_gateways_detailed_unfiltered( - cache: &NodeStatusCache, -) -> Vec { - cache.gateways_annotated_full().await.unwrap_or_default() -} diff --git a/nym-api/src/node_status_api/mod.rs b/nym-api/src/node_status_api/mod.rs index 5f50e62964..942a476467 100644 --- a/nym-api/src/node_status_api/mod.rs +++ b/nym-api/src/node_status_api/mod.rs @@ -9,20 +9,13 @@ use crate::{ }; pub(crate) use cache::NodeStatusCache; use nym_task::TaskManager; -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::{openapi_get_routes_spec, settings::OpenApiSettings}; use std::time::Duration; pub(crate) mod cache; -#[cfg(feature = "axum")] pub(crate) mod handlers; -#[cfg(feature = "axum")] pub(crate) mod helpers; -pub(crate) mod helpers_deprecated; pub(crate) mod models; pub(crate) mod reward_estimate; -pub(crate) mod routes_deprecated; pub(crate) mod uptime_updater; pub(crate) mod utils; @@ -30,52 +23,6 @@ pub(crate) const FIFTEEN_MINUTES: Duration = Duration::from_secs(900); pub(crate) const ONE_HOUR: Duration = Duration::from_secs(3600); pub(crate) const ONE_DAY: Duration = Duration::from_secs(86400); -pub(crate) fn node_status_routes( - settings: &OpenApiSettings, - enabled: bool, -) -> (Vec, OpenApi) { - if enabled { - openapi_get_routes_spec![ - settings: routes_deprecated::gateway_report, - routes_deprecated::gateway_uptime_history, - routes_deprecated::gateway_core_status_count, - routes_deprecated::mixnode_report, - routes_deprecated::mixnode_uptime_history, - routes_deprecated::mixnode_core_status_count, - routes_deprecated::get_mixnode_status, - routes_deprecated::get_mixnode_reward_estimation, - routes_deprecated::compute_mixnode_reward_estimation, - routes_deprecated::get_mixnode_stake_saturation, - routes_deprecated::get_mixnode_inclusion_probability, - routes_deprecated::get_mixnode_avg_uptime, - routes_deprecated::get_gateway_avg_uptime, - routes_deprecated::get_mixnode_inclusion_probabilities, - routes_deprecated::get_mixnodes_detailed, - routes_deprecated::get_mixnodes_detailed_unfiltered, - routes_deprecated::get_rewarded_set_detailed, - routes_deprecated::get_active_set_detailed, - routes_deprecated::get_gateways_detailed, - routes_deprecated::get_gateways_detailed_unfiltered, - routes_deprecated::unstable::mixnode_test_results, - routes_deprecated::unstable::gateway_test_results, - routes_deprecated::submit_gateway_monitoring_results, - routes_deprecated::submit_node_monitoring_results, - ] - } else { - // in the minimal variant we would not have access to endpoints relying on existence - // of the network monitor and the associated storage - openapi_get_routes_spec![ - settings: routes_deprecated::get_mixnode_status, - routes_deprecated::get_mixnode_stake_saturation, - routes_deprecated::get_mixnode_inclusion_probability, - routes_deprecated::get_mixnode_inclusion_probabilities, - routes_deprecated::get_mixnodes_detailed, - routes_deprecated::get_rewarded_set_detailed, - routes_deprecated::get_active_set_detailed, - ] - } -} - /// Spawn the node status cache refresher. /// /// It is primarily refreshed in-sync with the nym contract cache, however provide a fallback diff --git a/nym-api/src/node_status_api/models.rs b/nym-api/src/node_status_api/models.rs index 480fae65d2..7fd879982b 100644 --- a/nym-api/src/node_status_api/models.rs +++ b/nym-api/src/node_status_api/models.rs @@ -1,29 +1,26 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::ecash::error::{EcashError, RedemptionError}; use crate::node_status_api::utils::NodeUptimes; use crate::storage::models::NodeStatus; +use crate::support::caching::cache::UninitialisedCache; use nym_api_requests::models::{ - GatewayStatusReportResponse, GatewayUptimeHistoryResponse, HistoricalUptimeResponse, - MixnodeStatusReportResponse, MixnodeUptimeHistoryResponse, NodePerformance, RequestError, + HistoricalPerformanceResponse, HistoricalUptimeResponse, NodePerformance, + OldHistoricalUptimeResponse, RequestError, }; +use nym_contracts_common::NaiveFloat; use nym_mixnet_contract_common::reward_params::Performance; -use nym_mixnet_contract_common::{IdentityKey, MixId}; -use okapi::openapi3::{Responses, SchemaObject}; -use rocket::http::Status; -use rocket::response::{self, Responder, Response}; -use rocket::serde::json::Json; -use rocket::Request; -use rocket_okapi::gen::OpenApiGenerator; -use rocket_okapi::response::OpenApiResponderInner; -use rocket_okapi::util::ensure_status_code_exists; -use schemars::gen::SchemaGenerator; -use schemars::schema::{InstanceType, Schema}; +use nym_mixnet_contract_common::{IdentityKey, NodeId}; +use nym_serde_helpers::date::DATE_FORMAT; +use reqwest::StatusCode; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; - +use sqlx::Error; +use std::fmt::Display; use thiserror::Error; -use time::OffsetDateTime; +use time::{Date, OffsetDateTime}; +use tracing::error; #[derive(Error, Debug)] #[error("Received uptime value was within 0-100 range (got {received})")] @@ -40,6 +37,10 @@ impl Uptime { Uptime(0) } + pub fn is_zero(&self) -> bool { + self.0 == 0 + } + pub fn new(uptime: f32) -> Self { if uptime > 100f32 { error!("Got uptime {}, max is 100, returning 0", uptime); @@ -128,9 +129,8 @@ impl From for Performance { #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct MixnodeStatusReport { - pub(crate) mix_id: MixId, + pub(crate) mix_id: NodeId, pub(crate) identity: IdentityKey, - pub(crate) owner: String, pub(crate) most_recent: Uptime, @@ -141,9 +141,8 @@ pub struct MixnodeStatusReport { impl MixnodeStatusReport { pub(crate) fn construct_from_last_day_reports( report_time: OffsetDateTime, - mix_id: MixId, + mix_id: NodeId, identity: IdentityKey, - owner: String, last_day: Vec, last_hour_test_runs: usize, last_day_test_runs: usize, @@ -158,7 +157,6 @@ impl MixnodeStatusReport { MixnodeStatusReport { mix_id, identity, - owner, most_recent: node_uptimes.most_recent, last_hour: node_uptimes.last_hour, last_day: node_uptimes.last_day, @@ -166,19 +164,6 @@ impl MixnodeStatusReport { } } -impl From for MixnodeStatusReportResponse { - fn from(status: MixnodeStatusReport) -> Self { - MixnodeStatusReportResponse { - mix_id: status.mix_id, - identity: status.identity, - owner: status.owner, - most_recent: status.most_recent.0, - last_hour: status.last_hour.0, - last_day: status.last_day.0, - } - } -} - impl From for NodePerformance { fn from(report: MixnodeStatusReport) -> Self { NodePerformance { @@ -191,8 +176,8 @@ impl From for NodePerformance { #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct GatewayStatusReport { + pub(crate) node_id: NodeId, pub(crate) identity: String, - pub(crate) owner: String, pub(crate) most_recent: Uptime, @@ -203,8 +188,8 @@ pub struct GatewayStatusReport { impl GatewayStatusReport { pub(crate) fn construct_from_last_day_reports( report_time: OffsetDateTime, + node_id: NodeId, identity: String, - owner: String, last_day: Vec, last_hour_test_runs: usize, last_day_test_runs: usize, @@ -218,7 +203,7 @@ impl GatewayStatusReport { GatewayStatusReport { identity, - owner, + node_id, most_recent: node_uptimes.most_recent, last_hour: node_uptimes.last_hour, last_day: node_uptimes.last_day, @@ -226,18 +211,6 @@ impl GatewayStatusReport { } } -impl From for GatewayStatusReportResponse { - fn from(status: GatewayStatusReport) -> Self { - GatewayStatusReportResponse { - identity: status.identity, - owner: status.owner, - most_recent: status.most_recent.0, - last_hour: status.last_hour.0, - last_day: status.last_day.0, - } - } -} - impl From for NodePerformance { fn from(report: GatewayStatusReport) -> Self { NodePerformance { @@ -250,68 +223,44 @@ impl From for NodePerformance { #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct MixnodeUptimeHistory { - pub(crate) mix_id: MixId, + pub(crate) mix_id: NodeId, pub(crate) identity: String, - pub(crate) owner: String, pub(crate) history: Vec, } impl MixnodeUptimeHistory { - pub(crate) fn new( - mix_id: MixId, - identity: String, - owner: String, - history: Vec, - ) -> Self { + pub(crate) fn new(mix_id: NodeId, identity: String, history: Vec) -> Self { MixnodeUptimeHistory { mix_id, identity, - owner, history, } } } -impl From for MixnodeUptimeHistoryResponse { - fn from(history: MixnodeUptimeHistory) -> Self { - MixnodeUptimeHistoryResponse { - mix_id: history.mix_id, - identity: history.identity, - owner: history.owner, - history: history.history.into_iter().map(Into::into).collect(), - } - } -} - -#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] +#[derive(Default, Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct GatewayUptimeHistory { pub(crate) identity: String, - pub(crate) owner: String, + pub(crate) node_id: NodeId, pub(crate) history: Vec, } impl GatewayUptimeHistory { - pub(crate) fn new(identity: String, owner: String, history: Vec) -> Self { + pub(crate) fn new( + node_id: NodeId, + identity: impl Into, + history: Vec, + ) -> Self { GatewayUptimeHistory { - identity, - owner, + node_id, + identity: identity.into(), history, } } } -impl From for GatewayUptimeHistoryResponse { - fn from(history: GatewayUptimeHistory) -> Self { - GatewayUptimeHistoryResponse { - identity: history.identity, - owner: history.owner, - history: history.history.into_iter().map(Into::into).collect(), - } - } -} - #[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)] pub struct HistoricalUptime { // ISO 8601 date string @@ -321,192 +270,190 @@ pub struct HistoricalUptime { pub(crate) uptime: Uptime, } -impl From for HistoricalUptimeResponse { +#[derive(Error, Debug)] +pub enum InvalidHistoricalPerformance { + #[error("the provided date could not be parsed")] + UnparsableDate, + + #[error("the provided uptime could not be parsed")] + MalformedPerformance, +} + +impl TryFrom for HistoricalPerformanceResponse { + type Error = InvalidHistoricalPerformance; + fn try_from(value: HistoricalUptime) -> Result { + Ok(HistoricalPerformanceResponse { + date: Date::parse(&value.date, DATE_FORMAT) + .map_err(|_| InvalidHistoricalPerformance::UnparsableDate)?, + performance: Performance::from_percentage_value(value.uptime.u8() as u64) + .map_err(|_| InvalidHistoricalPerformance::MalformedPerformance)? + .naive_to_f64(), + }) + } +} + +impl TryFrom for HistoricalUptimeResponse { + type Error = InvalidHistoricalPerformance; + fn try_from(value: HistoricalUptime) -> Result { + Ok(HistoricalUptimeResponse { + date: Date::parse(&value.date, DATE_FORMAT) + .map_err(|_| InvalidHistoricalPerformance::UnparsableDate)?, + uptime: value.uptime.u8(), + }) + } +} + +impl From for OldHistoricalUptimeResponse { fn from(uptime: HistoricalUptime) -> Self { - HistoricalUptimeResponse { + OldHistoricalUptimeResponse { date: uptime.date, uptime: uptime.uptime.0, } } } -#[deprecated(note = "TODO rocket remove once Rocket is phased out")] -pub(crate) struct RocketErrorResponse { - error_message: RequestError, - status: Status, +// TODO rocket remove smurf name after eliminating `rocket` +pub(crate) type AxumResult = Result; +pub(crate) struct AxumErrorResponse { + message: RequestError, + status: StatusCode, } -impl RocketErrorResponse { - pub(crate) fn new(error_message: impl Into, status: Status) -> Self { - RocketErrorResponse { - error_message: RequestError::new(error_message), - status, +impl AxumErrorResponse { + pub(crate) fn internal_msg(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::INTERNAL_SERVER_ERROR, } } -} -impl<'r, 'o: 'r> Responder<'r, 'o> for RocketErrorResponse { - fn respond_to(self, req: &'r Request<'_>) -> response::Result<'o> { - // piggyback on the existing implementation - // also prefer json over plain for ease of use in frontend - Response::build() - .merge(Json(self.error_message).respond_to(req)?) - .status(self.status) - .ok() + pub(crate) fn internal() -> Self { + Self { + message: RequestError::new("Internal server error"), + status: StatusCode::INTERNAL_SERVER_ERROR, + } } -} -impl JsonSchema for RocketErrorResponse { - fn schema_name() -> String { - "ErrorResponse".to_owned() + pub(crate) fn not_implemented() -> Self { + Self { + message: RequestError::empty(), + status: StatusCode::NOT_IMPLEMENTED, + } } - fn json_schema(gen: &mut SchemaGenerator) -> Schema { - let mut schema_object = SchemaObject { - instance_type: Some(InstanceType::Object.into()), - ..SchemaObject::default() - }; - - let object_validation = schema_object.object(); - object_validation - .properties - .insert("error_message".to_owned(), gen.subschema_for::()); - object_validation - .required - .insert("error_message".to_owned()); - - // Status does not implement JsonSchema so we just explicitly specify the inner type. - object_validation - .properties - .insert("status".to_owned(), gen.subschema_for::()); - object_validation.required.insert("status".to_owned()); - - Schema::Object(schema_object) + pub(crate) fn not_found(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::NOT_FOUND, + } } -} -impl OpenApiResponderInner for RocketErrorResponse { - fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result { - let mut responses = Responses::default(); - ensure_status_code_exists(&mut responses, 404); - Ok(responses) + pub(crate) fn service_unavailable() -> Self { + Self { + message: RequestError::empty(), + status: StatusCode::SERVICE_UNAVAILABLE, + } } -} -#[cfg(feature = "axum")] -pub(crate) use axum_error::{AxumErrorResponse, AxumResult}; - -#[cfg(feature = "axum")] -/// TODO rocket: extract types from this module when axum becomes the only server in Nym API -mod axum_error { - pub use super::*; - use crate::ecash::error::{EcashError, RedemptionError}; - use std::fmt::Display; - - // TODO rocket remove smurf name after eliminating `rocket` - pub(crate) type AxumResult = Result; - pub(crate) struct AxumErrorResponse { - message: RequestError, - status: axum::http::StatusCode, - } - - impl AxumErrorResponse { - pub(crate) fn internal_msg(msg: impl Display) -> Self { - Self { - message: RequestError::new(msg.to_string()), - status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, - } + pub(crate) fn unauthorised(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::UNAUTHORIZED, } + } - pub(crate) fn internal() -> Self { - Self { - message: RequestError::new("Internal server error"), - status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, - } + pub(crate) fn unprocessable_entity(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::UNPROCESSABLE_ENTITY, } + } - pub(crate) fn not_implemented() -> Self { - Self { - message: RequestError::empty(), - status: axum::http::StatusCode::NOT_IMPLEMENTED, - } + pub(crate) fn forbidden(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::FORBIDDEN, } + } - pub(crate) fn not_found(msg: impl Display) -> Self { - Self { - message: RequestError::new(msg.to_string()), - status: axum::http::StatusCode::NOT_FOUND, - } + pub(crate) fn bad_request(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::BAD_REQUEST, } + } - pub(crate) fn service_unavailable() -> Self { - Self { - message: RequestError::empty(), - status: axum::http::StatusCode::SERVICE_UNAVAILABLE, - } + pub(crate) fn too_many(msg: impl Display) -> Self { + Self { + message: RequestError::new(msg.to_string()), + status: StatusCode::TOO_MANY_REQUESTS, } + } +} - pub(crate) fn unprocessable_entity(msg: impl Display) -> Self { - Self { - message: RequestError::new(msg.to_string()), - status: axum::http::StatusCode::UNPROCESSABLE_ENTITY, - } +impl From for AxumErrorResponse { + fn from(_: UninitialisedCache) -> Self { + AxumErrorResponse { + message: RequestError::new("relevant cache hasn't been initialised yet"), + status: StatusCode::SERVICE_UNAVAILABLE, } } +} - impl axum::response::IntoResponse for AxumErrorResponse { - fn into_response(self) -> axum::response::Response { - (self.status, self.message.message().to_string()).into_response() - } +impl axum::response::IntoResponse for AxumErrorResponse { + fn into_response(self) -> axum::response::Response { + (self.status, self.message.message().to_string()).into_response() } +} - impl From for AxumErrorResponse { - fn from(value: NymApiStorageError) -> Self { - error!("{value}"); - Self { - message: RequestError::empty(), - status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, - } +impl From for AxumErrorResponse { + fn from(value: NymApiStorageError) -> Self { + error!("{value}"); + Self { + message: RequestError::empty(), + status: StatusCode::INTERNAL_SERVER_ERROR, } } +} - impl From for AxumErrorResponse { - fn from(value: EcashError) -> Self { - Self { - message: RequestError::new(value.to_string()), - status: axum::http::StatusCode::BAD_REQUEST, - } +impl From for AxumErrorResponse { + fn from(value: EcashError) -> Self { + Self { + message: RequestError::new(value.to_string()), + status: StatusCode::BAD_REQUEST, } } +} - #[cfg(feature = "axum")] - impl From for AxumErrorResponse { - fn from(value: RedemptionError) -> Self { - Self { - message: RequestError::new(value.to_string()), - status: axum::http::StatusCode::BAD_REQUEST, - } +impl From for AxumErrorResponse { + fn from(value: RedemptionError) -> Self { + Self { + message: RequestError::new(value.to_string()), + status: StatusCode::BAD_REQUEST, } } } -#[derive(Debug, thiserror::Error)] +#[derive(Debug, Error)] pub enum NymApiStorageError { #[error("could not find status report associated with mixnode {mix_id}")] - MixnodeReportNotFound { mix_id: MixId }, + MixnodeReportNotFound { mix_id: NodeId }, - #[error("Could not find status report associated with gateway {identity}")] - GatewayReportNotFound { identity: IdentityKey }, + #[error("Could not find status report associated with gateway {node_id}")] + GatewayReportNotFound { node_id: NodeId }, #[error("could not find uptime history associated with mixnode {mix_id}")] - MixnodeUptimeHistoryNotFound { mix_id: MixId }, + MixnodeUptimeHistoryNotFound { mix_id: NodeId }, - #[error("could not find uptime history associated with gateway {identity}")] - GatewayUptimeHistoryNotFound { identity: IdentityKey }, + #[error("could not find uptime history associated with gateway {node_id}")] + GatewayUptimeHistoryNotFound { node_id: NodeId }, + + #[error("could not find gateway {identity} in the storage")] + GatewayNotFound { identity: String }, // I don't think we want to expose errors to the user about what really happened #[error("experienced internal database error")] - InternalDatabaseError(#[from] sqlx::Error), + InternalDatabaseError(sqlx::Error), // the same is true here (also note that the message is subtly different so we would be able to distinguish them) #[error("experienced internal storage error")] @@ -517,6 +464,14 @@ pub enum NymApiStorageError { StartupMigrationFailure(#[from] sqlx::migrate::MigrateError), } +impl From for NymApiStorageError { + fn from(err: Error) -> Self { + // those should realistically never be happening so an `error!` is warranted + error!("storage failure: {err}"); + NymApiStorageError::InternalDatabaseError(err) + } +} + impl NymApiStorageError { pub fn database_inconsistency>(reason: S) -> NymApiStorageError { NymApiStorageError::DatabaseInconsistency { diff --git a/nym-api/src/node_status_api/reward_estimate.rs b/nym-api/src/node_status_api/reward_estimate.rs index e9bd60ba09..6b1798227d 100644 --- a/nym-api/src/node_status_api/reward_estimate.rs +++ b/nym-api/src/node_status_api/reward_estimate.rs @@ -1,11 +1,14 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node_status_api::helpers::RewardedSetStatus; use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::mixnode::MixNodeDetails; -use nym_mixnet_contract_common::reward_params::{NodeRewardParams, Performance, RewardingParams}; +use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; +use nym_mixnet_contract_common::reward_params::{ + NodeRewardingParameters, Performance, RewardingParams, +}; use nym_mixnet_contract_common::rewarding::RewardEstimate; -use nym_mixnet_contract_common::{Interval, RewardedSetNodeStatus}; +use nym_mixnet_contract_common::Interval; fn compute_apy(epochs_in_year: Decimal, reward: Decimal, pledge_amount: Decimal) -> Decimal { if pledge_amount.is_zero() { @@ -17,9 +20,9 @@ fn compute_apy(epochs_in_year: Decimal, reward: Decimal, pledge_amount: Decimal) } pub fn compute_reward_estimate( - mixnode: &MixNodeDetails, + mixnode: &LegacyMixNodeDetailsWithLayer, performance: Performance, - rewarded_set_status: Option, + rewarded_set_status: RewardedSetStatus, rewarding_params: RewardingParams, interval: Interval, ) -> RewardEstimate { @@ -31,13 +34,22 @@ pub fn compute_reward_estimate( return Default::default(); } - let node_status = match rewarded_set_status { - Some(status) => status, - // if node is not in the rewarded set, it's not going to get anything - None => return Default::default(), + let is_active = match rewarded_set_status { + RewardedSetStatus::Active => true, + RewardedSetStatus::Standby => false, + RewardedSetStatus::Inactive => return Default::default(), }; - let node_reward_params = NodeRewardParams::new(performance, node_status.is_active()); + let work_factor = if is_active { + rewarding_params.active_node_work() + } else { + rewarding_params.standby_node_work() + }; + + let node_reward_params = NodeRewardingParameters { + performance, + work_factor, + }; let node_reward = mixnode .rewarding_details .node_reward(&rewarding_params, node_reward_params); @@ -63,7 +75,7 @@ pub fn compute_reward_estimate( } pub fn compute_apy_from_reward( - mixnode: &MixNodeDetails, + mixnode: &LegacyMixNodeDetailsWithLayer, reward_estimate: RewardEstimate, interval: Interval, ) -> (Decimal, Decimal) { diff --git a/nym-api/src/node_status_api/routes_deprecated.rs b/nym-api/src/node_status_api/routes_deprecated.rs deleted file mode 100644 index 29ab89fb4d..0000000000 --- a/nym-api/src/node_status_api/routes_deprecated.rs +++ /dev/null @@ -1,592 +0,0 @@ -// Copyright 2021-2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use nym_api_requests::models::{ - AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayBondAnnotated, - GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, - GatewayUptimeResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, - MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse, - MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse, - UptimeResponse, -}; -use nym_mixnet_contract_common::MixId; -use nym_types::monitoring::MonitorMessage; -use rocket::serde::json::Json; -use rocket::State; -use rocket_okapi::openapi; - -use super::helpers_deprecated::_get_gateways_detailed; -use super::NodeStatusCache; -use crate::node_status_api::helpers_deprecated::{ - _compute_mixnode_reward_estimation, _gateway_core_status_count, _gateway_report, - _gateway_uptime_history, _get_active_set_detailed, _get_gateway_avg_uptime, - _get_gateways_detailed_unfiltered, _get_mixnode_avg_uptime, - _get_mixnode_inclusion_probabilities, _get_mixnode_inclusion_probability, - _get_mixnode_reward_estimation, _get_mixnode_stake_saturation, _get_mixnode_status, - _get_mixnodes_detailed, _get_mixnodes_detailed_unfiltered, _get_rewarded_set_detailed, - _mixnode_core_status_count, _mixnode_report, _mixnode_uptime_history, -}; -use crate::node_status_api::models::RocketErrorResponse; -use crate::storage::NymApiStorage; -use crate::NymContractCache; - -#[openapi(tag = "status")] -#[post("/submit-gateway-monitoring-results", data = "")] -pub(crate) async fn submit_gateway_monitoring_results( - message: Json, - storage: &State, -) -> Result<(), RocketErrorResponse> { - if !message.from_allowed() { - return Err(RocketErrorResponse::new( - "Monitor not registered to submit results".to_string(), - rocket::http::Status::Forbidden, - )); - } - - if !message.timely() { - return Err(RocketErrorResponse::new( - "Message is too old".to_string(), - rocket::http::Status::BadRequest, - )); - } - - if !message.verify() { - return Err(RocketErrorResponse::new( - "Invalid signature".to_string(), - rocket::http::Status::BadRequest, - )); - } - - match storage - .manager - .submit_gateway_statuses_v2(message.results()) - .await - { - Ok(_) => Ok(()), - Err(err) => { - error!("failed to submit gateway monitoring results: {}", err); - Err(RocketErrorResponse::new( - "failed to submit gateway monitoring results".to_string(), - rocket::http::Status::InternalServerError, - )) - } - } -} - -#[openapi(tag = "status")] -#[post("/submit-node-monitoring-results", data = "")] -pub(crate) async fn submit_node_monitoring_results( - message: Json, - storage: &State, -) -> Result<(), RocketErrorResponse> { - if !message.from_allowed() { - return Err(RocketErrorResponse::new( - "Monitor not registered to submit results".to_string(), - rocket::http::Status::Forbidden, - )); - } - - if !message.timely() { - return Err(RocketErrorResponse::new( - "Message is too old".to_string(), - rocket::http::Status::BadRequest, - )); - } - - if !message.verify() { - return Err(RocketErrorResponse::new( - "Invalid signature".to_string(), - rocket::http::Status::BadRequest, - )); - } - - match storage - .manager - .submit_mixnode_statuses_v2(message.results()) - .await - { - Ok(_) => Ok(()), - Err(err) => { - error!("failed to submit node monitoring results: {}", err); - Err(RocketErrorResponse::new( - "failed to submit node monitoring results".to_string(), - rocket::http::Status::InternalServerError, - )) - } - } -} - -#[openapi(tag = "status")] -#[get("/gateway//report")] -pub(crate) async fn gateway_report( - cache: &State, - identity: &str, -) -> Result, RocketErrorResponse> { - Ok(Json(_gateway_report(cache, identity).await?)) -} - -#[openapi(tag = "status")] -#[get("/gateway//history")] -pub(crate) async fn gateway_uptime_history( - storage: &State, - identity: &str, -) -> Result, RocketErrorResponse> { - Ok(Json(_gateway_uptime_history(storage, identity).await?)) -} - -#[openapi(tag = "status")] -#[get("/gateway//core-status-count?")] -pub(crate) async fn gateway_core_status_count( - storage: &State, - identity: &str, - since: Option, -) -> Result, RocketErrorResponse> { - Ok(Json( - _gateway_core_status_count(storage, identity, since).await?, - )) -} - -#[openapi(tag = "status")] -#[get("/mixnode//report")] -pub(crate) async fn mixnode_report( - cache: &State, - mix_id: MixId, -) -> Result, RocketErrorResponse> { - Ok(Json(_mixnode_report(cache, mix_id).await?)) -} - -#[openapi(tag = "status")] -#[get("/mixnode//history")] -pub(crate) async fn mixnode_uptime_history( - storage: &State, - mix_id: MixId, -) -> Result, RocketErrorResponse> { - Ok(Json(_mixnode_uptime_history(storage, mix_id).await?)) -} - -#[openapi(tag = "status")] -#[get("/mixnode//core-status-count?")] -pub(crate) async fn mixnode_core_status_count( - storage: &State, - mix_id: MixId, - since: Option, -) -> Result, RocketErrorResponse> { - Ok(Json( - _mixnode_core_status_count(storage, mix_id, since).await?, - )) -} - -#[openapi(tag = "status")] -#[get("/mixnode//status")] -pub(crate) async fn get_mixnode_status( - cache: &State, - mix_id: MixId, -) -> Json { - Json(_get_mixnode_status(cache, mix_id).await) -} - -#[openapi(tag = "status")] -#[get("/mixnode//reward-estimation")] -pub(crate) async fn get_mixnode_reward_estimation( - cache: &State, - validator_cache: &State, - mix_id: MixId, -) -> Result, RocketErrorResponse> { - Ok(Json( - _get_mixnode_reward_estimation(cache, validator_cache, mix_id).await?, - )) -} - -#[openapi(tag = "status")] -#[post( - "/mixnode//compute-reward-estimation", - data = "" -)] -pub(crate) async fn compute_mixnode_reward_estimation( - user_reward_param: Json, - cache: &State, - validator_cache: &State, - mix_id: MixId, -) -> Result, RocketErrorResponse> { - Ok(Json( - _compute_mixnode_reward_estimation( - user_reward_param.into_inner(), - cache, - validator_cache, - mix_id, - ) - .await?, - )) -} - -#[openapi(tag = "status")] -#[get("/mixnode//stake-saturation")] -pub(crate) async fn get_mixnode_stake_saturation( - cache: &State, - validator_cache: &State, - mix_id: MixId, -) -> Result, RocketErrorResponse> { - Ok(Json( - _get_mixnode_stake_saturation(cache, validator_cache, mix_id).await?, - )) -} - -#[openapi(tag = "status")] -#[get("/mixnode//inclusion-probability")] -pub(crate) async fn get_mixnode_inclusion_probability( - cache: &State, - mix_id: MixId, -) -> Result, RocketErrorResponse> { - Ok(Json( - _get_mixnode_inclusion_probability(cache, mix_id).await?, - )) -} - -#[openapi(tag = "status")] -#[get("/mixnode//avg_uptime")] -pub(crate) async fn get_mixnode_avg_uptime( - cache: &State, - mix_id: MixId, -) -> Result, RocketErrorResponse> { - Ok(Json(_get_mixnode_avg_uptime(cache, mix_id).await?)) -} - -#[openapi(tag = "status")] -#[get("/gateway//avg_uptime")] -pub(crate) async fn get_gateway_avg_uptime( - cache: &State, - identity: &str, -) -> Result, RocketErrorResponse> { - Ok(Json(_get_gateway_avg_uptime(cache, identity).await?)) -} - -#[openapi(tag = "status")] -#[get("/mixnodes/inclusion_probability")] -pub(crate) async fn get_mixnode_inclusion_probabilities( - cache: &State, -) -> Result, RocketErrorResponse> { - Ok(Json(_get_mixnode_inclusion_probabilities(cache).await?)) -} - -#[openapi(tag = "status")] -#[get("/mixnodes/detailed")] -pub async fn get_mixnodes_detailed( - cache: &State, -) -> Json> { - Json(_get_mixnodes_detailed(cache).await) -} - -#[openapi(tag = "status")] -#[get("/mixnodes/detailed-unfiltered")] -pub async fn get_mixnodes_detailed_unfiltered( - cache: &State, -) -> Json> { - Json(_get_mixnodes_detailed_unfiltered(cache).await) -} - -#[openapi(tag = "status")] -#[get("/mixnodes/rewarded/detailed")] -pub async fn get_rewarded_set_detailed( - cache: &State, -) -> Json> { - Json(_get_rewarded_set_detailed(cache).await) -} - -#[openapi(tag = "status")] -#[get("/mixnodes/active/detailed")] -pub async fn get_active_set_detailed( - cache: &State, -) -> Json> { - Json(_get_active_set_detailed(cache).await) -} - -#[openapi(tag = "status")] -#[get("/gateways/detailed")] -pub async fn get_gateways_detailed( - cache: &State, -) -> Json> { - Json(_get_gateways_detailed(cache).await) -} - -#[openapi(tag = "status")] -#[get("/gateways/detailed-unfiltered")] -pub async fn get_gateways_detailed_unfiltered( - cache: &State, -) -> Json> { - Json(_get_gateways_detailed_unfiltered(cache).await) -} - -pub mod unstable { - use crate::node_status_api::models::RocketErrorResponse; - use crate::support::http::helpers::PaginationRequest; - use crate::support::storage::NymApiStorage; - use nym_api_requests::models::{ - GatewayTestResultResponse, MixnodeTestResultResponse, PartialTestResult, TestNode, - TestRoute, - }; - use nym_api_requests::pagination::Pagination; - use nym_mixnet_contract_common::MixId; - use rocket::http::Status; - use rocket::serde::json::Json; - use rocket::State; - use rocket_okapi::openapi; - use std::cmp::min; - use std::collections::HashMap; - use std::sync::Arc; - use tokio::sync::RwLock; - - pub type DbId = i64; - - // a simply in-memory cache of node details - #[derive(Debug, Default)] - pub struct NodeInfoCache { - inner: Arc>, - } - - impl NodeInfoCache { - async fn get_mix_node_details(&self, db_id: DbId, storage: &NymApiStorage) -> TestNode { - { - let read_guard = self.inner.read().await; - if let Some(cached) = read_guard.mixnodes.get(&db_id) { - trace!("cache hit for mixnode {db_id}"); - return cached.clone(); - } - } - trace!("cache miss for mixnode {db_id}"); - - let mut write_guard = self.inner.write().await; - // double-check the cache in case somebody already updated it while we were waiting for the lock - if let Some(cached) = write_guard.mixnodes.get(&db_id) { - return cached.clone(); - } - - let details = match storage.get_mixnode_details_by_db_id(db_id).await { - Ok(Some(details)) => details.into(), - Ok(None) => { - error!("somebody has been messing with the database! details for mixnode with database id {db_id} have been removed!"); - TestNode::default() - } - Err(err) => { - // don't insert into the cache in case another request is successful - error!("failed to retrieve details for mixnode {db_id}: {err}"); - return TestNode::default(); - } - }; - - write_guard.mixnodes.insert(db_id, details.clone()); - details - } - - async fn get_gateway_details(&self, db_id: DbId, storage: &NymApiStorage) -> TestNode { - { - let read_guard = self.inner.read().await; - if let Some(cached) = read_guard.gateways.get(&db_id) { - trace!("cache hit for gateway {db_id}"); - return cached.clone(); - } - } - trace!("cache miss for gateway {db_id}"); - - let mut write_guard = self.inner.write().await; - // double-check the cache in case somebody already updated it while we were waiting for the lock - if let Some(cached) = write_guard.gateways.get(&db_id) { - return cached.clone(); - } - - let details = match storage.get_gateway_details_by_db_id(db_id).await { - Ok(Some(details)) => details.into(), - Ok(None) => { - error!("somebody has been messing with the database! details for gateway with database id {db_id} have been removed!"); - TestNode::default() - } - Err(err) => { - // don't insert into the cache in case another request is successful - error!("failed to retrieve details for gateway {db_id}: {err}"); - return TestNode::default(); - } - }; - - write_guard.gateways.insert(db_id, details.clone()); - details - } - } - - #[derive(Debug, Default)] - struct NodeInfoCacheInner { - mixnodes: HashMap, - gateways: HashMap, - } - - const MAX_TEST_RESULTS_PAGE_SIZE: u32 = 100; - const DEFAULT_TEST_RESULTS_PAGE_SIZE: u32 = 50; - - async fn _mixnode_test_results( - mix_id: MixId, - page: u32, - per_page: u32, - info_cache: &State, - storage: &State, - ) -> anyhow::Result { - // convert to db offset - // we're paging from page 0 like civilised people, - // so we have to skip (page * per_page) results - let offset = page * per_page; - let limit = per_page; - - let raw_results = storage - .get_mixnode_detailed_statuses(mix_id, limit, offset) - .await?; - let total = match raw_results.first() { - None => 0, - Some(r) => storage.get_mixnode_detailed_statuses_count(r.db_id).await?, - }; - - let mut partial_results = Vec::new(); - for result in raw_results { - let gateway = info_cache - .get_gateway_details(result.gateway_id, storage) - .await; - let layer1 = info_cache - .get_mix_node_details(result.layer1_mix_id, storage) - .await; - let layer2 = info_cache - .get_mix_node_details(result.layer2_mix_id, storage) - .await; - let layer3 = info_cache - .get_mix_node_details(result.layer3_mix_id, storage) - .await; - - partial_results.push(PartialTestResult { - monitor_run_id: result.monitor_run_id, - timestamp: result.timestamp, - overall_reliability_for_all_routes_in_monitor_run: result.reliability, - test_routes: TestRoute { - gateway, - layer1, - layer2, - layer3, - }, - }) - } - - Ok(MixnodeTestResultResponse { - pagination: Pagination { - total, - page, - size: partial_results.len(), - }, - data: partial_results, - }) - } - - #[openapi(tag = "UNSTABLE - DO **NOT** USE")] - #[get("/mixnodes/unstable//test-results?")] - pub async fn mixnode_test_results( - mix_id: MixId, - pagination: PaginationRequest, - info_cache: &State, - storage: &State, - ) -> Result, RocketErrorResponse> { - let page = pagination.page.unwrap_or_default(); - let per_page = min( - pagination - .per_page - .unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE), - MAX_TEST_RESULTS_PAGE_SIZE, - ); - - match _mixnode_test_results(mix_id, page, per_page, info_cache, storage).await { - Ok(res) => Ok(Json(res)), - Err(err) => Err(RocketErrorResponse::new( - format!("failed to retrieve mixnode test results for node {mix_id}: {err}"), - Status::InternalServerError, - )), - } - } - - async fn _gateway_test_results( - gateway_identity: &str, - page: u32, - per_page: u32, - info_cache: &State, - storage: &State, - ) -> anyhow::Result { - // convert to db offset - // we're paging from page 0 like civilised people, - // so we have to skip (page * per_page) results - let offset = page * per_page; - let limit = per_page; - - let raw_results = storage - .get_gateway_detailed_statuses(gateway_identity, limit, offset) - .await?; - let total = match raw_results.first() { - None => 0, - Some(r) => storage.get_gateway_detailed_statuses_count(r.db_id).await?, - }; - - let mut partial_results = Vec::new(); - for result in raw_results { - let gateway = info_cache - .get_gateway_details(result.gateway_id, storage) - .await; - let layer1 = info_cache - .get_mix_node_details(result.layer1_mix_id, storage) - .await; - let layer2 = info_cache - .get_mix_node_details(result.layer2_mix_id, storage) - .await; - let layer3 = info_cache - .get_mix_node_details(result.layer3_mix_id, storage) - .await; - - partial_results.push(PartialTestResult { - monitor_run_id: result.monitor_run_id, - timestamp: result.timestamp, - overall_reliability_for_all_routes_in_monitor_run: result.reliability, - test_routes: TestRoute { - gateway, - layer1, - layer2, - layer3, - }, - }) - } - - Ok(GatewayTestResultResponse { - pagination: Pagination { - total, - page, - size: partial_results.len(), - }, - data: partial_results, - }) - } - - #[openapi(tag = "UNSTABLE - DO **NOT** USE")] - #[get("/gateways/unstable//test-results?")] - pub async fn gateway_test_results( - gateway_identity: &str, - pagination: PaginationRequest, - info_cache: &State, - storage: &State, - ) -> Result, RocketErrorResponse> { - let page = pagination.page.unwrap_or_default(); - let per_page = min( - pagination - .per_page - .unwrap_or(DEFAULT_TEST_RESULTS_PAGE_SIZE), - MAX_TEST_RESULTS_PAGE_SIZE, - ); - - match _gateway_test_results(gateway_identity, page, per_page, info_cache, storage).await { - Ok(res) => Ok(Json(res)), - Err(err) => Err(RocketErrorResponse::new( - format!( - "failed to retrieve mixnode test results for gateway {gateway_identity}: {err}" - ), - Status::InternalServerError, - )), - } - } -} diff --git a/nym-api/src/node_status_api/uptime_updater.rs b/nym-api/src/node_status_api/uptime_updater.rs index 2a33a0118f..affca9f7f2 100644 --- a/nym-api/src/node_status_api/uptime_updater.rs +++ b/nym-api/src/node_status_api/uptime_updater.rs @@ -1,4 +1,4 @@ -// Copyright 2021 - Nym Technologies SA +// Copyright 2021-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use crate::node_status_api::models::{ @@ -6,11 +6,12 @@ use crate::node_status_api::models::{ }; use crate::node_status_api::ONE_DAY; use crate::storage::NymApiStorage; -use log::error; use nym_task::{TaskClient, TaskManager}; use std::time::Duration; use time::{OffsetDateTime, PrimitiveDateTime, Time}; -use tokio::time::{interval, sleep}; +use tokio::time::{interval_at, Instant}; +use tracing::error; +use tracing::{info, trace, warn}; pub(crate) struct HistoricalUptimeUpdater { storage: NymApiStorage, @@ -88,19 +89,12 @@ impl HistoricalUptimeUpdater { // resultant Duration is positive let time_left: Duration = (update_datetime - now).try_into().unwrap(); - log::info!( + info!( "waiting until {update_datetime} to update the historical uptimes for the first time ({} seconds left)", time_left.as_secs() ); - tokio::select! { - biased; - _ = shutdown.recv() => { - trace!("UpdateHandler: Received shutdown"); - } - _ = sleep(time_left) => {} - } - - let mut interval = interval(ONE_DAY); + let start = Instant::now() + time_left; + let mut interval = interval_at(start, ONE_DAY); while !shutdown.is_shutdown() { tokio::select! { biased; @@ -108,6 +102,7 @@ impl HistoricalUptimeUpdater { trace!("UpdateHandler: Received shutdown"); } _ = interval.tick() => { + info!("updating historical uptimes of nodes"); // we don't want to have another select here; uptime update is relatively speedy // and we don't want to exit while we're in the middle of database update if let Err(err) = self.update_uptimes().await { diff --git a/nym-api/src/node_status_api/utils.rs b/nym-api/src/node_status_api/utils.rs index 0682cf62ba..4d86be5faa 100644 --- a/nym-api/src/node_status_api/utils.rs +++ b/nym-api/src/node_status_api/utils.rs @@ -4,23 +4,23 @@ use crate::node_status_api::models::Uptime; use crate::node_status_api::{FIFTEEN_MINUTES, ONE_HOUR}; use crate::storage::models::NodeStatus; -use log::warn; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; +use tracing::warn; use time::OffsetDateTime; // A temporary helper structs used to produce reports for active nodes. pub(crate) struct ActiveMixnodeStatuses { - pub(crate) mix_id: MixId, + pub(crate) mix_id: NodeId, pub(crate) identity: String, - pub(crate) owner: String, pub(crate) statuses: Vec, } pub(crate) struct ActiveGatewayStatuses { + pub(crate) node_id: NodeId, + pub(crate) identity: String, - pub(crate) owner: String, pub(crate) statuses: Vec, } diff --git a/nym-api/src/nym_contract_cache/cache/data.rs b/nym-api/src/nym_contract_cache/cache/data.rs index c04404f2df..f32c19c9cd 100644 --- a/nym-api/src/nym_contract_cache/cache/data.rs +++ b/nym-api/src/nym_contract_cache/cache/data.rs @@ -2,44 +2,143 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::caching::Cache; +use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeDetailsWithLayer}; use nym_contracts_common::ContractBuildInformation; -use nym_mixnet_contract_common::{ - families::FamilyHead, GatewayBond, IdentityKey, Interval, MixId, MixNodeDetails, - RewardingParams, -}; +use nym_mixnet_contract_common::nym_node::Role; +use nym_mixnet_contract_common::{Interval, NodeId, NymNodeDetails, RewardedSet, RewardingParams}; use nym_validator_client::nyxd::AccountId; use std::collections::{HashMap, HashSet}; -pub(crate) struct ValidatorCacheData { - pub(crate) mixnodes: Cache>, - pub(crate) gateways: Cache>, +#[derive(Default, Clone)] +pub(crate) struct CachedRewardedSet { + pub(crate) entry_gateways: HashSet, + + pub(crate) exit_gateways: HashSet, + + pub(crate) layer1: HashSet, - pub(crate) mixnodes_blacklist: Cache>, - pub(crate) gateways_blacklist: Cache>, + pub(crate) layer2: HashSet, - pub(crate) rewarded_set: Cache>, - pub(crate) active_set: Cache>, + pub(crate) layer3: HashSet, + + pub(crate) standby: HashSet, +} + +impl From for CachedRewardedSet { + fn from(value: RewardedSet) -> Self { + CachedRewardedSet { + entry_gateways: value.entry_gateways.into_iter().collect(), + exit_gateways: value.exit_gateways.into_iter().collect(), + layer1: value.layer1.into_iter().collect(), + layer2: value.layer2.into_iter().collect(), + layer3: value.layer3.into_iter().collect(), + standby: value.standby.into_iter().collect(), + } + } +} + +impl From for RewardedSet { + fn from(value: CachedRewardedSet) -> Self { + RewardedSet { + entry_gateways: value.entry_gateways.into_iter().collect(), + exit_gateways: value.exit_gateways.into_iter().collect(), + layer1: value.layer1.into_iter().collect(), + layer2: value.layer2.into_iter().collect(), + layer3: value.layer3.into_iter().collect(), + standby: value.standby.into_iter().collect(), + } + } +} + +impl CachedRewardedSet { + pub(crate) fn role(&self, node_id: NodeId) -> Option { + if self.entry_gateways.contains(&node_id) { + Some(Role::EntryGateway) + } else if self.exit_gateways.contains(&node_id) { + Some(Role::ExitGateway) + } else if self.layer1.contains(&node_id) { + Some(Role::Layer1) + } else if self.layer2.contains(&node_id) { + Some(Role::Layer2) + } else if self.layer3.contains(&node_id) { + Some(Role::Layer3) + } else if self.standby.contains(&node_id) { + Some(Role::Standby) + } else { + None + } + } + + pub fn try_get_mix_layer(&self, node_id: &NodeId) -> Option { + if self.layer1.contains(node_id) { + Some(1) + } else if self.layer2.contains(node_id) { + Some(2) + } else if self.layer3.contains(node_id) { + Some(3) + } else { + None + } + } + + pub fn is_standby(&self, node_id: &NodeId) -> bool { + self.standby.contains(node_id) + } + + pub fn is_active_mixnode(&self, node_id: &NodeId) -> bool { + self.layer1.contains(node_id) + || self.layer2.contains(node_id) + || self.layer3.contains(node_id) + } + + #[allow(dead_code)] + pub(crate) fn gateways(&self) -> HashSet { + let mut gateways = + HashSet::with_capacity(self.entry_gateways.len() + self.exit_gateways.len()); + gateways.extend(&self.entry_gateways); + gateways.extend(&self.exit_gateways); + gateways + } + + pub(crate) fn active_mixnodes(&self) -> HashSet { + let mut mixnodes = + HashSet::with_capacity(self.layer1.len() + self.layer2.len() + self.layer3.len()); + mixnodes.extend(&self.layer1); + mixnodes.extend(&self.layer2); + mixnodes.extend(&self.layer3); + mixnodes + } +} + +pub(crate) struct ValidatorCacheData { + pub(crate) legacy_mixnodes: Cache>, + pub(crate) legacy_gateways: Cache>, + pub(crate) nym_nodes: Cache>, + pub(crate) rewarded_set: Cache, + + // this purposely does not deal with nym-nodes as they don't have a concept of a blacklist. + // instead clients are meant to be filtering out them themselves based on the provided scores. + pub(crate) legacy_mixnodes_blacklist: Cache>, + pub(crate) legacy_gateways_blacklist: Cache>, pub(crate) current_reward_params: Cache>, pub(crate) current_interval: Cache>, - pub(crate) mix_to_family: Cache>, - pub(crate) contracts_info: Cache, } impl ValidatorCacheData { pub(crate) fn new() -> Self { ValidatorCacheData { - mixnodes: Cache::default(), - gateways: Cache::default(), + legacy_mixnodes: Cache::default(), + legacy_gateways: Cache::default(), + nym_nodes: Default::default(), rewarded_set: Cache::default(), - active_set: Cache::default(), - mixnodes_blacklist: Cache::default(), - gateways_blacklist: Cache::default(), + + legacy_mixnodes_blacklist: Cache::default(), + legacy_gateways_blacklist: Cache::default(), current_interval: Cache::default(), current_reward_params: Cache::default(), - mix_to_family: Cache::default(), contracts_info: Cache::default(), } } diff --git a/nym-api/src/nym_contract_cache/cache/mod.rs b/nym-api/src/nym_contract_cache/cache/mod.rs index efaeac74a6..2f08c26914 100644 --- a/nym-api/src/nym_contract_cache/cache/mod.rs +++ b/nym-api/src/nym_contract_cache/cache/mod.rs @@ -1,15 +1,16 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use crate::node_describe_cache::RefreshData; use crate::nym_contract_cache::cache::data::CachedContractsInfo; use crate::support::caching::Cache; use data::ValidatorCacheData; -use nym_api_requests::models::MixnodeStatus; -use nym_mixnet_contract_common::{ - families::FamilyHead, GatewayBond, IdentityKey, Interval, MixId, MixNodeBond, MixNodeDetails, - RewardingParams, +use nym_api_requests::legacy::{ + LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, }; -use rocket::fairing::AdHoc; +use nym_api_requests::models::MixnodeStatus; +use nym_crypto::asymmetric::ed25519; +use nym_mixnet_contract_common::{Interval, NodeId, NymNodeDetails, RewardedSet, RewardingParams}; use std::{ collections::HashSet, sync::{ @@ -18,12 +19,17 @@ use std::{ }, time::Duration, }; -use tokio::sync::RwLock; +use tokio::sync::{RwLock, RwLockReadGuard}; use tokio::time; +use tracing::{debug, error}; mod data; pub(crate) mod refresher; +pub(crate) use self::data::CachedRewardedSet; + +const CACHE_TIMEOUT_MS: u64 = 100; + #[derive(Clone)] pub struct NymContractCache { pub(crate) initialised: Arc, @@ -38,38 +44,56 @@ impl NymContractCache { } } - #[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")] - pub fn stage() -> AdHoc { - AdHoc::on_ignite("Validator Cache Stage", |rocket| async { - rocket.manage(Self::new()) - }) + /// Returns a copy of the current cache data. + async fn get_owned( + &self, + fn_arg: impl FnOnce(RwLockReadGuard<'_, ValidatorCacheData>) -> Cache, + ) -> Option> { + match time::timeout(Duration::from_millis(CACHE_TIMEOUT_MS), self.inner.read()).await { + Ok(cache) => Some(fn_arg(cache)), + Err(e) => { + error!("{e}"); + None + } + } + } + + async fn get<'a, T: 'a>( + &'a self, + fn_arg: impl FnOnce(&ValidatorCacheData) -> &Cache, + ) -> Option>> { + match time::timeout(Duration::from_millis(CACHE_TIMEOUT_MS), self.inner.read()).await { + Ok(cache) => Some(RwLockReadGuard::map(cache, |item| fn_arg(item))), + Err(e) => { + error!("{e}"); + None + } + } } #[allow(clippy::too_many_arguments)] pub(crate) async fn update( &self, - mixnodes: Vec, - gateways: Vec, - rewarded_set: Vec, - active_set: Vec, + mixnodes: Vec, + gateways: Vec, + nym_nodes: Vec, + rewarded_set: RewardedSet, rewarding_params: RewardingParams, current_interval: Interval, - mix_to_family: Vec<(IdentityKey, FamilyHead)>, nym_contracts_info: CachedContractsInfo, ) { match time::timeout(Duration::from_millis(100), self.inner.write()).await { Ok(mut cache) => { - cache.mixnodes.unchecked_update(mixnodes); - cache.gateways.unchecked_update(gateways); + cache.legacy_mixnodes.unchecked_update(mixnodes); + cache.legacy_gateways.unchecked_update(gateways); + cache.nym_nodes.unchecked_update(nym_nodes); cache.rewarded_set.unchecked_update(rewarded_set); - cache.active_set.unchecked_update(active_set); cache .current_reward_params .unchecked_update(Some(rewarding_params)); cache .current_interval .unchecked_update(Some(current_interval)); - cache.mix_to_family.unchecked_update(mix_to_family); cache.contracts_info.unchecked_update(nym_contracts_info) } Err(err) => { @@ -78,39 +102,31 @@ impl NymContractCache { } } - pub async fn mixnodes_blacklist(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.mixnodes_blacklist.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(HashSet::new()) - } - } + pub async fn mixnodes_blacklist(&self) -> Cache> { + self.get_owned(|cache| cache.legacy_mixnodes_blacklist.clone_cache()) + .await + .unwrap_or_default() } - pub async fn gateways_blacklist(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.gateways_blacklist.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(HashSet::new()) - } - } + pub async fn gateways_blacklist(&self) -> Cache> { + self.get_owned(|cache| cache.legacy_gateways_blacklist.clone_cache()) + .await + .unwrap_or_default() } - pub async fn update_mixnodes_blacklist(&self, add: HashSet, remove: HashSet) { + pub async fn update_mixnodes_blacklist(&self, add: HashSet, remove: HashSet) { let blacklist = self.mixnodes_blacklist().await; - let mut blacklist = blacklist.union(&add).cloned().collect::>(); + let mut blacklist = blacklist.union(&add).cloned().collect::>(); let to_remove = blacklist .intersection(&remove) .cloned() - .collect::>(); + .collect::>(); for key in to_remove { blacklist.remove(&key); } match time::timeout(Duration::from_millis(100), self.inner.write()).await { Ok(mut cache) => { - cache.mixnodes_blacklist.unchecked_update(blacklist); + cache.legacy_mixnodes_blacklist.unchecked_update(blacklist); } Err(err) => { error!("Failed to update mixnodes blacklist: {err}"); @@ -118,26 +134,19 @@ impl NymContractCache { } } - pub async fn update_gateways_blacklist( - &self, - add: HashSet, - remove: HashSet, - ) { + pub async fn update_gateways_blacklist(&self, add: HashSet, remove: HashSet) { let blacklist = self.gateways_blacklist().await; - let mut blacklist = blacklist - .union(&add) - .cloned() - .collect::>(); + let mut blacklist = blacklist.union(&add).cloned().collect::>(); let to_remove = blacklist .intersection(&remove) .cloned() - .collect::>(); + .collect::>(); for key in to_remove { blacklist.remove(&key); } match time::timeout(Duration::from_millis(100), self.inner.write()).await { Ok(mut cache) => { - cache.gateways_blacklist.unchecked_update(blacklist); + cache.legacy_gateways_blacklist.unchecked_update(blacklist); } Err(err) => { error!("Failed to update gateways blacklist: {err}"); @@ -145,8 +154,8 @@ impl NymContractCache { } } - pub async fn mixnodes_filtered(&self) -> Vec { - let mixnodes = self.mixnodes_all().await; + pub async fn legacy_mixnodes_filtered(&self) -> Vec { + let mixnodes = self.legacy_mixnodes_all().await; if mixnodes.is_empty() { return Vec::new(); } @@ -162,34 +171,66 @@ impl NymContractCache { } } - pub async fn mixnodes_all(&self) -> Vec { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.mixnodes.clone(), - Err(err) => { - error!("{err}"); - Vec::new() - } - } + pub async fn all_cached_legacy_mixnodes( + &self, + ) -> Option>>> { + self.get(|c| &c.legacy_mixnodes).await + } + + pub async fn legacy_gateway_owner(&self, node_id: NodeId) -> Option { + self.get(|c| &c.legacy_gateways) + .await? + .iter() + .find(|g| g.node_id == node_id) + .map(|g| g.owner.to_string()) + } + + #[allow(dead_code)] + pub async fn legacy_mixnode_owner(&self, node_id: NodeId) -> Option { + self.get(|c| &c.legacy_mixnodes) + .await? + .iter() + .find(|m| m.mix_id() == node_id) + .map(|m| m.bond_information.owner.to_string()) + } + + pub async fn all_cached_legacy_gateways( + &self, + ) -> Option>>> { + self.get(|c| &c.legacy_gateways).await + } + + pub async fn all_cached_nym_nodes( + &self, + ) -> Option>>> { + self.get(|c| &c.nym_nodes).await + } + + pub async fn legacy_mixnodes_all(&self) -> Vec { + self.get_owned(|cache| cache.legacy_mixnodes.clone_cache()) + .await + .unwrap_or_default() + .into_inner() } - pub async fn mixnodes_filtered_basic(&self) -> Vec { - self.mixnodes_filtered() + pub async fn legacy_mixnodes_filtered_basic(&self) -> Vec { + self.legacy_mixnodes_filtered() .await .into_iter() .map(|bond| bond.bond_information) .collect() } - pub async fn mixnodes_all_basic(&self) -> Vec { - self.mixnodes_all() + pub async fn legacy_mixnodes_all_basic(&self) -> Vec { + self.legacy_mixnodes_all() .await .into_iter() .map(|bond| bond.bond_information) .collect() } - pub async fn gateways_filtered(&self) -> Vec { - let gateways = self.gateways_all().await; + pub async fn legacy_gateways_filtered(&self) -> Vec { + let gateways = self.legacy_gateways_all().await; if gateways.is_empty() { return Vec::new(); } @@ -199,107 +240,160 @@ impl NymContractCache { if !blacklist.is_empty() { gateways .into_iter() - .filter(|mix| !blacklist.contains(mix.identity())) + .filter(|gw| !blacklist.contains(&gw.node_id)) .collect() } else { gateways } } - pub async fn gateways_all(&self) -> Vec { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.gateways.clone(), - Err(err) => { - error!("{err}"); - Vec::new() - } - } + pub async fn legacy_gateways_all(&self) -> Vec { + self.get_owned(|cache| cache.legacy_gateways.clone_cache()) + .await + .unwrap_or_default() + .into_inner() } - pub async fn rewarded_set(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.rewarded_set.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(Vec::new()) - } - } + pub async fn nym_nodes(&self) -> Vec { + self.get_owned(|cache| cache.nym_nodes.clone_cache()) + .await + .unwrap_or_default() + .into_inner() } - pub async fn active_set(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.active_set.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(Vec::new()) - } - } + pub async fn rewarded_set(&self) -> Option>> { + self.get(|cache| &cache.rewarded_set).await } - pub async fn mix_to_family(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.mix_to_family.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(Vec::new()) - } + pub async fn rewarded_set_owned(&self) -> Cache { + self.get_owned(|cache| cache.rewarded_set.clone_cache()) + .await + .unwrap_or_default() + } + + pub async fn legacy_v1_rewarded_set_mixnodes(&self) -> Vec { + let Some(rewarded_set) = self.rewarded_set().await else { + return Vec::new(); + }; + + let mut rewarded_nodes = rewarded_set + .active_mixnodes() + .into_iter() + .collect::>(); + + // rewarded mixnode = active or standby + for standby in &rewarded_set.standby { + rewarded_nodes.insert(*standby); } + + self.legacy_mixnodes_all() + .await + .into_iter() + .filter(|m| rewarded_nodes.contains(&m.mix_id())) + .collect() + } + + pub async fn legacy_v1_active_set_mixnodes(&self) -> Vec { + let Some(rewarded_set) = self.rewarded_set().await else { + return Vec::new(); + }; + + let active_nodes = rewarded_set + .active_mixnodes() + .into_iter() + .collect::>(); + + self.legacy_mixnodes_all() + .await + .into_iter() + .filter(|m| active_nodes.contains(&m.mix_id())) + .collect() } pub(crate) async fn interval_reward_params(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.current_reward_params.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(None) - } - } + self.get_owned(|cache| cache.current_reward_params.clone_cache()) + .await + .unwrap_or_default() } pub(crate) async fn current_interval(&self) -> Cache> { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.current_interval.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::new(None) - } - } + self.get_owned(|cache| cache.current_interval.clone_cache()) + .await + .unwrap_or_default() } pub(crate) async fn contract_details(&self) -> Cache { - match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.contracts_info.clone_cache(), - Err(err) => { - error!("{err}"); - Cache::default() - } - } + self.get_owned(|cache| cache.contracts_info.clone_cache()) + .await + .unwrap_or_default() } - pub async fn mixnode_details(&self, mix_id: MixId) -> (Option, MixnodeStatus) { - // it might not be the most optimal to possibly iterate the entire vector to find (or not) - // the relevant value. However, the vectors are relatively small (< 10_000 elements, < 1000 for active set) - - let active_set = &self.active_set().await; - if let Some(bond) = active_set.iter().find(|mix| mix.mix_id() == mix_id) { + pub async fn legacy_mixnode_details( + &self, + mix_id: NodeId, + ) -> (Option, MixnodeStatus) { + // the old behaviour was to get the nodes from the filtered list, so let's not change it here + let rewarded_set = self.rewarded_set_owned().await; + let all_bonded = &self.legacy_mixnodes_filtered().await; + let Some(bond) = all_bonded.iter().find(|mix| mix.mix_id() == mix_id) else { + return (None, MixnodeStatus::NotFound); + }; + + if rewarded_set.is_active_mixnode(&mix_id) { return (Some(bond.clone()), MixnodeStatus::Active); } - let rewarded_set = &self.rewarded_set().await; - if let Some(bond) = rewarded_set.iter().find(|mix| mix.mix_id() == mix_id) { + if rewarded_set.is_standby(&mix_id) { return (Some(bond.clone()), MixnodeStatus::Standby); } - let all_bonded = &self.mixnodes_filtered().await; - if let Some(bond) = all_bonded.iter().find(|mix| mix.mix_id() == mix_id) { - (Some(bond.clone()), MixnodeStatus::Inactive) - } else { - (None, MixnodeStatus::NotFound) - } + (Some(bond.clone()), MixnodeStatus::Inactive) } - pub async fn mixnode_status(&self, mix_id: MixId) -> MixnodeStatus { - self.mixnode_details(mix_id).await.1 + pub async fn mixnode_status(&self, mix_id: NodeId) -> MixnodeStatus { + self.legacy_mixnode_details(mix_id).await.1 + } + + pub async fn get_node_refresh_data( + &self, + node_identity: ed25519::PublicKey, + ) -> Option { + if !self.initialised() { + return None; + } + + let inner = self.inner.read().await; + + let encoded_identity = node_identity.to_base58_string(); + + // 1. check nymnodes + if let Some(nym_node) = inner + .nym_nodes + .iter() + .find(|n| n.bond_information.identity() == encoded_identity) + { + return Some(nym_node.into()); + } + + // 2. check legacy mixnodes + if let Some(mixnode) = inner + .legacy_mixnodes + .iter() + .find(|n| n.bond_information.identity() == encoded_identity) + { + return Some(mixnode.into()); + } + + // 3. check legacy gateways + if let Some(gateway) = inner + .legacy_gateways + .iter() + .find(|n| n.identity() == &encoded_identity) + { + return Some(gateway.into()); + } + + None } pub fn initialised(&self) -> bool { diff --git a/nym-api/src/nym_contract_cache/cache/refresher.rs b/nym-api/src/nym_contract_cache/cache/refresher.rs index d200a3445d..badf0e2344 100644 --- a/nym-api/src/nym_contract_cache/cache/refresher.rs +++ b/nym-api/src/nym_contract_cache/cache/refresher.rs @@ -6,14 +6,21 @@ use crate::nym_contract_cache::cache::data::{CachedContractInfo, CachedContracts use crate::nyxd::Client; use crate::support::caching::CacheNotification; use anyhow::Result; -use nym_mixnet_contract_common::{MixId, MixNodeDetails, RewardedSetNodeStatus}; +use nym_api_requests::legacy::{ + LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer, +}; +use nym_mixnet_contract_common::{LegacyMixLayer, RewardedSet}; use nym_task::TaskClient; use nym_validator_client::nyxd::contract_traits::{ MixnetQueryClient, NymContractsProvider, VestingQueryClient, }; +use rand::prelude::SliceRandom; +use rand::rngs::OsRng; +use std::collections::HashSet; use std::{collections::HashMap, sync::atomic::Ordering, time::Duration}; use tokio::sync::watch; use tokio::time; +use tracing::{error, info, trace, warn}; pub struct NymContractCacheRefresher { nyxd_client: Client, @@ -109,33 +116,84 @@ impl NymContractCacheRefresher { let rewarding_params = self.nyxd_client.get_current_rewarding_parameters().await?; let current_interval = self.nyxd_client.get_current_interval().await?.interval; - let mixnodes = self.nyxd_client.get_mixnodes().await?; - let gateways = self.nyxd_client.get_gateways().await?; + let nym_nodes = self.nyxd_client.get_nymnodes().await?; + let mixnode_details = self.nyxd_client.get_mixnodes().await?; + let gateway_bonds = self.nyxd_client.get_gateways().await?; + let gateway_ids: HashMap<_, _> = self + .nyxd_client + .get_gateway_ids() + .await? + .into_iter() + .map(|id| (id.identity, id.node_id)) + .collect(); + + let mut gateways = Vec::with_capacity(gateway_bonds.len()); + for bond in gateway_bonds { + // we explicitly panic here because that value MUST exist. + // if it doesn't, we messed up the migration and we have big problems + let node_id = *gateway_ids.get(bond.identity()).unwrap_or_else(|| { + panic!( + "CONTRACT DATA INCONSISTENCY: MISSING GATEWAY ID FOR: {}", + bond.identity() + ) + }); + gateways.push(LegacyGatewayBondWithId { bond, node_id }) + } - let mix_to_family = self.nyxd_client.get_all_family_members().await?; + let rewarded_set = self.get_rewarded_set().await; + let layer1 = rewarded_set.layer1.iter().collect::>(); + let layer2 = rewarded_set.layer2.iter().collect::>(); + let layer3 = rewarded_set.layer3.iter().collect::>(); - let rewarded_set_map = self.get_rewarded_set_map().await; + let layer_choices = [ + LegacyMixLayer::One, + LegacyMixLayer::Two, + LegacyMixLayer::Three, + ]; + let mut rng = OsRng; + let mut mixnodes = Vec::with_capacity(mixnode_details.len()); + for detail in mixnode_details { + // if node is not in the rewarded set, well. + // slap a random layer on it because legacy clients don't understand a concept of layerless mixnodes + let layer = if layer1.contains(&detail.mix_id()) { + LegacyMixLayer::One + } else if layer2.contains(&detail.mix_id()) { + LegacyMixLayer::Two + } else if layer3.contains(&detail.mix_id()) { + LegacyMixLayer::Three + } else { + // SAFETY: the slice is not empty so the unwrap is fine + #[allow(clippy::unwrap_used)] + layer_choices.choose(&mut rng).copied().unwrap() + }; - let (rewarded_set, active_set) = - Self::collect_rewarded_and_active_set_details(&mixnodes, &rewarded_set_map); + mixnodes.push(LegacyMixNodeDetailsWithLayer { + bond_information: LegacyMixNodeBondWithLayer { + bond: detail.bond_information, + layer, + }, + rewarding_details: detail.rewarding_details, + pending_changes: detail.pending_changes.into(), + }) + } let contract_info = self.get_nym_contracts_info().await?; info!( - "Updating validator cache. There are {} mixnodes and {} gateways", + "Updating validator cache. There are {} [legacy] mixnodes, {} [legacy] gateways and {} nym nodes", mixnodes.len(), gateways.len(), + nym_nodes.len(), ); self.cache .update( mixnodes, gateways, + nym_nodes, rewarded_set, - active_set, rewarding_params, current_interval, - mix_to_family, contract_info, ) .await; @@ -147,32 +205,31 @@ impl NymContractCacheRefresher { Ok(()) } - async fn get_rewarded_set_map(&self) -> HashMap { + async fn get_rewarded_set(&self) -> RewardedSet { self.nyxd_client - .get_rewarded_set_mixnodes() + .get_rewarded_set_nodes() .await - .map(|nodes| nodes.into_iter().collect()) .unwrap_or_default() } - fn collect_rewarded_and_active_set_details( - all_mixnodes: &[MixNodeDetails], - rewarded_set_nodes: &HashMap, - ) -> (Vec, Vec) { - let mut active_set = Vec::new(); - let mut rewarded_set = Vec::new(); - - for mix in all_mixnodes { - if let Some(status) = rewarded_set_nodes.get(&mix.mix_id()) { - rewarded_set.push(mix.clone()); - if status.is_active() { - active_set.push(mix.clone()) - } - } - } - - (rewarded_set, active_set) - } + // fn collect_rewarded_and_active_set_details( + // all_mixnodes: &[MixNodeDetails], + // rewarded_set_nodes: RewardedSet, + // ) -> (Vec, Vec) { + // let mut active_set = Vec::new(); + // let mut rewarded_set = Vec::new(); + // + // for mix in all_mixnodes { + // if let Some(status) = rewarded_set_nodes.get(&mix.mix_id()) { + // rewarded_set.push(mix.clone()); + // if status.is_active() { + // active_set.push(mix.clone()) + // } + // } + // } + // + // (rewarded_set, active_set) + // } pub(crate) async fn run(&self, mut shutdown: TaskClient) { let mut interval = time::interval(self.caching_interval); diff --git a/nym-api/src/nym_contract_cache/handlers.rs b/nym-api/src/nym_contract_cache/handlers.rs index 84f2c70fd2..00f02e1fe2 100644 --- a/nym-api/src/nym_contract_cache/handlers.rs +++ b/nym-api/src/nym_contract_cache/handlers.rs @@ -1,20 +1,23 @@ // Copyright 2021-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::{ - node_status_api::helpers_deprecated::{ - _get_active_set_detailed, _get_mixnodes_detailed, _get_rewarded_set_detailed, - }, - v2::AxumAppState, +use crate::node_status_api::helpers::{ + _get_active_set_legacy_mixnodes_detailed, _get_legacy_mixnodes_detailed, + _get_rewarded_set_legacy_mixnodes_detailed, }; -use axum::{extract, Router}; +use crate::support::http::state::AppState; +use crate::support::legacy_helpers::{to_legacy_gateway, to_legacy_mixnode}; +use axum::extract::State; +use axum::{Json, Router}; +use nym_api_requests::legacy::LegacyMixNodeDetailsWithLayer; use nym_api_requests::models::MixNodeBondAnnotated; -use nym_mixnet_contract_common::{ - mixnode::MixNodeDetails, reward_params::RewardingParams, GatewayBond, Interval, MixId, -}; +use nym_mixnet_contract_common::reward_params::Performance; +use nym_mixnet_contract_common::{reward_params::RewardingParams, GatewayBond, Interval, NodeId}; use std::collections::HashSet; -pub(crate) fn nym_contract_cache_routes() -> Router { +// we want to mark the routes as deprecated in swagger, but still expose them +#[allow(deprecated)] +pub(crate) fn nym_contract_cache_routes() -> Router { Router::new() .route("/mixnodes", axum::routing::get(get_mixnodes)) .route( @@ -52,13 +55,52 @@ pub(crate) fn nym_contract_cache_routes() -> Router { get, path = "/v1/mixnodes", responses( - (status = 200, body = Vec) + (status = 200, body = Vec) ) )] -async fn get_mixnodes( - extract::State(state): extract::State, -) -> axum::Json> { - state.nym_contract_cache().mixnodes_filtered().await.into() +#[deprecated] +async fn get_mixnodes(State(state): State) -> Json> { + let mut out = state.nym_contract_cache().legacy_mixnodes_filtered().await; + + let Ok(describe_cache) = state.described_nodes_cache.get().await else { + return Json(out); + }; + + let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { + return Json(out); + }; + + let Ok(annotations) = state.node_annotations().await else { + return Json(out); + }; + + // safety: valid percentage value + #[allow(clippy::unwrap_used)] + let p50 = Performance::from_percentage_value(50).unwrap(); + + for nym_node in &**migrated_nymnodes { + // if we can't get it self-described data, ignore it + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a mixnode, ignore it + if !description.declared_role.mixnode { + continue; + } + // if we don't have annotation for this node, ignore it + let Some(annotation) = annotations.get(&nym_node.node_id()) else { + continue; + }; + // equivalent of legacy mixnode being blacklisted + if annotation.last_24h_performance < p50 { + continue; + } + + let node = to_legacy_mixnode(nym_node, description); + out.push(node); + } + + Json(out) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -76,10 +118,9 @@ async fn get_mixnodes( (status = 200, body = Vec) ) )] -async fn get_mixnodes_detailed( - extract::State(state): extract::State, -) -> axum::Json> { - _get_mixnodes_detailed(state.node_status_cache()) +#[deprecated] +async fn get_mixnodes_detailed(State(state): State) -> Json> { + _get_legacy_mixnodes_detailed(state.node_status_cache()) .await .into() } @@ -92,10 +133,56 @@ async fn get_mixnodes_detailed( (status = 200, body = Vec) ) )] -async fn get_gateways( - extract::State(state): extract::State, -) -> axum::Json> { - state.nym_contract_cache().gateways_filtered().await.into() +#[deprecated] +async fn get_gateways(State(state): State) -> Json> { + // legacy + let mut out: Vec = state + .nym_contract_cache() + .legacy_gateways_filtered() + .await + .into_iter() + .map(Into::into) + .collect(); + + let Ok(describe_cache) = state.described_nodes_cache.get().await else { + return Json(out); + }; + + let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { + return Json(out); + }; + + let Ok(annotations) = state.node_annotations().await else { + return Json(out); + }; + + // safety: valid percentage value + #[allow(clippy::unwrap_used)] + let p50 = Performance::from_percentage_value(50).unwrap(); + + for nym_node in &**migrated_nymnodes { + // if we can't get it self-described data, ignore it + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a gateway, ignore it + if !description.declared_role.entry { + continue; + } + // if we don't have annotation for this node, ignore it + let Some(annotation) = annotations.get(&nym_node.node_id()) else { + continue; + }; + // equivalent of legacy gateway being blacklisted + if annotation.last_24h_performance < p50 { + continue; + } + + let node = to_legacy_gateway(nym_node, description); + out.push(node); + } + + Json(out) } #[utoipa::path( @@ -103,18 +190,20 @@ async fn get_gateways( get, path = "/v1/mixnodes/rewarded", responses( - (status = 200, body = Vec) + (status = 200, body = Vec) ) )] +#[deprecated] async fn get_rewarded_set( - extract::State(state): extract::State, -) -> axum::Json> { - state - .nym_contract_cache() - .rewarded_set() - .await - .to_owned() - .into() + State(state): State, +) -> Json> { + Json( + state + .nym_contract_cache() + .legacy_v1_rewarded_set_mixnodes() + .await + .clone(), + ) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -132,12 +221,16 @@ async fn get_rewarded_set( (status = 200, body = Vec) ) )] +#[deprecated] async fn get_rewarded_set_detailed( - extract::State(state): extract::State, -) -> axum::Json> { - _get_rewarded_set_detailed(state.node_status_cache()) - .await - .into() + State(state): State, +) -> Json> { + _get_rewarded_set_legacy_mixnodes_detailed( + state.node_status_cache(), + state.nym_contract_cache(), + ) + .await + .into() } #[utoipa::path( @@ -145,18 +238,64 @@ async fn get_rewarded_set_detailed( get, path = "/v1/mixnodes/active", responses( - (status = 200, body = Vec) + (status = 200, body = Vec) ) )] -async fn get_active_set( - extract::State(state): extract::State, -) -> axum::Json> { - state +#[deprecated] +async fn get_active_set(State(state): State) -> Json> { + let mut out = state .nym_contract_cache() - .active_set() + .legacy_v1_active_set_mixnodes() .await - .to_owned() - .into() + .clone(); + + let Some(rewarded_set) = state.nym_contract_cache().rewarded_set().await else { + return Json(out); + }; + + let Ok(describe_cache) = state.described_nodes_cache.get().await else { + return Json(out); + }; + + let Some(migrated_nymnodes) = state.nym_contract_cache().all_cached_nym_nodes().await else { + return Json(out); + }; + + let Ok(annotations) = state.node_annotations().await else { + return Json(out); + }; + + // safety: valid percentage value + #[allow(clippy::unwrap_used)] + let p50 = Performance::from_percentage_value(50).unwrap(); + + for nym_node in &**migrated_nymnodes { + // if we can't get it self-described data, ignore it + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a mixnode, ignore it + if !description.declared_role.mixnode { + continue; + } + // if we don't have annotation for this node, ignore it + let Some(annotation) = annotations.get(&nym_node.node_id()) else { + continue; + }; + // equivalent of legacy mixnode being blacklisted + if annotation.last_24h_performance < p50 { + continue; + } + // if the node is not in the active set, ignore it + if !rewarded_set.is_active_mixnode(&nym_node.node_id()) { + continue; + } + + let node = to_legacy_mixnode(nym_node, description); + out.push(node); + } + + Json(out) } // DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, @@ -175,10 +314,9 @@ async fn get_active_set( (status = 200, body = Vec) ) )] -async fn get_active_set_detailed( - extract::State(state): extract::State, -) -> axum::Json> { - _get_active_set_detailed(state.node_status_cache()) +#[deprecated] +async fn get_active_set_detailed(State(state): State) -> Json> { + _get_active_set_legacy_mixnodes_detailed(state.node_status_cache(), state.nym_contract_cache()) .await .into() } @@ -188,12 +326,11 @@ async fn get_active_set_detailed( get, path = "/v1/mixnodes/blacklisted", responses( - (status = 200, body = Option>) + (status = 200, body = Option>) ) )] -async fn get_blacklisted_mixnodes( - extract::State(state): extract::State, -) -> axum::Json>> { +#[deprecated] +async fn get_blacklisted_mixnodes(State(state): State) -> Json>> { let blacklist = state .nym_contract_cache() .mixnodes_blacklist() @@ -215,20 +352,22 @@ async fn get_blacklisted_mixnodes( (status = 200, body = Option>) ) )] -async fn get_blacklisted_gateways( - extract::State(state): extract::State, -) -> axum::Json>> { - let blacklist = state - .nym_contract_cache() - .gateways_blacklist() - .await - .to_owned(); +#[deprecated] +async fn get_blacklisted_gateways(State(state): State) -> Json>> { + let cache = state.nym_contract_cache(); + let blacklist = cache.gateways_blacklist().await.clone(); if blacklist.is_empty() { - None + Json(None) } else { - Some(blacklist) + let gateways = cache.legacy_gateways_all().await; + Json(Some( + gateways + .into_iter() + .filter(|g| blacklist.contains(&g.node_id)) + .map(|g| g.gateway.identity_key.clone()) + .collect(), + )) } - .into() } #[utoipa::path( @@ -240,8 +379,8 @@ async fn get_blacklisted_gateways( ) )] async fn get_interval_reward_params( - extract::State(state): extract::State, -) -> axum::Json> { + State(state): State, +) -> Json> { state .nym_contract_cache() .interval_reward_params() @@ -258,9 +397,7 @@ async fn get_interval_reward_params( (status = 200, body = Option) ) )] -async fn get_current_epoch( - extract::State(state): extract::State, -) -> axum::Json> { +async fn get_current_epoch(State(state): State) -> Json> { state .nym_contract_cache() .current_interval() diff --git a/nym-api/src/nym_contract_cache/mod.rs b/nym-api/src/nym_contract_cache/mod.rs index 9b1f48d9ad..6ca509ef1e 100644 --- a/nym-api/src/nym_contract_cache/mod.rs +++ b/nym-api/src/nym_contract_cache/mod.rs @@ -4,33 +4,11 @@ use crate::nym_contract_cache::cache::NymContractCache; use crate::support::{self, config, nyxd}; use nym_task::TaskManager; -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; use self::cache::refresher::NymContractCacheRefresher; pub(crate) mod cache; -#[cfg(feature = "axum")] pub(crate) mod handlers; -pub(crate) mod routes; - -pub(crate) fn nym_contract_cache_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: routes::get_mixnodes, - routes::get_mixnodes_detailed, - routes::get_gateways, - routes::get_active_set, - routes::get_active_set_detailed, - routes::get_rewarded_set, - routes::get_rewarded_set_detailed, - routes::get_blacklisted_mixnodes, - routes::get_blacklisted_gateways, - routes::get_interval_reward_params, - routes::get_current_epoch, - ] -} pub(crate) fn start_refresher( config: &config::NodeStatusAPI, diff --git a/nym-api/src/nym_contract_cache/routes.rs b/nym-api/src/nym_contract_cache/routes.rs deleted file mode 100644 index cef7f04c9c..0000000000 --- a/nym-api/src/nym_contract_cache/routes.rs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright 2021-2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::{ - node_status_api::{ - helpers_deprecated::{ - _get_active_set_detailed, _get_mixnodes_detailed, _get_rewarded_set_detailed, - }, - NodeStatusCache, - }, - nym_contract_cache::cache::NymContractCache, -}; -use nym_api_requests::models::MixNodeBondAnnotated; -use nym_mixnet_contract_common::{ - mixnode::MixNodeDetails, reward_params::RewardingParams, GatewayBond, Interval, MixId, -}; - -use rocket::{serde::json::Json, State}; -use rocket_okapi::openapi; -use std::collections::HashSet; - -#[openapi(tag = "contract-cache")] -#[get("/mixnodes")] -pub async fn get_mixnodes(cache: &State) -> Json> { - Json(cache.mixnodes_filtered().await) -} - -// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, -// replace this with -// ``` -// pub fn get_mixnodes_detailed() -> Redirect { -// Redirect::to(uri!("/v1/status/mixnodes/detailed")) -// } -// ``` -#[openapi(tag = "contract-cache")] -#[get("/mixnodes/detailed")] -pub async fn get_mixnodes_detailed( - cache: &State, -) -> Json> { - Json(_get_mixnodes_detailed(cache).await) -} - -#[openapi(tag = "contract-cache")] -#[get("/gateways")] -pub async fn get_gateways(cache: &State) -> Json> { - Json(cache.gateways_filtered().await) -} - -#[openapi(tag = "contract-cache")] -#[get("/mixnodes/rewarded")] -pub async fn get_rewarded_set(cache: &State) -> Json> { - Json(cache.rewarded_set().await.clone()) -} - -// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, -// replace this with -// ``` -// pub fn get_mixnodes_set_detailed() -> Redirect { -// Redirect::to(uri!("/v1/status/mixnodes/rewarded/detailed")) -// } -// ``` -#[openapi(tag = "contract-cache")] -#[get("/mixnodes/rewarded/detailed")] -pub async fn get_rewarded_set_detailed( - cache: &State, -) -> Json> { - Json(_get_rewarded_set_detailed(cache).await) -} - -#[openapi(tag = "contract-cache")] -#[get("/mixnodes/active")] -pub async fn get_active_set(cache: &State) -> Json> { - Json(cache.active_set().await.clone()) -} - -// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated, -// replace this with -// ``` -// pub fn get_active_set_detailed() -> Redirect { -// Redirect::to(uri!("/status/mixnodes/active/detailed")) -// } -// ``` -#[openapi(tag = "contract-cache")] -#[get("/mixnodes/active/detailed")] -pub async fn get_active_set_detailed( - cache: &State, -) -> Json> { - Json(_get_active_set_detailed(cache).await) -} - -#[openapi(tag = "contract-cache")] -#[get("/mixnodes/blacklisted")] -pub async fn get_blacklisted_mixnodes( - cache: &State, -) -> Json>> { - let blacklist = cache.mixnodes_blacklist().await.clone(); - if blacklist.is_empty() { - Json(None) - } else { - Json(Some(blacklist)) - } -} - -#[openapi(tag = "contract-cache")] -#[get("/gateways/blacklisted")] -pub async fn get_blacklisted_gateways( - cache: &State, -) -> Json>> { - let blacklist = cache.gateways_blacklist().await.clone(); - if blacklist.is_empty() { - Json(None) - } else { - Json(Some(blacklist)) - } -} - -#[openapi(tag = "contract-cache")] -#[get("/epoch/reward_params")] -pub async fn get_interval_reward_params( - cache: &State, -) -> Json> { - Json(*cache.interval_reward_params().await) -} - -#[openapi(tag = "contract-cache")] -#[get("/epoch/current")] -pub async fn get_current_epoch(cache: &State) -> Json> { - Json(*cache.current_interval().await) -} diff --git a/nym-api/src/nym_nodes/handlers.rs b/nym-api/src/nym_nodes/handlers.rs deleted file mode 100644 index 7f775d8de4..0000000000 --- a/nym-api/src/nym_nodes/handlers.rs +++ /dev/null @@ -1,95 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::v2::AxumAppState; -use axum::{extract::State, Json, Router}; -use nym_api_requests::models::{DescribedGateway, DescribedMixNode}; -use nym_mixnet_contract_common::MixNodeBond; -use std::ops::Deref; - -// obviously this should get refactored later on because gateways will go away. -// unless maybe this will be filtering based on which nodes got assigned gateway role? TBD - -pub(crate) fn nym_node_routes() -> axum::Router { - Router::new() - .route( - "/gateways/described", - axum::routing::get(get_gateways_described), - ) - .route( - "/mixnodes/described", - axum::routing::get(get_mixnodes_described), - ) -} - -#[utoipa::path( - tag = "Nym Nodes", - get, - path = "/v1/gateways/described", - responses( - (status = 200, body = Vec) - ) -)] -async fn get_gateways_described(State(state): State) -> Json> { - let gateways = state.nym_contract_cache().gateways_filtered().await; - if gateways.is_empty() { - return Json(Vec::new()); - } - - // if the self describe cache is unavailable, well, don't attach describe data - let Ok(self_descriptions) = state.described_nodes_state().get().await else { - return Json(gateways.into_iter().map(Into::into).collect()); - }; - - // TODO: this is extremely inefficient, but given we don't have many gateways, - // it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo, - // but at that point (I hope) the whole caching situation should get refactored - Json( - gateways - .into_iter() - .map(|bond| DescribedGateway { - self_described: self_descriptions.deref().get(bond.identity()).cloned(), - bond, - }) - .collect(), - ) -} - -#[utoipa::path( - tag = "Nym Nodes", - get, - path = "/v1/mixnodes/described", - responses( - (status = 200, body = Vec) - ) -)] -async fn get_mixnodes_described(State(state): State) -> Json> { - let mixnodes = state - .nym_contract_cache() - .mixnodes_filtered() - .await - .into_iter() - .map(|m| m.bond_information) - .collect::>(); - if mixnodes.is_empty() { - return Json(Vec::new()); - } - - // if the self describe cache is unavailable, well, don't attach describe data - let Ok(self_descriptions) = state.described_nodes_state().get().await else { - return Json(mixnodes.into_iter().map(Into::into).collect()); - }; - - // TODO: this is extremely inefficient, but given we don't have many gateways, - // it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo, - // but at that point (I hope) the whole caching situation should get refactored - Json( - mixnodes - .into_iter() - .map(|bond| DescribedMixNode { - self_described: self_descriptions.deref().get(bond.identity()).cloned(), - bond, - }) - .collect(), - ) -} diff --git a/nym-api/src/nym_nodes/handlers/legacy.rs b/nym-api/src/nym_nodes/handlers/legacy.rs new file mode 100644 index 0000000000..3dd6a51f74 --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/legacy.rs @@ -0,0 +1,133 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::support::http::state::AppState; +use crate::support::legacy_helpers::{to_legacy_gateway, to_legacy_mixnode}; +use axum::extract::State; +use axum::{Json, Router}; +use nym_api_requests::legacy::LegacyMixNodeBondWithLayer; +use nym_api_requests::models::{LegacyDescribedGateway, LegacyDescribedMixNode}; + +// we want to mark the routes as deprecated in swagger, but still expose them +#[allow(deprecated)] +pub(crate) fn legacy_nym_node_routes() -> Router { + Router::new() + .route( + "/gateways/described", + axum::routing::get(get_gateways_described), + ) + .route( + "/mixnodes/described", + axum::routing::get(get_mixnodes_described), + ) +} + +#[utoipa::path( + tag = "Legacy gateways", + get, + path = "/v1/gateways/described", + responses( + (status = 200, body = Vec) + ) +)] +#[deprecated] +async fn get_gateways_described( + State(state): State, +) -> Json> { + let contract_cache = state.nym_contract_cache(); + let describe_cache = state.described_nodes_cache(); + + // legacy + let legacy = contract_cache.legacy_gateways_filtered().await; + + // if the self describe cache is unavailable, well, don't attach describe data and only return legacy gateways + let Ok(describe_cache) = describe_cache.get().await else { + return Json(legacy.into_iter().map(Into::into).collect()); + }; + + let migrated_nymnodes = state.nym_contract_cache().nym_nodes().await; + let mut out = Vec::new(); + + for legacy_bond in legacy { + out.push(LegacyDescribedGateway { + self_described: describe_cache + .get_description(&legacy_bond.node_id) + .cloned(), + bond: legacy_bond.bond, + }) + } + + for nym_node in migrated_nymnodes { + // we ALWAYS need description to set legacy fields + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a gateway, ignore it + if !description.declared_role.entry { + continue; + } + + out.push(LegacyDescribedGateway { + bond: to_legacy_gateway(&nym_node, description), + self_described: Some(description.clone()), + }) + } + + Json(out) +} + +#[utoipa::path( + tag = "Legacy Mixnodes", + get, + path = "/v1/mixnodes/described", + responses( + (status = 200, body = Vec) + ) +)] +#[deprecated] +async fn get_mixnodes_described( + State(state): State, +) -> Json> { + let contract_cache = state.nym_contract_cache(); + let describe_cache = state.described_nodes_cache(); + + let legacy: Vec = contract_cache + .legacy_mixnodes_filtered() + .await + .into_iter() + .map(|m| m.bond_information) + .collect::>(); + + // if the self describe cache is unavailable, well, don't attach describe data and only return legacy mixnodes + let Ok(describe_cache) = describe_cache.get().await else { + return Json(legacy.into_iter().map(Into::into).collect()); + }; + + let migrated_nymnodes = state.nym_contract_cache().nym_nodes().await; + let mut out = Vec::new(); + + for legacy_bond in legacy { + out.push(LegacyDescribedMixNode { + self_described: describe_cache.get_description(&legacy_bond.mix_id).cloned(), + bond: legacy_bond, + }) + } + + for nym_node in migrated_nymnodes { + // we ALWAYS need description to set legacy fields + let Some(description) = describe_cache.get_description(&nym_node.node_id()) else { + continue; + }; + // if the node hasn't declared it can be a gateway, ignore it + if !description.declared_role.mixnode { + continue; + } + + out.push(LegacyDescribedMixNode { + bond: to_legacy_mixnode(&nym_node, description).bond_information, + self_described: Some(description.clone()), + }) + } + + Json(out) +} diff --git a/nym-api/src/nym_nodes/handlers/mod.rs b/nym-api/src/nym_nodes/handlers/mod.rs new file mode 100644 index 0000000000..42cfee3c86 --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/mod.rs @@ -0,0 +1,384 @@ +// Copyright 2023 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::support::http::helpers::{NodeIdParam, PaginationRequest}; +use crate::support::http::state::AppState; +use axum::extract::{Path, Query, State}; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use nym_api_requests::models::{ + AnnotationResponse, NodeDatePerformanceResponse, NodePerformanceResponse, NodeRefreshBody, + NoiseDetails, NymNodeDescription, PerformanceHistoryResponse, UptimeHistoryResponse, +}; +use nym_api_requests::pagination::{PaginatedResponse, Pagination}; +use nym_contracts_common::NaiveFloat; +use nym_mixnet_contract_common::reward_params::Performance; +use nym_mixnet_contract_common::NymNodeDetails; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::time::Duration; +use time::{Date, OffsetDateTime}; +use utoipa::{IntoParams, ToSchema}; + +pub(crate) mod legacy; +pub(crate) mod unstable; + +pub(crate) fn nym_node_routes() -> Router { + Router::new() + .route("/refresh-described", post(refresh_described)) + .route("/noise", get(nodes_noise)) + .route("/bonded", get(get_bonded_nodes)) + .route("/described", get(get_described_nodes)) + .route("/annotation/:node_id", get(get_node_annotation)) + .route("/performance/:node_id", get(get_current_node_performance)) + .route( + "/historical-performance/:node_id", + get(get_historical_performance), + ) + .route( + "/performance-history/:node_id", + get(get_node_performance_history), + ) + // to make it compatible with all the explorers that were used to using 0-100 values + .route("/uptime-history/:node_id", get(get_node_uptime_history)) +} + +#[utoipa::path( + tag = "Nym Nodes", + post, + request_body = NodeRefreshBody, + path = "/refresh-described", + context_path = "/v1/nym-nodes", +)] +async fn refresh_described( + State(state): State, + Json(request_body): Json, +) -> AxumResult> { + let Some(refresh_data) = state + .nym_contract_cache() + .get_node_refresh_data(request_body.node_identity) + .await + else { + return Err(AxumErrorResponse::not_found(format!( + "node with identity {} does not seem to exist", + request_body.node_identity + ))); + }; + + if !request_body.verify_signature() { + return Err(AxumErrorResponse::unauthorised("invalid request signature")); + } + + if request_body.is_stale() { + return Err(AxumErrorResponse::bad_request("the request is stale")); + } + + let node_id = refresh_data.node_id(); + if let Some(last) = state.forced_refresh.last_refreshed(node_id).await { + // max 1 refresh a minute + let minute_ago = OffsetDateTime::now_utc() - Duration::from_secs(60); + if last > minute_ago { + return Err(AxumErrorResponse::too_many( + "already refreshed node in the last minute", + )); + } + } + // to make sure you can't ddos the endpoint while a request is in progress + state.forced_refresh.set_last_refreshed(node_id).await; + let allow_all_ips = state.forced_refresh.allow_all_ip_addresses; + + if let Some(updated_data) = refresh_data.try_refresh(allow_all_ips).await { + let Ok(mut describe_cache) = state.described_nodes_cache.write().await else { + return Err(AxumErrorResponse::service_unavailable()); + }; + describe_cache.get_mut().force_update(updated_data) + } else { + return Err(AxumErrorResponse::unprocessable_entity( + "failed to refresh node description", + )); + } + + Ok(Json(())) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/noise", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = PaginatedResponse) + ), + params(PaginationRequest) +)] +async fn nodes_noise( + State(state): State, + Query(pagination): Query, +) -> AxumResult>> { + // TODO: implement it + let _ = pagination; + + let describe_cache = state.describe_nodes_cache_data().await?; + + let nodes = describe_cache + .all_nodes() + .filter_map(|n| { + n.description + .host_information + .keys + .x25519_noise + .map(|noise_key| (noise_key, n)) + }) + .map(|(noise_key, node)| NoiseDetails { + x25119_pubkey: noise_key, + mixnet_port: node.description.mix_port(), + ip_addresses: node.description.host_information.ip_address.clone(), + }) + .collect::>(); + + let total = nodes.len(); + + Ok(Json(PaginatedResponse { + pagination: Pagination { + total, + page: 0, + size: total, + }, + data: nodes, + })) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/bonded", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = PaginatedResponse) + ), + params(PaginationRequest) +)] +async fn get_bonded_nodes( + State(state): State, + Query(pagination): Query, +) -> Json> { + // TODO: implement it + let _ = pagination; + + let details = state.nym_contract_cache().nym_nodes().await; + let total = details.len(); + + Json(PaginatedResponse { + pagination: Pagination { + total, + page: 0, + size: total, + }, + data: details, + }) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/described", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = PaginatedResponse) + ), + params(PaginationRequest) +)] +async fn get_described_nodes( + State(state): State, + Query(pagination): Query, +) -> AxumResult>> { + // TODO: implement it + let _ = pagination; + + let cache = state.described_nodes_cache.get().await?; + let descriptions = cache.all_nodes().cloned().collect::>(); + + Ok(Json(PaginatedResponse { + pagination: Pagination { + total: descriptions.len(), + page: 0, + size: descriptions.len(), + }, + data: descriptions, + })) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/annotation/{node_id}", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = AnnotationResponse) + ), + params(NodeIdParam), +)] +async fn get_node_annotation( + Path(NodeIdParam { node_id }): Path, + State(state): State, +) -> AxumResult> { + let annotations = state + .node_status_cache + .node_annotations() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + Ok(Json(AnnotationResponse { + node_id, + annotation: annotations.get(&node_id).cloned(), + })) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/performance/{node_id}", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = NodePerformanceResponse) + ), + params(NodeIdParam), +)] +async fn get_current_node_performance( + Path(NodeIdParam { node_id }): Path, + State(state): State, +) -> AxumResult> { + let annotations = state + .node_status_cache + .node_annotations() + .await + .ok_or_else(AxumErrorResponse::internal)?; + + Ok(Json(NodePerformanceResponse { + node_id, + performance: annotations + .get(&node_id) + .map(|n| n.last_24h_performance.naive_to_f64()), + })) +} + +// todo; probably extract it to requests crate +#[derive(Debug, Serialize, Deserialize, Copy, Clone, IntoParams, ToSchema, JsonSchema)] +#[into_params(parameter_in = Query)] +pub(crate) struct DateQuery { + #[schema(value_type = String, example = "1970-01-01")] + #[schemars(with = "String")] + pub(crate) date: Date, +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/historical-performance/{node_id}", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = NodeDatePerformanceResponse) + ), + params(DateQuery, NodeIdParam) +)] +async fn get_historical_performance( + Path(NodeIdParam { node_id }): Path, + Query(DateQuery { date }): Query, + State(state): State, +) -> AxumResult> { + let uptime = state + .storage() + .get_historical_node_uptime_on(node_id, date) + .await?; + + Ok(Json(NodeDatePerformanceResponse { + node_id, + date, + performance: uptime.and_then(|u| { + Performance::from_percentage_value(u.uptime as u64) + .map(|p| p.naive_to_f64()) + .ok() + }), + })) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/performance-history/{node_id}", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = PerformanceHistoryResponse) + ), + params(PaginationRequest, NodeIdParam) +)] +async fn get_node_performance_history( + Path(NodeIdParam { node_id }): Path, + State(state): State, + Query(pagination): Query, +) -> AxumResult> { + // TODO: implement it + let _ = pagination; + + let history = state + .storage() + .get_node_uptime_history(node_id) + .await? + .into_iter() + .filter_map(|u| u.try_into().ok()) + .collect::>(); + let total = history.len(); + + Ok(Json(PerformanceHistoryResponse { + node_id, + history: PaginatedResponse { + pagination: Pagination { + total, + page: 0, + size: total, + }, + data: history, + }, + })) +} + +#[utoipa::path( + tag = "Nym Nodes", + get, + path = "/uptime-history/{node_id}", + context_path = "/v1/nym-nodes", + responses( + (status = 200, body = PerformanceHistoryResponse) + ), + params(PaginationRequest, NodeIdParam) +)] +async fn get_node_uptime_history( + Path(NodeIdParam { node_id }): Path, + State(state): State, + Query(pagination): Query, +) -> AxumResult> { + // TODO: implement it + let _ = pagination; + + let history = state + .storage() + .get_node_uptime_history(node_id) + .await? + .into_iter() + .filter_map(|u| u.try_into().ok()) + .collect::>(); + let total = history.len(); + + Ok(Json(UptimeHistoryResponse { + node_id, + history: PaginatedResponse { + pagination: Pagination { + total, + page: 0, + size: total, + }, + data: history, + }, + })) +} diff --git a/nym-api/src/nym_nodes/handlers/unstable/full_fat.rs b/nym-api/src/nym_nodes/handlers/unstable/full_fat.rs new file mode 100644 index 0000000000..d16d68e240 --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/unstable/full_fat.rs @@ -0,0 +1,27 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::nym_nodes::handlers::unstable::NodesParamsWithRole; +use crate::support::http::state::AppState; +use axum::extract::{Query, State}; +use axum::Json; +use nym_api_requests::nym_nodes::{CachedNodesResponse, FullFatNode}; + +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "/", + context_path = "/v1/unstable/nym-nodes/full-fat", + responses( + // (status = 200, body = CachedNodesResponse) + (status = 501) + ) +)] +pub(super) async fn nodes_detailed( + _state: State, + _query_params: Query, +) -> AxumResult>> { + Err(AxumErrorResponse::not_implemented()) +} diff --git a/nym-api/src/nym_nodes/handlers/unstable/helpers.rs b/nym-api/src/nym_nodes/handlers/unstable/helpers.rs new file mode 100644 index 0000000000..4f7a20155e --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/unstable/helpers.rs @@ -0,0 +1,71 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_api_requests::models::{ + GatewayBondAnnotated, MalformedNodeBond, MixNodeBondAnnotated, OffsetDateTimeJsonSchemaWrapper, +}; +use nym_api_requests::nym_nodes::{NodeRole, SkimmedNode}; +use nym_bin_common::version_checker; +use nym_mixnet_contract_common::reward_params::Performance; +use time::OffsetDateTime; + +pub(crate) trait LegacyAnnotation { + fn version(&self) -> &str; + + fn performance(&self) -> Performance; + + fn identity(&self) -> &str; + + fn try_to_skimmed_node(&self, role: NodeRole) -> Result; +} + +impl LegacyAnnotation for MixNodeBondAnnotated { + fn version(&self) -> &str { + self.version() + } + + fn performance(&self) -> Performance { + self.node_performance.last_24h + } + + fn identity(&self) -> &str { + self.identity_key() + } + + fn try_to_skimmed_node(&self, role: NodeRole) -> Result { + self.try_to_skimmed_node(role) + } +} + +impl LegacyAnnotation for GatewayBondAnnotated { + fn version(&self) -> &str { + self.version() + } + + fn performance(&self) -> Performance { + self.node_performance.last_24h + } + + fn identity(&self) -> &str { + self.identity() + } + + fn try_to_skimmed_node(&self, role: NodeRole) -> Result { + self.try_to_skimmed_node(role) + } +} + +pub(crate) fn refreshed_at( + iter: impl IntoIterator, +) -> OffsetDateTimeJsonSchemaWrapper { + iter.into_iter().min().unwrap().into() +} + +pub(crate) fn semver(requirement: &Option, declared: &str) -> bool { + if let Some(semver_compat) = requirement.as_ref() { + if !version_checker::is_minor_version_compatible(declared, semver_compat) { + return false; + } + } + true +} diff --git a/nym-api/src/nym_nodes/handlers/unstable/mod.rs b/nym-api/src/nym_nodes/handlers/unstable/mod.rs new file mode 100644 index 0000000000..a9110a599d --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/unstable/mod.rs @@ -0,0 +1,115 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +//! All routes/nodes are split into three tiers: +//! +//! `/skimmed` +//! - used by clients +//! - returns the very basic information for routing purposes +//! +//! `/semi-skimmed` +//! - used by other nodes/VPN +//! - returns more additional information such noise keys +//! +//! `/full-fat` +//! - used by explorers, et al. +//! - returns almost everything there is about the nodes +//! +//! There's also additional split based on the role: +//! - `?role` => filters based on the specific role (mixnode/gateway/(in the future: entry/exit)) +//! - `/mixnodes/` => only returns mixnode role data +//! - `/gateway/` => only returns (entry) gateway role data + +use crate::nym_nodes::handlers::unstable::full_fat::nodes_detailed; +use crate::nym_nodes::handlers::unstable::semi_skimmed::nodes_expanded; +use crate::nym_nodes::handlers::unstable::skimmed::{ + entry_gateways_basic_active, entry_gateways_basic_all, exit_gateways_basic_active, + exit_gateways_basic_all, mixnodes_basic_active, mixnodes_basic_all, nodes_basic_active, + nodes_basic_all, +}; +use crate::support::http::helpers::PaginationRequest; +use crate::support::http::state::AppState; +use axum::routing::get; +use axum::Router; +use nym_api_requests::nym_nodes::NodeRoleQueryParam; +use serde::Deserialize; + +pub(crate) mod full_fat; +mod helpers; +pub(crate) mod semi_skimmed; +pub(crate) mod skimmed; + +#[allow(deprecated)] +pub(crate) fn nym_node_routes_unstable() -> Router { + Router::new() + .nest( + "/skimmed", + Router::new() + .route("/", get(nodes_basic_all)) + .route("/active", get(nodes_basic_active)) + .nest( + "/mixnodes", + Router::new() + .route("/active", get(mixnodes_basic_active)) + .route("/all", get(mixnodes_basic_all)), + ) + .nest( + "/entry-gateways", + Router::new() + .route("/active", get(entry_gateways_basic_active)) + .route("/all", get(entry_gateways_basic_all)), + ) + .nest( + "/exit-gateways", + Router::new() + .route("/active", get(exit_gateways_basic_active)) + .route("/all", get(exit_gateways_basic_all)), + ), + ) + .nest( + "/semi-skimmed", + Router::new().route("/", get(nodes_expanded)), + ) + .nest("/full-fat", Router::new().route("/", get(nodes_detailed))) + .route("/gateways/skimmed", get(skimmed::deprecated_gateways_basic)) + .route("/mixnodes/skimmed", get(skimmed::deprecated_mixnodes_basic)) +} + +#[derive(Debug, Deserialize, utoipa::IntoParams)] +struct NodesParamsWithRole { + #[param(inline)] + role: Option, + + semver_compatibility: Option, + no_legacy: Option, + page: Option, + per_page: Option, +} + +#[derive(Debug, Deserialize, utoipa::IntoParams)] +struct NodesParams { + semver_compatibility: Option, + no_legacy: Option, + page: Option, + per_page: Option, +} + +impl From for NodesParams { + fn from(params: NodesParamsWithRole) -> Self { + NodesParams { + semver_compatibility: params.semver_compatibility, + no_legacy: params.no_legacy, + page: params.page, + per_page: params.per_page, + } + } +} + +impl<'a> From<&'a NodesParams> for PaginationRequest { + fn from(params: &'a NodesParams) -> Self { + PaginationRequest { + page: params.page, + per_page: params.per_page, + } + } +} diff --git a/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs new file mode 100644 index 0000000000..294ec222e5 --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/unstable/semi_skimmed.rs @@ -0,0 +1,27 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::nym_nodes::handlers::unstable::NodesParamsWithRole; +use crate::support::http::state::AppState; +use axum::extract::{Query, State}; +use axum::Json; +use nym_api_requests::nym_nodes::{CachedNodesResponse, SemiSkimmedNode}; + +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "/", + context_path = "/v1/unstable/nym-nodes/semi-skimmed", + responses( + // (status = 200, body = CachedNodesResponse) + (status = 501) + ) +)] +pub(super) async fn nodes_expanded( + _state: State, + _query_params: Query, +) -> AxumResult>> { + Err(AxumErrorResponse::not_implemented()) +} diff --git a/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs new file mode 100644 index 0000000000..a4aba882cf --- /dev/null +++ b/nym-api/src/nym_nodes/handlers/unstable/skimmed.rs @@ -0,0 +1,535 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::node_describe_cache::DescribedNodes; +use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; +use crate::nym_contract_cache::cache::CachedRewardedSet; +use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, semver, LegacyAnnotation}; +use crate::nym_nodes::handlers::unstable::{NodesParams, NodesParamsWithRole}; +use crate::support::caching::Cache; +use crate::support::http::state::AppState; +use axum::extract::{Query, State}; +use axum::Json; +use nym_api_requests::models::{NodeAnnotation, NymNodeDescription}; +use nym_api_requests::nym_nodes::{ + CachedNodesResponse, NodeRole, NodeRoleQueryParam, PaginatedCachedNodesResponse, SkimmedNode, +}; +use nym_mixnet_contract_common::NodeId; +use std::collections::HashMap; +use std::future::Future; +use tokio::sync::RwLockReadGuard; +use tracing::trace; + +pub type PaginatedSkimmedNodes = AxumResult>>; + +/// Given all relevant caches, build part of response for JUST Nym Nodes +fn build_nym_nodes_response<'a, NI>( + rewarded_set: &CachedRewardedSet, + required_semver: &Option, + nym_nodes_subset: NI, + annotations: &HashMap, + active_only: bool, +) -> Vec +where + NI: Iterator + 'a, +{ + let mut nodes = Vec::new(); + for nym_node in nym_nodes_subset { + let node_id = nym_node.node_id; + + // if we have wrong version, ignore + if !semver(required_semver, nym_node.version()) { + continue; + } + + let role: NodeRole = rewarded_set.role(node_id).into(); + + // if the role is inactive, see if our filter allows it + if active_only && role.is_inactive() { + continue; + } + + // honestly, not sure under what exact circumstances this value could be missing, + // but in that case just use 0 performance + let annotation = annotations.get(&node_id).copied().unwrap_or_default(); + + nodes.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance)); + } + nodes +} + +/// Given all relevant caches, add appropriate legacy nodes to the part of the response +fn add_legacy( + nodes: &mut Vec, + required_semver: &Option, + rewarded_set: &CachedRewardedSet, + describe_cache: &DescribedNodes, + annotated_legacy_nodes: &HashMap, + active_only: bool, +) where + LN: LegacyAnnotation, +{ + for (node_id, legacy) in annotated_legacy_nodes.iter() { + // if we have wrong version, ignore + if !semver(required_semver, legacy.version()) { + continue; + } + + let role: NodeRole = rewarded_set.role(*node_id).into(); + + // if the role is inactive, see if our filter allows it + if active_only && role.is_inactive() { + continue; + } + + // if we have self-described info, prefer it over contract data + if let Some(described) = describe_cache.get_node(node_id) { + nodes.push(described.to_skimmed_node(role, legacy.performance())) + } else { + match legacy.try_to_skimmed_node(role) { + Ok(node) => nodes.push(node), + Err(err) => { + let id = legacy.identity(); + trace!("node {id} is malformed: {err}") + } + } + } + } +} + +// hehe, what an abomination, but it's used in multiple different places and I hate copy-pasting code, +// especially if it has multiple loops, etc +async fn build_skimmed_nodes_response<'a, NI, LG, Fut, LN>( + state: &'a AppState, + Query(query_params): Query, + nym_nodes_subset: NI, + annotated_legacy_nodes_getter: LG, + active_only: bool, +) -> PaginatedSkimmedNodes +where + // iterator returning relevant subset of nym-nodes (like mixing nym-nodes, entries, etc.) + NI: Iterator + 'a, + + // async function that returns cache of appropriate legacy nodes (mixnodes or gateways) + LG: Fn(&'a AppState) -> Fut, + Fut: + Future>>, AxumErrorResponse>>, + + // the legacy node (MixNodeBondAnnotated or GatewayBondAnnotated) + LN: LegacyAnnotation + 'a, +{ + // TODO: implement it + let _ = query_params.per_page; + let _ = query_params.page; + let semver_compatibility = query_params.semver_compatibility; + + // 1. get the rewarded set + let rewarded_set = state.rewarded_set().await?; + + // 2. grab all annotations so that we could attach scores to the [nym] nodes + let annotations = state.node_annotations().await?; + + // 3. implicitly grab the relevant described nodes + // (ideally it'd be tied directly to the NI iterator, but I couldn't defeat the compiler) + let describe_cache = state.describe_nodes_cache_data().await?; + + // 4. start building the response + let mut nodes = build_nym_nodes_response( + &rewarded_set, + &semver_compatibility, + nym_nodes_subset, + &annotations, + active_only, + ); + + // 5. if we allow legacy nodes, repeat the procedure for them, otherwise return just nym-nodes + if let Some(true) = query_params.no_legacy { + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + ]); + + return Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))); + } + + // 6. grab relevant legacy nodes + // (due to the existence of the legacy endpoints, we already have fully annotated data on them) + let annotated_legacy_nodes = annotated_legacy_nodes_getter(state).await?; + add_legacy( + &mut nodes, + &semver_compatibility, + &rewarded_set, + &describe_cache, + &annotated_legacy_nodes, + active_only, + ); + + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + annotated_legacy_nodes.timestamp(), + ]); + + Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))) +} + +/// Deprecated query that gets ALL gateways +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/gateways/skimmed", + context_path = "/v1/unstable/nym-nodes", + responses( + (status = 200, body = CachedNodesResponse) + ) +)] +#[deprecated(note = "use '/v1/unstable/nym-nodes/entry-gateways/skimmed/all' instead")] +pub(super) async fn deprecated_gateways_basic( + state: State, + query_params: Query, +) -> AxumResult>> { + // 1. call '/v1/unstable/skimmed/entry-gateways/all' + let all_gateways = entry_gateways_basic_all(state, query_params).await?; + + // 3. return result + Ok(Json(CachedNodesResponse { + refreshed_at: all_gateways.refreshed_at, + // 2. remove pagination + nodes: all_gateways.0.nodes.data, + })) +} + +/// Deprecated query that gets ACTIVE-ONLY mixnodes +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/mixnodes/skimmed", + context_path = "/v1/unstable/nym-nodes", + responses( + (status = 200, body = CachedNodesResponse) + ) +)] +#[deprecated(note = "use '/v1/unstable/nym-nodes/skimmed/mixnodes/active' instead")] +pub(super) async fn deprecated_mixnodes_basic( + state: State, + query_params: Query, +) -> AxumResult>> { + // 1. call '/v1/unstable/nym-nodes/skimmed/mixnodes/active' + let active_mixnodes = mixnodes_basic_active(state, query_params).await?; + + // 3. return result + Ok(Json(CachedNodesResponse { + refreshed_at: active_mixnodes.refreshed_at, + // 2. remove pagination + nodes: active_mixnodes.0.nodes.data, + })) +} + +async fn nodes_basic( + state: State, + Query(query_params): Query, + active_only: bool, +) -> PaginatedSkimmedNodes { + // unfortunately we have to build the response semi-manually here as we need to add two sources of legacy nodes + + // 1. grab all relevant described nym-nodes + let rewarded_set = state.rewarded_set().await?; + let semver_compatibility = &query_params.semver_compatibility; + + let describe_cache = state.describe_nodes_cache_data().await?; + let all_nym_nodes = describe_cache.all_nym_nodes(); + let annotations = state.node_annotations().await?; + let legacy_mixnodes = state.legacy_mixnode_annotations().await?; + let legacy_gateways = state.legacy_gateways_annotations().await?; + + let mut nodes = build_nym_nodes_response( + &rewarded_set, + semver_compatibility, + all_nym_nodes, + &annotations, + active_only, + ); + + // add legacy gateways to the response + add_legacy( + &mut nodes, + semver_compatibility, + &rewarded_set, + &describe_cache, + &legacy_gateways, + active_only, + ); + + // add legacy mixnodes to the response + add_legacy( + &mut nodes, + semver_compatibility, + &rewarded_set, + &describe_cache, + &legacy_mixnodes, + active_only, + ); + + // min of all caches + let refreshed_at = refreshed_at([ + rewarded_set.timestamp(), + annotations.timestamp(), + describe_cache.timestamp(), + legacy_mixnodes.timestamp(), + legacy_gateways.timestamp(), + ]); + + Ok(Json(PaginatedCachedNodesResponse::new_full( + refreshed_at, + nodes, + ))) +} + +/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) +/// that are currently bonded. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParamsWithRole), + path = "/", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn nodes_basic_all( + state: State, + Query(query_params): Query, +) -> PaginatedSkimmedNodes { + if let Some(role) = query_params.role { + return match role { + NodeRoleQueryParam::ActiveMixnode => { + mixnodes_basic_all(state, Query(query_params.into())).await + } + NodeRoleQueryParam::EntryGateway => { + entry_gateways_basic_all(state, Query(query_params.into())).await + } + NodeRoleQueryParam::ExitGateway => { + exit_gateways_basic_all(state, Query(query_params.into())).await + } + }; + } + + nodes_basic(state, Query(query_params.into()), false).await +} + +/// Return Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used) +/// that are currently bonded and are in the **active set** +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/active", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn nodes_basic_active( + state: State, + Query(query_params): Query, +) -> PaginatedSkimmedNodes { + if let Some(role) = query_params.role { + return match role { + NodeRoleQueryParam::ActiveMixnode => { + mixnodes_basic_active(state, Query(query_params.into())).await + } + NodeRoleQueryParam::EntryGateway => { + entry_gateways_basic_active(state, Query(query_params.into())).await + } + NodeRoleQueryParam::ExitGateway => { + exit_gateways_basic_active(state, Query(query_params.into())).await + } + }; + } + + nodes_basic(state, Query(query_params.into()), true).await +} + +async fn mixnodes_basic( + state: State, + query_params: Query, + active_only: bool, +) -> PaginatedSkimmedNodes { + // 1. grab all relevant described nym-nodes + let describe_cache = state.describe_nodes_cache_data().await?; + let mixing_nym_nodes = describe_cache.mixing_nym_nodes(); + + build_skimmed_nodes_response( + &state.0, + query_params, + mixing_nym_nodes, + |state| state.legacy_mixnode_annotations(), + active_only, + ) + .await +} + +/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used) +/// that are currently bonded and support mixing role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/mixnodes/all", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn mixnodes_basic_all( + state: State, + query_params: Query, +) -> PaginatedSkimmedNodes { + mixnodes_basic(state, query_params, false).await +} + +/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used) +/// that are currently bonded and are in the active set with one of the mixing roles. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/mixnodes/active", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn mixnodes_basic_active( + state: State, + query_params: Query, +) -> PaginatedSkimmedNodes { + mixnodes_basic(state, query_params, true).await +} + +async fn entry_gateways_basic( + state: State, + query_params: Query, + active_only: bool, +) -> PaginatedSkimmedNodes { + // 1. grab all relevant described nym-nodes + let describe_cache = state.describe_nodes_cache_data().await?; + let mixing_nym_nodes = describe_cache.entry_capable_nym_nodes(); + + build_skimmed_nodes_response( + &state.0, + query_params, + mixing_nym_nodes, + |state| state.legacy_gateways_annotations(), + active_only, + ) + .await +} + +/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) +/// that are currently bonded and are in the active set with the entry role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/entry-gateways/active", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn entry_gateways_basic_active( + state: State, + query_params: Query, +) -> PaginatedSkimmedNodes { + entry_gateways_basic(state, query_params, true).await +} + +/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) +/// that are currently bonded and support entry gateway role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/entry-gateways/all", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn entry_gateways_basic_all( + state: State, + query_params: Query, +) -> PaginatedSkimmedNodes { + entry_gateways_basic(state, query_params, false).await +} + +async fn exit_gateways_basic( + state: State, + query_params: Query, + active_only: bool, +) -> PaginatedSkimmedNodes { + // 1. grab all relevant described nym-nodes + let describe_cache = state.describe_nodes_cache_data().await?; + let mixing_nym_nodes = describe_cache.exit_capable_nym_nodes(); + + build_skimmed_nodes_response( + &state.0, + query_params, + mixing_nym_nodes, + |state| state.legacy_gateways_annotations(), + active_only, + ) + .await +} + +/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) +/// that are currently bonded and are in the active set with the exit role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/exit-gateways/active", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn exit_gateways_basic_active( + state: State, + query_params: Query, +) -> PaginatedSkimmedNodes { + exit_gateways_basic(state, query_params, true).await +} + +/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used) +/// that are currently bonded and support exit gateway role. +#[utoipa::path( + tag = "Unstable Nym Nodes", + get, + params(NodesParams), + path = "/exit-gateways/all", + context_path = "/v1/unstable/nym-nodes/skimmed", + responses( + (status = 200, body = PaginatedCachedNodesResponse) + ) +)] +pub(super) async fn exit_gateways_basic_all( + state: State, + query_params: Query, +) -> PaginatedSkimmedNodes { + exit_gateways_basic(state, query_params, false).await +} diff --git a/nym-api/src/nym_nodes/handlers_unstable.rs b/nym-api/src/nym_nodes/handlers_unstable.rs deleted file mode 100644 index f0a7d8cc8b..0000000000 --- a/nym-api/src/nym_nodes/handlers_unstable.rs +++ /dev/null @@ -1,377 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -//! All routes/nodes are split into three tiers: -//! -//! `/skimmed` -//! - used by clients -//! - returns the very basic information for routing purposes -//! -//! `/semi-skimmed` -//! - used by other nodes/VPN -//! - returns more additional information such noise keys -//! -//! `/full-fat` -//! - used by explorers, et al. -//! - returns almost everything there is about the nodes -//! -//! There's also additional split based on the role: -//! - `?role` => filters based on the specific role (mixnode/gateway/(in the future: entry/exit)) -//! - `/mixnodes/` => only returns mixnode role data -//! - `/gateway/` => only returns (entry) gateway role data - -use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; -use crate::v2::AxumAppState; -use axum::extract::Query; -use axum::extract::State; -use axum::{Json, Router}; -use nym_api_requests::nym_nodes::{ - CachedNodesResponse, FullFatNode, NodeRoleQueryParam, SemiSkimmedNode, SkimmedNode, -}; -use nym_bin_common::version_checker; -use serde::Deserialize; -use std::cmp::min; -use std::ops::Deref; - -pub(crate) fn nym_node_routes_unstable() -> axum::Router { - Router::new() - .route("/skimmed", axum::routing::get(nodes_basic)) - .route("/semi-skimmed", axum::routing::get(nodes_expanded)) - .route("/full-fat", axum::routing::get(nodes_detailed)) - .nest( - "/gateways", - Router::new() - .route("/skimmed", axum::routing::get(gateways_basic)) - .route("/semi-skimmed", axum::routing::get(gateways_expanded)) - .route("/full-fat", axum::routing::get(gateways_detailed)), - ) - .nest( - "/mixnodes", - Router::new() - .route("/skimmed", axum::routing::get(mixnodes_basic)) - .route("/semi-skimmed", axum::routing::get(mixnodes_expanded)) - .route("/full-fat", axum::routing::get(mixnodes_detailed)), - ) -} - -#[derive(Debug, Deserialize, utoipa::IntoParams)] -struct NodesParams { - #[param(inline)] - role: Option, - semver_compatibility: Option, -} - -#[derive(Debug, Deserialize, utoipa::IntoParams)] -struct SemverCompatibilityQueryParam { - semver_compatibility: Option, -} - -impl SemverCompatibilityQueryParam { - pub fn new(semver_compatibility: Option) -> Self { - Self { - semver_compatibility, - } - } -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParams), - path = "/v1/unstable/nym-nodes/skimmed", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -async fn nodes_basic( - state: State, - Query(NodesParams { - role, - semver_compatibility, - }): Query, -) -> AxumResult>> { - if let Some(role) = role { - match role { - NodeRoleQueryParam::ActiveMixnode => { - return mixnodes_basic( - state, - Query(SemverCompatibilityQueryParam::new(semver_compatibility)), - ) - .await - } - NodeRoleQueryParam::EntryGateway => { - return gateways_basic( - state, - Query(SemverCompatibilityQueryParam::new(semver_compatibility)), - ) - .await; - } - _ => {} - } - } - - Err(AxumErrorResponse::not_implemented()) -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParams), - path = "/v1/unstable/nym-nodes/semi-skimmed", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -async fn nodes_expanded( - state: State, - Query(NodesParams { - role, - semver_compatibility, - }): Query, -) -> AxumResult>> { - if let Some(role) = role { - match role { - NodeRoleQueryParam::ActiveMixnode => { - return mixnodes_expanded( - state, - Query(SemverCompatibilityQueryParam::new(semver_compatibility)), - ) - .await - } - NodeRoleQueryParam::EntryGateway => { - return gateways_expanded( - state, - Query(SemverCompatibilityQueryParam::new(semver_compatibility)), - ) - .await - } - _ => {} - } - } - - Err(AxumErrorResponse::not_implemented()) -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(NodesParams), - path = "/v1/unstable/nym-nodes/full-fat", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -async fn nodes_detailed( - state: State, - Query(NodesParams { - role, - semver_compatibility, - }): Query, -) -> AxumResult>> { - if let Some(role) = role { - match role { - NodeRoleQueryParam::ActiveMixnode => { - return mixnodes_detailed( - state, - Query(SemverCompatibilityQueryParam::new(semver_compatibility)), - ) - .await - } - NodeRoleQueryParam::EntryGateway => { - return gateways_detailed( - state, - Query(SemverCompatibilityQueryParam::new(semver_compatibility)), - ) - .await - } - _ => {} - } - } - - Err(AxumErrorResponse::not_implemented()) -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(SemverCompatibilityQueryParam), - path = "/v1/unstable/nym-nodes/gateways/skimmed", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -async fn gateways_basic( - state: State, - Query(SemverCompatibilityQueryParam { - semver_compatibility, - }): Query, -) -> AxumResult>> { - let status_cache = state.node_status_cache(); - let describe_cache = state.described_nodes_state(); - let gateways_cache = - status_cache - .gateways_cache() - .await - .ok_or(AxumErrorResponse::internal_msg( - "could not obtain gateways cache", - ))?; - - if gateways_cache.is_empty() { - return Ok(Json(CachedNodesResponse { - refreshed_at: gateways_cache.timestamp().into(), - nodes: vec![], - })); - } - - // if the self describe cache is unavailable don't try to use self-describe data - let Ok(self_descriptions) = describe_cache.get().await else { - return Ok(Json(CachedNodesResponse { - refreshed_at: gateways_cache.timestamp().into(), - nodes: gateways_cache.values().map(Into::into).collect(), - })); - }; - - let refreshed_at = min(gateways_cache.timestamp(), self_descriptions.timestamp()); - - // the same comment holds as with `get_gateways_described`. - // this is inefficient and will have to get refactored with directory v3 - Ok(Json(CachedNodesResponse { - refreshed_at: refreshed_at.into(), - nodes: gateways_cache - .values() - .filter(|annotated_bond| { - if let Some(semver_compatibility) = semver_compatibility.as_ref() { - version_checker::is_minor_version_compatible( - &annotated_bond.gateway_bond.gateway.version, - semver_compatibility, - ) - } else { - true - } - }) - .map(|annotated_bond| { - SkimmedNode::from_described_gateway( - annotated_bond, - self_descriptions.deref().get(annotated_bond.identity()), - ) - }) - .collect(), - })) -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(SemverCompatibilityQueryParam), - path = "/v1/unstable/nym-nodes/gateways/semi-skimmed", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -async fn gateways_expanded( - State(_state): State, - Query(SemverCompatibilityQueryParam { - semver_compatibility: _semver_compatibility, - }): Query, -) -> AxumResult>> { - Err(AxumErrorResponse::not_implemented()) -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(SemverCompatibilityQueryParam), - path = "/v1/unstable/nym-nodes/gateways/full-fat", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -async fn gateways_detailed( - State(_state): State, - Query(SemverCompatibilityQueryParam { - semver_compatibility: _semver_compatibility, - }): Query, -) -> AxumResult>> { - Err(AxumErrorResponse::not_implemented()) -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(SemverCompatibilityQueryParam), - path = "/v1/unstable/nym-nodes/mixnodes/skimmed", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -async fn mixnodes_basic( - state: State, - Query(SemverCompatibilityQueryParam { - semver_compatibility, - }): Query, -) -> AxumResult>> { - let mixnodes_cache = state - .node_status_cache() - .active_mixnodes_cache() - .await - .ok_or(AxumErrorResponse::internal_msg( - "could not obtain mixnodes cache", - ))?; - Ok(Json(CachedNodesResponse { - refreshed_at: mixnodes_cache.timestamp().into(), - nodes: mixnodes_cache - .iter() - .filter(|annotated_bond| { - if let Some(semver_compatibility) = semver_compatibility.as_ref() { - version_checker::is_minor_version_compatible( - &annotated_bond - .mixnode_details - .bond_information - .mix_node - .version, - semver_compatibility, - ) - } else { - true - } - }) - .map(Into::into) - .collect(), - })) -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(SemverCompatibilityQueryParam), - path = "/v1/unstable/nym-nodes/mixnodes/semi-skimmed", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -async fn mixnodes_expanded( - State(_state): State, - Query(SemverCompatibilityQueryParam { - semver_compatibility: _semver_compatibility, - }): Query, -) -> AxumResult>> { - Err(AxumErrorResponse::not_implemented()) -} - -#[utoipa::path( - tag = "Unstable Nym Nodes", - get, - params(SemverCompatibilityQueryParam), - path = "/v1/unstable/nym-nodes/mixnodes/full-fat", - responses( - (status = 200, body = CachedNodesResponse) - ) -)] -async fn mixnodes_detailed( - State(_state): State, - Query(SemverCompatibilityQueryParam { - semver_compatibility: _semver_compatibility, - }): Query, -) -> AxumResult>> { - Err(AxumErrorResponse::not_implemented()) -} diff --git a/nym-api/src/nym_nodes/mod.rs b/nym-api/src/nym_nodes/mod.rs index d7d5b98a7e..8e3725c7ec 100644 --- a/nym-api/src/nym_nodes/mod.rs +++ b/nym-api/src/nym_nodes/mod.rs @@ -1,37 +1,4 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; - -#[cfg(feature = "axum")] pub(crate) mod handlers; -#[cfg(feature = "axum")] -pub(crate) mod handlers_unstable; - -pub(crate) mod routes; -mod unstable_routes; - -/// Merges the routes with http information and returns it to Rocket for serving -pub(crate) fn nym_node_routes_deprecated(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: routes::get_gateways_described, routes::get_mixnodes_described - ] -} - -pub(crate) fn nym_node_routes_next(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: - unstable_routes::nodes_basic, - unstable_routes::nodes_expanded, - unstable_routes::nodes_detailed, - unstable_routes::gateways_basic, - unstable_routes::gateways_expanded, - unstable_routes::gateways_detailed, - unstable_routes::mixnodes_basic, - unstable_routes::mixnodes_expanded, - unstable_routes::mixnodes_detailed, - ] -} diff --git a/nym-api/src/nym_nodes/routes.rs b/nym-api/src/nym_nodes/routes.rs deleted file mode 100644 index 061cc8cf9c..0000000000 --- a/nym-api/src/nym_nodes/routes.rs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_describe_cache::DescribedNodes; -use crate::nym_contract_cache::cache::NymContractCache; -use crate::support::caching::cache::SharedCache; -use nym_api_requests::models::{DescribedGateway, DescribedMixNode}; -use nym_mixnet_contract_common::MixNodeBond; -use rocket::serde::json::Json; -use rocket::State; -use rocket_okapi::openapi; -use std::ops::Deref; - -// obviously this should get refactored later on because gateways will go away. -// unless maybe this will be filtering based on which nodes got assigned gateway role? TBD - -#[openapi(tag = "Nym Nodes")] -#[get("/gateways/described")] -pub async fn get_gateways_described( - contract_cache: &State, - describe_cache: &State>, -) -> Json> { - let gateways = contract_cache.gateways_filtered().await; - if gateways.is_empty() { - return Json(Vec::new()); - } - - // if the self describe cache is unavailable, well, don't attach describe data - let Ok(self_descriptions) = describe_cache.get().await else { - return Json(gateways.into_iter().map(Into::into).collect()); - }; - - // TODO: this is extremely inefficient, but given we don't have many gateways, - // it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo, - // but at that point (I hope) the whole caching situation should get refactored - Json( - gateways - .into_iter() - .map(|bond| DescribedGateway { - self_described: self_descriptions.deref().get(bond.identity()).cloned(), - bond, - }) - .collect(), - ) -} - -#[openapi(tag = "Nym Nodes")] -#[get("/mixnodes/described")] -pub async fn get_mixnodes_described( - contract_cache: &State, - describe_cache: &State>, -) -> Json> { - let mixnodes = contract_cache - .mixnodes_filtered() - .await - .into_iter() - .map(|m| m.bond_information) - .collect::>(); - if mixnodes.is_empty() { - return Json(Vec::new()); - } - - // if the self describe cache is unavailable, well, don't attach describe data - let Ok(self_descriptions) = describe_cache.get().await else { - return Json(mixnodes.into_iter().map(Into::into).collect()); - }; - - // TODO: this is extremely inefficient, but given we don't have many gateways, - // it shouldn't be too much of a problem until we go ahead with directory v3 / the smoosh 2: electric smoosharoo, - // but at that point (I hope) the whole caching situation should get refactored - Json( - mixnodes - .into_iter() - .map(|bond| DescribedMixNode { - self_described: self_descriptions.deref().get(bond.identity()).cloned(), - bond, - }) - .collect(), - ) -} diff --git a/nym-api/src/nym_nodes/unstable_routes.rs b/nym-api/src/nym_nodes/unstable_routes.rs deleted file mode 100644 index aa12e5b7f3..0000000000 --- a/nym-api/src/nym_nodes/unstable_routes.rs +++ /dev/null @@ -1,259 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_describe_cache::DescribedNodes; -use crate::node_status_api::models::RocketErrorResponse; -use crate::node_status_api::NodeStatusCache; -use crate::support::caching::cache::SharedCache; -use nym_api_requests::nym_nodes::{ - CachedNodesResponse, FullFatNode, NodeRoleQueryParam, SemiSkimmedNode, SkimmedNode, -}; -use nym_bin_common::version_checker; -use rocket::http::Status; -use rocket::serde::json::Json; -use rocket::State; -use rocket_okapi::openapi; -use std::cmp::min; -use std::ops::Deref; - -/* - routes: - - // all routes/nodes are split into three tiers: - // /skimmed => [used by clients] returns the very basic information for routing purposes - // /semi-skimmed => [used by other nodes/VPN] returns more additional information such noise keys - // /full-fat => [used by explorers, et al.] returns almost everything there is about the nodes - - // there's also additional split based on the role: - ?role => filters based on the specific role (mixnode/gateway/(in the future: entry/exit)) - /mixnodes/ => only returns mixnode role data - /gateway/ => only returns (entry) gateway role data - - -*/ - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/skimmed?&")] -pub async fn nodes_basic( - status_cache: &State, - describe_cache: &State>, - role: Option, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - if let Some(role) = role { - match role { - NodeRoleQueryParam::ActiveMixnode => { - return mixnodes_basic(status_cache, semver_compatibility).await - } - NodeRoleQueryParam::EntryGateway => { - return gateways_basic(status_cache, describe_cache, semver_compatibility).await - } - _ => {} - } - } - - Err(RocketErrorResponse::new( - "unimplemented", - Status::NotImplemented, - )) -} - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/semi-skimmed?&")] -pub async fn nodes_expanded( - cache: &State, - role: Option, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - if let Some(role) = role { - match role { - NodeRoleQueryParam::ActiveMixnode => { - return mixnodes_expanded(cache, semver_compatibility).await - } - NodeRoleQueryParam::EntryGateway => { - return gateways_expanded(cache, semver_compatibility).await - } - _ => {} - } - } - - Err(RocketErrorResponse::new( - "unimplemented", - Status::NotImplemented, - )) -} - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/full-fat?&")] -pub async fn nodes_detailed( - cache: &State, - role: Option, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - if let Some(role) = role { - match role { - NodeRoleQueryParam::ActiveMixnode => { - return mixnodes_detailed(cache, semver_compatibility).await - } - NodeRoleQueryParam::EntryGateway => { - return gateways_detailed(cache, semver_compatibility).await - } - _ => {} - } - } - - Err(RocketErrorResponse::new( - "unimplemented", - Status::NotImplemented, - )) -} - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/gateways/skimmed?")] -pub async fn gateways_basic( - status_cache: &State, - describe_cache: &State>, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - let gateways_cache = status_cache - .gateways_cache() - .await - .ok_or(RocketErrorResponse::new( - "could not obtain gateways cache", - Status::InternalServerError, - ))?; - - if gateways_cache.is_empty() { - return Ok(Json(CachedNodesResponse { - refreshed_at: gateways_cache.timestamp().into(), - nodes: vec![], - })); - } - - // if the self describe cache is unavailable don't try to use self-describe data - let Ok(self_descriptions) = describe_cache.get().await else { - return Ok(Json(CachedNodesResponse { - refreshed_at: gateways_cache.timestamp().into(), - nodes: gateways_cache.values().map(Into::into).collect(), - })); - }; - - let refreshed_at = min(gateways_cache.timestamp(), self_descriptions.timestamp()); - - // the same comment holds as with `get_gateways_described`. - // this is inefficient and will have to get refactored with directory v3 - Ok(Json(CachedNodesResponse { - refreshed_at: refreshed_at.into(), - nodes: gateways_cache - .values() - .filter(|annotated_bond| { - if let Some(semver_compatibility) = semver_compatibility.as_ref() { - version_checker::is_minor_version_compatible( - &annotated_bond.gateway_bond.gateway.version, - semver_compatibility, - ) - } else { - true - } - }) - .map(|annotated_bond| { - SkimmedNode::from_described_gateway( - annotated_bond, - self_descriptions.deref().get(annotated_bond.identity()), - ) - }) - .collect(), - })) -} - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/gateways/semi-skimmed?")] -pub async fn gateways_expanded( - cache: &State, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - let _ = cache; - let _ = semver_compatibility; - Err(RocketErrorResponse::new( - "unimplemented", - Status::NotImplemented, - )) -} - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/gateways/full-fat?")] -pub async fn gateways_detailed( - cache: &State, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - let _ = cache; - let _ = semver_compatibility; - Err(RocketErrorResponse::new( - "unimplemented", - Status::NotImplemented, - )) -} - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/mixnodes/skimmed?")] -pub async fn mixnodes_basic( - cache: &State, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - let mixnodes_cache = cache - .active_mixnodes_cache() - .await - .ok_or(RocketErrorResponse::new( - "could not obtain mixnodes cache", - Status::InternalServerError, - ))?; - Ok(Json(CachedNodesResponse { - refreshed_at: mixnodes_cache.timestamp().into(), - nodes: mixnodes_cache - .iter() - .filter(|annotated_bond| { - if let Some(semver_compatibility) = semver_compatibility.as_ref() { - version_checker::is_minor_version_compatible( - &annotated_bond - .mixnode_details - .bond_information - .mix_node - .version, - semver_compatibility, - ) - } else { - true - } - }) - .map(Into::into) - .collect(), - })) -} - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/mixnodes/semi-skimmed?")] -pub async fn mixnodes_expanded( - cache: &State, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - let _ = cache; - let _ = semver_compatibility; - Err(RocketErrorResponse::new( - "unimplemented", - Status::NotImplemented, - )) -} - -#[openapi(tag = "Unstable Nym Nodes")] -#[get("/mixnodes/full-fat?")] -pub async fn mixnodes_detailed( - cache: &State, - semver_compatibility: Option, -) -> Result>, RocketErrorResponse> { - let _ = cache; - let _ = semver_compatibility; - Err(RocketErrorResponse::new( - "unimplemented", - Status::NotImplemented, - )) -} diff --git a/nym-api/src/status/handlers.rs b/nym-api/src/status/handlers.rs index ea34727f9a..7dc316aac2 100644 --- a/nym-api/src/status/handlers.rs +++ b/nym-api/src/status/handlers.rs @@ -3,7 +3,7 @@ use crate::node_status_api::models::{AxumErrorResponse, AxumResult}; use crate::status::ApiStatusState; -use crate::v2::AxumAppState; +use crate::support::http::state::AppState; use axum::Json; use axum::Router; use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse}; @@ -11,7 +11,7 @@ use nym_bin_common::build_information::BinaryBuildInformationOwned; use nym_compact_ecash::Base58; use std::sync::Arc; -pub(crate) fn api_status_routes() -> Router { +pub(crate) fn api_status_routes() -> Router { let api_status_state = Arc::new(ApiStatusState::new()); Router::new() @@ -84,7 +84,7 @@ async fn signer_information( identity: signer_state.identity.clone(), announce_address: signer_state.announce_address.clone(), verification_key: signer_state - .coconut_keypair + .ecash_keypair .verification_key() .await .map(|maybe_vk| maybe_vk.to_bs58()), diff --git a/nym-api/src/status/mod.rs b/nym-api/src/status/mod.rs index 1d09b341c8..0ed94a1819 100644 --- a/nym-api/src/status/mod.rs +++ b/nym-api/src/status/mod.rs @@ -4,15 +4,10 @@ use crate::ecash; use nym_bin_common::bin_info; use nym_bin_common::build_information::BinaryBuildInformation; -use okapi::openapi3::OpenApi; -use rocket::Route; -use rocket_okapi::openapi_get_routes_spec; -use rocket_okapi::settings::OpenApiSettings; + use tokio::time::Instant; -#[cfg(feature = "axum")] pub(crate) mod handlers; -pub(crate) mod routes; pub(crate) struct ApiStatusState { startup_time: Instant, @@ -28,7 +23,7 @@ pub(crate) struct SignerState { pub announce_address: String, - pub(crate) coconut_keypair: ecash::keys::KeyPair, + pub(crate) ecash_keypair: ecash::keys::KeyPair, } impl ApiStatusState { @@ -44,12 +39,3 @@ impl ApiStatusState { self.signer_information = Some(signer_information) } } - -pub(crate) fn api_status_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { - openapi_get_routes_spec![ - settings: - routes::health, - routes::build_information, - routes::signer_information - ] -} diff --git a/nym-api/src/status/routes.rs b/nym-api/src/status/routes.rs deleted file mode 100644 index 804744df18..0000000000 --- a/nym-api/src/status/routes.rs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2024 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::node_status_api::models::RocketErrorResponse; -use crate::status::ApiStatusState; -use nym_api_requests::models::{ApiHealthResponse, SignerInformationResponse}; -use nym_bin_common::build_information::BinaryBuildInformationOwned; -use nym_compact_ecash::Base58; -use rocket::http::Status; -use rocket::serde::json::Json; -use rocket::State; -use rocket_okapi::openapi; - -#[openapi(tag = "Api Status")] -#[get("/health")] -pub(crate) async fn health(state: &State) -> Json { - let uptime = state.startup_time.elapsed(); - let health = ApiHealthResponse::new_healthy(uptime); - Json(health) -} - -#[openapi(tag = "Api Status")] -#[get("/build-information")] -pub(crate) async fn build_information( - state: &State, -) -> Json { - Json(state.build_information.to_owned()) -} - -#[openapi(tag = "Api Status")] -#[get("/signer-information")] -pub(crate) async fn signer_information( - state: &State, -) -> Result, RocketErrorResponse> { - let signer_state = state.signer_information.as_ref().ok_or_else(|| { - RocketErrorResponse::new( - "this api does not expose zk-nym signing functionalities", - Status::InternalServerError, - ) - })?; - - Ok(Json(SignerInformationResponse { - cosmos_address: signer_state.cosmos_address.clone(), - identity: signer_state.identity.clone(), - announce_address: signer_state.announce_address.clone(), - verification_key: signer_state - .coconut_keypair - .verification_key() - .await - .map(|maybe_vk| maybe_vk.to_bs58()), - })) -} diff --git a/nym-api/src/support/caching/cache.rs b/nym-api/src/support/caching/cache.rs index 4df805728a..405f6022a3 100644 --- a/nym-api/src/support/caching/cache.rs +++ b/nym-api/src/support/caching/cache.rs @@ -6,7 +6,7 @@ use std::sync::Arc; use std::time::Duration; use thiserror::Error; use time::OffsetDateTime; -use tokio::sync::{RwLock, RwLockReadGuard}; +use tokio::sync::{RwLock, RwLockMappedWriteGuard, RwLockReadGuard, RwLockWriteGuard}; #[derive(Debug, Error)] #[error("the cache item has not been initialised")] @@ -31,12 +31,12 @@ impl SharedCache { SharedCache::default() } - pub(crate) async fn update(&self, value: T) { + pub(crate) async fn update(&self, value: impl Into) { let mut guard = self.0.write().await; if let Some(ref mut existing) = guard.inner { existing.unchecked_update(value) } else { - guard.inner = Some(Cache::new(value)) + guard.inner = Some(Cache::new(value.into())) } } @@ -45,6 +45,13 @@ impl SharedCache { RwLockReadGuard::try_map(guard, |a| a.inner.as_ref()).map_err(|_| UninitialisedCache) } + pub(crate) async fn write( + &self, + ) -> Result>, UninitialisedCache> { + let guard = self.0.write().await; + RwLockWriteGuard::try_map(guard, |a| a.inner.as_mut()).map_err(|_| UninitialisedCache) + } + // ignores expiration data #[allow(dead_code)] pub(crate) async fn unchecked_get_inner( @@ -52,6 +59,17 @@ impl SharedCache { ) -> Result, UninitialisedCache> { Ok(RwLockReadGuard::map(self.get().await?, |a| &a.value)) } + + pub(crate) async fn naive_wait_for_initial_values(&self) { + let initialisation_backoff = Duration::from_secs(5); + loop { + if self.get().await.is_ok() { + break; + } else { + tokio::time::sleep(initialisation_backoff).await; + } + } + } } impl From> for SharedCache { @@ -118,11 +136,15 @@ impl Cache { } // ugh. I hate to expose it, but it'd have broken pre-existing code - pub(crate) fn unchecked_update(&mut self, value: T) { - self.value = value; + pub(crate) fn unchecked_update(&mut self, value: impl Into) { + self.value = value.into(); self.as_at = OffsetDateTime::now_utc() } + pub(crate) fn get_mut(&mut self) -> &mut T { + &mut self.value + } + #[allow(dead_code)] pub fn has_expired(&self, ttl: Duration, now: Option) -> bool { let now = now.unwrap_or(OffsetDateTime::now_utc()); @@ -135,7 +157,6 @@ impl Cache { self.as_at } - #[allow(dead_code)] pub fn into_inner(self) -> T { self.value } diff --git a/nym-api/src/support/caching/refresher.rs b/nym-api/src/support/caching/refresher.rs index 6d32bb4f5a..b301220a0e 100644 --- a/nym-api/src/support/caching/refresher.rs +++ b/nym-api/src/support/caching/refresher.rs @@ -2,9 +2,11 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::caching::cache::SharedCache; +use async_trait::async_trait; use nym_task::TaskClient; use std::time::Duration; use tokio::time::interval; +use tracing::{error, info, trace}; pub struct CacheRefresher { name: String, @@ -88,6 +90,8 @@ where self.shared_cache.clone() } + // TODO: in the future offer 2 options of refreshing cache. either provide `T` directly + // or via `FnMut(&mut T)` closure async fn do_refresh_cache(&self) { match self.provider.try_refresh().await { Ok(updated_items) => { @@ -100,12 +104,12 @@ where } pub async fn refresh(&self, task_client: &mut TaskClient) { - log::info!("{}: refreshing cache state", self.name); + info!("{}: refreshing cache state", self.name); tokio::select! { biased; _ = task_client.recv() => { - log::trace!("{}: Received shutdown while refreshing cache", self.name) + trace!("{}: Received shutdown while refreshing cache", self.name) } _ = self.do_refresh_cache() => (), } @@ -119,7 +123,7 @@ where tokio::select! { biased; _ = task_client.recv() => { - log::trace!("{}: Received shutdown", self.name) + trace!("{}: Received shutdown", self.name) } _ = refresh_interval.tick() => self.refresh(&mut task_client).await, } diff --git a/nym-api/src/support/cli/init.rs b/nym-api/src/support/cli/init.rs index 10ce41b6f9..71efedbea4 100644 --- a/nym-api/src/support/cli/init.rs +++ b/nym-api/src/support/cli/init.rs @@ -4,32 +4,39 @@ use crate::support::config::default_config_filepath; use crate::support::config::helpers::initialise_new; use anyhow::bail; +use std::net::SocketAddr; #[derive(clap::Args, Debug)] pub(crate) struct Args { /// Id of the nym-api we want to initialise. if unspecified, a default value will be used. /// default: "default" - #[clap(long, default_value = "default")] + #[clap(long, default_value = "default", env = "NYMAPI_ID_ARG")] pub(crate) id: String, /// Specifies whether network monitoring is enabled on this API /// default: false - #[clap(short = 'm', long)] + #[clap(short = 'm', long, env = "NYMAPI_ENABLE_MONITOR_ARG")] pub(crate) enable_monitor: bool, /// Specifies whether network rewarding is enabled on this API /// default: false - #[clap(short = 'r', long, requires = "enable_monitor", requires = "mnemonic")] + #[clap( + short = 'r', + long, + requires = "enable_monitor", + requires = "mnemonic", + env = "NYMAPI_ENABLE_REWARDING_ARG" + )] pub(crate) enable_rewarding: bool, /// Endpoint to nyxd instance used for contract information. /// default: http://localhost:26657 - #[clap(long)] + #[clap(long, env = "NYMAPI_NYXD_VALIDATOR_ARG")] pub(crate) nyxd_validator: Option, /// Mnemonic of the network monitor used for sending rewarding and zk-nyms transactions /// default: None - #[clap(long)] + #[clap(long, env = "NYMAPI_MNEMONIC_ARG")] pub(crate) mnemonic: Option, /// Flag to indicate whether credential signer authority is enabled on this API @@ -38,19 +45,29 @@ pub(crate) struct Args { long, requires = "mnemonic", requires = "announce_address", - alias = "enable_coconut" + alias = "enable_coconut", + env = "NYMAPI_ENABLE_ZK_NYM_ARG" )] pub(crate) enable_zk_nym: bool, /// Announced address that is going to be put in the DKG contract where zk-nym clients will connect /// to obtain their credentials /// default: None - #[clap(long)] + #[clap(long, env = "NYMAPI_ANNOUNCE_ADDRESS_NYM_ARG")] pub(crate) announce_address: Option, /// Set this nym api to work in a enabled credentials that would attempt to use gateway with the bandwidth credential requirement - #[clap(long, requires = "enable_monitor")] + #[clap( + long, + requires = "enable_monitor", + env = "NYMAPI_MONITOR_CREDENTIALS_MODE_ARG" + )] pub(crate) monitor_credentials_mode: bool, + + /// Socket address this api will use for binding its http API. + /// default: `127.0.0.1:8080` in `debug` builds and `0.0.0.0:8080` in `release` + #[clap(long)] + pub(crate) bind_address: Option, // #[clap(short, long, default_value_t = OutputFormat::default())] // output: OutputFormat, } diff --git a/nym-api/src/support/cli/mod.rs b/nym-api/src/support/cli/mod.rs index 6667adccc6..7ba0b6411f 100644 --- a/nym-api/src/support/cli/mod.rs +++ b/nym-api/src/support/cli/mod.rs @@ -20,11 +20,11 @@ fn pretty_build_info_static() -> &'static str { #[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] pub(crate) struct Cli { /// Path pointing to an env file that configures the Nym API. - #[clap(short, long)] + #[clap(short, long, env = "NYMAPI_CONFIG_ENV_FILE_ARG")] pub(crate) config_env_file: Option, /// A no-op flag included for consistency with other binaries (and compatibility with nymvisor, oops) - #[clap(long)] + #[clap(long, env = "NYMAPI_NO_BANNER_ARG")] pub(crate) no_banner: bool, #[clap(subcommand)] diff --git a/nym-api/src/support/cli/run.rs b/nym-api/src/support/cli/run.rs index a19d2c1c29..fa08abe376 100644 --- a/nym-api/src/support/cli/run.rs +++ b/nym-api/src/support/cli/run.rs @@ -1,35 +1,78 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::start_nym_api_tasks; +use crate::circulating_supply_api::cache::CirculatingSupplyCache; +use crate::ecash::api_routes::handlers::ecash_routes; +use crate::ecash::client::Client; +use crate::ecash::comm::QueryCommunicationChannel; +use crate::ecash::dkg::controller::keys::{ + can_validate_coconut_keys, load_bte_keypair, load_ecash_keypair_if_exists, +}; +use crate::ecash::dkg::controller::DkgController; +use crate::ecash::state::EcashState; +use crate::epoch_operations::EpochAdvancer; +use crate::network::models::NetworkDetails; +use crate::node_describe_cache::DescribedNodes; +use crate::node_status_api::handlers::unstable; +use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; +use crate::node_status_api::NodeStatusCache; +use crate::nym_contract_cache::cache::NymContractCache; +use crate::status::{ApiStatusState, SignerState}; +use crate::support::caching::cache::SharedCache; use crate::support::config::helpers::try_load_current_config; -use cfg_if::cfg_if; +use crate::support::config::Config; +use crate::support::http::state::{ + AppState, ForcedRefresh, ShutdownHandles, TASK_MANAGER_TIMEOUT_S, +}; +use crate::support::http::RouterBuilder; +use crate::support::nyxd; +use crate::support::storage::NymApiStorage; +use crate::v3_migration::migrate_v3_database; +use crate::{ + circulating_supply_api, ecash, epoch_operations, network_monitor, node_describe_cache, + node_status_api, nym_contract_cache, +}; +use anyhow::{bail, Context}; +use nym_config::defaults::NymNetworkDetails; +use nym_sphinx::receiver::SphinxMessageReceiver; +use nym_task::TaskManager; +use nym_validator_client::nyxd::Coin; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio_util::sync::CancellationToken; +use tracing::{error, info}; #[derive(clap::Args, Debug)] pub(crate) struct Args { /// Id of the nym-api we want to run.if unspecified, a default value will be used. /// default: "default" - #[clap(long, default_value = "default")] + #[clap(long, default_value = "default", env = "NYMAPI_ID_ARG")] pub(crate) id: String, /// Specifies whether network monitoring is enabled on this API /// default: None - config value will be used instead - #[clap(short = 'm', long)] + #[clap(short = 'm', long, env = "NYMAPI_ENABLE_MONITOR_ARG")] pub(crate) enable_monitor: Option, /// Specifies whether network rewarding is enabled on this API /// default: None - config value will be used instead - #[clap(short = 'r', long, requires = "enable_monitor", requires = "mnemonic")] + #[clap( + short = 'r', + long, + requires = "enable_monitor", + requires = "mnemonic", + env = "NYMAPI_ENABLE_REWARDING_ARG" + )] pub(crate) enable_rewarding: Option, /// Endpoint to nyxd instance used for contract information. /// default: None - config value will be used instead - #[clap(long)] + #[clap(long, env = "NYMAPI_NYXD_VALIDATOR_ARG")] pub(crate) nyxd_validator: Option, /// Mnemonic of the network monitor used for sending rewarding and zk-nyms transactions /// default: None - config value will be used instead - #[clap(long)] + #[clap(long, env = "NYMAPI_MNEMONIC_ARG")] pub(crate) mnemonic: Option, /// Flag to indicate whether coconut signer authority is enabled on this API @@ -38,20 +81,222 @@ pub(crate) struct Args { long, requires = "mnemonic", requires = "announce_address", - alias = "enable_coconut" + alias = "enable_coconut", + env = "NYMAPI_ENABLE_ZK_NYM_ARG" )] pub(crate) enable_zk_nym: Option, /// Announced address that is going to be put in the DKG contract where zk-nym clients will connect /// to obtain their credentials /// default: None - config value will be used instead - #[clap(long)] + #[clap(long, env = "NYMAPI_ANNOUNCE_ADDRESS_ARG")] pub(crate) announce_address: Option, /// Set this nym api to work in a enabled credentials that would attempt to use gateway with the bandwidth credential requirement /// default: None - config value will be used instead - #[clap(long)] + #[clap(long, env = "NYMAPI_MONITOR_CREDENTIALS_MODE_ARG")] pub(crate) monitor_credentials_mode: Option, + + /// Socket address this api will use for binding its http API. + /// default: `127.0.0.1:8080` in `debug` builds and `0.0.0.0:8080` in `release` + #[clap(long)] + pub(crate) bind_address: Option, +} + +async fn start_nym_api_tasks_axum(config: &Config) -> anyhow::Result { + let nyxd_client = nyxd::Client::new(config); + let connected_nyxd = config.get_nyxd_url(); + let nym_network_details = NymNetworkDetails::new_from_env(); + let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details); + + let ecash_keypair_wrapper = ecash::keys::KeyPair::new(); + + // if the keypair doesnt exist (because say this API is running in the caching mode), nothing will happen + if let Some(loaded_keys) = load_ecash_keypair_if_exists(&config.ecash_signer)? { + let issued_for = loaded_keys.issued_for_epoch; + ecash_keypair_wrapper.set(loaded_keys).await; + + if can_validate_coconut_keys(&nyxd_client, issued_for).await? { + ecash_keypair_wrapper.validate() + } + } + + let storage = NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?; + + // try to perform any needed migrations of the storage + migrate_v3_database(&storage, &nyxd_client).await?; + + let identity_keypair = config.base.storage_paths.load_identity()?; + let identity_public_key = *identity_keypair.public_key(); + + let router = RouterBuilder::with_default_routes(config.network_monitor.enabled); + + let nym_contract_cache_state = NymContractCache::new(); + let node_status_cache_state = NodeStatusCache::new(); + let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); + let circulating_supply_cache = CirculatingSupplyCache::new(mix_denom.to_owned()); + let described_nodes_cache = SharedCache::::new(); + let node_info_cache = unstable::NodeInfoCache::default(); + + let mut status_state = ApiStatusState::new(); + + let ecash_contract = nyxd_client + .get_ecash_contract_address() + .await + .context("e-cash contract address is required to setup the nym-api routes")?; + + let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); + + let encoded_identity = identity_keypair.public_key().to_base58_string(); + let ecash_state = EcashState::new( + ecash_contract, + nyxd_client.clone(), + identity_keypair, + ecash_keypair_wrapper.clone(), + comm_channel, + storage.clone(), + !config.ecash_signer.enabled, + ) + .await?; + + // if ecash signer is enabled, there are additional constraints on the nym-api, + // such as having sufficient token balance + let router = if config.ecash_signer.enabled { + let cosmos_address = nyxd_client.address().await; + + // make sure we have some tokens to cover multisig fees + let balance = nyxd_client.balance(&mix_denom).await?; + if balance.amount < ecash::MINIMUM_BALANCE { + let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); + bail!("the account ({cosmos_address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") + } + + let announce_address = config + .ecash_signer + .announce_address + .clone() + .map(|u| u.to_string()) + .unwrap_or_default(); + status_state.add_zk_nym_signer(SignerState { + cosmos_address: cosmos_address.to_string(), + identity: encoded_identity, + announce_address, + ecash_keypair: ecash_keypair_wrapper.clone(), + }); + + router.nest("/v1/ecash", ecash_routes(Arc::new(ecash_state))) + } else { + router + }; + + let router = router.with_state(AppState { + forced_refresh: ForcedRefresh::new( + config.topology_cacher.debug.node_describe_allow_illegal_ips, + ), + nym_contract_cache: nym_contract_cache_state.clone(), + node_status_cache: node_status_cache_state.clone(), + circulating_supply_cache: circulating_supply_cache.clone(), + storage: storage.clone(), + described_nodes_cache: described_nodes_cache.clone(), + network_details, + node_info_cache, + }); + + let task_manager = TaskManager::new(TASK_MANAGER_TIMEOUT_S); + + // start note describe cache refresher + // we should be doing the below, but can't due to our current startup structure + // let refresher = node_describe_cache::new_refresher(&config.topology_cacher); + // let cache = refresher.get_shared_cache(); + node_describe_cache::new_refresher_with_initial_value( + &config.topology_cacher, + nym_contract_cache_state.clone(), + described_nodes_cache.clone(), + ) + .named("node-self-described-data-refresher") + .start(task_manager.subscribe_named("node-self-described-data-refresher")); + + // start all the caches first + let nym_contract_cache_listener = nym_contract_cache::start_refresher( + &config.node_status_api, + &nym_contract_cache_state, + nyxd_client.clone(), + &task_manager, + ); + node_status_api::start_cache_refresh( + &config.node_status_api, + &nym_contract_cache_state, + &node_status_cache_state, + storage.clone(), + nym_contract_cache_listener, + &task_manager, + ); + circulating_supply_api::start_cache_refresh( + &config.circulating_supply_cacher, + nyxd_client.clone(), + &circulating_supply_cache, + &task_manager, + ); + + // start dkg task + if config.ecash_signer.enabled { + let dkg_bte_keypair = load_bte_keypair(&config.ecash_signer)?; + + DkgController::start( + &config.ecash_signer, + nyxd_client.clone(), + ecash_keypair_wrapper, + dkg_bte_keypair, + identity_public_key, + rand::rngs::OsRng, + &task_manager, + )?; + } + + // and then only start the uptime updater (and the monitor itself, duh) + // if the monitoring is enabled + if config.network_monitor.enabled { + network_monitor::start::( + &config.network_monitor, + &nym_contract_cache_state, + described_nodes_cache.clone(), + &storage, + nyxd_client.clone(), + &task_manager, + ) + .await; + + HistoricalUptimeUpdater::start(storage.to_owned(), &task_manager); + + // start 'rewarding' if its enabled + if config.rewarding.enabled { + epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; + EpochAdvancer::start( + nyxd_client, + &nym_contract_cache_state, + described_nodes_cache.clone(), + &storage, + &task_manager, + ); + } + } + + let bind_address = config.base.bind_address.to_owned(); + let server = router.build_server(&bind_address).await?; + + let cancellation_token = CancellationToken::new(); + let shutdown_button = cancellation_token.clone(); + let axum_shutdown_receiver = cancellation_token.cancelled_owned(); + let server_handle = tokio::spawn(async move { + { + info!("Started Axum HTTP V2 server on {bind_address}"); + server.run(axum_shutdown_receiver).await + } + }); + + let shutdown = ShutdownHandles::new(task_manager, server_handle, shutdown_button); + + Ok(shutdown) } pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { @@ -62,41 +307,32 @@ pub(crate) async fn execute(args: Args) -> anyhow::Result<()> { config.validate()?; - #[cfg(feature = "axum")] - let mut axum_shutdown = crate::v2::start_nym_api_tasks_v2(&config).await?; - let mut rocket_shutdown = start_nym_api_tasks(config).await?; + let mut axum_shutdown = start_nym_api_tasks_axum(&config).await?; // it doesn't matter which server catches the interrupt: it needs only be caught once - if let Err(err) = rocket_shutdown.task_manager_handle.catch_interrupt().await { + if let Err(err) = axum_shutdown.task_manager_mut().catch_interrupt().await { error!("Error stopping Rocket tasks: {err}"); } - log::info!("Stopping nym API"); - rocket_shutdown.rocket_handle.notify(); - - // because Rocket caught the interrupt, it had already signalled its - // background tasks to retire. Now do that for axum - cfg_if! { - if #[cfg(feature = "axum")] { - axum_shutdown.task_manager_mut().signal_shutdown().ok(); - axum_shutdown.task_manager_mut().wait_for_shutdown().await; - - let running_server = axum_shutdown.shutdown_axum(); - - match running_server.await { - Ok(Ok(_)) => { - info!("Axum HTTP server shut down without errors"); - }, - Ok(Err(err)) => { - error!("Axum HTTP server terminated with: {err}"); - anyhow::bail!(err) - }, - Err(err) => { - error!("Server task panicked: {err}"); - } - }; + info!("Stopping nym API"); + + axum_shutdown.task_manager_mut().signal_shutdown().ok(); + axum_shutdown.task_manager_mut().wait_for_shutdown().await; + + let running_server = axum_shutdown.shutdown_axum(); + + match running_server.await { + Ok(Ok(_)) => { + info!("Axum HTTP server shut down without errors"); } - } + Ok(Err(err)) => { + error!("Axum HTTP server terminated with: {err}"); + anyhow::bail!(err) + } + Err(err) => { + error!("Server task panicked: {err}"); + } + }; Ok(()) } diff --git a/nym-api/src/support/config/helpers.rs b/nym-api/src/support/config/helpers.rs index 6521c8f7f4..8a6652e4f8 100644 --- a/nym-api/src/support/config/helpers.rs +++ b/nym-api/src/support/config/helpers.rs @@ -42,7 +42,7 @@ pub(crate) fn initialise_new(id: &str) -> Result { // create DKG BTE keys let mut rng = OsRng; - init_bte_keypair(&mut rng, &config.coconut_signer)?; + init_bte_keypair(&mut rng, &config.ecash_signer)?; Ok(config) } diff --git a/nym-api/src/support/config/mod.rs b/nym-api/src/support/config/mod.rs index a26384652f..edb56934ee 100644 --- a/nym-api/src/support/config/mod.rs +++ b/nym-api/src/support/config/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::config::persistence::{ - CoconutSignerPaths, NetworkMonitorPaths, NodeStatusAPIPaths, NymApiPaths, + EcashSignerPaths, NetworkMonitorPaths, NodeStatusAPIPaths, NymApiPaths, }; use crate::support::config::r#override::OverrideConfig; use crate::support::config::template::CONFIG_TEMPLATE; @@ -19,6 +19,7 @@ use std::io; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::{Path, PathBuf}; use std::time::Duration; +use tracing::debug; use url::Url; use zeroize::{Zeroize, ZeroizeOnDrop}; @@ -106,7 +107,8 @@ pub struct Config { pub rewarding: Rewarding, - pub coconut_signer: CoconutSigner, + #[serde(alias = "coconut_signer")] + pub ecash_signer: EcashSigner, } impl NymConfigTemplate for Config { @@ -125,7 +127,7 @@ impl Config { topology_cacher: Default::default(), circulating_supply_cacher: Default::default(), rewarding: Default::default(), - coconut_signer: CoconutSigner::new_default(id.as_ref()), + ecash_signer: EcashSigner::new_default(id.as_ref()), } } @@ -136,7 +138,7 @@ impl Config { bail!("can't enable rewarding without providing a mnemonic") } - if !can_sign && self.coconut_signer.enabled { + if !can_sign && self.ecash_signer.enabled { bail!("can't enable coconut signer without providing a mnemonic") } @@ -159,14 +161,17 @@ impl Config { self.base.mnemonic = Some(mnemonic) } if let Some(enable_zk_nym) = args.enable_zk_nym { - self.coconut_signer.enabled = enable_zk_nym + self.ecash_signer.enabled = enable_zk_nym } if let Some(announce_address) = args.announce_address { - self.coconut_signer.announce_address = Some(announce_address) + self.ecash_signer.announce_address = Some(announce_address) } if let Some(monitor_credentials_mode) = args.monitor_credentials_mode { self.network_monitor.debug.disabled_credentials_mode = !monitor_credentials_mode } + if let Some(http_bind_address) = args.bind_address { + self.base.bind_address = http_bind_address + } self } @@ -226,13 +231,12 @@ impl Config { } } -// TODO rocket: when axum becomes the main server, change its bind addr default here fn default_http_socket_addr() -> SocketAddr { cfg_if::cfg_if! { if #[cfg(debug_assertions)] { - SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8081) + SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8000) } else { - SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8081) + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8000) } } } @@ -432,6 +436,8 @@ pub struct TopologyCacherDebug { pub node_describe_caching_interval: Duration, pub node_describe_batch_size: usize, + + pub node_describe_allow_illegal_ips: bool, } impl Default for TopologyCacherDebug { @@ -440,6 +446,7 @@ impl Default for TopologyCacherDebug { caching_interval: DEFAULT_TOPOLOGY_CACHE_INTERVAL, node_describe_caching_interval: DEFAULT_NODE_DESCRIBE_CACHE_INTERVAL, node_describe_batch_size: DEFAULT_NODE_DESCRIBE_BATCH_SIZE, + node_describe_allow_illegal_ips: false, } } } @@ -518,25 +525,25 @@ impl Default for RewardingDebug { } #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] -pub struct CoconutSigner { +pub struct EcashSigner { /// Specifies whether rewarding service is enabled in this process. pub enabled: bool, #[serde(deserialize_with = "de_maybe_stringified")] pub announce_address: Option, - pub storage_paths: CoconutSignerPaths, + pub storage_paths: EcashSignerPaths, #[serde(default)] - pub debug: CoconutSignerDebug, + pub debug: EcashSignerDebug, } -impl CoconutSigner { +impl EcashSigner { pub fn new_default>(id: P) -> Self { - CoconutSigner { + EcashSigner { enabled: false, announce_address: None, - storage_paths: CoconutSignerPaths::new_default(id), + storage_paths: EcashSignerPaths::new_default(id), debug: Default::default(), } } @@ -544,15 +551,15 @@ impl CoconutSigner { #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(default)] -pub struct CoconutSignerDebug { +pub struct EcashSignerDebug { /// Duration of the interval for polling the dkg contract. #[serde(with = "humantime_serde")] pub dkg_contract_polling_rate: Duration, } -impl Default for CoconutSignerDebug { +impl Default for EcashSignerDebug { fn default() -> Self { - CoconutSignerDebug { + EcashSignerDebug { dkg_contract_polling_rate: DEFAULT_DKG_CONTRACT_POLLING_RATE, } } diff --git a/nym-api/src/support/config/override.rs b/nym-api/src/support/config/override.rs index dea892d2a9..059d0f8136 100644 --- a/nym-api/src/support/config/override.rs +++ b/nym-api/src/support/config/override.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::support::cli::{init, run}; +use std::net::SocketAddr; // Configuration that can be overridden. pub(crate) struct OverrideConfig { @@ -26,6 +27,10 @@ pub(crate) struct OverrideConfig { /// Set this nym api to work in a enabled credentials that would attempt to use gateway with the bandwidth credential requirement pub(crate) monitor_credentials_mode: Option, + + /// Socket address this api will use for binding its http API. + /// default: `127.0.0.1:8080` in `debug` builds and `0.0.0.0:8080` in `release` + pub(crate) bind_address: Option, } impl From for OverrideConfig { @@ -38,6 +43,7 @@ impl From for OverrideConfig { enable_zk_nym: Some(args.enable_zk_nym), announce_address: args.announce_address, monitor_credentials_mode: Some(args.monitor_credentials_mode), + bind_address: args.bind_address, } } } @@ -52,6 +58,7 @@ impl From for OverrideConfig { enable_zk_nym: args.enable_zk_nym, announce_address: args.announce_address, monitor_credentials_mode: args.monitor_credentials_mode, + bind_address: args.bind_address, } } } diff --git a/nym-api/src/support/config/persistence.rs b/nym-api/src/support/config/persistence.rs index d14996b743..f2c98c038f 100644 --- a/nym-api/src/support/config/persistence.rs +++ b/nym-api/src/support/config/persistence.rs @@ -14,7 +14,9 @@ pub const DEFAULT_NODE_STATUS_API_DATABASE_FILENAME: &str = "db.sqlite"; pub const DEFAULT_DKG_PERSISTENT_STATE_FILENAME: &str = "dkg_persistent_state.json"; pub const DEFAULT_DKG_DECRYPTION_KEY_FILENAME: &str = "dkg_decryption_key.pem"; pub const DEFAULT_DKG_PUBLIC_KEY_WITH_PROOF_FILENAME: &str = "dkg_public_key_with_proof.pem"; -pub const DEFAULT_COCONUT_KEY_FILENAME: &str = "coconut.pem"; + +// don't want to be changing the defaults in case something breaks..., but it should be called ecash.pem instead +pub const DEFAULT_ECASH_KEY_FILENAME: &str = "coconut.pem"; pub const DEFAULT_PRIVATE_IDENTITY_KEY_FILENAME: &str = "private_identity.pem"; pub const DEFAULT_PUBLIC_IDENTITY_KEY_FILENAME: &str = "public_identity.pem"; @@ -73,12 +75,13 @@ impl NodeStatusAPIPaths { } #[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] -pub struct CoconutSignerPaths { +pub struct EcashSignerPaths { /// Path to a JSON file where state is persisted between different stages of DKG. pub dkg_persistent_state_path: PathBuf, /// Path to the coconut key. - pub coconut_key_path: PathBuf, + #[serde(alias = "coconut_key_path")] + pub ecash_key_path: PathBuf, /// Path to the dkg dealer decryption key. pub decryption_key_path: PathBuf, @@ -87,13 +90,13 @@ pub struct CoconutSignerPaths { pub public_key_with_proof_path: PathBuf, } -impl CoconutSignerPaths { +impl EcashSignerPaths { pub fn new_default>(id: P) -> Self { let data_dir = default_data_directory(id); - CoconutSignerPaths { + EcashSignerPaths { dkg_persistent_state_path: data_dir.join(DEFAULT_DKG_PERSISTENT_STATE_FILENAME), - coconut_key_path: data_dir.join(DEFAULT_COCONUT_KEY_FILENAME), + ecash_key_path: data_dir.join(DEFAULT_ECASH_KEY_FILENAME), decryption_key_path: data_dir.join(DEFAULT_DKG_DECRYPTION_KEY_FILENAME), public_key_with_proof_path: data_dir.join(DEFAULT_DKG_PUBLIC_KEY_WITH_PROOF_FILENAME), } diff --git a/nym-api/src/support/config/template.rs b/nym-api/src/support/config/template.rs index 108cc0f426..5ee155e016 100644 --- a/nym-api/src/support/config/template.rs +++ b/nym-api/src/support/config/template.rs @@ -16,7 +16,6 @@ id = '{{ base.id }}' local_validator = '{{ base.local_validator }}' # Socket address this api will use for binding its http API. -# Note: only used if `axum` feature is enabled. bind_address = '{{ base.bind_address }}' # Mnemonic used for rewarding and validator interaction @@ -109,26 +108,26 @@ enabled = {{ rewarding.enabled }} # Note, only values in range 0-100 are valid minimum_interval_monitor_threshold = {{ rewarding.debug.minimum_interval_monitor_threshold }} -[coconut_signer] +[ecash_signer] -# Specifies whether coconut signing protocol is enabled in this process. -enabled = {{ coconut_signer.enabled }} +# Specifies whether ecash signing protocol is enabled in this process. +enabled = {{ ecash_signer.enabled }} # address of this nym-api as announced to other instances for the purposes of performing the DKG. -announce_address = '{{ coconut_signer.announce_address }}' +announce_address = '{{ ecash_signer.announce_address }}' -[coconut_signer.storage_paths] +[ecash_signer.storage_paths] # Path to a JSON file where state is persisted between different stages of DKG. -dkg_persistent_state_path = '{{ coconut_signer.storage_paths.dkg_persistent_state_path }}' +dkg_persistent_state_path = '{{ ecash_signer.storage_paths.dkg_persistent_state_path }}' -# Path to the coconut key. -coconut_key_path = '{{ coconut_signer.storage_paths.coconut_key_path }}' +# Path to the ecash key. +ecash_key_path = '{{ ecash_signer.storage_paths.ecash_key_path }}' # Path to the dkg dealer decryption key -decryption_key_path = '{{ coconut_signer.storage_paths.decryption_key_path }}' +decryption_key_path = '{{ ecash_signer.storage_paths.decryption_key_path }}' # Path to the dkg dealer public key with proof -public_key_with_proof_path = '{{ coconut_signer.storage_paths.public_key_with_proof_path }}' +public_key_with_proof_path = '{{ ecash_signer.storage_paths.public_key_with_proof_path }}' "#; diff --git a/nym-api/src/support/http/helpers.rs b/nym-api/src/support/http/helpers.rs index 45efa8f586..4ea95d4cd2 100644 --- a/nym-api/src/support/http/helpers.rs +++ b/nym-api/src/support/http/helpers.rs @@ -1,11 +1,21 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use nym_mixnet_contract_common::NodeId; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use utoipa::{IntoParams, ToSchema}; -#[derive(Serialize, Deserialize, FromForm, Debug, JsonSchema)] +#[derive(Serialize, Deserialize, Debug, JsonSchema, ToSchema, IntoParams)] +#[into_params(parameter_in = Query)] pub struct PaginationRequest { pub page: Option, pub per_page: Option, } + +#[derive(Deserialize, IntoParams, ToSchema)] +#[into_params(parameter_in = Path)] +pub(crate) struct NodeIdParam { + #[schema(value_type = u32)] + pub(crate) node_id: NodeId, +} diff --git a/nym-api/src/support/http/mod.rs b/nym-api/src/support/http/mod.rs index 612dcb9333..9446891443 100644 --- a/nym-api/src/support/http/mod.rs +++ b/nym-api/src/support/http/mod.rs @@ -1,141 +1,120 @@ // Copyright 2022-2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::circulating_supply_api::cache::CirculatingSupplyCache; -use crate::ecash::client::Client; -use crate::ecash::state::EcashState; -use crate::ecash::{self, comm::QueryCommunicationChannel}; -use crate::network::models::NetworkDetails; -use crate::network::network_routes; -use crate::node_describe_cache::DescribedNodes; -use crate::node_status_api::routes_deprecated::unstable; -use crate::node_status_api::{self, NodeStatusCache}; -use crate::nym_contract_cache::cache::NymContractCache; -use crate::nym_nodes::{nym_node_routes_deprecated, nym_node_routes_next}; -use crate::status::{api_status_routes, ApiStatusState, SignerState}; -use crate::support::caching::cache::SharedCache; -use crate::support::config::Config; -use crate::support::{nyxd, storage}; -use crate::{circulating_supply_api, nym_contract_cache}; -use anyhow::{bail, Context, Result}; -use nym_crypto::asymmetric::identity; -use nym_validator_client::nyxd::Coin; -use rocket::{Ignite, Rocket}; -use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors}; -use rocket_okapi::mount_endpoints_and_merged_docs; -use rocket_okapi::swagger_ui::make_swagger_ui; - pub(crate) mod helpers; pub(crate) mod openapi; - -pub(crate) async fn setup_rocket( - config: &Config, - network_details: NetworkDetails, - nyxd_client: nyxd::Client, - identity_keypair: identity::KeyPair, - coconut_keypair: ecash::keys::KeyPair, -) -> anyhow::Result> { - let openapi_settings = rocket_okapi::settings::OpenApiSettings::default(); - let mut rocket = rocket::build(); - - let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); - - mount_endpoints_and_merged_docs! { - rocket, - "/v1".to_owned(), - openapi_settings, - "/" => (vec![], openapi::custom_openapi_spec()), - "" => circulating_supply_api::circulating_supply_routes(&openapi_settings), - "" => nym_contract_cache::nym_contract_cache_routes(&openapi_settings), - "/status" => node_status_api::node_status_routes(&openapi_settings, config.network_monitor.enabled), - "/network" => network_routes(&openapi_settings), - "/api-status" => api_status_routes(&openapi_settings), - "/ecash" => ecash::routes_open_api(&openapi_settings, config.coconut_signer.enabled), - "" => nym_node_routes_deprecated(&openapi_settings), - - // => when we move those routes, we'll need to add a redirection for backwards compatibility - "/unstable/nym-nodes" => nym_node_routes_next(&openapi_settings) - } - - let storage = - storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?; - - let rocket = rocket - .manage(network_details) - .manage(SharedCache::::new()) - .mount("/swagger", make_swagger_ui(&openapi::get_docs())) - .attach(setup_rocket_cors()?) - .attach(NymContractCache::stage()) - .attach(NodeStatusCache::stage()) - .attach(CirculatingSupplyCache::stage(mix_denom.clone())) - .attach(storage::NymApiStorage::stage(storage.clone())) - .manage(unstable::NodeInfoCache::default()); - - let mut status_state = ApiStatusState::new(); - - let rocket = if config.coconut_signer.enabled { - // make sure we have some tokens to cover multisig fees - let balance = nyxd_client.balance(&mix_denom).await?; - if balance.amount < ecash::MINIMUM_BALANCE { - let address = nyxd_client.address().await; - let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); - bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") - } - - let cosmos_address = nyxd_client.address().await.to_string(); - let announce_address = config - .coconut_signer - .announce_address - .clone() - .map(|u| u.to_string()) - .unwrap_or_default(); - status_state.add_zk_nym_signer(SignerState { - cosmos_address, - identity: identity_keypair.public_key().to_base58_string(), - announce_address, - coconut_keypair: coconut_keypair.clone(), - }); - - let ecash_contract = nyxd_client - .get_ecash_contract_address() - .await - .context("e-cash contract address is required to setup the zk-nym signer")?; - - let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); - - let ecash_state = EcashState::new( - ecash_contract, - nyxd_client.clone(), - identity_keypair, - coconut_keypair, - comm_channel, - storage.clone(), - ) - .await?; - - rocket.manage(ecash_state) - } else { - rocket - }; - - Ok(rocket.manage(status_state).ignite().await?) -} - -fn setup_rocket_cors() -> Result { - let allowed_origins = AllowedOrigins::all(); - - // You can also deserialize this - let cors = rocket_cors::CorsOptions { - allowed_origins, - allowed_methods: vec![rocket::http::Method::Post, rocket::http::Method::Get] - .into_iter() - .map(From::from) - .collect(), - allowed_headers: AllowedHeaders::all(), - allow_credentials: true, - ..Default::default() - } - .to_cors()?; - - Ok(cors) -} +pub(crate) mod router; +pub(crate) mod state; +mod unstable_routes; + +pub(crate) use router::RouterBuilder; + +// pub(crate) async fn setup_rocket( +// config: &Config, +// network_details: NetworkDetails, +// nyxd_client: nyxd::Client, +// identity_keypair: identity::KeyPair, +// coconut_keypair: ecash::keys::KeyPair, +// storage: NymApiStorage, +// ) -> anyhow::Result> { +// let openapi_settings = rocket_okapi::settings::OpenApiSettings::default(); +// let mut rocket = rocket::build(); +// +// let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); +// +// mount_endpoints_and_merged_docs! { +// rocket, +// "/v1".to_owned(), +// openapi_settings, +// "/" => (vec![], openapi::custom_openapi_spec()), +// "" => circulating_supply_api::circulating_supply_routes(&openapi_settings), +// "" => nym_contract_cache::nym_contract_cache_routes(&openapi_settings), +// "/status" => node_status_api::node_status_routes(&openapi_settings, config.network_monitor.enabled), +// "/network" => network_routes(&openapi_settings), +// "/api-status" => api_status_routes(&openapi_settings), +// "/ecash" => ecash::routes_open_api(&openapi_settings, config.ecash_signer.enabled), +// "" => nym_node_routes_deprecated(&openapi_settings), +// +// // => when we move those routes, we'll need to add a redirection for backwards compatibility +// "/unstable/nym-nodes" => nym_node_routes_next(&openapi_settings) +// } +// +// let rocket = rocket +// .manage(network_details) +// .manage(SharedCache::::new()) +// .mount("/swagger", make_swagger_ui(&openapi::get_docs())) +// .attach(setup_rocket_cors()?) +// .attach(NymContractCache::stage()) +// .attach(NodeStatusCache::stage()) +// .attach(CirculatingSupplyCache::stage(mix_denom.clone())) +// .manage(unstable::NodeInfoCache::default()) +// .manage(storage.clone()); +// +// let mut status_state = ApiStatusState::new(); +// +// let rocket = if config.ecash_signer.enabled { +// // make sure we have some tokens to cover multisig fees +// let balance = nyxd_client.balance(&mix_denom).await?; +// if balance.amount < ecash::MINIMUM_BALANCE { +// let address = nyxd_client.address().await; +// let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); +// bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") +// } +// +// let cosmos_address = nyxd_client.address().await.to_string(); +// let announce_address = config +// .ecash_signer +// .announce_address +// .clone() +// .map(|u| u.to_string()) +// .unwrap_or_default(); +// status_state.add_zk_nym_signer(SignerState { +// cosmos_address, +// identity: identity_keypair.public_key().to_base58_string(), +// announce_address, +// ecash_keypair: coconut_keypair.clone(), +// }); +// +// let ecash_contract = nyxd_client +// .get_ecash_contract_address() +// .await +// .context("e-cash contract address is required to setup the zk-nym signer")?; +// +// let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); +// +// let ecash_state = EcashState::new( +// ecash_contract, +// nyxd_client.clone(), +// identity_keypair, +// coconut_keypair, +// comm_channel, +// storage.clone(), +// ) +// .await?; +// +// rocket.manage(ecash_state) +// } else { +// rocket +// }; +// +// Ok(rocket.manage(status_state).ignite().await?) +// } +// +// fn setup_rocket_cors() -> Result { +// let allowed_origins = AllowedOrigins::all(); +// +// // You can also deserialize this +// let cors = rocket_cors::CorsOptions { +// allowed_origins, +// allowed_methods: vec![rocket::http::Method::Post, rocket::http::Method::Get] +// .into_iter() +// .map(From::from) +// .collect(), +// allowed_headers: AllowedHeaders::all(), +// allow_credentials: true, +// ..Default::default() +// } +// .to_cors()?; +// +// Ok(cors) +// } diff --git a/nym-api/src/support/http/openapi.rs b/nym-api/src/support/http/openapi.rs index 858d201dc7..0797cc03b8 100644 --- a/nym-api/src/support/http/openapi.rs +++ b/nym-api/src/support/http/openapi.rs @@ -1,41 +1,90 @@ -// Copyright 2023 - Nym Technologies SA +// Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use okapi::openapi3::OpenApi; -use rocket_okapi::swagger_ui::SwaggerUIConfig; +use crate::network::handlers::ContractVersionSchemaResponse; +use nym_api_requests::models; +use utoipa::OpenApi; +use utoipauto::utoipauto; -pub fn custom_openapi_spec() -> OpenApi { - use rocket_okapi::okapi::openapi3::*; - OpenApi { - openapi: OpenApi::default_version(), - info: Info { - title: "Nym API".to_owned(), - description: None, - terms_of_service: None, - contact: None, - license: None, - version: env!("CARGO_PKG_VERSION").to_owned(), - ..Default::default() - }, - servers: get_servers(), - ..Default::default() - } -} +// TODO once https://github.com/ProbablyClem/utoipauto/pull/38 is released: +// include ",./nym-api/nym-api-requests/src from nym-api-requests" (and other packages mentioned below) +// for automatic model discovery based on ToSchema / IntoParams implementation. +// Then you can remove `components(schemas)` manual imports below -fn get_servers() -> Vec { - if std::env::var_os("CARGO").is_some() { - return vec![]; - } - vec![rocket_okapi::okapi::openapi3::Server { - url: std::env::var("OPEN_API_BASE").unwrap_or_else(|_| "/api/v1/".to_owned()), - description: Some("API".to_owned()), - ..Default::default() - }] -} - -pub(crate) fn get_docs() -> SwaggerUIConfig { - SwaggerUIConfig { - url: "../v1/openapi.json".to_owned(), - ..SwaggerUIConfig::default() - } -} +#[utoipauto(paths = "./nym-api/src")] +#[derive(OpenApi)] +#[openapi( + info(title = "Nym API"), + tags(), + components(schemas( + models::CirculatingSupplyResponse, + models::CoinSchema, + nym_mixnet_contract_common::Interval, + nym_api_requests::models::NodeRefreshBody, + nym_api_requests::models::GatewayStatusReportResponse, + nym_api_requests::models::GatewayUptimeHistoryResponse, + nym_api_requests::models::GatewayCoreStatusResponse, + nym_api_requests::models::GatewayUptimeResponse, + nym_api_requests::models::RewardEstimationResponse, + nym_api_requests::models::UptimeResponse, + nym_api_requests::models::ComputeRewardEstParam, + nym_api_requests::models::MixNodeBondAnnotated, + nym_api_requests::models::GatewayBondAnnotated, + nym_api_requests::models::MixnodeTestResultResponse, + nym_api_requests::models::StakeSaturationResponse, + nym_api_requests::models::InclusionProbabilityResponse, + nym_api_requests::models::AllInclusionProbabilitiesResponse, + nym_api_requests::models::InclusionProbability, + nym_api_requests::models::SelectionChance, + crate::network::models::NetworkDetails, + nym_config::defaults::NymNetworkDetails, + nym_config::defaults::ChainDetails, + nym_config::defaults::DenomDetailsOwned, + nym_config::defaults::ValidatorDetails, + nym_config::defaults::NymContracts, + ContractVersionSchemaResponse, + crate::network::models::ContractInformation, + nym_api_requests::models::ApiHealthResponse, + nym_api_requests::models::ApiStatus, + nym_bin_common::build_information::BinaryBuildInformationOwned, + nym_api_requests::models::SignerInformationResponse, + nym_api_requests::models::LegacyDescribedGateway, + nym_mixnet_contract_common::Gateway, + nym_mixnet_contract_common::GatewayBond, + nym_api_requests::models::NymNodeDescription, + nym_api_requests::models::HostInformation, + nym_api_requests::models::HostKeys, + nym_node_requests::api::v1::node::models::AuxiliaryDetails, + nym_api_requests::models::NetworkRequesterDetails, + nym_api_requests::models::IpPacketRouterDetails, + nym_api_requests::models::AuthenticatorDetails, + nym_api_requests::models::WebSockets, + nym_api_requests::nym_nodes::NodeRole, + nym_api_requests::models::LegacyDescribedMixNode, + nym_api_requests::ecash::VerificationKeyResponse, + nym_api_requests::ecash::models::AggregatedExpirationDateSignatureResponse, + nym_api_requests::ecash::models::AggregatedCoinIndicesSignatureResponse, + nym_api_requests::ecash::models::EpochCredentialsResponse, + nym_api_requests::ecash::models::IssuedCredentialResponse, + nym_api_requests::ecash::models::IssuedTicketbookBody, + nym_api_requests::ecash::models::BlindedSignatureResponse, + nym_api_requests::ecash::models::PartialExpirationDateSignatureResponse, + nym_api_requests::ecash::models::PartialCoinIndicesSignatureResponse, + nym_api_requests::ecash::models::EcashTicketVerificationResponse, + nym_api_requests::ecash::models::EcashTicketVerificationRejection, + nym_api_requests::ecash::models::EcashBatchTicketRedemptionResponse, + nym_api_requests::ecash::models::SpentCredentialsResponse, + nym_api_requests::ecash::models::IssuedCredentialsResponse, + nym_api_requests::nym_nodes::SkimmedNode, + nym_api_requests::nym_nodes::SemiSkimmedNode, + nym_api_requests::nym_nodes::FullFatNode, + nym_api_requests::nym_nodes::BasicEntryInformation, + nym_api_requests::nym_nodes::NodeRoleQueryParam, + nym_api_requests::models::AnnotationResponse, + nym_api_requests::models::NodePerformanceResponse, + nym_api_requests::models::NodeDatePerformanceResponse, + nym_api_requests::models::PerformanceHistoryResponse, + nym_api_requests::models::UptimeHistoryResponse, + )) +)] +pub(crate) struct ApiDoc; diff --git a/nym-api/src/v2/router.rs b/nym-api/src/support/http/router.rs similarity index 81% rename from nym-api/src/v2/router.rs rename to nym-api/src/support/http/router.rs index 6ece0b7323..38eec8d76e 100644 --- a/nym-api/src/v2/router.rs +++ b/nym-api/src/support/http/router.rs @@ -5,11 +5,15 @@ use crate::circulating_supply_api::handlers::circulating_supply_routes; use crate::network::handlers::nym_network_routes; use crate::node_status_api::handlers::node_status_routes; use crate::nym_contract_cache::handlers::nym_contract_cache_routes; +use crate::nym_nodes::handlers::legacy::legacy_nym_node_routes; use crate::nym_nodes::handlers::nym_node_routes; -use crate::nym_nodes::handlers_unstable::nym_node_routes_unstable; use crate::status; -use crate::v2::AxumAppState; +use crate::support::http::openapi::ApiDoc; +use crate::support::http::state::AppState; +use crate::support::http::unstable_routes::unstable_routes; use anyhow::anyhow; +use axum::response::Redirect; +use axum::routing::get; use axum::Router; use core::net::SocketAddr; use nym_http_api_common::logging::logger; @@ -27,7 +31,7 @@ use utoipa_swagger_ui::SwaggerUi; /// /// [order]: https://docs.rs/axum/latest/axum/middleware/index.html#ordering pub(crate) struct RouterBuilder { - unfinished_router: Router, + unfinished_router: Router, } impl RouterBuilder { @@ -43,23 +47,22 @@ impl RouterBuilder { // .on_request(DefaultOnRequest::new()) // .on_response(DefaultOnResponse::new().latency_unit(tower_http::LatencyUnit::Micros)), // ) - // .route("/", axum::routing::get(|| async {axum::response::Redirect::permanent("/swagger")})) // .route("/swagger", axum::routing::get(hello)) let default_routes = Router::new() - .merge( - SwaggerUi::new("/swagger") - .url("/api-docs/openapi.json", super::api_docs::ApiDoc::openapi()), - ) + .merge(SwaggerUi::new("/swagger").url("/api-docs/openapi.json", ApiDoc::openapi())) + .route("/", get(|| async { Redirect::to("/swagger") })) .nest( "/v1", Router::new() - .nest("/circulating-supply", circulating_supply_routes()) + // unfortunately some routes didn't use correct prefix and were attached to the root .merge(nym_contract_cache_routes()) + .merge(legacy_nym_node_routes()) + .nest("/circulating-supply", circulating_supply_routes()) .nest("/status", node_status_routes(network_monitor)) .nest("/network", nym_network_routes()) .nest("/api-status", status::handlers::api_status_routes()) - .merge(nym_node_routes()) - .nest("/unstable/nym-nodes", nym_node_routes_unstable()), // CORS layer needs to be "outside" of routes + .nest("/nym-nodes", nym_node_routes()) + .nest("/unstable", unstable_routes()), // CORS layer needs to be "outside" of routes ); Self { @@ -67,7 +70,7 @@ impl RouterBuilder { } } - pub(crate) fn nest(self, path: &str, router: Router) -> Self { + pub(crate) fn nest(self, path: &str, router: Router) -> Self { Self { unfinished_router: self.unfinished_router.nest(path, router), } @@ -75,14 +78,14 @@ impl RouterBuilder { /// Invoke this as late as possible before constructing HTTP server /// (after all routes were added). - pub(crate) fn with_state(self, state: AxumAppState) -> RouterWithState { + pub(crate) fn with_state(self, state: AppState) -> RouterWithState { RouterWithState { router: self.finalize_routes().with_state(state), } } /// Middleware added here intercepts the request before it gets to other routes. - fn finalize_routes(self) -> Router { + fn finalize_routes(self) -> Router { self.unfinished_router .layer(setup_cors()) .layer(axum::middleware::from_fn(logger)) diff --git a/nym-api/src/support/http/state.rs b/nym-api/src/support/http/state.rs new file mode 100644 index 0000000000..f45d25bbd2 --- /dev/null +++ b/nym-api/src/support/http/state.rs @@ -0,0 +1,186 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::circulating_supply_api::cache::CirculatingSupplyCache; +use crate::network::models::NetworkDetails; +use crate::node_describe_cache::DescribedNodes; +use crate::node_status_api::handlers::unstable; +use crate::node_status_api::models::AxumErrorResponse; +use crate::node_status_api::NodeStatusCache; +use crate::nym_contract_cache::cache::{CachedRewardedSet, NymContractCache}; +use crate::support::caching::cache::SharedCache; +use crate::support::caching::Cache; +use crate::support::storage; +use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation}; +use nym_mixnet_contract_common::NodeId; +use nym_task::TaskManager; +use std::collections::HashMap; +use std::sync::Arc; +use time::OffsetDateTime; +use tokio::sync::{RwLock, RwLockReadGuard}; +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; + +pub(crate) const TASK_MANAGER_TIMEOUT_S: u64 = 10; + +/// Shutdown goes 2 directions: +/// 1. signal background tasks to gracefully finish +/// 2. signal server itself +/// +/// These are done through separate shutdown handles. Of course, shut down server +/// AFTER you have shut down BG tasks (or past their grace period). +pub(crate) struct ShutdownHandles { + task_manager: TaskManager, + axum_shutdown_button: ShutdownAxum, + /// Tokio JoinHandle for axum server's task + axum_join_handle: AxumJoinHandle, +} + +impl ShutdownHandles { + /// Cancellation token is given to Axum server constructor. When the token + /// receives a shutdown signal, Axum server will shut down gracefully. + pub(crate) fn new( + task_manager: TaskManager, + axum_server_handle: AxumJoinHandle, + shutdown_button: CancellationToken, + ) -> Self { + Self { + task_manager, + axum_shutdown_button: ShutdownAxum(shutdown_button.clone()), + axum_join_handle: axum_server_handle, + } + } + + pub(crate) fn task_manager_mut(&mut self) -> &mut TaskManager { + &mut self.task_manager + } + + /// Signal server to shut down, then return join handle to its + /// `tokio` task + /// + /// https://tikv.github.io/doc/tokio/task/struct.JoinHandle.html + #[must_use] + pub(crate) fn shutdown_axum(self) -> AxumJoinHandle { + self.axum_shutdown_button.0.cancel(); + self.axum_join_handle + } +} + +struct ShutdownAxum(CancellationToken); + +type AxumJoinHandle = JoinHandle>; + +#[derive(Clone)] +pub(crate) struct AppState { + pub(crate) forced_refresh: ForcedRefresh, + pub(crate) nym_contract_cache: NymContractCache, + pub(crate) node_status_cache: NodeStatusCache, + pub(crate) circulating_supply_cache: CirculatingSupplyCache, + pub(crate) storage: storage::NymApiStorage, + pub(crate) described_nodes_cache: SharedCache, + pub(crate) network_details: NetworkDetails, + pub(crate) node_info_cache: unstable::NodeInfoCache, +} + +#[derive(Clone)] +pub(crate) struct ForcedRefresh { + pub(crate) allow_all_ip_addresses: bool, + pub(crate) refreshes: Arc>>, +} + +impl ForcedRefresh { + pub(crate) fn new(allow_all_ip_addresses: bool) -> ForcedRefresh { + ForcedRefresh { + allow_all_ip_addresses, + refreshes: Arc::new(Default::default()), + } + } + + pub(crate) async fn last_refreshed(&self, node_id: NodeId) -> Option { + self.refreshes.read().await.get(&node_id).copied() + } + + pub(crate) async fn set_last_refreshed(&self, node_id: NodeId) { + self.refreshes + .write() + .await + .insert(node_id, OffsetDateTime::now_utc()); + } +} + +impl AppState { + pub(crate) fn nym_contract_cache(&self) -> &NymContractCache { + &self.nym_contract_cache + } + + pub(crate) fn node_status_cache(&self) -> &NodeStatusCache { + &self.node_status_cache + } + + pub(crate) fn circulating_supply_cache(&self) -> &CirculatingSupplyCache { + &self.circulating_supply_cache + } + + pub(crate) fn network_details(&self) -> &NetworkDetails { + &self.network_details + } + + pub(crate) fn described_nodes_cache(&self) -> &SharedCache { + &self.described_nodes_cache + } + + pub(crate) fn storage(&self) -> &storage::NymApiStorage { + &self.storage + } + + pub(crate) fn node_info_cache(&self) -> &unstable::NodeInfoCache { + &self.node_info_cache + } +} + +// handler helpers to easily get data or return error response +impl AppState { + pub(crate) async fn describe_nodes_cache_data( + &self, + ) -> Result>, AxumErrorResponse> { + Ok(self.described_nodes_cache().get().await?) + } + + pub(crate) async fn rewarded_set( + &self, + ) -> Result>, AxumErrorResponse> { + self.nym_contract_cache() + .rewarded_set() + .await + .ok_or_else(AxumErrorResponse::internal) + } + + pub(crate) async fn node_annotations( + &self, + ) -> Result>>, AxumErrorResponse> { + self.node_status_cache() + .node_annotations() + .await + .ok_or_else(AxumErrorResponse::internal) + } + + pub(crate) async fn legacy_mixnode_annotations( + &self, + ) -> Result>>, AxumErrorResponse> + { + self.node_status_cache() + .annotated_legacy_mixnodes() + .await + .ok_or_else(AxumErrorResponse::internal) + } + + pub(crate) async fn legacy_gateways_annotations( + &self, + ) -> Result>>, AxumErrorResponse> + { + self.node_status_cache() + .annotated_legacy_gateways() + .await + .ok_or_else(AxumErrorResponse::internal) + } +} diff --git a/nym-api/src/support/http/unstable_routes.rs b/nym-api/src/support/http/unstable_routes.rs new file mode 100644 index 0000000000..7bc1a9644c --- /dev/null +++ b/nym-api/src/support/http/unstable_routes.rs @@ -0,0 +1,11 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::nym_nodes::handlers::unstable::nym_node_routes_unstable; +use crate::support::http::state::AppState; +use axum::Router; + +// as those get stabilised, they should get deprecated and use a redirection instead +pub(crate) fn unstable_routes() -> Router { + Router::new().nest("/nym-nodes", nym_node_routes_unstable()) +} diff --git a/nym-api/src/support/legacy_helpers.rs b/nym-api/src/support/legacy_helpers.rs new file mode 100644 index 0000000000..d5135c7643 --- /dev/null +++ b/nym-api/src/support/legacy_helpers.rs @@ -0,0 +1,85 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_api_requests::legacy::{LegacyMixNodeBondWithLayer, LegacyMixNodeDetailsWithLayer}; +use nym_api_requests::models::NymNodeData; +use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT; +use nym_crypto::aes::cipher::crypto_common::rand_core::OsRng; +use nym_mixnet_contract_common::mixnode::LegacyPendingMixNodeChanges; +use nym_mixnet_contract_common::{ + Gateway, GatewayBond, LegacyMixLayer, MixNode, MixNodeBond, NymNodeDetails, +}; +use rand::prelude::SliceRandom; + +pub(crate) fn to_legacy_mixnode( + nym_node: &NymNodeDetails, + description: &NymNodeData, +) -> LegacyMixNodeDetailsWithLayer { + let layer_choices = [ + LegacyMixLayer::One, + LegacyMixLayer::Two, + LegacyMixLayer::Three, + ]; + let mut rng = OsRng; + + // slap a random layer on it because legacy clients don't understand a concept of layerless mixnodes + // SAFETY: the slice is not empty so the unwrap is fine + #[allow(clippy::unwrap_used)] + let layer = layer_choices.choose(&mut rng).copied().unwrap(); + + LegacyMixNodeDetailsWithLayer { + bond_information: LegacyMixNodeBondWithLayer { + bond: MixNodeBond { + mix_id: nym_node.node_id(), + owner: nym_node.bond_information.owner.clone(), + original_pledge: nym_node.bond_information.original_pledge.clone(), + mix_node: MixNode { + host: nym_node.bond_information.node.host.clone(), + mix_port: description.mix_port(), + verloc_port: description.verloc_port(), + http_api_port: nym_node + .bond_information + .node + .custom_http_port + .unwrap_or(DEFAULT_NYM_NODE_HTTP_PORT), + sphinx_key: description.host_information.keys.x25519.to_base58_string(), + identity_key: nym_node.bond_information.node.identity_key.clone(), + version: description.build_information.build_version.clone(), + }, + proxy: None, + bonding_height: nym_node.bond_information.bonding_height, + is_unbonding: nym_node.bond_information.is_unbonding, + }, + layer, + }, + rewarding_details: nym_node.rewarding_details.clone(), + pending_changes: LegacyPendingMixNodeChanges { + pledge_change: nym_node.pending_changes.pledge_change, + }, + } +} + +pub(crate) fn to_legacy_gateway( + nym_node: &NymNodeDetails, + description: &NymNodeData, +) -> GatewayBond { + GatewayBond { + pledge_amount: nym_node.bond_information.original_pledge.clone(), + owner: nym_node.bond_information.owner.clone(), + block_height: nym_node.bond_information.bonding_height, + gateway: Gateway { + host: nym_node.bond_information.node.host.clone(), + mix_port: description.mix_port(), + clients_port: description.mixnet_websockets.ws_port, + location: description + .auxiliary_details + .location + .map(|c| c.to_string()) + .unwrap_or_default(), + sphinx_key: description.host_information.keys.x25519.to_base58_string(), + identity_key: nym_node.bond_information.node.identity_key.clone(), + version: description.build_information.build_version.clone(), + }, + proxy: None, + } +} diff --git a/nym-api/src/support/mod.rs b/nym-api/src/support/mod.rs index 28abaf3b94..a1773f9804 100644 --- a/nym-api/src/support/mod.rs +++ b/nym-api/src/support/mod.rs @@ -7,3 +7,5 @@ pub(crate) mod config; pub(crate) mod http; pub(crate) mod nyxd; pub(crate) mod storage; + +pub(crate) mod legacy_helpers; diff --git a/nym-api/src/support/nyxd/mod.rs b/nym-api/src/support/nyxd/mod.rs index 69f9a49360..46ae0c67dc 100644 --- a/nym-api/src/support/nyxd/mod.rs +++ b/nym-api/src/support/nyxd/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::ecash::error::EcashError; -use crate::epoch_operations::MixnodeWithPerformance; +use crate::epoch_operations::RewardedNodeWithParams; use crate::support::config::Config; use anyhow::Result; use async_trait::async_trait; @@ -24,14 +24,16 @@ use nym_config::defaults::{ChainDetails, NymNetworkDetails}; use nym_dkg::Threshold; use nym_ecash_contract_common::blacklist::BlacklistedAccountResponse; use nym_ecash_contract_common::deposit::{DepositId, DepositResponse}; -use nym_mixnet_contract_common::families::FamilyHead; +use nym_mixnet_contract_common::gateway::PreassignedId; use nym_mixnet_contract_common::mixnode::MixNodeDetails; +use nym_mixnet_contract_common::nym_node::Role; use nym_mixnet_contract_common::reward_params::RewardingParams; use nym_mixnet_contract_common::{ - CurrentIntervalResponse, EpochStatus, ExecuteMsg, GatewayBond, IdentityKey, LayerAssignment, - MixId, RewardedSetNodeStatus, + CurrentIntervalResponse, EpochStatus, ExecuteMsg, GatewayBond, IdentityKey, NymNodeDetails, + RewardedSet, RoleAssignment, }; use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nyxd::contract_traits::mixnet_query_client::MixnetQueryClientExt; use nym_validator_client::nyxd::contract_traits::PagedDkgQueryClient; use nym_validator_client::nyxd::error::NyxdError; use nym_validator_client::nyxd::{ @@ -213,6 +215,10 @@ impl Client { Ok(hash) } + pub(crate) async fn get_nymnodes(&self) -> Result, NyxdError> { + nyxd_query!(self, get_all_nymnodes_detailed().await) + } + pub(crate) async fn get_mixnodes(&self) -> Result, NyxdError> { nyxd_query!(self, get_all_mixnodes_detailed().await) } @@ -221,6 +227,10 @@ impl Client { nyxd_query!(self, get_all_gateways().await) } + pub(crate) async fn get_gateway_ids(&self) -> Result, NyxdError> { + nyxd_query!(self, get_all_preassigned_gateway_ids().await) + } + pub(crate) async fn get_current_interval(&self) -> Result { nyxd_query!(self, get_current_interval_details().await) } @@ -235,10 +245,8 @@ impl Client { nyxd_query!(self, get_rewarding_parameters().await) } - pub(crate) async fn get_rewarded_set_mixnodes( - &self, - ) -> Result, NyxdError> { - nyxd_query!(self, get_all_rewarded_set_mixnodes().await) + pub(crate) async fn get_rewarded_set_nodes(&self) -> Result { + nyxd_query!(self, get_rewarded_set().await) } pub(crate) async fn get_current_vesting_account_storage_key(&self) -> Result { @@ -266,13 +274,6 @@ impl Client { ) -> Result, NyxdError> { nyxd_query!(self, get_all_accounts_vesting_coins().await) } - - pub(crate) async fn get_all_family_members( - &self, - ) -> Result, NyxdError> { - nyxd_query!(self, get_all_family_members().await) - } - pub(crate) async fn get_pending_events_count(&self) -> Result { let pending = nyxd_query!(self, get_number_of_pending_events().await?); Ok(pending.epoch_events + pending.interval_events) @@ -283,29 +284,21 @@ impl Client { Ok(()) } + fn generate_reward_messages( + &self, + rewarded_set: &[RewardedNodeWithParams], + ) -> Vec<(ExecuteMsg, Vec)> { + rewarded_set + .iter() + .map(|node| (*node).into()) + .zip(std::iter::repeat(Vec::new())) + .collect() + } + pub(crate) async fn send_rewarding_messages( &self, - nodes: &[MixnodeWithPerformance], + rewarded_set: &[RewardedNodeWithParams], ) -> Result<(), NyxdError> { - // for some reason, compiler complains if this is explicitly inline in code ¯\_(ツ)_/¯ - #[inline] - #[allow(unused_variables)] - fn generate_reward_messages( - eligible_mixnodes: &[MixnodeWithPerformance], - ) -> Vec<(ExecuteMsg, Vec)> { - cfg_if::cfg_if! { - if #[cfg(feature = "no-reward")] { - vec![] - } else { - eligible_mixnodes - .iter() - .map(|node| (*node).into()) - .zip(std::iter::repeat(Vec::new())) - .collect() - } - } - } - // the expect is fine as we always construct the client with the mixnet contract explicitly set let mixnet_contract = nyxd_query!( self, @@ -314,7 +307,7 @@ impl Client { .clone() ); - let msgs = generate_reward_messages(nodes); + let msgs = self.generate_reward_messages(rewarded_set); // "technically" we don't need a write access to the client, // but we REALLY don't want to accidentally send any transactions while we're sending rewarding messages @@ -325,21 +318,65 @@ impl Client { &mixnet_contract, msgs, Default::default(), - format!("rewarding {} mixnodes", nodes.len()), + format!("rewarding {} nodes", rewarded_set.len()), ) .await? ); Ok(()) } - pub(crate) async fn advance_current_epoch( + fn generate_role_assignment_messages( + &self, + rewarded_set: RewardedSet, + ) -> Vec<(ExecuteMsg, Vec)> { + // currently we just assign all of them together, + // but the contract is ready to handle them separately should we need it + // if the tx is too big + let mut msgs = Vec::new(); + for (role, nodes) in [ + (Role::ExitGateway, rewarded_set.exit_gateways), + (Role::EntryGateway, rewarded_set.entry_gateways), + (Role::Layer1, rewarded_set.layer1), + (Role::Layer2, rewarded_set.layer2), + (Role::Layer3, rewarded_set.layer3), + (Role::Standby, rewarded_set.standby), + ] { + msgs.push(( + ExecuteMsg::AssignRoles { + assignment: RoleAssignment { role, nodes }, + }, + Vec::new(), + )); + } + msgs + } + + pub(crate) async fn send_role_assignment_messages( &self, - new_rewarded_set: Vec, - expected_active_set_size: u32, + rewarded_set: RewardedSet, ) -> Result<(), NyxdError> { + // the expect is fine as we always construct the client with the mixnet contract explicitly set + let mixnet_contract = nyxd_query!( + self, + mixnet_contract_address() + .expect("mixnet contract address is not available") + .clone() + ); + + let msgs = self.generate_role_assignment_messages(rewarded_set); + + // "technically" we don't need a write access to the client, + // but we REALLY don't want to accidentally send any transactions while we're sending rewarding messages + // as that would have messed up sequence numbers nyxd_signing!( self, - advance_current_epoch(new_rewarded_set, expected_active_set_size, None).await? + execute_multiple( + &mixnet_contract, + msgs, + Default::default(), + "assigning all the rewarded set roles", + ) + .await? ); Ok(()) } diff --git a/nym-api/src/support/storage/manager.rs b/nym-api/src/support/storage/manager.rs index 62bb77921b..f34ddb4045 100644 --- a/nym-api/src/support/storage/manager.rs +++ b/nym-api/src/support/storage/manager.rs @@ -1,14 +1,18 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::node_status_api::models::{HistoricalUptime, Uptime}; + +use crate::node_status_api::models::{HistoricalUptime as ApiHistoricalUptime, Uptime}; use crate::node_status_api::utils::{ActiveGatewayStatuses, ActiveMixnodeStatuses}; use crate::support::storage::models::{ - ActiveGateway, ActiveMixnode, GatewayDetails, MixnodeDetails, NodeStatus, RewardingReport, - TestedGatewayStatus, TestedMixnodeStatus, TestingRoute, + ActiveGateway, ActiveMixnode, GatewayDetails, HistoricalUptime, MixnodeDetails, NodeStatus, + RewardingReport, TestedGatewayStatus, TestedMixnodeStatus, TestingRoute, }; -use nym_mixnet_contract_common::{EpochId, IdentityKey, MixId}; -use nym_types::monitoring::{GatewayResult, MixnodeResult, NodeResult}; -use time::OffsetDateTime; +use crate::support::storage::DbIdCache; +use nym_mixnet_contract_common::{EpochId, IdentityKey, NodeId}; +use nym_types::monitoring::NodeResult; +use sqlx::FromRow; +use time::{Date, OffsetDateTime}; +use tracing::info; #[derive(Clone)] pub(crate) struct StorageManager { @@ -16,12 +20,12 @@ pub(crate) struct StorageManager { } pub struct AvgMixnodeReliability { - mix_id: MixId, + mix_id: NodeId, value: Option, } impl AvgMixnodeReliability { - pub fn mix_id(&self) -> MixId { + pub fn mix_id(&self) -> NodeId { self.mix_id } @@ -30,14 +34,15 @@ impl AvgMixnodeReliability { } } +#[derive(FromRow)] pub struct AvgGatewayReliability { - identity: String, + node_id: NodeId, value: Option, } impl AvgGatewayReliability { - pub fn identity(&self) -> &str { - &self.identity + pub fn node_id(&self) -> NodeId { + self.node_id } pub fn value(&self) -> f32 { @@ -47,24 +52,7 @@ impl AvgGatewayReliability { // all SQL goes here impl StorageManager { - pub(crate) async fn get_mixnode_mix_ids_by_identity( - &self, - identity: &str, - ) -> Result, sqlx::Error> { - let ids = sqlx::query!( - r#"SELECT mix_id as "mix_id: MixId" FROM mixnode_details WHERE identity_key = ?"#, - identity - ) - .fetch_all(&self.connection_pool) - .await? - .into_iter() - .map(|row| row.mix_id) - .collect(); - - Ok(ids) - } - - pub(crate) async fn get_all_avg_mix_reliability_in_last_24hr( + pub(super) async fn get_all_avg_mix_reliability_in_last_24hr( &self, end_ts_secs: i64, ) -> Result, sqlx::Error> { @@ -73,7 +61,7 @@ impl StorageManager { .await } - pub(crate) async fn get_all_avg_gateway_reliability_in_last_24hr( + pub(super) async fn get_all_avg_gateway_reliability_in_last_24hr( &self, end_ts_secs: i64, ) -> Result, sqlx::Error> { @@ -82,7 +70,7 @@ impl StorageManager { .await } - pub(crate) async fn get_all_avg_mix_reliability_in_time_interval( + pub(super) async fn get_all_avg_mix_reliability_in_time_interval( &self, start_ts_secs: i64, end_ts_secs: i64, @@ -91,7 +79,7 @@ impl StorageManager { AvgMixnodeReliability, r#" SELECT - d.mix_id as "mix_id: MixId", + d.mix_id as "mix_id: NodeId", AVG(s.reliability) as "value: f32" FROM mixnode_details d @@ -110,7 +98,7 @@ impl StorageManager { Ok(result) } - pub(crate) async fn get_all_avg_gateway_reliability_in_interval( + pub(super) async fn get_all_avg_gateway_reliability_in_interval( &self, start_ts_secs: i64, end_ts_secs: i64, @@ -119,7 +107,7 @@ impl StorageManager { AvgGatewayReliability, r#" SELECT - d.identity as "identity: String", + d.node_id as "node_id: NodeId", CASE WHEN count(*) > 3 THEN AVG(reliability) ELSE 100 END as "value: f32" FROM gateway_details d @@ -143,9 +131,9 @@ impl StorageManager { /// # Arguments /// /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - pub(crate) async fn get_mixnode_database_id( + pub(super) async fn get_mixnode_database_id( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result, sqlx::Error> { let id = sqlx::query!("SELECT id FROM mixnode_details WHERE mix_id = ?", mix_id) .fetch_optional(&self.connection_pool) @@ -155,12 +143,23 @@ impl StorageManager { Ok(id) } + pub(super) async fn get_gateway_database_id( + &self, + node_id: NodeId, + ) -> Result, sqlx::Error> { + let id = sqlx::query!("SELECT id FROM gateway_details WHERE node_id = ?", node_id) + .fetch_optional(&self.connection_pool) + .await? + .map(|row| row.id); + + Ok(id) + } + /// Tries to obtain row id of given gateway given its identity - /// - /// # Arguments - /// - /// * `identity`: identity (base58-encoded public key) of the gateway. - pub(crate) async fn get_gateway_id(&self, identity: &str) -> Result, sqlx::Error> { + pub(super) async fn get_gateway_database_id_by_identity( + &self, + identity: &str, + ) -> Result, sqlx::Error> { let id = sqlx::query!( "SELECT id FROM gateway_details WHERE identity = ?", identity @@ -172,61 +171,54 @@ impl StorageManager { Ok(id) } - /// Tries to obtain owner value of given mixnode given its mix_id - /// - /// # Arguments - /// - /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - pub(crate) async fn get_mixnode_owner( + pub(super) async fn get_gateway_node_id_from_identity_key( &self, - mix_id: MixId, - ) -> Result, sqlx::Error> { - let owner = sqlx::query!("SELECT owner FROM mixnode_details WHERE mix_id = ?", mix_id) - .fetch_optional(&self.connection_pool) - .await? - .map(|row| row.owner); + identity: &str, + ) -> Result, sqlx::Error> { + let node_id = sqlx::query!( + r#"SELECT node_id as "node_id: NodeId" FROM gateway_details WHERE identity = ?"#, + identity + ) + .fetch_optional(&self.connection_pool) + .await? + .map(|row| row.node_id); - Ok(owner) + Ok(node_id) } - /// Tries to obtain identity value of given mixnode given its mix_id - /// - /// # Arguments - /// - /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - pub(crate) async fn get_mixnode_identity_key( + pub(super) async fn get_gateway_identity_key( &self, - mix_id: MixId, + node_id: NodeId, ) -> Result, sqlx::Error> { let identity_key = sqlx::query!( - "SELECT identity_key FROM mixnode_details WHERE mix_id = ?", - mix_id + "SELECT identity FROM gateway_details WHERE node_id = ?", + node_id ) .fetch_optional(&self.connection_pool) .await? - .map(|row| row.identity_key); + .map(|row| row.identity); Ok(identity_key) } - /// Tries to obtain owner value of given gateway given its identity + /// Tries to obtain identity value of given mixnode given its mix_id /// /// # Arguments /// - /// * `identity`: identity (base58-encoded public key) of the gateway. - pub(crate) async fn get_gateway_owner( + /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. + pub(super) async fn get_mixnode_identity_key( &self, - identity: &str, - ) -> Result, sqlx::Error> { - let owner = sqlx::query!( - "SELECT owner FROM gateway_details WHERE identity = ?", - identity + mix_id: NodeId, + ) -> Result, sqlx::Error> { + let identity_key = sqlx::query!( + "SELECT identity_key FROM mixnode_details WHERE mix_id = ?", + mix_id ) .fetch_optional(&self.connection_pool) .await? - .map(|row| row.owner); + .map(|row| row.identity_key); - Ok(owner) + Ok(identity_key) } /// Gets all reliability statuses for mixnode with particular identity that were inserted @@ -236,9 +228,9 @@ impl StorageManager { /// /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. /// * `timestamp`: unix timestamp of the lower bound of the selection. - pub(crate) async fn get_mixnode_statuses_since( + pub(super) async fn get_mixnode_statuses_since( &self, - mix_id: MixId, + mix_id: NodeId, timestamp: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( @@ -264,9 +256,9 @@ impl StorageManager { /// /// * `identity`: identity (base58-encoded public key) of the gateway. /// * `timestamp`: unix timestamp of the lower bound of the selection. - pub(crate) async fn get_gateway_statuses_since( + pub(super) async fn get_gateway_statuses_since( &self, - identity: &str, + node_id: NodeId, timestamp: i64, ) -> Result, sqlx::Error> { sqlx::query_as!( @@ -276,9 +268,9 @@ impl StorageManager { FROM gateway_status JOIN gateway_details ON gateway_status.gateway_details_id = gateway_details.id - WHERE gateway_details.identity=? AND gateway_status.timestamp > ?; + WHERE gateway_details.node_id=? AND gateway_status.timestamp > ?; "#, - identity, + node_id, timestamp, ) .fetch_all(&self.connection_pool) @@ -290,18 +282,18 @@ impl StorageManager { /// # Arguments /// /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. - pub(crate) async fn get_mixnode_historical_uptimes( + pub(super) async fn get_mixnode_historical_uptimes( &self, - mix_id: MixId, - ) -> Result, sqlx::Error> { + mix_id: NodeId, + ) -> Result, sqlx::Error> { let uptimes = sqlx::query!( r#" SELECT date, uptime - FROM mixnode_historical_uptime - JOIN mixnode_details - ON mixnode_historical_uptime.mixnode_details_id = mixnode_details.id - WHERE mixnode_details.mix_id = ? - ORDER BY date ASC + FROM mixnode_historical_uptime + JOIN mixnode_details + ON mixnode_historical_uptime.mixnode_details_id = mixnode_details.id + WHERE mixnode_details.mix_id = ? + ORDER BY date ASC "#, mix_id ) @@ -312,7 +304,7 @@ impl StorageManager { // better safe than sorry and not use an unwrap) .filter_map(|row| { Uptime::try_from(row.uptime.unwrap_or_default()) - .map(|uptime| HistoricalUptime { + .map(|uptime| ApiHistoricalUptime { date: row.date.unwrap_or_default(), uptime, }) @@ -328,20 +320,20 @@ impl StorageManager { /// # Arguments /// /// * `identity`: identity (base58-encoded public key) of the gateway. - pub(crate) async fn get_gateway_historical_uptimes( + pub(super) async fn get_gateway_historical_uptimes( &self, - identity: &str, - ) -> Result, sqlx::Error> { + node_id: NodeId, + ) -> Result, sqlx::Error> { let uptimes = sqlx::query!( r#" SELECT date, uptime - FROM gateway_historical_uptime - JOIN gateway_details - ON gateway_historical_uptime.gateway_details_id = gateway_details.id - WHERE gateway_details.identity = ? - ORDER BY date ASC + FROM gateway_historical_uptime + JOIN gateway_details + ON gateway_historical_uptime.gateway_details_id = gateway_details.id + WHERE gateway_details.node_id = ? + ORDER BY date ASC "#, - identity + node_id ) .fetch_all(&self.connection_pool) .await? @@ -350,7 +342,7 @@ impl StorageManager { // better safe than sorry and not use an unwrap) .filter_map(|row| { Uptime::try_from(row.uptime.unwrap_or_default()) - .map(|uptime| HistoricalUptime { + .map(|uptime| ApiHistoricalUptime { date: row.date.unwrap_or_default(), uptime, }) @@ -361,6 +353,54 @@ impl StorageManager { Ok(uptimes) } + pub(super) async fn get_historical_mix_uptime_on( + &self, + contract_node_id: i64, + date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + HistoricalUptime, + r#" + SELECT date as "date!: Date", uptime as "uptime!" + FROM mixnode_historical_uptime + JOIN mixnode_details + ON mixnode_historical_uptime.mixnode_details_id = mixnode_details.id + WHERE + mixnode_details.mix_id = ? + AND + mixnode_historical_uptime.date = ? + "#, + contract_node_id, + date + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(super) async fn get_historical_gateway_uptime_on( + &self, + contract_node_id: i64, + date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + HistoricalUptime, + r#" + SELECT date as "date!: Date", uptime as "uptime!" + FROM gateway_historical_uptime + JOIN gateway_details + ON gateway_historical_uptime.gateway_details_id = gateway_details.id + WHERE + gateway_details.node_id = ? + AND + gateway_historical_uptime.date = ? + "#, + contract_node_id, + date + ) + .fetch_optional(&self.connection_pool) + .await + } + /// Gets all reliability statuses for mixnode with particular id that were inserted /// into the database within the specified time interval. /// @@ -368,7 +408,7 @@ impl StorageManager { /// /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `until`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_mixnode_statuses_by_database_id( + pub(super) async fn get_mixnode_statuses_by_database_id( &self, id: i64, since: i64, @@ -389,24 +429,39 @@ impl StorageManager { .await } - pub(crate) async fn get_mixnode_average_reliability_in_interval( + pub(super) async fn get_mixnode_average_reliability_in_interval( &self, id: i64, start: i64, end: i64, ) -> Result, sqlx::Error> { - let result = sqlx::query!( - r#" - SELECT AVG(reliability) as "reliability: f32" FROM mixnode_status - WHERE mixnode_details_id= ? AND timestamp >= ? AND timestamp <= ? - "#, - id, - start, - end - ) - .fetch_one(&self.connection_pool) - .await?; - Ok(result.reliability) + if cfg!(feature = "v2-performance") { + let result = sqlx::query!( + r#" + SELECT AVG(reliability) as "reliability: f32" FROM mixnode_status_v2 + WHERE mixnode_details_id= ? AND timestamp >= ? AND timestamp <= ? + "#, + id, + start, + end + ) + .fetch_one(&self.connection_pool) + .await?; + Ok(result.reliability) + } else { + let result = sqlx::query!( + r#" + SELECT AVG(reliability) as "reliability: f32" FROM mixnode_status + WHERE mixnode_details_id= ? AND timestamp >= ? AND timestamp <= ? + "#, + id, + start, + end + ) + .fetch_one(&self.connection_pool) + .await?; + Ok(result.reliability) + } } pub(super) async fn get_gateway_average_reliability_in_interval( @@ -436,7 +491,7 @@ impl StorageManager { /// /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `until`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_gateway_statuses_by_id( + pub(super) async fn get_gateway_statuses_by_database_id( &self, id: i64, since: i64, @@ -463,28 +518,36 @@ impl StorageManager { /// /// * `timestamp`: unix timestamp indicating when the measurements took place. /// * `mixnode_results`: reliability results of each node that got tested. - pub(crate) async fn submit_mixnode_statuses( + pub(super) async fn submit_mixnode_statuses( &self, timestamp: i64, - mixnode_results: Vec, + mixnode_results: Vec, + id_cache: &DbIdCache, ) -> Result<(), sqlx::Error> { // insert it all in a transaction to make sure all nodes are updated at the same time // (plus it's a nice guard against new nodes) let mut tx = self.connection_pool.begin().await?; for mixnode_result in mixnode_results { - let mixnode_id = sqlx::query!( - r#" - INSERT OR IGNORE INTO mixnode_details(mix_id, identity_key, owner) VALUES (?, ?, ?); - SELECT id FROM mixnode_details WHERE mix_id = ?; - "#, - mixnode_result.mix_id, - mixnode_result.identity, - mixnode_result.owner, - mixnode_result.mix_id, - ) - .fetch_one(&mut tx) - .await? - .id; + let mixnode_id = match id_cache.mixnode_db_id(mixnode_result.node_id) { + Some(id) => id, + None => { + let mixnode_id = sqlx::query!( + r#" + INSERT OR IGNORE INTO mixnode_details(mix_id, identity_key) VALUES (?, ?); + SELECT id FROM mixnode_details WHERE mix_id = ?; + "#, + mixnode_result.node_id, + mixnode_result.identity, + mixnode_result.node_id, + ) + .fetch_one(&mut *tx) + .await? + .id; + id_cache.set_mixnode_db_id(mixnode_result.node_id, mixnode_id); + + mixnode_id + } + }; // insert the actual status sqlx::query!( @@ -495,7 +558,7 @@ impl StorageManager { mixnode_result.reliability, timestamp ) - .execute(&mut tx) + .execute(&mut *tx) .await?; } @@ -503,7 +566,7 @@ impl StorageManager { tx.commit().await } - pub(crate) async fn submit_mixnode_statuses_v2( + pub(super) async fn submit_mixnode_statuses_v2( &self, mixnode_results: &[NodeResult], ) -> Result<(), sqlx::Error> { @@ -523,7 +586,7 @@ impl StorageManager { mixnode_result.identity, mixnode_result.node_id, ) - .fetch_one(&mut tx) + .fetch_one(&mut *tx) .await? .id; @@ -536,7 +599,7 @@ impl StorageManager { mixnode_result.reliability, timestamp ) - .execute(&mut tx) + .execute(&mut *tx) .await?; } @@ -550,10 +613,11 @@ impl StorageManager { /// /// * `timestamp`: unix timestamp indicating when the measurements took place. /// * `gateway_results`: reliability results of each node that got tested. - pub(crate) async fn submit_gateway_statuses( + pub(super) async fn submit_gateway_statuses( &self, timestamp: i64, - gateway_results: Vec, + gateway_results: Vec, + id_cache: &DbIdCache, ) -> Result<(), sqlx::Error> { // insert it all in a transaction to make sure all nodes are updated at the same time // (plus it's a nice guard against new nodes) @@ -561,39 +625,45 @@ impl StorageManager { for gateway_result in gateway_results { // if gateway info doesn't exist, insert it and get its id - // same ID "problem" as described for mixnode insertion - let gateway_id = sqlx::query!( - r#" - INSERT OR IGNORE INTO gateway_details(identity, owner) VALUES (?, ?); - SELECT id FROM gateway_details WHERE identity = ?; - "#, - gateway_result.identity, - gateway_result.owner, - gateway_result.identity, - ) - .fetch_one(&mut tx) - .await? - .id; + let gateway_id = match id_cache.gateway_db_id(gateway_result.node_id) { + Some(id) => id, + None => { + let gateway_id = sqlx::query!( + r#" + INSERT OR IGNORE INTO gateway_details(node_id, identity) VALUES (?, ?); + SELECT id FROM gateway_details WHERE identity = ?; + "#, + gateway_result.node_id, + gateway_result.identity, + gateway_result.identity, + ) + .fetch_one(&mut *tx) + .await? + .id; + id_cache.set_gateway_db_id(gateway_result.node_id, gateway_id); + gateway_id + } + }; // insert the actual status sqlx::query!( - r#" - INSERT INTO gateway_status (gateway_details_id, reliability, timestamp) VALUES (?, ?, ?); - "#, - gateway_id, - gateway_result.reliability, - timestamp - ) - .execute(&mut tx) - .await?; + r#" + INSERT INTO gateway_status (gateway_details_id, reliability, timestamp) VALUES (?, ?, ?); + "#, + gateway_id, + gateway_result.reliability, + timestamp + ) + .execute(&mut *tx) + .await?; } // finally commit the transaction tx.commit().await } - pub(crate) async fn submit_gateway_statuses_v2( + pub(super) async fn submit_gateway_statuses_v2( &self, gateway_results: &[NodeResult], ) -> Result<(), sqlx::Error> { @@ -617,7 +687,7 @@ impl StorageManager { gateway_result.node_id, gateway_result.identity, ) - .fetch_one(&mut tx) + .fetch_one(&mut *tx) .await? .id; @@ -630,7 +700,7 @@ impl StorageManager { gateway_result.reliability, timestamp ) - .execute(&mut tx) + .execute(&mut *tx) .await?; } @@ -644,7 +714,7 @@ impl StorageManager { /// # Arguments /// /// * `testing_route`: test route used for this particular network monitor run. - pub(crate) async fn submit_testing_route_used( + pub(super) async fn submit_testing_route_used( &self, testing_route: TestingRoute, ) -> Result<(), sqlx::Error> { @@ -672,7 +742,7 @@ impl StorageManager { /// /// * `db_mixnode_id`: id (as saved in the database) of the mixnode. /// * `since`: unix timestamp indicating the lower bound interval of the selection. - pub(crate) async fn get_mixnode_testing_route_presence_count_since( + pub(super) async fn get_mixnode_testing_route_presence_count_since( &self, db_mixnode_id: i64, since: i64, @@ -681,14 +751,14 @@ impl StorageManager { r#" SELECT COUNT(*) as count FROM ( - SELECT monitor_run_id - FROM testing_route + SELECT monitor_run_id + FROM testing_route WHERE testing_route.layer1_mix_id = ? OR testing_route.layer2_mix_id = ? OR testing_route.layer3_mix_id = ? ) testing_route - JOIN + JOIN ( - SELECT id - FROM monitor_run + SELECT id + FROM monitor_run WHERE monitor_run.timestamp > ? ) monitor_run ON monitor_run.id = testing_route.monitor_run_id; @@ -711,7 +781,7 @@ impl StorageManager { /// /// * `gateway_id`: id (as saved in the database) of the gateway. /// * `since`: unix timestamp indicating the lower bound interval of the selection. - pub(crate) async fn get_gateway_testing_route_presence_count_since( + pub(super) async fn get_gateway_testing_route_presence_count_since( &self, gateway_id: i64, since: i64, @@ -720,14 +790,14 @@ impl StorageManager { r#" SELECT COUNT(*) as count FROM ( - SELECT monitor_run_id - FROM testing_route + SELECT monitor_run_id + FROM testing_route WHERE testing_route.gateway_id = ? ) testing_route - JOIN + JOIN ( - SELECT id - FROM monitor_run + SELECT id + FROM monitor_run WHERE monitor_run.timestamp > ? ) monitor_run ON monitor_run.id = testing_route.monitor_run_id; @@ -743,7 +813,7 @@ impl StorageManager { } /// Checks whether there are already any historical uptimes with this particular date. - pub(crate) async fn check_for_historical_uptime_existence( + pub(super) async fn check_for_historical_uptime_existence( &self, today_iso_8601: &str, ) -> Result { @@ -753,7 +823,7 @@ impl StorageManager { ) .fetch_one(&self.connection_pool) .await - .map(|result| result.exists == 1) + .map(|result| result.exists == Some(1)) } /// Creates new entry for mixnode historical uptime @@ -763,7 +833,7 @@ impl StorageManager { /// * `node_id`: id of the mixnode (as inserted in `mixnode_details_id` table). /// * `date`: date associated with the uptime represented in ISO 8601, i.e. YYYY-MM-DD. /// * `uptime`: the actual uptime of the node during the specified day. - pub(crate) async fn insert_mixnode_historical_uptime( + pub(super) async fn insert_mixnode_historical_uptime( &self, mix_id: i64, date: &str, @@ -785,15 +855,15 @@ impl StorageManager { /// * `node_id`: id of the gateway (as inserted in `gateway_details_id` table). /// * `date`: date associated with the uptime represented in ISO 8601, i.e. YYYY-MM-DD. /// * `uptime`: the actual uptime of the node during the specified day. - pub(crate) async fn insert_gateway_historical_uptime( + pub(super) async fn insert_gateway_historical_uptime( &self, - mix_id: i64, + db_id: i64, date: &str, uptime: u8, ) -> Result<(), sqlx::Error> { sqlx::query!( "INSERT INTO gateway_historical_uptime(gateway_details_id, date, uptime) VALUES (?, ?, ?)", - mix_id, + db_id, date, uptime, ).execute(&self.connection_pool).await?; @@ -806,7 +876,7 @@ impl StorageManager { /// # Arguments /// /// * `timestamp`: unix timestamp at which the monitor test run has occurred - pub(crate) async fn insert_monitor_run(&self, timestamp: i64) -> Result { + pub(super) async fn insert_monitor_run(&self, timestamp: i64) -> Result { let res = sqlx::query!("INSERT INTO monitor_run(timestamp) VALUES (?)", timestamp) .execute(&self.connection_pool) .await?; @@ -819,7 +889,7 @@ impl StorageManager { /// /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `until`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_monitor_runs_count( + pub(super) async fn get_monitor_runs_count( &self, since: i64, until: i64, @@ -841,7 +911,7 @@ impl StorageManager { /// # Arguments /// /// * `until`: timestamp specifying the purge cutoff. - pub(crate) async fn purge_old_mixnode_statuses( + pub(super) async fn purge_old_mixnode_statuses( &self, timestamp: i64, ) -> Result<(), sqlx::Error> { @@ -857,7 +927,7 @@ impl StorageManager { /// # Arguments /// /// * `until`: timestamp specifying the purge cutoff. - pub(crate) async fn purge_old_gateway_statuses( + pub(super) async fn purge_old_gateway_statuses( &self, timestamp: i64, ) -> Result<(), sqlx::Error> { @@ -874,7 +944,7 @@ impl StorageManager { /// /// * `since`: indicates the lower bound timestamp for deciding whether given mixnode is active /// * `until`: indicates the upper bound timestamp for deciding whether given mixnode is active - pub(crate) async fn get_all_active_mixnodes_in_interval( + pub(super) async fn get_all_active_mixnodes_in_interval( &self, since: i64, until: i64, @@ -886,7 +956,7 @@ impl StorageManager { sqlx::query_as!( ActiveMixnode, r#" - SELECT DISTINCT identity_key, mix_id as "mix_id: MixId", owner, id + SELECT DISTINCT identity_key, mix_id as "mix_id: NodeId", id FROM mixnode_details JOIN mixnode_status ON mixnode_details.id = mixnode_status.mixnode_details_id @@ -908,7 +978,7 @@ impl StorageManager { /// /// * `since`: indicates the lower bound timestamp for deciding whether given gateway is active /// * `until`: indicates the upper bound timestamp for deciding whether given gateway is active - pub(crate) async fn get_all_active_gateways_in_interval( + pub(super) async fn get_all_active_gateways_in_interval( &self, since: i64, until: i64, @@ -916,7 +986,7 @@ impl StorageManager { sqlx::query_as!( ActiveGateway, r#" - SELECT DISTINCT identity, owner, id + SELECT DISTINCT identity, node_id as "node_id: NodeId", id FROM gateway_details JOIN gateway_status ON gateway_details.id = gateway_status.gateway_details_id @@ -925,7 +995,7 @@ impl StorageManager { ) "#, since, - until, + until ) .fetch_all(&self.connection_pool) .await @@ -955,7 +1025,7 @@ impl StorageManager { /// /// * `report`: report to insert into the database #[allow(unused)] - pub(crate) async fn insert_rewarding_report( + pub(super) async fn insert_rewarding_report( &self, report: RewardingReport, ) -> Result<(), sqlx::Error> { @@ -974,17 +1044,17 @@ impl StorageManager { } #[allow(unused)] - pub(crate) async fn get_rewarding_report( + pub(super) async fn get_rewarding_report( &self, absolute_epoch_id: EpochId, ) -> Result, sqlx::Error> { sqlx::query_as!( RewardingReport, r#" - SELECT + SELECT absolute_epoch_id as "absolute_epoch_id: u32", eligible_mixnodes as "eligible_mixnodes: u32" - FROM rewarding_report + FROM rewarding_report WHERE absolute_epoch_id = ? "#, absolute_epoch_id @@ -999,7 +1069,7 @@ impl StorageManager { /// /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `until`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_all_active_mixnodes_statuses_in_interval( + pub(super) async fn get_all_active_mixnodes_statuses_in_interval( &self, since: i64, until: i64, @@ -1017,7 +1087,6 @@ impl StorageManager { let statuses = ActiveMixnodeStatuses { mix_id: active_node.mix_id, identity: active_node.identity_key, - owner: active_node.owner, statuses, }; @@ -1033,7 +1102,7 @@ impl StorageManager { /// /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `until`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_all_active_gateways_statuses_in_interval( + pub(super) async fn get_all_active_gateways_statuses_in_interval( &self, since: i64, until: i64, @@ -1045,12 +1114,12 @@ impl StorageManager { let mut active_day_statuses = Vec::with_capacity(active_nodes.len()); for active_node in active_nodes.into_iter() { let statuses = self - .get_gateway_statuses_by_id(active_node.id, since, until) + .get_gateway_statuses_by_database_id(active_node.id, since, until) .await?; let statuses = ActiveGatewayStatuses { + node_id: active_node.node_id, identity: active_node.identity, - owner: active_node.owner, statuses, }; @@ -1060,7 +1129,7 @@ impl StorageManager { Ok(active_day_statuses) } - pub(crate) async fn get_mixnode_details_by_db_id( + pub(super) async fn get_mixnode_details_by_db_id( &self, id: i64, ) -> Result, sqlx::Error> { @@ -1073,20 +1142,19 @@ impl StorageManager { .await } - pub(crate) async fn get_gateway_details_by_db_id( + pub(super) async fn get_gateway_details_by_db_id( &self, id: i64, ) -> Result, sqlx::Error> { - sqlx::query_as!( - GatewayDetails, - "SELECT * FROM gateway_details WHERE id = ?", - id - ) - .fetch_optional(&self.connection_pool) - .await + // we can't use `query_as!` macro because we don't apply all required table changes during sqlx migrations. + // some (like v3 directory) happens at runtime + sqlx::query_as("SELECT * FROM gateway_details WHERE id = ?") + .bind(id) + .fetch_optional(&self.connection_pool) + .await } - pub(crate) async fn get_mixnode_statuses_count(&self, db_id: i64) -> Result { + pub(super) async fn get_mixnode_statuses_count(&self, db_id: i64) -> Result { sqlx::query!( r#" SELECT COUNT(*) as count @@ -1102,9 +1170,9 @@ impl StorageManager { .map(|record| record.count) } - pub(crate) async fn get_mixnode_statuses( + pub(super) async fn get_mixnode_statuses( &self, - mix_id: MixId, + mix_id: NodeId, limit: u32, offset: u32, ) -> Result, sqlx::Error> { @@ -1138,7 +1206,7 @@ impl StorageManager { .await } - pub(crate) async fn get_gateway_statuses_count(&self, db_id: i64) -> Result { + pub(super) async fn get_gateway_statuses_count(&self, db_id: i64) -> Result { sqlx::query!( r#" SELECT COUNT(*) as count @@ -1154,7 +1222,7 @@ impl StorageManager { .map(|record| record.count) } - pub(crate) async fn get_gateway_statuses( + pub(super) async fn get_gateway_statuses( &self, gateway_identity: &str, limit: u32, @@ -1189,3 +1257,86 @@ impl StorageManager { .await } } + +pub(crate) mod v3_migration { + use crate::support::storage::manager::StorageManager; + use crate::support::storage::models::GatewayDetailsBeforeMigration; + use nym_mixnet_contract_common::NodeId; + + impl StorageManager { + pub(crate) async fn check_v3_migration(&self) -> Result { + sqlx::query!("SELECT EXISTS (SELECT 1 FROM v3_migration_info) AS 'exists'",) + .fetch_one(&self.connection_pool) + .await + .map(|result| result.exists == Some(1)) + } + + pub(crate) async fn set_v3_migration_completion(&self) -> Result<(), sqlx::Error> { + sqlx::query!("INSERT INTO v3_migration_info(id) VALUES (0)") + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_all_known_gateways( + &self, + ) -> Result, sqlx::Error> { + sqlx::query_as("SELECT * FROM gateway_details") + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn set_gateway_node_id( + &self, + identity: &str, + node_id: NodeId, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "UPDATE gateway_details SET node_id = ? WHERE identity = ?", + node_id, + identity + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn purge_gateway(&self, db_id: i64) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + DELETE FROM gateway_historical_uptime WHERE gateway_details_id = ?; + DELETE FROM gateway_status WHERE gateway_details_id = ?; + DELETE FROM testing_route WHERE gateway_id = ?; + DELETE FROM gateway_details WHERE id = ?; + "#, + db_id, + db_id, + db_id, + db_id, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn make_node_id_not_null(&self) -> Result<(), sqlx::Error> { + sqlx::query( + r#" + CREATE TABLE gateway_details_temp + ( + id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + node_id INTEGER NOT NULL UNIQUE, + identity VARCHAR NOT NULL UNIQUE + ); + + INSERT INTO gateway_details_temp(id, node_id, identity) SELECT id, node_id, identity FROM gateway_details; + DROP TABLE gateway_details; + ALTER TABLE gateway_details_temp RENAME TO gateway_details; + "#, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + } +} diff --git a/nym-api/src/support/storage/mod.rs b/nym-api/src/support/storage/mod.rs index d4cdc64c02..26accca24e 100644 --- a/nym-api/src/support/storage/mod.rs +++ b/nym-api/src/support/storage/mod.rs @@ -1,48 +1,82 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only +use self::manager::{AvgGatewayReliability, AvgMixnodeReliability}; use crate::network_monitor::test_route::TestRoute; use crate::node_status_api::models::{ - GatewayStatusReport, GatewayUptimeHistory, MixnodeStatusReport, MixnodeUptimeHistory, - NymApiStorageError, Uptime, + GatewayStatusReport, GatewayUptimeHistory, HistoricalUptime as ApiHistoricalUptime, + MixnodeStatusReport, MixnodeUptimeHistory, NymApiStorageError, Uptime, }; use crate::node_status_api::{ONE_DAY, ONE_HOUR}; use crate::storage::manager::StorageManager; use crate::storage::models::{NodeStatus, TestingRoute}; use crate::support::storage::models::{ - GatewayDetails, MixnodeDetails, TestedGatewayStatus, TestedMixnodeStatus, + GatewayDetails, HistoricalUptime, MixnodeDetails, TestedGatewayStatus, TestedMixnodeStatus, }; -use nym_mixnet_contract_common::MixId; -use nym_types::monitoring::{GatewayResult, MixnodeResult}; -use rocket::fairing::AdHoc; +use dashmap::DashMap; +use nym_mixnet_contract_common::NodeId; +use nym_types::monitoring::NodeResult; use sqlx::ConnectOptions; use std::path::Path; -use time::OffsetDateTime; - -use self::manager::{AvgGatewayReliability, AvgMixnodeReliability}; +use std::sync::Arc; +use std::time::Duration; +use time::{Date, OffsetDateTime}; +use tracing::log::LevelFilter; +use tracing::{error, info, warn}; pub(crate) mod manager; pub(crate) mod models; +#[derive(Default)] +pub(crate) struct DbIdCache { + pub mixnodes_v1: DashMap, + pub gateways_v1: DashMap, +} + +impl DbIdCache { + pub(crate) fn mixnode_db_id(&self, node_id: NodeId) -> Option { + self.mixnodes_v1.get(&node_id).map(|v| *v) + } + + pub(crate) fn gateway_db_id(&self, node_id: NodeId) -> Option { + self.gateways_v1.get(&node_id).map(|v| *v) + } + + pub(crate) fn set_mixnode_db_id(&self, node_id: NodeId, db_id: i64) { + self.mixnodes_v1.insert(node_id, db_id); + } + + pub(crate) fn set_gateway_db_id(&self, node_id: NodeId, db_id: i64) { + self.gateways_v1.insert(node_id, db_id); + } +} + // note that clone here is fine as upon cloning the same underlying pool will be used #[derive(Clone)] pub(crate) struct NymApiStorage { pub manager: StorageManager, + + pub db_id_cache: Arc, } impl NymApiStorage { pub async fn init>(database_path: P) -> Result { // TODO: we can inject here more stuff based on our nym-api global config // struct. Maybe different pool size or timeout intervals? - let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + let connect_opts = sqlx::sqlite::SqliteConnectOptions::new() .filename(database_path) - .create_if_missing(true); + .create_if_missing(true) + .log_statements(LevelFilter::Trace) + .log_slow_statements(LevelFilter::Warn, Duration::from_millis(250)); // TODO: do we want auto_vacuum ? - opts.disable_statement_logging(); + let pool_opts = sqlx::sqlite::SqlitePoolOptions::new() + .min_connections(5) + .max_connections(25) + .acquire_timeout(Duration::from_secs(60)); - let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { + let connection_pool = match pool_opts.connect_with(connect_opts).await { Ok(db) => db, Err(err) => { error!("Failed to connect to SQLx database: {err}"); @@ -59,39 +93,38 @@ impl NymApiStorage { let storage = NymApiStorage { manager: StorageManager { connection_pool }, + db_id_cache: Arc::new(Default::default()), }; Ok(storage) } - #[deprecated(note = "TODO rocket: obsolete because it's used for Rocket")] - pub(crate) fn stage(storage: NymApiStorage) -> AdHoc { - AdHoc::try_on_ignite("SQLx Database", |rocket| async { - Ok(rocket.manage(storage)) - }) - } - - #[allow(unused)] - pub(crate) async fn mix_identity_to_mix_ids( + pub(crate) async fn get_mixnode_database_id( &self, - identity: &str, - ) -> Result, NymApiStorageError> { - Ok(self - .manager - .get_mixnode_mix_ids_by_identity(identity) - .await?) + node_id: NodeId, + ) -> Result, NymApiStorageError> { + if let Some(cached) = self.db_id_cache.mixnode_db_id(node_id) { + return Ok(Some(cached)); + } + if let Some(retrieved) = self.manager.get_mixnode_database_id(node_id).await? { + self.db_id_cache.set_mixnode_db_id(node_id, retrieved); + return Ok(Some(retrieved)); + } + Ok(None) } - #[allow(unused)] - pub(crate) async fn mix_identity_to_latest_mix_id( + pub(crate) async fn get_gateway_database_id( &self, - identity: &str, - ) -> Result, NymApiStorageError> { - Ok(self - .mix_identity_to_mix_ids(identity) - .await? - .into_iter() - .max()) + node_id: NodeId, + ) -> Result, NymApiStorageError> { + if let Some(cached) = self.db_id_cache.gateway_db_id(node_id) { + return Ok(Some(cached)); + } + if let Some(retrieved) = self.manager.get_gateway_database_id(node_id).await? { + self.db_id_cache.set_gateway_db_id(node_id, retrieved); + return Ok(Some(retrieved)); + } + Ok(None) } pub(crate) async fn get_all_avg_gateway_reliability_in_last_24hr( @@ -127,7 +160,7 @@ impl NymApiStorage { /// * `since`: unix timestamp indicating the lower bound interval of the selection. async fn get_mixnode_statuses( &self, - mix_id: MixId, + mix_id: NodeId, since: i64, ) -> Result, NymApiStorageError> { let statuses = self @@ -143,16 +176,15 @@ impl NymApiStorage { /// /// # Arguments /// - /// * `identity`: identity key of the gateway to query. /// * `since`: unix timestamp indicating the lower bound interval of the selection. async fn get_gateway_statuses( &self, - identity: &str, + node_id: NodeId, since: i64, ) -> Result, NymApiStorageError> { let statuses = self .manager - .get_gateway_statuses_since(identity, since) + .get_gateway_statuses_since(node_id, since) .await?; Ok(statuses) @@ -165,7 +197,7 @@ impl NymApiStorage { /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. pub(crate) async fn construct_mixnode_report( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { let now = OffsetDateTime::now_utc(); let day_ago = (now - ONE_DAY).unix_timestamp(); @@ -186,21 +218,14 @@ impl NymApiStorage { .get_monitor_runs_count(day_ago, now.unix_timestamp()) .await?; - let mixnode_owner = - self.manager.get_mixnode_owner(mix_id).await?.expect( - "The node doesn't have an owner even though we have status information on it!", - ); - - let mixnode_identity = - self.manager.get_mixnode_identity_key(mix_id).await?.expect( - "The node doesn't have an owner even though we have status information on it!", - ); + let mixnode_identity = self.manager.get_mixnode_identity_key(mix_id).await?.expect( + "The node doesn't have an identity even though we have status information on it!", + ); Ok(MixnodeStatusReport::construct_from_last_day_reports( now, mix_id, mixnode_identity, - mixnode_owner, statuses, last_hour_runs_count, last_day_runs_count, @@ -209,19 +234,17 @@ impl NymApiStorage { pub(crate) async fn construct_gateway_report( &self, - identity: &str, + node_id: NodeId, ) -> Result { let now = OffsetDateTime::now_utc(); let day_ago = (now - ONE_DAY).unix_timestamp(); let hour_ago = (now - ONE_HOUR).unix_timestamp(); - let statuses = self.get_gateway_statuses(identity, day_ago).await?; + let statuses = self.get_gateway_statuses(node_id, day_ago).await?; // if we have no statuses, the node doesn't exist (or monitor is down), but either way, we can't make a report if statuses.is_empty() { - return Err(NymApiStorageError::GatewayReportNotFound { - identity: identity.to_owned(), - }); + return Err(NymApiStorageError::GatewayReportNotFound { node_id }); } // determine the number of runs the gateway should have been online for @@ -232,14 +255,18 @@ impl NymApiStorage { .get_monitor_runs_count(day_ago, now.unix_timestamp()) .await?; - let gateway_owner = self.manager.get_gateway_owner(identity).await?.expect( - "The gateway doesn't have an owner even though we have status information on it!", - ); + let gateway_identity = self + .manager + .get_gateway_identity_key(node_id) + .await? + .expect( + "The node doesn't have an identity even though we have status information on it!", + ); Ok(GatewayStatusReport::construct_from_last_day_reports( now, - identity.to_owned(), - gateway_owner, + node_id, + gateway_identity, statuses, last_hour_runs_count, last_day_runs_count, @@ -248,7 +275,7 @@ impl NymApiStorage { pub(crate) async fn get_mixnode_uptime_history( &self, - mix_id: MixId, + mix_id: NodeId, ) -> Result { let history = self.manager.get_mixnode_historical_uptimes(mix_id).await?; @@ -256,69 +283,123 @@ impl NymApiStorage { return Err(NymApiStorageError::MixnodeUptimeHistoryNotFound { mix_id }); } - let mixnode_owner = - self.manager.get_mixnode_owner(mix_id).await?.expect( - "The node doesn't have an owner even though we have uptime history for it!", - ); - let mixnode_identity = self.manager.get_mixnode_identity_key(mix_id).await?.expect( "The node doesn't have an identity even though we have uptime history for it!", ); - Ok(MixnodeUptimeHistory::new( - mix_id, - mixnode_identity, - mixnode_owner, - history, - )) + Ok(MixnodeUptimeHistory::new(mix_id, mixnode_identity, history)) } - pub(crate) async fn get_gateway_uptime_history( + pub(crate) async fn get_gateway_uptime_history_by_identity( &self, - identity: &str, + gateway_identity: &str, ) -> Result { - let history = self + let Some(node_id) = self .manager - .get_gateway_historical_uptimes(identity) - .await?; + .get_gateway_node_id_from_identity_key(gateway_identity) + .await? + else { + return Err(NymApiStorageError::GatewayNotFound { + identity: gateway_identity.to_string(), + }); + }; + + let history = self.manager.get_gateway_historical_uptimes(node_id).await?; if history.is_empty() { - return Err(NymApiStorageError::GatewayUptimeHistoryNotFound { - identity: identity.to_owned(), - }); + return Err(NymApiStorageError::GatewayUptimeHistoryNotFound { node_id }); } - let gateway_owner = - self.manager.get_gateway_owner(identity).await?.expect( - "The gateway doesn't have an owner even though we have uptime history for it!", - ); - Ok(GatewayUptimeHistory::new( - identity.to_owned(), - gateway_owner, + node_id, + gateway_identity, history, )) } + pub(crate) async fn get_node_uptime_history( + &self, + node_id: NodeId, + ) -> Result, NymApiStorageError> { + let history = self.manager.get_mixnode_historical_uptimes(node_id).await?; + + if !history.is_empty() { + return Ok(history); + } + + Ok(self.manager.get_gateway_historical_uptimes(node_id).await?) + } + pub(crate) async fn get_average_mixnode_uptime_in_the_last_24hrs( &self, - mix_id: MixId, + node_id: NodeId, end_ts_secs: i64, ) -> Result { let start = end_ts_secs - 86400; - self.get_average_mixnode_uptime_in_time_interval(mix_id, start, end_ts_secs) - .await + let reliability = self + .get_average_mixnode_reliability_in_time_interval(node_id, start, end_ts_secs) + .await?; + Ok(Uptime::new(reliability)) } pub(crate) async fn get_average_gateway_uptime_in_the_last_24hrs( &self, - identity: &str, + node_id: NodeId, end_ts_secs: i64, ) -> Result { let start = end_ts_secs - 86400; - self.get_average_gateway_uptime_in_time_interval(identity, start, end_ts_secs) + let reliability = self + .get_average_gateway_reliability_in_time_interval(node_id, start, end_ts_secs) + .await?; + Ok(Uptime::new(reliability)) + } + + pub(crate) async fn get_average_node_uptime_in_the_last_24hrs( + &self, + node_id: NodeId, + end_ts_secs: i64, + ) -> Result { + let start = end_ts_secs - 86400; + self.get_average_node_reliability_in_time_interval(node_id, start, end_ts_secs) .await + .map(Uptime::new) + } + + pub(crate) async fn get_historical_mix_uptime_on( + &self, + node_id: NodeId, + date: Date, + ) -> Result, NymApiStorageError> { + Ok(self + .manager + .get_historical_mix_uptime_on(node_id as i64, date) + .await?) + } + + pub(crate) async fn get_historical_gateway_uptime_on( + &self, + node_id: NodeId, + date: Date, + ) -> Result, NymApiStorageError> { + Ok(self + .manager + .get_historical_gateway_uptime_on(node_id as i64, date) + .await?) + } + + pub(crate) async fn get_historical_node_uptime_on( + &self, + node_id: NodeId, + date: Date, + ) -> Result, NymApiStorageError> { + if let Ok(result_as_mix) = self.get_historical_mix_uptime_on(node_id, date).await { + if result_as_mix.is_some() { + return Ok(result_as_mix); + } + } + + self.get_historical_gateway_uptime_on(node_id, date).await } /// Based on the data available in the validator API, determines the average uptime of particular @@ -329,15 +410,16 @@ impl NymApiStorage { /// * `mix_id`: mix-id (as assigned by the smart contract) of the mixnode. /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `end`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_average_mixnode_uptime_in_time_interval( + pub(crate) async fn get_average_mixnode_reliability_in_time_interval( &self, - mix_id: MixId, + mix_id: NodeId, start: i64, end: i64, - ) -> Result { + ) -> Result { + // those two should have been a single sql query /shrug let mixnode_database_id = match self.manager.get_mixnode_database_id(mix_id).await? { Some(id) => id, - None => return Ok(Uptime::zero()), + None => return Ok(0.), }; let reliability = self @@ -345,11 +427,7 @@ impl NymApiStorage { .get_mixnode_average_reliability_in_interval(mixnode_database_id, start, end) .await?; - if let Some(reliability) = reliability { - Ok(Uptime::new(reliability)) - } else { - Ok(Uptime::zero()) - } + Ok(reliability.unwrap_or_default()) } /// Based on the data available in the validator API, determines the average uptime of particular @@ -360,15 +438,16 @@ impl NymApiStorage { /// * `identity`: base58-encoded identity of the gateway. /// * `since`: unix timestamp indicating the lower bound interval of the selection. /// * `end`: unix timestamp indicating the upper bound interval of the selection. - pub(crate) async fn get_average_gateway_uptime_in_time_interval( + pub(crate) async fn get_average_gateway_reliability_in_time_interval( &self, - identity: &str, + node_id: NodeId, start: i64, end: i64, - ) -> Result { - let gateway_database_id = match self.manager.get_gateway_id(identity).await? { + ) -> Result { + // those two should have been a single sql query /shrug + let gateway_database_id = match self.manager.get_gateway_database_id(node_id).await? { Some(id) => id, - None => return Ok(Uptime::zero()), + None => return Ok(0.), }; let reliability = self @@ -376,11 +455,26 @@ impl NymApiStorage { .get_gateway_average_reliability_in_interval(gateway_database_id, start, end) .await?; - if let Some(reliability) = reliability { - Ok(Uptime::new(reliability)) - } else { - Ok(Uptime::zero()) + Ok(reliability.unwrap_or_default()) + } + + pub(crate) async fn get_average_node_reliability_in_time_interval( + &self, + node_id: NodeId, + start: i64, + end: i64, + ) -> Result { + if let Ok(result_as_mix) = self + .get_average_mixnode_reliability_in_time_interval(node_id, start, end) + .await + { + if result_as_mix != 0. { + return Ok(result_as_mix); + } } + + self.get_average_gateway_reliability_in_time_interval(node_id, start, end) + .await } /// Obtain status reports of mixnodes that were active in the specified time interval. @@ -416,7 +510,6 @@ impl NymApiStorage { OffsetDateTime::from_unix_timestamp(end).unwrap(), statuses.mix_id, statuses.identity, - statuses.owner, statuses.statuses, last_hour_runs_count, last_day_runs_count, @@ -458,8 +551,8 @@ impl NymApiStorage { .map(|statuses| { GatewayStatusReport::construct_from_last_day_reports( OffsetDateTime::from_unix_timestamp(end).unwrap(), + statuses.node_id, statuses.identity, - statuses.owner, statuses.statuses, last_hour_runs_count, last_day_runs_count, @@ -484,7 +577,6 @@ impl NymApiStorage { // we MUST have those entries in the database, otherwise the route wouldn't have been chosen // in the first place let layer1_mix_db_id = self - .manager .get_mixnode_database_id(test_route.layer_one_mix().mix_id) .await? .ok_or_else(|| NymApiStorageError::DatabaseInconsistency { @@ -492,7 +584,6 @@ impl NymApiStorage { })?; let layer2_mix_db_id = self - .manager .get_mixnode_database_id(test_route.layer_two_mix().mix_id) .await? .ok_or_else(|| NymApiStorageError::DatabaseInconsistency { @@ -500,7 +591,6 @@ impl NymApiStorage { })?; let layer3_mix_db_id = self - .manager .get_mixnode_database_id(test_route.layer_three_mix().mix_id) .await? .ok_or_else(|| NymApiStorageError::DatabaseInconsistency { @@ -508,8 +598,7 @@ impl NymApiStorage { })?; let gateway_db_id = self - .manager - .get_gateway_id(&test_route.gateway().identity_key.to_base58_string()) + .get_gateway_database_id(test_route.gateway().node_id) .await? .ok_or_else(|| NymApiStorageError::DatabaseInconsistency { reason: format!( @@ -539,7 +628,7 @@ impl NymApiStorage { /// * `since`: optional unix timestamp indicating the lower bound interval of the selection. pub(crate) async fn get_core_mixnode_status_count( &self, - mix_id: MixId, + mix_id: NodeId, since: Option, ) -> Result { let db_id = self.manager.get_mixnode_database_id(mix_id).await?; @@ -565,12 +654,15 @@ impl NymApiStorage { /// /// * `identity`: identity (base58-encoded public key) of the gateway. /// * `since`: optional unix timestamp indicating the lower bound interval of the selection. - pub(crate) async fn get_core_gateway_status_count( + pub(crate) async fn get_core_gateway_status_count_by_identity( &self, identity: &str, since: Option, ) -> Result { - let node_id = self.manager.get_gateway_id(identity).await?; + let node_id = self + .manager + .get_gateway_database_id_by_identity(identity) + .await?; if let Some(node_id) = node_id { let since = since @@ -595,8 +687,8 @@ impl NymApiStorage { /// * `route_results`: pub(crate) async fn insert_monitor_run_results( &self, - mixnode_results: Vec, - gateway_results: Vec, + mixnode_results: Vec, + gateway_results: Vec, test_routes: Vec, ) -> Result<(), NymApiStorageError> { info!("Submitting new node results to the database. There are {} mixnode results and {} gateway results", mixnode_results.len(), gateway_results.len()); @@ -606,11 +698,11 @@ impl NymApiStorage { let monitor_run_id = self.manager.insert_monitor_run(now).await?; self.manager - .submit_mixnode_statuses(now, mixnode_results) + .submit_mixnode_statuses(now, mixnode_results, &self.db_id_cache) .await?; self.manager - .submit_gateway_statuses(now, gateway_results) + .submit_gateway_statuses(now, gateway_results, &self.db_id_cache) .await?; for test_route in test_routes { @@ -620,6 +712,26 @@ impl NymApiStorage { Ok(()) } + pub(crate) async fn submit_mixnode_statuses_v2( + &self, + mixnode_results: &[NodeResult], + ) -> Result<(), NymApiStorageError> { + self.manager + .submit_mixnode_statuses_v2(mixnode_results) + .await?; + Ok(()) + } + + pub(crate) async fn submit_gateway_statuses_v2( + &self, + gateway_results: &[NodeResult], + ) -> Result<(), NymApiStorageError> { + self.manager + .submit_gateway_statuses_v2(gateway_results) + .await?; + Ok(()) + } + /// Obtains number of network monitor test runs that have occurred within the specified interval. /// /// # Arguments @@ -678,8 +790,8 @@ impl NymApiStorage { for report in gateway_reports { // if this ever fails, we have a super weird error because we just constructed report for that node // and we never delete node data! - let node_id = match self.manager.get_gateway_id(&report.identity).await? { - Some(node_id) => node_id, + let db_id = match self.manager.get_gateway_database_id(report.node_id).await? { + Some(db_id) => db_id, None => { error!( "Somehow we failed to grab id of gateway {} from the database!", @@ -690,7 +802,7 @@ impl NymApiStorage { }; self.manager - .insert_gateway_historical_uptime(node_id, today_iso_8601, report.last_day.u8()) + .insert_gateway_historical_uptime(db_id, today_iso_8601, report.last_day.u8()) .await?; } @@ -749,7 +861,7 @@ impl NymApiStorage { pub(crate) async fn get_mixnode_detailed_statuses( &self, - mix_id: MixId, + mix_id: NodeId, limit: u32, offset: u32, ) -> Result, NymApiStorageError> { @@ -783,3 +895,42 @@ impl NymApiStorage { .await?) } } + +pub(crate) mod v3_migration { + use crate::node_status_api::models::NymApiStorageError; + use crate::support::storage::models::GatewayDetailsBeforeMigration; + use crate::support::storage::NymApiStorage; + use nym_mixnet_contract_common::NodeId; + + impl NymApiStorage { + pub(crate) async fn check_v3_migration(&self) -> Result { + Ok(self.manager.check_v3_migration().await?) + } + + pub(crate) async fn set_v3_migration_completion(&self) -> Result<(), NymApiStorageError> { + Ok(self.manager.set_v3_migration_completion().await?) + } + + pub(crate) async fn get_all_known_gateways( + &self, + ) -> Result, NymApiStorageError> { + Ok(self.manager.get_all_known_gateways().await?) + } + + pub(crate) async fn set_gateway_node_id( + &self, + identity: &str, + node_id: NodeId, + ) -> Result<(), NymApiStorageError> { + Ok(self.manager.set_gateway_node_id(identity, node_id).await?) + } + + pub(crate) async fn purge_gateway(&self, db_id: i64) -> Result<(), NymApiStorageError> { + Ok(self.manager.purge_gateway(db_id).await?) + } + + pub(crate) async fn make_node_id_not_null(&self) -> Result<(), NymApiStorageError> { + Ok(self.manager.make_node_id_not_null().await?) + } + } +} diff --git a/nym-api/src/support/storage/models.rs b/nym-api/src/support/storage/models.rs index f48f2187f1..1418cab9f8 100644 --- a/nym-api/src/support/storage/models.rs +++ b/nym-api/src/support/storage/models.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: GPL-3.0-only use nym_api_requests::models::TestNode; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; +use sqlx::FromRow; +use time::Date; // Internally used struct to catch results from the database to calculate uptimes for given mixnode/gateway pub(crate) struct NodeStatus { @@ -23,15 +25,15 @@ impl NodeStatus { // Internally used structs to catch results from the database to find active mixnodes pub(crate) struct ActiveMixnode { pub(crate) id: i64, - pub(crate) mix_id: MixId, + pub(crate) mix_id: NodeId, pub(crate) identity_key: String, - pub(crate) owner: String, } +#[derive(FromRow)] pub(crate) struct ActiveGateway { pub(crate) id: i64, + pub(crate) node_id: NodeId, pub(crate) identity: String, - pub(crate) owner: String, } pub(crate) struct TestingRoute { @@ -54,7 +56,6 @@ pub(crate) struct RewardingReport { pub struct MixnodeDetails { pub id: i64, pub mix_id: i64, - pub owner: String, pub identity_key: String, } @@ -67,16 +68,26 @@ impl From for TestNode { } } +#[derive(FromRow)] +pub struct GatewayDetailsBeforeMigration { + pub id: i64, + #[sqlx(default)] + #[allow(dead_code)] + pub node_id: Option, + pub identity: String, +} + +#[derive(FromRow)] pub struct GatewayDetails { pub id: i64, - pub owner: String, + pub node_id: NodeId, pub identity: String, } impl From for TestNode { fn from(value: GatewayDetails) -> Self { TestNode { - node_id: None, + node_id: Some(value.node_id), identity_key: Some(value.identity), } } @@ -111,3 +122,10 @@ pub struct TestedGatewayStatus { pub layer3_mix_id: i64, pub monitor_run_id: i64, } + +#[derive(FromRow)] +pub struct HistoricalUptime { + #[allow(dead_code)] + pub date: Date, + pub uptime: i64, +} diff --git a/nym-api/src/v2/api_docs.rs b/nym-api/src/v2/api_docs.rs deleted file mode 100644 index 9b248bb803..0000000000 --- a/nym-api/src/v2/api_docs.rs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use crate::network::handlers::ContractVersionSchemaResponse; -use nym_api_requests::models; -use utoipa::OpenApi; -use utoipauto::utoipauto; - -// TODO once https://github.com/ProbablyClem/utoipauto/pull/38 is released: -// include ",./nym-api/nym-api-requests/src from nym-api-requests" (and other packages mentioned below) -// for automatic model discovery based on ToSchema / IntoParams implementation. -// Then you can remove `components(schemas)` manual imports below - -#[utoipauto(paths = "./nym-api/src")] -#[derive(OpenApi)] -#[openapi( - info(title = "Nym API"), - tags(), - components(schemas( - models::CirculatingSupplyResponse, - models::CoinSchema, - nym_mixnet_contract_common::Interval, - nym_api_requests::models::GatewayStatusReportResponse, - nym_api_requests::models::GatewayUptimeHistoryResponse, - nym_api_requests::models::HistoricalUptimeResponse, - nym_api_requests::models::GatewayCoreStatusResponse, - nym_api_requests::models::GatewayUptimeResponse, - nym_api_requests::models::RewardEstimationResponse, - nym_api_requests::models::UptimeResponse, - nym_api_requests::models::ComputeRewardEstParam, - nym_api_requests::models::MixNodeBondAnnotated, - nym_api_requests::models::GatewayBondAnnotated, - nym_api_requests::models::MixnodeTestResultResponse, - nym_api_requests::models::StakeSaturationResponse, - nym_api_requests::models::InclusionProbabilityResponse, - nym_api_requests::models::AllInclusionProbabilitiesResponse, - nym_api_requests::models::InclusionProbability, - nym_api_requests::models::SelectionChance, - crate::network::models::NetworkDetails, - nym_config::defaults::NymNetworkDetails, - nym_config::defaults::ChainDetails, - nym_config::defaults::DenomDetailsOwned, - nym_config::defaults::ValidatorDetails, - nym_config::defaults::NymContracts, - ContractVersionSchemaResponse, - crate::network::models::ContractInformation, - nym_api_requests::models::ApiHealthResponse, - nym_api_requests::models::ApiStatus, - nym_bin_common::build_information::BinaryBuildInformationOwned, - nym_api_requests::models::SignerInformationResponse, - nym_api_requests::models::DescribedGateway, - nym_api_requests::models::MixNodeDetailsSchema, - nym_mixnet_contract_common::Gateway, - nym_mixnet_contract_common::GatewayBond, - nym_api_requests::models::NymNodeDescription, - nym_api_requests::models::HostInformation, - nym_api_requests::models::HostKeys, - nym_node_requests::api::v1::node::models::AuxiliaryDetails, - nym_api_requests::models::NetworkRequesterDetails, - nym_api_requests::models::IpPacketRouterDetails, - nym_api_requests::models::AuthenticatorDetails, - nym_api_requests::models::WebSockets, - nym_api_requests::nym_nodes::NodeRole, - nym_api_requests::models::DescribedMixNode, - nym_api_requests::ecash::VerificationKeyResponse, - nym_api_requests::ecash::models::AggregatedExpirationDateSignatureResponse, - nym_api_requests::ecash::models::AggregatedCoinIndicesSignatureResponse, - nym_api_requests::ecash::models::EpochCredentialsResponse, - nym_api_requests::ecash::models::IssuedCredentialResponse, - nym_api_requests::ecash::models::IssuedTicketbookBody, - nym_api_requests::ecash::models::BlindedSignatureResponse, - nym_api_requests::ecash::models::PartialExpirationDateSignatureResponse, - nym_api_requests::ecash::models::PartialCoinIndicesSignatureResponse, - nym_api_requests::ecash::models::EcashTicketVerificationResponse, - nym_api_requests::ecash::models::EcashTicketVerificationRejection, - nym_api_requests::ecash::models::EcashBatchTicketRedemptionResponse, - nym_api_requests::ecash::models::SpentCredentialsResponse, - nym_api_requests::ecash::models::IssuedCredentialsResponse, - nym_api_requests::nym_nodes::SkimmedNode, - nym_api_requests::nym_nodes::BasicEntryInformation, - nym_api_requests::nym_nodes::SemiSkimmedNode, - nym_api_requests::nym_nodes::NodeRoleQueryParam, - )) -)] -pub(super) struct ApiDoc; diff --git a/nym-api/src/v2/mod.rs b/nym-api/src/v2/mod.rs deleted file mode 100644 index 59109d16b7..0000000000 --- a/nym-api/src/v2/mod.rs +++ /dev/null @@ -1,313 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -use super::support::nyxd; -use crate::circulating_supply_api::cache::CirculatingSupplyCache; -use crate::ecash::api_routes::handlers::ecash_routes; -use crate::ecash::client::Client; -use crate::ecash::comm::QueryCommunicationChannel; -use crate::ecash::dkg::controller::keys::{ - can_validate_coconut_keys, load_bte_keypair, load_ecash_keypair_if_exists, -}; -use crate::ecash::dkg::controller::DkgController; -use crate::ecash::state::EcashState; -use crate::epoch_operations::{self, RewardedSetUpdater}; -use crate::network::models::NetworkDetails; -use crate::node_describe_cache::{self, DescribedNodes}; -use crate::node_status_api::handlers::unstable; -use crate::node_status_api::uptime_updater::HistoricalUptimeUpdater; -use crate::node_status_api::{self, NodeStatusCache}; -use crate::nym_contract_cache::cache::NymContractCache; -use crate::status::{ApiStatusState, SignerState}; -use crate::support::caching::cache::SharedCache; -use crate::support::config::Config; -use crate::support::storage; -use crate::{circulating_supply_api, ecash, network_monitor, nym_contract_cache}; -use anyhow::{bail, Context}; -use nym_config::defaults::NymNetworkDetails; -use nym_sphinx::receiver::SphinxMessageReceiver; -use nym_task::TaskManager; -use nym_validator_client::nyxd::Coin; -use router::RouterBuilder; -use std::sync::Arc; -use tokio::task::JoinHandle; -use tokio_util::sync::CancellationToken; - -pub(crate) mod api_docs; -pub(crate) mod router; - -const TASK_MANAGER_TIMEOUT_S: u64 = 10; - -/// Shutdown goes 2 directions: -/// 1. signal background tasks to gracefully finish -/// 2. signal server itself -/// -/// These are done through separate shutdown handles. Ofcourse, shut down server -/// AFTER you have shut down BG tasks (or past their grace period). -pub(crate) struct ShutdownHandles { - task_manager: TaskManager, - axum_shutdown_button: ShutdownAxum, - /// Tokio JoinHandle for axum server's task - axum_join_handle: AxumJoinHandle, -} - -impl ShutdownHandles { - /// Cancellation token is given to Axum server constructor. When the token - /// receives a shutdown signal, Axum server will shut down gracefully. - pub(crate) fn new( - task_manager: TaskManager, - axum_server_handle: AxumJoinHandle, - shutdown_button: CancellationToken, - ) -> Self { - Self { - task_manager, - axum_shutdown_button: ShutdownAxum(shutdown_button.clone()), - axum_join_handle: axum_server_handle, - } - } - - pub(crate) fn task_manager_mut(&mut self) -> &mut TaskManager { - &mut self.task_manager - } - - /// Signal server to shut down, then return join handle to its - /// `tokio` task - /// - /// https://tikv.github.io/doc/tokio/task/struct.JoinHandle.html - #[must_use] - pub(crate) fn shutdown_axum(self) -> AxumJoinHandle { - self.axum_shutdown_button.0.cancel(); - self.axum_join_handle - } -} - -struct ShutdownAxum(CancellationToken); - -type AxumJoinHandle = JoinHandle>; - -#[derive(Clone)] -// TODO rocket remove smurf name after eliminating rocket -pub(crate) struct AxumAppState { - nym_contract_cache: NymContractCache, - node_status_cache: NodeStatusCache, - circulating_supply_cache: CirculatingSupplyCache, - storage: storage::NymApiStorage, - described_nodes_state: SharedCache, - network_details: NetworkDetails, - node_info_cache: unstable::NodeInfoCache, -} - -impl AxumAppState { - pub(crate) fn nym_contract_cache(&self) -> &NymContractCache { - &self.nym_contract_cache - } - - pub(crate) fn node_status_cache(&self) -> &NodeStatusCache { - &self.node_status_cache - } - - pub(crate) fn circulating_supply_cache(&self) -> &CirculatingSupplyCache { - &self.circulating_supply_cache - } - - pub(crate) fn network_details(&self) -> &NetworkDetails { - &self.network_details - } - - pub(crate) fn described_nodes_state(&self) -> &SharedCache { - &self.described_nodes_state - } - - pub(crate) fn storage(&self) -> &storage::NymApiStorage { - &self.storage - } - - pub(crate) fn node_info_cache(&self) -> &unstable::NodeInfoCache { - &self.node_info_cache - } -} - -pub(crate) async fn start_nym_api_tasks_v2(config: &Config) -> anyhow::Result { - let nyxd_client = nyxd::Client::new(config); - let connected_nyxd = config.get_nyxd_url(); - let nym_network_details = NymNetworkDetails::new_from_env(); - let network_details = NetworkDetails::new(connected_nyxd.to_string(), nym_network_details); - - let coconut_keypair = ecash::keys::KeyPair::new(); - - // if the keypair doesnt exist (because say this API is running in the caching mode), nothing will happen - if let Some(loaded_keys) = load_ecash_keypair_if_exists(&config.coconut_signer)? { - let issued_for = loaded_keys.issued_for_epoch; - coconut_keypair.set(loaded_keys).await; - - if can_validate_coconut_keys(&nyxd_client, issued_for).await? { - coconut_keypair.validate() - } - } - - let identity_keypair = config.base.storage_paths.load_identity()?; - let identity_public_key = *identity_keypair.public_key(); - - let router = RouterBuilder::with_default_routes(config.network_monitor.enabled); - - let nym_contract_cache_state = NymContractCache::new(); - let node_status_cache_state = NodeStatusCache::new(); - let mix_denom = network_details.network.chain_details.mix_denom.base.clone(); - let circulating_supply_cache = CirculatingSupplyCache::new(mix_denom.to_owned()); - let described_nodes_state = SharedCache::::new(); - let storage = - storage::NymApiStorage::init(&config.node_status_api.storage_paths.database_path).await?; - let node_info_cache = unstable::NodeInfoCache::default(); - - let mut status_state = ApiStatusState::new(); - - // if coconut signer is enabled, add /coconut to server - let router = if config.coconut_signer.enabled { - // make sure we have some tokens to cover multisig fees - let balance = nyxd_client.balance(&mix_denom).await?; - if balance.amount < ecash::MINIMUM_BALANCE { - let address = nyxd_client.address().await; - let min = Coin::new(ecash::MINIMUM_BALANCE, mix_denom); - bail!("the account ({address}) doesn't have enough funds to cover verification fees. it has {balance} while it needs at least {min}") - } - - let cosmos_address = nyxd_client.address().await.to_string(); - let announce_address = config - .coconut_signer - .announce_address - .clone() - .map(|u| u.to_string()) - .unwrap_or_default(); - status_state.add_zk_nym_signer(SignerState { - cosmos_address, - identity: identity_keypair.public_key().to_base58_string(), - announce_address, - coconut_keypair: coconut_keypair.clone(), - }); - - let ecash_contract = nyxd_client - .get_ecash_contract_address() - .await - .context("e-cash contract address is required to setup the zk-nym signer")?; - - let comm_channel = QueryCommunicationChannel::new(nyxd_client.clone()); - - let ecash_state = EcashState::new( - ecash_contract, - nyxd_client.clone(), - identity_keypair, - coconut_keypair.clone(), - comm_channel, - storage.clone(), - ) - .await?; - - router.nest("/v1/ecash", ecash_routes(Arc::new(ecash_state))) - } else { - router - }; - - let router = router.with_state(AxumAppState { - nym_contract_cache: nym_contract_cache_state.clone(), - node_status_cache: node_status_cache_state.clone(), - circulating_supply_cache: circulating_supply_cache.clone(), - storage: storage.clone(), - described_nodes_state: described_nodes_state.clone(), - network_details, - node_info_cache, - }); - - let task_manager = TaskManager::new(TASK_MANAGER_TIMEOUT_S); - - // start note describe cache refresher - // we should be doing the below, but can't due to our current startup structure - // let refresher = node_describe_cache::new_refresher(&config.topology_cacher); - // let cache = refresher.get_shared_cache(); - node_describe_cache::new_refresher_with_initial_value( - &config.topology_cacher, - nym_contract_cache_state.clone(), - described_nodes_state, - ) - .named("node-self-described-data-refresher") - .start(task_manager.subscribe_named("node-self-described-data-refresher")); - - // start all the caches first - let nym_contract_cache_listener = nym_contract_cache::start_refresher( - &config.node_status_api, - &nym_contract_cache_state, - nyxd_client.clone(), - &task_manager, - ); - node_status_api::start_cache_refresh( - &config.node_status_api, - &nym_contract_cache_state, - &node_status_cache_state, - storage.clone(), - nym_contract_cache_listener, - &task_manager, - ); - circulating_supply_api::start_cache_refresh( - &config.circulating_supply_cacher, - nyxd_client.clone(), - &circulating_supply_cache, - &task_manager, - ); - - // start dkg task - if config.coconut_signer.enabled { - let dkg_bte_keypair = load_bte_keypair(&config.coconut_signer)?; - - DkgController::start( - &config.coconut_signer, - nyxd_client.clone(), - coconut_keypair, - dkg_bte_keypair, - identity_public_key, - rand::rngs::OsRng, - &task_manager, - )?; - } - - // and then only start the uptime updater (and the monitor itself, duh) - // if the monitoring is enabled - if config.network_monitor.enabled { - network_monitor::start::( - &config.network_monitor, - &nym_contract_cache_state, - &storage, - nyxd_client.clone(), - &task_manager, - ) - .await; - - HistoricalUptimeUpdater::start(storage.to_owned(), &task_manager); - - // start 'rewarding' if its enabled - if config.rewarding.enabled { - epoch_operations::ensure_rewarding_permission(&nyxd_client).await?; - RewardedSetUpdater::start( - nyxd_client, - &nym_contract_cache_state, - storage, - &task_manager, - ); - } - } - - let bind_address = config.base.bind_address.to_owned(); - let server = router.build_server(&bind_address).await?; - - let cancellation_token = CancellationToken::new(); - let shutdown_button = cancellation_token.clone(); - let axum_shutdown_receiver = cancellation_token.cancelled_owned(); - let server_handle = tokio::spawn(async move { - { - info!("Started Axum HTTP V2 server on {bind_address}"); - server.run(axum_shutdown_receiver).await - } - }); - - let shutdown = ShutdownHandles::new(task_manager, server_handle, shutdown_button); - - Ok(shutdown) -} diff --git a/nym-api/src/v3_migration.rs b/nym-api/src/v3_migration.rs new file mode 100644 index 0000000000..42401ad6fd --- /dev/null +++ b/nym-api/src/v3_migration.rs @@ -0,0 +1,78 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::support::nyxd::Client; +use crate::support::storage::NymApiStorage; +use anyhow::bail; +use std::collections::HashMap; +use tracing::{debug, info, warn}; + +pub async fn migrate_v3_database( + storage: &NymApiStorage, + nyxd_client: &Client, +) -> anyhow::Result<()> { + if storage.check_v3_migration().await? { + // we have already run the migration + return Ok(()); + } + + info!( + "migrating the database to be compatible with the v3 directory. this might take a while..." + ); + + // get the ids of all the gateways + let preassigned_ids = nyxd_client + .get_gateway_ids() + .await? + .into_iter() + .map(|id| (id.identity, id.node_id)) + .collect::>(); + let contract_gateways = nyxd_client.get_gateways().await?; + let nym_nodes = nyxd_client.get_nymnodes().await?; + + // assign node_id to every gateway + let all_known = storage.get_all_known_gateways().await?; + for gateway in all_known { + let identity = &gateway.identity; + debug!("migrating gateway {identity}"); + if let Some(assigned) = preassigned_ids.get(identity) { + storage + .set_gateway_node_id(&gateway.identity, *assigned) + .await?; + continue; + }; + + // no pre-assigned id, perhaps the operator has already migrated into a nym-node? + if let Some(nym_node) = nym_nodes + .iter() + .find(|n| &n.bond_information.node.identity_key == identity) + { + storage + .set_gateway_node_id(identity, nym_node.node_id()) + .await?; + continue; + } + + // check if that gateway is even still bonded + let bonded = contract_gateways + .iter() + .any(|g| &g.gateway.identity_key == identity); + + if !bonded { + warn!("could not migrate gateway {identity}, as it does not appear to be bonded. all of its data is going to get purged."); + storage.purge_gateway(gateway.id).await?; + } else { + // this is critical issue because it should have never happened + warn!("could not migrate gateway {identity} even though it's still bonded. something bad has happened!"); + bail!("could not migrate gateway {identity}") + } + } + + debug!("making the column not nullable"); + storage.make_node_id_not_null().await?; + + debug!("marking v3 migration as complete"); + storage.set_v3_migration_completion().await?; + + Ok(()) +} diff --git a/nym-credential-proxy/Cargo.lock b/nym-credential-proxy/Cargo.lock new file mode 100644 index 0000000000..50e39249dc --- /dev/null +++ b/nym-credential-proxy/Cargo.lock @@ -0,0 +1,5417 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "getrandom", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" + +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "async-trait" +version = "0.1.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "atoi" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "axum" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-client-ip" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eefda7e2b27e1bda4d6fa8a06b50803b8793769045918bc37ad062d48a6efac" +dependencies = [ + "axum", + "forwarded-header-value", + "serde", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.1", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bip32" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa13fae8b6255872fd86f7faf4b41168661d7d78609f7bfe6771b85c6739a15b" +dependencies = [ + "bs58", + "hmac", + "k256", + "rand_core 0.6.4", + "ripemd", + "sha2 0.10.8", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "bip39" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33415e24172c1b7d6066f6d999545375ab8e1d95421d6784bdfff9496f292387" +dependencies = [ + "bitcoin_hashes", + "rand", + "rand_core 0.6.4", + "serde", + "unicode-normalization", + "zeroize", +] + +[[package]] +name = "bitcoin-internals" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9425c3bf7089c983facbae04de54513cce73b41c7f9ff8c845b54e7bc64ebbfb" + +[[package]] +name = "bitcoin_hashes" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1930a4dabfebb8d7d9992db18ebe3ae2876f0a305fab206fd168df931ede293b" +dependencies = [ + "bitcoin-internals", + "hex-conservative", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +dependencies = [ + "serde", +] + +[[package]] +name = "blake2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330" +dependencies = [ + "byte-tools", + "crypto-mac", + "digest 0.8.1", + "opaque-debug 0.2.3", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "bls12_381" +version = "0.8.0" +source = "git+https://github.com/jstuczyn/bls12_381?branch=temp/experimental-serdect#22cd0a16b674af1629110a2dc8b6cf6c73ea4cd9" +dependencies = [ + "digest 0.9.0", + "ff", + "group", + "pairing", + "rand_core 0.6.4", + "serde", + "serdect 0.3.0-rc.0", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "bnum" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab9008b6bb9fc80b5277f2fe481c09e828743d9151203e804583eb4c9e15b31d" + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "sha2 0.10.8", + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" +dependencies = [ + "serde", +] + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cc" +version = "1.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e80e3b6a3ab07840e1cae9b0666a63970dc28e8ed5ffbcdacbfc760c281bfc1" +dependencies = [ + "shlex", +] + +[[package]] +name = "celes" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39b9a21273925d7cc9e8a9a5f068122341336813c607014f5ef64f82b6acba58" +dependencies = [ + "serde", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chacha" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddf3c081b5fba1e5615640aae998e0fbd10c24cbd897ee39ed754a77601a4862" +dependencies = [ + "byteorder", + "keystream", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clap" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.5.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-str" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3618cccc083bb987a415d85c02ca6c9994ea5b44731ec28b9ecf09658655fba9" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cosmos-sdk-proto" +version = "0.22.0-pre" +source = "git+https://github.com/cosmos/cosmos-rust?rev=4b1332e6d8258ac845cef71589c8d362a669675a#4b1332e6d8258ac845cef71589c8d362a669675a" +dependencies = [ + "prost", + "prost-types", + "tendermint-proto", +] + +[[package]] +name = "cosmrs" +version = "0.17.0-pre" +source = "git+https://github.com/cosmos/cosmos-rust?rev=4b1332e6d8258ac845cef71589c8d362a669675a#4b1332e6d8258ac845cef71589c8d362a669675a" +dependencies = [ + "bip32", + "cosmos-sdk-proto", + "ecdsa", + "eyre", + "k256", + "rand_core 0.6.4", + "serde", + "serde_json", + "signature", + "subtle-encoding", + "tendermint", + "tendermint-rpc", + "thiserror", +] + +[[package]] +name = "cosmwasm-crypto" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6aa9f904de106fa16443ad14ec2abe75e94ba003bb61c681c0e43d4c58d2a" +dependencies = [ + "digest 0.10.7", + "ecdsa", + "ed25519-zebra", + "k256", + "rand_core 0.6.4", + "thiserror", +] + +[[package]] +name = "cosmwasm-derive" +version = "1.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8e07de16c800ac82fd188d055ecdb923ead0cf33960d3350089260bb982c09f" +dependencies = [ + "syn 1.0.109", +] + +[[package]] +name = "cosmwasm-schema" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ae2e971fb831d0c4fa3c8c3d2291cdbdd73786a73d65196dbf983d9b2468af" +dependencies = [ + "cosmwasm-schema-derive", + "schemars", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cosmwasm-schema-derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cadc57fd0825b85bc2f9b972c17da718b9efb4bc17e5935cc2d6036324f853d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "cosmwasm-std" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e98e19fae6c3f468412f731274b0f9434602722009d6a77432d39c7c4bb09202" +dependencies = [ + "base64 0.21.7", + "bnum", + "cosmwasm-crypto", + "cosmwasm-derive", + "derivative", + "forward_ref", + "hex", + "schemars", + "serde", + "serde-json-wasm", + "sha2 0.10.8", + "thiserror", +] + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.7", + "rand_core 0.6.4", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array 0.14.7", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" +dependencies = [ + "generic-array 0.12.4", + "subtle 1.0.0", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "serde", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", + "zeroize", +] + +[[package]] +name = "cw-controllers" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5d8edce4b78785f36413f67387e4be7d0cb7d032b5d4164bcc024f9c3f3f2ea" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw-storage-plus" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5ff29294ee99373e2cd5fd21786a3c0ced99a52fec2ca347d565489c61b723c" +dependencies = [ + "cosmwasm-std", + "schemars", + "serde", +] + +[[package]] +name = "cw-utils" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c80e93d1deccb8588db03945016a292c3c631e6325d349ebb35d2db6f4f946f7" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw2", + "schemars", + "semver", + "serde", + "thiserror", +] + +[[package]] +name = "cw2" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c120b24fbbf5c3bedebb97f2cc85fbfa1c3287e09223428e7e597b5293c1fa" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "semver", + "serde", + "thiserror", +] + +[[package]] +name = "cw20" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "526e39bb20534e25a1cd0386727f0038f4da294e5e535729ba3ef54055246abd" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "schemars", + "serde", +] + +[[package]] +name = "cw3" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2967fbd073d4b626dd9e7148e05a84a3bebd9794e71342e12351110ffbb12395" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "cw20", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw4" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24754ff6e45f2a1c60adc409d9b2eb87666012c44021329141ffaab3388fccd2" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common", + "subtle 2.6.1", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dotenvy" +version = "0.15.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" + +[[package]] +name = "dyn-clone" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "serdect 0.2.0", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "serde", + "signature", +] + +[[package]] +name = "ed25519-consensus" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +dependencies = [ + "curve25519-dalek-ng", + "hex", + "rand_core 0.6.4", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek 4.1.3", + "ed25519", + "rand_core 0.6.4", + "serde", + "sha2 0.10.8", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "ed25519-zebra" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" +dependencies = [ + "curve25519-dalek 3.2.0", + "hashbrown 0.12.3", + "hex", + "rand_core 0.6.4", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" +dependencies = [ + "serde", +] + +[[package]] +name = "elliptic-curve" +version = "0.13.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9775b22bc152ad86a0cf23f0f348b884b26add12bf741e7ffc4d4ab2ab4d205" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array 0.14.7", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "serdect 0.2.0", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" +dependencies = [ + "atty", + "humantime 1.3.0", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "etcetera" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.48.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core 0.6.4", + "subtle 2.6.1", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "flate2" +version = "1.0.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "flex-error" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c606d892c9de11507fa0dcffc116434f94e105d0bbdc4e405b61519464c49d7b" +dependencies = [ + "eyre", + "paste", +] + +[[package]] +name = "flume" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +dependencies = [ + "futures-core", + "futures-sink", + "spin", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "forward_ref" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8cbd1169bd7b4a0a20d92b9af7a7e0422888bd38a6f5ec29c1fd8c1558a272e" + +[[package]] +name = "forwarded-header-value" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" +dependencies = [ + "nonempty", + "thiserror", +] + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getset" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "gloo-utils" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037fcb07216cb3a30f7292bd0176b050b7b9a052ba830ef7d5d65f6dc64ba58e" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle 2.6.1", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.6.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.6.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "handlebars" +version = "3.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4498fc115fa7d34de968184e473529abb40eeb6be8bc5f7faba3d08c316cb3e3" +dependencies = [ + "log", + "pest", + "pest_derive", + "quick-error 2.0.1", + "serde", + "serde_json", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.11", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-conservative" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" +dependencies = [ + "quick-error 1.2.3", +] + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "humantime-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" +dependencies = [ + "humantime 2.1.0", + "serde", +] + +[[package]] +name = "hyper" +version = "0.14.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.30", + "rustls 0.21.12", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.4.1", + "hyper-util", + "rustls 0.22.4", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.25.0", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.4.1", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" +dependencies = [ + "equivalent", + "hashbrown 0.15.0", + "serde", +] + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "ipnet" +version = "2.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "js-sys" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cb94a0ffd3f3ee755c20f7d8752f45cac88605a4dcf808abcff72873296ec7b" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f01b677d82ef7a676aa37e099defd83a28e15687112cafdd112d60236b6115b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2 0.10.8", + "signature", +] + +[[package]] +name = "keystream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33070833c9ee02266356de0c43f723152bd38bd96ddf52c82b3af10c9138b28" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.6.0", + "libc", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4e226dcd58b4be396f7bd3c20da8fdee2911400705297ba7d2d7cc2c30f716" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "lioness" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae926706ba42c425c9457121178330d75e273df2e82e28b758faf3de3a9acb9" +dependencies = [ + "arrayref", + "blake2", + "chacha", + "keystream", +] + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nonempty" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e613fc340b2220f734a8595782c551f1250e969d87d3be1ae0579e8d4065179" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af1844ef2428cc3e1cb900be36181049ef3d3193c63e43026cfe202983b27a56" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "nym-api-requests" +version = "0.1.0" +dependencies = [ + "bs58", + "cosmrs", + "cosmwasm-std", + "ecdsa", + "getset", + "nym-compact-ecash", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-time", + "nym-mixnet-contract-common", + "nym-network-defaults", + "nym-node-requests", + "nym-serde-helpers", + "schemars", + "serde", + "serde_json", + "sha2 0.10.8", + "tendermint", + "thiserror", + "time", + "utoipa", +] + +[[package]] +name = "nym-bin-common" +version = "0.6.0" +dependencies = [ + "const-str", + "log", + "pretty_env_logger", + "schemars", + "semver", + "serde", + "tracing-subscriber", + "utoipa", + "vergen", +] + +[[package]] +name = "nym-coconut-bandwidth-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "nym-multisig-contract-common", +] + +[[package]] +name = "nym-coconut-dkg-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-utils", + "cw2", + "cw4", + "nym-contracts-common", + "nym-multisig-contract-common", +] + +[[package]] +name = "nym-compact-ecash" +version = "0.1.0" +dependencies = [ + "bincode", + "bls12_381", + "bs58", + "cfg-if", + "digest 0.9.0", + "ff", + "group", + "itertools 0.13.0", + "nym-network-defaults", + "nym-pemstore", + "rand", + "serde", + "sha2 0.9.9", + "subtle 2.6.1", + "thiserror", + "zeroize", +] + +[[package]] +name = "nym-config" +version = "0.1.0" +dependencies = [ + "dirs", + "handlebars", + "log", + "nym-network-defaults", + "serde", + "toml", + "url", +] + +[[package]] +name = "nym-contracts-common" +version = "0.5.0" +dependencies = [ + "bs58", + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", + "thiserror", + "vergen", +] + +[[package]] +name = "nym-credential-proxy" +version = "0.1.3" +dependencies = [ + "anyhow", + "async-trait", + "axum", + "bip39", + "bs58", + "cfg-if", + "clap", + "colored", + "dotenv", + "futures", + "humantime 2.1.0", + "nym-bin-common", + "nym-compact-ecash", + "nym-config", + "nym-credential-proxy-requests", + "nym-credentials", + "nym-credentials-interface", + "nym-crypto", + "nym-http-api-common", + "nym-network-defaults", + "nym-validator-client", + "rand", + "reqwest 0.12.4", + "serde", + "serde_json", + "sqlx", + "strum", + "strum_macros", + "tempfile", + "thiserror", + "time", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing", + "url", + "utoipa", + "utoipa-swagger-ui", + "uuid", + "zeroize", +] + +[[package]] +name = "nym-credential-proxy-requests" +version = "0.1.0" +dependencies = [ + "async-trait", + "nym-credentials", + "nym-credentials-interface", + "nym-http-api-client", + "nym-http-api-common", + "nym-serde-helpers", + "reqwest 0.12.4", + "schemars", + "serde", + "serde_json", + "time", + "tsify", + "utoipa", + "uuid", + "wasm-bindgen", + "wasmtimer", +] + +[[package]] +name = "nym-credentials" +version = "0.1.0" +dependencies = [ + "bincode", + "bls12_381", + "cosmrs", + "log", + "nym-api-requests", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-contract-common", + "nym-ecash-time", + "nym-network-defaults", + "nym-serde-helpers", + "nym-validator-client", + "serde", + "thiserror", + "time", + "zeroize", +] + +[[package]] +name = "nym-credentials-interface" +version = "0.1.0" +dependencies = [ + "bls12_381", + "nym-compact-ecash", + "nym-ecash-time", + "nym-network-defaults", + "rand", + "serde", + "strum", + "thiserror", + "time", +] + +[[package]] +name = "nym-crypto" +version = "0.4.0" +dependencies = [ + "bs58", + "ed25519-dalek", + "nym-pemstore", + "nym-sphinx-types", + "rand", + "serde", + "serde_bytes", + "subtle-encoding", + "thiserror", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "nym-ecash-contract-common" +version = "0.1.0" +dependencies = [ + "bs58", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "nym-multisig-contract-common", + "thiserror", +] + +[[package]] +name = "nym-ecash-time" +version = "0.1.0" +dependencies = [ + "nym-compact-ecash", + "time", +] + +[[package]] +name = "nym-exit-policy" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", + "thiserror", + "tracing", + "utoipa", +] + +[[package]] +name = "nym-group-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cw-controllers", + "cw4", + "schemars", + "serde", +] + +[[package]] +name = "nym-http-api-client" +version = "0.1.0" +dependencies = [ + "async-trait", + "http 1.1.0", + "nym-bin-common", + "reqwest 0.12.4", + "serde", + "serde_json", + "thiserror", + "tracing", + "url", + "wasmtimer", +] + +[[package]] +name = "nym-http-api-common" +version = "0.1.0" +dependencies = [ + "axum", + "axum-client-ip", + "bytes", + "colored", + "mime", + "serde", + "serde_json", + "serde_yaml", + "tracing", + "utoipa", +] + +[[package]] +name = "nym-mixnet-contract-common" +version = "0.6.0" +dependencies = [ + "bs58", + "cosmwasm-schema", + "cosmwasm-std", + "cw-controllers", + "cw-storage-plus", + "humantime-serde", + "log", + "nym-contracts-common", + "schemars", + "serde", + "serde-json-wasm", + "serde_repr", + "thiserror", + "time", +] + +[[package]] +name = "nym-multisig-contract-common" +version = "0.1.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "cw3", + "cw4", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "nym-network-defaults" +version = "0.1.0" +dependencies = [ + "dotenvy", + "log", + "schemars", + "serde", + "url", + "utoipa", +] + +[[package]] +name = "nym-node-requests" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "celes", + "humantime 2.1.0", + "humantime-serde", + "nym-bin-common", + "nym-crypto", + "nym-exit-policy", + "nym-wireguard-types", + "schemars", + "serde", + "serde_json", + "thiserror", + "time", + "utoipa", +] + +[[package]] +name = "nym-pemstore" +version = "0.3.0" +dependencies = [ + "pem", +] + +[[package]] +name = "nym-serde-helpers" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "bs58", + "hex", + "serde", + "time", +] + +[[package]] +name = "nym-sphinx-types" +version = "0.2.0" +dependencies = [ + "sphinx-packet", + "thiserror", +] + +[[package]] +name = "nym-validator-client" +version = "0.1.0" +dependencies = [ + "async-trait", + "base64 0.22.1", + "bip32", + "bip39", + "colored", + "cosmrs", + "cosmwasm-std", + "cw-controllers", + "cw-utils", + "cw2", + "cw3", + "cw4", + "eyre", + "flate2", + "futures", + "itertools 0.13.0", + "nym-api-requests", + "nym-coconut-bandwidth-contract-common", + "nym-coconut-dkg-common", + "nym-compact-ecash", + "nym-config", + "nym-contracts-common", + "nym-ecash-contract-common", + "nym-group-contract-common", + "nym-http-api-client", + "nym-mixnet-contract-common", + "nym-multisig-contract-common", + "nym-network-defaults", + "nym-serde-helpers", + "nym-vesting-contract-common", + "prost", + "reqwest 0.12.4", + "serde", + "serde_json", + "sha2 0.9.9", + "tendermint-rpc", + "thiserror", + "time", + "tokio", + "tracing", + "url", + "wasmtimer", + "zeroize", +] + +[[package]] +name = "nym-vesting-contract-common" +version = "0.7.0" +dependencies = [ + "cosmwasm-schema", + "cosmwasm-std", + "nym-contracts-common", + "nym-mixnet-contract-common", + "serde", + "thiserror", +] + +[[package]] +name = "nym-vpn-api-lib-wasm" +version = "0.1.0" +dependencies = [ + "bs58", + "getrandom", + "js-sys", + "nym-bin-common", + "nym-compact-ecash", + "nym-credential-proxy-requests", + "nym-credentials", + "nym-credentials-interface", + "nym-crypto", + "nym-ecash-time", + "serde", + "serde-wasm-bindgen 0.6.5", + "serde_json", + "thiserror", + "time", + "tsify", + "wasm-bindgen", + "wasm-utils", + "zeroize", +] + +[[package]] +name = "nym-wireguard-types" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "log", + "nym-config", + "nym-network-defaults", + "serde", + "thiserror", + "x25519-dalek", +] + +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group", +] + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "peg" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "295283b02df346d1ef66052a757869b2876ac29a6bb0ac3f5f7cd44aebe40e8f" +dependencies = [ + "peg-macros", + "peg-runtime", +] + +[[package]] +name = "peg-macros" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdad6a1d9cf116a059582ce415d5f5566aabcd4008646779dab7fdc2a9a9d426" +dependencies = [ + "peg-runtime", + "proc-macro2", + "quote", +] + +[[package]] +name = "peg-runtime" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3aeb8f54c078314c2065ee649a7241f46b9d8e418e1a9581ba0546657d7aa3a" + +[[package]] +name = "pem" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" +dependencies = [ + "base64 0.13.1", + "once_cell", + "regex", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdbef9d1d47087a895abd220ed25eb4ad973a5e26f6a4367b038c25e28dfc2d9" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d3a6e3394ec80feb3b6393c725571754c6188490265c61aaf260810d6b95aa0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94429506bde1ca69d1b5601962c73f4172ab4726571a59ea95931218cb0e930e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "pest_meta" +version = "2.7.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac8a071862e93690b6e34e9a5fb8e33ff3734473ac0245b27232222c4906a33f" +dependencies = [ + "once_cell", + "pest", + "sha2 0.10.8", +] + +[[package]] +name = "pin-project" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_env_logger" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" +dependencies = [ + "env_logger", + "log", +] + +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "proc-macro2" +version = "1.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prost" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deb1435c188b76130da55f17a466d252ff7b1418b2ad3e037d127b94e3411f29" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1" +dependencies = [ + "anyhow", + "itertools 0.12.1", + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "prost-types" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9091c90b0a32608e984ff2fa4091273cbdd755d54935c51d520887f4a1dbd5b0" +dependencies = [ + "prost", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_distr" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" +dependencies = [ + "num-traits", + "rand", +] + +[[package]] +name = "redox_syscall" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.30", + "hyper-rustls 0.24.2", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-native-certs", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg 0.50.0", +] + +[[package]] +name = "reqwest" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "566cafdd92868e0939d3fb961bd0dc25fcfaaed179291093b3d43e6b3150ea10" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-rustls 0.26.0", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.22.4", + "rustls-pemfile 2.2.0", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.25.0", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.26.6", + "winreg 0.52.0", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle 2.6.1", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "rsa" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e5124fcb30e76a7e79bfee683a2746db83784b86289f6251b54b7950a0dfc" +dependencies = [ + "const-oid", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "signature", + "spki", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "rust-embed" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa66af4a4fdd5e7ebc276f115e895611a34739a9c1c01028383d612d550953c0" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6125dbc8867951125eec87294137f4e9c2c96566e61bf72c45095a7c77761478" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.79", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e5347777e9aacb56039b0e1f28785929a8a3b709e87482e7442c72e7c12529d" +dependencies = [ + "sha2 0.10.8", + "walkdir", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "schemars" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09c024468a378b7e36765cd36702b7a90cc3cba11654f6685c8f233408e89e92" +dependencies = [ + "dyn-clone", + "indexmap 1.9.3", + "schemars_derive", + "serde", + "serde_json", + "uuid", +] + +[[package]] +name = "schemars_derive" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1eee588578aff73f856ab961cd2f79e36bc45d7ded33a7562adba4667aecc0e" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals 0.29.1", + "syn 2.0.79", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array 0.14.7", + "pkcs8", + "serdect 0.2.0", + "subtle 2.6.1", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-json-wasm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15bee9b04dd165c3f4e142628982ddde884c2022a89e8ddf99c4829bf2c3a58" +dependencies = [ + "serde", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3b143e2833c57ab9ad3ea280d21fd34e285a42837aeb0ee301f4f41890fa00e" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "serde_derive_internals" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e578a843d40b4189a4d66bba51d7684f57da5bd7c304c64e14bd63efbef49509" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "serde_derive_internals" +version = "0.29.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "serde_json" +version = "1.0.132" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "serde_spanned" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.34+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" +dependencies = [ + "indexmap 2.6.0", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "serdect" +version = "0.3.0-rc.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a504c8ee181e3e594d84052f983d60afe023f4d94d050900be18062bbbf7b58" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug 0.3.1", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "sphinx-packet" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dabeca95bf5fd0563d6be7ebcb1c6a9fcb135746a0ba9050c47dc68c8607e595" +dependencies = [ + "aes", + "arrayref", + "blake2", + "bs58", + "byteorder", + "chacha", + "ctr", + "curve25519-dalek 4.1.3", + "digest 0.10.7", + "hkdf", + "hmac", + "lioness", + "log", + "rand", + "rand_distr", + "sha2 0.10.8", + "subtle 2.6.1", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +dependencies = [ + "lock_api", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "sqlformat" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" +dependencies = [ + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa" +dependencies = [ + "sqlx-core", + "sqlx-macros", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", +] + +[[package]] +name = "sqlx-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" +dependencies = [ + "ahash 0.8.11", + "atoi", + "byteorder", + "bytes", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "futures-channel", + "futures-core", + "futures-intrusive", + "futures-io", + "futures-util", + "hashlink", + "hex", + "indexmap 2.6.0", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "sha2 0.10.8", + "smallvec", + "sqlformat", + "thiserror", + "time", + "tokio", + "tokio-stream", + "tracing", + "url", + "webpki-roots 0.25.4", +] + +[[package]] +name = "sqlx-macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127" +dependencies = [ + "proc-macro2", + "quote", + "sqlx-core", + "sqlx-macros-core", + "syn 1.0.109", +] + +[[package]] +name = "sqlx-macros-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8" +dependencies = [ + "dotenvy", + "either", + "heck 0.4.1", + "hex", + "once_cell", + "proc-macro2", + "quote", + "serde", + "serde_json", + "sha2 0.10.8", + "sqlx-core", + "sqlx-mysql", + "sqlx-postgres", + "sqlx-sqlite", + "syn 1.0.109", + "tempfile", + "tokio", + "url", +] + +[[package]] +name = "sqlx-mysql" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.6.0", + "byteorder", + "bytes", + "crc", + "digest 0.10.7", + "dotenvy", + "either", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "generic-array 0.14.7", + "hex", + "hkdf", + "hmac", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "percent-encoding", + "rand", + "rsa", + "serde", + "sha1", + "sha2 0.10.8", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-postgres" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" +dependencies = [ + "atoi", + "base64 0.21.7", + "bitflags 2.6.0", + "byteorder", + "crc", + "dotenvy", + "etcetera", + "futures-channel", + "futures-core", + "futures-io", + "futures-util", + "hex", + "hkdf", + "hmac", + "home", + "itoa", + "log", + "md-5", + "memchr", + "once_cell", + "rand", + "serde", + "serde_json", + "sha2 0.10.8", + "smallvec", + "sqlx-core", + "stringprep", + "thiserror", + "time", + "tracing", + "whoami", +] + +[[package]] +name = "sqlx-sqlite" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa" +dependencies = [ + "atoi", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "libsqlite3-sys", + "log", + "percent-encoding", + "serde", + "sqlx-core", + "time", + "tracing", + "url", + "urlencoding", +] + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.79", +] + +[[package]] +name = "subtle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tempfile" +version = "3.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" +dependencies = [ + "cfg-if", + "fastrand", + "once_cell", + "rustix", + "windows-sys 0.59.0", +] + +[[package]] +name = "tendermint" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "954496fbc9716eb4446cdd6d00c071a3e2f22578d62aa03b40c7e5b4fda3ed42" +dependencies = [ + "bytes", + "digest 0.10.7", + "ed25519", + "ed25519-consensus", + "flex-error", + "futures", + "k256", + "num-traits", + "once_cell", + "prost", + "prost-types", + "ripemd", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.10.8", + "signature", + "subtle 2.6.1", + "subtle-encoding", + "tendermint-proto", + "time", + "zeroize", +] + +[[package]] +name = "tendermint-config" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84b11b57d20ee4492a1452faff85f5c520adc36ca9fe5e701066935255bb89f" +dependencies = [ + "flex-error", + "serde", + "serde_json", + "tendermint", + "toml", + "url", +] + +[[package]] +name = "tendermint-proto" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc87024548c7f3da479885201e3da20ef29e85a3b13d04606b380ac4c7120d87" +dependencies = [ + "bytes", + "flex-error", + "prost", + "prost-types", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + +[[package]] +name = "tendermint-rpc" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfdc2281e271277fda184d96d874a6fe59f569b130b634289257baacfc95aa85" +dependencies = [ + "async-trait", + "bytes", + "flex-error", + "futures", + "getrandom", + "peg", + "pin-project", + "rand", + "reqwest 0.11.27", + "semver", + "serde", + "serde_bytes", + "serde_json", + "subtle 2.6.1", + "subtle-encoding", + "tendermint", + "tendermint-config", + "tendermint-proto", + "thiserror", + "time", + "tokio", + "tracing", + "url", + "uuid", + "walkdir", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "js-sys", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls 0.22.4", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "hashbrown 0.14.5", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" +dependencies = [ + "indexmap 2.6.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tsify" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b26cf145f2f3b9ff84e182c448eaf05468e247f148cf3d2a7d67d78ff023a0" +dependencies = [ + "gloo-utils 0.1.7", + "serde", + "serde-wasm-bindgen 0.5.0", + "serde_json", + "tsify-macros", + "wasm-bindgen", +] + +[[package]] +name = "tsify-macros" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a94b0f0954b3e59bfc2c246b4c8574390d94a4ad4ad246aaf2fb07d7dfd3b47" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals 0.28.0", + "syn 2.0.79", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "utoipa" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5afb1a60e207dca502682537fefcfd9921e71d0b83e9576060f09abc6efab23" +dependencies = [ + "indexmap 2.6.0", + "serde", + "serde_json", + "utoipa-gen", +] + +[[package]] +name = "utoipa-gen" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c24e8ab68ff9ee746aad22d39b5535601e6416d1b0feeabf78be986a5c4392" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 2.0.79", + "uuid", +] + +[[package]] +name = "utoipa-swagger-ui" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "943e0ff606c6d57d410fd5663a4d7c074ab2c5f14ab903b9514565e59fa1189e" +dependencies = [ + "axum", + "mime_guess", + "regex", + "reqwest 0.12.4", + "rust-embed", + "serde", + "serde_json", + "url", + "utoipa", + "zip", +] + +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "serde", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vergen" +version = "8.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e27d6bdd219887a9eadd19e1c34f32e47fa332301184935c6d9bca26f3cca525" +dependencies = [ + "anyhow", + "cargo_metadata", + "cfg-if", + "regex", + "rustc_version", + "rustversion", + "time", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef073ced962d62984fb38a36e5fdc1a2b23c9e0e1fa0689bb97afa4202ef6887" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4bfab14ef75323f4eb75fa52ee0a3fb59611977fd3240da19b2cf36ff85030e" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.79", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65471f79c1022ffa5291d33520cbbb53b7687b01c2f8e83b57d102eed7ed479d" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7bec9830f60924d9ceb3ef99d55c155be8afa76954edffbb5936ff4509474e7" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c74f6e152a76a2ad448e223b0fc0b6b5747649c3d769cc6bf45737bf97d0ed6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a42f6c679374623f295a8623adfe63d9284091245c3504bde47c17a3ce2777d9" + +[[package]] +name = "wasm-utils" +version = "0.1.0" +dependencies = [ + "futures", + "gloo-utils 0.2.0", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "wasmtimer" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f656cd8858a5164932d8a90f936700860976ec21eb00e0fe2aa8cab13f6b4cf" +dependencies = [ + "futures", + "js-sys", + "parking_lot", + "pin-utils", + "slab", + "wasm-bindgen", +] + +[[package]] +name = "web-sys" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44188d185b5bdcae1052d08bcbcf9091a5524038d4572cc4f4f2bb9d5554ddd9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "webpki-roots" +version = "0.26.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841c67bff177718f1d4dfefde8d8f0e78f9b6589319ba88312f567fc5841a958" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall", + "wasite", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek 4.1.3", + "rand_core 0.6.4", + "serde", + "zeroize", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", +] + +[[package]] +name = "zip" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cc23c04387f4da0374be4533ad1208cbb091d5c11d070dfef13676ad6497164" +dependencies = [ + "arbitrary", + "crc32fast", + "crossbeam-utils", + "displaydoc", + "flate2", + "indexmap 2.6.0", + "num_enum", + "thiserror", +] diff --git a/nym-credential-proxy/Cargo.toml b/nym-credential-proxy/Cargo.toml new file mode 100644 index 0000000000..07e82599d0 --- /dev/null +++ b/nym-credential-proxy/Cargo.toml @@ -0,0 +1,62 @@ +[profile.release] +panic = "abort" +opt-level = "s" +overflow-checks = true + +[profile.dev] +panic = "abort" + +[workspace] + +resolver = "2" +members = [ + "nym-credential-proxy", + "nym-credential-proxy-requests", + "vpn-api-lib-wasm" +] + +[workspace.package] +authors = ["Nym Technologies SA"] +repository = "https://github.com/nymtech/nym" +homepage = "https://nymtech.net" +documentation = "https://nymtech.net" +edition = "2021" +license = "GPL-3.0" + +[workspace.dependencies] +async-trait = "0.1.80" +axum = "0.7.5" +anyhow = "1.0.71" +bip39 = "2.0.0" +bs58 = "0.5.1" +colored = "2.1.0" +cfg-if = "1.0.0" +clap = "4.5.4" +dotenv = "0.15.0" +futures = "0.3.30" +humantime = "2.1.0" +thiserror = "1.0.59" +rand = "0.8.5" +reqwest = { version = "0.12.4", default-features = false } +schemars = "0.8.17" +strum = "0.26.3" +strum_macros = "0.26.4" +serde = "1.0.200" +serde_json = "1.0.117" +sqlx = "0.7.4" +tempfile = "3.12.0" +time = "0.3.36" +tracing = "0.1.40" +tsify = "0.4.5" +tokio = "1.37.0" +tokio-util = "0.7.10" +tower = "0.5.0" +tower-http = "0.5.2" +uuid = "1.8.0" +url = "2.5.2" +utoipa = "4.2.0" +utoipa-swagger-ui = "7.0.1" +zeroize = "1.6.0" + +wasm-bindgen = "0.2.93" +wasmtimer = "0.2.0" diff --git a/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml new file mode 100644 index 0000000000..86e281eb8a --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy-requests/Cargo.toml @@ -0,0 +1,42 @@ +[package] +name = "nym-credential-proxy-requests" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-trait = { workspace = true } +schemars = { workspace = true, features = ["preserve_order", "uuid1"] } +uuid = { workspace = true, features = ["serde"] } +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +time = { workspace = true, features = ["serde", "formatting", "parsing"] } +tsify = { workspace = true, optional = true } +reqwest = { workspace = true, features = ["json", "rustls-tls"] } +wasm-bindgen = { workspace = true, optional = true } + +## openapi: +utoipa = { workspace = true, optional = true, features = ["uuid"] } + +nym-credentials = { path = "../../common/credentials" } +nym-credentials-interface = { path = "../../common/credentials-interface" } +nym-http-api-common = { path = "../../common/http-api-common", optional = true } +nym-http-api-client = { path = "../../common/http-api-client" } +nym-serde-helpers = { path = "../../common/serde-helpers", features = ["bs58"] } + +[target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer] +workspace = true +features = ["tokio"] + + +[features] +default = ["query-types"] +query-types = ["nym-http-api-common"] +openapi = ["utoipa"] +tsify = ["dep:tsify", "wasm-bindgen"] diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/api/mod.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/api/mod.rs new file mode 100644 index 0000000000..60df60aa1f --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/api/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod v1; diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/mod.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/mod.rs new file mode 100644 index 0000000000..2e131231cd --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +// pub mod bandwidth_voucher; +// pub mod freepass; +pub mod ticketbook; + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct ErrorResponse { + #[cfg_attr(feature = "openapi",schema(value_type = Option, example = "c48f9ce3-a1e9-4886-8000-13f290f34501"))] + pub uuid: Option, + pub message: String, +} diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/mod.rs new file mode 100644 index 0000000000..363be75767 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod models; diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs new file mode 100644 index 0000000000..1d88b28d00 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/api/v1/ticketbook/models.rs @@ -0,0 +1,290 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_credentials::ecash::bandwidth::serialiser::signatures::{ + AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, +}; +use nym_credentials_interface::{PublicKeyUser, TicketType, WithdrawalRequest}; +use schemars::gen::SchemaGenerator; +use schemars::schema::Schema; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::ops::{Deref, DerefMut}; +use time::{Date, OffsetDateTime}; + +#[cfg(feature = "query-types")] +use nym_http_api_common::Output; + +#[cfg(feature = "tsify")] +use tsify::Tsify; +use uuid::Uuid; + +#[cfg(feature = "tsify")] +use wasm_bindgen::prelude::wasm_bindgen; + +#[derive(JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct PlaceholderJsonSchemaImpl {} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct TicketbookRequest { + /// base58 encoded withdrawal request + pub withdrawal_request: WithdrawalRequestBs58Wrapper, + + /// bs58-encoded **ECASH** public key. + /// this is **NOT** a device key or anything like that. + /// it is derived from user's **SECRET** key! + /// + /// you **MUST** provide a valid value otherwise blacklisting won't work + #[schemars(with = "String")] + #[serde(with = "bs58_ecash")] + #[cfg_attr(feature = "openapi", schema(value_type = String))] + pub ecash_pubkey: PublicKeyUser, + + // needs to be explicit in case user creates request at 23:59:59.999, but it reaches vpn-api at 00:00:00.001 + #[schemars(with = "String")] + #[serde(with = "crate::helpers::date_serde")] + pub expiration_date: Date, + + #[schemars(with = "String")] + #[cfg_attr(feature = "openapi", schema(value_type = String))] + pub ticketbook_type: TicketType, + + pub is_freepass_request: bool, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct TicketbookAsyncRequest { + #[serde(flatten)] + pub inner: TicketbookRequest, + + /// unique id of the device + pub device_id: String, + /// unique id of the credential + pub credential_id: String, + /// secret used for webhook responses + pub secret: String, +} + +mod bs58_ecash { + use nym_credentials_interface::Base58; + use serde::{Deserialize, Deserializer, Serializer}; + + pub fn serialize(req: &T, serializer: S) -> Result + where + T: Base58, + { + serializer.serialize_str(&req.to_bs58()) + } + + pub fn deserialize<'de, D: Deserializer<'de>, T>(deserializer: D) -> Result + where + T: Base58, + { + let s = ::deserialize(deserializer)?; + T::try_from_bs58(&s).map_err(serde::de::Error::custom) + } +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "openapi", schema(value_type = String))] +pub struct WithdrawalRequestBs58Wrapper(#[serde(with = "bs58_ecash")] pub WithdrawalRequest); + +impl From for WithdrawalRequest { + fn from(value: WithdrawalRequestBs58Wrapper) -> Self { + value.0 + } +} + +impl From for WithdrawalRequestBs58Wrapper { + fn from(value: WithdrawalRequest) -> Self { + WithdrawalRequestBs58Wrapper(value) + } +} + +impl Deref for WithdrawalRequestBs58Wrapper { + type Target = WithdrawalRequest; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for WithdrawalRequestBs58Wrapper { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +// implementation taken from: https://github.com/GREsau/schemars/pull/207 +impl JsonSchema for WithdrawalRequestBs58Wrapper { + fn is_referenceable() -> bool { + true + } + + fn schema_name() -> String { + "WithdrawalRequestBs58Wrapper".into() + } + + fn json_schema(gen: &mut SchemaGenerator) -> Schema { + // during serialisation we just use bs58 representation + String::json_schema(gen) + } +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "tsify", derive(Tsify))] +#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))] +#[serde(rename_all = "camelCase")] +pub struct CurrentEpochResponse { + pub epoch_id: u64, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "tsify", derive(Tsify))] +#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))] +#[serde(rename_all = "camelCase")] +pub struct PartialVerificationKeysResponse { + pub epoch_id: u64, + pub keys: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "tsify", derive(Tsify))] +#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))] +#[serde(rename_all = "camelCase")] +pub struct PartialVerificationKey { + pub node_index: u64, + pub bs58_encoded_key: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "tsify", derive(Tsify))] +#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))] +#[serde(rename_all = "camelCase")] +pub struct MasterVerificationKeyResponse { + pub epoch_id: u64, + pub bs58_encoded_key: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct DepositResponse { + pub current_deposit_amount: u128, + pub current_deposit_denom: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct AggregatedExpirationDateSignaturesResponse { + #[schemars(with = "PlaceholderJsonSchemaImpl")] + #[cfg_attr(feature = "openapi", schema(value_type = PlaceholderJsonSchemaImpl))] + pub signatures: AggregatedExpirationDateSignatures, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct AggregatedCoinIndicesSignaturesResponse { + #[schemars(with = "PlaceholderJsonSchemaImpl")] + #[cfg_attr(feature = "openapi", schema(value_type = PlaceholderJsonSchemaImpl))] + pub signatures: AggregatedCoinIndicesSignatures, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[cfg_attr(feature = "tsify", derive(Tsify))] +#[cfg_attr(feature = "tsify", tsify(into_wasm_abi, from_wasm_abi))] +#[serde(rename_all = "camelCase")] +pub struct WalletShare { + pub node_index: u64, + pub bs58_encoded_share: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct TicketbookWalletSharesResponse { + pub epoch_id: u64, + pub shares: Vec, + pub master_verification_key: Option, + pub aggregated_coin_index_signatures: Option, + pub aggregated_expiration_date_signatures: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct TicketbookWalletSharesAsyncResponse { + pub id: i64, + + // maybe redundant, but could be useful for debugging + pub uuid: Uuid, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct WebhookTicketbookWalletShares { + pub id: i64, + pub status: String, + pub device_id: String, + pub credential_id: String, + pub data: Option, + pub error_message: Option, + + #[schemars(with = "String")] + #[serde(with = "time::serde::rfc3339")] + pub created: OffsetDateTime, + + #[schemars(with = "String")] + #[serde(with = "time::serde::rfc3339")] + pub updated: OffsetDateTime, +} + +#[derive(Serialize, Deserialize, Debug, Clone, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +#[serde(rename_all = "camelCase")] +pub struct WebhookTicketbookWalletSharesRequest { + pub ticketbook_wallet_shares: WebhookTicketbookWalletShares, + pub secret: String, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema, utoipa::IntoParams))] +#[cfg(feature = "query-types")] +#[serde(default, rename_all = "kebab-case")] +pub struct TicketbookObtainQueryParams { + pub output: Option, + + pub include_master_verification_key: bool, + + pub include_coin_index_signatures: bool, + + pub include_expiration_date_signatures: bool, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema, utoipa::IntoParams))] +#[cfg(feature = "query-types")] +#[serde(default, rename_all = "kebab-case")] +pub struct SharesQueryParams { + pub output: Option, + + pub include_master_verification_key: bool, + + pub include_coin_index_signatures: bool, + + pub include_expiration_date_signatures: bool, +} diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs new file mode 100644 index 0000000000..87b486e677 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/client.rs @@ -0,0 +1,172 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::api::v1::ticketbook::models::{ + MasterVerificationKeyResponse, PartialVerificationKeysResponse, TicketbookRequest, + TicketbookWalletSharesResponse, +}; +use async_trait::async_trait; +use nym_http_api_client::{parse_response, HttpClientError, Params, PathSegments, NO_PARAMS}; +use reqwest::IntoUrl; +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; + +pub use nym_http_api_client::Client; +pub type VpnApiClientError = HttpClientError; + +#[allow(dead_code)] +pub struct VpnApiClient { + inner: Client, + bearer_token: String, +} + +pub fn new_client( + base_url: impl IntoUrl, + bearer_token: impl Into, +) -> Result { + Ok(VpnApiClient { + inner: Client::builder(base_url)? + .with_user_agent(format!( + "nym-credential-proxy-requests/{}", + env!("CARGO_PKG_VERSION") + )) + .build()?, + bearer_token: bearer_token.into(), + }) +} + +// TODO: do it properly by implementing auth headers on `ApiClient` trait +#[allow(dead_code)] +#[async_trait(?Send)] +pub trait NymVpnApiClient { + async fn simple_get(&self, path: PathSegments<'_>) -> Result + where + T: DeserializeOwned; + + async fn simple_post( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + json_body: &B, + ) -> Result + where + B: Serialize + ?Sized, + for<'a> T: Deserialize<'a>, + K: AsRef, + V: AsRef; + + async fn get_partial_verification_keys( + &self, + ) -> Result { + self.simple_get(&["/api", "/v1", "/ticketbook", "/partial-verification-keys"]) + .await + } + + async fn get_master_verification_key( + &self, + ) -> Result { + self.simple_get(&["/api", "/v1", "/ticketbook", "/master-verification-key"]) + .await + } + + async fn get_ticketbook_wallet_shares( + &self, + request: &TicketbookRequest, + full_response: bool, + ) -> Result { + let params = vec![("full-response", full_response.to_string())]; + + self.simple_post(&["/api", "/v1", "/ticketbook", "/obtain"], ¶ms, request) + .await + } + // + // async fn get_bandwidth_voucher_blinded_shares( + // &self, + // blind_sign_request: BlindSignRequest, + // ) -> Result; +} + +#[async_trait(?Send)] +impl NymVpnApiClient for VpnApiClient { + async fn simple_get(&self, path: PathSegments<'_>) -> Result + where + T: DeserializeOwned, + { + let req = self + .inner + .create_get_request(path, NO_PARAMS) + .bearer_auth(&self.bearer_token) + .send(); + + // the only reason for that target lock is so that I could call this method from an ephemeral test + // running in non-wasm mode (since I wanted to use tokio) + + #[cfg(target_arch = "wasm32")] + let res = wasmtimer::tokio::timeout(std::time::Duration::from_secs(5), req) + .await + .map_err(|_timeout| HttpClientError::RequestTimeout)??; + + #[cfg(not(target_arch = "wasm32"))] + let res = req.await?; + + parse_response(res, false).await + } + + async fn simple_post( + &self, + path: PathSegments<'_>, + params: Params<'_, K, V>, + json_body: &B, + ) -> Result + where + B: Serialize + ?Sized, + for<'a> T: Deserialize<'a>, + K: AsRef, + V: AsRef, + { + let req = self + .inner + .create_post_request(path, params, json_body) + .bearer_auth(&self.bearer_token) + .send(); + + // the only reason for that target lock is so that I could call this method from an ephemeral test + // running in non-wasm mode (since I wanted to use tokio) + + #[cfg(target_arch = "wasm32")] + let res = wasmtimer::tokio::timeout(std::time::Duration::from_secs(5), req) + .await + .map_err(|_timeout| HttpClientError::RequestTimeout)??; + + #[cfg(not(target_arch = "wasm32"))] + let res = req.await?; + + parse_response(res, false).await + } + + // async fn get_bandwidth_voucher_blinded_shares( + // &self, + // blind_sign_request: BlindSignRequest, + // ) -> Result { + // let req = self.inner.create_post_request( + // &["/api", "/v1", "/bandwidth-voucher", "/obtain"], + // NO_PARAMS, + // &BandwidthVoucherRequest { blind_sign_request }, + // ); + // + // let fut = req.bearer_auth(&self.bearer_token).send(); + // + // // the only reason for that target lock is so that I could call this method from an ephemeral test + // // running in non-wasm mode (since I wanted to use tokio) + // + // #[cfg(target_arch = "wasm32")] + // let res = wasmtimer::tokio::timeout(std::time::Duration::from_secs(5), fut) + // .await + // .map_err(|_timeout| HttpClientError::RequestTimeout)??; + // + // #[cfg(not(target_arch = "wasm32"))] + // let res = fut.await?; + // + // parse_response(res, false).await + // } +} diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/helpers.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/helpers.rs new file mode 100644 index 0000000000..5420be21c5 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/helpers.rs @@ -0,0 +1,40 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use time::format_description::{modifier, BorrowedFormatItem, Component}; + +const DATE_FORMAT: &[BorrowedFormatItem<'_>] = &[ + BorrowedFormatItem::Component(Component::Year(modifier::Year::default())), + BorrowedFormatItem::Literal(b"-"), + BorrowedFormatItem::Component(Component::Month(modifier::Month::default())), + BorrowedFormatItem::Literal(b"-"), + BorrowedFormatItem::Component(Component::Day(modifier::Day::default())), +]; + +pub(crate) mod date_serde { + use crate::helpers::DATE_FORMAT; + use serde::ser::Error; + use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; + use time::Date; + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + + Date::parse(&s, DATE_FORMAT).map_err(de::Error::custom) + } + + pub(crate) fn serialize(datetime: &Date, serializer: S) -> Result + where + S: Serializer, + { + // serialize it with human-readable format for compatibility with eclipse and nutella clients + // in the future change it back to rfc3339 + datetime + .format(&DATE_FORMAT) + .map_err(S::Error::custom)? + .serialize(serializer) + } +} diff --git a/nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs b/nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs new file mode 100644 index 0000000000..8415c35ba6 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy-requests/src/lib.rs @@ -0,0 +1,84 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] + +pub mod api; +pub mod client; +mod helpers; + +macro_rules! absolute_route { + ( $name:ident, $parent:expr, $suffix:expr ) => { + pub fn $name() -> String { + format!("{}{}", $parent, $suffix) + } + }; +} + +pub mod routes { + pub const ROOT: &str = "/"; + pub const API: &str = "/api"; + + pub mod api { + pub const V1: &str = "/v1"; + + absolute_route!(v1_absolute, super::API, V1); + + pub mod v1 { + use super::*; + + pub const SWAGGER: &str = "/swagger"; + pub const TICKETBOOK: &str = "/ticketbook"; + + // define helper functions to get absolute routes + absolute_route!(swagger_absolute, v1_absolute(), SWAGGER); + absolute_route!(ticketbook_absolute, v1_absolute(), TICKETBOOK); + + pub mod ticketbook { + use super::*; + + pub const OBTAIN: &str = "/obtain"; + pub const OBTAIN_ASYNC: &str = "/obtain-async"; + pub const DEPOSIT_AMOUNT: &str = "/deposit-amount"; + pub const MASTER_KEY: &str = "/master-verification-key"; + pub const PARTIAL_KEYS: &str = "/partial-verification-keys"; + pub const CURRENT_EPOCH: &str = "/current-epoch"; + pub const SHARES: &str = "/shares"; + + absolute_route!(obtain_wallet_shares_absolute, ticketbook_absolute(), OBTAIN); + absolute_route!( + obtain_async_wallet_shares_absolute, + ticketbook_absolute(), + OBTAIN_ASYNC + ); + absolute_route!( + current_deposit_amount_absolute, + ticketbook_absolute(), + DEPOSIT_AMOUNT + ); + absolute_route!(master_key_absolute, ticketbook_absolute(), MASTER_KEY); + absolute_route!(partial_keys_absolute, ticketbook_absolute(), PARTIAL_KEYS); + absolute_route!(current_epoch_absolute, ticketbook_absolute(), CURRENT_EPOCH); + absolute_route!(shares_absolute, ticketbook_absolute(), SHARES); + + pub mod shares { + use super::*; + + pub const SHARE_BY_ID: &str = "/:share_id"; + pub const SHARE_BY_DEVICE_AND_CREDENTIAL_ID: &str = + "/device/:device_id/credential/:credential_id"; + + absolute_route!(share_by_id_absolute, shares_absolute(), SHARE_BY_ID); + absolute_route!( + share_by_device_and_credential_id_absolute, + shares_absolute(), + SHARE_BY_DEVICE_AND_CREDENTIAL_ID + ); + } + } + } + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/.gitignore b/nym-credential-proxy/nym-credential-proxy/.gitignore new file mode 100644 index 0000000000..1dc2d107db --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/.gitignore @@ -0,0 +1 @@ +.db \ No newline at end of file diff --git a/nym-credential-proxy/nym-credential-proxy/Cargo.toml b/nym-credential-proxy/nym-credential-proxy/Cargo.toml new file mode 100644 index 0000000000..143e163f54 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/Cargo.toml @@ -0,0 +1,66 @@ +[package] +name = "nym-credential-proxy" +version = "0.1.3" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +async-trait.workspace = true +axum.workspace = true +anyhow.workspace = true +bip39 = { workspace = true, features = ["zeroize"] } +bs58.workspace = true +cfg-if = { workspace = true } +colored.workspace = true +clap = { workspace = true, features = ["derive", "env"] } +dotenv.workspace = true +futures.workspace = true +humantime.workspace = true +rand.workspace = true +reqwest = { workspace = true, features = ["rustls-tls"] } +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] } +strum = { workspace = true, features = ["derive"] } +strum_macros.workspace = true +time.workspace = true +thiserror.workspace = true +tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal"] } +tokio-util = { workspace = true, features = ["rt"] } +tower.workspace = true +tower-http = { workspace = true, features = ["cors"], optional = true } +tracing.workspace = true +url.workspace = true +uuid = { workspace = true, features = ["serde"] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } +utoipa-swagger-ui = { workspace = true, features = ["axum"] } +zeroize.workspace = true + +nym-bin-common = { path = "../../common/bin-common", features = ["basic_tracing"] } +nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } +nym-config = { path = "../../common/config" } +nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand", "serde"] } +nym-credentials = { path = "../../common/credentials" } +nym-credentials-interface = { path = "../../common/credentials-interface" } +nym-http-api-common = { path = "../../common/http-api-common", features = ["utoipa"] } +nym-validator-client = { path = "../../common/client-libs/validator-client" } +nym-network-defaults = { path = "../../common/network-defaults" } + +nym-credential-proxy-requests = { path = "../nym-credential-proxy-requests", features = ["openapi"] } + +[dev-dependencies] +tempfile = { workspace = true } + +[build-dependencies] +tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] } + +[features] +default = ["cors"] +cors = ["tower-http"] diff --git a/nym-credential-proxy/nym-credential-proxy/Dockerfile b/nym-credential-proxy/nym-credential-proxy/Dockerfile new file mode 100644 index 0000000000..e4548e8928 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/Dockerfile @@ -0,0 +1,35 @@ +FROM rust:latest AS builder + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-credential-proxy/nym-credential-proxy + +RUN cargo build --release + +#------------------------------------------------------------------- +# The following environment variables are required at runtime: +# +# NYM_CREDENTIAL_PROXY_MNEMONIC +# NYM_CREDENTIAL_PROXY_AUTH_TOKEN +# +# WEBHOOK_ZK_NYMS_URL +# WEBHOOK_ZK_NYMS_CLIENT_ID +# WEBHOOK_ZK_NYMS_CLIENT_SECRET +# +# And optionally: +# +# NYM_CREDENTIAL_PROXY_PORT +# NYM_CREDENTIAL_PROXY_BIND_ADDRESS +# NYM_CREDENTIAL_PROXY_PERSISTENT_STORAGE_STORAGE +# +# see https://github.com/nymtech/nym/blob/develop/nym-credential-proxy/nym-credential-proxy/src/cli.rs for details +#------------------------------------------------------------------- + +FROM ubuntu:24.04 + +RUN apt update && apt install -yy curl ca-certificates + +WORKDIR /nym + +COPY --from=builder /usr/src/nym/nym-credential-proxy/target/release/nym-credential-proxy ./ +ENTRYPOINT [ "/nym/nym-credential-proxy" ] + diff --git a/nym-credential-proxy/nym-credential-proxy/build.rs b/nym-credential-proxy/nym-credential-proxy/build.rs new file mode 100644 index 0000000000..bdbdcf0d22 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/build.rs @@ -0,0 +1,22 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +#[tokio::main] +async fn main() { + use sqlx::{Connection, SqliteConnection}; + use std::env; + + let out_dir = env::var("OUT_DIR").unwrap(); + let database_path = format!("{out_dir}/nym-credential-proxy-example.sqlite"); + + let mut conn = SqliteConnection::connect(&format!("sqlite://{database_path}?mode=rwc")) + .await + .expect("Failed to create SQLx database connection"); + + sqlx::migrate!("./migrations") + .run(&mut conn) + .await + .expect("Failed to perform SQLx migrations"); + + println!("cargo:rustc-env=DATABASE_URL=sqlite://{}", &database_path); +} diff --git a/nym-credential-proxy/nym-credential-proxy/migrations/01_initial.sql b/nym-credential-proxy/nym-credential-proxy/migrations/01_initial.sql new file mode 100644 index 0000000000..c3900d9b36 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/migrations/01_initial.sql @@ -0,0 +1,19 @@ +/* + * Copyright 2023 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + +CREATE TABLE blinded_shares +( + id INTEGER NOT NULL PRIMARY KEY, + status TEXT NOT NULL, + device_id TEXT NOT NULL, + credential_id TEXT NOT NULL, + data TEXT DEFAULT NULL, + error_message TEXT DEFAULT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE UNIQUE INDEX blinded_shares_index ON blinded_shares (credential_id, device_id); + diff --git a/nym-credential-proxy/nym-credential-proxy/migrations/02_cherry_picking_chaos.sql b/nym-credential-proxy/nym-credential-proxy/migrations/02_cherry_picking_chaos.sql new file mode 100644 index 0000000000..674b6d328d --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/migrations/02_cherry_picking_chaos.sql @@ -0,0 +1,78 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: GPL-3.0-only + */ + + +DROP TABLE blinded_shares; +CREATE TABLE blinded_shares +( + id INTEGER NOT NULL PRIMARY KEY, +-- added request_uuid to tie it to deposit and actual share data + request_uuid TEXT NOT NULL REFERENCES ticketbook_deposit(request_uuid), + status TEXT NOT NULL, + device_id TEXT NOT NULL, + credential_id TEXT NOT NULL, +-- replaced the explicit data field in favour of separate table alongside +-- the information on the number of shares available (need min. threshold) + available_shares INTEGER NOT NULL DEFAULT 0, + error_message TEXT DEFAULT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated TIMESTAMP WITHOUT TIME ZONE NOT NULL +); + +CREATE UNIQUE INDEX blinded_shares_index ON blinded_shares (credential_id, device_id); + + +CREATE TABLE ticketbook_deposit ( + deposit_id INTEGER PRIMARY KEY NOT NULL, + deposit_tx_hash TEXT NOT NULL, + requested_on TIMESTAMP WITHOUT TIME ZONE NOT NULL, + request_uuid TEXT UNIQUE NOT NULL, + deposit_amount TEXT NOT NULL, + client_pubkey BLOB NOT NULL, + ed25519_deposit_private_key BLOB NOT NULL +); + +CREATE TABLE partial_blinded_wallet ( + corresponding_deposit INTEGER NOT NULL REFERENCES ticketbook_deposit(deposit_id), + epoch_id INTEGER NOT NULL, + expiration_date DATE NOT NULL, + node_id INTEGER NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + blinded_signature BLOB NOT NULL +); + +CREATE TABLE partial_blinded_wallet_failure ( + corresponding_deposit INTEGER NOT NULL REFERENCES ticketbook_deposit(deposit_id), + epoch_id INTEGER NOT NULL, + expiration_date DATE NOT NULL, + node_id INTEGER NOT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + failure_message TEXT NOT NULL +); + +-- copied (+rev) from nym-api +CREATE TABLE master_verification_key ( + epoch_id INTEGER PRIMARY KEY NOT NULL, + serialization_revision INTEGER NOT NULL, + serialised_key BLOB NOT NULL +); + +CREATE TABLE global_coin_index_signatures ( + -- we can only have a single entry + epoch_id INTEGER PRIMARY KEY NOT NULL, + serialization_revision INTEGER NOT NULL, + + -- combined signatures for all indices + serialised_signatures BLOB NOT NULL +); + +CREATE TABLE global_expiration_date_signatures ( + expiration_date DATE NOT NULL UNIQUE PRIMARY KEY, + epoch_id INTEGER NOT NULL, + serialization_revision INTEGER NOT NULL, + + -- combined signatures for all tuples issued for given day + serialised_signatures BLOB NOT NULL +); diff --git a/nym-credential-proxy/nym-credential-proxy/migrations/03_blinded_shares_no_foreign_table.sql b/nym-credential-proxy/nym-credential-proxy/migrations/03_blinded_shares_no_foreign_table.sql new file mode 100644 index 0000000000..b7bc48ee96 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/migrations/03_blinded_shares_no_foreign_table.sql @@ -0,0 +1,20 @@ +/* + * Copyright 2024 - Nym Technologies SA + * SPDX-License-Identifier: Apache-2.0 + */ + + +DROP TABLE blinded_shares; +CREATE TABLE blinded_shares +( + id INTEGER NOT NULL PRIMARY KEY, +-- removed reference to `ticketbook_deposit` as the deposit wouldn't actually have been made before the pending share is inserted + request_uuid TEXT NOT NULL, + status TEXT NOT NULL, + device_id TEXT NOT NULL, + credential_id TEXT NOT NULL, + available_shares INTEGER NOT NULL DEFAULT 0, + error_message TEXT DEFAULT NULL, + created TIMESTAMP WITHOUT TIME ZONE NOT NULL, + updated TIMESTAMP WITHOUT TIME ZONE NOT NULL +); \ No newline at end of file diff --git a/nym-credential-proxy/nym-credential-proxy/src/cli.rs b/nym-credential-proxy/nym-credential-proxy/src/cli.rs new file mode 100644 index 0000000000..3ade5649b0 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/cli.rs @@ -0,0 +1,92 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::config::default_database_filepath; +use crate::webhook::ZkNymWebHookConfig; +use clap::builder::ArgPredicate; +use clap::Parser; +use nym_bin_common::bin_info; +use std::fs::create_dir_all; +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::PathBuf; +use std::sync::OnceLock; +use tracing::info; + +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +// if needed this could be split into subcommands +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, about, long_version = pretty_build_info_static())] +pub struct Cli { + #[clap(flatten)] + pub(crate) webhook: ZkNymWebHookConfig, + + /// Path pointing to an env file that configures the binary. + #[clap(short, long)] + pub(crate) config_env_file: Option, + + /// Specifies the custom port value used for the api server. + /// default: `8080` + #[clap( + long, + env = "NYM_CREDENTIAL_PROXY_PORT", + default_value = "8080", + default_value_if("bind_address", ArgPredicate::IsPresent, None) + )] + pub port: Option, + + /// Specifies the custom bind address value used for the api server. + /// default: `0.0.0.0:8080` + #[clap(long, env = "NYM_CREDENTIAL_PROXY_BIND_ADDRESS")] + pub bind_address: Option, + + /// Specifies the mnemonic authorised for making deposits for "free pass" ticketbooks + #[clap(long, env = "NYM_CREDENTIAL_PROXY_MNEMONIC")] + pub mnemonic: bip39::Mnemonic, + + /// Bearer token for accessing the http endpoints. + #[clap( + long, + env = "NYM_CREDENTIAL_PROXY_AUTH_TOKEN", + alias = "http-bearer-token" + )] + pub(crate) http_auth_token: String, + + #[clap(long, env = "NYM_CREDENTIAL_PROXY_PERSISTENT_STORAGE_STORAGE")] + pub(crate) persistent_storage_path: Option, +} + +impl Cli { + pub fn bind_address(&self) -> SocketAddr { + // SAFETY: + // if `bind_address` hasn't been specified, `port` will default to "8080", + // so some value will always be available to use + #[allow(clippy::unwrap_used)] + self.bind_address.unwrap_or_else(|| { + SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), self.port.unwrap()) + }) + } + + pub fn persistent_storage_path(&self) -> PathBuf { + self.persistent_storage_path.clone().unwrap_or_else(|| { + // if this blows up, then we shouldn't continue + #[allow(clippy::expect_used)] + let default_path = default_database_filepath(); + if let Some(parent) = default_path.parent() { + // make sure it exists + #[allow(clippy::unwrap_used)] + create_dir_all(parent).unwrap(); + } + + info!( + "setting the storage path path to {}", + default_path.display() + ); + + default_path + }) + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/config/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/config/mod.rs new file mode 100644 index 0000000000..eeafe05203 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/config/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_config::{must_get_home, DEFAULT_DATA_DIR, NYM_DIR}; +use std::path::PathBuf; + +pub const DEFAULT_NYM_CREDENTIAL_PROXY_DIR: &str = "nym-credential-proxy"; + +pub const DEFAULT_DB_FILENAME: &str = "nym-credential-proxy.sqlite"; + +pub fn default_data_directory() -> PathBuf { + must_get_home() + .join(NYM_DIR) + .join(DEFAULT_NYM_CREDENTIAL_PROXY_DIR) + .join(DEFAULT_DATA_DIR) +} + +pub fn default_database_filepath() -> PathBuf { + default_data_directory().join(DEFAULT_DB_FILENAME) +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/credentials/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/credentials/mod.rs new file mode 100644 index 0000000000..a6ee988de5 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/credentials/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod ticketbook; diff --git a/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs new file mode 100644 index 0000000000..3a91724a4f --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/credentials/ticketbook/mod.rs @@ -0,0 +1,371 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiError; +use crate::http::state::ApiState; +use crate::storage::models::BlindedShares; +use futures::{stream, StreamExt}; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + TicketbookAsyncRequest, TicketbookObtainQueryParams, TicketbookRequest, + TicketbookWalletSharesResponse, WalletShare, WebhookTicketbookWalletShares, + WebhookTicketbookWalletSharesRequest, +}; +use nym_credentials::IssuanceTicketBook; +use nym_credentials_interface::Base58; +use nym_crypto::asymmetric::ed25519; +use nym_validator_client::ecash::BlindSignRequestBody; +use nym_validator_client::nyxd::contract_traits::EcashSigningClient; +use nym_validator_client::nyxd::cosmwasm_client::ToSingletonContractData; +use rand::rngs::OsRng; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; +use time::OffsetDateTime; +use tokio::sync::Mutex; +use tokio::time::timeout; +use tracing::{debug, error, info, instrument}; +use uuid::Uuid; + +// use the same type alias as our contract without importing the whole thing just for this single line +pub type NodeId = u64; + +#[instrument( + skip(state, request_data, request, requested_on), + fields( + expiration_date = %request_data.expiration_date, + ticketbook_type = %request_data.ticketbook_type + ) +)] +pub(crate) async fn try_obtain_wallet_shares( + state: &ApiState, + request: Uuid, + requested_on: OffsetDateTime, + request_data: TicketbookRequest, +) -> Result, VpnApiError> { + let mut rng = OsRng; + + let ed25519_keypair = ed25519::KeyPair::new(&mut rng); + + let epoch = state.current_epoch_id().await?; + let deposit_amount = state.deposit_amount().await?; + let threshold = state.ecash_threshold(epoch).await?; + let expiration_date = request_data.expiration_date; + + // before we commit to making the deposit, ensure we have required signatures cached and stored + let _ = state.master_verification_key(Some(epoch)).await?; + let _ = state.master_coin_index_signatures(Some(epoch)).await?; + let _ = state + .master_expiration_date_signatures(expiration_date) + .await?; + let ecash_api_clients = state.ecash_clients(epoch).await?.clone(); + + let chain_write_permit = state.start_chain_tx().await; + + info!("starting the deposit!"); + // TODO: batch those up + // TODO: batch those up + let deposit_res = chain_write_permit + .make_ticketbook_deposit( + ed25519_keypair.public_key().to_base58_string(), + deposit_amount.clone(), + None, + ) + .await?; + + // explicitly drop it here so other tasks could start using it + drop(chain_write_permit); + + let deposit_id = deposit_res.parse_singleton_u32_contract_data()?; + let tx_hash = deposit_res.transaction_hash; + info!(deposit_id = %deposit_id, tx_hash = %tx_hash, "deposit finished"); + + // store the deposit information so if we fail, we could perhaps still reuse it for another issuance + state + .storage() + .insert_deposit_data( + deposit_id, + tx_hash, + requested_on, + request, + deposit_amount, + &request_data.ecash_pubkey, + &ed25519_keypair, + ) + .await?; + + let plaintext = + IssuanceTicketBook::request_plaintext(&request_data.withdrawal_request, deposit_id); + let signature = ed25519_keypair.private_key().sign(plaintext); + + let credential_request = BlindSignRequestBody::new( + request_data.withdrawal_request.into(), + deposit_id, + signature, + request_data.ecash_pubkey, + request_data.expiration_date, + request_data.ticketbook_type, + ); + + let wallet_shares = Arc::new(Mutex::new(HashMap::new())); + + info!("attempting to contract all nym-apis for the partial wallets..."); + stream::iter(ecash_api_clients) + .for_each_concurrent(None, |client| async { + // move the client into the block + let client = client; + + debug!("contacting {client} for blinded partial wallet"); + let res = timeout( + Duration::from_secs(5), + client.api_client.blind_sign(&credential_request), + ) + .await + .map_err(|_| VpnApiError::EcashApiRequestTimeout { + client_repr: client.to_string(), + }) + .and_then(|res| res.map_err(Into::into)); + + // 1. try to store it + if let Err(err) = state + .storage() + .insert_partial_wallet_share( + deposit_id, + epoch, + expiration_date, + client.node_id, + &res, + ) + .await + { + error!("failed to persist issued partial share: {err}") + } + + // 2. add it to the map + match res { + Ok(share) => { + wallet_shares + .lock() + .await + .insert(client.node_id, share.blinded_signature); + } + Err(err) => { + error!("failed to obtain partial blinded wallet share from {client}: {err}") + } + } + }) + .await; + + // SAFETY: the futures have completed, so we MUST have the only arc reference + #[allow(clippy::unwrap_used)] + let wallet_shares = Arc::into_inner(wallet_shares).unwrap().into_inner(); + let shares = wallet_shares.len(); + + if shares < threshold as usize { + return Err(VpnApiError::InsufficientNumberOfCredentials { + available: shares, + threshold, + }); + } + + Ok(wallet_shares + .into_iter() + .map(|(node_index, share)| WalletShare { + node_index, + bs58_encoded_share: share.to_bs58(), + }) + .collect()) +} + +// same as try_obtain_wallet_shares, but writes failures into the db +async fn try_obtain_wallet_shares_async( + state: &ApiState, + request: Uuid, + requested_on: OffsetDateTime, + request_data: TicketbookRequest, + device_id: &str, + credential_id: &str, +) -> Result, VpnApiError> { + let shares = match try_obtain_wallet_shares(state, request, requested_on, request_data).await { + Ok(shares) => shares, + Err(err) => { + let obtained = match err { + VpnApiError::InsufficientNumberOfCredentials { available, .. } => available, + _ => 0, + }; + + // currently there's no retry mechanisms, but, who knows, that might change + if let Err(err) = state + .storage() + .update_pending_async_blinded_shares_error( + obtained, + device_id, + credential_id, + &err.to_string(), + ) + .await + { + error!("failed to update database with the error information: {err}") + } + return Err(err); + } + }; + + Ok(shares) +} + +async fn try_obtain_blinded_ticketbook_async_inner( + state: &ApiState, + request: Uuid, + requested_on: OffsetDateTime, + request_data: TicketbookAsyncRequest, + params: TicketbookObtainQueryParams, + pending: &BlindedShares, +) -> Result<(), VpnApiError> { + let epoch_id = state.current_epoch_id().await?; + + let device_id = &request_data.device_id; + let credential_id = &request_data.credential_id; + let secret = request_data.secret.clone(); + + // 1. try to obtain global data + let ( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + ) = state + .global_data( + params.include_master_verification_key, + params.include_coin_index_signatures, + params.include_expiration_date_signatures, + epoch_id, + request_data.inner.expiration_date, + ) + .await?; + + // 2. try to obtain shares (failures are written to the DB) + let shares = try_obtain_wallet_shares_async( + state, + request, + requested_on, + request_data.inner, + device_id, + credential_id, + ) + .await?; + + // 3. update the storage, if possible + // (as long as we can trigger webhook, we should still be good) + if let Err(err) = state + .storage() + .update_pending_async_blinded_shares_issued(shares.len(), device_id, credential_id) + .await + { + error!(uuid = %request, "failed to update db with issued information: {err}") + } + + // 4. build the webhook request body + let data = Some(TicketbookWalletSharesResponse { + epoch_id, + shares, + master_verification_key, + aggregated_coin_index_signatures, + aggregated_expiration_date_signatures, + }); + + let ticketbook_wallet_shares = WebhookTicketbookWalletShares { + id: pending.id, + status: pending.status.to_string(), + device_id: device_id.clone(), + credential_id: credential_id.clone(), + data, + error_message: None, + created: pending.created, + updated: pending.updated, + }; + + let webhook_request = WebhookTicketbookWalletSharesRequest { + ticketbook_wallet_shares, + secret, + }; + + // 5. call the webhook + state + .zk_nym_web_hook() + .try_trigger(request, &webhook_request) + .await; + + Ok(()) +} + +async fn try_trigger_webhook_request_for_error( + state: &ApiState, + request: Uuid, + request_data: TicketbookAsyncRequest, + pending: &BlindedShares, + error_message: String, +) -> Result<(), VpnApiError> { + let device_id = &request_data.device_id; + let credential_id = &request_data.credential_id; + let secret = request_data.secret.clone(); + + let ticketbook_wallet_shares = WebhookTicketbookWalletShares { + id: pending.id, + status: "error".to_string(), + device_id: device_id.clone(), + credential_id: credential_id.clone(), + data: None, + error_message: Some(error_message), + created: pending.created, + updated: pending.updated, + }; + + let webhook_request = WebhookTicketbookWalletSharesRequest { + ticketbook_wallet_shares, + secret, + }; + + state + .zk_nym_web_hook() + .try_trigger(request, &webhook_request) + .await; + + Ok(()) +} + +#[instrument(skip_all, fields(credential_id = %request_data.credential_id, device_id = %request_data.device_id))] +#[allow(clippy::too_many_arguments)] +pub(crate) async fn try_obtain_blinded_ticketbook_async( + state: ApiState, + request: Uuid, + requested_on: OffsetDateTime, + request_data: TicketbookAsyncRequest, + params: TicketbookObtainQueryParams, + pending: BlindedShares, +) { + if let Err(err) = try_obtain_blinded_ticketbook_async_inner( + &state, + request, + requested_on, + request_data.clone(), + params, + &pending, + ) + .await + { + // post to the webhook to notify of errors on this side + if let Err(webhook_err) = try_trigger_webhook_request_for_error( + &state, + request, + request_data, + &pending, + format!("Failed to get ticketbook: {err}"), + ) + .await + { + error!(uuid = %request, "failed to make webhook request to report error: {webhook_err}") + } + error!(uuid = %request, "failed to resolve the blinded ticketbook issuance: {err}") + } else { + info!(uuid = %request, "managed to resolve the blinded ticketbook issuance") + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/error.rs b/nym-credential-proxy/nym-credential-proxy/src/error.rs new file mode 100644 index 0000000000..4ffba86be7 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/error.rs @@ -0,0 +1,126 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::error::NyxdError; +use std::io; +use std::net::SocketAddr; +use thiserror::Error; +use time::OffsetDateTime; + +#[derive(Debug, Error)] +pub enum VpnApiError { + #[error("encountered an internal io error: {source}")] + IoError { + #[from] + source: io::Error, + }, + + #[error("could not derive valid client url with the provided webhook parameters")] + InvalidWebhookUrl, + + #[error("failed to serialise recovery data: {source}")] + SerdeJsonFailure { + #[from] + source: serde_json::Error, + }, + + #[error("the provided expiration date is too late")] + ExpirationDateTooLate, + + #[error("the provided expiration date is too early")] + ExpirationDateTooEarly, + + #[error("failed to bind to {address}: {source}. Are you sure nothing else is running on the specified port and your user has sufficient permission to bind to the requested address?")] + SocketBindFailure { + address: SocketAddr, + source: io::Error, + }, + + #[error("the api server failed with the following message: {source}")] + HttpServerFailure { source: io::Error }, + + #[error("the ecash contract address is not set")] + UnavailableEcashContract, + + #[error("the DKG contract address is not set")] + UnavailableDKGContract, + + #[error("the bandwidth contract doesn't have any admin set")] + MissingBandwidthContractAdmin, + + #[error( + "the provided mnemonic does not correspond to the current admin of the bandwidth contract" + )] + MismatchedMnemonic, + + #[error("failed to interact with the nyx chain: {source}")] + NyxdFailure { + #[from] + source: NyxdError, + }, + + #[error("validator client error: {0}")] + ValidatorClientError(#[from] nym_validator_client::ValidatorClientError), + + #[error("failed to perform ecash operation: {source}")] + EcashApiFailure { + #[from] + source: EcashApiError, + }, + + #[error("Compact ecash internal error: {0}")] + CompactEcashInternalError(#[from] nym_compact_ecash::error::CompactEcashError), + + #[error("there are no rpc endpoints provided in the environment")] + NoNyxEndpointsAvailable, + + #[error("the threshold value for epoch {epoch_id} is not available")] + UnavailableThreshold { epoch_id: EpochId }, + + #[error( + "we have only {available} api clients available while the minimum threshold is {threshold}" + )] + InsufficientNumberOfSigners { available: usize, threshold: u64 }, + + #[error( + "we have only managed to obtain {available} partial credentials while the minimum threshold is {threshold}" + )] + InsufficientNumberOfCredentials { available: usize, threshold: u64 }, + + #[error("failed to interact with the credentials: {source}")] + CredentialsFailure { + #[from] + source: nym_credentials::Error, + }, + + #[error("the DKG has not yet been initialised in the system")] + UninitialisedDkg, + + #[error("credentials can't yet be issued in the system. approximate expected availability: {availability}")] + CredentialsNotYetIssuable { availability: OffsetDateTime }, + + #[error("reached seemingly impossible ecash failure")] + UnknownEcashFailure, + + #[error("experienced internal database error: {0}")] + InternalDatabaseError(#[from] sqlx::Error), + + #[error("experienced internal storage error: {reason}")] + DatabaseInconsistency { reason: String }, + + #[error("failed to perform startup SQL migration: {0}")] + StartupMigrationFailure(#[from] sqlx::migrate::MigrateError), + + #[error("timed out while attempting to obtain partial wallet from {client_repr}")] + EcashApiRequestTimeout { client_repr: String }, +} + +impl VpnApiError { + pub fn database_inconsistency>(reason: S) -> VpnApiError { + VpnApiError::DatabaseInconsistency { + reason: reason.into(), + } + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs new file mode 100644 index 0000000000..12ba427a70 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/helpers.rs @@ -0,0 +1,42 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use time::OffsetDateTime; +use tracing::{debug, info, warn}; + +pub struct LockTimer { + created: OffsetDateTime, + message: String, +} + +impl LockTimer { + pub fn new>(message: S) -> Self { + LockTimer { + message: message.into(), + ..Default::default() + } + } +} + +impl Drop for LockTimer { + fn drop(&mut self) { + let time_taken = OffsetDateTime::now_utc() - self.created; + let time_taken_formatted = humantime::format_duration(time_taken.unsigned_abs()); + if time_taken > time::Duration::SECOND * 10 { + warn!(time_taken = %time_taken_formatted, "{}", self.message) + } else if time_taken > time::Duration::SECOND * 5 { + info!(time_taken = %time_taken_formatted, "{}", self.message) + } else { + debug!(time_taken = %time_taken_formatted, "{}", self.message) + }; + } +} + +impl Default for LockTimer { + fn default() -> Self { + LockTimer { + created: OffsetDateTime::now_utc(), + message: "released the lock".to_string(), + } + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/http/helpers.rs new file mode 100644 index 0000000000..df35c59221 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/http/helpers.rs @@ -0,0 +1,13 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use rand::rngs::OsRng; +use rand::RngCore; +use uuid::Uuid; + +pub fn random_uuid() -> Uuid { + let mut bytes = [0u8; 16]; + let mut rng = OsRng; + rng.fill_bytes(&mut bytes); + Uuid::from_bytes(bytes) +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/middleware/auth.rs b/nym-credential-proxy/nym-credential-proxy/src/http/middleware/auth.rs new file mode 100644 index 0000000000..ce9c4e0912 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/http/middleware/auth.rs @@ -0,0 +1,115 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use axum::http::{header, HeaderValue, StatusCode}; +use axum::response::IntoResponse; +use axum::{extract::Request, response::Response}; +use futures::future::BoxFuture; +use std::sync::Arc; +use std::task::{Context, Poll}; +use tower::{Layer, Service}; +use tracing::{debug, instrument, trace}; +use zeroize::Zeroizing; + +#[derive(Debug, Clone)] +pub struct AuthLayer { + bearer_token: Arc>, +} + +impl AuthLayer { + pub fn new(bearer_token: Arc>) -> Self { + AuthLayer { bearer_token } + } +} + +impl Layer for AuthLayer { + type Service = RequireAuth; + + fn layer(&self, inner: S) -> Self::Service { + RequireAuth::new(inner, self.bearer_token.clone()) + } +} + +#[derive(Debug, Clone)] +pub struct RequireAuth { + inner: S, + bearer_token: Arc>, +} + +impl RequireAuth { + pub fn new(inner: S, bearer_token: Arc>) -> Self { + RequireAuth { + inner, + bearer_token, + } + } + + fn check_auth_header(&self, header: Option<&HeaderValue>) -> Result<(), &'static str> { + let Some(token) = header else { + trace!("missing header"); + return Err("`Authorization` header is missing"); + }; + + let Ok(authorization) = token.to_str() else { + trace!("invalid header"); + return Err("`Authorization` header contains invalid characters"); + }; + + debug!("header value: '{authorization}'"); + + let split = authorization.split_once(' '); + let bearer_token = match split { + // Found proper bearer + Some(("Bearer", contents)) => contents, + // Found empty bearer; + _ if authorization == "Bearer" => "", + // Found nothing + _ => return Err("`Authorization` header must be a bearer token"), + }; + + debug!("parsed token: '{bearer_token}'"); + + if self.bearer_token.is_empty() && bearer_token.is_empty() { + return Ok(()); + } + if bearer_token.is_empty() { + return Err("`Authorization` header must contain non-empty `Bearer` token"); + } + + if self.bearer_token.as_str() != bearer_token { + return Err("`Authorization` header does not contain the correct `Bearer` token"); + } + + Ok(()) + } +} + +impl Service for RequireAuth +where + S: Service + Send + 'static, + S: Send + Sync + 'static, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = S::Error; + type Future = BoxFuture<'static, Result>; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + #[instrument(skip_all, fields(uri = %req.uri()))] + fn call(&mut self, req: Request) -> Self::Future { + debug!("checking the auth"); + + let auth_header = req.headers().get(header::AUTHORIZATION); + + match self.check_auth_header(auth_header) { + Ok(_authorised) => Box::pin(self.inner.call(req)), + Err(err) => { + Box::pin(async move { Ok((StatusCode::UNAUTHORIZED, err).into_response()) }) + } + } + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/middleware/logging.rs b/nym-credential-proxy/nym-credential-proxy/src/http/middleware/logging.rs new file mode 100644 index 0000000000..825104ec5b --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/http/middleware/logging.rs @@ -0,0 +1,64 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use axum::{ + extract::{ConnectInfo, Request}, + http::{ + header::{HOST, USER_AGENT}, + HeaderValue, + }, + middleware::Next, + response::IntoResponse, +}; +use colored::*; +use std::net::SocketAddr; +use tokio::time::Instant; +use tracing::info; + +/// Simple logger for requests +pub async fn logger( + ConnectInfo(addr): ConnectInfo, + req: Request, + next: Next, +) -> impl IntoResponse { + let method = req.method().to_string().green(); + let uri = req.uri().to_string().blue(); + let agent = header_map( + req.headers().get(USER_AGENT), + "Unknown User Agent".to_string(), + ); + + let host = header_map(req.headers().get(HOST), "Unknown Host".to_string()); + + let start = Instant::now(); + let res = next.run(req).await; + let time_taken = start.elapsed(); + let status = res.status(); + let print_status = if status.is_client_error() || status.is_server_error() { + status.to_string().red() + } else if status.is_success() { + status.to_string().green() + } else { + status.to_string().yellow() + }; + + let taken = "time taken".bold(); + + let time_taken = match time_taken.as_millis() { + ms if ms > 500 => format!("{taken}: {}", format!("{ms}ms").red()), + ms if ms > 200 => format!("{taken}: {}", format!("{ms}ms").yellow()), + ms if ms > 50 => format!("{taken}: {}", format!("{ms}ms").bright_yellow()), + ms => format!("{taken}: {ms}ms"), + }; + + let agent_str = "agent".bold(); + info!("[{addr} -> {host}] {method} '{uri}': {print_status} {time_taken} {agent_str}: {agent}"); + + res +} + +fn header_map(header: Option<&HeaderValue>, msg: String) -> String { + header + .map(|x| x.to_str().unwrap_or(&msg).to_string()) + .unwrap_or(msg) +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/middleware/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/middleware/mod.rs new file mode 100644 index 0000000000..b51d9a59b5 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/http/middleware/mod.rs @@ -0,0 +1,5 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +pub mod auth; +pub mod logging; diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs new file mode 100644 index 0000000000..1267b3abfb --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/http/mod.rs @@ -0,0 +1,52 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiError; +use crate::http::router::build_router; +use crate::http::state::ApiState; +use axum::Router; +use std::net::SocketAddr; +use tokio_util::sync::CancellationToken; +use tracing::info; + +pub mod helpers; +pub mod middleware; +pub mod router; +pub mod state; +pub mod types; + +pub struct HttpServer { + bind_address: SocketAddr, + cancellation: CancellationToken, + router: Router, +} + +impl HttpServer { + pub fn new(bind_address: SocketAddr, state: ApiState, auth_token: String) -> Self { + HttpServer { + bind_address, + cancellation: state.cancellation_token(), + router: build_router(state, auth_token), + } + } + + pub async fn run_forever(self) -> Result<(), VpnApiError> { + let address = self.bind_address; + info!("starting the http server on http://{address}"); + + let listener = tokio::net::TcpListener::bind(address) + .await + .map_err(|source| VpnApiError::SocketBindFailure { address, source })?; + + let cancellation = self.cancellation; + + axum::serve( + listener, + self.router + .into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(async move { cancellation.cancelled().await }) + .await + .map_err(|source| VpnApiError::HttpServerFailure { source }) + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/mod.rs new file mode 100644 index 0000000000..597887fb30 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::http::state::ApiState; +use axum::Router; +use nym_credential_proxy_requests::routes; + +use crate::http::middleware::auth::AuthLayer; +pub(crate) use nym_http_api_common::{Output, OutputParams}; + +pub mod v1; + +pub(super) fn routes(auth_layer: AuthLayer) -> Router { + Router::new().nest(routes::api::V1, v1::routes(auth_layer)) +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/mod.rs new file mode 100644 index 0000000000..59ee7b4df9 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/mod.rs @@ -0,0 +1,25 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::http::middleware::auth::AuthLayer; +use crate::http::state::ApiState; +use axum::Router; +use nym_credential_proxy_requests::routes::api::v1; + +// pub mod bandwidth_voucher; +// pub mod freepass; +pub mod openapi; +pub mod ticketbook; + +pub(super) fn routes(auth_layer: AuthLayer) -> Router { + // from docs: + // ``` + // Note that the middleware is only applied to existing routes. + // So you have to first add your routes (and / or fallback) and then call layer afterwards. + // Additional routes added after layer is called will not have the middleware added. + // ``` + // thus we first add relevant API routes, then the auth layer and finally the swagger routes + Router::new() + .nest(v1::TICKETBOOK, ticketbook::routes().route_layer(auth_layer)) + .merge(openapi::route()) +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/openapi.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/openapi.rs new file mode 100644 index 0000000000..2315a658a1 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/openapi.rs @@ -0,0 +1,121 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::http::router::api; +use crate::http::types::RequestError; +use axum::Router; +use nym_credential_proxy_requests::api as api_requests; +use nym_credential_proxy_requests::routes::api::{v1, v1_absolute}; +use utoipa::openapi::security::{Http, HttpAuthScheme, SecurityScheme}; +use utoipa::{Modify, OpenApi}; +use utoipa_swagger_ui::SwaggerUi; + +/* +#[derive(OpenApi)] +#[openapi( + info(title = "Nym VPN Api"), + paths( + api::v1::freepass::generate_freepass, + api::v1::bandwidth_voucher::obtain_bandwidth_voucher_shares, + api::v1::bandwidth_voucher::obtain_async_bandwidth_voucher_shares, + api::v1::bandwidth_voucher::current_deposit, + api::v1::bandwidth_voucher::prehashed_public_attributes, + api::v1::bandwidth_voucher::partial_verification_keys, + api::v1::bandwidth_voucher::master_verification_key, + api::v1::bandwidth_voucher::current_epoch, + api::v1::bandwidth_voucher::shares::query_for_shares_by_id, + ), + components( + schemas( + api::Output, + api::OutputParams, + api_requests::v1::ErrorResponse, + api_requests::v1::freepass::models::FreepassCredentialResponse, + api_requests::v1::freepass::models::FreepassQueryParams, + api_requests::v1::bandwidth_voucher::models::DepositResponse, + api_requests::v1::bandwidth_voucher::models::AttributesResponse, + api_requests::v1::bandwidth_voucher::models::BandwidthVoucherResponse, + api_requests::v1::bandwidth_voucher::models::BandwidthVoucherAsyncResponse, + api_requests::v1::bandwidth_voucher::models::PartialVerificationKeysResponse, + api_requests::v1::bandwidth_voucher::models::CurrentEpochResponse, + api_requests::v1::bandwidth_voucher::models::CredentialShare, + api_requests::v1::bandwidth_voucher::models::PartialVerificationKey, + api_requests::v1::bandwidth_voucher::models::MasterVerificationKeyResponse, + api_requests::v1::bandwidth_voucher::models::BandwidthVoucherAsyncRequest, + api_requests::v1::bandwidth_voucher::models::BandwidthVoucherRequest, + api_requests::v1::bandwidth_voucher::models::BlindSignRequestJsonSchemaWrapper + ), + responses(RequestError), + ), + modifiers(&SecurityAddon), +)] +pub(crate) struct ApiDoc; + */ + +#[derive(OpenApi)] +#[openapi( + info(title = "Nym Credential Proxy Api"), + paths( + api::v1::ticketbook::obtain_ticketbook_shares, + api::v1::ticketbook::obtain_ticketbook_shares_async, + api::v1::ticketbook::current_deposit, + api::v1::ticketbook::partial_verification_keys, + api::v1::ticketbook::master_verification_key, + api::v1::ticketbook::current_epoch, + api::v1::ticketbook::shares::query_for_shares_by_id, + api::v1::ticketbook::shares::query_for_shares_by_device_id_and_credential_id, + ), + components( + schemas( + api::Output, + api::OutputParams, + api_requests::v1::ErrorResponse, + api_requests::v1::ticketbook::models::DepositResponse, + api_requests::v1::ticketbook::models::PartialVerificationKeysResponse, + api_requests::v1::ticketbook::models::CurrentEpochResponse, + api_requests::v1::ticketbook::models::PartialVerificationKey, + api_requests::v1::ticketbook::models::MasterVerificationKeyResponse, + api_requests::v1::ticketbook::models::TicketbookRequest, + api_requests::v1::ticketbook::models::TicketbookAsyncRequest, + api_requests::v1::ticketbook::models::WithdrawalRequestBs58Wrapper, + api_requests::v1::ticketbook::models::PartialVerificationKey, + api_requests::v1::ticketbook::models::AggregatedExpirationDateSignaturesResponse, + api_requests::v1::ticketbook::models::AggregatedCoinIndicesSignaturesResponse, + api_requests::v1::ticketbook::models::WalletShare, + api_requests::v1::ticketbook::models::TicketbookWalletSharesResponse, + api_requests::v1::ticketbook::models::TicketbookWalletSharesAsyncResponse, + api_requests::v1::ticketbook::models::WebhookTicketbookWalletShares, + api_requests::v1::ticketbook::models::WebhookTicketbookWalletSharesRequest, + api_requests::v1::ticketbook::models::TicketbookObtainQueryParams, + api_requests::v1::ticketbook::models::SharesQueryParams, + api_requests::v1::ticketbook::models::PlaceholderJsonSchemaImpl, + ), + responses(RequestError), + ), + modifiers(&SecurityAddon), +)] +pub(crate) struct ApiDoc; + +struct SecurityAddon; + +impl Modify for SecurityAddon { + fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { + if let Some(components) = openapi.components.as_mut() { + components.add_security_scheme( + "auth_token", + SecurityScheme::Http(Http::new(HttpAuthScheme::Bearer)), + ) + } + } +} + +// if reverse proxy doesn't work, we might have to look into: https://github.com/juhaku/utoipa/issues/842 +pub(crate) fn route() -> Router { + // provide absolute path to the openapi.json + let config = + utoipa_swagger_ui::Config::from(format!("{}/api-docs/openapi.json", v1_absolute())); + SwaggerUi::new(v1::SWAGGER) + .url("/api-docs/openapi.json", ApiDoc::openapi()) + .config(config) + .into() +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs new file mode 100644 index 0000000000..2dccc8d349 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/mod.rs @@ -0,0 +1,399 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::credentials::ticketbook::{ + try_obtain_blinded_ticketbook_async, try_obtain_wallet_shares, +}; +use crate::http::helpers::random_uuid; +use crate::http::state::ApiState; +use crate::http::types::RequestError; +use crate::nym_api_helpers::ensure_sane_expiration_date; +use axum::extract::{Query, State}; +use axum::http::StatusCode; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use nym_compact_ecash::Base58; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + CurrentEpochResponse, DepositResponse, MasterVerificationKeyResponse, PartialVerificationKey, + PartialVerificationKeysResponse, TicketbookAsyncRequest, TicketbookObtainQueryParams, + TicketbookRequest, TicketbookWalletSharesAsyncResponse, TicketbookWalletSharesResponse, +}; +use nym_credential_proxy_requests::routes::api::v1::ticketbook; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use time::OffsetDateTime; +use tracing::{error, info, span, warn, Level}; + +pub(crate) mod shares; + +pub type FormattedDepositResponse = FormattedResponse; +pub type FormattedCurrentEpochResponse = FormattedResponse; +pub type FormattedMasterVerificationKeyResponse = FormattedResponse; +pub type FormattedPartialVerificationKeysResponse = + FormattedResponse; +pub type FormattedTicketbookWalletSharesResponse = + FormattedResponse; +pub type FormattedTicketbookWalletSharesAsyncResponse = + FormattedResponse; + +/// Attempt to obtain blinded shares of an ecash ticketbook wallet +#[utoipa::path( + post, + path = "/obtain", + context_path = "/api/v1/ticketbook", + tag = "Ticketbook", + request_body( + content = TicketbookRequest, + description = "cryptographic material required for obtaining ticketbook wallet shares", + content_type = "application/json" + ), + responses( + (status = 200, content( + ("application/json" = TicketbookWalletSharesResponse), + ("application/yaml" = TicketbookWalletSharesResponse), + )), + (status = 400, description = "the provided request hasn't been created against correct attributes"), + (status = 401, description = "authentication token is missing or is invalid"), + (status = 422, description = "provided request was malformed"), + (status = 500, body = ErrorResponse, description = "failed to obtain a ticketbook"), + (status = 503, body = ErrorResponse, description = "ticketbooks can't be issued at this moment: the epoch transition is probably taking place"), + ), + params(TicketbookObtainQueryParams), + security( + ("auth_token" = []) + ) +)] +pub(crate) async fn obtain_ticketbook_shares( + State(state): State, + Query(params): Query, + Json(payload): Json, +) -> Result { + let uuid = random_uuid(); + let requested_on = OffsetDateTime::now_utc(); + + let span = span!(Level::INFO, "obtain ticketboook", uuid = %uuid); + let _entered = span.enter(); + info!(""); + + let output = params.output.unwrap_or_default(); + + state.ensure_not_in_epoch_transition(Some(uuid)).await?; + let epoch_id = state + .current_epoch_id() + .await + .map_err(|err| RequestError::new_server_error(err, uuid))?; + + if let Err(err) = ensure_sane_expiration_date(payload.expiration_date) { + warn!("failure due to invalid expiration date"); + return Err(RequestError::new_with_uuid( + err.to_string(), + uuid, + StatusCode::BAD_REQUEST, + )); + } + + // if additional data was requested, grab them first in case there are any cache/network issues + let ( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + ) = state + .response_global_data( + params.include_master_verification_key, + params.include_expiration_date_signatures, + params.include_coin_index_signatures, + epoch_id, + payload.expiration_date, + uuid, + ) + .await?; + + let shares = try_obtain_wallet_shares(&state, uuid, requested_on, payload) + .await + .inspect_err(|err| warn!("request failure: {err}")) + .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + + info!("request was successful!"); + Ok(output.to_response(TicketbookWalletSharesResponse { + epoch_id, + shares, + master_verification_key, + aggregated_coin_index_signatures, + aggregated_expiration_date_signatures, + })) +} + +/// Attempt to obtain blinded shares of an ecash ticketbook wallet asynchronously +#[utoipa::path( + post, + path = "/obtain-async", + context_path = "/api/v1/ticketbook", + tag = "Ticketbook", + request_body( + content = TicketbookAsyncRequest, + description = "cryptographic material required for obtaining ticketbook wallet shares", + content_type = "application/json" + ), + responses( + (status = 200, content( + ("application/json" = TicketbookWalletSharesAsyncResponse), + ("application/yaml" = TicketbookWalletSharesAsyncResponse), + )), + (status = 400, description = "the provided request hasn't been created against correct attributes"), + (status = 401, description = "authentication token is missing or is invalid"), + (status = 409, description = "shares were already requested"), + (status = 422, description = "provided request was malformed"), + (status = 500, body = ErrorResponse, description = "failed to obtain a ticketbook"), + (status = 503, body = ErrorResponse, description = "ticketbooks can't be issued at this moment: the epoch transition is probably taking place"), + ), + params(TicketbookObtainQueryParams), + security( + ("auth_token" = []) + ) +)] +pub(crate) async fn obtain_ticketbook_shares_async( + State(state): State, + Query(params): Query, + Json(payload): Json, +) -> Result { + let uuid = random_uuid(); + let requested_on = OffsetDateTime::now_utc(); + + let span = span!(Level::INFO, "[async] obtain ticketboook", uuid = %uuid); + let _entered = span.enter(); + info!(""); + + let output = params.output.unwrap_or_default(); + + // 1. perform basic validation + state.ensure_not_in_epoch_transition(Some(uuid)).await?; + + if let Err(err) = ensure_sane_expiration_date(payload.inner.expiration_date) { + warn!("failure due to invalid expiration date"); + return Err(RequestError::new_with_uuid( + err.to_string(), + uuid, + StatusCode::BAD_REQUEST, + )); + } + + // 2. store the request to retrieve the id + let pending = match state + .storage() + .insert_new_pending_async_shares_request(uuid, &payload.device_id, &payload.credential_id) + .await + { + Err(err) => { + error!("failed to insert new pending async shares: {err}"); + return Err(RequestError::new_with_uuid( + err.to_string(), + uuid, + StatusCode::CONFLICT, + )); + } + Ok(pending) => pending, + }; + let id = pending.id; + + // 3. try to spawn a new task attempting to resolve the request + if state + .try_spawn(try_obtain_blinded_ticketbook_async( + state.clone(), + uuid, + requested_on, + payload, + params, + pending, + )) + .is_none() + { + // we're going through the shutdown + return Err(RequestError::new_with_uuid( + "server shutdown in progress", + uuid, + StatusCode::INTERNAL_SERVER_ERROR, + )); + } + + // 4. in the meantime, return the id to the user + Ok(output.to_response(TicketbookWalletSharesAsyncResponse { id, uuid })) +} + +/// Obtain the current value of the bandwidth voucher deposit +#[utoipa::path( + get, + path = "/deposit-amount", + context_path = "/api/v1/ticketbook", + tag = "Ticketbook", + responses( + (status = 200, content( + ("application/json" = DepositResponse), + ("application/yaml" = DepositResponse), + )), + (status = 401, description = "authentication token is missing or is invalid"), + (status = 500, body = ErrorResponse, description = "failed to obtain current deposit information"), + ), + params(OutputParams), + security( + ("auth_token" = []) + ) +)] +pub(crate) async fn current_deposit( + Query(output): Query, + State(state): State, +) -> Result { + let output = output.output.unwrap_or_default(); + let current_deposit = state + .deposit_amount() + .await + .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + + Ok(output.to_response(DepositResponse { + current_deposit_amount: current_deposit.amount, + current_deposit_denom: current_deposit.denom, + })) +} + +/// Obtain partial verification keys of all signers for the current epoch. +#[utoipa::path( + get, + path = "/partial-verification-keys", + context_path = "/api/v1/ticketbook", + tag = "Ticketbook", + responses( + (status = 200, content( + ("application/json" = PartialVerificationKeysResponse), + ("application/yaml" = PartialVerificationKeysResponse), + )), + (status = 401, description = "authentication token is missing or is invalid"), + (status = 500, body = ErrorResponse, description = "failed to obtain current epoch information"), + (status = 503, body = ErrorResponse, description = "credentials can't be issued at this moment: the epoch transition is probably taking place"), + ), + params(OutputParams), + security( + ("auth_token" = []) + ) +)] +pub(crate) async fn partial_verification_keys( + Query(output): Query, + State(state): State, +) -> Result { + let output = output.output.unwrap_or_default(); + + state.ensure_not_in_epoch_transition(None).await?; + + let epoch_id = state + .current_epoch_id() + .await + .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + + let signers = state + .ecash_clients(epoch_id) + .await + .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + + Ok(output.to_response(PartialVerificationKeysResponse { + epoch_id, + keys: signers + .iter() + .map(|signer| PartialVerificationKey { + node_index: signer.node_id, + bs58_encoded_key: signer.verification_key.to_bs58(), + }) + .collect(), + })) +} + +/// Obtain the master verification key for the current epoch. +#[utoipa::path( + get, + path = "/master-verification-key", + context_path = "/api/v1/ticketbook", + tag = "Ticketbook", + responses( + (status = 200, content( + ("application/json" = MasterVerificationKeyResponse), + ("application/yaml" = MasterVerificationKeyResponse), + )), + (status = 401, description = "authentication token is missing or is invalid"), + (status = 500, body = ErrorResponse, description = "failed to obtain current epoch information"), + (status = 503, body = ErrorResponse, description = "credentials can't be issued at this moment: the epoch transition is probably taking place"), + ), + params(OutputParams), + security( + ("auth_token" = []) + ) +)] +pub(crate) async fn master_verification_key( + Query(output): Query, + State(state): State, +) -> Result { + let output = output.output.unwrap_or_default(); + + state.ensure_not_in_epoch_transition(None).await?; + + let epoch_id = state + .current_epoch_id() + .await + .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + + let key = state + .master_verification_key(Some(epoch_id)) + .await + .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + + Ok(output.to_response(MasterVerificationKeyResponse { + epoch_id, + bs58_encoded_key: key.to_bs58(), + })) +} + +/// Obtain the id of the current epoch. +/// This is exposed to allow clients to cache verification keys. +#[utoipa::path( + get, + path = "/current-epoch", + context_path = "/api/v1/ticketbook", + tag = "Ticketbook", + responses( + (status = 200, content( + ("application/json" = CurrentEpochResponse), + ("application/yaml" = CurrentEpochResponse), + )), + (status = 401, description = "authentication token is missing or is invalid"), + (status = 500, body = ErrorResponse, description = "failed to obtain current epoch information"), + (status = 503, body = ErrorResponse, description = "credentials can't be issued at this moment: the epoch transition is probably taking place"), + ), + params(OutputParams), + security( + ("auth_token" = []) + ) +)] +pub(crate) async fn current_epoch( + Query(output): Query, + State(state): State, +) -> Result { + let output = output.output.unwrap_or_default(); + + state.ensure_not_in_epoch_transition(None).await?; + + let epoch_id = state + .current_epoch_id() + .await + .map_err(|err| RequestError::new(err.to_string(), StatusCode::INTERNAL_SERVER_ERROR))?; + + Ok(output.to_response(CurrentEpochResponse { epoch_id })) +} + +pub(crate) fn routes() -> Router { + Router::new() + .route(ticketbook::DEPOSIT_AMOUNT, get(current_deposit)) + .route(ticketbook::MASTER_KEY, get(master_verification_key)) + .route(ticketbook::PARTIAL_KEYS, get(partial_verification_keys)) + .route(ticketbook::CURRENT_EPOCH, get(current_epoch)) + .route(ticketbook::OBTAIN, post(obtain_ticketbook_shares)) + .route( + ticketbook::OBTAIN_ASYNC, + post(obtain_ticketbook_shares_async), + ) + .nest(ticketbook::SHARES, shares::routes()) +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs new file mode 100644 index 0000000000..a42ac2c870 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/api/v1/ticketbook/shares.rs @@ -0,0 +1,206 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiError; +use crate::http::helpers::random_uuid; +use crate::http::router::api::v1::ticketbook::FormattedTicketbookWalletSharesResponse; +use crate::http::state::ApiState; +use crate::http::types::RequestError; +use crate::storage::models::MinimalWalletShare; +use axum::extract::{Path, Query, State}; +use axum::http::StatusCode; +use axum::routing::get; +use axum::Router; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + SharesQueryParams, TicketbookWalletSharesResponse, +}; +use nym_credential_proxy_requests::routes::api::v1::ticketbook::shares; +use nym_http_api_common::OutputParams; +use nym_validator_client::nym_api::EpochId; +use tracing::{debug, span, warn, Level}; +use uuid::Uuid; + +async fn shares_to_response( + state: ApiState, + uuid: Uuid, + shares: Vec, + params: SharesQueryParams, +) -> Result { + // in all calls we ensured the shares are non-empty + #[allow(clippy::unwrap_used)] + let first = shares.first().unwrap(); + let expiration_date = first.expiration_date; + let epoch_id = first.epoch_id as EpochId; + + let threshold = state.response_ecash_threshold(uuid, epoch_id).await?; + if shares.len() < threshold as usize { + return Err(RequestError::new_server_error( + VpnApiError::InsufficientNumberOfCredentials { + available: shares.len(), + threshold, + }, + uuid, + )); + } + + // grab any requested additional data + let ( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + ) = state + .response_global_data( + params.include_master_verification_key, + params.include_expiration_date_signatures, + params.include_coin_index_signatures, + epoch_id, + expiration_date, + uuid, + ) + .await?; + + // finally produce a response + Ok(params + .output + .unwrap_or_default() + .to_response(TicketbookWalletSharesResponse { + epoch_id, + shares: shares.into_iter().map(Into::into).collect(), + master_verification_key, + aggregated_coin_index_signatures, + aggregated_expiration_date_signatures, + })) +} + +/// Query by id for blinded shares of a bandwidth voucher +#[utoipa::path( + get, + path = "/{share_id}", + context_path = "/api/v1/ticketbook/shares", + tag = "Ticketbook Wallet Shares", + responses( + (status = 200, content( + ("application/json" = TicketbookWalletSharesResponse), + ("application/yaml" = TicketbookWalletSharesResponse), + )), + (status = 404, description = "share_id not found"), + (status = 401, description = "authentication token is missing or is invalid"), + (status = 500, body = ErrorResponse, description = "failed to query for bandwidth blinded shares"), + ), + params(OutputParams), + security( + ("auth_token" = []) + ) +)] +pub(crate) async fn query_for_shares_by_id( + State(state): State, + Query(params): Query, + Path(share_id): Path, +) -> Result { + let uuid = random_uuid(); + + let span = span!(Level::INFO, "query shares by id", uuid = %uuid, share_id = %share_id); + let _entered = span.enter(); + debug!(""); + + // TODO: edge case: this will **NOT** work if shares got created in epoch X, + // but this query happened in epoch X+1 + let shares = match state + .storage() + .load_wallet_shares_by_shares_id(share_id) + .await + { + Ok(shares) => { + if shares.is_empty() { + debug!("not found"); + return Err(RequestError::new_with_uuid( + format!("not found - share_id = {share_id}"), + uuid, + StatusCode::NOT_FOUND, + )); + } + shares + } + Err(err) => { + warn!("db failure: {err}"); + return Err(RequestError::new_with_uuid( + format!("oh no, something went wrong {err}"), + uuid, + StatusCode::INTERNAL_SERVER_ERROR, + )); + } + }; + + shares_to_response(state, uuid, shares, params).await +} + +/// Query by id for blinded wallet shares of a ticketbook +#[utoipa::path( + get, + path = "/device/{device_id}/credential/{credential_id}", + context_path = "/api/v1/ticketbook/shares", + tag = "Ticketbook Wallet Shares", + responses( + (status = 200, content( + ("application/json" = TicketbookWalletSharesResponse), + ("application/yaml" = TicketbookWalletSharesResponse), + )), + (status = 404, description = "share_id not found"), + (status = 401, description = "authentication token is missing or is invalid"), + (status = 500, body = ErrorResponse, description = "failed to query for bandwidth blinded shares"), + ), + params(SharesQueryParams), + security( + ("auth_token" = []) + ) +)] +pub(crate) async fn query_for_shares_by_device_id_and_credential_id( + State(state): State, + Query(params): Query, + Path((device_id, credential_id)): Path<(String, String)>, +) -> Result { + let uuid = random_uuid(); + + let span = span!(Level::INFO, "query shares by device and credential ids", uuid = %uuid, device_id = %device_id, credential_id = %credential_id); + let _entered = span.enter(); + debug!(""); + + // TODO: edge case: this will **NOT** work if shares got created in epoch X, + // but this query happened in epoch X+1 + let shares = match state + .storage() + .load_wallet_shares_by_device_and_credential_id(&device_id, &credential_id) + .await + { + Ok(shares) => { + if shares.is_empty() { + debug!("not found"); + return Err(RequestError::new_with_uuid( + format!("not found - device_id = {device_id}, credential_id = {credential_id}"), + uuid, + StatusCode::NOT_FOUND, + )); + } + shares + } + Err(err) => { + warn!("db failure: {err}"); + return Err(RequestError::new_with_uuid( + format!("oh no, something went wrong {err}"), + uuid, + StatusCode::INTERNAL_SERVER_ERROR, + )); + } + }; + + shares_to_response(state, uuid, shares, params).await +} + +pub(crate) fn routes() -> Router { + Router::new() + .route(shares::SHARE_BY_ID, get(query_for_shares_by_id)) + .route( + shares::SHARE_BY_DEVICE_AND_CREDENTIAL_ID, + get(query_for_shares_by_device_id_and_credential_id), + ) +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs new file mode 100644 index 0000000000..fd0ecf267a --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/http/router/mod.rs @@ -0,0 +1,44 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::http::middleware::auth::AuthLayer; +use crate::http::middleware::logging; +use crate::http::state::ApiState; +use axum::response::Redirect; +use axum::routing::{get, MethodRouter}; +use axum::Router; +use nym_credential_proxy_requests::routes; +use std::sync::Arc; +use zeroize::Zeroizing; + +pub mod api; + +fn swagger_redirect() -> MethodRouter { + // redirects with 303 status code + get(|| async { Redirect::to("/api/v1/swagger/") }) +} + +pub fn build_router(state: ApiState, auth_token: String) -> Router { + // let auth_layer = from_extractor::(); + let auth_middleware = AuthLayer::new(Arc::new(Zeroizing::new(auth_token))); + + let router = Router::new() + // just redirect root and common typos for swagger for the current api version page (v1) + .route("/", swagger_redirect()) + .route("/swagger", swagger_redirect()) + .route("/swagger/", swagger_redirect()) + .route("/swagger/index.html", swagger_redirect()) + .nest(routes::API, api::routes(auth_middleware)) + // we don't have to be using middleware, but we already had that code + // we might want something like: https://github.com/tokio-rs/axum/blob/main/examples/tracing-aka-logging/src/main.rs#L44 instead + .layer(axum::middleware::from_fn(logging::logger)) + .with_state(state); + + cfg_if::cfg_if! { + if #[cfg(feature = "cors")] { + router.layer(tower_http::cors::CorsLayer::very_permissive()) + } else { + router + } + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs new file mode 100644 index 0000000000..c4c4a8ebe3 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/http/state/mod.rs @@ -0,0 +1,683 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiError; +use crate::helpers::LockTimer; +use crate::http::types::RequestError; +use crate::nym_api_helpers::{ + ensure_sane_expiration_date, query_all_threshold_apis, CachedEpoch, CachedImmutableEpochItem, + CachedImmutableItems, +}; +use crate::storage::VpnApiStorage; +use crate::webhook::ZkNymWebHookConfig; +use axum::http::StatusCode; +use bip39::Mnemonic; +use nym_compact_ecash::scheme::coin_indices_signatures::{ + aggregate_annotated_indices_signatures, CoinIndexSignatureShare, +}; +use nym_compact_ecash::scheme::expiration_date_signatures::{ + aggregate_annotated_expiration_signatures, ExpirationDateSignatureShare, +}; +use nym_compact_ecash::Base58; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + AggregatedCoinIndicesSignaturesResponse, AggregatedExpirationDateSignaturesResponse, + MasterVerificationKeyResponse, +}; +use nym_credentials::ecash::utils::{ecash_today, EcashTime}; +use nym_credentials::{ + AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, +}; +use nym_credentials_interface::VerificationKeyAuth; +use nym_validator_client::coconut::EcashApiError; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch; +use nym_validator_client::nyxd::contract_traits::{ + DkgQueryClient, EcashQueryClient, NymContractsProvider, PagedDkgQueryClient, +}; +use nym_validator_client::nyxd::{Coin, NyxdClient}; +use nym_validator_client::{nyxd, DirectSigningHttpRpcNyxdClient, EcashApiClient}; +use std::future::Future; +use std::ops::Deref; +use std::sync::Arc; +use time::{Date, OffsetDateTime}; +use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; +use tokio_util::task::TaskTracker; +use tracing::{debug, info, warn}; +use uuid::Uuid; + +// currently we need to hold our keypair so that we could request a freepass credential +#[derive(Clone)] +pub struct ApiState { + inner: Arc, +} + +// a lot of functionalities, mostly to do with caching and storage is just copy-pasted from nym-api, +// since we have to do more or less the same work +impl ApiState { + pub async fn new( + storage: VpnApiStorage, + zk_nym_web_hook_config: ZkNymWebHookConfig, + mnemonic: Mnemonic, + ) -> Result { + let network_details = nym_network_defaults::NymNetworkDetails::new_from_env(); + let client_config = nyxd::Config::try_from_nym_network_details(&network_details)?; + + let nyxd_url = network_details + .endpoints + .first() + .ok_or_else(|| VpnApiError::NoNyxEndpointsAvailable)? + .nyxd_url + .as_str(); + + let client = NyxdClient::connect_with_mnemonic(client_config, nyxd_url, mnemonic)?; + + if client.ecash_contract_address().is_none() { + return Err(VpnApiError::UnavailableEcashContract); + } + + if client.dkg_contract_address().is_none() { + return Err(VpnApiError::UnavailableDKGContract); + } + + let state = ApiState { + inner: Arc::new(ApiStateInner { + storage, + client: RwLock::new(client), + ecash_state: EcashState::default(), + zk_nym_web_hook_config, + task_tracker: TaskTracker::new(), + cancellation_token: CancellationToken::new(), + }), + }; + + // since this is startup, + // might as well do all the needed network queries to establish needed global signatures + // if we don't already have them + state.build_initial_cache().await?; + + Ok(state) + } + + async fn build_initial_cache(&self) -> Result<(), VpnApiError> { + let today = ecash_today().date(); + + let epoch_id = self.current_epoch_id().await?; + let _ = self.deposit_amount().await?; + let _ = self.master_verification_key(Some(epoch_id)).await?; + let _ = self.ecash_threshold(epoch_id).await?; + let _ = self.ecash_clients(epoch_id).await?; + let _ = self.master_coin_index_signatures(Some(epoch_id)).await?; + let _ = self.master_expiration_date_signatures(today).await?; + + Ok(()) + } + + pub(crate) fn try_spawn(&self, task: F) -> Option> + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + // don't spawn new task if we've received cancellation token + if self.inner.cancellation_token.is_cancelled() { + None + } else { + self.inner.task_tracker.reopen(); + // TODO: later use a task queue since most requests will be blocked waiting on chain permit anyway + let join_handle = self.inner.task_tracker.spawn(task); + self.inner.task_tracker.close(); + Some(join_handle) + } + } + + pub(crate) async fn cancel_and_wait(&self) { + self.inner.cancellation_token.cancel(); + self.inner.task_tracker.wait().await + } + + pub(crate) fn cancellation_token(&self) -> CancellationToken { + self.inner.cancellation_token.clone() + } + + pub(crate) fn zk_nym_web_hook(&self) -> &ZkNymWebHookConfig { + &self.inner.zk_nym_web_hook_config + } + + async fn ensure_credentials_issuable(&self) -> Result<(), VpnApiError> { + let epoch = self.current_epoch().await?; + + if epoch.state.is_final() { + Ok(()) + } else if let Some(final_timestamp) = epoch.final_timestamp_secs() { + // SAFETY: the timestamp values in our DKG contract should be valid timestamps, + // otherwise it means the chain is seriously misbehaving + #[allow(clippy::unwrap_used)] + let finish_dt = OffsetDateTime::from_unix_timestamp(final_timestamp as i64).unwrap(); + + Err(VpnApiError::CredentialsNotYetIssuable { + availability: finish_dt, + }) + } else if epoch.state.is_waiting_initialisation() { + return Err(VpnApiError::UninitialisedDkg); + } else { + Err(VpnApiError::UnknownEcashFailure) + } + } + + pub(crate) fn storage(&self) -> &VpnApiStorage { + &self.inner.storage + } + + pub async fn deposit_amount(&self) -> Result { + let read_guard = self.inner.ecash_state.required_deposit_cache.read().await; + if read_guard.is_valid() { + return Ok(read_guard.required_amount.clone()); + } + + // update cache + drop(read_guard); + let mut write_guard = self.inner.ecash_state.required_deposit_cache.write().await; + let deposit_amount = self + .query_chain() + .await + .get_required_deposit_amount() + .await?; + + write_guard.update(deposit_amount.clone().into()); + + Ok(deposit_amount.into()) + } + + async fn current_epoch(&self) -> Result { + let read_guard = self.inner.ecash_state.cached_epoch.read().await; + if read_guard.is_valid() { + return Ok(read_guard.current_epoch); + } + + // update cache + drop(read_guard); + let mut write_guard = self.inner.ecash_state.cached_epoch.write().await; + let epoch = self.query_chain().await.get_current_epoch().await?; + + write_guard.update(epoch); + Ok(epoch) + } + + pub async fn current_epoch_id(&self) -> Result { + let read_guard = self.inner.ecash_state.cached_epoch.read().await; + if read_guard.is_valid() { + return Ok(read_guard.current_epoch.epoch_id); + } + + // update cache + drop(read_guard); + let mut write_guard = self.inner.ecash_state.cached_epoch.write().await; + let epoch = self.query_chain().await.get_current_epoch().await?; + + write_guard.update(epoch); + Ok(epoch.epoch_id) + } + + pub(crate) async fn query_chain(&self) -> RwLockReadGuard { + let _acquire_timer = LockTimer::new("acquire chain query permit"); + self.inner.client.read().await + } + + pub(crate) async fn start_chain_tx(&self) -> ChainWritePermit { + let _acquire_timer = LockTimer::new("acquire exclusive chain write permit"); + + ChainWritePermit { + lock_timer: LockTimer::new("exclusive chain access permit"), + inner: self.inner.client.write().await, + } + } + + pub(crate) async fn global_data( + &self, + include_master_verification_key: bool, + include_expiration_date_signatures: bool, + include_coin_index_signatures: bool, + epoch_id: EpochId, + expiration_date: Date, + ) -> Result< + ( + Option, + Option, + Option, + ), + VpnApiError, + > { + let master_verification_key = if include_master_verification_key { + debug!("including master verification key in the response"); + Some( + self.master_verification_key(Some(epoch_id)) + .await + .map(|key| MasterVerificationKeyResponse { + epoch_id, + bs58_encoded_key: key.to_bs58(), + }) + .inspect_err(|err| warn!("request failure: {err}"))?, + ) + } else { + None + }; + + let aggregated_expiration_date_signatures = if include_expiration_date_signatures { + debug!("including expiration date signatures in the response"); + Some( + self.master_expiration_date_signatures(expiration_date) + .await + .map(|signatures| AggregatedExpirationDateSignaturesResponse { + signatures: signatures.clone(), + }) + .inspect_err(|err| warn!("request failure: {err}"))?, + ) + } else { + None + }; + + let aggregated_coin_index_signatures = if include_coin_index_signatures { + debug!("including coin index signatures in the response"); + Some( + self.master_coin_index_signatures(Some(epoch_id)) + .await + .map(|signatures| AggregatedCoinIndicesSignaturesResponse { + signatures: signatures.clone(), + }) + .inspect_err(|err| warn!("request failure: {err}"))?, + ) + } else { + None + }; + + Ok(( + master_verification_key, + aggregated_expiration_date_signatures, + aggregated_coin_index_signatures, + )) + } + + pub(crate) async fn response_global_data( + &self, + include_master_verification_key: bool, + include_expiration_date_signatures: bool, + include_coin_index_signatures: bool, + epoch_id: EpochId, + expiration_date: Date, + uuid: Uuid, + ) -> Result< + ( + Option, + Option, + Option, + ), + RequestError, + > { + self.global_data( + include_master_verification_key, + include_expiration_date_signatures, + include_coin_index_signatures, + epoch_id, + expiration_date, + ) + .await + .map_err(|err| RequestError::new_server_error(err, uuid)) + } + + pub async fn ensure_not_in_epoch_transition( + &self, + uuid: Option, + ) -> Result<(), RequestError> { + if let Err(err) = self.ensure_credentials_issuable().await { + return if let Some(uuid) = uuid { + Err(RequestError::new_with_uuid( + err.to_string(), + uuid, + StatusCode::SERVICE_UNAVAILABLE, + )) + } else { + Err(RequestError::new( + err.to_string(), + StatusCode::SERVICE_UNAVAILABLE, + )) + }; + } + Ok(()) + } + + pub(crate) async fn ecash_clients( + &self, + epoch_id: EpochId, + ) -> Result>, VpnApiError> { + self.inner + .ecash_state + .epoch_clients + .get_or_init(epoch_id, || async { + Ok(self + .query_chain() + .await + .get_all_verification_key_shares(epoch_id) + .await? + .into_iter() + .map(TryInto::try_into) + .collect::, EcashApiError>>()?) + }) + .await + } + + pub(crate) async fn ecash_threshold(&self, epoch_id: EpochId) -> Result { + self.inner + .ecash_state + .threshold_values + .get_or_init(epoch_id, || async { + if let Some(threshold) = self + .query_chain() + .await + .get_epoch_threshold(epoch_id) + .await? + { + Ok(threshold) + } else { + Err(VpnApiError::UnavailableThreshold { epoch_id }) + } + }) + .await + .map(|t| *t) + } + + pub(crate) async fn response_ecash_threshold( + &self, + uuid: Uuid, + epoch_id: EpochId, + ) -> Result { + self.ecash_threshold(epoch_id) + .await + .map_err(|err| RequestError::new_server_error(err, uuid)) + } + + pub(crate) async fn master_verification_key( + &self, + epoch_id: Option, + ) -> Result, VpnApiError> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.current_epoch_id().await?, + }; + + self.inner + .ecash_state + .master_verification_key + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(stored) = self + .inner + .storage + .get_master_verification_key(epoch_id) + .await? + { + return Ok(stored.key); + } + + info!("attempting to establish master verification key for epoch {epoch_id}..."); + + // 2. perform actual aggregation + let all_apis = self.ecash_clients(epoch_id).await?; + let threshold = self.ecash_threshold(epoch_id).await?; + + if all_apis.len() < threshold as usize { + return Err(VpnApiError::InsufficientNumberOfSigners { + threshold, + available: all_apis.len(), + }); + } + + let master_key = nym_credentials::aggregate_verification_keys(&all_apis)?; + + let epoch = EpochVerificationKey { + epoch_id, + key: master_key, + }; + + // 3. save the key in the storage for when we reboot + self.inner + .storage + .insert_master_verification_key(&epoch) + .await?; + + Ok(epoch.key) + }) + .await + } + + pub(crate) async fn master_coin_index_signatures( + &self, + epoch_id: Option, + ) -> Result, VpnApiError> { + let epoch_id = match epoch_id { + Some(id) => id, + None => self.current_epoch_id().await?, + }; + + self.inner + .ecash_state + .coin_index_signatures + .get_or_init(epoch_id, || async { + // 1. check the storage + if let Some(master_sigs) = self + .inner + .storage + .get_master_coin_index_signatures(epoch_id) + .await? + { + return Ok(master_sigs); + } + + info!( + "attempting to establish master coin index signatures for epoch {epoch_id}..." + ); + + // 2. go around APIs and attempt to aggregate the data + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let all_apis = self.ecash_clients(epoch_id).await?; + let threshold = self.ecash_threshold(epoch_id).await?; + + let get_partial_signatures = |api: EcashApiClient| async { + // move the api into the closure + let api = api; + let node_index = api.node_id; + let partial_vk = api.verification_key; + + let partial = api + .api_client + .partial_coin_indices_signatures(Some(epoch_id)) + .await? + .signatures; + Ok(CoinIndexSignatureShare { + index: node_index, + key: partial_vk, + signatures: partial, + }) + }; + + let shares = + query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures) + .await?; + + let aggregated = aggregate_annotated_indices_signatures( + nym_credentials_interface::ecash_parameters(), + &master_vk, + &shares, + )?; + + let sigs = AggregatedCoinIndicesSignatures { + epoch_id, + signatures: aggregated, + }; + + // 3. save the signatures in the storage for when we reboot + self.inner + .storage + .insert_master_coin_index_signatures(&sigs) + .await?; + + Ok(sigs) + }) + .await + } + + pub(crate) async fn master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, VpnApiError> { + self.inner + .ecash_state + .expiration_date_signatures + .get_or_init(expiration_date, || async { + // 1. sanity check to see if the expiration_date is not nonsense + ensure_sane_expiration_date(expiration_date)?; + + // 2. check the storage + if let Some(master_sigs) = self + .inner + .storage + .get_master_expiration_date_signatures(expiration_date) + .await? + { + return Ok(master_sigs); + } + + + info!( + "attempting to establish master expiration date signatures for {expiration_date}..." + ); + + // 3. go around APIs and attempt to aggregate the data + let epoch_id = self.current_epoch_id().await?; + let master_vk = self.master_verification_key(Some(epoch_id)).await?; + let all_apis = self.ecash_clients(epoch_id).await?; + let threshold = self.ecash_threshold(epoch_id).await?; + + let get_partial_signatures = |api: EcashApiClient| async { + // move the api into the closure + let api = api; + let node_index = api.node_id; + let partial_vk = api.verification_key; + + let partial = api + .api_client + .partial_expiration_date_signatures(Some(expiration_date)) + .await? + .signatures; + Ok(ExpirationDateSignatureShare { + index: node_index, + key: partial_vk, + signatures: partial, + }) + }; + + let shares = + query_all_threshold_apis(all_apis.clone(), threshold, get_partial_signatures) + .await?; + + let aggregated = aggregate_annotated_expiration_signatures( + &master_vk, + expiration_date.ecash_unix_timestamp(), + &shares, + )?; + + let sigs = AggregatedExpirationDateSignatures { + epoch_id, + expiration_date, + signatures: aggregated, + }; + + // 4. save the signatures in the storage for when we reboot + self.inner + .storage + .insert_master_expiration_date_signatures(&sigs) + .await?; + + Ok(sigs) + }) + .await + } +} + +struct ApiStateInner { + storage: VpnApiStorage, + + client: RwLock, + + zk_nym_web_hook_config: ZkNymWebHookConfig, + + ecash_state: EcashState, + + task_tracker: TaskTracker, + + cancellation_token: CancellationToken, +} + +pub(crate) struct CachedDeposit { + valid_until: OffsetDateTime, + required_amount: Coin, +} + +impl CachedDeposit { + const MAX_VALIDITY: time::Duration = time::Duration::MINUTE; + + fn is_valid(&self) -> bool { + self.valid_until > OffsetDateTime::now_utc() + } + + fn update(&mut self, required_amount: Coin) { + self.valid_until = OffsetDateTime::now_utc() + Self::MAX_VALIDITY; + self.required_amount = required_amount; + } +} + +impl Default for CachedDeposit { + fn default() -> Self { + CachedDeposit { + valid_until: OffsetDateTime::UNIX_EPOCH, + required_amount: Coin { + amount: u128::MAX, + denom: "unym".to_string(), + }, + } + } +} + +#[derive(Default)] +pub(crate) struct EcashState { + pub(crate) required_deposit_cache: RwLock, + + pub(crate) cached_epoch: RwLock, + + pub(crate) master_verification_key: CachedImmutableEpochItem, + + pub(crate) threshold_values: CachedImmutableEpochItem, + + pub(crate) epoch_clients: CachedImmutableEpochItem>, + + pub(crate) coin_index_signatures: CachedImmutableEpochItem, + + pub(crate) expiration_date_signatures: + CachedImmutableItems, +} + +// explicitly wrap the WriteGuard for extra information regarding time taken +pub(crate) struct ChainWritePermit<'a> { + // it's not really dead, we only care about it being dropped + #[allow(dead_code)] + lock_timer: LockTimer, + inner: RwLockWriteGuard<'a, DirectSigningHttpRpcNyxdClient>, +} + +impl<'a> Deref for ChainWritePermit<'a> { + type Target = DirectSigningHttpRpcNyxdClient; + + fn deref(&self) -> &Self::Target { + self.inner.deref() + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/http/types.rs b/nym-credential-proxy/nym-credential-proxy/src/http/types.rs new file mode 100644 index 0000000000..8602103456 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/http/types.rs @@ -0,0 +1,64 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiError; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use axum::Json; +use nym_credential_proxy_requests::api::v1::ErrorResponse; +use utoipa::ToResponse; +use uuid::Uuid; + +#[derive(Debug, Clone, ToResponse)] +#[response(description = "Error response with additional message")] +pub struct RequestError { + pub inner: ErrorResponse, + + pub status: StatusCode, +} + +impl RequestError { + pub fn new>(message: S, status: StatusCode) -> Self { + RequestError { + inner: ErrorResponse { + uuid: None, + message: message.into(), + }, + status, + } + } + + pub fn new_status(status: StatusCode) -> Self { + RequestError { + inner: ErrorResponse { + uuid: None, + message: String::new(), + }, + status, + } + } + + pub fn new_server_error(err: VpnApiError, uuid: Uuid) -> Self { + RequestError::new_with_uuid(err.to_string(), uuid, StatusCode::INTERNAL_SERVER_ERROR) + } + + pub fn new_with_uuid>(message: S, uuid: Uuid, status: StatusCode) -> Self { + RequestError { + inner: ErrorResponse { + uuid: Some(uuid), + message: message.into(), + }, + status, + } + } + + pub fn from_err(err: E, status: StatusCode) -> Self { + Self::new(err.to_string(), status) + } +} + +impl IntoResponse for RequestError { + fn into_response(self) -> Response { + (self.status, Json(self.inner)).into_response() + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/main.rs b/nym-credential-proxy/nym-credential-proxy/src/main.rs new file mode 100644 index 0000000000..b534f442c8 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/main.rs @@ -0,0 +1,95 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +#![warn(clippy::expect_used)] +#![warn(clippy::unwrap_used)] +#![warn(clippy::todo)] +#![warn(clippy::dbg_macro)] + +use crate::cli::Cli; +use crate::error::VpnApiError; +use crate::http::state::ApiState; +use crate::http::HttpServer; +use crate::storage::VpnApiStorage; +use crate::tasks::StoragePruner; +use clap::Parser; +use nym_bin_common::logging::setup_tracing_logger; +use nym_network_defaults::setup_env; +use tracing::{info, trace}; + +pub mod cli; +pub mod config; +pub mod credentials; +pub mod error; +pub mod helpers; +pub mod http; +pub mod nym_api_helpers; +pub mod storage; +pub mod tasks; +mod webhook; + +pub async fn wait_for_signal() { + use tokio::signal::unix::{signal, SignalKind}; + + // if we fail to setup the signals, we should just blow up + #[allow(clippy::expect_used)] + let mut sigterm = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM channel"); + #[allow(clippy::expect_used)] + let mut sigquit = signal(SignalKind::quit()).expect("Failed to setup SIGQUIT channel"); + + tokio::select! { + _ = tokio::signal::ctrl_c() => { + info!("Received SIGINT"); + }, + _ = sigterm.recv() => { + info!("Received SIGTERM"); + } + _ = sigquit.recv() => { + info!("Received SIGQUIT"); + } + } +} + +async fn run_api(cli: Cli) -> Result<(), VpnApiError> { + // create the tasks + let bind_address = cli.bind_address(); + + let storage = VpnApiStorage::init(cli.persistent_storage_path()).await?; + let mnemonic = cli.mnemonic; + let auth_token = cli.http_auth_token; + let webhook_cfg = cli.webhook; + let api_state = ApiState::new(storage.clone(), webhook_cfg, mnemonic).await?; + let http_server = HttpServer::new(bind_address, api_state.clone(), auth_token); + + let storage_pruner = StoragePruner::new(api_state.cancellation_token(), storage); + + // spawn all the tasks + api_state.try_spawn(http_server.run_forever()); + api_state.try_spawn(storage_pruner.run_forever()); + + // wait for cancel signal (SIGINT, SIGTERM or SIGQUIT) + wait_for_signal().await; + + // cancel all the tasks and wait for all task to terminate + api_state.cancel_and_wait().await; + + Ok(()) +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + std::env::set_var( + "RUST_LOG", + "trace,handlebars=warn,tendermint_rpc=warn,h2=warn,hyper=warn,rustls=warn,reqwest=warn,tungstenite=warn,async_tungstenite=warn,tokio_util=warn,tokio_tungstenite=warn,tokio-util=warn,nym_validator_client=info", + ); + + let cli = Cli::parse(); + cli.webhook.ensure_valid_client_url()?; + trace!("args: {cli:#?}"); + + setup_env(cli.config_env_file.as_ref()); + setup_tracing_logger(); + + run_api(cli).await?; + Ok(()) +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/nym_api_helpers.rs b/nym-credential-proxy/nym-credential-proxy/src/nym_api_helpers.rs new file mode 100644 index 0000000000..c862cf8047 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/nym_api_helpers.rs @@ -0,0 +1,181 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// TODO: this was just copied from nym-api; +// it should have been therefore extracted to a common crate instead and imported as dependency + +use crate::error::VpnApiError; +use futures::{stream, StreamExt}; +use nym_credentials::ecash::utils::{cred_exp_date, ecash_today}; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::dkg_query_client::Epoch; +use nym_validator_client::EcashApiClient; +use std::cmp::min; +use std::collections::HashMap; +use std::future::Future; +use std::hash::Hash; +use std::ops::Deref; +use time::{Date, OffsetDateTime}; +use tokio::sync::{Mutex, RwLock, RwLockReadGuard}; +use tracing::warn; + +pub(crate) struct CachedEpoch { + valid_until: OffsetDateTime, + pub(crate) current_epoch: Epoch, +} + +impl Default for CachedEpoch { + fn default() -> Self { + CachedEpoch { + valid_until: OffsetDateTime::UNIX_EPOCH, + current_epoch: Epoch::default(), + } + } +} + +impl CachedEpoch { + pub(crate) fn is_valid(&self) -> bool { + self.valid_until > OffsetDateTime::now_utc() + } + + pub(crate) fn update(&mut self, epoch: Epoch) { + let now = OffsetDateTime::now_utc(); + + let validity_duration = if let Some(epoch_finish) = epoch.deadline { + #[allow(clippy::unwrap_used)] + let state_end = + OffsetDateTime::from_unix_timestamp(epoch_finish.seconds() as i64).unwrap(); + let until_epoch_state_end = state_end - now; + // make it valid until the next epoch transition or next 5min, whichever is smaller + min(until_epoch_state_end, 5 * time::Duration::MINUTE) + } else { + 5 * time::Duration::MINUTE + }; + + self.valid_until = now + validity_duration; + self.current_epoch = epoch; + } +} + +// a map of items that never change for given key +pub(crate) struct CachedImmutableItems { + // I wonder if there's a more efficient structure with OnceLock or OnceCell or something + inner: RwLock>, +} + +// an item that stays constant throughout given epoch +pub(crate) type CachedImmutableEpochItem = CachedImmutableItems; + +impl Default for CachedImmutableItems { + fn default() -> Self { + CachedImmutableItems { + inner: RwLock::new(HashMap::new()), + } + } +} + +impl Deref for CachedImmutableItems { + type Target = RwLock>; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl CachedImmutableItems +where + K: Eq + Hash, +{ + pub(crate) async fn get_or_init(&self, key: K, f: F) -> Result, E> + where + F: FnOnce() -> U, + U: Future>, + K: Clone, + { + // 1. see if we already have the item cached + let guard = self.inner.read().await; + if let Ok(item) = RwLockReadGuard::try_map(guard, |map| map.get(&key)) { + return Ok(item); + } + + // 2. attempt to retrieve (and cache) it + let mut write_guard = self.inner.write().await; + + // see if another task has already set the item whilst we were waiting for the lock + if write_guard.get(&key).is_some() { + let read_guard = write_guard.downgrade(); + + // SAFETY: we just checked the entry exists and we never dropped the guard + #[allow(clippy::unwrap_used)] + return Ok(RwLockReadGuard::map(read_guard, |map| { + map.get(&key).unwrap() + })); + } + + let init = f().await?; + write_guard.insert(key.clone(), init); + + let guard = write_guard.downgrade(); + + // SAFETY: + // we just inserted the entry into the map while NEVER dropping the lock (only downgraded it) + // so it MUST exist and thus the unwrap is fine + #[allow(clippy::unwrap_used)] + Ok(RwLockReadGuard::map(guard, |map| map.get(&key).unwrap())) + } +} + +pub(crate) fn ensure_sane_expiration_date(expiration_date: Date) -> Result<(), VpnApiError> { + let today = ecash_today(); + + if expiration_date < today.date() { + // what's the point of signatures with expiration in the past? + return Err(VpnApiError::ExpirationDateTooEarly); + } + + // SAFETY: we're nowhere near MAX date + #[allow(clippy::unwrap_used)] + if expiration_date > cred_exp_date().date().next_day().unwrap() { + // don't allow issuing signatures too far in advance (1 day beyond current value is fine) + return Err(VpnApiError::ExpirationDateTooLate); + } + + Ok(()) +} + +pub(crate) async fn query_all_threshold_apis( + all_apis: Vec, + threshold: u64, + f: F, +) -> Result, VpnApiError> +where + F: Fn(EcashApiClient) -> U, + U: Future>, +{ + let shares = Mutex::new(Vec::with_capacity(all_apis.len())); + + stream::iter(all_apis) + .for_each_concurrent(8, |api| async { + // can't be bothered to restructure the code to appease the borrow checker properly, + // so just assign this to a variable + let disp = api.to_string(); + match f(api).await { + Ok(partial_share) => shares.lock().await.push(partial_share), + Err(err) => { + warn!("failed to obtain partial threshold data from API: {disp}: {err}") + } + } + }) + .await; + + let shares = shares.into_inner(); + + if shares.len() < threshold as usize { + return Err(VpnApiError::InsufficientNumberOfSigners { + threshold, + available: shares.len(), + }); + } + + Ok(shares) +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs new file mode 100644 index 0000000000..68990e5ba5 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/storage/manager.rs @@ -0,0 +1,417 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiError; +use crate::storage::models::{ + BlindedShares, BlindedSharesStatus, MinimalWalletShare, RawCoinIndexSignatures, + RawExpirationDateSignatures, RawVerificationKey, +}; +use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; +use time::{Date, OffsetDateTime}; + +#[derive(Clone)] +pub(crate) struct SqliteStorageManager { + pub(crate) connection_pool: sqlx::SqlitePool, +} + +impl SqliteStorageManager { + pub(crate) async fn load_blinded_shares_status_by_shares_id( + &self, + id: i64, + ) -> Result, sqlx::Error> { + let res = sqlx::query_as( + r#" + SELECT * + FROM blinded_shares + WHERE id = ?; + "#, + ) + .bind(id) + .fetch_optional(&self.connection_pool) + .await?; + + Ok(res) + } + + pub(crate) async fn load_wallet_shares_by_shares_id( + &self, + id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + MinimalWalletShare, + r#" + SELECT t1.node_id, t1.blinded_signature, t1.epoch_id, t1.expiration_date + FROM partial_blinded_wallet as t1 + JOIN ticketbook_deposit as t2 + on t1.corresponding_deposit = t2.deposit_id + JOIN blinded_shares as t3 + ON t2.request_uuid = t3.request_uuid + WHERE t3.id = ?; + "#, + id + ) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn load_blinded_shares_status_by_device_and_credential_id( + &self, + device_id: &str, + credential_id: &str, + ) -> Result, sqlx::Error> { + let res = sqlx::query_as( + r#" + SELECT * + FROM blinded_shares + WHERE device_id = ? AND credential_id = ?; + "#, + ) + .bind(device_id) + .bind(credential_id) + .fetch_optional(&self.connection_pool) + .await?; + + Ok(res) + } + + pub(crate) async fn load_wallet_shares_by_device_and_credential_id( + &self, + device_id: &str, + credential_id: &str, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + MinimalWalletShare, + r#" + SELECT + t1.node_id as "node_id!", + t1.blinded_signature as "blinded_signature!", + t1.epoch_id as "epoch_id!", + t1.expiration_date as "expiration_date!" + FROM partial_blinded_wallet as t1 + JOIN ticketbook_deposit as t2 + on t1.corresponding_deposit = t2.deposit_id + JOIN blinded_shares as t3 + ON t2.request_uuid = t3.request_uuid + WHERE t3.device_id = ? AND t3.credential_id = ?; + "#, + device_id, + credential_id + ) + .fetch_all(&self.connection_pool) + .await + } + + pub(crate) async fn insert_new_pending_async_shares_request( + &self, + request: String, + device_id: &str, + credential_id: &str, + ) -> Result { + let now = OffsetDateTime::now_utc(); + let res = sqlx::query_as( + r#" + INSERT INTO blinded_shares (status, request_uuid, device_id, credential_id, created, updated) + VALUES (?, ?, ?, ?, ?, ?) + RETURNING * + "#, + ) + .bind(BlindedSharesStatus::Pending) + .bind(request) + .bind(device_id) + .bind(credential_id) + .bind(now) + .bind(now) + .fetch_one(&self.connection_pool) + .await?; + + Ok(res) + } + + pub(crate) async fn update_pending_async_blinded_shares_issued( + &self, + available_shares: i64, + device_id: &str, + credential_id: &str, + ) -> Result { + let now = OffsetDateTime::now_utc(); + let res = sqlx::query_as( + r#" + UPDATE blinded_shares + SET status = ?, updated = ?, error_message = NULL, available_shares = ? + WHERE device_id = ? AND credential_id = ? + RETURNING *; + "#, + ) + .bind(BlindedSharesStatus::Issued) + .bind(now) + .bind(available_shares) + .bind(device_id) + .bind(credential_id) + .fetch_one(&self.connection_pool) + .await?; + + Ok(res) + } + + pub(crate) async fn update_pending_async_blinded_shares_error( + &self, + available_shares: i64, + device_id: &str, + credential_id: &str, + error: &str, + ) -> Result { + let now = time::OffsetDateTime::now_utc(); + let res = sqlx::query_as( + r#" + UPDATE blinded_shares + SET status = ?, error_message = ?, updated = ?, available_shares = ? + WHERE device_id = ? AND credential_id = ? + RETURNING *; + "#, + ) + .bind(BlindedSharesStatus::Error) + .bind(error) + .bind(now) + .bind(available_shares) + .bind(device_id) + .bind(credential_id) + .fetch_one(&self.connection_pool) + .await?; + + Ok(res) + } + + pub(crate) async fn prune_old_blinded_shares( + &self, + delete_after: OffsetDateTime, + ) -> Result<(), VpnApiError> { + sqlx::query!( + r#" + DELETE FROM blinded_shares WHERE created < ? + "#, + delete_after, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn prune_old_partial_blinded_wallets( + &self, + delete_after: OffsetDateTime, + ) -> Result<(), VpnApiError> { + sqlx::query!( + r#" + DELETE FROM partial_blinded_wallet WHERE created < ? + "#, + delete_after, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn prune_old_partial_blinded_wallet_failures( + &self, + delete_after: OffsetDateTime, + ) -> Result<(), VpnApiError> { + sqlx::query!( + r#" + DELETE FROM partial_blinded_wallet_failure WHERE created < ? + "#, + delete_after, + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_master_verification_key( + &self, + epoch_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + RawVerificationKey, + r#" + SELECT epoch_id as "epoch_id: u32", serialised_key, serialization_revision as "serialization_revision: u8" + FROM master_verification_key WHERE epoch_id = ? + "#, + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn insert_master_verification_key( + &self, + serialisation_revision: u8, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO master_verification_key(epoch_id, serialised_key, serialization_revision) VALUES (?, ?, ?)", + epoch_id, + data, + serialisation_revision + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_master_coin_index_signatures( + &self, + epoch_id: i64, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + RawCoinIndexSignatures, + r#" + SELECT epoch_id as "epoch_id: u32", serialised_signatures, serialization_revision as "serialization_revision: u8" + FROM global_coin_index_signatures WHERE epoch_id = ? + "#, + epoch_id + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn insert_master_coin_index_signatures( + &self, + serialisation_revision: u8, + epoch_id: i64, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + "INSERT INTO global_coin_index_signatures(epoch_id, serialised_signatures, serialization_revision) VALUES (?, ?, ?)", + epoch_id, + data, + serialisation_revision + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + pub(crate) async fn get_master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, sqlx::Error> { + sqlx::query_as!( + RawExpirationDateSignatures, + r#" + SELECT epoch_id as "epoch_id: u32", serialised_signatures, serialization_revision as "serialization_revision: u8" + FROM global_expiration_date_signatures + WHERE expiration_date = ? + "#, + expiration_date + ) + .fetch_optional(&self.connection_pool) + .await + } + + pub(crate) async fn insert_master_expiration_date_signatures( + &self, + serialisation_revision: u8, + epoch_id: i64, + expiration_date: Date, + data: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO global_expiration_date_signatures(expiration_date, epoch_id, serialised_signatures, serialization_revision) + VALUES (?, ?, ?, ?) + "#, + expiration_date, + epoch_id, + data, + serialisation_revision + ) + .execute(&self.connection_pool) + .await?; + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) async fn insert_deposit_data( + &self, + deposit_id: DepositId, + deposit_tx_hash: String, + requested_on: OffsetDateTime, + request_uuid: String, + deposit_amount: String, + client_pubkey: &[u8], + deposit_ed25519_private_key: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO ticketbook_deposit(deposit_id, deposit_tx_hash, requested_on, request_uuid, deposit_amount, client_pubkey, ed25519_deposit_private_key) + VALUES (?, ?, ?, ?, ?, ?, ?) + "#, + deposit_id, + deposit_tx_hash, + requested_on, + request_uuid, + deposit_amount, + client_pubkey, + deposit_ed25519_private_key, + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn insert_partial_wallet_share( + &self, + deposit_id: DepositId, + epoch_id: i64, + expiration_date: Date, + node_id: i64, + created: OffsetDateTime, + share: &[u8], + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO partial_blinded_wallet(corresponding_deposit, epoch_id, expiration_date, node_id, created, blinded_signature) + VALUES (?, ?, ?, ?, ?, ?) + "#, + deposit_id, + epoch_id, + expiration_date, + node_id, + created, + share + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } + + pub(crate) async fn insert_partial_wallet_issuance_failure( + &self, + deposit_id: DepositId, + epoch_id: i64, + expiration_date: Date, + node_id: i64, + created: OffsetDateTime, + failure_message: String, + ) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO partial_blinded_wallet_failure(corresponding_deposit, epoch_id, expiration_date, node_id, created, failure_message) + VALUES (?, ?, ?, ?, ?, ?) + "#, + deposit_id, + epoch_id, + expiration_date, + node_id, + created, + failure_message + ) + .execute(&self.connection_pool) + .await?; + + Ok(()) + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs new file mode 100644 index 0000000000..e165907871 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/storage/mod.rs @@ -0,0 +1,460 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::credentials::ticketbook::NodeId; +use crate::error::VpnApiError; +use crate::storage::manager::SqliteStorageManager; +use crate::storage::models::{BlindedShares, MinimalWalletShare}; +use nym_compact_ecash::PublicKeyUser; +use nym_credentials::ecash::bandwidth::issuance::Hash; +use nym_credentials::ecash::bandwidth::serialiser::VersionedSerialise; +use nym_credentials::{ + AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, +}; +use nym_crypto::asymmetric::ed25519; +use nym_validator_client::ecash::BlindedSignatureResponse; +use nym_validator_client::nym_api::EpochId; +use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; +use nym_validator_client::nyxd::Coin; +use sqlx::ConnectOptions; +use std::fmt::Debug; +use std::path::Path; +use time::{Date, OffsetDateTime}; +use tracing::{debug, error, info, instrument}; +use uuid::Uuid; +use zeroize::Zeroizing; + +mod manager; +pub mod models; + +#[derive(Clone)] +pub struct VpnApiStorage { + pub(crate) storage_manager: SqliteStorageManager, +} + +impl VpnApiStorage { + #[instrument] + pub async fn init + Debug>(database_path: P) -> Result { + debug!("Attempting to connect to database"); + + let opts = sqlx::sqlite::SqliteConnectOptions::new() + .filename(database_path) + .create_if_missing(true) + .disable_statement_logging(); + + let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { + Ok(db) => db, + Err(err) => { + error!("Failed to connect to SQLx database: {err}"); + return Err(err.into()); + } + }; + + if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await { + error!("Failed to initialize SQLx database: {err}"); + return Err(err.into()); + } + + info!("Database migration finished!"); + + Ok(VpnApiStorage { + storage_manager: SqliteStorageManager { connection_pool }, + }) + } + + #[allow(dead_code)] + pub(crate) async fn load_blinded_shares_status_by_shares_id( + &self, + id: i64, + ) -> Result, VpnApiError> { + Ok(self + .storage_manager + .load_blinded_shares_status_by_shares_id(id) + .await?) + } + + pub(crate) async fn load_wallet_shares_by_shares_id( + &self, + id: i64, + ) -> Result, VpnApiError> { + Ok(self + .storage_manager + .load_wallet_shares_by_shares_id(id) + .await?) + } + + #[allow(dead_code)] + pub(crate) async fn load_blinded_shares_status_by_device_and_credential_id( + &self, + device_id: &str, + credential_id: &str, + ) -> Result, VpnApiError> { + Ok(self + .storage_manager + .load_blinded_shares_status_by_device_and_credential_id(device_id, credential_id) + .await?) + } + + pub(crate) async fn load_wallet_shares_by_device_and_credential_id( + &self, + device_id: &str, + credential_id: &str, + ) -> Result, VpnApiError> { + Ok(self + .storage_manager + .load_wallet_shares_by_device_and_credential_id(device_id, credential_id) + .await?) + } + + pub(crate) async fn insert_new_pending_async_shares_request( + &self, + request: Uuid, + device_id: &str, + credential_id: &str, + ) -> Result { + Ok(self + .storage_manager + .insert_new_pending_async_shares_request(request.to_string(), device_id, credential_id) + .await?) + } + + pub(crate) async fn update_pending_async_blinded_shares_issued( + &self, + available_shares: usize, + device_id: &str, + credential_id: &str, + ) -> Result { + self.storage_manager + .update_pending_async_blinded_shares_issued( + available_shares as i64, + device_id, + credential_id, + ) + .await + } + + pub(crate) async fn update_pending_async_blinded_shares_error( + &self, + available_shares: usize, + device_id: &str, + credential_id: &str, + error: &str, + ) -> Result { + self.storage_manager + .update_pending_async_blinded_shares_error( + available_shares as i64, + device_id, + credential_id, + error, + ) + .await + } + + pub(crate) async fn prune_old_blinded_shares(&self) -> Result<(), VpnApiError> { + let max_age = OffsetDateTime::now_utc() - time::Duration::days(31); + + self.storage_manager + .prune_old_partial_blinded_wallets(max_age) + .await?; + self.storage_manager + .prune_old_partial_blinded_wallet_failures(max_age) + .await?; + self.storage_manager.prune_old_blinded_shares(max_age).await + } + + #[allow(clippy::too_many_arguments)] + pub(crate) async fn insert_deposit_data( + &self, + deposit_id: DepositId, + deposit_tx_hash: Hash, + requested_on: OffsetDateTime, + request: Uuid, + deposit_amount: Coin, + client_ecash_pubkey: &PublicKeyUser, + ed22519_keypair: &ed25519::KeyPair, + ) -> Result<(), VpnApiError> { + debug!("inserting deposit data"); + + let private_key_bytes = Zeroizing::new(ed22519_keypair.private_key().to_bytes()); + + self.storage_manager + .insert_deposit_data( + deposit_id, + deposit_tx_hash.to_string(), + requested_on, + request.to_string(), + deposit_amount.to_string(), + &client_ecash_pubkey.to_bytes(), + private_key_bytes.as_ref(), + ) + .await?; + Ok(()) + } + + pub(crate) async fn insert_partial_wallet_share( + &self, + deposit_id: DepositId, + epoch_id: EpochId, + expiration_date: Date, + node_id: NodeId, + res: &Result, + ) -> Result<(), VpnApiError> { + debug!("inserting partial wallet share"); + let now = OffsetDateTime::now_utc(); + + match res { + Ok(share) => { + self.storage_manager + .insert_partial_wallet_share( + deposit_id, + epoch_id as i64, + expiration_date, + node_id as i64, + now, + &share.blinded_signature.to_bytes(), + ) + .await?; + } + Err(err) => { + self.storage_manager + .insert_partial_wallet_issuance_failure( + deposit_id, + epoch_id as i64, + expiration_date, + node_id as i64, + now, + err.to_string(), + ) + .await? + } + } + Ok(()) + } + + pub(crate) async fn get_master_verification_key( + &self, + epoch_id: EpochId, + ) -> Result, VpnApiError> { + let Some(raw) = self + .storage_manager + .get_master_verification_key(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + let deserialised = + EpochVerificationKey::try_unpack(&raw.serialised_key, raw.serialization_revision) + .map_err(|err| VpnApiError::database_inconsistency(err.to_string()))?; + Ok(Some(deserialised)) + } + + pub(crate) async fn insert_master_verification_key( + &self, + key: &EpochVerificationKey, + ) -> Result<(), VpnApiError> { + let packed = key.pack(); + Ok(self + .storage_manager + .insert_master_verification_key(packed.revision, key.epoch_id as i64, &packed.data) + .await?) + } + + pub(crate) async fn get_master_coin_index_signatures( + &self, + epoch_id: EpochId, + ) -> Result, VpnApiError> { + let Some(raw) = self + .storage_manager + .get_master_coin_index_signatures(epoch_id as i64) + .await? + else { + return Ok(None); + }; + + let deserialised = AggregatedCoinIndicesSignatures::try_unpack( + &raw.serialised_signatures, + raw.serialization_revision, + ) + .map_err(|err| VpnApiError::database_inconsistency(err.to_string()))?; + Ok(Some(deserialised)) + } + + pub(crate) async fn insert_master_coin_index_signatures( + &self, + signatures: &AggregatedCoinIndicesSignatures, + ) -> Result<(), VpnApiError> { + let packed = signatures.pack(); + self.storage_manager + .insert_master_coin_index_signatures( + packed.revision, + signatures.epoch_id as i64, + &packed.data, + ) + .await?; + Ok(()) + } + + pub(crate) async fn get_master_expiration_date_signatures( + &self, + expiration_date: Date, + ) -> Result, VpnApiError> { + let Some(raw) = self + .storage_manager + .get_master_expiration_date_signatures(expiration_date) + .await? + else { + return Ok(None); + }; + + let deserialised = AggregatedExpirationDateSignatures::try_unpack( + &raw.serialised_signatures, + raw.serialization_revision, + ) + .map_err(|err| VpnApiError::database_inconsistency(err.to_string()))?; + Ok(Some(deserialised)) + } + + pub(crate) async fn insert_master_expiration_date_signatures( + &self, + signatures: &AggregatedExpirationDateSignatures, + ) -> Result<(), VpnApiError> { + let packed = signatures.pack(); + self.storage_manager + .insert_master_expiration_date_signatures( + packed.revision, + signatures.epoch_id as i64, + signatures.expiration_date, + &packed.data, + ) + .await?; + Ok(()) + } +} + +#[allow(clippy::expect_used)] +#[allow(clippy::unwrap_used)] +#[cfg(test)] +mod tests { + use super::*; + use crate::http::helpers; + use crate::storage::models::BlindedSharesStatus; + use nym_compact_ecash::scheme::keygen::KeyPairUser; + use rand::rngs::OsRng; + use rand::RngCore; + use std::ops::Deref; + use tempfile::{NamedTempFile, TempPath}; + + // create the wrapper so the underlying file gets deleted when it's no longer needed + struct StorageTestWrapper { + inner: VpnApiStorage, + _path: TempPath, + } + + impl StorageTestWrapper { + async fn new() -> anyhow::Result { + let file = NamedTempFile::new()?; + let path = file.into_temp_path(); + + println!("Creating database at {:?}...", path); + + Ok(StorageTestWrapper { + inner: VpnApiStorage::init(&path).await?, + _path: path, + }) + } + + async fn insert_dummy_deposit(&self, uuid: Uuid) -> anyhow::Result { + let mut rng = OsRng; + let deposit_id = rng.next_u32(); + let tx_hash = Hash::Sha256(Default::default()); + let requested_on = OffsetDateTime::now_utc(); + let deposit_amount = Coin::new(1, "ufoomp"); + let client_keypair = KeyPairUser::new(); + let client_ecash_pubkey = &client_keypair.public_key(); + + let deposit_keypair = ed25519::KeyPair::new(&mut rng); + + self.inner + .insert_deposit_data( + deposit_id, + tx_hash, + requested_on, + uuid, + deposit_amount, + client_ecash_pubkey, + &deposit_keypair, + ) + .await?; + + Ok(deposit_id) + } + } + + impl Deref for StorageTestWrapper { + type Target = VpnApiStorage; + fn deref(&self) -> &Self::Target { + &self.inner + } + } + + async fn get_storage() -> anyhow::Result { + StorageTestWrapper::new().await + } + + #[tokio::test] + async fn test_creation() -> anyhow::Result<()> { + let storage = get_storage().await; + assert!(storage.is_ok()); + + Ok(()) + } + + #[tokio::test] + async fn test_add() -> anyhow::Result<()> { + let storage = get_storage().await?; + + let dummy_uuid = helpers::random_uuid(); + println!("🚀 insert_pending_blinded_share..."); + + storage.insert_dummy_deposit(dummy_uuid).await?; + let res = storage + .insert_new_pending_async_shares_request(dummy_uuid, "1234", "1234") + .await; + if let Err(e) = &res { + println!("❌ {}", e); + } + assert!(res.is_ok()); + let res = res.unwrap(); + println!("res = {:?}", res); + assert_eq!(res.status, BlindedSharesStatus::Pending); + + println!("🚀 update_pending_blinded_share_error..."); + let res = storage + .update_pending_async_blinded_shares_error(0, "1234", "1234", "this is an error") + .await; + if let Err(e) = &res { + println!("❌ {}", e); + } + assert!(res.is_ok()); + let res = res.unwrap(); + println!("res = {:?}", res); + assert!(res.error_message.is_some()); + assert_eq!(res.status, BlindedSharesStatus::Error); + + println!("🚀 update_pending_blinded_share_data..."); + let res = storage + .update_pending_async_blinded_shares_issued(42, "1234", "1234") + .await; + if let Err(e) = &res { + println!("❌ {}", e); + } + assert!(res.is_ok()); + let res = res.unwrap(); + println!("res = {:?}", res); + assert_eq!(res.status, BlindedSharesStatus::Issued); + assert!(res.error_message.is_none()); + + Ok(()) + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/storage/models.rs b/nym-credential-proxy/nym-credential-proxy/src/storage/models.rs new file mode 100644 index 0000000000..5bc443bf45 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/storage/models.rs @@ -0,0 +1,88 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, Type}; +use std::convert::Into; +use strum_macros::{Display, EnumString}; +use time::{Date, OffsetDateTime}; + +#[derive(Serialize, Deserialize, Debug, Clone, EnumString, Type, PartialEq, Display)] +#[sqlx(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum BlindedSharesStatus { + Pending, + Issued, + Error, +} + +#[derive(Serialize, Deserialize, Debug, Clone, FromRow)] +pub struct BlindedShares { + pub id: i64, + pub request_uuid: String, + pub status: BlindedSharesStatus, + pub device_id: String, + pub credential_id: String, + pub available_shares: i64, + pub error_message: Option, + pub created: OffsetDateTime, + pub updated: OffsetDateTime, +} + +pub struct FullBlindedShares { + pub status: BlindedShares, + pub shares: (), +} + +#[derive(FromRow)] +pub struct RawExpirationDateSignatures { + #[allow(dead_code)] + pub epoch_id: u32, + pub serialised_signatures: Vec, + pub serialization_revision: u8, +} + +#[derive(FromRow)] +pub struct RawCoinIndexSignatures { + #[allow(dead_code)] + pub epoch_id: u32, + pub serialised_signatures: Vec, + pub serialization_revision: u8, +} + +#[derive(FromRow)] +pub struct RawVerificationKey { + #[allow(dead_code)] + pub epoch_id: u32, + pub serialised_key: Vec, + pub serialization_revision: u8, +} + +#[derive(FromRow)] +pub struct WalletShare { + #[allow(dead_code)] + pub corresponding_deposit: i64, + pub node_id: i64, + #[allow(dead_code)] + pub created: OffsetDateTime, + pub blinded_signature: Vec, +} + +#[derive(FromRow)] +pub struct MinimalWalletShare { + pub epoch_id: i64, + pub expiration_date: Date, + pub node_id: i64, + pub blinded_signature: Vec, +} + +impl From + for nym_credential_proxy_requests::api::v1::ticketbook::models::WalletShare +{ + fn from(value: MinimalWalletShare) -> Self { + nym_credential_proxy_requests::api::v1::ticketbook::models::WalletShare { + node_index: value.node_id as u64, + bs58_encoded_share: bs58::encode(&value.blinded_signature).into_string(), + } + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/tasks.rs b/nym-credential-proxy/nym-credential-proxy/src/tasks.rs new file mode 100644 index 0000000000..6389f58452 --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/tasks.rs @@ -0,0 +1,36 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::storage::VpnApiStorage; +use tokio_util::sync::CancellationToken; +use tracing::{error, info}; + +pub struct StoragePruner { + cancellation_token: CancellationToken, + storage: VpnApiStorage, +} + +impl StoragePruner { + pub fn new(cancellation_token: CancellationToken, storage: VpnApiStorage) -> Self { + Self { + cancellation_token, + storage, + } + } + + pub async fn run_forever(self) { + while !self.cancellation_token.is_cancelled() { + tokio::select! { + _ = self.cancellation_token.cancelled() => { + // The token was cancelled, task can shut down + } + _ = tokio::time::sleep(std::time::Duration::from_secs(60 * 60)) => { + match self.storage.prune_old_blinded_shares().await { + Ok(_res) => info!("🧹 Pruning old blinded shares complete"), + Err(err) => error!("Failed to prune old blinded shares: {err}"), + } + } + } + } + } +} diff --git a/nym-credential-proxy/nym-credential-proxy/src/webhook.rs b/nym-credential-proxy/nym-credential-proxy/src/webhook.rs new file mode 100644 index 0000000000..9ebdca250f --- /dev/null +++ b/nym-credential-proxy/nym-credential-proxy/src/webhook.rs @@ -0,0 +1,75 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiError; +use clap::Args; +use reqwest::header::AUTHORIZATION; +use serde::Serialize; +use tracing::{debug, error, instrument, span, Level}; +use url::Url; +use uuid::Uuid; + +#[derive(Args, Debug, Clone)] +pub struct ZkNymWebHookConfig { + #[clap(long, env = "WEBHOOK_ZK_NYMS_URL")] + pub webhook_url: Url, + + #[clap(long, env = "WEBHOOK_ZK_NYMS_CLIENT_ID")] + pub webhook_client_id: String, + + #[clap(long, env = "WEBHOOK_ZK_NYMS_CLIENT_SECRET")] + pub webhook_client_secret: String, +} + +impl ZkNymWebHookConfig { + pub fn ensure_valid_client_url(&self) -> Result<(), VpnApiError> { + self.client_url() + .map_err(|_| VpnApiError::InvalidWebhookUrl) + .map(|_| ()) + } + + fn client_url(&self) -> Result { + self.webhook_url.join(&self.webhook_client_id) + } + + fn unchecked_client_url(&self) -> Url { + // we ensured we have valid url on startup + #[allow(clippy::unwrap_used)] + self.client_url().unwrap() + } + + fn bearer_token(&self) -> String { + format!("Bearer {}", self.webhook_client_secret) + } + + #[instrument(skip_all)] + pub async fn try_trigger(&self, original_uuid: Uuid, payload: &T) { + let url = self.unchecked_client_url(); + let span = span!(Level::DEBUG, "webhook", uuid = %original_uuid, url = %url); + let _entered = span.enter(); + + debug!("🕸️ about to trigger the webhook"); + + match reqwest::Client::new() + .post(url.clone()) + .header(AUTHORIZATION, self.bearer_token()) + .json(payload) + .send() + .await + { + Ok(res) => { + if !res.status().is_success() { + error!("❌🕸️ failed to call webhook: {res:?}"); + } else { + debug!("✅🕸️ webhook triggered successfully: {res:?}"); + if let Ok(body) = res.text().await { + debug!("body = {body}"); + } + } + } + Err(err) => { + error!("failed to call webhook: {err}") + } + } + } +} diff --git a/nym-credential-proxy/vpn-api-lib-wasm/Cargo.toml b/nym-credential-proxy/vpn-api-lib-wasm/Cargo.toml new file mode 100644 index 0000000000..f3437ae951 --- /dev/null +++ b/nym-credential-proxy/vpn-api-lib-wasm/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "nym-vpn-api-lib-wasm" +version = "0.1.0" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +bs58 = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +time = { workspace = true, features = ["wasm-bindgen"] } +thiserror.workspace = true +zeroize = { workspace = true } + +# wasm-specific deps +getrandom = { version = "0.2", features = ["js"] } +wasm-bindgen = { workspace = true } +js-sys = "0.3.70" +tsify = { workspace = true, features = ["js"] } +serde-wasm-bindgen = "0.6.5" + +# NYM: +nym-bin-common = { path = "../../common/bin-common" } +nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } +nym-compact-ecash = { path = "../../common/nym_offline_compact_ecash" } +nym-credentials = { path = "../../common/credentials" } +nym-credentials-interface = { path = "../../common/credentials-interface" } +nym-ecash-time = { path = "../../common/ecash-time", features = ["expiration"] } +nym-credential-proxy-requests = { path = "../nym-credential-proxy-requests", default-features = false, features = ["tsify"] } +wasm-utils = { path = "../../common/wasm/utils" } + diff --git a/nym-credential-proxy/vpn-api-lib-wasm/Makefile b/nym-credential-proxy/vpn-api-lib-wasm/Makefile new file mode 100644 index 0000000000..2edce6e5fc --- /dev/null +++ b/nym-credential-proxy/vpn-api-lib-wasm/Makefile @@ -0,0 +1,4 @@ +build: + wasm-pack build --scope nymproject --target web --out-dir ../../dist/wasm/nym-vpn-api-lib-wasm + wasm-opt -Oz -o ../../dist/wasm/nym-vpn-api-lib-wasm/nym_vpn_api_lib_wasm_bg.wasm ../../dist/wasm/nym-vpn-api-lib-wasm/nym_vpn_api_lib_wasm_bg.wasm + diff --git a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/bootstrap.js b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/bootstrap.js new file mode 100644 index 0000000000..362cdb2431 --- /dev/null +++ b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/bootstrap.js @@ -0,0 +1,2 @@ +import("./index.js") + .catch(e => console.error("Error importing `index.js`:", e)); diff --git a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.html b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.html new file mode 100644 index 0000000000..5cd272596a --- /dev/null +++ b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.html @@ -0,0 +1,17 @@ + + + + + + + Nym WebAssembly Demo + + + + + +

everything happens in the console

+ + + + \ No newline at end of file diff --git a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.js b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.js new file mode 100644 index 0000000000..3921411115 --- /dev/null +++ b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/index.js @@ -0,0 +1,49 @@ +import init, { NymIssuanceTicketbook } from "@nymproject/nym-vpn-api-lib-wasm"; + +async function main() { + await init(); + + let cryptoData = new NymIssuanceTicketbook({}); + + console.log("getting partial vks"); + const partialVksRes = await fetch("http://localhost:8080/api/v1/ticketbook/partial-verification-keys", { + headers: new Headers({ "Authorization": "Bearer foomp" }) + }); + const partialVks = await partialVksRes.json(); + console.debug(partialVks); + + console.log("getting master vk"); + const masterVkRes = await fetch("http://localhost:8080/api/v1/ticketbook/master-verification-key", { + headers: new Headers({ "Authorization": "Bearer foomp" }) + }); + const masterVk = await masterVkRes.json(); + console.debug(masterVk); + + let request = cryptoData.buildRequestPayload(false); + console.log(request); + + + console.log("getting blinded wallet shares"); + const sharesRes = await fetch("http://localhost:8080/api/v1/ticketbook/obtain?include-coin-index-signatures=true&include-expiration-date-signatures=true", { + method: "POST", + headers: new Headers( + { + "Authorization": "Bearer foomp", + "Content-Type": "application/json" + } + ), + body: request + }); + + const credentialShares = await sharesRes.json(); + console.log(credentialShares); + + console.log("unblinding shares"); + const unblinded = cryptoData.unblindWalletShares(credentialShares, partialVks, masterVk); + + const serialised = unblinded.serialise(); + console.log("serialised:\n", serialised); +} + + +main(); diff --git a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/package.json b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/package.json new file mode 100644 index 0000000000..17032501e8 --- /dev/null +++ b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/package.json @@ -0,0 +1,40 @@ +{ + "name": "create-wasm-app", + "version": "0.1.0", + "description": "create an app to consume rust-generated wasm packages", + "main": "index.js", + "bin": { + "create-wasm-app": ".bin/create-wasm-app.js" + }, + "scripts": { + "build": "webpack --config webpack.config.js", + "build:wasm": "cd ../ && make wasm-build", + "start": "webpack-dev-server --port 8001" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/rustwasm/create-wasm-app.git" + }, + "keywords": [ + "webassembly", + "wasm", + "rust", + "webpack" + ], + "author": "Dave Hrycyszyn ", + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/nymtech/nym/issues" + }, + "homepage": "https://nymtech.net/docs", + "devDependencies": { + "copy-webpack-plugin": "^11.0.0", + "hello-wasm-pack": "^0.1.0", + "webpack": "^5.70.0", + "webpack-cli": "^4.9.2", + "webpack-dev-server": "^4.7.4" + }, + "dependencies": { + "@nymproject/nym-vpn-api-lib-wasm": "file:../../../dist/wasm/nym-vpn-api-lib-wasm" + } +} diff --git a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/webpack.config.js b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/webpack.config.js new file mode 100644 index 0000000000..d5f839485f --- /dev/null +++ b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/webpack.config.js @@ -0,0 +1,33 @@ +const CopyWebpackPlugin = require("copy-webpack-plugin"); +const path = require("path"); + +module.exports = { + performance: { + hints: false, + maxEntrypointSize: 512000, + maxAssetSize: 512000 + }, + entry: { + bootstrap: "./bootstrap.js" + // worker: "./worker.js" + }, + output: { + path: path.resolve(__dirname, "dist"), + filename: "[name].js" + }, + mode: "development", + // mode: 'production', + plugins: [ + new CopyWebpackPlugin({ + patterns: [ + "index.html", + { + from: "../../../dist/wasm/nym-vpn-api-lib-wasm/*.(js|wasm)", + to: "[name][ext]" + } + ] + }) + + ], + experiments: { syncWebAssembly: true } +}; diff --git a/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/yarn.lock b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/yarn.lock new file mode 100644 index 0000000000..43e804cb6e --- /dev/null +++ b/nym-credential-proxy/vpn-api-lib-wasm/internal-dev/yarn.lock @@ -0,0 +1,2253 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@discoveryjs/json-ext@^0.5.0": + version "0.5.7" + resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" + integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + integrity sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.20", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@leichtgewicht/ip-codec@^2.0.1": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz#4fc56c15c580b9adb7dc3c333a134e540b44bfb1" + integrity sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@nymproject/nym-vpn-api-lib-wasm@file:../../../dist/wasm/nym-vpn-api-lib-wasm": + version "0.1.0" + +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/bonjour@^3.5.9": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@types/bonjour/-/bonjour-3.5.13.tgz#adf90ce1a105e81dd1f9c61fdc5afda1bfb92956" + integrity sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ== + dependencies: + "@types/node" "*" + +"@types/connect-history-api-fallback@^1.3.5": + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz#7de71645a103056b48ac3ce07b3520b819c1d5b3" + integrity sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw== + dependencies: + "@types/express-serve-static-core" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + +"@types/estree@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.33": + version "4.19.5" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.19.5.tgz#218064e321126fcf9048d1ca25dd2465da55d9c6" + integrity sha512-y6W03tvrACO72aijJ5uF02FRq5cgDR9lUxddQ8vyF+GvmjJQqbzDcJngEjURc+ZsG31VI3hODNZJ2URj86pzmg== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@*", "@types/express@^4.17.13": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/http-proxy@^1.17.8": + version "1.17.15" + resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.15.tgz#12118141ce9775a6499ecb4c01d02f90fc839d36" + integrity sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ== + dependencies: + "@types/node" "*" + +"@types/json-schema@^7.0.8", "@types/json-schema@^7.0.9": + version "7.0.15" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.15.tgz#596a1747233694d50f6ad8a7869fcb6f56cf5841" + integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + +"@types/node-forge@^1.3.0": + version "1.3.11" + resolved "https://registry.yarnpkg.com/@types/node-forge/-/node-forge-1.3.11.tgz#0972ea538ddb0f4d9c2fa0ec5db5724773a604da" + integrity sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ== + dependencies: + "@types/node" "*" + +"@types/node@*": + version "22.5.0" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.0.tgz#10f01fe9465166b4cab72e75f60d8b99d019f958" + integrity sha512-DkFrJOe+rfdHTqqMg0bSNlGlQ85hSoh2TPzZyhHsXnMtligRWpxUySiyw8FY14ITt24HVCiQPWxS3KO/QlGmWg== + dependencies: + undici-types "~6.19.2" + +"@types/qs@*": + version "6.9.15" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce" + integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/retry@0.12.0": + version "0.12.0" + resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d" + integrity sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-index@^1.9.1": + version "1.9.4" + resolved "https://registry.yarnpkg.com/@types/serve-index/-/serve-index-1.9.4.tgz#e6ae13d5053cb06ed36392110b4f9a49ac4ec898" + integrity sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug== + dependencies: + "@types/express" "*" + +"@types/serve-static@*", "@types/serve-static@^1.13.10": + version "1.15.7" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.7.tgz#22174bbd74fb97fe303109738e9b5c2f3064f714" + integrity sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw== + dependencies: + "@types/http-errors" "*" + "@types/node" "*" + "@types/send" "*" + +"@types/sockjs@^0.3.33": + version "0.3.36" + resolved "https://registry.yarnpkg.com/@types/sockjs/-/sockjs-0.3.36.tgz#ce322cf07bcc119d4cbf7f88954f3a3bd0f67535" + integrity sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q== + dependencies: + "@types/node" "*" + +"@types/ws@^8.5.5": + version "8.5.12" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" + integrity sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ== + dependencies: + "@types/node" "*" + +"@webassemblyjs/ast@1.12.1", "@webassemblyjs/ast@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.12.1.tgz#bb16a0e8b1914f979f45864c23819cc3e3f0d4bb" + integrity sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg== + dependencies: + "@webassemblyjs/helper-numbers" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + +"@webassemblyjs/floating-point-hex-parser@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz#dacbcb95aff135c8260f77fa3b4c5fea600a6431" + integrity sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw== + +"@webassemblyjs/helper-api-error@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz#6132f68c4acd59dcd141c44b18cbebbd9f2fa768" + integrity sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q== + +"@webassemblyjs/helper-buffer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz#6df20d272ea5439bf20ab3492b7fb70e9bfcb3f6" + integrity sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw== + +"@webassemblyjs/helper-numbers@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz#cbce5e7e0c1bd32cf4905ae444ef64cea919f1b5" + integrity sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g== + dependencies: + "@webassemblyjs/floating-point-hex-parser" "1.11.6" + "@webassemblyjs/helper-api-error" "1.11.6" + "@xtuc/long" "4.2.2" + +"@webassemblyjs/helper-wasm-bytecode@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz#bb2ebdb3b83aa26d9baad4c46d4315283acd51e9" + integrity sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA== + +"@webassemblyjs/helper-wasm-section@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz#3da623233ae1a60409b509a52ade9bc22a37f7bf" + integrity sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/wasm-gen" "1.12.1" + +"@webassemblyjs/ieee754@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz#bb665c91d0b14fffceb0e38298c329af043c6e3a" + integrity sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg== + dependencies: + "@xtuc/ieee754" "^1.2.0" + +"@webassemblyjs/leb128@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/leb128/-/leb128-1.11.6.tgz#70e60e5e82f9ac81118bc25381a0b283893240d7" + integrity sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ== + dependencies: + "@xtuc/long" "4.2.2" + +"@webassemblyjs/utf8@1.11.6": + version "1.11.6" + resolved "https://registry.yarnpkg.com/@webassemblyjs/utf8/-/utf8-1.11.6.tgz#90f8bc34c561595fe156603be7253cdbcd0fab5a" + integrity sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA== + +"@webassemblyjs/wasm-edit@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz#9f9f3ff52a14c980939be0ef9d5df9ebc678ae3b" + integrity sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/helper-wasm-section" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-opt" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + "@webassemblyjs/wast-printer" "1.12.1" + +"@webassemblyjs/wasm-gen@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz#a6520601da1b5700448273666a71ad0a45d78547" + integrity sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wasm-opt@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz#9e6e81475dfcfb62dab574ac2dda38226c232bc5" + integrity sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-buffer" "1.12.1" + "@webassemblyjs/wasm-gen" "1.12.1" + "@webassemblyjs/wasm-parser" "1.12.1" + +"@webassemblyjs/wasm-parser@1.12.1", "@webassemblyjs/wasm-parser@^1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz#c47acb90e6f083391e3fa61d113650eea1e95937" + integrity sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@webassemblyjs/helper-api-error" "1.11.6" + "@webassemblyjs/helper-wasm-bytecode" "1.11.6" + "@webassemblyjs/ieee754" "1.11.6" + "@webassemblyjs/leb128" "1.11.6" + "@webassemblyjs/utf8" "1.11.6" + +"@webassemblyjs/wast-printer@1.12.1": + version "1.12.1" + resolved "https://registry.yarnpkg.com/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz#bcecf661d7d1abdaf989d8341a4833e33e2b31ac" + integrity sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA== + dependencies: + "@webassemblyjs/ast" "1.12.1" + "@xtuc/long" "4.2.2" + +"@webpack-cli/configtest@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/configtest/-/configtest-1.2.0.tgz#7b20ce1c12533912c3b217ea68262365fa29a6f5" + integrity sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg== + +"@webpack-cli/info@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/info/-/info-1.5.0.tgz#6c78c13c5874852d6e2dd17f08a41f3fe4c261b1" + integrity sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ== + dependencies: + envinfo "^7.7.3" + +"@webpack-cli/serve@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-1.7.0.tgz#e1993689ac42d2b16e9194376cfb6753f6254db1" + integrity sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q== + +"@xtuc/ieee754@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@xtuc/ieee754/-/ieee754-1.2.0.tgz#eef014a3145ae477a1cbc00cd1e552336dceb790" + integrity sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA== + +"@xtuc/long@4.2.2": + version "4.2.2" + resolved "https://registry.yarnpkg.com/@xtuc/long/-/long-4.2.2.tgz#d291c6a4e97989b5c61d9acf396ae4fe133a718d" + integrity sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ== + +accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8: + version "1.3.8" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e" + integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw== + dependencies: + mime-types "~2.1.34" + negotiator "0.6.3" + +acorn-import-attributes@^1.9.5: + version "1.9.5" + resolved "https://registry.yarnpkg.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz#7eb1557b1ba05ef18b5ed0ec67591bfab04688ef" + integrity sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ== + +acorn@^8.7.1, acorn@^8.8.2: + version "8.12.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.12.1.tgz#71616bdccbe25e27a54439e0046e89ca76df2248" + integrity sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg== + +ajv-formats@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-2.1.1.tgz#6e669400659eb74973bbf2e33327180a0996b520" + integrity sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA== + dependencies: + ajv "^8.0.0" + +ajv-keywords@^3.5.2: + version "3.5.2" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" + integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== + +ajv-keywords@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-5.1.0.tgz#69d4d385a4733cdbeab44964a1170a88f87f0e16" + integrity sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw== + dependencies: + fast-deep-equal "^3.1.3" + +ajv@^6.12.5: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ajv@^8.0.0, ajv@^8.9.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ansi-html-community@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ansi-html-community/-/ansi-html-community-0.0.8.tgz#69fbc4d6ccbe383f9736934ae34c3f8290f1bf41" + integrity sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg== + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +batch@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" + integrity sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +body-parser@1.20.2: + version "1.20.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd" + integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA== + dependencies: + bytes "3.1.2" + content-type "~1.0.5" + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + http-errors "2.0.0" + iconv-lite "0.4.24" + on-finished "2.4.1" + qs "6.11.0" + raw-body "2.5.2" + type-is "~1.6.18" + unpipe "1.0.0" + +bonjour-service@^1.0.11: + version "1.2.1" + resolved "https://registry.yarnpkg.com/bonjour-service/-/bonjour-service-1.2.1.tgz#eb41b3085183df3321da1264719fbada12478d02" + integrity sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw== + dependencies: + fast-deep-equal "^3.1.3" + multicast-dns "^7.2.5" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.21.10: + version "4.23.3" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.3.tgz#debb029d3c93ebc97ffbc8d9cbb03403e227c800" + integrity sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA== + dependencies: + caniuse-lite "^1.0.30001646" + electron-to-chromium "^1.5.4" + node-releases "^2.0.18" + update-browserslist-db "^1.1.0" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + integrity sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw== + +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + +call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + set-function-length "^1.2.1" + +caniuse-lite@^1.0.30001646: + version "1.0.30001653" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001653.tgz#b8af452f8f33b1c77f122780a4aecebea0caca56" + integrity sha512-XGWQVB8wFQ2+9NZwZ10GxTYC5hk0Fa+q8cSkr0tgvMhYhMHP/QC+WTgrePMDBWiWc/pV+1ik82Al20XOK25Gcw== + +chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chrome-trace-event@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz#05bffd7ff928465093314708c93bdfa9bd1f0f5b" + integrity sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ== + +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== + dependencies: + is-plain-object "^2.0.4" + kind-of "^6.0.2" + shallow-clone "^3.0.0" + +colorette@^2.0.10, colorette@^2.0.14: + version "2.0.20" + resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a" + integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commander@^7.0.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" + integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== + +compressible@~2.0.16: + version "2.0.18" + resolved "https://registry.yarnpkg.com/compressible/-/compressible-2.0.18.tgz#af53cca6b070d4c3c0750fbd77286a6d7cc46fba" + integrity sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg== + dependencies: + mime-db ">= 1.43.0 < 2" + +compression@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/compression/-/compression-1.7.4.tgz#95523eff170ca57c29a0ca41e6fe131f41e5bb8f" + integrity sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ== + dependencies: + accepts "~1.3.5" + bytes "3.0.0" + compressible "~2.0.16" + debug "2.6.9" + on-headers "~1.0.2" + safe-buffer "5.1.2" + vary "~1.1.2" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +connect-history-api-fallback@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz#647264845251a0daf25b97ce87834cace0f5f1c8" + integrity sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA== + +content-disposition@0.5.4: + version "0.5.4" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" + integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ== + dependencies: + safe-buffer "5.2.1" + +content-type@~1.0.4, content-type@~1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ== + +cookie@0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" + integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== + +copy-webpack-plugin@^11.0.0: + version "11.0.0" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz#96d4dbdb5f73d02dd72d0528d1958721ab72e04a" + integrity sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ== + dependencies: + fast-glob "^3.2.11" + glob-parent "^6.0.1" + globby "^13.1.1" + normalize-path "^3.0.0" + schema-utils "^4.0.0" + serialize-javascript "^6.0.0" + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + +cross-spawn@^7.0.3: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.1.0: + version "4.3.6" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.6.tgz#2ab2c38fbaffebf8aa95fdfe6d88438c7a13c52b" + integrity sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg== + dependencies: + ms "2.1.2" + +default-gateway@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/default-gateway/-/default-gateway-6.0.3.tgz#819494c888053bdb743edbf343d6cdf7f2943a71" + integrity sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg== + dependencies: + execa "^5.0.0" + +define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-lazy-prop@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" + integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== + +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + +depd@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ== + +destroy@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" + integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== + +detect-node@^2.0.4: + version "2.1.0" + resolved "https://registry.yarnpkg.com/detect-node/-/detect-node-2.1.0.tgz#c9c70775a49c3d03bc2c06d9a73be550f978f8b1" + integrity sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dns-packet@^5.2.2: + version "5.6.1" + resolved "https://registry.yarnpkg.com/dns-packet/-/dns-packet-5.6.1.tgz#ae888ad425a9d1478a0674256ab866de1012cf2f" + integrity sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw== + dependencies: + "@leichtgewicht/ip-codec" "^2.0.1" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== + +electron-to-chromium@^1.5.4: + version "1.5.13" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz#1abf0410c5344b2b829b7247e031f02810d442e6" + integrity sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q== + +encodeurl@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" + integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== + +enhanced-resolve@^5.17.1: + version "5.17.1" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz#67bfbbcc2f81d511be77d686a90267ef7f898a15" + integrity sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +envinfo@^7.7.3: + version "7.13.0" + resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.13.0.tgz#81fbb81e5da35d74e814941aeab7c325a606fb31" + integrity sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q== + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-module-lexer@^1.2.1: + version "1.5.4" + resolved "https://registry.yarnpkg.com/es-module-lexer/-/es-module-lexer-1.5.4.tgz#a8efec3a3da991e60efa6b633a7cad6ab8d26b78" + integrity sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw== + +escalade@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== + +eslint-scope@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== + +eventemitter3@^4.0.0: + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + +events@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +execa@^5.0.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" + integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + +express@^4.17.3: + version "4.19.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465" + integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q== + dependencies: + accepts "~1.3.8" + array-flatten "1.1.1" + body-parser "1.20.2" + content-disposition "0.5.4" + content-type "~1.0.4" + cookie "0.6.0" + cookie-signature "1.0.6" + debug "2.6.9" + depd "2.0.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.2.0" + fresh "0.5.2" + http-errors "2.0.0" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "2.4.1" + parseurl "~1.3.3" + path-to-regexp "0.1.7" + proxy-addr "~2.0.7" + qs "6.11.0" + range-parser "~1.2.1" + safe-buffer "5.2.1" + send "0.18.0" + serve-static "1.15.0" + setprototypeof "1.2.0" + statuses "2.0.1" + type-is "~1.6.18" + utils-merge "1.0.1" + vary "~1.1.2" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.11, fast-glob@^3.3.0: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-uri@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.1.tgz#cddd2eecfc83a71c1be2cc2ef2061331be8a7134" + integrity sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw== + +fastest-levenshtein@^1.0.12: + version "1.0.16" + resolved "https://registry.yarnpkg.com/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz#210e61b6ff181de91ea9b3d1b84fdedd47e034e5" + integrity sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg== + +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + +faye-websocket@^0.11.3: + version "0.11.4" + resolved "https://registry.yarnpkg.com/faye-websocket/-/faye-websocket-0.11.4.tgz#7f0d9275cfdd86a1c963dc8b65fcc451edcbb1da" + integrity sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g== + dependencies: + websocket-driver ">=0.5.1" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +finalhandler@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32" + integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg== + dependencies: + debug "2.6.9" + encodeurl "~1.0.2" + escape-html "~1.0.3" + on-finished "2.4.1" + parseurl "~1.3.3" + statuses "2.0.1" + unpipe "~1.0.0" + +find-up@^4.0.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" + integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== + dependencies: + locate-path "^5.0.0" + path-exists "^4.0.0" + +flat@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" + integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== + +follow-redirects@^1.0.0: + version "1.15.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b" + integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA== + +forwarded@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811" + integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow== + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== + +fs-monkey@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/fs-monkey/-/fs-monkey-1.0.6.tgz#8ead082953e88d992cf3ff844faa907b26756da2" + integrity sha512-b1FMfwetIKymC0eioW7mTywihSQE4oLzQn1dB6rZB5fx/3NpNEdAWeCSMB+60/AeT0TCXsxzAlcYVEFCTAksWg== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + has-proto "^1.0.1" + has-symbols "^1.0.3" + hasown "^2.0.0" + +get-stream@^6.0.0: + version "6.0.1" + resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" + integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-to-regexp@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e" + integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw== + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globby@^13.1.1: + version "13.2.2" + resolved "https://registry.yarnpkg.com/globby/-/globby-13.2.2.tgz#63b90b1bf68619c2135475cbd4e71e66aa090592" + integrity sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w== + dependencies: + dir-glob "^3.0.1" + fast-glob "^3.3.0" + ignore "^5.2.4" + merge2 "^1.4.1" + slash "^4.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.1.2, graceful-fs@^4.2.11, graceful-fs@^4.2.4, graceful-fs@^4.2.6: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +handle-thing@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-2.0.1.tgz#857f79ce359580c340d43081cc648970d0bb234e" + integrity sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +hasown@^2.0.0, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +hello-wasm-pack@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/hello-wasm-pack/-/hello-wasm-pack-0.1.0.tgz#482a2e3371828056ac35f5b5fec76c0b99dcd530" + integrity sha512-3hx0GDkDLf/a9ThCMV2qG4mwza8N/MCtm8aeFFc/cdBCL2zMJ1kW1wjNl7xPqD1lz8Yl5+uhnc/cpui4dLwz/w== + +hpack.js@^2.1.6: + version "2.1.6" + resolved "https://registry.yarnpkg.com/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" + integrity sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ== + dependencies: + inherits "^2.0.1" + obuf "^1.0.0" + readable-stream "^2.0.1" + wbuf "^1.1.0" + +html-entities@^2.3.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-2.5.2.tgz#201a3cf95d3a15be7099521620d19dfb4f65359f" + integrity sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA== + +http-deceiver@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87" + integrity sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw== + +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +http-errors@~1.6.2: + version "1.6.3" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d" + integrity sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A== + dependencies: + depd "~1.1.2" + inherits "2.0.3" + setprototypeof "1.1.0" + statuses ">= 1.4.0 < 2" + +http-parser-js@>=0.5.1: + version "0.5.8" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3" + integrity sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q== + +http-proxy-middleware@^2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz#e1a4dd6979572c7ab5a4e4b55095d1f32a74963f" + integrity sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw== + dependencies: + "@types/http-proxy" "^1.17.8" + http-proxy "^1.18.1" + is-glob "^4.0.1" + is-plain-obj "^3.0.0" + micromatch "^4.0.2" + +http-proxy@^1.18.1: + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== + dependencies: + eventemitter3 "^4.0.0" + follow-redirects "^1.0.0" + requires-port "^1.0.0" + +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + +iconv-lite@0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore@^5.2.4: + version "5.3.2" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" + integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== + +import-local@^3.0.2: + version "3.2.0" + resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.2.0.tgz#c3d5c745798c02a6f8b897726aba5100186ee260" + integrity sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA== + dependencies: + pkg-dir "^4.2.0" + resolve-cwd "^3.0.0" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + integrity sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw== + +interpret@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/interpret/-/interpret-2.2.0.tgz#1a78a0b5965c40a5416d007ad6f50ad27c417df9" + integrity sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw== + +ipaddr.js@1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3" + integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g== + +ipaddr.js@^2.0.1: + version "2.2.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-2.2.0.tgz#d33fa7bac284f4de7af949638c9d68157c6b92e8" + integrity sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA== + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-core-module@^2.13.0: + version "2.15.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.15.1.tgz#a7363a25bee942fefab0de13bf6aa372c82dcc37" + integrity sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ== + dependencies: + hasown "^2.0.2" + +is-docker@^2.0.0, is-docker@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" + integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-plain-obj@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-3.0.0.tgz#af6f2ea14ac5a646183a5bbdb5baabbc156ad9d7" + integrity sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA== + +is-plain-object@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-plain-object/-/is-plain-object-2.0.4.tgz#2c163b3fafb1b606d9d17928f05c2a1c38e07677" + integrity sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og== + dependencies: + isobject "^3.0.1" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-wsl@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" + integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== + dependencies: + is-docker "^2.0.0" + +isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +isobject@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-3.0.1.tgz#4e431e92b11a9731636aa1f9c8d1ccbcfdab78df" + integrity sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg== + +jest-worker@^27.4.5: + version "27.5.1" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.5.1.tgz#8d146f0900e8973b106b6f73cc1e9a8cb86f8db0" + integrity sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg== + dependencies: + "@types/node" "*" + merge-stream "^2.0.0" + supports-color "^8.0.0" + +json-parse-even-better-errors@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +kind-of@^6.0.2: + version "6.0.3" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.3.tgz#07c05034a6c349fa06e24fa35aa76db4580ce4dd" + integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw== + +launch-editor@^2.6.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/launch-editor/-/launch-editor-2.8.1.tgz#3bda72af213ec9b46b170e39661916ec66c2f463" + integrity sha512-elBx2l/tp9z99X5H/qev8uyDywVh0VXAwEbjk8kJhnc5grOFkGh7aW6q55me9xnYbss261XtnUrysZ+XvGbhQA== + dependencies: + picocolors "^1.0.0" + shell-quote "^1.8.1" + +loader-runner@^4.2.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.3.0.tgz#c1b4a163b99f614830353b16755e7149ac2314e1" + integrity sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg== + +locate-path@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" + integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== + dependencies: + p-locate "^4.1.0" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ== + +memfs@^3.4.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/memfs/-/memfs-3.6.0.tgz#d7a2110f86f79dd950a8b6df6d57bc984aa185f6" + integrity sha512-EGowvkkgbMcIChjMTMkESFDbZeSh8xZ7kNSF0hAiAN4Jh6jgHCRS0Ga/+C8y6Au+oqpezRHCfPsmJ2+DwAgiwQ== + dependencies: + fs-monkey "^1.0.4" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w== + +merge-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" + integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== + +micromatch@^4.0.2, micromatch@^4.0.4: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +"mime-db@>= 1.43.0 < 2": + version "1.53.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.53.0.tgz#3cb63cd820fc29896d9d4e8c32ab4fcd74ccb447" + integrity sha512-oHlN/w+3MQ3rba9rqFr6V/ypF10LSkdwUysQL7GkXoTgIWeV+tcXGA852TBxH+gsh8UWoyhR1hKcoMJTuWflpg== + +mime-types@^2.1.27, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.24, mime-types@~2.1.34: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + +mime@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +minimalistic-assert@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" + integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== + +minimatch@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +multicast-dns@^7.2.5: + version "7.2.5" + resolved "https://registry.yarnpkg.com/multicast-dns/-/multicast-dns-7.2.5.tgz#77eb46057f4d7adbd16d9290fa7299f6fa64cced" + integrity sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg== + dependencies: + dns-packet "^5.2.2" + thunky "^1.0.2" + +negotiator@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" + integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== + +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + +node-forge@^1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" + integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== + +node-releases@^2.0.18: + version "2.0.18" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.18.tgz#f010e8d35e2fe8d6b2944f03f70213ecedc4ca3f" + integrity sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +object-inspect@^1.13.1: + version "1.13.2" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" + integrity sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g== + +obuf@^1.0.0, obuf@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" + integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== + +on-finished@2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" + integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== + dependencies: + ee-first "1.1.1" + +on-headers@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.2.tgz#772b0ae6aaa525c399e489adfad90c403eb3c28f" + integrity sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA== + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +open@^8.0.9: + version "8.4.2" + resolved "https://registry.yarnpkg.com/open/-/open-8.4.2.tgz#5b5ffe2a8f793dcd2aad73e550cb87b59cb084f9" + integrity sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ== + dependencies: + define-lazy-prop "^2.0.0" + is-docker "^2.1.1" + is-wsl "^2.2.0" + +p-limit@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" + integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== + dependencies: + p-try "^2.0.0" + +p-locate@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" + integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== + dependencies: + p-limit "^2.2.0" + +p-retry@^4.5.0: + version "4.6.2" + resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-4.6.2.tgz#9baae7184057edd4e17231cee04264106e092a16" + integrity sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ== + dependencies: + "@types/retry" "0.12.0" + retry "^0.13.1" + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parseurl@~1.3.2, parseurl@~1.3.3: + version "1.3.3" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" + integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0, picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pkg-dir@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" + integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== + dependencies: + find-up "^4.0.0" + +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +proxy-addr@~2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" + integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg== + dependencies: + forwarded "0.2.0" + ipaddr.js "1.9.1" + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +qs@6.11.0: + version "6.11.0" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a" + integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q== + dependencies: + side-channel "^1.0.4" + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +range-parser@^1.2.1, range-parser@~1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" + integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== + +raw-body@2.5.2: + version "2.5.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a" + integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.4.24" + unpipe "1.0.0" + +readable-stream@^2.0.1: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readable-stream@^3.0.6: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +rechoir@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.7.1.tgz#9478a96a1ca135b5e88fc027f03ee92d6c645686" + integrity sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg== + dependencies: + resolve "^1.9.0" + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +requires-port@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" + integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== + +resolve-cwd@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" + integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== + dependencies: + resolve-from "^5.0.0" + +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + +resolve@^1.9.0: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +retry@^0.13.1: + version "0.13.1" + resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658" + integrity sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg== + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +schema-utils@^3.1.1, schema-utils@^3.2.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-3.3.0.tgz#f50a88877c3c01652a15b622ae9e9795df7a60fe" + integrity sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg== + dependencies: + "@types/json-schema" "^7.0.8" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + +schema-utils@^4.0.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-4.2.0.tgz#70d7c93e153a273a805801882ebd3bff20d89c8b" + integrity sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw== + dependencies: + "@types/json-schema" "^7.0.9" + ajv "^8.9.0" + ajv-formats "^2.1.1" + ajv-keywords "^5.1.0" + +select-hose@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" + integrity sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg== + +selfsigned@^2.1.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/selfsigned/-/selfsigned-2.4.1.tgz#560d90565442a3ed35b674034cec4e95dceb4ae0" + integrity sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q== + dependencies: + "@types/node-forge" "^1.3.0" + node-forge "^1" + +send@0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" + integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== + dependencies: + debug "2.6.9" + depd "2.0.0" + destroy "1.2.0" + encodeurl "~1.0.2" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "2.0.0" + mime "1.6.0" + ms "2.1.3" + on-finished "2.4.1" + range-parser "~1.2.1" + statuses "2.0.1" + +serialize-javascript@^6.0.0, serialize-javascript@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +serve-index@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" + integrity sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw== + dependencies: + accepts "~1.3.4" + batch "0.6.1" + debug "2.6.9" + escape-html "~1.0.3" + http-errors "~1.6.2" + mime-types "~2.1.17" + parseurl "~1.3.2" + +serve-static@1.15.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" + integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== + dependencies: + encodeurl "~1.0.2" + escape-html "~1.0.3" + parseurl "~1.3.3" + send "0.18.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + integrity sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ== + +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== + dependencies: + kind-of "^6.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +shell-quote@^1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.8.1.tgz#6dbf4db75515ad5bac63b4f1894c3a154c766680" + integrity sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA== + +side-channel@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^3.0.3: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +slash@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" + integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== + +sockjs@^0.3.24: + version "0.3.24" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.24.tgz#c9bc8995f33a111bea0395ec30aa3206bdb5ccce" + integrity sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ== + dependencies: + faye-websocket "^0.11.3" + uuid "^8.3.2" + websocket-driver "^0.7.4" + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdy-transport@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdy-transport/-/spdy-transport-3.0.0.tgz#00d4863a6400ad75df93361a1608605e5dcdcf31" + integrity sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw== + dependencies: + debug "^4.1.0" + detect-node "^2.0.4" + hpack.js "^2.1.6" + obuf "^1.1.2" + readable-stream "^3.0.6" + wbuf "^1.7.3" + +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== + dependencies: + debug "^4.1.0" + handle-thing "^2.0.0" + http-deceiver "^1.2.7" + select-hose "^2.0.0" + spdy-transport "^3.0.0" + +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + +"statuses@>= 1.4.0 < 2": + version "1.5.0" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" + integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA== + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + +strip-final-newline@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" + integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== + +supports-color@^8.0.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tapable@^2.1.1, tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +terser-webpack-plugin@^5.3.10: + version "5.3.10" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz#904f4c9193c6fd2a03f693a2150c62a92f40d199" + integrity sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w== + dependencies: + "@jridgewell/trace-mapping" "^0.3.20" + jest-worker "^27.4.5" + schema-utils "^3.1.1" + serialize-javascript "^6.0.1" + terser "^5.26.0" + +terser@^5.26.0: + version "5.31.6" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.31.6.tgz#c63858a0f0703988d0266a82fcbf2d7ba76422b1" + integrity sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +thunky@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d" + integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA== + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + +type-is@~1.6.18: + version "1.6.18" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" + integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g== + dependencies: + media-typer "0.3.0" + mime-types "~2.1.24" + +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + +update-browserslist-db@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz#7ca61c0d8650766090728046e416a8cde682859e" + integrity sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ== + dependencies: + escalade "^3.1.2" + picocolors "^1.0.1" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1, util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== + +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== + +watchpack@^2.4.1: + version "2.4.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.2.tgz#2feeaed67412e7c33184e5a79ca738fbd38564da" + integrity sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw== + dependencies: + glob-to-regexp "^0.4.1" + graceful-fs "^4.1.2" + +wbuf@^1.1.0, wbuf@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" + integrity sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA== + dependencies: + minimalistic-assert "^1.0.0" + +webpack-cli@^4.9.2: + version "4.10.0" + resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-4.10.0.tgz#37c1d69c8d85214c5a65e589378f53aec64dab31" + integrity sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w== + dependencies: + "@discoveryjs/json-ext" "^0.5.0" + "@webpack-cli/configtest" "^1.2.0" + "@webpack-cli/info" "^1.5.0" + "@webpack-cli/serve" "^1.7.0" + colorette "^2.0.14" + commander "^7.0.0" + cross-spawn "^7.0.3" + fastest-levenshtein "^1.0.12" + import-local "^3.0.2" + interpret "^2.2.0" + rechoir "^0.7.0" + webpack-merge "^5.7.3" + +webpack-dev-middleware@^5.3.4: + version "5.3.4" + resolved "https://registry.yarnpkg.com/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz#eb7b39281cbce10e104eb2b8bf2b63fce49a3517" + integrity sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q== + dependencies: + colorette "^2.0.10" + memfs "^3.4.3" + mime-types "^2.1.31" + range-parser "^1.2.1" + schema-utils "^4.0.0" + +webpack-dev-server@^4.7.4: + version "4.15.2" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz#9e0c70a42a012560860adb186986da1248333173" + integrity sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g== + dependencies: + "@types/bonjour" "^3.5.9" + "@types/connect-history-api-fallback" "^1.3.5" + "@types/express" "^4.17.13" + "@types/serve-index" "^1.9.1" + "@types/serve-static" "^1.13.10" + "@types/sockjs" "^0.3.33" + "@types/ws" "^8.5.5" + ansi-html-community "^0.0.8" + bonjour-service "^1.0.11" + chokidar "^3.5.3" + colorette "^2.0.10" + compression "^1.7.4" + connect-history-api-fallback "^2.0.0" + default-gateway "^6.0.3" + express "^4.17.3" + graceful-fs "^4.2.6" + html-entities "^2.3.2" + http-proxy-middleware "^2.0.3" + ipaddr.js "^2.0.1" + launch-editor "^2.6.0" + open "^8.0.9" + p-retry "^4.5.0" + rimraf "^3.0.2" + schema-utils "^4.0.0" + selfsigned "^2.1.1" + serve-index "^1.9.1" + sockjs "^0.3.24" + spdy "^4.0.2" + webpack-dev-middleware "^5.3.4" + ws "^8.13.0" + +webpack-merge@^5.7.3: + version "5.10.0" + resolved "https://registry.yarnpkg.com/webpack-merge/-/webpack-merge-5.10.0.tgz#a3ad5d773241e9c682803abf628d4cd62b8a4177" + integrity sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA== + dependencies: + clone-deep "^4.0.1" + flat "^5.0.2" + wildcard "^2.0.0" + +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack@^5.70.0: + version "5.94.0" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.94.0.tgz#77a6089c716e7ab90c1c67574a28da518a20970f" + integrity sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg== + dependencies: + "@types/estree" "^1.0.5" + "@webassemblyjs/ast" "^1.12.1" + "@webassemblyjs/wasm-edit" "^1.12.1" + "@webassemblyjs/wasm-parser" "^1.12.1" + acorn "^8.7.1" + acorn-import-attributes "^1.9.5" + browserslist "^4.21.10" + chrome-trace-event "^1.0.2" + enhanced-resolve "^5.17.1" + es-module-lexer "^1.2.1" + eslint-scope "5.1.1" + events "^3.2.0" + glob-to-regexp "^0.4.1" + graceful-fs "^4.2.11" + json-parse-even-better-errors "^2.3.1" + loader-runner "^4.2.0" + mime-types "^2.1.27" + neo-async "^2.6.2" + schema-utils "^3.2.0" + tapable "^2.1.1" + terser-webpack-plugin "^5.3.10" + watchpack "^2.4.1" + webpack-sources "^3.2.3" + +websocket-driver@>=0.5.1, websocket-driver@^0.7.4: + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== + dependencies: + http-parser-js ">=0.5.1" + safe-buffer ">=5.1.0" + websocket-extensions ">=0.1.1" + +websocket-extensions@>=0.1.1: + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +wildcard@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/wildcard/-/wildcard-2.0.1.tgz#5ab10d02487198954836b6349f74fff961e10f67" + integrity sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ== + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@^8.13.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== diff --git a/nym-credential-proxy/vpn-api-lib-wasm/src/error.rs b/nym-credential-proxy/vpn-api-lib-wasm/src/error.rs new file mode 100644 index 0000000000..7322254064 --- /dev/null +++ b/nym-credential-proxy/vpn-api-lib-wasm/src/error.rs @@ -0,0 +1,35 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use serde_wasm_bindgen::Error; +use thiserror::Error; +use wasm_utils::wasm_error; + +#[derive(Debug, Error)] +pub enum VpnApiLibError { + #[error("{0}")] + Json(String), + + #[error("[ecash] cryptographic failure: {source}")] + EcashFailure { + #[from] + source: nym_compact_ecash::CompactEcashError, + }, + + #[error("provided invalid ticket type")] + MalformedTicketType, + + #[error("the provided shares and issuers are not from the same epoch! {shares} and {issuers}")] + InconsistentEpochId { shares: u64, issuers: u64 }, + + #[error("failed to recover ed25519 private key from its base58 representation")] + MalformedEd25519Key, +} + +wasm_error!(VpnApiLibError); + +impl From for VpnApiLibError { + fn from(value: Error) -> Self { + VpnApiLibError::Json(value.to_string()) + } +} diff --git a/nym-credential-proxy/vpn-api-lib-wasm/src/lib.rs b/nym-credential-proxy/vpn-api-lib-wasm/src/lib.rs new file mode 100644 index 0000000000..02c3b757f3 --- /dev/null +++ b/nym-credential-proxy/vpn-api-lib-wasm/src/lib.rs @@ -0,0 +1,274 @@ +// Copyright 2024 Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::error::VpnApiLibError; +use nym_compact_ecash::scheme::keygen::KeyPairUser; +use nym_compact_ecash::scheme::withdrawal::RequestInfo; +use nym_compact_ecash::{ + aggregate_wallets, issue_verify, withdrawal_request, Base58, BlindedSignature, + VerificationKeyAuth, WithdrawalRequest, +}; +use nym_credential_proxy_requests::api::v1::ticketbook::models::{ + MasterVerificationKeyResponse, PartialVerificationKeysResponse, TicketbookRequest, + TicketbookWalletSharesResponse, WalletShare, +}; +use nym_credentials::{ + AggregatedCoinIndicesSignatures, AggregatedExpirationDateSignatures, EpochVerificationKey, + IssuedTicketBook, +}; +use nym_credentials_interface::TicketType; +use nym_crypto::asymmetric::ed25519; +use nym_ecash_time::{ecash_default_expiration_date, EcashTime}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use time::Date; +use tsify::Tsify; +use wasm_bindgen::prelude::*; +use wasm_utils::console_error; +use zeroize::Zeroizing; + +pub mod error; + +#[derive(Tsify, Debug, Default, Clone, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct WalletShares(Vec); + +pub type WalletIssuers = PartialVerificationKeysResponse; + +impl From> for WalletShares { + fn from(shares: Vec) -> Self { + WalletShares(shares) + } +} + +#[derive(Tsify, Debug, Default, Clone, Serialize, Deserialize)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct NymIssuanceTicketbookOpts { + #[tsify(optional)] + pub ticketbook_type: Option, + + // bs58-encoded user secret key used for seeding the ecash crypto keypair generation + // I reiterate, this is a **SECRET** key, not a public key. + #[tsify(optional)] + pub user_secret_key: Option, +} + +#[wasm_bindgen] +#[derive(Debug)] +#[allow(dead_code)] +pub struct NymIssuanceTicketbook { + /// ecash keypair related to the credential + ecash_keypair: KeyPairUser, + + withdrawal_request: WithdrawalRequest, + + ticketbook_type: TicketType, + + expiration_date: Date, + + request_info: Zeroizing, +} + +#[wasm_bindgen] +impl NymIssuanceTicketbook { + #[wasm_bindgen(constructor)] + pub fn new(opts: NymIssuanceTicketbookOpts) -> Result { + let ecash_keypair = match opts.user_secret_key { + None => KeyPairUser::new(), + Some(maybe_sk) => { + let pk = ed25519::PrivateKey::from_base58_string(maybe_sk) + .map(Zeroizing::new) + .map_err(|_| VpnApiLibError::MalformedEd25519Key)?; + let bytes = Zeroizing::new(pk.to_bytes()); + KeyPairUser::new_seeded(&bytes) + } + }; + + let ticketbook_type = match opts.ticketbook_type { + None => TicketType::V1MixnetEntry, + Some(typ) => typ + .parse() + .map_err(|_| VpnApiLibError::MalformedTicketType)?, + }; + + let expiration_date = ecash_default_expiration_date(); + + let (withdrawal_request, request_info) = withdrawal_request( + ecash_keypair.secret_key(), + expiration_date.ecash_unix_timestamp(), + ticketbook_type.encode(), + )?; + + Ok(NymIssuanceTicketbook { + ecash_keypair, + withdrawal_request, + ticketbook_type, + expiration_date, + request_info: Zeroizing::new(request_info), + }) + } + + #[wasm_bindgen(js_name = "buildRequestPayload")] + pub fn build_request_payload(&self, is_freepass_request: bool) -> String { + serde_json::to_string(&TicketbookRequest { + withdrawal_request: self.withdrawal_request.clone().into(), + ecash_pubkey: self.ecash_keypair.public_key(), + expiration_date: self.expiration_date, + ticketbook_type: self.ticketbook_type, + is_freepass_request, + }) + .unwrap() + } + + #[wasm_bindgen(js_name = "getWithdrawalRequest")] + pub fn get_encoded_withdrawal_request(&self) -> String { + self.withdrawal_request.to_bs58() + } + + #[wasm_bindgen(js_name = "getEncodedPublicKey")] + pub fn get_encoded_public_key(&self) -> String { + self.ecash_keypair.public_key().to_bs58() + } + + // + // #[wasm_bindgen(js_name = "unblindShare")] + // pub fn unblind_share(&self, share: UnblindableShare) -> Result { + // let blinded_sig = BlindedSignature::try_from_bs58(share.blinded_share_bs58)?; + // let vk = VerificationKey::try_from_bs58(share.issuer_key_bs58)?; + // + // Ok(blinded_sig + // .unblind(&vk, &self.pedersen_commitments_openings) + // .into()) + // } + // + #[wasm_bindgen(js_name = "unblindWalletShares")] + pub fn unblind_wallet_shares( + self, + shares: JsValue, + issuers: WalletIssuers, + master_key: MasterVerificationKeyResponse, + ) -> Result { + // we couldn't derive all the required abi traits due to crypto types deep in the stack + let shares: TicketbookWalletSharesResponse = serde_wasm_bindgen::from_value(shares)?; + + if shares.epoch_id != issuers.epoch_id { + console_error!( + "the provided shares and issuers are not from the same epoch! {} and {}", + shares.epoch_id, + issuers.epoch_id + ); + return Err(VpnApiLibError::InconsistentEpochId { + shares: shares.epoch_id, + issuers: issuers.epoch_id, + }); + } + + let master_vk = VerificationKeyAuth::try_from_bs58(master_key.bs58_encoded_key)?; + + let mut decoded_keys = HashMap::new(); + for key in issuers.keys { + let vk = VerificationKeyAuth::try_from_bs58(key.bs58_encoded_key)?; + decoded_keys.insert(key.node_index, vk); + } + + let mut partial_wallets = Vec::new(); + for share in shares.shares { + let blinded_sig = BlindedSignature::try_from_bs58(share.bs58_encoded_share)?; + let Some(vk) = decoded_keys.get(&share.node_index) else { + console_error!("received a share from issuer {} but did not receive a corresponding verification key!", share.node_index); + continue; + }; + + match issue_verify( + vk, + self.ecash_keypair.secret_key(), + &blinded_sig, + &self.request_info, + share.node_index, + ) { + Ok(partial_wallet) => partial_wallets.push(partial_wallet), + Err(err) => { + console_error!( + "failed to unblind partial wallet corresponding to index {}: {err}", + share.node_index + ) + } + } + } + + let aggregated_wallet = aggregate_wallets( + &master_vk, + self.ecash_keypair.secret_key(), + &partial_wallets, + &self.request_info, + )?; + + Ok(NymIssuedTicketbook { + inner_ticketbook: IssuedTicketBook::new( + aggregated_wallet.into_wallet_signatures(), + shares.epoch_id, + self.ecash_keypair.into(), + self.ticketbook_type, + self.expiration_date, + ), + master_vk: EpochVerificationKey { + epoch_id: shares.epoch_id, + key: master_vk, + }, + expiration_date_signatures: shares + .aggregated_expiration_date_signatures + .map(|s| s.signatures), + coin_index_signatures: shares + .aggregated_coin_index_signatures + .map(|s| s.signatures), + }) + } +} + +#[wasm_bindgen] +pub struct NymIssuedTicketbook { + inner_ticketbook: IssuedTicketBook, + + master_vk: EpochVerificationKey, + expiration_date_signatures: Option, + coin_index_signatures: Option, +} + +#[wasm_bindgen] +impl NymIssuedTicketbook { + pub fn serialise(self) -> FullSerialisedNymIssuedTicketbook { + let serialised = self + .inner_ticketbook + .begin_export() + .with_master_verification_key(&self.master_vk) + .with_maybe_expiration_date_signatures(&self.expiration_date_signatures) + .with_maybe_coin_index_signatures(&self.coin_index_signatures) + .finalize_export(); + + FullSerialisedNymIssuedTicketbook { + serialisation_revision: serialised.revision, + bs58_encoded_data: bs58::encode(serialised.data).into_string(), + } + } +} + +#[derive(Tsify, Serialize, Deserialize, Debug, PartialEq, Eq)] +#[tsify(into_wasm_abi, from_wasm_abi)] +#[serde(rename_all = "camelCase")] +pub struct FullSerialisedNymIssuedTicketbook { + pub serialisation_revision: u8, + pub bs58_encoded_data: String, +} + +#[wasm_bindgen(start)] +pub fn main() { + wasm_utils::console_log!("[rust main]: rust module loaded"); + wasm_utils::console_log!( + "vpn-api-lib version used:\n{}", + nym_bin_common::bin_info!().pretty_print() + ); + wasm_utils::console_log!("[rust main]: setting panic hook"); + wasm_utils::set_panic_hook(); +} diff --git a/nym-data-observatory/.sqlx/query-249faa11b88b749f50342bb5c9cc41d20896db543eed74a6f320c041bcbb723d.json b/nym-data-observatory/.sqlx/query-249faa11b88b749f50342bb5c9cc41d20896db543eed74a6f320c041bcbb723d.json new file mode 100644 index 0000000000..a42f7b7e32 --- /dev/null +++ b/nym-data-observatory/.sqlx/query-249faa11b88b749f50342bb5c9cc41d20896db543eed74a6f320c041bcbb723d.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO responses\n (joke_id, joke, date_created)\n VALUES\n ($1, $2, $3)\n ON CONFLICT(joke_id) DO UPDATE SET\n joke=excluded.joke,\n date_created=excluded.date_created;", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Varchar", + "Text", + "Int4" + ] + }, + "nullable": [] + }, + "hash": "249faa11b88b749f50342bb5c9cc41d20896db543eed74a6f320c041bcbb723d" +} diff --git a/nym-data-observatory/.sqlx/query-aff7fbd06728004d2f2226d20c32f1482df00de2dc1d2b4debbb2e12553d997b.json b/nym-data-observatory/.sqlx/query-aff7fbd06728004d2f2226d20c32f1482df00de2dc1d2b4debbb2e12553d997b.json new file mode 100644 index 0000000000..88e19ca1b9 --- /dev/null +++ b/nym-data-observatory/.sqlx/query-aff7fbd06728004d2f2226d20c32f1482df00de2dc1d2b4debbb2e12553d997b.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT joke_id, joke, date_created FROM responses WHERE joke_id = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "joke_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "joke", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "date_created", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "aff7fbd06728004d2f2226d20c32f1482df00de2dc1d2b4debbb2e12553d997b" +} diff --git a/nym-data-observatory/.sqlx/query-e53f479f8cead3dc8aa1875e5d450ad69686cf6a109e37d6c3f0623c3e9f91d0.json b/nym-data-observatory/.sqlx/query-e53f479f8cead3dc8aa1875e5d450ad69686cf6a109e37d6c3f0623c3e9f91d0.json new file mode 100644 index 0000000000..e770f72289 --- /dev/null +++ b/nym-data-observatory/.sqlx/query-e53f479f8cead3dc8aa1875e5d450ad69686cf6a109e37d6c3f0623c3e9f91d0.json @@ -0,0 +1,32 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT joke_id, joke, date_created FROM responses", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "joke_id", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "joke", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "date_created", + "type_info": "Int4" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "e53f479f8cead3dc8aa1875e5d450ad69686cf6a109e37d6c3f0623c3e9f91d0" +} diff --git a/nym-data-observatory/Cargo.toml b/nym-data-observatory/Cargo.toml index b4a1ea8619..220449bfb1 100644 --- a/nym-data-observatory/Cargo.toml +++ b/nym-data-observatory/Cargo.toml @@ -17,14 +17,17 @@ readme.workspace = true anyhow = { workspace = true } axum = { workspace = true, features = ["tokio"] } chrono = { workspace = true } +clap = { workspace = true, features = ["derive", "env"] } nym-bin-common = { path = "../common/bin-common" } nym-network-defaults = { path = "../common/network-defaults" } nym-task = { path = "../common/task" } -nym-node-requests = { path = "../nym-node/nym-node-requests", features = ["openapi"] } +nym-node-requests = { path = "../nym-node/nym-node-requests", features = [ + "openapi", +] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres", "offline"] } -tokio = { workspace = true, features = ["process"] } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres"] } +tokio = { workspace = true, features = ["process", "rt-multi-thread"] } tokio-util = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } @@ -36,5 +39,5 @@ utoipauto = { workspace = true } [build-dependencies] anyhow = { workspace = true } -tokio = { workspace = true, features = ["macros" ] } +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres"] } diff --git a/nym-data-observatory/Dockerfile b/nym-data-observatory/Dockerfile new file mode 100644 index 0000000000..bc756ebaa5 --- /dev/null +++ b/nym-data-observatory/Dockerfile @@ -0,0 +1,15 @@ +FROM rust:latest AS builder + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-data-observatory + +RUN cargo build --release + +FROM ubuntu:24.04 + +RUN apt update && apt install -yy curl ca-certificates + +WORKDIR /nym + +COPY --from=builder /usr/src/nym/target/release/nym-data-observatory ./ +ENTRYPOINT [ "/nym/nym-data-observatory" ] diff --git a/nym-data-observatory/README_SQLX.md b/nym-data-observatory/README_SQLX.md index 706f1d47ad..72cca4294f 100644 --- a/nym-data-observatory/README_SQLX.md +++ b/nym-data-observatory/README_SQLX.md @@ -39,35 +39,25 @@ If it's outside of code (i.e. when running `cargo check`) - make sure password doesn't have any special characters that could be interpreted by the command line/shell weirdly, like `$#\` etc. -## Cannot generate `sqlx-data.json` +## Offline query data looks like this -In order for `sqlx` to generate schema for "offline" work (without DB -connection), as of `v0.6.3` you first **need an active DB connection**. - -So make sure - -- DB is running -- `DATABASE_URL` is set correctly -- `SQLX_OFFLINE` isn't exported to true - -Then run `cargo sqlx prepare` - -After you have the file, you can ignore `DATABASE_URL` and terminate the DB -instance. This file represents the DB schema, so when your migrations change, -you'll need to re-generate it - -Make sure to commit the file to VCS if you want to avoid re-doing this again on -each machine (e.g. other developers, CI). +``` +.sqlx/ +├─ new_file +├─ query-249faa11b88b749f50342bb5c9cc41d20896db543eed74a6f320c041bcbb723d.json +├─ query-aff7fbd06728004d2f2226d20c32f1482df00de2dc1d2b4debbb2e12553d997b.json +├─ ... +├─ query-e53f479f8cead3dc8aa1875e5d450ad69686cf6a109e37d6c3f0623c3e9f91d0.json +``` -## Generated `sqlx-data.json` looks like this +The offline mode for the queries uses a separate file per `query!()` invocation -```json -{ - "db": "PostgreSQL" -} -``` +Each workspace member that works with `sqlx` has `.sqlx` directory, containing +its own schema description. This allows compile-time checks without needing a +live DB connection (so called `OFFLINE_MODE`). -after running `cargo sqlx prepare` +To initialize those files, you need to run `cargo sqlx prepare` with a live +connection to DB (to pull schema information). ### Similar to: @@ -78,6 +68,18 @@ warning: no queries found; do you have the `offline` feature enabled ### Possible solutions - does your `sqlx-cli` version match `sqlx` version from `Cargo.toml`? + + `cargo install -f sqlx-cli --version ` +``` +cargo install sqlx-cli --version --force +``` +- is your crate a library? +``` +cargo sqlx prepare -- --lib +``` +- are your `query!` invocations hidden behind a feature? +``` +cargo sqlx prepare -- --features +``` - do you have `offline` cargo feature enabled? - make sure to `cargo clean` after these updates diff --git a/nym-data-observatory/build.rs b/nym-data-observatory/build.rs index f9c96061da..ba795cf1d0 100644 --- a/nym-data-observatory/build.rs +++ b/nym-data-observatory/build.rs @@ -14,9 +14,9 @@ async fn main() -> Result<()> { let db_url = format!("postgresql://{POSTGRES_USER}:{POSTGRES_PASSWORD}@localhost:5432/{POSTGRES_DB}"); + export_db_variables(&db_url)?; // if a live DB is reachable, use that if PgConnection::connect(&db_url).await.is_ok() { - export_db_variables(&db_url)?; println!("cargo::rustc-env=SQLX_OFFLINE=false"); run_migrations(&db_url).await?; } else { diff --git a/nym-data-observatory/docker-compose.yml b/nym-data-observatory/docker-compose.yml index 3d93d058f3..15da0609b7 100644 --- a/nym-data-observatory/docker-compose.yml +++ b/nym-data-observatory/docker-compose.yml @@ -2,12 +2,24 @@ services: postgres: image: postgres:13 container_name: nym-data-observatory-pg - env_file: - - .env + environment: + POSTGRES_PASSWORD: password ports: - "5432:5432" volumes: - pgdata:/var/lib/postgresql/data + data-observatory: + depends_on: + - postgres + image: nym-data-observatory:latest + build: + context: ../ + dockerfile: nym-data-observatory/Dockerfile + container_name: nym-data-observatory + environment: + NYM_DATA_OBSERVATORY_CONNECTION_URL: "postgres://postgres:password@postgres:5432" + NYM_DATA_OBSERVATORY_HTTP_PORT: 8000 + volumes: pgdata: diff --git a/nym-data-observatory/pg_up.sh b/nym-data-observatory/pg_up.sh index 880131dd93..ca4e1f7e51 100755 --- a/nym-data-observatory/pg_up.sh +++ b/nym-data-observatory/pg_up.sh @@ -1,5 +1,6 @@ #!/bin/bash +# .env is generated in build.rs source .env # Launching a container in such a way that it's destroyed after you detach from the terminal: diff --git a/nym-data-observatory/sqlx-data.json b/nym-data-observatory/sqlx-data.json deleted file mode 100644 index 5164e48b6b..0000000000 --- a/nym-data-observatory/sqlx-data.json +++ /dev/null @@ -1,79 +0,0 @@ -{ - "db": "PostgreSQL", - "249faa11b88b749f50342bb5c9cc41d20896db543eed74a6f320c041bcbb723d": { - "describe": { - "columns": [], - "nullable": [], - "parameters": { - "Left": [ - "Varchar", - "Text", - "Int4" - ] - } - }, - "query": "INSERT INTO responses\n (joke_id, joke, date_created)\n VALUES\n ($1, $2, $3)\n ON CONFLICT(joke_id) DO UPDATE SET\n joke=excluded.joke,\n date_created=excluded.date_created;" - }, - "aff7fbd06728004d2f2226d20c32f1482df00de2dc1d2b4debbb2e12553d997b": { - "describe": { - "columns": [ - { - "name": "joke_id", - "ordinal": 0, - "type_info": "Varchar" - }, - { - "name": "joke", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "date_created", - "ordinal": 2, - "type_info": "Int4" - } - ], - "nullable": [ - false, - false, - false - ], - "parameters": { - "Left": [ - "Text" - ] - } - }, - "query": "SELECT joke_id, joke, date_created FROM responses WHERE joke_id = $1" - }, - "e53f479f8cead3dc8aa1875e5d450ad69686cf6a109e37d6c3f0623c3e9f91d0": { - "describe": { - "columns": [ - { - "name": "joke_id", - "ordinal": 0, - "type_info": "Varchar" - }, - { - "name": "joke", - "ordinal": 1, - "type_info": "Text" - }, - { - "name": "date_created", - "ordinal": 2, - "type_info": "Int4" - } - ], - "nullable": [ - false, - false, - false - ], - "parameters": { - "Left": [] - } - }, - "query": "SELECT joke_id, joke, date_created FROM responses" - } -} \ No newline at end of file diff --git a/nym-data-observatory/src/db/mod.rs b/nym-data-observatory/src/db/mod.rs index 307d2a97b3..05c71a1728 100644 --- a/nym-data-observatory/src/db/mod.rs +++ b/nym-data-observatory/src/db/mod.rs @@ -5,8 +5,6 @@ use std::str::FromStr; pub(crate) mod models; pub(crate) mod queries; -pub(crate) const DATABASE_URL_ENV_VAR: &str = "DATABASE_URL"; - static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); pub(crate) type DbPool = PgPool; @@ -16,13 +14,11 @@ pub(crate) struct Storage { } impl Storage { - pub async fn init() -> Result { - let connection_url = std::env::var(DATABASE_URL_ENV_VAR).map_err(anyhow::Error::from)?; - let connect_options = { - let mut connect_options = PgConnectOptions::from_str(&connection_url)?; - let connect_options = connect_options.disable_statement_logging(); - (*connect_options).clone() - }; + pub async fn init(connection_url: Option) -> Result { + let connection_url = + connection_url.ok_or_else(|| anyhow!("Missing the connection url for database!"))?; + let connect_options = + PgConnectOptions::from_str(&connection_url)?.disable_statement_logging(); let pool = DbPool::connect_with(connect_options) .await diff --git a/nym-data-observatory/src/http/config.rs b/nym-data-observatory/src/http/config.rs deleted file mode 100644 index e2088eb32d..0000000000 --- a/nym-data-observatory/src/http/config.rs +++ /dev/null @@ -1,23 +0,0 @@ -use crate::read_env_var; - -#[derive(Debug)] -pub(crate) struct Config { - http_port: u16, -} - -const HTTP_PORT_DEFAULT: u16 = 8000; - -impl Config { - pub(crate) fn from_env() -> Self { - Self { - http_port: read_env_var("HTTP_PORT") - .unwrap_or(HTTP_PORT_DEFAULT.to_string()) - .parse() - .unwrap_or(HTTP_PORT_DEFAULT), - } - } - - pub(crate) fn http_port(&self) -> u16 { - self.http_port - } -} diff --git a/nym-data-observatory/src/http/mod.rs b/nym-data-observatory/src/http/mod.rs index 683391c839..1506514f0f 100644 --- a/nym-data-observatory/src/http/mod.rs +++ b/nym-data-observatory/src/http/mod.rs @@ -1,6 +1,5 @@ pub(crate) mod api; pub(crate) mod api_docs; -pub(crate) mod config; pub(crate) mod error; pub(crate) mod server; pub(crate) mod state; diff --git a/nym-data-observatory/src/main.rs b/nym-data-observatory/src/main.rs index 074279d2ed..4f902f4b46 100644 --- a/nym-data-observatory/src/main.rs +++ b/nym-data-observatory/src/main.rs @@ -1,36 +1,47 @@ +use clap::Parser; use nym_network_defaults::setup_env; use nym_task::signal::wait_for_signal; -use crate::http::config; - mod background_task; mod db; mod http; mod logging; +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Port to listen on + #[arg(long, default_value_t = 8000, env = "NYM_DATA_OBSERVATORY_HTTP_PORT")] + http_port: u16, + + /// Path to the environment variables file. If you don't provide one, variables for the mainnet will be used. + #[arg(short, long, default_value = None, env = "NYM_DATA_OBSERVATORY_ENV_FILE")] + env_file: Option, + + /// DB connection url + #[arg(short, long, default_value = None, env = "NYM_DATA_OBSERVATORY_CONNECTION_URL")] + connection_url: Option, +} + #[tokio::main] async fn main() -> anyhow::Result<()> { logging::setup_tracing_logger(); - // if dotenv file is present, load its values - // otherwise, default to mainnet - setup_env(Some(".env")); + let args = Args::parse(); - let conf = config::Config::from_env(); - tracing::debug!("Using config:\n{:?}", conf); + setup_env(args.env_file); // Defaults to mainnet if empty - let storage = db::Storage::init().await?; + let storage = db::Storage::init(args.connection_url).await?; let db_pool = storage.pool_owned().await; tokio::spawn(async move { background_task::spawn_in_background(db_pool).await; tracing::info!("Started task"); }); - let shutdown_handles = - http::server::start_http_api(storage.pool_owned().await, conf.http_port()) - .await - .expect("Failed to start server"); - tracing::info!("Started HTTP server on port {}", conf.http_port()); + let shutdown_handles = http::server::start_http_api(storage.pool_owned().await, args.http_port) + .await + .expect("Failed to start server"); + tracing::info!("Started HTTP server on port {}", args.http_port); wait_for_signal().await; @@ -40,13 +51,3 @@ async fn main() -> anyhow::Result<()> { Ok(()) } - -// TODO dz move this to common -fn read_env_var(env_var: &str) -> anyhow::Result { - std::env::var(env_var) - .map_err(|_| anyhow::anyhow!("You need to set {}", env_var)) - .map(|value| { - tracing::trace!("{}={}", env_var, value); - value - }) -} diff --git a/nym-network-monitor/src/accounting.rs b/nym-network-monitor/src/accounting.rs index d658e9dc63..b87f795762 100644 --- a/nym-network-monitor/src/accounting.rs +++ b/nym-network-monitor/src/accounting.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use anyhow::Result; use futures::{stream::FuturesUnordered, StreamExt}; use log::{debug, info}; -use nym_sphinx::chunking::{SentFragment, FRAGMENTS_RECEIVED, FRAGMENTS_SENT}; +use nym_sphinx::chunking::{monitoring, SentFragment}; use nym_topology::{gateway, mix, NymTopology}; use nym_types::monitoring::{MonitorMessage, NodeResult}; use nym_validator_client::nym_api::routes::{API_VERSION, STATUS, SUBMIT_GATEWAY, SUBMIT_NODE}; @@ -15,16 +15,16 @@ use utoipa::ToSchema; use crate::{NYM_API_URL, PRIVATE_KEY, TOPOLOGY}; struct HydratedRoute { - mix_nodes: Vec, - gateway_node: gateway::Node, + mix_nodes: Vec, + gateway_node: gateway::LegacyNode, } #[derive(Serialize, Deserialize, Debug, Default, ToSchema)] -struct GatewayStats(u32, u32, Option); +struct GatewayStats(u32, u32); impl GatewayStats { - fn new(sent: u32, recv: u32, owner: Option) -> Self { - GatewayStats(sent, recv, owner) + fn new(sent: u32, recv: u32) -> Self { + GatewayStats(sent, recv) } fn success(&self) -> u32 { @@ -60,9 +60,9 @@ pub struct NetworkAccount { topology: NymTopology, tested_nodes: HashSet, #[serde(skip)] - mix_details: HashMap, + mix_details: HashMap, #[serde(skip)] - gateway_details: HashMap, + gateway_details: HashMap, } impl NetworkAccount { @@ -82,7 +82,6 @@ impl NetworkAccount { complete_routes, incomplete_routes, node.identity_key.to_base58_string(), - node.owner.clone(), ) } @@ -116,8 +115,8 @@ impl NetworkAccount { } pub fn empty_buffers() { - FRAGMENTS_SENT.clear(); - FRAGMENTS_RECEIVED.clear(); + monitoring::FRAGMENTS_SENT.clear(); + monitoring::FRAGMENTS_RECEIVED.clear(); } fn new() -> Self { @@ -126,7 +125,7 @@ impl NetworkAccount { topology, ..Default::default() }; - for fragment_set in FRAGMENTS_SENT.iter() { + for fragment_set in monitoring::FRAGMENTS_SENT.iter() { let sent_fragments = fragment_set .value() .first() @@ -139,7 +138,7 @@ impl NetworkAccount { sent_fragments ); - let recv = FRAGMENTS_RECEIVED.get(fragment_set.key()); + let recv = monitoring::FRAGMENTS_RECEIVED.get(fragment_set.key()); let recv_fragments = recv.as_ref().map(|r| r.value().len()).unwrap_or(0); debug!( "RECV Fragment set {} has {} fragments", @@ -171,7 +170,7 @@ impl NetworkAccount { } fn hydrate_all_fragments(&mut self) -> Result<()> { - for fragment_set in FRAGMENTS_SENT.iter() { + for fragment_set in monitoring::FRAGMENTS_SENT.iter() { let fragment_set_id = fragment_set.key(); for fragment in fragment_set.value() { let route = self.hydrate_route(fragment.clone())?; @@ -186,7 +185,7 @@ impl NetworkAccount { let gateway_stats_entry = self .gateway_stats .entry(route.gateway_node.identity_key.to_base58_string()) - .or_insert(GatewayStats::new(0, 0, route.gateway_node.owner.clone())); + .or_insert(GatewayStats::new(0, 0)); self.gateway_details.insert( route.gateway_node.identity_key.to_base58_string(), route.gateway_node, @@ -206,7 +205,7 @@ impl NetworkAccount { fn find_missing_fragments(&mut self) { let mut missing_fragments_map = HashMap::new(); for fragment_set_id in &self.incomplete_fragment_sets { - if let Some(fragment_ref) = FRAGMENTS_RECEIVED.get(fragment_set_id) { + if let Some(fragment_ref) = monitoring::FRAGMENTS_RECEIVED.get(fragment_set_id) { if let Some(ref_fragment) = fragment_ref.value().first() { let ref_header = ref_fragment.header(); let ref_id_set = (0..ref_header.total_fragments()).collect::>(); @@ -265,7 +264,6 @@ pub struct NodeStats { incomplete_routes: usize, reliability: f64, identity: String, - owner: Option, } impl NodeStats { @@ -274,7 +272,6 @@ impl NodeStats { complete_routes: usize, incomplete_routes: usize, identity: String, - owner: Option, ) -> Self { NodeStats { mix_id, @@ -282,7 +279,6 @@ impl NodeStats { incomplete_routes, reliability: complete_routes as f64 / (complete_routes + incomplete_routes) as f64, identity, - owner, } } diff --git a/nym-network-monitor/src/handlers.rs b/nym-network-monitor/src/handlers.rs index 50c3ee749f..dbdafc4700 100644 --- a/nym-network-monitor/src/handlers.rs +++ b/nym-network-monitor/src/handlers.rs @@ -6,7 +6,7 @@ use axum::{ use futures::StreamExt; use log::{debug, error, warn}; use nym_sdk::mixnet::MixnetMessageSender; -use nym_sphinx::chunking::{ReceivedFragment, SentFragment, FRAGMENTS_RECEIVED, FRAGMENTS_SENT}; +use nym_sphinx::chunking::{monitoring, ReceivedFragment, SentFragment}; use petgraph::{dot::Dot, Graph}; use rand::{distributions::Alphanumeric, seq::SliceRandom, Rng}; use serde::Serialize; @@ -113,7 +113,7 @@ pub async fn graph_handler() -> Result { )] pub async fn sent_handler() -> Json { Json(FragmentsSent( - (*FRAGMENTS_SENT) + (*monitoring::FRAGMENTS_SENT) .clone() .into_iter() .collect::>(), @@ -129,7 +129,7 @@ pub async fn sent_handler() -> Json { )] pub async fn recv_handler() -> Json { Json(FragmentsReceived( - (*FRAGMENTS_RECEIVED) + (*monitoring::FRAGMENTS_RECEIVED) .clone() .into_iter() .collect::>(), diff --git a/nym-network-monitor/src/main.rs b/nym-network-monitor/src/main.rs index d2ad40707e..36b206b712 100644 --- a/nym-network-monitor/src/main.rs +++ b/nym-network-monitor/src/main.rs @@ -7,6 +7,7 @@ use nym_crypto::asymmetric::ed25519::PrivateKey; use nym_network_defaults::setup_env; use nym_network_defaults::var_names::NYM_API; use nym_sdk::mixnet::{self, MixnetClient}; +use nym_sphinx::chunking::monitoring; use nym_topology::{HardcodedTopologyProvider, NymTopology}; use std::fs::File; use std::io::Write; @@ -154,6 +155,9 @@ async fn main() -> Result<()> { setup_env(args.env); // Defaults to mainnet if empty + // enable monitoring client-side + monitoring::enable(); + let cancel_token = CancellationToken::new(); let server_cancel_token = cancel_token.clone(); let clients = Arc::new(RwLock::new(VecDeque::with_capacity(args.n_clients))); diff --git a/nym-node-status-agent/.gitignore b/nym-node-status-agent/.gitignore new file mode 100644 index 0000000000..bf462f3e05 --- /dev/null +++ b/nym-node-status-agent/.gitignore @@ -0,0 +1 @@ +nym-gateway-probe diff --git a/nym-node-status-agent/Cargo.toml b/nym-node-status-agent/Cargo.toml new file mode 100644 index 0000000000..f7d8543a1f --- /dev/null +++ b/nym-node-status-agent/Cargo.toml @@ -0,0 +1,27 @@ +# Copyright 2024 - Nym Technologies SA +# SPDX-License-Identifier: Apache-2.0 + + +[package] +name = "nym-node-status-agent" +version = "0.1.6" +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} +clap = { workspace = true, features = ["derive", "env"] } +nym-bin-common = { path = "../common/bin-common", features = ["models"]} +nym-common-models = { path = "../common/models" } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "process"] } +tokio-util = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +reqwest = { workspace = true, features = ["json"] } +serde_json = { workspace = true } diff --git a/nym-node-status-agent/Dockerfile b/nym-node-status-agent/Dockerfile new file mode 100644 index 0000000000..b50ce8be3d --- /dev/null +++ b/nym-node-status-agent/Dockerfile @@ -0,0 +1,40 @@ +FROM rust:latest AS builder + +ARG GIT_REF=main + +RUN apt update && apt install -yy libdbus-1-dev pkg-config libclang-dev + +# Install go +RUN wget https://go.dev/dl/go1.22.5.linux-amd64.tar.gz -O go.tar.gz +RUN tar -xzvf go.tar.gz -C /usr/local + +RUN git clone https://github.com/nymtech/nym-vpn-client /usr/src/nym-vpn-client +RUN cd /usr/src/nym-vpn-client && git checkout $GIT_REF +ENV PATH=/go/bin:/usr/local/go/bin:$PATH +WORKDIR /usr/src/nym-vpn-client/nym-vpn-core +RUN cargo build --release --package nym-gateway-probe + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-node-status-agent +RUN cargo build --release + +#------------------------------------------------------------------- +# The following environment variables are required at runtime: +# +# NODE_STATUS_AGENT_SERVER_ADDRESS +# NODE_STATUS_AGENT_SERVER_PORT +# +# see https://github.com/nymtech/nym/blob/develop/nym-node-status-agent/src/cli.rs for details +#------------------------------------------------------------------- + +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y ca-certificates + +WORKDIR /nym + +COPY --from=builder /usr/src/nym/target/release/nym-node-status-agent ./ +COPY --from=builder /usr/src/nym-vpn-client/nym-vpn-core/target/release/nym-gateway-probe ./ + +ENV NODE_STATUS_AGENT_PROBE_PATH=/nym/nym-gateway-probe +ENTRYPOINT [ "/nym/nym-node-status-agent", "run-probe" ] \ No newline at end of file diff --git a/nym-node-status-agent/run.sh b/nym-node-status-agent/run.sh new file mode 100755 index 0000000000..675e39c109 --- /dev/null +++ b/nym-node-status-agent/run.sh @@ -0,0 +1,55 @@ +#!/bin/bash + +set -eu + +environment="qa" + +source ../envs/${environment}.env + +export RUST_LOG="debug" + +crate_root=$(dirname $(realpath "$0")) +gateway_probe_src=$(dirname $(dirname "$crate_root"))/nym-vpn-client/nym-vpn-core +echo "gateway_probe_src=$gateway_probe_src" +echo "crate_root=$crate_root" + +export NODE_STATUS_AGENT_PROBE_PATH="$crate_root/nym-gateway-probe" + +# build & copy over GW probe +function copy_gw_probe() { + pushd $gateway_probe_src + git switch main + git pull + cargo build --release --package nym-gateway-probe + cp target/release/nym-gateway-probe "$crate_root" + $crate_root/nym-gateway-probe --version + popd +} + +function build_agent() { + cargo build --package nym-node-status-agent --release +} + +function swarm() { + local workers=$1 + echo "Running $workers in parallel" + + build_agent + + for ((i = 1; i <= $workers; i++)); do + ../target/release/nym-node-status-agent run-probe & + done + + wait + + echo "All agents completed" +} + +export NODE_STATUS_AGENT_SERVER_ADDRESS="http://127.0.0.1" +export NODE_STATUS_AGENT_SERVER_PORT="8000" + +copy_gw_probe + +swarm 8 + +# cargo run -- run-probe diff --git a/nym-node-status-agent/src/cli.rs b/nym-node-status-agent/src/cli.rs new file mode 100644 index 0000000000..a7bbef5ebe --- /dev/null +++ b/nym-node-status-agent/src/cli.rs @@ -0,0 +1,118 @@ +use anyhow::bail; +use clap::{Parser, Subcommand}; +use nym_bin_common::bin_info; +use nym_common_models::ns_api::TestrunAssignment; +use std::sync::OnceLock; +use tracing::instrument; + +use crate::probe::GwProbe; + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Parser, Debug)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Args { + #[command(subcommand)] + pub(crate) command: Command, + #[arg(short, long, env = "NODE_STATUS_AGENT_SERVER_ADDRESS")] + pub(crate) server_address: String, + + #[arg(short = 'p', long, env = "NODE_STATUS_AGENT_SERVER_PORT")] + pub(crate) server_port: u16, + // TODO dz accept keypair for identification / auth +} + +#[derive(Subcommand, Debug)] +pub(crate) enum Command { + RunProbe { + /// path of binary to run + #[arg(long, env = "NODE_STATUS_AGENT_PROBE_PATH")] + probe_path: String, + }, +} + +impl Args { + pub(crate) async fn execute(&self) -> anyhow::Result<()> { + match &self.command { + Command::RunProbe { probe_path } => self.run_probe(probe_path).await?, + } + + Ok(()) + } + + async fn run_probe(&self, probe_path: &str) -> anyhow::Result<()> { + let server_address = format!("{}:{}", &self.server_address, self.server_port); + + let probe = GwProbe::new(probe_path.to_string()); + + let version = probe.version().await; + tracing::info!("Probe version:\n{}", version); + + if let Some(testrun) = request_testrun(&server_address).await? { + let log = probe.run_and_get_log(&Some(testrun.gateway_identity_key)); + + submit_results(&server_address, testrun.testrun_id, log).await?; + } else { + tracing::info!("No testruns available, exiting") + } + + Ok(()) + } +} + +const URL_BASE: &str = "internal/testruns"; + +#[instrument(level = "debug", skip_all)] +async fn request_testrun(server_addr: &str) -> anyhow::Result> { + let target_url = format!("{}/{}", server_addr, URL_BASE); + let client = reqwest::Client::new(); + let res = client.get(target_url).send().await?; + let status = res.status(); + let response_text = res.text().await?; + + if status.is_client_error() { + bail!("{}: {}", status, response_text); + } else if status.is_server_error() { + if matches!(status, reqwest::StatusCode::SERVICE_UNAVAILABLE) + && response_text.contains("No testruns available") + { + return Ok(None); + } else { + bail!("{}: {}", status, response_text); + } + } + + serde_json::from_str(&response_text) + .map(|testrun| { + tracing::info!("Received testrun assignment: {:?}", testrun); + testrun + }) + .map_err(|err| { + tracing::error!("err"); + err.into() + }) +} + +#[instrument(level = "debug", skip(probe_outcome))] +async fn submit_results( + server_addr: &str, + testrun_id: i64, + probe_outcome: String, +) -> anyhow::Result<()> { + let target_url = format!("{}/{}/{}", server_addr, URL_BASE, testrun_id); + let client = reqwest::Client::new(); + + let res = client + .post(target_url) + .body(probe_outcome) + .send() + .await + .and_then(|response| response.error_for_status())?; + + tracing::debug!("Submitted results: {})", res.status()); + Ok(()) +} diff --git a/nym-node-status-agent/src/main.rs b/nym-node-status-agent/src/main.rs new file mode 100644 index 0000000000..133828b7fa --- /dev/null +++ b/nym-node-status-agent/src/main.rs @@ -0,0 +1,78 @@ +use crate::cli::Args; +use clap::Parser; +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{filter::Directive, EnvFilter}; + +mod cli; +mod probe; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + setup_tracing(); + let args = Args::parse(); + + let server_addr = format!("{}:{}", args.server_address, args.server_port); + test_ns_api_conn(&server_addr).await?; + + args.execute().await?; + + Ok(()) +} + +async fn test_ns_api_conn(server_addr: &str) -> anyhow::Result<()> { + reqwest::get(server_addr) + .await + .map(|res| { + tracing::info!( + "Testing connection to NS API at {server_addr}: {}", + res.status() + ); + }) + .map_err(|err| anyhow::anyhow!("Couldn't connect to server on {}: {}", server_addr, err)) +} + +pub(crate) fn setup_tracing() { + fn directive_checked(directive: impl Into) -> Directive { + directive + .into() + .parse() + .expect("Failed to parse log directive") + } + + let log_builder = tracing_subscriber::fmt() + // Use a more compact, abbreviated log format + .compact() + // Display source code file paths + .with_file(true) + // Display source code line numbers + .with_line_number(true) + .with_thread_ids(true) + // Don't display the event's target (module path) + .with_target(false); + + let mut filter = EnvFilter::builder() + // if RUST_LOG isn't set, set default level + .with_default_directive(LevelFilter::INFO.into()) + .from_env_lossy(); + // these crates are more granularly filtered + let filter_crates = [ + "reqwest", + "rustls", + "hyper", + "sqlx", + "h2", + "tendermint_rpc", + "tower_http", + "axum", + ]; + for crate_name in filter_crates { + filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))); + } + + filter = filter.add_directive(directive_checked("nym_bin_common=debug")); + filter = filter.add_directive(directive_checked("nym_explorer_client=debug")); + filter = filter.add_directive(directive_checked("nym_network_defaults=debug")); + filter = filter.add_directive(directive_checked("nym_validator_client=debug")); + + log_builder.with_env_filter(filter).init(); +} diff --git a/nym-node-status-agent/src/probe.rs b/nym-node-status-agent/src/probe.rs new file mode 100644 index 0000000000..f779f3af53 --- /dev/null +++ b/nym-node-status-agent/src/probe.rs @@ -0,0 +1,60 @@ +use tracing::error; + +pub(crate) struct GwProbe { + path: String, +} + +impl GwProbe { + pub(crate) fn new(probe_path: String) -> Self { + Self { path: probe_path } + } + + pub(crate) async fn version(&self) -> String { + let mut command = tokio::process::Command::new(&self.path); + command.stdout(std::process::Stdio::piped()); + command.arg("--version"); + + match command.spawn() { + Ok(child) => { + if let Ok(output) = child.wait_with_output().await { + return String::from_utf8(output.stdout) + .unwrap_or("Unable to get log from test run".to_string()); + } + "Unable to get probe version".to_string() + } + Err(e) => { + error!("Failed to get probe version: {}", e); + "Failed to get probe version".to_string() + } + } + } + + pub(crate) fn run_and_get_log(&self, gateway_key: &Option) -> String { + let mut command = std::process::Command::new(&self.path); + command.stdout(std::process::Stdio::piped()); + + if let Some(gateway_id) = gateway_key { + command.arg("--gateway").arg(gateway_id); + } + + match command.spawn() { + Ok(child) => { + if let Ok(output) = child.wait_with_output() { + if !output.status.success() { + let out = String::from_utf8_lossy(&output.stdout); + let err = String::from_utf8_lossy(&output.stderr); + tracing::error!("Probe exited with {:?}:\n{}\n{}", output.status, out, err); + } + + return String::from_utf8(output.stdout) + .unwrap_or("Unable to get log from test run".to_string()); + } + "Unable to get log from test run".to_string() + } + Err(e) => { + error!("Failed to spawn test: {}", e); + "Failed to spawn test run task".to_string() + } + } + } +} diff --git a/nym-node-status-api/.gitignore b/nym-node-status-api/.gitignore new file mode 100644 index 0000000000..91459afabe --- /dev/null +++ b/nym-node-status-api/.gitignore @@ -0,0 +1,6 @@ +data/ +enter_db.sh +nym-gateway-probe +nym-node-status-api +*.sqlite +*.sqlite-journal diff --git a/nym-node-status-api/.sqlx/query-06b17d1e5f61201a1b7542896ba55c69cd5c1a7e7d87073c94600c783a0a3984.json b/nym-node-status-api/.sqlx/query-06b17d1e5f61201a1b7542896ba55c69cd5c1a7e7d87073c94600c783a0a3984.json new file mode 100644 index 0000000000..ad57224826 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-06b17d1e5f61201a1b7542896ba55c69cd5c1a7e7d87073c94600c783a0a3984.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n gateway_identity_key\n FROM\n gateways\n WHERE\n id = ?", + "describe": { + "columns": [ + { + "name": "gateway_identity_key", + "ordinal": 0, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false + ] + }, + "hash": "06b17d1e5f61201a1b7542896ba55c69cd5c1a7e7d87073c94600c783a0a3984" +} diff --git a/nym-node-status-api/.sqlx/query-1327b5118f9144dddbcf8edb11f7dc549cf503409fd6dfedcdc02dbcd61d5454.json b/nym-node-status-api/.sqlx/query-1327b5118f9144dddbcf8edb11f7dc549cf503409fd6dfedcdc02dbcd61d5454.json new file mode 100644 index 0000000000..8b69daa6a3 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-1327b5118f9144dddbcf8edb11f7dc549cf503409fd6dfedcdc02dbcd61d5454.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n key as \"key!\",\n value_json as \"value_json!\",\n last_updated_utc as \"last_updated_utc!\"\n FROM summary", + "describe": { + "columns": [ + { + "name": "key!", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "value_json!", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "last_updated_utc!", + "ordinal": 2, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + true, + true, + false + ] + }, + "hash": "1327b5118f9144dddbcf8edb11f7dc549cf503409fd6dfedcdc02dbcd61d5454" +} diff --git a/nym-node-status-api/.sqlx/query-18abc8fde56cf86baed7b4afa38f2c63cdf90f2f3b6d81afb9000bb0968dcaea.json b/nym-node-status-api/.sqlx/query-18abc8fde56cf86baed7b4afa38f2c63cdf90f2f3b6d81afb9000bb0968dcaea.json new file mode 100644 index 0000000000..9bc2966149 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-18abc8fde56cf86baed7b4afa38f2c63cdf90f2f3b6d81afb9000bb0968dcaea.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE mixnodes\n SET bonded = ?, last_updated_utc = ?\n WHERE id = ?;", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "18abc8fde56cf86baed7b4afa38f2c63cdf90f2f3b6d81afb9000bb0968dcaea" +} diff --git a/nym-node-status-api/.sqlx/query-2236299f9f691376db54cbd58ec5ceb89b9925cba46efcf4ed79ef0759a01129.json b/nym-node-status-api/.sqlx/query-2236299f9f691376db54cbd58ec5ceb89b9925cba46efcf4ed79ef0759a01129.json new file mode 100644 index 0000000000..d9bf0dd6aa --- /dev/null +++ b/nym-node-status-api/.sqlx/query-2236299f9f691376db54cbd58ec5ceb89b9925cba46efcf4ed79ef0759a01129.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n id,\n gateway_identity_key\n FROM gateways\n WHERE id = ?\n LIMIT 1", + "describe": { + "columns": [ + { + "name": "id", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "gateway_identity_key", + "ordinal": 1, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false + ] + }, + "hash": "2236299f9f691376db54cbd58ec5ceb89b9925cba46efcf4ed79ef0759a01129" +} diff --git a/nym-node-status-api/.sqlx/query-3c584e211d07c511644c8079187965acf3bcfb3f84ba8d24ed645d79976cf784.json b/nym-node-status-api/.sqlx/query-3c584e211d07c511644c8079187965acf3bcfb3f84ba8d24ed645d79976cf784.json new file mode 100644 index 0000000000..56898ca5ad --- /dev/null +++ b/nym-node-status-api/.sqlx/query-3c584e211d07c511644c8079187965acf3bcfb3f84ba8d24ed645d79976cf784.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO testruns (gateway_id, status, ip_address, timestamp_utc, log) VALUES (?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "3c584e211d07c511644c8079187965acf3bcfb3f84ba8d24ed645d79976cf784" +} diff --git a/nym-node-status-api/.sqlx/query-3d3a1fa429e3090741c6b6a8e82e692afc04b51e8782bcbf59f1eb4116112536.json b/nym-node-status-api/.sqlx/query-3d3a1fa429e3090741c6b6a8e82e692afc04b51e8782bcbf59f1eb4116112536.json new file mode 100644 index 0000000000..10158436b6 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-3d3a1fa429e3090741c6b6a8e82e692afc04b51e8782bcbf59f1eb4116112536.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE gateways\n SET bonded = ?, last_updated_utc = ?\n WHERE id = ?;", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "3d3a1fa429e3090741c6b6a8e82e692afc04b51e8782bcbf59f1eb4116112536" +} diff --git a/nym-node-status-api/.sqlx/query-3d5fc502f976f5081f01352856b8632c29c81bfafb043bb8744129cf9e0266ad.json b/nym-node-status-api/.sqlx/query-3d5fc502f976f5081f01352856b8632c29c81bfafb043bb8744129cf9e0266ad.json new file mode 100644 index 0000000000..2a834fe87e --- /dev/null +++ b/nym-node-status-api/.sqlx/query-3d5fc502f976f5081f01352856b8632c29c81bfafb043bb8744129cf9e0266ad.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n id as \"id!\",\n gateway_identity_key as \"gateway_identity_key!\",\n self_described as \"self_described?\",\n explorer_pretty_bond as \"explorer_pretty_bond?\"\n FROM gateways\n WHERE gateway_identity_key = ?\n ORDER BY gateway_identity_key\n LIMIT 1", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "gateway_identity_key!", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "self_described?", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "explorer_pretty_bond?", + "ordinal": 3, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + true, + false, + true, + true + ] + }, + "hash": "3d5fc502f976f5081f01352856b8632c29c81bfafb043bb8744129cf9e0266ad" +} diff --git a/nym-node-status-api/.sqlx/query-418944f2eccb838cb3882f34469203c8569f03fdd39ce09d7b74177896e52a8c.json b/nym-node-status-api/.sqlx/query-418944f2eccb838cb3882f34469203c8569f03fdd39ce09d7b74177896e52a8c.json new file mode 100644 index 0000000000..f9eb3657e7 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-418944f2eccb838cb3882f34469203c8569f03fdd39ce09d7b74177896e52a8c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE testruns SET status = ? WHERE id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "418944f2eccb838cb3882f34469203c8569f03fdd39ce09d7b74177896e52a8c" +} diff --git a/nym-node-status-api/.sqlx/query-46d76bc6d3fba2dae3b21511a36289dd776749dd7a20cda61b0480f2fba60889.json b/nym-node-status-api/.sqlx/query-46d76bc6d3fba2dae3b21511a36289dd776749dd7a20cda61b0480f2fba60889.json new file mode 100644 index 0000000000..2cb8760009 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-46d76bc6d3fba2dae3b21511a36289dd776749dd7a20cda61b0480f2fba60889.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n id as \"id!\",\n gateway_id as \"gateway_id!\",\n status as \"status!\",\n timestamp_utc as \"timestamp_utc!\",\n ip_address as \"ip_address!\",\n log as \"log!\"\n FROM testruns\n WHERE gateway_id = ? AND status != 2\n ORDER BY id DESC\n LIMIT 1", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "gateway_id!", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "status!", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "timestamp_utc!", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "ip_address!", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "log!", + "ordinal": 5, + "type_info": "Text" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "46d76bc6d3fba2dae3b21511a36289dd776749dd7a20cda61b0480f2fba60889" +} diff --git a/nym-node-status-api/.sqlx/query-4afcc6673890f795c2793f1e2f8570ee787fc7daf00fcb916f18d1cb7d6c8f08.json b/nym-node-status-api/.sqlx/query-4afcc6673890f795c2793f1e2f8570ee787fc7daf00fcb916f18d1cb7d6c8f08.json new file mode 100644 index 0000000000..6a9fd9d4eb --- /dev/null +++ b/nym-node-status-api/.sqlx/query-4afcc6673890f795c2793f1e2f8570ee787fc7daf00fcb916f18d1cb7d6c8f08.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE gateways SET last_probe_log = ? WHERE id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "4afcc6673890f795c2793f1e2f8570ee787fc7daf00fcb916f18d1cb7d6c8f08" +} diff --git a/nym-node-status-api/.sqlx/query-4b61a4bc32333c92a8f5ad4ad0017b40dc01845f554b5479f37855d89b309e6f.json b/nym-node-status-api/.sqlx/query-4b61a4bc32333c92a8f5ad4ad0017b40dc01845f554b5479f37855d89b309e6f.json new file mode 100644 index 0000000000..8b9c9699f8 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-4b61a4bc32333c92a8f5ad4ad0017b40dc01845f554b5479f37855d89b309e6f.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n id as \"id!\",\n gateway_identity_key as \"identity_key!\",\n bonded as \"bonded: bool\"\n FROM gateways\n WHERE bonded = ?", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "identity_key!", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "bonded: bool", + "ordinal": 2, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "4b61a4bc32333c92a8f5ad4ad0017b40dc01845f554b5479f37855d89b309e6f" +} diff --git a/nym-node-status-api/.sqlx/query-670b7ed7d57a6986181b24be24ca667e8cacdf677ccb906415b3fe92be0c436b.json b/nym-node-status-api/.sqlx/query-670b7ed7d57a6986181b24be24ca667e8cacdf677ccb906415b3fe92be0c436b.json new file mode 100644 index 0000000000..38fe641a3a --- /dev/null +++ b/nym-node-status-api/.sqlx/query-670b7ed7d57a6986181b24be24ca667e8cacdf677ccb906415b3fe92be0c436b.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT count(id) FROM mixnodes", + "describe": { + "columns": [ + { + "name": "count(id)", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "670b7ed7d57a6986181b24be24ca667e8cacdf677ccb906415b3fe92be0c436b" +} diff --git a/nym-node-status-api/.sqlx/query-6d7967b831b355d5f2c77950abc56f816956b0824c66a25da611dce688105d36.json b/nym-node-status-api/.sqlx/query-6d7967b831b355d5f2c77950abc56f816956b0824c66a25da611dce688105d36.json new file mode 100644 index 0000000000..e12434a85a --- /dev/null +++ b/nym-node-status-api/.sqlx/query-6d7967b831b355d5f2c77950abc56f816956b0824c66a25da611dce688105d36.json @@ -0,0 +1,50 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n id as \"id!\",\n gateway_id as \"gateway_id!\",\n status as \"status!\",\n timestamp_utc as \"timestamp_utc!\",\n ip_address as \"ip_address!\",\n log as \"log!\"\n FROM testruns\n WHERE\n id = ?\n AND\n status = ?\n ORDER BY timestamp_utc", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "gateway_id!", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "status!", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "timestamp_utc!", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "ip_address!", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "log!", + "ordinal": 5, + "type_info": "Text" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + false, + false, + false, + false, + false, + false + ] + }, + "hash": "6d7967b831b355d5f2c77950abc56f816956b0824c66a25da611dce688105d36" +} diff --git a/nym-node-status-api/.sqlx/query-6eb1a682cf13205cf701590021cdf795147ac3724e89df5b2f24f7215d87dce1.json b/nym-node-status-api/.sqlx/query-6eb1a682cf13205cf701590021cdf795147ac3724e89df5b2f24f7215d87dce1.json new file mode 100644 index 0000000000..86b61631a6 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-6eb1a682cf13205cf701590021cdf795147ac3724e89df5b2f24f7215d87dce1.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO mixnodes\n (mix_id, identity_key, bonded, total_stake,\n host, http_api_port, blacklisted, full_details,\n self_described, last_updated_utc, is_dp_delegatee)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(mix_id) DO UPDATE SET\n bonded=excluded.bonded,\n total_stake=excluded.total_stake, host=excluded.host,\n http_api_port=excluded.http_api_port,blacklisted=excluded.blacklisted,\n full_details=excluded.full_details,self_described=excluded.self_described,\n last_updated_utc=excluded.last_updated_utc,\n is_dp_delegatee = excluded.is_dp_delegatee;", + "describe": { + "columns": [], + "parameters": { + "Right": 11 + }, + "nullable": [] + }, + "hash": "6eb1a682cf13205cf701590021cdf795147ac3724e89df5b2f24f7215d87dce1" +} diff --git a/nym-node-status-api/.sqlx/query-6ef3efde571d46961244cd90420f3de5949a5ff2083453cb879af8a1689efe2f.json b/nym-node-status-api/.sqlx/query-6ef3efde571d46961244cd90420f3de5949a5ff2083453cb879af8a1689efe2f.json new file mode 100644 index 0000000000..9d93b219f9 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-6ef3efde571d46961244cd90420f3de5949a5ff2083453cb879af8a1689efe2f.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE gateways SET last_probe_result = ? WHERE id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "6ef3efde571d46961244cd90420f3de5949a5ff2083453cb879af8a1689efe2f" +} diff --git a/nym-node-status-api/.sqlx/query-71a455c705f9c25d3843ff2fb8629d1320a5eb10797cdb5a435455d22c6aeac1.json b/nym-node-status-api/.sqlx/query-71a455c705f9c25d3843ff2fb8629d1320a5eb10797cdb5a435455d22c6aeac1.json new file mode 100644 index 0000000000..f061747404 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-71a455c705f9c25d3843ff2fb8629d1320a5eb10797cdb5a435455d22c6aeac1.json @@ -0,0 +1,98 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n gw.gateway_identity_key as \"gateway_identity_key!\",\n gw.bonded as \"bonded: bool\",\n gw.blacklisted as \"blacklisted: bool\",\n gw.performance as \"performance!\",\n gw.self_described as \"self_described?\",\n gw.explorer_pretty_bond as \"explorer_pretty_bond?\",\n gw.last_probe_result as \"last_probe_result?\",\n gw.last_probe_log as \"last_probe_log?\",\n gw.last_testrun_utc as \"last_testrun_utc?\",\n gw.last_updated_utc as \"last_updated_utc!\",\n COALESCE(gd.moniker, \"NA\") as \"moniker!\",\n COALESCE(gd.website, \"NA\") as \"website!\",\n COALESCE(gd.security_contact, \"NA\") as \"security_contact!\",\n COALESCE(gd.details, \"NA\") as \"details!\"\n FROM gateways gw\n LEFT JOIN gateway_description gd\n ON gw.gateway_identity_key = gd.gateway_identity_key\n ORDER BY gw.gateway_identity_key", + "describe": { + "columns": [ + { + "name": "gateway_identity_key!", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "bonded: bool", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "blacklisted: bool", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "performance!", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "self_described?", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "explorer_pretty_bond?", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "last_probe_result?", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "last_probe_log?", + "ordinal": 7, + "type_info": "Text" + }, + { + "name": "last_testrun_utc?", + "ordinal": 8, + "type_info": "Int64" + }, + { + "name": "last_updated_utc!", + "ordinal": 9, + "type_info": "Int64" + }, + { + "name": "moniker!", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "website!", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "security_contact!", + "ordinal": 12, + "type_info": "Text" + }, + { + "name": "details!", + "ordinal": 13, + "type_info": "Text" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + false, + true, + true, + true, + true, + true, + false, + false, + false, + false, + false + ] + }, + "hash": "71a455c705f9c25d3843ff2fb8629d1320a5eb10797cdb5a435455d22c6aeac1" +} diff --git a/nym-node-status-api/.sqlx/query-7600823da7ce80b8ffda933608603a2752e28df775d1af8fd943a5fc8d7dc00d.json b/nym-node-status-api/.sqlx/query-7600823da7ce80b8ffda933608603a2752e28df775d1af8fd943a5fc8d7dc00d.json new file mode 100644 index 0000000000..9cba203bdb --- /dev/null +++ b/nym-node-status-api/.sqlx/query-7600823da7ce80b8ffda933608603a2752e28df775d1af8fd943a5fc8d7dc00d.json @@ -0,0 +1,38 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n id as \"id!\",\n date as \"date!\",\n timestamp_utc as \"timestamp_utc!\",\n value_json as \"value_json!\"\n FROM summary_history\n ORDER BY date DESC\n LIMIT 30", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "date!", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "timestamp_utc!", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "value_json!", + "ordinal": 3, + "type_info": "Text" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + true, + false, + false, + true + ] + }, + "hash": "7600823da7ce80b8ffda933608603a2752e28df775d1af8fd943a5fc8d7dc00d" +} diff --git a/nym-node-status-api/.sqlx/query-788515c34588aec352773df4b6e6c5e41f3c0bb56a27648b5e25466b8634a578.json b/nym-node-status-api/.sqlx/query-788515c34588aec352773df4b6e6c5e41f3c0bb56a27648b5e25466b8634a578.json new file mode 100644 index 0000000000..5252ccf2e7 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-788515c34588aec352773df4b6e6c5e41f3c0bb56a27648b5e25466b8634a578.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO summary_history\n (date, timestamp_utc, value_json)\n VALUES (?, ?, ?)\n ON CONFLICT(date) DO UPDATE SET\n timestamp_utc=excluded.timestamp_utc,\n value_json=excluded.value_json;", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "788515c34588aec352773df4b6e6c5e41f3c0bb56a27648b5e25466b8634a578" +} diff --git a/nym-node-status-api/.sqlx/query-8571faad2f66e08f24acfbfe036d17ca6eb090df7f6d52ef89c5d51564f8b45c.json b/nym-node-status-api/.sqlx/query-8571faad2f66e08f24acfbfe036d17ca6eb090df7f6d52ef89c5d51564f8b45c.json new file mode 100644 index 0000000000..3f171170db --- /dev/null +++ b/nym-node-status-api/.sqlx/query-8571faad2f66e08f24acfbfe036d17ca6eb090df7f6d52ef89c5d51564f8b45c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE gateways\n SET blacklisted = true\n WHERE gateway_identity_key = ?;", + "describe": { + "columns": [], + "parameters": { + "Right": 1 + }, + "nullable": [] + }, + "hash": "8571faad2f66e08f24acfbfe036d17ca6eb090df7f6d52ef89c5d51564f8b45c" +} diff --git a/nym-node-status-api/.sqlx/query-86ff64db477a1d6235179b0b88d86b86d1b9be62336c9eac0eef44987a5451b5.json b/nym-node-status-api/.sqlx/query-86ff64db477a1d6235179b0b88d86b86d1b9be62336c9eac0eef44987a5451b5.json new file mode 100644 index 0000000000..60583ed8b2 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-86ff64db477a1d6235179b0b88d86b86d1b9be62336c9eac0eef44987a5451b5.json @@ -0,0 +1,20 @@ +{ + "db_name": "SQLite", + "query": "SELECT count(id) FROM gateways", + "describe": { + "columns": [ + { + "name": "count(id)", + "ordinal": 0, + "type_info": "Int" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false + ] + }, + "hash": "86ff64db477a1d6235179b0b88d86b86d1b9be62336c9eac0eef44987a5451b5" +} diff --git a/nym-node-status-api/.sqlx/query-930a41e612b4e964ae214843da190f6c66c14d4267a2cc2ca73354becc2c8bb8.json b/nym-node-status-api/.sqlx/query-930a41e612b4e964ae214843da190f6c66c14d4267a2cc2ca73354becc2c8bb8.json new file mode 100644 index 0000000000..6c2a194e80 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-930a41e612b4e964ae214843da190f6c66c14d4267a2cc2ca73354becc2c8bb8.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n gateway_identity_key as \"gateway_identity_key!\",\n bonded as \"bonded: bool\"\n FROM gateways\n ORDER BY last_testrun_utc", + "describe": { + "columns": [ + { + "name": "gateway_identity_key!", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "bonded: bool", + "ordinal": 1, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false + ] + }, + "hash": "930a41e612b4e964ae214843da190f6c66c14d4267a2cc2ca73354becc2c8bb8" +} diff --git a/nym-node-status-api/.sqlx/query-c214c001acbbf79fa499816f36ec586c4c29c03efb4cf0c40b73a5c76159cf5c.json b/nym-node-status-api/.sqlx/query-c214c001acbbf79fa499816f36ec586c4c29c03efb4cf0c40b73a5c76159cf5c.json new file mode 100644 index 0000000000..8cc95e72de --- /dev/null +++ b/nym-node-status-api/.sqlx/query-c214c001acbbf79fa499816f36ec586c4c29c03efb4cf0c40b73a5c76159cf5c.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE gateways SET last_testrun_utc = ?, last_updated_utc = ? WHERE id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "c214c001acbbf79fa499816f36ec586c4c29c03efb4cf0c40b73a5c76159cf5c" +} diff --git a/nym-node-status-api/.sqlx/query-c5e3cd7284b334df5aa979b1627ea1f6dc2aed00cedde25f2be3567e47064351.json b/nym-node-status-api/.sqlx/query-c5e3cd7284b334df5aa979b1627ea1f6dc2aed00cedde25f2be3567e47064351.json new file mode 100644 index 0000000000..2e4f34fd85 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-c5e3cd7284b334df5aa979b1627ea1f6dc2aed00cedde25f2be3567e47064351.json @@ -0,0 +1,44 @@ +{ + "db_name": "SQLite", + "query": "\n SELECT\n date_utc as \"date_utc!\",\n packets_received as \"total_packets_received!: i64\",\n packets_sent as \"total_packets_sent!: i64\",\n packets_dropped as \"total_packets_dropped!: i64\",\n total_stake as \"total_stake!: i64\"\n FROM (\n SELECT\n date_utc,\n SUM(packets_received) as packets_received,\n SUM(packets_sent) as packets_sent,\n SUM(packets_dropped) as packets_dropped,\n SUM(total_stake) as total_stake\n FROM mixnode_daily_stats\n GROUP BY date_utc\n ORDER BY date_utc DESC\n LIMIT 30\n )\n GROUP BY date_utc\n ORDER BY date_utc\n ", + "describe": { + "columns": [ + { + "name": "date_utc!", + "ordinal": 0, + "type_info": "Text" + }, + { + "name": "total_packets_received!: i64", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "total_packets_sent!: i64", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "total_packets_dropped!: i64", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "total_stake!: i64", + "ordinal": 4, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + true, + true, + true, + false + ] + }, + "hash": "c5e3cd7284b334df5aa979b1627ea1f6dc2aed00cedde25f2be3567e47064351" +} diff --git a/nym-node-status-api/.sqlx/query-c7ba2621becb9ac4b5dee0ce303dadfcf19095935867a51cbd5b8362d1505fcc.json b/nym-node-status-api/.sqlx/query-c7ba2621becb9ac4b5dee0ce303dadfcf19095935867a51cbd5b8362d1505fcc.json new file mode 100644 index 0000000000..4d0edd1e88 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-c7ba2621becb9ac4b5dee0ce303dadfcf19095935867a51cbd5b8362d1505fcc.json @@ -0,0 +1,32 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n id as \"id!\",\n identity_key as \"identity_key!\",\n bonded as \"bonded: bool\"\n FROM mixnodes\n WHERE bonded = ?", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "identity_key!", + "ordinal": 1, + "type_info": "Text" + }, + { + "name": "bonded: bool", + "ordinal": 2, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "c7ba2621becb9ac4b5dee0ce303dadfcf19095935867a51cbd5b8362d1505fcc" +} diff --git a/nym-node-status-api/.sqlx/query-d8ea93e781666e6267902170709ee2aa37f6163525bbdce1a4cebef4a285f8d9.json b/nym-node-status-api/.sqlx/query-d8ea93e781666e6267902170709ee2aa37f6163525bbdce1a4cebef4a285f8d9.json new file mode 100644 index 0000000000..94e5513279 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-d8ea93e781666e6267902170709ee2aa37f6163525bbdce1a4cebef4a285f8d9.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO gateways\n (gateway_identity_key, bonded, blacklisted,\n self_described, explorer_pretty_bond,\n last_updated_utc, performance)\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT(gateway_identity_key) DO UPDATE SET\n bonded=excluded.bonded,\n blacklisted=excluded.blacklisted,\n self_described=excluded.self_described,\n explorer_pretty_bond=excluded.explorer_pretty_bond,\n last_updated_utc=excluded.last_updated_utc,\n performance = excluded.performance;", + "describe": { + "columns": [], + "parameters": { + "Right": 7 + }, + "nullable": [] + }, + "hash": "d8ea93e781666e6267902170709ee2aa37f6163525bbdce1a4cebef4a285f8d9" +} diff --git a/nym-node-status-api/.sqlx/query-e0c76a959276e3b0f44c720af9c74a5bf4912ee73468e62e7d0d96b1d9074cbe.json b/nym-node-status-api/.sqlx/query-e0c76a959276e3b0f44c720af9c74a5bf4912ee73468e62e7d0d96b1d9074cbe.json new file mode 100644 index 0000000000..5f7443bd50 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-e0c76a959276e3b0f44c720af9c74a5bf4912ee73468e62e7d0d96b1d9074cbe.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO summary\n (key, value_json, last_updated_utc)\n VALUES (?, ?, ?)\n ON CONFLICT(key) DO UPDATE SET\n value_json=excluded.value_json,\n last_updated_utc=excluded.last_updated_utc;", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "e0c76a959276e3b0f44c720af9c74a5bf4912ee73468e62e7d0d96b1d9074cbe" +} diff --git a/nym-node-status-api/.sqlx/query-f0a4316081d1be9444a87b95d933d31cb4bcc4071d31d8d2f7755e2d2c2e3e35.json b/nym-node-status-api/.sqlx/query-f0a4316081d1be9444a87b95d933d31cb4bcc4071d31d8d2f7755e2d2c2e3e35.json new file mode 100644 index 0000000000..d3f7611ba2 --- /dev/null +++ b/nym-node-status-api/.sqlx/query-f0a4316081d1be9444a87b95d933d31cb4bcc4071d31d8d2f7755e2d2c2e3e35.json @@ -0,0 +1,86 @@ +{ + "db_name": "SQLite", + "query": "SELECT\n mn.mix_id as \"mix_id!\",\n mn.bonded as \"bonded: bool\",\n mn.blacklisted as \"blacklisted: bool\",\n mn.is_dp_delegatee as \"is_dp_delegatee: bool\",\n mn.total_stake as \"total_stake!\",\n mn.full_details as \"full_details!\",\n mn.self_described as \"self_described\",\n mn.last_updated_utc as \"last_updated_utc!\",\n COALESCE(md.moniker, \"NA\") as \"moniker!\",\n COALESCE(md.website, \"NA\") as \"website!\",\n COALESCE(md.security_contact, \"NA\") as \"security_contact!\",\n COALESCE(md.details, \"NA\") as \"details!\"\n FROM mixnodes mn\n LEFT JOIN mixnode_description md ON mn.mix_id = md.mix_id\n ORDER BY mn.mix_id", + "describe": { + "columns": [ + { + "name": "mix_id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "bonded: bool", + "ordinal": 1, + "type_info": "Int64" + }, + { + "name": "blacklisted: bool", + "ordinal": 2, + "type_info": "Int64" + }, + { + "name": "is_dp_delegatee: bool", + "ordinal": 3, + "type_info": "Int64" + }, + { + "name": "total_stake!", + "ordinal": 4, + "type_info": "Int64" + }, + { + "name": "full_details!", + "ordinal": 5, + "type_info": "Text" + }, + { + "name": "self_described", + "ordinal": 6, + "type_info": "Text" + }, + { + "name": "last_updated_utc!", + "ordinal": 7, + "type_info": "Int64" + }, + { + "name": "moniker!", + "ordinal": 8, + "type_info": "Text" + }, + { + "name": "website!", + "ordinal": 9, + "type_info": "Text" + }, + { + "name": "security_contact!", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "details!", + "ordinal": 11, + "type_info": "Text" + } + ], + "parameters": { + "Right": 0 + }, + "nullable": [ + false, + false, + false, + false, + false, + true, + true, + false, + false, + false, + false, + false + ] + }, + "hash": "f0a4316081d1be9444a87b95d933d31cb4bcc4071d31d8d2f7755e2d2c2e3e35" +} diff --git a/nym-node-status-api/.sqlx/query-f5048d9926a5f5329f7f3b96d43b925e033ceec4f8112258feb4ac9e96fc5924.json b/nym-node-status-api/.sqlx/query-f5048d9926a5f5329f7f3b96d43b925e033ceec4f8112258feb4ac9e96fc5924.json new file mode 100644 index 0000000000..06d098bebc --- /dev/null +++ b/nym-node-status-api/.sqlx/query-f5048d9926a5f5329f7f3b96d43b925e033ceec4f8112258feb4ac9e96fc5924.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE\n testruns\n SET\n status = ?\n WHERE\n status = ?\n AND\n timestamp_utc < ?\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "f5048d9926a5f5329f7f3b96d43b925e033ceec4f8112258feb4ac9e96fc5924" +} diff --git a/nym-node-status-api/.sqlx/query-ff9334ba7b670b218b2f9100e9ab5d2f2d08b2e53203aab9f07ea9b52acbd407.json b/nym-node-status-api/.sqlx/query-ff9334ba7b670b218b2f9100e9ab5d2f2d08b2e53203aab9f07ea9b52acbd407.json new file mode 100644 index 0000000000..24e735e96d --- /dev/null +++ b/nym-node-status-api/.sqlx/query-ff9334ba7b670b218b2f9100e9ab5d2f2d08b2e53203aab9f07ea9b52acbd407.json @@ -0,0 +1,26 @@ +{ + "db_name": "SQLite", + "query": "UPDATE testruns\n SET status = ?\n WHERE rowid =\n (\n SELECT rowid\n FROM testruns\n WHERE status = ?\n ORDER BY timestamp_utc asc\n LIMIT 1\n )\n RETURNING\n id as \"id!\",\n gateway_id\n ", + "describe": { + "columns": [ + { + "name": "id!", + "ordinal": 0, + "type_info": "Int64" + }, + { + "name": "gateway_id", + "ordinal": 1, + "type_info": "Int64" + } + ], + "parameters": { + "Right": 2 + }, + "nullable": [ + true, + false + ] + }, + "hash": "ff9334ba7b670b218b2f9100e9ab5d2f2d08b2e53203aab9f07ea9b52acbd407" +} diff --git a/nym-node-status-api/Cargo.toml b/nym-node-status-api/Cargo.toml new file mode 100644 index 0000000000..511ab91b59 --- /dev/null +++ b/nym-node-status-api/Cargo.toml @@ -0,0 +1,62 @@ +# Copyright 2024 - Nym Technologies SA +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "nym-node-status-api" +version = "0.1.6" +authors.workspace = true +repository.workspace = true +homepage.workspace = true +documentation.workspace = true +edition.workspace = true +license.workspace = true +rust-version.workspace = true + +[dependencies] +anyhow = { workspace = true } +axum = { workspace = true, features = ["tokio", "macros"] } +chrono = { workspace = true } +clap = { workspace = true, features = ["cargo", "derive", "env", "string"] } +cosmwasm-std = { workspace = true } +envy = { workspace = true } +futures-util = { workspace = true } +moka = { workspace = true, features = ["future"] } +nym-bin-common = { path = "../common/bin-common", features = ["models"]} +nym-common-models = { path = "../common/models" } +nym-explorer-client = { path = "../explorer-api/explorer-client" } +nym-network-defaults = { path = "../common/network-defaults" } +nym-validator-client = { path = "../common/client-libs/validator-client" } +nym-task = { path = "../common/task" } +nym-node-requests = { path = "../nym-node/nym-node-requests", features = ["openapi"] } +regex = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +serde_json_path = { workspace = true } +strum = { workspace = true } +strum_macros = { workspace = true } +sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite"] } +thiserror = { workspace = true } +tokio = { workspace = true, features = ["rt-multi-thread"] } +tokio-util = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = ["env-filter"] } +tracing-log = { workspace = true } +tower-http = { workspace = true, features = ["cors", "trace"] } +utoipa = { workspace = true, features = ["axum_extras", "time"] } +utoipa-swagger-ui = { workspace = true, features = ["axum"] } +# TODO dz `cargo update async-trait` +# for automatic schema detection, which was merged, but not released yet +# https://github.com/ProbablyClem/utoipauto/pull/38 +# utoipauto = { git = "https://github.com/ProbablyClem/utoipauto", rev = "eb04cba" } +utoipauto = { workspace = true } + +[build-dependencies] +anyhow = { workspace = true } +tokio = { workspace = true, features = ["macros"] } +sqlx = { workspace = true, features = [ + "runtime-tokio-rustls", + "sqlite", + "macros", + "migrate", +] } diff --git a/nym-node-status-api/Dockerfile b/nym-node-status-api/Dockerfile new file mode 100644 index 0000000000..ceab7c9392 --- /dev/null +++ b/nym-node-status-api/Dockerfile @@ -0,0 +1,15 @@ +FROM rust:latest AS builder + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-node-status-api + +RUN cargo build --release + +FROM ubuntu:24.04 + +RUN apt-get update && apt-get install -y ca-certificates + +WORKDIR /nym + +COPY --from=builder /usr/src/nym/target/release/nym-node-status-api ./ +ENTRYPOINT [ "/nym/nym-node-status-api" ] diff --git a/nym-node-status-api/Dockerfile.dev b/nym-node-status-api/Dockerfile.dev new file mode 100644 index 0000000000..2967a6e605 --- /dev/null +++ b/nym-node-status-api/Dockerfile.dev @@ -0,0 +1,8 @@ +FROM ubuntu:22.04 + +RUN apt-get update && apt-get install -y ca-certificates + +WORKDIR /nym + +COPY nym-node-status-api/nym-node-status-api ./ +ENTRYPOINT [ "/nym/nym-node-status-api" ] diff --git a/nym-node-status-api/build.rs b/nym-node-status-api/build.rs new file mode 100644 index 0000000000..025e755088 --- /dev/null +++ b/nym-node-status-api/build.rs @@ -0,0 +1,45 @@ +use anyhow::{anyhow, Result}; +use sqlx::{Connection, SqliteConnection}; +use std::fs::Permissions; +use std::os::unix::fs::PermissionsExt; +use tokio::{fs::File, io::AsyncWriteExt}; + +const SQLITE_DB_FILENAME: &str = "nym-node-status-api.sqlite"; + +/// If you need to re-run migrations or reset the db, just run +/// cargo clean -p nym-node-status-api +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + let out_dir = read_env_var("OUT_DIR")?; + let database_path = format!("{}/{}?mode=rwc", out_dir, SQLITE_DB_FILENAME); + + write_db_path_to_file(&out_dir, SQLITE_DB_FILENAME).await?; + let mut conn = SqliteConnection::connect(&database_path).await?; + sqlx::migrate!("./migrations").run(&mut conn).await?; + + #[cfg(target_family = "unix")] + println!("cargo::rustc-env=DATABASE_URL=sqlite://{}", &database_path); + + #[cfg(target_family = "windows")] + // for some strange reason we need to add a leading `/` to the windows path even though it's + // not a valid windows path... but hey, it works... + println!("cargo::rustc-env=DATABASE_URL=sqlite:///{}", &database_path); + + Ok(()) +} + +fn read_env_var(var: &str) -> Result { + std::env::var(var).map_err(|_| anyhow!("You need to set {} env var", var)) +} + +/// use `./enter_db.sh` to inspect DB +async fn write_db_path_to_file(out_dir: &str, db_filename: &str) -> anyhow::Result<()> { + let mut file = File::create("enter_db.sh").await?; + let _ = file.write(b"#!/bin/bash\n").await?; + file.write_all(format!("sqlite3 {}/{}", out_dir, db_filename).as_bytes()) + .await?; + + file.set_permissions(Permissions::from_mode(0o755)) + .await + .map_err(From::from) +} diff --git a/nym-node-status-api/launch_node_status_api.sh b/nym-node-status-api/launch_node_status_api.sh new file mode 100755 index 0000000000..5d92675412 --- /dev/null +++ b/nym-node-status-api/launch_node_status_api.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +set -e + +export RUST_LOG=${RUST_LOG:-debug} + +export NYM_API_CLIENT_TIMEOUT=60 +export EXPLORER_CLIENT_TIMEOUT=60 +export NODE_STATUS_API_TESTRUN_REFRESH_INTERVAL=60 + +export ENVIRONMENT="qa.env" + +function run_bare() { + # export necessary env vars + set -a + source ../envs/$ENVIRONMENT + set +a + export RUST_LOG=debug + + # --conection-url is provided in build.rs + cargo run --package nym-node-status-api +} + +function run_docker() { + cargo build --package nym-node-status-api --release + cp ../target/release/nym-node-status-api . + + cd .. + docker build -t node-status-api -f nym-node-status-api/Dockerfile.dev . + docker run --env-file envs/${ENVIRONMENT} \ + -e EXPLORER_CLIENT_TIMEOUT=$EXPLORER_CLIENT_TIMEOUT \ + -e NYM_API_CLIENT_TIMEOUT=$NYM_API_CLIENT_TIMEOUT \ + -e DATABASE_URL="sqlite://node-status-api.sqlite?mode=rwc" \ + -e RUST_LOG=${RUST_LOG} node-status-api + +} + +run_bare + +# run_docker diff --git a/nym-node-status-api/migrations/000_init.sql b/nym-node-status-api/migrations/000_init.sql new file mode 100644 index 0000000000..4f9fd7da60 --- /dev/null +++ b/nym-node-status-api/migrations/000_init.sql @@ -0,0 +1,112 @@ +CREATE TABLE gateways +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gateway_identity_key VARCHAR NOT NULL UNIQUE, + self_described VARCHAR NOT NULL, + explorer_pretty_bond VARCHAR, + last_probe_result VARCHAR, + last_probe_log VARCHAR, + config_score INTEGER NOT NULL DEFAULT (0), + config_score_successes REAL NOT NULL DEFAULT (0), + config_score_samples REAL NOT NULL DEFAULT (0), + routing_score INTEGER NOT NULL DEFAULT (0), + routing_score_successes REAL NOT NULL DEFAULT (0), + routing_score_samples REAL NOT NULL DEFAULT (0), + test_run_samples REAL NOT NULL DEFAULT (0), + last_testrun_utc INTEGER, + last_updated_utc INTEGER NOT NULL, + bonded INTEGER CHECK (bonded in (0, 1)) NOT NULL DEFAULT 0, + blacklisted INTEGER CHECK (bonded in (0, 1)) NOT NULL DEFAULT 0, + performance INTEGER NOT NULL DEFAULT 0 +); + +CREATE INDEX idx_gateway_description_gateway_identity_key ON gateways (gateway_identity_key); + + +CREATE TABLE mixnodes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + identity_key VARCHAR NOT NULL UNIQUE, + mix_id INTEGER NOT NULL UNIQUE, + bonded INTEGER CHECK (bonded in (0, 1)) NOT NULL DEFAULT 0, + total_stake INTEGER NOT NULL, + host VARCHAR NOT NULL, + http_api_port INTEGER NOT NULL, + blacklisted INTEGER CHECK (blacklisted in (0, 1)) NOT NULL DEFAULT 0, + full_details VARCHAR, + self_described VARCHAR, + last_updated_utc INTEGER NOT NULL + , is_dp_delegatee INTEGER CHECK (is_dp_delegatee IN (0, 1)) NOT NULL DEFAULT 0); +CREATE INDEX idx_mixnodes_mix_id ON mixnodes (mix_id); +CREATE INDEX idx_mixnodes_identity_key ON mixnodes (identity_key); + +CREATE TABLE + mixnode_description ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mix_id INTEGER UNIQUE NOT NULL, + moniker VARCHAR, + website VARCHAR, + security_contact VARCHAR, + details VARCHAR, + last_updated_utc INTEGER NOT NULL, + FOREIGN KEY (mix_id) REFERENCES mixnodes (mix_id) + ); + +-- Indexes for description table +CREATE INDEX idx_mixnode_description_mix_id ON mixnode_description (mix_id); + + +CREATE TABLE summary +( + key VARCHAR PRIMARY KEY, + value_json VARCHAR, + last_updated_utc INTEGER NOT NULL +); + + +CREATE TABLE summary_history +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + date VARCHAR UNIQUE NOT NULL, + timestamp_utc INTEGER NOT NULL, + value_json VARCHAR +); +CREATE INDEX idx_summary_history_timestamp_utc ON summary_history (timestamp_utc); +CREATE INDEX idx_summary_history_date ON summary_history (date); + + +CREATE TABLE gateway_description ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gateway_identity_key VARCHAR UNIQUE NOT NULL, + moniker VARCHAR, + website VARCHAR, + security_contact VARCHAR, + details VARCHAR, + last_updated_utc INTEGER NOT NULL, + FOREIGN KEY (gateway_identity_key) REFERENCES gateways (gateway_identity_key) + ); + + +CREATE TABLE + mixnode_daily_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + mix_id INTEGER NOT NULL, + total_stake BIGINT NOT NULL, + date_utc VARCHAR NOT NULL, + packets_received INTEGER DEFAULT 0, + packets_sent INTEGER DEFAULT 0, + packets_dropped INTEGER DEFAULT 0, + FOREIGN KEY (mix_id) REFERENCES mixnodes (mix_id), + UNIQUE (mix_id, date_utc) -- This constraint automatically creates an index + ); + + +CREATE TABLE testruns +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + gateway_id INTEGER NOT NULL, + status INTEGER NOT NULL, -- 0=pending, 1=in-progress, 2=complete + timestamp_utc INTEGER NOT NULL, + ip_address VARCHAR NOT NULL, + log VARCHAR NOT NULL, + FOREIGN KEY (gateway_id) REFERENCES gateways (id) +); diff --git a/nym-node-status-api/src/cli/mod.rs b/nym-node-status-api/src/cli/mod.rs new file mode 100644 index 0000000000..84ee86577f --- /dev/null +++ b/nym-node-status-api/src/cli/mod.rs @@ -0,0 +1,77 @@ +use clap::Parser; +use nym_bin_common::bin_info; +use reqwest::Url; +use std::{sync::OnceLock, time::Duration}; + +// Helper for passing LONG_VERSION to clap +fn pretty_build_info_static() -> &'static str { + static PRETTY_BUILD_INFORMATION: OnceLock = OnceLock::new(); + PRETTY_BUILD_INFORMATION.get_or_init(|| bin_info!().pretty_print()) +} + +#[derive(Clone, Debug, Parser)] +#[clap(author = "Nymtech", version, long_version = pretty_build_info_static(), about)] +pub(crate) struct Cli { + /// Network name for the network to which we're connecting. + #[clap(long, env = "NETWORK_NAME")] + pub(crate) network_name: String, + + /// Explorer api url. + #[clap(short, long, env = "EXPLORER_API")] + pub(crate) explorer_api: String, + + /// Nym api url. + #[clap(short, long, env = "NYM_API")] + pub(crate) nym_api: String, + + /// TTL for the http cache. + #[clap( + long, + default_value_t = 30, + env = "NYM_NODE_STATUS_API_NYM_HTTP_CACHE_TTL" + )] + pub(crate) nym_http_cache_ttl: u64, + + /// HTTP port on which to run node status api. + #[clap(long, default_value_t = 8000, env = "NYM_NODE_STATUS_API_HTTP_PORT")] + pub(crate) http_port: u16, + + /// Nyxd address. + #[clap(long, env = "NYXD")] + pub(crate) nyxd_addr: Url, + + /// Nym api client timeout. + #[clap(long, default_value = "15", env = "NYM_API_CLIENT_TIMEOUT")] + #[arg(value_parser = parse_duration)] + pub(crate) nym_api_client_timeout: Duration, + + /// Explorer api client timeout. + #[clap(long, default_value = "15", env = "EXPLORER_CLIENT_TIMEOUT")] + #[arg(value_parser = parse_duration)] + pub(crate) explorer_client_timeout: Duration, + + /// Connection url for the database. + #[clap(long, env = "DATABASE_URL")] + pub(crate) database_url: String, + + #[clap( + long, + default_value = "600", + env = "NODE_STATUS_API_MONITOR_REFRESH_INTERVAL" + )] + #[arg(value_parser = parse_duration)] + pub(crate) monitor_refresh_interval: Duration, + + #[clap( + long, + default_value = "600", + env = "NODE_STATUS_API_TESTRUN_REFRESH_INTERVAL" + )] + #[arg(value_parser = parse_duration)] + pub(crate) testruns_refresh_interval: Duration, +} + +fn parse_duration(arg: &str) -> Result { + let seconds = arg.parse()?; + Ok(std::time::Duration::from_secs(seconds)) +} diff --git a/nym-node-status-api/src/db/mod.rs b/nym-node-status-api/src/db/mod.rs new file mode 100644 index 0000000000..86e25e9227 --- /dev/null +++ b/nym-node-status-api/src/db/mod.rs @@ -0,0 +1,35 @@ +use anyhow::{anyhow, Result}; +use sqlx::{migrate::Migrator, sqlite::SqliteConnectOptions, ConnectOptions, SqlitePool}; +use std::str::FromStr; + +pub(crate) mod models; +pub(crate) mod queries; + +static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); + +pub(crate) type DbPool = SqlitePool; + +pub(crate) struct Storage { + pool: DbPool, +} + +impl Storage { + pub async fn init(connection_url: String) -> Result { + let connect_options = SqliteConnectOptions::from_str(&connection_url)? + .create_if_missing(true) + .disable_statement_logging(); + + let pool = sqlx::SqlitePool::connect_with(connect_options) + .await + .map_err(|err| anyhow!("Failed to connect to {}: {}", &connection_url, err))?; + + MIGRATOR.run(&pool).await?; + + Ok(Storage { pool }) + } + + /// Cloning pool is cheap, it's the same underlying set of connections + pub fn pool_owned(&self) -> DbPool { + self.pool.clone() + } +} diff --git a/nym-node-status-api/src/db/models.rs b/nym-node-status-api/src/db/models.rs new file mode 100644 index 0000000000..a5511787f9 --- /dev/null +++ b/nym-node-status-api/src/db/models.rs @@ -0,0 +1,335 @@ +use crate::{ + http::{self, models::SummaryHistory}, + monitor::NumericalCheckedCast, +}; +use nym_node_requests::api::v1::node::models::NodeDescription; +use serde::{Deserialize, Serialize}; +use strum_macros::{EnumString, FromRepr}; +use utoipa::ToSchema; + +pub(crate) struct GatewayRecord { + pub(crate) identity_key: String, + pub(crate) bonded: bool, + pub(crate) blacklisted: bool, + pub(crate) self_described: String, + pub(crate) explorer_pretty_bond: Option, + pub(crate) last_updated_utc: i64, + pub(crate) performance: u8, +} + +#[derive(Debug, Clone)] +pub(crate) struct GatewayDto { + pub(crate) gateway_identity_key: String, + pub(crate) bonded: bool, + pub(crate) blacklisted: bool, + pub(crate) performance: i64, + pub(crate) self_described: Option, + pub(crate) explorer_pretty_bond: Option, + pub(crate) last_probe_result: Option, + pub(crate) last_probe_log: Option, + pub(crate) last_testrun_utc: Option, + pub(crate) last_updated_utc: i64, + pub(crate) moniker: String, + pub(crate) security_contact: String, + pub(crate) details: String, + pub(crate) website: String, +} + +impl TryFrom for http::models::Gateway { + type Error = anyhow::Error; + + fn try_from(value: GatewayDto) -> Result { + // Instead of using routing_score_successes / routing_score_samples, we use the + // number of successful testruns in the last 24h. + let routing_score = 0f32; + let config_score = 0u32; + let last_updated_utc = + timestamp_as_utc(value.last_updated_utc.cast_checked()?).to_rfc3339(); + let last_testrun_utc = value + .last_testrun_utc + .and_then(|i| i.cast_checked().ok()) + .map(|t| timestamp_as_utc(t).to_rfc3339()); + + let self_described = value.self_described.clone().unwrap_or("null".to_string()); + let explorer_pretty_bond = value + .explorer_pretty_bond + .clone() + .unwrap_or("null".to_string()); + let last_probe_result = value + .last_probe_result + .clone() + .unwrap_or("null".to_string()); + let last_probe_log = value.last_probe_log.clone(); + + let self_described = serde_json::from_str(&self_described).unwrap_or(None); + let explorer_pretty_bond = serde_json::from_str(&explorer_pretty_bond).unwrap_or(None); + let last_probe_result = serde_json::from_str(&last_probe_result).unwrap_or(None); + + let bonded = value.bonded; + let blacklisted = value.blacklisted; + let performance = value.performance as u8; + + let description = NodeDescription { + moniker: value.moniker.clone(), + website: value.website.clone(), + security_contact: value.security_contact.clone(), + details: value.details.clone(), + }; + + Ok(http::models::Gateway { + gateway_identity_key: value.gateway_identity_key.clone(), + bonded, + blacklisted, + performance, + self_described, + explorer_pretty_bond, + description, + last_probe_result, + last_probe_log, + routing_score, + config_score, + last_testrun_utc, + last_updated_utc, + }) + } +} + +fn timestamp_as_utc(unix_timestamp: u64) -> chrono::DateTime { + let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(unix_timestamp); + d.into() +} + +pub(crate) struct MixnodeRecord { + pub(crate) mix_id: u32, + pub(crate) identity_key: String, + pub(crate) bonded: bool, + pub(crate) total_stake: i64, + pub(crate) host: String, + pub(crate) http_port: u16, + pub(crate) blacklisted: bool, + pub(crate) full_details: String, + pub(crate) self_described: Option, + pub(crate) last_updated_utc: i64, + pub(crate) is_dp_delegatee: bool, +} + +#[derive(Debug, Clone)] +pub(crate) struct MixnodeDto { + pub(crate) mix_id: i64, + pub(crate) bonded: bool, + pub(crate) blacklisted: bool, + pub(crate) is_dp_delegatee: bool, + pub(crate) total_stake: i64, + pub(crate) full_details: String, + pub(crate) self_described: Option, + pub(crate) last_updated_utc: i64, + pub(crate) moniker: String, + pub(crate) website: String, + pub(crate) security_contact: String, + pub(crate) details: String, +} + +impl TryFrom for http::models::Mixnode { + type Error = anyhow::Error; + + fn try_from(value: MixnodeDto) -> Result { + let mix_id = value.mix_id.cast_checked()?; + let full_details = value.full_details.clone(); + let full_details = serde_json::from_str(&full_details).unwrap_or(None); + + let self_described = value + .self_described + .clone() + .map(|v| serde_json::from_str(&v).unwrap_or(serde_json::Value::Null)); + + let last_updated_utc = + timestamp_as_utc(value.last_updated_utc.cast_checked()?).to_rfc3339(); + let blacklisted = value.blacklisted; + let is_dp_delegatee = value.is_dp_delegatee; + let moniker = value.moniker.clone(); + let website = value.website.clone(); + let security_contact = value.security_contact.clone(); + let details = value.details.clone(); + + Ok(http::models::Mixnode { + mix_id, + bonded: value.bonded, + blacklisted, + is_dp_delegatee, + total_stake: value.total_stake, + full_details, + description: NodeDescription { + moniker, + website, + security_contact, + details, + }, + self_described, + last_updated_utc, + }) + } +} + +#[allow(unused)] +#[derive(Debug, Clone)] +pub(crate) struct BondedStatusDto { + pub(crate) id: i64, + pub(crate) identity_key: String, + pub(crate) bonded: bool, +} + +#[allow(unused)] +#[derive(Debug, Clone, Default)] +pub(crate) struct SummaryDto { + pub(crate) key: String, + pub(crate) value_json: String, + pub(crate) last_updated_utc: i64, +} + +#[derive(Debug, Clone, Default)] +pub(crate) struct SummaryHistoryDto { + #[allow(dead_code)] + pub id: i64, + pub date: String, + pub value_json: String, + pub timestamp_utc: i64, +} + +impl TryFrom for SummaryHistory { + type Error = anyhow::Error; + + fn try_from(value: SummaryHistoryDto) -> Result { + let value_json = serde_json::from_str(&value.value_json).unwrap_or_default(); + Ok(SummaryHistory { + value_json, + date: value.date.clone(), + timestamp_utc: timestamp_as_utc(value.timestamp_utc.cast_checked()?).to_rfc3339(), + }) + } +} + +pub(crate) const MIXNODES_BONDED_COUNT: &str = "mixnodes.bonded.count"; +pub(crate) const MIXNODES_BONDED_ACTIVE: &str = "mixnodes.bonded.active"; +pub(crate) const MIXNODES_BONDED_INACTIVE: &str = "mixnodes.bonded.inactive"; +pub(crate) const MIXNODES_BONDED_RESERVE: &str = "mixnodes.bonded.reserve"; +pub(crate) const MIXNODES_BLACKLISTED_COUNT: &str = "mixnodes.blacklisted.count"; + +pub(crate) const GATEWAYS_BONDED_COUNT: &str = "gateways.bonded.count"; +pub(crate) const GATEWAYS_EXPLORER_COUNT: &str = "gateways.explorer.count"; +pub(crate) const GATEWAYS_BLACKLISTED_COUNT: &str = "gateways.blacklisted.count"; + +pub(crate) const MIXNODES_HISTORICAL_COUNT: &str = "mixnodes.historical.count"; +pub(crate) const GATEWAYS_HISTORICAL_COUNT: &str = "gateways.historical.count"; + +// `utoipa`` goes crazy if you use module-qualified prefix as field type so we +// have to import it +use gateway::GatewaySummary; +use mixnode::MixnodeSummary; + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub(crate) struct NetworkSummary { + pub(crate) mixnodes: MixnodeSummary, + pub(crate) gateways: GatewaySummary, +} + +pub(crate) mod mixnode { + use super::*; + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct MixnodeSummary { + pub(crate) bonded: MixnodeSummaryBonded, + pub(crate) blacklisted: MixnodeSummaryBlacklisted, + pub(crate) historical: MixnodeSummaryHistorical, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct MixnodeSummaryBonded { + pub(crate) count: i32, + pub(crate) active: i32, + pub(crate) inactive: i32, + pub(crate) reserve: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct MixnodeSummaryBlacklisted { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct MixnodeSummaryHistorical { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } +} + +pub(crate) mod gateway { + use super::*; + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct GatewaySummary { + pub(crate) bonded: GatewaySummaryBonded, + pub(crate) blacklisted: GatewaySummaryBlacklisted, + pub(crate) historical: GatewaySummaryHistorical, + pub(crate) explorer: GatewaySummaryExplorer, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct GatewaySummaryExplorer { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct GatewaySummaryBonded { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct GatewaySummaryHistorical { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } + + #[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] + pub(crate) struct GatewaySummaryBlacklisted { + pub(crate) count: i32, + pub(crate) last_updated_utc: String, + } +} + +#[allow(dead_code)] // not dead code, this is SQL data model +#[derive(Debug, Clone)] +pub struct TestRunDto { + pub id: i64, + pub gateway_id: i64, + pub status: i64, + pub timestamp_utc: i64, + pub ip_address: String, + pub log: String, +} + +#[derive(Debug, Clone, strum_macros::Display, EnumString, FromRepr, PartialEq)] +#[repr(u8)] +pub(crate) enum TestRunStatus { + Complete = 2, + InProgress = 1, + Queued = 0, +} + +#[derive(Debug, Clone)] +pub struct GatewayIdentityDto { + pub gateway_identity_key: String, + pub bonded: bool, +} + +#[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros +#[derive(Debug, Clone)] +pub struct GatewayInfoDto { + pub id: i64, + pub gateway_identity_key: String, + pub self_described: Option, + pub explorer_pretty_bond: Option, +} diff --git a/nym-node-status-api/src/db/queries/gateways.rs b/nym-node-status-api/src/db/queries/gateways.rs new file mode 100644 index 0000000000..bcf9c2d6ca --- /dev/null +++ b/nym-node-status-api/src/db/queries/gateways.rs @@ -0,0 +1,180 @@ +use crate::{ + db::{ + models::{BondedStatusDto, GatewayDto, GatewayRecord}, + DbPool, + }, + http::models::Gateway, +}; +use futures_util::TryStreamExt; +use nym_validator_client::models::NymNodeDescription; +use sqlx::{pool::PoolConnection, Sqlite}; +use tracing::error; + +pub(crate) async fn select_gateway_identity( + conn: &mut PoolConnection, + gateway_pk: i64, +) -> anyhow::Result { + let record = sqlx::query!( + r#"SELECT + gateway_identity_key + FROM + gateways + WHERE + id = ?"#, + gateway_pk + ) + .fetch_one(conn.as_mut()) + .await?; + + Ok(record.gateway_identity_key) +} + +pub(crate) async fn insert_gateways( + pool: &DbPool, + gateways: Vec, +) -> anyhow::Result<()> { + let mut db = pool.acquire().await?; + for record in gateways { + sqlx::query!( + "INSERT INTO gateways + (gateway_identity_key, bonded, blacklisted, + self_described, explorer_pretty_bond, + last_updated_utc, performance) + VALUES (?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(gateway_identity_key) DO UPDATE SET + bonded=excluded.bonded, + blacklisted=excluded.blacklisted, + self_described=excluded.self_described, + explorer_pretty_bond=excluded.explorer_pretty_bond, + last_updated_utc=excluded.last_updated_utc, + performance = excluded.performance;", + record.identity_key, + record.bonded, + record.blacklisted, + record.self_described, + record.explorer_pretty_bond, + record.last_updated_utc, + record.performance + ) + .execute(&mut *db) + .await?; + } + + Ok(()) +} + +pub(crate) async fn write_blacklisted_gateways_to_db<'a, I>( + pool: &DbPool, + gateways: I, +) -> anyhow::Result<()> +where + I: Iterator, +{ + let mut conn = pool.acquire().await?; + for gateway_identity_key in gateways { + sqlx::query!( + "UPDATE gateways + SET blacklisted = true + WHERE gateway_identity_key = ?;", + gateway_identity_key, + ) + .execute(&mut *conn) + .await?; + } + + Ok(()) +} + +/// Ensure all gateways that are set as bonded, are still bonded +pub(crate) async fn ensure_gateways_still_bonded( + pool: &DbPool, + gateways: &[&NymNodeDescription], +) -> anyhow::Result { + let bonded_gateways_rows = get_all_bonded_gateways_row_ids_by_status(pool, true).await?; + let unbonded_gateways_rows = bonded_gateways_rows.iter().filter(|v| { + !gateways + .iter() + .any(|bonded| *bonded.ed25519_identity_key().to_base58_string() == v.identity_key) + }); + + let recently_unbonded_gateways = unbonded_gateways_rows.to_owned().count(); + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + let mut transaction = pool.begin().await?; + for row in unbonded_gateways_rows { + sqlx::query!( + "UPDATE gateways + SET bonded = ?, last_updated_utc = ? + WHERE id = ?;", + false, + last_updated_utc, + row.id, + ) + .execute(&mut *transaction) + .await?; + } + transaction.commit().await?; + + Ok(recently_unbonded_gateways) +} + +async fn get_all_bonded_gateways_row_ids_by_status( + pool: &DbPool, + status: bool, +) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + BondedStatusDto, + r#"SELECT + id as "id!", + gateway_identity_key as "identity_key!", + bonded as "bonded: bool" + FROM gateways + WHERE bonded = ?"#, + status, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + Ok(items) +} + +pub(crate) async fn get_all_gateways(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + GatewayDto, + r#"SELECT + gw.gateway_identity_key as "gateway_identity_key!", + gw.bonded as "bonded: bool", + gw.blacklisted as "blacklisted: bool", + gw.performance as "performance!", + gw.self_described as "self_described?", + gw.explorer_pretty_bond as "explorer_pretty_bond?", + gw.last_probe_result as "last_probe_result?", + gw.last_probe_log as "last_probe_log?", + gw.last_testrun_utc as "last_testrun_utc?", + gw.last_updated_utc as "last_updated_utc!", + COALESCE(gd.moniker, "NA") as "moniker!", + COALESCE(gd.website, "NA") as "website!", + COALESCE(gd.security_contact, "NA") as "security_contact!", + COALESCE(gd.details, "NA") as "details!" + FROM gateways gw + LEFT JOIN gateway_description gd + ON gw.gateway_identity_key = gd.gateway_identity_key + ORDER BY gw.gateway_identity_key"#, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + let items: Vec = items + .into_iter() + .map(|item| item.try_into()) + .collect::>>() + .map_err(|e| { + error!("Conversion from DTO failed: {e}. Invalidly stored data?"); + e + })?; + tracing::trace!("Fetched {} gateways from DB", items.len()); + Ok(items) +} diff --git a/nym-node-status-api/src/db/queries/misc.rs b/nym-node-status-api/src/db/queries/misc.rs new file mode 100644 index 0000000000..2aa6356051 --- /dev/null +++ b/nym-node-status-api/src/db/queries/misc.rs @@ -0,0 +1,88 @@ +use crate::db::{models::NetworkSummary, DbPool}; +use chrono::{DateTime, Utc}; + +/// take `last_updated` instead of calculating it so that `summary` matches +/// `daily_summary` +pub(crate) async fn insert_summaries( + pool: &DbPool, + summaries: &[(&str, &usize)], + summary: &NetworkSummary, + last_updated: DateTime, +) -> anyhow::Result<()> { + insert_summary(pool, summaries, last_updated).await?; + + insert_summary_history(pool, summary, last_updated).await?; + + Ok(()) +} + +async fn insert_summary( + pool: &DbPool, + summaries: &[(&str, &usize)], + last_updated: DateTime, +) -> anyhow::Result<()> { + let timestamp = last_updated.timestamp(); + let mut tx = pool.begin().await?; + + for (kind, value) in summaries { + let value = value.to_string(); + sqlx::query!( + "INSERT INTO summary + (key, value_json, last_updated_utc) + VALUES (?, ?, ?) + ON CONFLICT(key) DO UPDATE SET + value_json=excluded.value_json, + last_updated_utc=excluded.last_updated_utc;", + kind, + value, + timestamp + ) + .execute(&mut *tx) + .await + .map_err(|err| { + tracing::error!("Failed to insert data for {kind}: {err}, aborting transaction",); + err + })?; + } + + tx.commit().await?; + + Ok(()) +} + +/// For ``, `summary_history` is updated with fresh data on every +/// iteration. +/// +/// After UTC midnight, summary is inserted for `` and last entry for +/// `` stays there forever. +/// +/// This is not aggregate data, it's a set of latest data points +async fn insert_summary_history( + pool: &DbPool, + summary: &NetworkSummary, + last_updated: DateTime, +) -> anyhow::Result<()> { + let mut conn = pool.acquire().await?; + + let value_json = serde_json::to_string(&summary)?; + let timestamp = last_updated.timestamp(); + let now_rfc3339 = last_updated.to_rfc3339(); + // YYYY-MM-DD, without time + let date = &now_rfc3339[..10]; + + sqlx::query!( + "INSERT INTO summary_history + (date, timestamp_utc, value_json) + VALUES (?, ?, ?) + ON CONFLICT(date) DO UPDATE SET + timestamp_utc=excluded.timestamp_utc, + value_json=excluded.value_json;", + date, + timestamp, + value_json + ) + .execute(&mut *conn) + .await?; + + Ok(()) +} diff --git a/nym-node-status-api/src/db/queries/mixnodes.rs b/nym-node-status-api/src/db/queries/mixnodes.rs new file mode 100644 index 0000000000..58af9bd429 --- /dev/null +++ b/nym-node-status-api/src/db/queries/mixnodes.rs @@ -0,0 +1,177 @@ +use futures_util::TryStreamExt; +use nym_validator_client::models::MixNodeBondAnnotated; +use tracing::error; + +use crate::{ + db::{ + models::{BondedStatusDto, MixnodeDto, MixnodeRecord}, + DbPool, + }, + http::models::{DailyStats, Mixnode}, +}; + +pub(crate) async fn insert_mixnodes( + pool: &DbPool, + mixnodes: Vec, +) -> anyhow::Result<()> { + let mut conn = pool.acquire().await?; + + for record in mixnodes.iter() { + // https://www.sqlite.org/lang_upsert.html + sqlx::query!( + "INSERT INTO mixnodes + (mix_id, identity_key, bonded, total_stake, + host, http_api_port, blacklisted, full_details, + self_described, last_updated_utc, is_dp_delegatee) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(mix_id) DO UPDATE SET + bonded=excluded.bonded, + total_stake=excluded.total_stake, host=excluded.host, + http_api_port=excluded.http_api_port,blacklisted=excluded.blacklisted, + full_details=excluded.full_details,self_described=excluded.self_described, + last_updated_utc=excluded.last_updated_utc, + is_dp_delegatee = excluded.is_dp_delegatee;", + record.mix_id, + record.identity_key, + record.bonded, + record.total_stake, + record.host, + record.http_port, + record.blacklisted, + record.full_details, + record.self_described, + record.last_updated_utc, + record.is_dp_delegatee + ) + .execute(&mut *conn) + .await?; + } + + Ok(()) +} + +pub(crate) async fn get_all_mixnodes(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + MixnodeDto, + r#"SELECT + mn.mix_id as "mix_id!", + mn.bonded as "bonded: bool", + mn.blacklisted as "blacklisted: bool", + mn.is_dp_delegatee as "is_dp_delegatee: bool", + mn.total_stake as "total_stake!", + mn.full_details as "full_details!", + mn.self_described as "self_described", + mn.last_updated_utc as "last_updated_utc!", + COALESCE(md.moniker, "NA") as "moniker!", + COALESCE(md.website, "NA") as "website!", + COALESCE(md.security_contact, "NA") as "security_contact!", + COALESCE(md.details, "NA") as "details!" + FROM mixnodes mn + LEFT JOIN mixnode_description md ON mn.mix_id = md.mix_id + ORDER BY mn.mix_id"# + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + let items = items + .into_iter() + .map(|item| item.try_into()) + .collect::>>() + .map_err(|e| { + error!("Conversion from DTO failed: {e}. Invalidly stored data?"); + e + })?; + Ok(items) +} + +/// We fetch the latest 30 days of data as a subquery and then +/// return it in ascending order, so we don't break existing UI +pub(crate) async fn get_daily_stats(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + DailyStats, + r#" + SELECT + date_utc as "date_utc!", + packets_received as "total_packets_received!: i64", + packets_sent as "total_packets_sent!: i64", + packets_dropped as "total_packets_dropped!: i64", + total_stake as "total_stake!: i64" + FROM ( + SELECT + date_utc, + SUM(packets_received) as packets_received, + SUM(packets_sent) as packets_sent, + SUM(packets_dropped) as packets_dropped, + SUM(total_stake) as total_stake + FROM mixnode_daily_stats + GROUP BY date_utc + ORDER BY date_utc DESC + LIMIT 30 + ) + GROUP BY date_utc + ORDER BY date_utc + "# + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + Ok(items) +} + +/// Ensure all mixnodes that are set as bonded, are still bonded +pub(crate) async fn ensure_mixnodes_still_bonded( + pool: &DbPool, + mixnodes: &[MixNodeBondAnnotated], +) -> anyhow::Result { + let bonded_mixnodes_rows = get_all_bonded_mixnodes_row_ids_by_status(pool, true).await?; + let unbonded_mixnodes_rows = bonded_mixnodes_rows.iter().filter(|v| { + !mixnodes + .iter() + .any(|bonded| *bonded.mixnode_details.bond_information.identity() == v.identity_key) + }); + + let recently_unbonded_mixnodes = unbonded_mixnodes_rows.to_owned().count(); + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + let mut transaction = pool.begin().await?; + for row in unbonded_mixnodes_rows { + sqlx::query!( + "UPDATE mixnodes + SET bonded = ?, last_updated_utc = ? + WHERE id = ?;", + false, + last_updated_utc, + row.id, + ) + .execute(&mut *transaction) + .await?; + } + transaction.commit().await?; + + Ok(recently_unbonded_mixnodes) +} + +async fn get_all_bonded_mixnodes_row_ids_by_status( + pool: &DbPool, + status: bool, +) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + BondedStatusDto, + r#"SELECT + id as "id!", + identity_key as "identity_key!", + bonded as "bonded: bool" + FROM mixnodes + WHERE bonded = ?"#, + status, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + Ok(items) +} diff --git a/nym-node-status-api/src/db/queries/mod.rs b/nym-node-status-api/src/db/queries/mod.rs new file mode 100644 index 0000000000..fe22ec27aa --- /dev/null +++ b/nym-node-status-api/src/db/queries/mod.rs @@ -0,0 +1,15 @@ +mod gateways; +mod misc; +mod mixnodes; +mod summary; +pub(crate) mod testruns; + +pub(crate) use gateways::{ + ensure_gateways_still_bonded, get_all_gateways, insert_gateways, select_gateway_identity, + write_blacklisted_gateways_to_db, +}; +pub(crate) use misc::insert_summaries; +pub(crate) use mixnodes::{ + ensure_mixnodes_still_bonded, get_all_mixnodes, get_daily_stats, insert_mixnodes, +}; +pub(crate) use summary::{get_summary, get_summary_history}; diff --git a/nym-node-status-api/src/db/queries/summary.rs b/nym-node-status-api/src/db/queries/summary.rs new file mode 100644 index 0000000000..103712a9a4 --- /dev/null +++ b/nym-node-status-api/src/db/queries/summary.rs @@ -0,0 +1,209 @@ +use chrono::{DateTime, Utc}; +use futures_util::TryStreamExt; +use std::collections::HashMap; +use tracing::error; + +use crate::{ + db::{ + models::{ + gateway::{ + GatewaySummary, GatewaySummaryBlacklisted, GatewaySummaryBonded, + GatewaySummaryExplorer, GatewaySummaryHistorical, + }, + mixnode::{ + MixnodeSummary, MixnodeSummaryBlacklisted, MixnodeSummaryBonded, + MixnodeSummaryHistorical, + }, + NetworkSummary, SummaryDto, SummaryHistoryDto, + }, + DbPool, + }, + http::{ + error::{HttpError, HttpResult}, + models::SummaryHistory, + }, +}; + +pub(crate) async fn get_summary_history(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + let items = sqlx::query_as!( + SummaryHistoryDto, + r#"SELECT + id as "id!", + date as "date!", + timestamp_utc as "timestamp_utc!", + value_json as "value_json!" + FROM summary_history + ORDER BY date DESC + LIMIT 30"#, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + let items = items + .into_iter() + .map(|item| item.try_into()) + .collect::>>() + .map_err(|e| { + error!("Conversion from DTO failed: {e}. Invalidly stored data?"); + e + })?; + Ok(items) +} + +async fn get_summary_dto(pool: &DbPool) -> anyhow::Result> { + let mut conn = pool.acquire().await?; + Ok(sqlx::query_as!( + SummaryDto, + r#"SELECT + key as "key!", + value_json as "value_json!", + last_updated_utc as "last_updated_utc!" + FROM summary"# + ) + .fetch(&mut *conn) + .try_collect::>() + .await?) +} + +pub(crate) async fn get_summary(pool: &DbPool) -> HttpResult { + let items = get_summary_dto(pool).await.map_err(|err| { + tracing::error!("Couldn't get Summary from DB: {err}"); + HttpError::internal() + })?; + from_summary_dto(items).await +} + +async fn from_summary_dto(items: Vec) -> HttpResult { + const MIXNODES_BONDED_COUNT: &str = "mixnodes.bonded.count"; + const MIXNODES_BONDED_ACTIVE: &str = "mixnodes.bonded.active"; + const MIXNODES_BONDED_INACTIVE: &str = "mixnodes.bonded.inactive"; + const MIXNODES_BONDED_RESERVE: &str = "mixnodes.bonded.reserve"; + const MIXNODES_BLACKLISTED_COUNT: &str = "mixnodes.blacklisted.count"; + const GATEWAYS_BONDED_COUNT: &str = "gateways.bonded.count"; + const GATEWAYS_EXPLORER_COUNT: &str = "gateways.explorer.count"; + const GATEWAYS_BLACKLISTED_COUNT: &str = "gateways.blacklisted.count"; + const MIXNODES_HISTORICAL_COUNT: &str = "mixnodes.historical.count"; + const GATEWAYS_HISTORICAL_COUNT: &str = "gateways.historical.count"; + + // convert database rows into a map by key + let mut map = HashMap::new(); + for item in items { + map.insert(item.key.clone(), item); + } + + // check we have all the keys we are expecting, and build up a map of errors for missing one + let keys = [ + GATEWAYS_BONDED_COUNT, + GATEWAYS_EXPLORER_COUNT, + GATEWAYS_HISTORICAL_COUNT, + GATEWAYS_BLACKLISTED_COUNT, + MIXNODES_BLACKLISTED_COUNT, + MIXNODES_BONDED_ACTIVE, + MIXNODES_BONDED_COUNT, + MIXNODES_BONDED_INACTIVE, + MIXNODES_BONDED_RESERVE, + MIXNODES_HISTORICAL_COUNT, + ]; + + let mut errors: Vec<&str> = vec![]; + for key in keys { + if !map.contains_key(key) { + errors.push(key); + } + } + + // return an error if anything is missing, with a nice list + if !errors.is_empty() { + tracing::error!("Summary value missing: {}", errors.join(", ")); + return Err(HttpError::internal()); + } + + // strip the options and use default values (anything missing is trapped above) + let mixnodes_bonded_count: SummaryDto = + map.get(MIXNODES_BONDED_COUNT).cloned().unwrap_or_default(); + let mixnodes_bonded_active: SummaryDto = + map.get(MIXNODES_BONDED_ACTIVE).cloned().unwrap_or_default(); + let mixnodes_bonded_inactive: SummaryDto = map + .get(MIXNODES_BONDED_INACTIVE) + .cloned() + .unwrap_or_default(); + let mixnodes_bonded_reserve: SummaryDto = map + .get(MIXNODES_BONDED_RESERVE) + .cloned() + .unwrap_or_default(); + let mixnodes_blacklisted_count: SummaryDto = map + .get(MIXNODES_BLACKLISTED_COUNT) + .cloned() + .unwrap_or_default(); + let gateways_bonded_count: SummaryDto = + map.get(GATEWAYS_BONDED_COUNT).cloned().unwrap_or_default(); + let gateways_explorer_count: SummaryDto = map + .get(GATEWAYS_EXPLORER_COUNT) + .cloned() + .unwrap_or_default(); + let mixnodes_historical_count: SummaryDto = map + .get(MIXNODES_HISTORICAL_COUNT) + .cloned() + .unwrap_or_default(); + let gateways_historical_count: SummaryDto = map + .get(GATEWAYS_HISTORICAL_COUNT) + .cloned() + .unwrap_or_default(); + let gateways_blacklisted_count: SummaryDto = map + .get(GATEWAYS_BLACKLISTED_COUNT) + .cloned() + .unwrap_or_default(); + + Ok(NetworkSummary { + mixnodes: MixnodeSummary { + bonded: MixnodeSummaryBonded { + count: to_count_i32(&mixnodes_bonded_count), + active: to_count_i32(&mixnodes_bonded_active), + reserve: to_count_i32(&mixnodes_bonded_reserve), + inactive: to_count_i32(&mixnodes_bonded_inactive), + last_updated_utc: to_timestamp(&mixnodes_bonded_count), + }, + blacklisted: MixnodeSummaryBlacklisted { + count: to_count_i32(&mixnodes_blacklisted_count), + last_updated_utc: to_timestamp(&mixnodes_blacklisted_count), + }, + historical: MixnodeSummaryHistorical { + count: to_count_i32(&mixnodes_historical_count), + last_updated_utc: to_timestamp(&mixnodes_historical_count), + }, + }, + gateways: GatewaySummary { + bonded: GatewaySummaryBonded { + count: to_count_i32(&gateways_bonded_count), + last_updated_utc: to_timestamp(&gateways_bonded_count), + }, + blacklisted: GatewaySummaryBlacklisted { + count: to_count_i32(&gateways_blacklisted_count), + last_updated_utc: to_timestamp(&gateways_blacklisted_count), + }, + historical: GatewaySummaryHistorical { + count: to_count_i32(&gateways_historical_count), + last_updated_utc: to_timestamp(&gateways_historical_count), + }, + explorer: GatewaySummaryExplorer { + count: to_count_i32(&gateways_explorer_count), + last_updated_utc: to_timestamp(&gateways_explorer_count), + }, + }, + }) +} + +fn to_count_i32(value: &SummaryDto) -> i32 { + value.value_json.parse::().unwrap_or_default() +} + +fn to_timestamp(value: &SummaryDto) -> String { + timestamp_as_utc(value.last_updated_utc as u64).to_rfc3339() +} + +fn timestamp_as_utc(unix_timestamp: u64) -> DateTime { + let d = std::time::UNIX_EPOCH + std::time::Duration::from_secs(unix_timestamp); + d.into() +} diff --git a/nym-node-status-api/src/db/queries/testruns.rs b/nym-node-status-api/src/db/queries/testruns.rs new file mode 100644 index 0000000000..91a3a86b13 --- /dev/null +++ b/nym-node-status-api/src/db/queries/testruns.rs @@ -0,0 +1,184 @@ +use crate::db::DbPool; +use crate::http::models::TestrunAssignment; +use crate::{ + db::models::{TestRunDto, TestRunStatus}, + testruns::now_utc, +}; +use anyhow::Context; +use chrono::Duration; +use sqlx::{pool::PoolConnection, Sqlite}; + +pub(crate) async fn get_in_progress_testrun_by_id( + conn: &mut PoolConnection, + testrun_id: i64, +) -> anyhow::Result { + sqlx::query_as!( + TestRunDto, + r#"SELECT + id as "id!", + gateway_id as "gateway_id!", + status as "status!", + timestamp_utc as "timestamp_utc!", + ip_address as "ip_address!", + log as "log!" + FROM testruns + WHERE + id = ? + AND + status = ? + ORDER BY timestamp_utc"#, + testrun_id, + TestRunStatus::InProgress as i64, + ) + .fetch_one(conn.as_mut()) + .await + .context(format!("Couldn't retrieve testrun {testrun_id}")) +} + +pub(crate) async fn update_testruns_older_than(db: &DbPool, age: Duration) -> anyhow::Result { + let mut conn = db.acquire().await?; + let previous_run = now_utc() - age; + let cutoff_timestamp = previous_run.timestamp(); + + let res = sqlx::query!( + r#"UPDATE + testruns + SET + status = ? + WHERE + status = ? + AND + timestamp_utc < ? + "#, + TestRunStatus::Queued as i64, + TestRunStatus::InProgress as i64, + cutoff_timestamp + ) + .execute(conn.as_mut()) + .await?; + + let stale_testruns = res.rows_affected(); + if stale_testruns > 0 { + tracing::debug!( + "Refreshed {} stale testruns, scheduled before {} but not yet finished", + stale_testruns, + previous_run + ); + } + + Ok(stale_testruns) +} + +pub(crate) async fn get_oldest_testrun_and_make_it_pending( + conn: &mut PoolConnection, +) -> anyhow::Result> { + // find & mark as "In progress" in the same transaction to avoid race conditions + let returning = sqlx::query!( + r#"UPDATE testruns + SET status = ? + WHERE rowid = + ( + SELECT rowid + FROM testruns + WHERE status = ? + ORDER BY timestamp_utc asc + LIMIT 1 + ) + RETURNING + id as "id!", + gateway_id + "#, + TestRunStatus::InProgress as i64, + TestRunStatus::Queued as i64, + ) + .fetch_optional(conn.as_mut()) + .await?; + + if let Some(testrun) = returning { + let gw_identity = sqlx::query!( + r#" + SELECT + id, + gateway_identity_key + FROM gateways + WHERE id = ? + LIMIT 1"#, + testrun.gateway_id + ) + .fetch_one(conn.as_mut()) + .await?; + + Ok(Some(TestrunAssignment { + testrun_id: testrun.id, + gateway_identity_key: gw_identity.gateway_identity_key, + })) + } else { + Ok(None) + } +} + +pub(crate) async fn update_testrun_status( + conn: &mut PoolConnection, + testrun_id: i64, + status: TestRunStatus, +) -> anyhow::Result<()> { + let status = status as u32; + sqlx::query!( + "UPDATE testruns SET status = ? WHERE id = ?", + status, + testrun_id + ) + .execute(conn.as_mut()) + .await?; + + Ok(()) +} + +pub(crate) async fn update_gateway_last_probe_log( + conn: &mut PoolConnection, + gateway_pk: i64, + log: &str, +) -> anyhow::Result<()> { + sqlx::query!( + "UPDATE gateways SET last_probe_log = ? WHERE id = ?", + log, + gateway_pk + ) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} + +pub(crate) async fn update_gateway_last_probe_result( + conn: &mut PoolConnection, + gateway_pk: i64, + result: &str, +) -> anyhow::Result<()> { + sqlx::query!( + "UPDATE gateways SET last_probe_result = ? WHERE id = ?", + result, + gateway_pk + ) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} + +pub(crate) async fn update_gateway_score( + conn: &mut PoolConnection, + gateway_pk: i64, +) -> anyhow::Result<()> { + let now = now_utc().timestamp(); + sqlx::query!( + "UPDATE gateways SET last_testrun_utc = ?, last_updated_utc = ? WHERE id = ?", + now, + now, + gateway_pk + ) + .execute(conn.as_mut()) + .await + .map(drop) + .map_err(From::from) +} diff --git a/nym-node-status-api/src/http/api/gateways.rs b/nym-node-status-api/src/http/api/gateways.rs new file mode 100644 index 0000000000..c1f4073767 --- /dev/null +++ b/nym-node-status-api/src/http/api/gateways.rs @@ -0,0 +1,110 @@ +use axum::{ + extract::{Path, Query, State}, + Json, Router, +}; +use serde::Deserialize; +use utoipa::IntoParams; + +use crate::http::{ + error::{HttpError, HttpResult}, + models::{Gateway, GatewaySkinny}, + state::AppState, + PagedResult, Pagination, +}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(gateways)) + .route("/skinny", axum::routing::get(gateways_skinny)) + .route("/:identity_key", axum::routing::get(get_gateway)) +} + +#[utoipa::path( + tag = "Gateways", + get, + params( + Pagination + ), + path = "/v2/gateways", + responses( + (status = 200, body = PagedGateway) + ) +)] +async fn gateways( + Query(pagination): Query, + State(state): State, +) -> HttpResult>> { + let db = state.db_pool(); + let res = state.cache().get_gateway_list(db).await; + + Ok(Json(PagedResult::paginate(pagination, res))) +} + +#[utoipa::path( + tag = "Gateways", + get, + params( + Pagination + ), + path = "/v2/gateways/skinny", + responses( + (status = 200, body = PagedGatewaySkinny) + ) +)] +async fn gateways_skinny( + Query(pagination): Query, + State(state): State, +) -> HttpResult>> { + let db = state.db_pool(); + let res = state.cache().get_gateway_list(db).await; + let res: Vec = res + .iter() + .filter(|g| g.bonded) + .map(|g| GatewaySkinny { + gateway_identity_key: g.gateway_identity_key.clone(), + self_described: g.self_described.clone(), + performance: g.performance, + explorer_pretty_bond: g.explorer_pretty_bond.clone(), + last_probe_result: g.last_probe_result.clone(), + last_testrun_utc: g.last_testrun_utc.clone(), + last_updated_utc: g.last_updated_utc.clone(), + routing_score: g.routing_score, + config_score: g.config_score, + }) + .collect(); + + Ok(Json(PagedResult::paginate(pagination, res))) +} + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct IdentityKeyParam { + identity_key: String, +} + +#[utoipa::path( + tag = "Gateways", + get, + params( + IdentityKeyParam + ), + path = "/v2/gateways/{identity_key}", + responses( + (status = 200, body = Gateway) + ) +)] +async fn get_gateway( + Path(IdentityKeyParam { identity_key }): Path, + State(state): State, +) -> HttpResult> { + let db = state.db_pool(); + let res = state.cache().get_gateway_list(db).await; + + match res + .iter() + .find(|item| item.gateway_identity_key == identity_key) + { + Some(res) => Ok(Json(res.clone())), + None => Err(HttpError::invalid_input(identity_key)), + } +} diff --git a/nym-node-status-api/src/http/api/mixnodes.rs b/nym-node-status-api/src/http/api/mixnodes.rs new file mode 100644 index 0000000000..f42d0bf91c --- /dev/null +++ b/nym-node-status-api/src/http/api/mixnodes.rs @@ -0,0 +1,91 @@ +use axum::{ + extract::{Path, Query, State}, + Json, Router, +}; +use serde::Deserialize; +use tracing::instrument; +use utoipa::IntoParams; + +use crate::http::{ + error::{HttpError, HttpResult}, + models::{DailyStats, Mixnode}, + state::AppState, + PagedResult, Pagination, +}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(mixnodes)) + .route("/:mix_id", axum::routing::get(get_mixnodes)) + .route("/stats", axum::routing::get(get_stats)) +} + +#[utoipa::path( + tag = "Mixnodes", + get, + params( + Pagination + ), + path = "/v2/mixnodes", + responses( + (status = 200, body = PagedMixnode) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip_all, fields(page=pagination.page, size=pagination.size))] +async fn mixnodes( + Query(pagination): Query, + State(state): State, +) -> HttpResult>> { + let db = state.db_pool(); + let res = state.cache().get_mixnodes_list(db).await; + + Ok(Json(PagedResult::paginate(pagination, res))) +} + +#[derive(Deserialize, IntoParams)] +#[into_params(parameter_in = Path)] +struct MixIdParam { + mix_id: String, +} + +#[utoipa::path( + tag = "Mixnodes", + get, + params( + MixIdParam + ), + path = "/v2/mixnodes/{mix_id}", + responses( + (status = 200, body = Mixnode) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip_all, fields(mix_id = mix_id))] +async fn get_mixnodes( + Path(MixIdParam { mix_id }): Path, + State(state): State, +) -> HttpResult> { + match mix_id.parse::() { + Ok(parsed_mix_id) => { + let res = state.cache().get_mixnodes_list(state.db_pool()).await; + + match res.iter().find(|item| item.mix_id == parsed_mix_id) { + Some(res) => Ok(Json(res.clone())), + None => Err(HttpError::invalid_input(mix_id)), + } + } + Err(_e) => Err(HttpError::invalid_input(mix_id)), + } +} + +#[utoipa::path( + tag = "Mixnodes", + get, + path = "/v2/mixnodes/stats", + responses( + (status = 200, body = Vec) + ) +)] +async fn get_stats(State(state): State) -> HttpResult>> { + let stats = state.cache().get_mixnode_stats(state.db_pool()).await; + Ok(Json(stats)) +} diff --git a/nym-node-status-api/src/http/api/mod.rs b/nym-node-status-api/src/http/api/mod.rs new file mode 100644 index 0000000000..ed24fa80f5 --- /dev/null +++ b/nym-node-status-api/src/http/api/mod.rs @@ -0,0 +1,88 @@ +use anyhow::anyhow; +use axum::{response::Redirect, Router}; +use tokio::net::ToSocketAddrs; +use tower_http::{cors::CorsLayer, trace::TraceLayer}; +use utoipa::OpenApi; +use utoipa_swagger_ui::SwaggerUi; + +use crate::http::{server::HttpServer, state::AppState}; + +pub(crate) mod gateways; +pub(crate) mod mixnodes; +pub(crate) mod services; +pub(crate) mod summary; +pub(crate) mod testruns; + +pub(crate) struct RouterBuilder { + unfinished_router: Router, +} + +impl RouterBuilder { + pub(crate) fn with_default_routes() -> Self { + let router = Router::new() + .merge( + SwaggerUi::new("/swagger") + .url("/api-docs/openapi.json", super::api_docs::ApiDoc::openapi()), + ) + .route( + "/", + axum::routing::get(|| async { Redirect::permanent("/swagger") }), + ) + .nest( + "/v2", + Router::new() + .nest("/gateways", gateways::routes()) + .nest("/mixnodes", mixnodes::routes()) + .nest("/services", services::routes()) + .nest("/summary", summary::routes()), + ) + .nest( + "/internal", + Router::new().nest("/testruns", testruns::routes()), + ); + + Self { + unfinished_router: router, + } + } + + pub(crate) fn with_state(self, state: AppState) -> RouterWithState { + RouterWithState { + router: self.finalize_routes().with_state(state), + } + } + + fn finalize_routes(self) -> Router { + // layers added later wrap earlier layers + self.unfinished_router + // CORS layer needs to wrap other API layers + .layer(setup_cors()) + // logger should be outermost layer + .layer(TraceLayer::new_for_http()) + } +} + +pub(crate) struct RouterWithState { + router: Router, +} + +impl RouterWithState { + pub(crate) async fn build_server( + self, + bind_address: A, + ) -> anyhow::Result { + tokio::net::TcpListener::bind(bind_address) + .await + .map(|listener| HttpServer::new(self.router, listener)) + .map_err(|err| anyhow!("Couldn't bind to address due to {}", err)) + } +} + +fn setup_cors() -> CorsLayer { + use axum::http::Method; + CorsLayer::new() + .allow_origin(tower_http::cors::Any) + .allow_methods([Method::POST, Method::GET, Method::PATCH, Method::OPTIONS]) + .allow_headers(tower_http::cors::Any) + .allow_credentials(false) +} diff --git a/nym-node-status-api/src/http/api/services/json_path.rs b/nym-node-status-api/src/http/api/services/json_path.rs new file mode 100644 index 0000000000..caefc6489d --- /dev/null +++ b/nym-node-status-api/src/http/api/services/json_path.rs @@ -0,0 +1,58 @@ +use serde_json_path::JsonPath; + +use crate::http::models::Gateway; + +pub(super) struct ParseJsonPaths { + pub(super) path_ip_address: JsonPath, + pub(super) path_hostname: JsonPath, + pub(super) path_service_provider_client_id: JsonPath, +} + +impl ParseJsonPaths { + pub fn new() -> Result { + Ok(ParseJsonPaths { + path_ip_address: JsonPath::parse("$.host_information.ip_address[0]")?, + path_hostname: JsonPath::parse("$.host_information.hostname")?, + path_service_provider_client_id: JsonPath::parse("$.network_requester.address")?, + }) + } +} + +pub(super) struct ParsedDetails { + pub(super) ip_address: Option, + pub(super) hostname: Option, + pub(super) service_provider_client_id: Option, +} + +impl ParsedDetails { + fn get_string_from_json_path( + value: &Option, + path: &JsonPath, + ) -> Option { + match value { + Some(value) => path + .query(value) + .exactly_one() + .map(|v2| v2.as_str().map(|v3| v3.to_string())) + .ok() + .flatten(), + None => None, + } + } + pub fn new(paths: &ParseJsonPaths, g: &Gateway) -> ParsedDetails { + ParsedDetails { + hostname: ParsedDetails::get_string_from_json_path( + &g.self_described, + &paths.path_hostname, + ), + ip_address: ParsedDetails::get_string_from_json_path( + &g.self_described, + &paths.path_ip_address, + ), + service_provider_client_id: ParsedDetails::get_string_from_json_path( + &g.self_described, + &paths.path_service_provider_client_id, + ), + } + } +} diff --git a/nym-node-status-api/src/http/api/services/mod.rs b/nym-node-status-api/src/http/api/services/mod.rs new file mode 100644 index 0000000000..5650684c43 --- /dev/null +++ b/nym-node-status-api/src/http/api/services/mod.rs @@ -0,0 +1,134 @@ +use crate::http::{ + error::{HttpError, HttpResult}, + models::Service, + state::AppState, + PagedResult, Pagination, +}; +use axum::{ + extract::{Query, State}, + Json, Router, +}; +use json_path::{ParseJsonPaths, ParsedDetails}; +use tracing::instrument; + +mod json_path; + +pub(crate) fn routes() -> Router { + Router::new().route("/", axum::routing::get(mixnodes)) +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::IntoParams)] +#[into_params(parameter_in = Query)] +pub(crate) struct ServicesQueryParams { + size: Option, + page: Option, + wss: Option, + hostname: Option, + entry: Option, +} + +#[utoipa::path( + tag = "Services", + get, + params( + ServicesQueryParams, + ), + path = "/v2/services", + responses( + (status = 200, body = PagedService) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip(state))] +async fn mixnodes( + Query(ServicesQueryParams { + size, + page, + wss, + hostname, + entry, + }): Query, + State(state): State, +) -> HttpResult>> { + let db = state.db_pool(); + let cache = state.cache(); + + let show_only_wss = wss.unwrap_or(false); + let show_only_with_hostname = hostname.unwrap_or(false); + let show_entry_gateways_only = entry.unwrap_or(false); + + let paths = ParseJsonPaths::new().map_err(|e| { + tracing::error!("Invalidly configured ParseJsonPaths: {e}"); + HttpError::internal() + })?; + let res = cache.get_gateway_list(db).await; + let res: Vec = res + .iter() + .map(|g| { + let details = ParsedDetails::new(&paths, g); + + let s = Service { + gateway_identity_key: g.gateway_identity_key.clone(), + ip_address: details.ip_address, + service_provider_client_id: details.service_provider_client_id, + hostname: details.hostname, + last_successful_ping_utc: g.last_testrun_utc.clone(), + last_updated_utc: g.last_updated_utc.clone(), + // routing_score: g.routing_score, + routing_score: 1f32, + mixnet_websockets: g + .self_described + .clone() + .and_then(|s| s.get("mixnet_websockets").cloned()), + }; + + let f = ServiceFilter::new(&s); + + (s, f) + }) + .filter(|(_, f)| { + let mut keep = f.has_network_requester_sp; + + if show_entry_gateways_only { + keep = true; + } + + if show_only_wss { + keep &= f.has_wss; + } + if show_only_with_hostname { + keep &= f.has_hostname; + } + + keep + }) + .map(|(s, _)| s) + .collect(); + + Ok(Json(PagedResult::paginate(Pagination { size, page }, res))) +} + +struct ServiceFilter { + has_wss: bool, + has_network_requester_sp: bool, + has_hostname: bool, +} + +impl ServiceFilter { + fn new(s: &Service) -> Self { + let has_wss = match &s.mixnet_websockets { + Some(v) => v.get("wss_port").map(|v2| !v2.is_null()).unwrap_or(false), + None => false, + }; + let has_hostname = s.hostname.is_some(); + let has_network_requester_sp = match &s.service_provider_client_id { + Some(v) => !v.is_empty(), + None => false, + }; + + ServiceFilter { + has_wss, + has_hostname, + has_network_requester_sp, + } + } +} diff --git a/nym-node-status-api/src/http/api/summary.rs b/nym-node-status-api/src/http/api/summary.rs new file mode 100644 index 0000000000..729141509c --- /dev/null +++ b/nym-node-status-api/src/http/api/summary.rs @@ -0,0 +1,43 @@ +use axum::{extract::State, Json, Router}; +use tracing::instrument; + +use crate::{ + db::models::NetworkSummary, + http::{error::HttpResult, models::SummaryHistory, state::AppState}, +}; + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(summary)) + .route("/history", axum::routing::get(summary_history)) +} + +#[utoipa::path( + tag = "Summary", + get, + path = "/v2/summary", + responses( + (status = 200, body = NetworkSummary) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip_all)] +async fn summary(State(state): State) -> HttpResult> { + crate::db::queries::get_summary(state.db_pool()) + .await + .map(Json) +} + +#[utoipa::path( + tag = "Summary", + get, + path = "/v2/summary/history", + responses( + (status = 200, body = Vec) + ) +)] +#[instrument(level = tracing::Level::DEBUG, skip_all)] +async fn summary_history(State(state): State) -> HttpResult>> { + Ok(Json( + state.cache().get_summary_history(state.db_pool()).await, + )) +} diff --git a/nym-node-status-api/src/http/api/testruns.rs b/nym-node-status-api/src/http/api/testruns.rs new file mode 100644 index 0000000000..b13d62ba88 --- /dev/null +++ b/nym-node-status-api/src/http/api/testruns.rs @@ -0,0 +1,126 @@ +use axum::extract::DefaultBodyLimit; +use axum::Json; +use axum::{ + extract::{Path, State}, + Router, +}; +use reqwest::StatusCode; + +use crate::db::models::TestRunStatus; +use crate::db::queries; +use crate::{ + db, + http::{ + error::{HttpError, HttpResult}, + models::TestrunAssignment, + state::AppState, + }, +}; + +// TODO dz consider adding endpoint to trigger testrun scan for a given gateway_id +// like in H< src/http/testruns.rs + +pub(crate) fn routes() -> Router { + Router::new() + .route("/", axum::routing::get(request_testrun)) + .route("/:testrun_id", axum::routing::post(submit_testrun)) + .layer(DefaultBodyLimit::max(1024 * 1024 * 5)) +} + +#[tracing::instrument(level = "debug", skip_all)] +async fn request_testrun(State(state): State) -> HttpResult> { + // TODO dz log agent's key + // TODO dz log agent's network probe version + tracing::debug!("Agent requested testrun"); + + let db = state.db_pool(); + let mut conn = db + .acquire() + .await + .map_err(HttpError::internal_with_logging)?; + + return match db::queries::testruns::get_oldest_testrun_and_make_it_pending(&mut conn).await { + Ok(res) => { + if let Some(testrun) = res { + tracing::debug!( + "🏃‍ Assigned testrun row_id {} gateway {} to agent", + &testrun.testrun_id, + testrun.gateway_identity_key + ); + Ok(Json(testrun)) + } else { + tracing::debug!("No testruns available for agent"); + Err(HttpError::no_testruns_available()) + } + } + Err(err) => Err(HttpError::internal_with_logging(err)), + }; +} + +// TODO dz accept testrun_id as query parameter +#[tracing::instrument(level = "debug", skip_all)] +async fn submit_testrun( + Path(testrun_id): Path, + State(state): State, + body: String, +) -> HttpResult { + let db = state.db_pool(); + let mut conn = db + .acquire() + .await + .map_err(HttpError::internal_with_logging)?; + + let testrun = queries::testruns::get_in_progress_testrun_by_id(&mut conn, testrun_id) + .await + .map_err(|e| { + tracing::error!("{e}"); + HttpError::not_found(testrun_id) + })?; + + let gw_identity = db::queries::select_gateway_identity(&mut conn, testrun.gateway_id) + .await + .map_err(|_| { + // should never happen: + HttpError::internal_with_logging("No gateway found for testrun") + })?; + tracing::debug!( + "Agent submitted testrun {} for gateway {} ({} bytes)", + testrun_id, + gw_identity, + body.len(), + ); + + // TODO dz this should be part of a single transaction: commit after everything is done + queries::testruns::update_testrun_status(&mut conn, testrun_id, TestRunStatus::Complete) + .await + .map_err(HttpError::internal_with_logging)?; + queries::testruns::update_gateway_last_probe_log(&mut conn, testrun.gateway_id, &body) + .await + .map_err(HttpError::internal_with_logging)?; + let result = get_result_from_log(&body); + queries::testruns::update_gateway_last_probe_result(&mut conn, testrun.gateway_id, &result) + .await + .map_err(HttpError::internal_with_logging)?; + queries::testruns::update_gateway_score(&mut conn, testrun.gateway_id) + .await + .map_err(HttpError::internal_with_logging)?; + // TODO dz log gw identity key + + tracing::info!( + "✅ Testrun row_id {} for gateway {} complete", + testrun.id, + gw_identity + ); + + Ok(StatusCode::CREATED) +} + +fn get_result_from_log(log: &str) -> String { + let re = regex::Regex::new(r"\n\{\s").unwrap(); + let result: Vec<_> = re.splitn(log, 2).collect(); + if result.len() == 2 { + let res = format!("{} {}", "{", result[1]).to_string(); + return res; + } + "".to_string() +} diff --git a/nym-node-status-api/src/http/api_docs.rs b/nym-node-status-api/src/http/api_docs.rs new file mode 100644 index 0000000000..9ac5238fc4 --- /dev/null +++ b/nym-node-status-api/src/http/api_docs.rs @@ -0,0 +1,15 @@ +use crate::http::{Gateway, GatewaySkinny, Mixnode, Service}; +use utoipa::OpenApi; +use utoipauto::utoipauto; + +// manually import external structs which are behind feature flags because they +// can't be automatically discovered +// https://github.com/ProbablyClem/utoipauto/issues/13#issuecomment-1974911829 +#[utoipauto(paths = "./nym-node-status-api/src")] +#[derive(OpenApi)] +#[openapi( + info(title = "Node Status API"), + tags(), + components(schemas(nym_node_requests::api::v1::node::models::NodeDescription,)) +)] +pub(super) struct ApiDoc; diff --git a/nym-node-status-api/src/http/error.rs b/nym-node-status-api/src/http/error.rs new file mode 100644 index 0000000000..a7fe9d98ab --- /dev/null +++ b/nym-node-status-api/src/http/error.rs @@ -0,0 +1,48 @@ +use std::fmt::Display; + +pub(crate) type HttpResult = Result; + +pub(crate) struct HttpError { + message: String, + status: axum::http::StatusCode, +} + +impl HttpError { + pub(crate) fn invalid_input(msg: impl Display) -> Self { + Self { + message: serde_json::json!({"message": msg.to_string()}).to_string(), + status: axum::http::StatusCode::BAD_REQUEST, + } + } + + pub(crate) fn internal_with_logging(msg: impl Display) -> Self { + tracing::error!("{}", msg.to_string()); + Self::internal() + } + + pub(crate) fn internal() -> Self { + Self { + message: serde_json::json!({"message": "Internal server error"}).to_string(), + status: axum::http::StatusCode::INTERNAL_SERVER_ERROR, + } + } + + pub(crate) fn no_testruns_available() -> Self { + Self { + message: serde_json::json!({"message": "No testruns available"}).to_string(), + status: axum::http::StatusCode::SERVICE_UNAVAILABLE, + } + } + pub(crate) fn not_found(msg: impl Display) -> Self { + Self { + message: serde_json::json!({"message": msg.to_string()}).to_string(), + status: axum::http::StatusCode::NOT_FOUND, + } + } +} + +impl axum::response::IntoResponse for HttpError { + fn into_response(self) -> axum::response::Response { + (self.status, self.message).into_response() + } +} diff --git a/nym-node-status-api/src/http/mod.rs b/nym-node-status-api/src/http/mod.rs new file mode 100644 index 0000000000..1cc317337f --- /dev/null +++ b/nym-node-status-api/src/http/mod.rs @@ -0,0 +1,71 @@ +use models::{Gateway, GatewaySkinny, Mixnode, Service}; + +pub(crate) mod api; +pub(crate) mod api_docs; +pub(crate) mod error; +pub(crate) mod models; +pub(crate) mod server; +pub(crate) mod state; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::ToSchema)] +// exclude generic from auto-discovery +#[utoipauto::utoipa_ignore] +// https://docs.rs/utoipa/latest/utoipa/derive.ToSchema.html#generic-schemas-with-aliases +// Generic structs can only be included via aliases, not directly, because they +// it would cause an error in generated Swagger docs. +// Instead, you have to manually monomorphize each generic struct that +// you wish to document +#[aliases( + PagedGateway = PagedResult, + PagedGatewaySkinny = PagedResult, + PagedMixnode = PagedResult, + PagedService = PagedResult, +)] +pub struct PagedResult { + pub page: usize, + pub size: usize, + pub total: usize, + pub items: Vec, +} + +impl PagedResult { + pub fn paginate(pagination: Pagination, res: Vec) -> Self { + let total = res.len(); + let (size, mut page) = pagination.intoto_inner_values(); + + if page * size > total { + page = total / size; + } + + let chunks: Vec<&[T]> = res.chunks(size).collect(); + + PagedResult { + page, + size, + total, + items: chunks.get(page).cloned().unwrap_or_default().into(), + } + } +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, utoipa::IntoParams)] +#[into_params(parameter_in = Query)] +pub(crate) struct Pagination { + size: Option, + page: Option, +} + +impl Pagination { + // unwrap stored values or use predefined defaults + pub(crate) fn intoto_inner_values(self) -> (usize, usize) { + const SIZE_DEFAULT: usize = 10; + const SIZE_MAX: usize = 200; + + const PAGE_DEFAULT: usize = 0; + + ( + self.size.unwrap_or(SIZE_DEFAULT).min(SIZE_MAX), + self.page.unwrap_or(PAGE_DEFAULT), + ) + } +} diff --git a/nym-node-status-api/src/http/models.rs b/nym-node-status-api/src/http/models.rs new file mode 100644 index 0000000000..82011fc286 --- /dev/null +++ b/nym-node-status-api/src/http/models.rs @@ -0,0 +1,76 @@ +use nym_node_requests::api::v1::node::models::NodeDescription; +use serde::{Deserialize, Serialize}; +use utoipa::ToSchema; + +pub(crate) use nym_common_models::ns_api::TestrunAssignment; + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +pub struct Gateway { + pub gateway_identity_key: String, + pub bonded: bool, + pub blacklisted: bool, + pub performance: u8, + pub self_described: Option, + pub explorer_pretty_bond: Option, + pub description: NodeDescription, + pub last_probe_result: Option, + pub last_probe_log: Option, + pub last_testrun_utc: Option, + pub last_updated_utc: String, + pub routing_score: f32, + pub config_score: u32, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct GatewaySkinny { + pub gateway_identity_key: String, + pub self_described: Option, + pub explorer_pretty_bond: Option, + pub last_probe_result: Option, + pub last_testrun_utc: Option, + pub last_updated_utc: String, + pub routing_score: f32, + pub config_score: u32, + pub performance: u8, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct Mixnode { + pub mix_id: u32, + pub bonded: bool, + pub blacklisted: bool, + pub is_dp_delegatee: bool, + pub total_stake: i64, + pub full_details: Option, + pub self_described: Option, + pub description: NodeDescription, + pub last_updated_utc: String, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct DailyStats { + pub date_utc: String, + pub total_packets_received: i64, + pub total_packets_sent: i64, + pub total_packets_dropped: i64, + pub total_stake: i64, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub struct Service { + pub gateway_identity_key: String, + pub last_updated_utc: String, + pub routing_score: f32, + pub service_provider_client_id: Option, + pub ip_address: Option, + pub hostname: Option, + pub mixnet_websockets: Option, + pub last_successful_ping_utc: Option, +} + +#[derive(Debug, Clone, Deserialize, Serialize, ToSchema)] +pub(crate) struct SummaryHistory { + pub date: String, + pub value_json: serde_json::Value, + pub timestamp_utc: String, +} diff --git a/nym-node-status-api/src/http/server.rs b/nym-node-status-api/src/http/server.rs new file mode 100644 index 0000000000..17d5d64ab0 --- /dev/null +++ b/nym-node-status-api/src/http/server.rs @@ -0,0 +1,93 @@ +use axum::Router; +use core::net::SocketAddr; +use tokio::{net::TcpListener, task::JoinHandle}; +use tokio_util::sync::{CancellationToken, WaitForCancellationFutureOwned}; + +use crate::{ + db::DbPool, + http::{api::RouterBuilder, state::AppState}, +}; + +/// Return handles that allow for graceful shutdown of server + awaiting its +/// background tokio task +pub(crate) async fn start_http_api( + db_pool: DbPool, + http_port: u16, + nym_http_cache_ttl: u64, +) -> anyhow::Result { + let router_builder = RouterBuilder::with_default_routes(); + + let state = AppState::new(db_pool, nym_http_cache_ttl); + let router = router_builder.with_state(state); + + // TODO dz do we need this to be configurable? + let bind_addr = format!("0.0.0.0:{}", http_port); + tracing::info!("Binding server to {bind_addr}"); + let server = router.build_server(bind_addr).await?; + + Ok(start_server(server)) +} + +fn start_server(server: HttpServer) -> ShutdownHandles { + // one copy is stored to trigger a graceful shutdown later + let shutdown_button = CancellationToken::new(); + // other copy is given to server to listen for a shutdown + let shutdown_receiver = shutdown_button.clone(); + let shutdown_receiver = shutdown_receiver.cancelled_owned(); + + let server_handle = tokio::spawn(async move { server.run(shutdown_receiver).await }); + + ShutdownHandles { + server_handle, + shutdown_button, + } +} + +pub(crate) struct ShutdownHandles { + server_handle: JoinHandle>, + shutdown_button: CancellationToken, +} + +impl ShutdownHandles { + /// Send graceful shutdown signal to server and wait for server task to complete + pub(crate) async fn shutdown(self) -> anyhow::Result<()> { + self.shutdown_button.cancel(); + + match self.server_handle.await { + Ok(Ok(_)) => { + tracing::info!("HTTP server shut down without errors"); + } + Ok(Err(err)) => { + tracing::error!("HTTP server terminated with: {err}"); + anyhow::bail!(err) + } + Err(err) => { + tracing::error!("Server task panicked: {err}"); + } + }; + + Ok(()) + } +} + +pub(crate) struct HttpServer { + router: Router, + listener: TcpListener, +} + +impl HttpServer { + pub(crate) fn new(router: Router, listener: TcpListener) -> Self { + Self { router, listener } + } + + pub(crate) async fn run(self, receiver: WaitForCancellationFutureOwned) -> std::io::Result<()> { + // into_make_service_with_connect_info allows us to see client ip address + axum::serve( + self.listener, + self.router + .into_make_service_with_connect_info::(), + ) + .with_graceful_shutdown(receiver) + .await + } +} diff --git a/nym-node-status-api/src/http/state.rs b/nym-node-status-api/src/http/state.rs new file mode 100644 index 0000000000..6bccea39a1 --- /dev/null +++ b/nym-node-status-api/src/http/state.rs @@ -0,0 +1,223 @@ +use std::{sync::Arc, time::Duration}; + +use moka::{future::Cache, Entry}; +use tokio::sync::RwLock; + +use crate::{ + db::DbPool, + http::models::{DailyStats, Gateway, Mixnode, SummaryHistory}, +}; + +#[derive(Debug, Clone)] +pub(crate) struct AppState { + db_pool: DbPool, + cache: HttpCache, +} + +impl AppState { + pub(crate) fn new(db_pool: DbPool, cache_ttl: u64) -> Self { + Self { + db_pool, + cache: HttpCache::new(cache_ttl), + } + } + + pub(crate) fn db_pool(&self) -> &DbPool { + &self.db_pool + } + + pub(crate) fn cache(&self) -> &HttpCache { + &self.cache + } +} + +static GATEWAYS_LIST_KEY: &str = "gateways"; +static MIXNODES_LIST_KEY: &str = "mixnodes"; +static MIXSTATS_LIST_KEY: &str = "mixstats"; +static SUMMARY_HISTORY_LIST_KEY: &str = "summary-history"; + +#[derive(Debug, Clone)] +pub(crate) struct HttpCache { + gateways: Cache>>>, + mixnodes: Cache>>>, + mixstats: Cache>>>, + history: Cache>>>, +} + +impl HttpCache { + pub fn new(ttl_seconds: u64) -> Self { + HttpCache { + gateways: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + mixnodes: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + mixstats: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + history: Cache::builder() + .max_capacity(2) + .time_to_live(Duration::from_secs(ttl_seconds)) + .build(), + } + } + + pub async fn upsert_gateway_list( + &self, + new_gateway_list: Vec, + ) -> Entry>>> { + self.gateways + .entry_by_ref(GATEWAYS_LIST_KEY) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = new_gateway_list; + v.clone() + } else { + Arc::new(RwLock::new(new_gateway_list)) + } + }) + .await + } + + pub async fn get_gateway_list(&self, db: &DbPool) -> Vec { + match self.gateways.get(GATEWAYS_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.clone() + } + None => { + // the key is missing so populate it + tracing::warn!("No gateways in cache, refreshing cache from DB..."); + + let gateways = crate::db::queries::get_all_gateways(db) + .await + .unwrap_or_default(); + self.upsert_gateway_list(gateways.clone()).await; + + if gateways.is_empty() { + tracing::warn!("Database contains 0 gateways"); + } + + gateways + } + } + } + + pub async fn upsert_mixnode_list( + &self, + new_mixnode_list: Vec, + ) -> Entry>>> { + self.mixnodes + .entry_by_ref(MIXNODES_LIST_KEY) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = new_mixnode_list; + v.clone() + } else { + Arc::new(RwLock::new(new_mixnode_list)) + } + }) + .await + } + + pub async fn get_mixnodes_list(&self, db: &DbPool) -> Vec { + match self.mixnodes.get(MIXNODES_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.clone() + } + None => { + tracing::warn!("No mixnodes in cache, refreshing cache from DB..."); + + let mixnodes = crate::db::queries::get_all_mixnodes(db) + .await + .unwrap_or_default(); + self.upsert_mixnode_list(mixnodes.clone()).await; + + if mixnodes.is_empty() { + tracing::warn!("Database contains 0 mixnodes"); + } + + mixnodes + } + } + } + + pub async fn upsert_mixnode_stats( + &self, + mixnode_stats: Vec, + ) -> Entry>>> { + self.mixstats + .entry_by_ref(MIXSTATS_LIST_KEY) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = mixnode_stats; + v.clone() + } else { + Arc::new(RwLock::new(mixnode_stats)) + } + }) + .await + } + + pub async fn get_mixnode_stats(&self, db: &DbPool) -> Vec { + match self.mixstats.get(MIXSTATS_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.to_vec() + } + None => { + let mixnode_stats = crate::db::queries::get_daily_stats(db) + .await + .unwrap_or_default(); + self.upsert_mixnode_stats(mixnode_stats.clone()).await; + mixnode_stats + } + } + } + + pub async fn get_summary_history(&self, db: &DbPool) -> Vec { + match self.history.get(SUMMARY_HISTORY_LIST_KEY).await { + Some(guard) => { + let read_lock = guard.read().await; + read_lock.to_vec() + } + None => { + let summary_history = crate::db::queries::get_summary_history(db) + .await + .unwrap_or(vec![]); + self.upsert_summary_history(summary_history.clone()).await; + summary_history + } + } + } + + pub async fn upsert_summary_history( + &self, + summary_history: Vec, + ) -> Entry>>> { + self.history + .entry_by_ref(SUMMARY_HISTORY_LIST_KEY) + .and_upsert_with(|maybe_entry| async { + if let Some(entry) = maybe_entry { + let v = entry.into_value(); + let mut guard = v.write().await; + *guard = summary_history; + v.clone() + } else { + Arc::new(RwLock::new(summary_history)) + } + }) + .await + } +} diff --git a/nym-node-status-api/src/logging.rs b/nym-node-status-api/src/logging.rs new file mode 100644 index 0000000000..6bdad3bae5 --- /dev/null +++ b/nym-node-status-api/src/logging.rs @@ -0,0 +1,69 @@ +use tracing::level_filters::LevelFilter; +use tracing_subscriber::{filter::Directive, EnvFilter}; + +// TODO dz you can get the tracing-subscriber via basic-tracing feature on nym-bin-common +pub(crate) fn setup_tracing_logger() -> anyhow::Result<()> { + fn directive_checked(directive: impl Into) -> anyhow::Result { + directive.into().parse().map_err(From::from) + } + + let log_builder = tracing_subscriber::fmt() + // Use a more compact, abbreviated log format + .compact() + // Display source code file paths + .with_file(true) + // Display source code line numbers + .with_line_number(true) + .with_thread_ids(true) + // Don't display the event's target (module path) + .with_target(false); + + let mut filter = EnvFilter::builder() + // if RUST_LOG isn't set, set default level + .with_default_directive(LevelFilter::DEBUG.into()) + .from_env_lossy(); + + // these crates are more granularly filtered + let warn_crates = [ + "reqwest", + "rustls", + "hyper", + "sqlx", + "h2", + "tendermint_rpc", + "tower_http", + "axum", + ]; + for crate_name in warn_crates { + filter = filter.add_directive(directive_checked(format!("{}=warn", crate_name))?); + } + + let log_level_hint = filter.max_level_hint(); + + // debug or higher granularity (e.g. trace) + let debug_or_higher = std::cmp::max( + log_level_hint.unwrap_or(LevelFilter::DEBUG), + LevelFilter::DEBUG, + ); + filter = filter.add_directive(directive_checked(format!( + "nym_bin_common={}", + debug_or_higher + ))?); + filter = filter.add_directive(directive_checked(format!( + "nym_explorer_client={}", + debug_or_higher + ))?); + filter = filter.add_directive(directive_checked(format!( + "nym_network_defaults={}", + debug_or_higher + ))?); + filter = filter.add_directive(directive_checked(format!( + "nym_validator_client={}", + debug_or_higher + ))?); + + log_builder.with_env_filter(filter).init(); + tracing::info!("Log level: {:?}", log_level_hint); + + Ok(()) +} diff --git a/nym-node-status-api/src/main.rs b/nym-node-status-api/src/main.rs new file mode 100644 index 0000000000..b2c68b391b --- /dev/null +++ b/nym-node-status-api/src/main.rs @@ -0,0 +1,53 @@ +use clap::Parser; +use nym_task::signal::wait_for_signal; + +mod cli; +mod db; +mod http; +mod logging; +mod monitor; +mod testruns; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + logging::setup_tracing_logger()?; + + let args = cli::Cli::parse(); + + let connection_url = args.database_url.clone(); + tracing::debug!("Using config:\n{:#?}", args); + + let storage = db::Storage::init(connection_url).await?; + let db_pool = storage.pool_owned(); + let args_clone = args.clone(); + tokio::spawn(async move { + monitor::spawn_in_background( + db_pool, + args_clone.explorer_client_timeout, + args_clone.nym_api_client_timeout, + &args_clone.nyxd_addr, + args_clone.monitor_refresh_interval, + ) + .await; + tracing::info!("Started monitor task"); + }); + testruns::spawn(storage.pool_owned(), args.testruns_refresh_interval).await; + + let shutdown_handles = http::server::start_http_api( + storage.pool_owned(), + args.http_port, + args.nym_http_cache_ttl, + ) + .await + .expect("Failed to start server"); + + tracing::info!("Started HTTP server on port {}", args.http_port); + + wait_for_signal().await; + + if let Err(err) = shutdown_handles.shutdown().await { + tracing::error!("{err}"); + }; + + Ok(()) +} diff --git a/nym-node-status-api/src/monitor/mod.rs b/nym-node-status-api/src/monitor/mod.rs new file mode 100644 index 0000000000..d0d1c5f638 --- /dev/null +++ b/nym-node-status-api/src/monitor/mod.rs @@ -0,0 +1,513 @@ +#![allow(deprecated)] + +use crate::db::models::{ + gateway, mixnode, GatewayRecord, MixnodeRecord, NetworkSummary, GATEWAYS_BLACKLISTED_COUNT, + GATEWAYS_BONDED_COUNT, GATEWAYS_EXPLORER_COUNT, GATEWAYS_HISTORICAL_COUNT, + MIXNODES_BLACKLISTED_COUNT, MIXNODES_BONDED_ACTIVE, MIXNODES_BONDED_COUNT, + MIXNODES_BONDED_INACTIVE, MIXNODES_BONDED_RESERVE, MIXNODES_HISTORICAL_COUNT, +}; +use crate::db::{queries, DbPool}; +use anyhow::anyhow; +use cosmwasm_std::Decimal; +use nym_explorer_client::{ExplorerClient, PrettyDetailedGatewayBond}; +use nym_network_defaults::NymNetworkDetails; +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::models::{ + LegacyDescribedMixNode, MixNodeBondAnnotated, NymNodeDescription, +}; +use nym_validator_client::nym_nodes::SkimmedNode; +use nym_validator_client::nyxd::contract_traits::PagedMixnetQueryClient; +use nym_validator_client::nyxd::{AccountId, NyxdClient}; +use nym_validator_client::NymApiClient; +use reqwest::Url; +use std::collections::HashSet; +use std::str::FromStr; +use tokio::time::Duration; +use tracing::instrument; + +// TODO dz should be configurable +const FAILURE_RETRY_DELAY: Duration = Duration::from_secs(60); + +static DELEGATION_PROGRAM_WALLET: &str = "n1rnxpdpx3kldygsklfft0gech7fhfcux4zst5lw"; + +// TODO dz: query many NYM APIs: +// multiple instances running directory cache, ask sachin +#[instrument(level = "debug", name = "data_monitor", skip_all)] +pub(crate) async fn spawn_in_background( + db_pool: DbPool, + explorer_client_timeout: Duration, + nym_api_client_timeout: Duration, + nyxd_addr: &Url, + refresh_interval: Duration, +) { + let network_defaults = nym_network_defaults::NymNetworkDetails::new_from_env(); + + loop { + tracing::info!("Refreshing node info..."); + + if let Err(e) = run( + &db_pool, + &network_defaults, + explorer_client_timeout, + nym_api_client_timeout, + nyxd_addr, + ) + .await + { + tracing::error!( + "Monitor run failed: {e}, retrying in {}s...", + FAILURE_RETRY_DELAY.as_secs() + ); + // TODO dz implement some sort of backoff + tokio::time::sleep(FAILURE_RETRY_DELAY).await; + } else { + tracing::info!( + "Info successfully collected, sleeping for {}s...", + refresh_interval.as_secs() + ); + tokio::time::sleep(refresh_interval).await; + } + } +} + +async fn run( + pool: &DbPool, + network_details: &NymNetworkDetails, + explorer_client_timeout: Duration, + nym_api_client_timeout: Duration, + nyxd_addr: &Url, +) -> anyhow::Result<()> { + let default_api_url = network_details + .endpoints + .first() + .expect("rust sdk mainnet default incorrectly configured") + .api_url() + .clone() + .expect("rust sdk mainnet default missing api_url"); + let default_explorer_url = network_details.explorer_api.clone().map(|url| { + url.parse() + .expect("rust sdk mainnet default explorer url not parseable") + }); + + // TODO dz replace explorer api with ipinfo.io + let default_explorer_url = + default_explorer_url.expect("explorer url missing in network config"); + let explorer_client = + ExplorerClient::new_with_timeout(default_explorer_url, explorer_client_timeout)?; + let explorer_gateways = explorer_client + .unstable_get_gateways() + .await + .log_error("unstable_get_gateways")?; + + let api_client = NymApiClient::new_with_timeout(default_api_url, nym_api_client_timeout); + + let all_nodes = api_client + .get_all_described_nodes() + .await + .log_error("get_all_described_nodes")?; + tracing::debug!("Fetched {} total nodes", all_nodes.len()); + + let gateways = all_nodes + .iter() + .filter(|node| node.description.declared_role.entry) + .collect::>(); + tracing::debug!("Of those, {} gateways", gateways.len()); + for gw in gateways.iter() { + tracing::debug!("{}", gw.ed25519_identity_key().to_base58_string()); + } + + let mixnodes = all_nodes + .iter() + .filter(|node| node.description.declared_role.mixnode) + .collect::>(); + tracing::debug!("Of those, {} mixnodes", mixnodes.len()); + + log_gw_in_explorer_not_api(explorer_gateways.as_slice(), gateways.as_slice()); + + let all_skimmed_nodes = api_client + .get_all_basic_nodes(None) + .await + .log_error("get_all_basic_nodes")?; + + let mixnodes = api_client + .get_cached_mixnodes() + .await + .log_error("get_cached_mixnodes")?; + tracing::debug!("Fetched {} mixnodes", mixnodes.len()); + + // let gateways_blacklisted = gateways.iter().filter(|gw|gw.) + let gateways_blacklisted = all_skimmed_nodes + .iter() + .filter_map(|node| { + if node.performance.round_to_integer() <= 50 && node.supported_roles.entry { + Some(node.ed25519_identity_pubkey.to_base58_string()) + } else { + None + } + }) + .collect::>(); + + // Cached mixnodes don't include blacklisted nodes + // We need that to calculate the total locked tokens later + let mixnodes = api_client + .nym_api + .get_mixnodes_detailed_unfiltered() + .await + .log_error("get_mixnodes_detailed_unfiltered")?; + let mixnodes_described = api_client + .nym_api + .get_mixnodes_described() + .await + .log_error("get_mixnodes_described")?; + let mixnodes_active = api_client + .nym_api + .get_active_mixnodes() + .await + .log_error("get_active_mixnodes")?; + let delegation_program_members = + get_delegation_program_details(network_details, nyxd_addr).await?; + + // keep stats for later + let count_bonded_mixnodes = mixnodes.len(); + let count_bonded_gateways = gateways.len(); + let count_explorer_gateways = explorer_gateways.len(); + let count_bonded_mixnodes_active = mixnodes_active.len(); + + let gateway_records = prepare_gateway_data( + &gateways, + &gateways_blacklisted, + explorer_gateways, + all_skimmed_nodes, + )?; + queries::insert_gateways(pool, gateway_records) + .await + .map(|_| { + tracing::debug!("Gateway info written to DB!"); + })?; + + // instead of counting blacklisted GWs returned from API cache, count from the active set + let count_gateways_blacklisted = gateways + .iter() + .filter(|gw| { + let gw_identity = gw.ed25519_identity_key().to_base58_string(); + gateways_blacklisted.contains(&gw_identity) + }) + .count(); + + if count_gateways_blacklisted > 0 { + queries::write_blacklisted_gateways_to_db(pool, gateways_blacklisted.iter()) + .await + .map(|_| { + tracing::debug!( + "Gateway blacklist info written to DB! {} blacklisted by Nym API", + count_gateways_blacklisted + ) + })?; + } + + let mixnode_records = + prepare_mixnode_data(&mixnodes, mixnodes_described, delegation_program_members)?; + queries::insert_mixnodes(pool, mixnode_records) + .await + .map(|_| { + tracing::debug!("Mixnode info written to DB!"); + })?; + + let count_mixnodes_blacklisted = mixnodes.iter().filter(|elem| elem.blacklisted).count(); + + let recently_unbonded_gateways = queries::ensure_gateways_still_bonded(pool, &gateways).await?; + let recently_unbonded_mixnodes = queries::ensure_mixnodes_still_bonded(pool, &mixnodes).await?; + + let count_bonded_mixnodes_reserve = 0; // TODO: NymAPI doesn't report the reserve set size + let count_bonded_mixnodes_inactive = count_bonded_mixnodes - count_bonded_mixnodes_active; + + let (all_historical_gateways, all_historical_mixnodes) = calculate_stats(pool).await?; + + // + // write summary keys and values to table + // + + let nodes_summary = vec![ + (MIXNODES_BONDED_COUNT, &count_bonded_mixnodes), + (MIXNODES_BONDED_ACTIVE, &count_bonded_mixnodes_active), + (MIXNODES_BONDED_INACTIVE, &count_bonded_mixnodes_inactive), + (MIXNODES_BONDED_RESERVE, &count_bonded_mixnodes_reserve), + (MIXNODES_BLACKLISTED_COUNT, &count_mixnodes_blacklisted), + (GATEWAYS_BONDED_COUNT, &count_bonded_gateways), + (GATEWAYS_EXPLORER_COUNT, &count_explorer_gateways), + (MIXNODES_HISTORICAL_COUNT, &all_historical_mixnodes), + (GATEWAYS_HISTORICAL_COUNT, &all_historical_gateways), + (GATEWAYS_BLACKLISTED_COUNT, &count_gateways_blacklisted), + ]; + + let last_updated = chrono::offset::Utc::now(); + let last_updated_utc = last_updated.timestamp().to_string(); + let network_summary = NetworkSummary { + mixnodes: mixnode::MixnodeSummary { + bonded: mixnode::MixnodeSummaryBonded { + count: count_bonded_mixnodes.cast_checked()?, + active: count_bonded_mixnodes_active.cast_checked()?, + inactive: count_bonded_mixnodes_inactive.cast_checked()?, + reserve: count_bonded_mixnodes_reserve.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + blacklisted: mixnode::MixnodeSummaryBlacklisted { + count: count_mixnodes_blacklisted.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + historical: mixnode::MixnodeSummaryHistorical { + count: all_historical_mixnodes.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + }, + gateways: gateway::GatewaySummary { + bonded: gateway::GatewaySummaryBonded { + count: count_bonded_gateways.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + blacklisted: gateway::GatewaySummaryBlacklisted { + count: count_gateways_blacklisted.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + historical: gateway::GatewaySummaryHistorical { + count: all_historical_gateways.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + explorer: gateway::GatewaySummaryExplorer { + count: count_explorer_gateways.cast_checked()?, + last_updated_utc: last_updated_utc.to_owned(), + }, + }, + }; + + queries::insert_summaries(pool, &nodes_summary, &network_summary, last_updated).await?; + + let mut log_lines: Vec = vec![]; + for (key, value) in nodes_summary.iter() { + log_lines.push(format!("{} = {}", key, value)); + } + log_lines.push(format!( + "recently_unbonded_mixnodes = {}", + recently_unbonded_mixnodes + )); + log_lines.push(format!( + "recently_unbonded_gateways = {}", + recently_unbonded_gateways + )); + + tracing::info!("Directory summary: \n{}", log_lines.join("\n")); + + Ok(()) +} + +fn prepare_gateway_data( + gateways: &[&NymNodeDescription], + gateways_blacklisted: &HashSet, + explorer_gateways: Vec, + skimmed_gateways: Vec, +) -> anyhow::Result> { + let mut gateway_records = Vec::new(); + + for gateway in gateways { + let identity_key = gateway.ed25519_identity_key().to_base58_string(); + let bonded = true; + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + let blacklisted = gateways_blacklisted.contains(&identity_key); + + let self_described = serde_json::to_string(&gateway.description)?; + + let explorer_pretty_bond = explorer_gateways + .iter() + .find(|g| g.gateway.identity_key.eq(&identity_key)); + let explorer_pretty_bond = explorer_pretty_bond.and_then(|g| serde_json::to_string(g).ok()); + + let performance = skimmed_gateways + .iter() + .find(|g| { + g.ed25519_identity_pubkey + .to_base58_string() + .eq(&identity_key) + }) + .map(|g| g.performance) + .unwrap_or_default() + .round_to_integer(); + + gateway_records.push(GatewayRecord { + identity_key: identity_key.to_owned(), + bonded, + blacklisted, + self_described, + explorer_pretty_bond, + last_updated_utc, + performance, + }); + } + + Ok(gateway_records) +} + +fn prepare_mixnode_data( + mixnodes: &[MixNodeBondAnnotated], + mixnodes_described: Vec, + delegation_program_members: Vec, +) -> anyhow::Result> { + let mut mixnode_records = Vec::new(); + + for mixnode in mixnodes { + let mix_id = mixnode.mix_id(); + let identity_key = mixnode.identity_key(); + let bonded = true; + let total_stake = decimal_to_i64(mixnode.mixnode_details.total_stake()); + let blacklisted = mixnode.blacklisted; + let node_info = mixnode.mix_node(); + let host = node_info.host.clone(); + let http_port = node_info.http_api_port; + // Contains all the information including what's above + let full_details = serde_json::to_string(&mixnode)?; + + let mixnode_described = mixnodes_described.iter().find(|m| m.bond.mix_id == mix_id); + let self_described = mixnode_described.and_then(|v| serde_json::to_string(v).ok()); + let is_dp_delegatee = delegation_program_members.contains(&mix_id); + + let last_updated_utc = chrono::offset::Utc::now().timestamp(); + + mixnode_records.push(MixnodeRecord { + mix_id, + identity_key: identity_key.to_owned(), + bonded, + total_stake, + host, + http_port, + blacklisted, + full_details, + self_described, + last_updated_utc, + is_dp_delegatee, + }); + } + + Ok(mixnode_records) +} + +fn log_gw_in_explorer_not_api( + explorer: &[PrettyDetailedGatewayBond], + api_gateways: &[&NymNodeDescription], +) { + let api_gateways = api_gateways + .iter() + .map(|gw| gw.ed25519_identity_key().to_base58_string()) + .collect::>(); + let explorer_only = explorer + .iter() + .filter(|gw| !api_gateways.contains(&gw.gateway.identity_key.to_string())) + .collect::>(); + + tracing::debug!( + "Gateways listed by explorer but not by Nym API: {}", + explorer_only.len() + ); + for gw in explorer_only.iter() { + tracing::debug!("{}", gw.gateway.identity_key.to_string()); + } +} + +// TODO dz is there a common monorepo place this can be put? +pub trait NumericalCheckedCast +where + T: TryFrom, + >::Error: std::error::Error, + Self: std::fmt::Display + Copy, +{ + fn cast_checked(self) -> anyhow::Result { + T::try_from(self).map_err(|e| { + anyhow::anyhow!( + "Couldn't cast {} to {}: {}", + self, + std::any::type_name::(), + e + ) + }) + } +} + +impl NumericalCheckedCast for T +where + U: TryFrom, + >::Error: std::error::Error, + T: std::fmt::Display + Copy, +{ +} + +async fn calculate_stats(pool: &DbPool) -> anyhow::Result<(usize, usize)> { + let mut conn = pool.acquire().await?; + + let all_historical_gateways = sqlx::query_scalar!(r#"SELECT count(id) FROM gateways"#) + .fetch_one(&mut *conn) + .await? + .cast_checked()?; + + let all_historical_mixnodes = sqlx::query_scalar!(r#"SELECT count(id) FROM mixnodes"#) + .fetch_one(&mut *conn) + .await? + .cast_checked()?; + + Ok((all_historical_gateways, all_historical_mixnodes)) +} + +async fn get_delegation_program_details( + network_details: &NymNetworkDetails, + nyxd_addr: &Url, +) -> anyhow::Result> { + let config = nym_validator_client::nyxd::Config::try_from_nym_network_details(network_details)?; + + let client = NyxdClient::connect(config, nyxd_addr.as_str()) + .map_err(|err| anyhow::anyhow!("Couldn't connect: {}", err))?; + + let account_id = AccountId::from_str(DELEGATION_PROGRAM_WALLET) + .map_err(|e| anyhow!("Invalid bech32 address: {}", e))?; + + let delegations = client.get_all_delegator_delegations(&account_id).await?; + + let mix_ids: Vec = delegations + .iter() + .map(|delegation| delegation.node_id) + .collect(); + + Ok(mix_ids) +} + +fn decimal_to_i64(decimal: Decimal) -> i64 { + // Convert the underlying Uint128 to a u128 + let atomics = decimal.atomics().u128(); + let precision = 1_000_000_000_000_000_000u128; + + // Get the fractional part + let fractional = atomics % precision; + + // Get the integer part + let integer = atomics / precision; + + // Combine them into a float + let float_value = integer as f64 + (fractional as f64 / 1_000_000_000_000_000_000_f64); + + // Limit to 6 decimal places + let rounded_value = (float_value * 1_000_000.0).round() / 1_000_000.0; + + rounded_value as i64 +} + +trait LogError { + fn log_error(self, msg: &str) -> Result; +} + +impl LogError for anyhow::Result +where + E: std::error::Error, +{ + fn log_error(self, msg: &str) -> Result { + if let Err(e) = &self { + tracing::error!("[{msg}]:\t{e}"); + } + self + } +} diff --git a/nym-node-status-api/src/testruns/mod.rs b/nym-node-status-api/src/testruns/mod.rs new file mode 100644 index 0000000000..f487523f36 --- /dev/null +++ b/nym-node-status-api/src/testruns/mod.rs @@ -0,0 +1,86 @@ +use crate::db::models::GatewayIdentityDto; +use crate::db::DbPool; +use futures_util::TryStreamExt; +use std::time::Duration; +use tracing::instrument; + +pub(crate) mod models; +mod queue; +pub(crate) use queue::now_utc; + +pub(crate) async fn spawn(pool: DbPool, refresh_interval: Duration) { + tokio::spawn(async move { + loop { + if let Err(e) = refresh_stale_testruns(&pool, refresh_interval).await { + tracing::error!("{e}"); + } + + if let Err(e) = run(&pool).await { + tracing::error!("Assigning testruns failed: {}", e); + } + tracing::debug!("Sleeping for {}s...", refresh_interval.as_secs()); + tokio::time::sleep(refresh_interval).await; + } + }); +} + +// TODO dz make number of max agents configurable + +#[instrument(level = "debug", name = "testrun_queue", skip_all)] +async fn run(pool: &DbPool) -> anyhow::Result<()> { + tracing::info!("Spawning testruns..."); + if pool.is_closed() { + tracing::debug!("DB pool closed, returning early"); + return Ok(()); + } + + let mut conn = pool.acquire().await?; + + let gateways = sqlx::query_as!( + GatewayIdentityDto, + r#"SELECT + gateway_identity_key as "gateway_identity_key!", + bonded as "bonded: bool" + FROM gateways + ORDER BY last_testrun_utc"#, + ) + .fetch(&mut *conn) + .try_collect::>() + .await?; + + // TODO dz this filtering could be done in SQL + let gateways: Vec = gateways.into_iter().filter(|g| g.bonded).collect(); + + tracing::debug!("Trying to queue {} testruns", gateways.len()); + let mut testruns_created = 0; + for gateway in gateways { + if let Err(e) = queue::try_queue_testrun( + &mut conn, + gateway.gateway_identity_key.clone(), + // TODO dz read from config + "127.0.0.1".to_string(), + ) + .await + // TODO dz measure how many were actually inserted and how many were skipped + { + tracing::debug!( + "Skipping test for identity {} with error {}", + &gateway.gateway_identity_key, + e + ); + } else { + testruns_created += 1; + } + } + tracing::debug!("{} testruns queued in total", testruns_created); + + Ok(()) +} + +#[instrument(level = "debug", skip_all)] +async fn refresh_stale_testruns(pool: &DbPool, refresh_interval: Duration) -> anyhow::Result<()> { + let chrono_duration = chrono::Duration::from_std(refresh_interval)?; + crate::db::queries::testruns::update_testruns_older_than(pool, chrono_duration).await?; + + Ok(()) +} diff --git a/nym-node-status-api/src/testruns/models.rs b/nym-node-status-api/src/testruns/models.rs new file mode 100644 index 0000000000..fe4b33384c --- /dev/null +++ b/nym-node-status-api/src/testruns/models.rs @@ -0,0 +1,16 @@ +use serde::{Deserialize, Serialize}; + +#[allow(dead_code)] // it's not dead code but clippy doesn't detect usage in sqlx macros +#[derive(Debug, Clone)] +pub struct GatewayIdentityDto { + pub gateway_identity_key: String, + pub bonded: bool, +} + +#[derive(Debug, Clone, Deserialize, Serialize, utoipa::ToSchema)] +pub struct TestRun { + pub id: u32, + pub identity_key: String, + pub status: String, + pub log: String, +} diff --git a/nym-node-status-api/src/testruns/queue.rs b/nym-node-status-api/src/testruns/queue.rs new file mode 100644 index 0000000000..88804fff1b --- /dev/null +++ b/nym-node-status-api/src/testruns/queue.rs @@ -0,0 +1,118 @@ +use crate::db::models::{GatewayInfoDto, TestRunDto, TestRunStatus}; +use crate::testruns::models::TestRun; +use anyhow::anyhow; +use chrono::DateTime; +use futures_util::TryStreamExt; +use sqlx::pool::PoolConnection; +use sqlx::Sqlite; +use std::time::SystemTime; + +pub(crate) async fn try_queue_testrun( + conn: &mut PoolConnection, + identity_key: String, + ip_address: String, +) -> anyhow::Result { + let timestamp = now_utc().timestamp(); + let timestamp_pretty = now_utc_as_rfc3339(); + + let items = sqlx::query_as!( + GatewayInfoDto, + r#"SELECT + id as "id!", + gateway_identity_key as "gateway_identity_key!", + self_described as "self_described?", + explorer_pretty_bond as "explorer_pretty_bond?" + FROM gateways + WHERE gateway_identity_key = ? + ORDER BY gateway_identity_key + LIMIT 1"#, + identity_key, + ) + // TODO dz shoudl call .fetch_one + // TODO dz replace this in other queries as well + .fetch(conn.as_mut()) + .try_collect::>() + .await?; + + let gateway = items + .iter() + .find(|g| g.gateway_identity_key == identity_key); + + // TODO dz if let Some() = gateway.first() ... + if gateway.is_none() { + return Err(anyhow!("Unknown gateway {identity_key}")); + } + let gateway_id = gateway.unwrap().id; + + // + // check if there is already a test run for this gateway + // + let items = sqlx::query_as!( + TestRunDto, + r#"SELECT + id as "id!", + gateway_id as "gateway_id!", + status as "status!", + timestamp_utc as "timestamp_utc!", + ip_address as "ip_address!", + log as "log!" + FROM testruns + WHERE gateway_id = ? AND status != 2 + ORDER BY id DESC + LIMIT 1"#, + gateway_id, + ) + .fetch(conn.as_mut()) + .try_collect::>() + .await?; + + if !items.is_empty() { + let testrun = items.first().unwrap(); + return Ok(TestRun { + id: testrun.id as u32, + identity_key, + status: format!( + "{}", + TestRunStatus::from_repr(testrun.status as u8).unwrap() + ), + log: testrun.log.clone(), + }); + } + + // + // save test run + // + let status = TestRunStatus::Queued as u32; + let log = format!( + "Test for {identity_key} requested at {} UTC\n\n", + timestamp_pretty + ); + + let id = sqlx::query!( + "INSERT INTO testruns (gateway_id, status, ip_address, timestamp_utc, log) VALUES (?, ?, ?, ?, ?)", + gateway_id, + status, + ip_address, + timestamp, + log, + ) + .execute(conn.as_mut()) + .await? + .last_insert_rowid(); + + Ok(TestRun { + id: id as u32, + identity_key, + status: format!("{}", TestRunStatus::Queued), + log, + }) +} + +// TODO dz do we need these? +pub fn now_utc() -> DateTime { + SystemTime::now().into() +} + +pub fn now_utc_as_rfc3339() -> String { + now_utc().to_rfc3339() +} diff --git a/nym-node/Cargo.toml b/nym-node/Cargo.toml index 442b62a3b9..07c8b33476 100644 --- a/nym-node/Cargo.toml +++ b/nym-node/Cargo.toml @@ -3,7 +3,7 @@ [package] name = "nym-node" -version = "1.1.9" +version = "1.1.11" authors.workspace = true repository.workspace = true homepage.workspace = true @@ -52,6 +52,7 @@ nym-sphinx-acknowledgements = { path = "../common/nymsphinx/acknowledgements" } nym-sphinx-addressing = { path = "../common/nymsphinx/addressing" } nym-task = { path = "../common/task" } nym-types = { path = "../common/types" } +nym-validator-client = { path = "../common/client-libs/validator-client" } nym-wireguard = { path = "../common/wireguard" } nym-wireguard-types = { path = "../common/wireguard-types", default-features = false } diff --git a/nym-node/Dockerfile b/nym-node/Dockerfile new file mode 100644 index 0000000000..d7a47a28e9 --- /dev/null +++ b/nym-node/Dockerfile @@ -0,0 +1,13 @@ +FROM rust:latest AS builder + +COPY ./ /usr/src/nym +WORKDIR /usr/src/nym/nym-node + +RUN cargo build --release + +FROM ubuntu:24.04 + +WORKDIR /nym + +COPY --from=builder /usr/src/nym/target/release/nym-node ./ +ENTRYPOINT [ "/nym/nym-node" ] diff --git a/nym-node/nym-node-http-api/Cargo.toml b/nym-node/nym-node-http-api/Cargo.toml index bf96afc08c..27b3fb53d5 100644 --- a/nym-node/nym-node-http-api/Cargo.toml +++ b/nym-node/nym-node-http-api/Cargo.toml @@ -29,10 +29,8 @@ rand = { workspace = true } fastrand = { workspace = true } nym-crypto = { path = "../../common/crypto", features = ["asymmetric", "rand"] } -nym-http-api-common = { path = "../../common/http-api-common" } -nym-node-requests = { path = "../nym-node-requests", default-features = false, features = [ - "openapi", -] } +nym-http-api-common = { path = "../../common/http-api-common", features = ["utoipa"] } +nym-node-requests = { path = "../nym-node-requests", default-features = false, features = ["openapi"] } nym-task = { path = "../../common/task" } nym-metrics = { path = "../../common/nym-metrics" } diff --git a/nym-node/nym-node-http-api/src/router/api/v1/metrics/mod.rs b/nym-node/nym-node-http-api/src/router/api/v1/metrics/mod.rs index 7f3dc9151e..cabf4aedf5 100644 --- a/nym-node/nym-node-http-api/src/router/api/v1/metrics/mod.rs +++ b/nym-node/nym-node-http-api/src/router/api/v1/metrics/mod.rs @@ -3,6 +3,7 @@ use crate::api::v1::metrics::mixing::mixing_stats; use crate::api::v1::metrics::prometheus::prometheus_metrics; +use crate::api::v1::metrics::sessions::sessions_stats; use crate::api::v1::metrics::verloc::verloc_stats; use crate::state::metrics::MetricsAppState; use axum::extract::FromRef; @@ -12,6 +13,7 @@ use nym_node_requests::routes::api::v1::metrics; pub mod mixing; pub mod prometheus; +pub mod sessions; pub mod verloc; #[derive(Debug, Clone, Default)] @@ -26,6 +28,7 @@ where { Router::new() .route(metrics::MIXING, get(mixing_stats)) + .route(metrics::SESSIONS, get(sessions_stats)) .route(metrics::VERLOC, get(verloc_stats)) .route(metrics::PROMETHEUS, get(prometheus_metrics)) } diff --git a/nym-node/nym-node-http-api/src/router/api/v1/metrics/sessions.rs b/nym-node/nym-node-http-api/src/router/api/v1/metrics/sessions.rs new file mode 100644 index 0000000000..59eceb8f89 --- /dev/null +++ b/nym-node/nym-node-http-api/src/router/api/v1/metrics/sessions.rs @@ -0,0 +1,33 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::state::metrics::MetricsAppState; +use axum::extract::{Query, State}; +use nym_http_api_common::{FormattedResponse, OutputParams}; +use nym_node_requests::api::v1::metrics::models::SessionStats; + +/// If applicable, returns sessions statistics information of this node. +/// This information is **PURELY** self-reported and in no way validated. +#[utoipa::path( + get, + path = "/sessions", + context_path = "/api/v1/metrics", + tag = "Metrics", + responses( + (status = 200, content( + ("application/json" = SessionStats), + ("application/yaml" = SessionStats) + )) + ), + params(OutputParams), +)] +pub(crate) async fn sessions_stats( + Query(output): Query, + State(metrics_state): State, +) -> SessionStatsResponse { + let output = output.output.unwrap_or_default(); + let response = metrics_state.session_stats.read().await.as_response(); + output.to_response(response) +} + +pub type SessionStatsResponse = FormattedResponse; diff --git a/nym-node/nym-node-http-api/src/state/metrics.rs b/nym-node/nym-node-http-api/src/state/metrics.rs index d4691727d9..67e4d311ef 100644 --- a/nym-node/nym-node-http-api/src/state/metrics.rs +++ b/nym-node/nym-node-http-api/src/state/metrics.rs @@ -4,11 +4,12 @@ use crate::state::AppState; use axum::extract::FromRef; use nym_node_requests::api::v1::metrics::models::{ - MixingStats, VerlocResult, VerlocResultData, VerlocStats, + MixingStats, Session, SessionStats, VerlocResult, VerlocResultData, VerlocStats, }; use std::collections::HashMap; use std::sync::Arc; -use time::OffsetDateTime; +use time::macros::time; +use time::{Date, OffsetDateTime}; use tokio::sync::{RwLock, RwLockReadGuard, RwLockWriteGuard}; pub use nym_node_requests::api::v1::metrics::models::{VerlocMeasurement, VerlocNodeResult}; @@ -132,12 +133,74 @@ impl VerlocStatsState { } } +#[derive(Clone, Debug, Default)] +pub struct SharedSessionStats { + inner: Arc>, +} + +impl SharedSessionStats { + pub fn new() -> SharedSessionStats { + SharedSessionStats { + inner: Arc::new(RwLock::new(Default::default())), + } + } + + pub async fn read(&self) -> RwLockReadGuard<'_, SessionStatsState> { + self.inner.read().await + } + + pub async fn write(&self) -> RwLockWriteGuard<'_, SessionStatsState> { + self.inner.write().await + } +} + +type FinishedSessions = Vec<(u64, String)>; + +#[derive(Debug, Clone)] +pub struct SessionStatsState { + pub update_time: Date, + pub unique_active_users: u32, + pub session_started: u32, + pub sessions: FinishedSessions, +} + +impl SessionStatsState { + pub fn as_response(&self) -> SessionStats { + let sessions = self + .sessions + .clone() + .into_iter() + .map(|(duration_ms, typ)| Session { duration_ms, typ }) + .collect(); + SessionStats { + update_time: self.update_time.with_time(time!(0:00)).assume_utc(), + unique_active_users: self.unique_active_users, + sessions, + sessions_started: self.session_started, + sessions_finished: self.sessions.len() as u32, + } + } +} + +impl Default for SessionStatsState { + fn default() -> Self { + SessionStatsState { + update_time: OffsetDateTime::UNIX_EPOCH.date(), + unique_active_users: 0, + session_started: 0, + sessions: Default::default(), + } + } +} + #[derive(Debug, Clone, Default)] pub struct MetricsAppState { pub(crate) prometheus_access_token: Option, pub(crate) mixing_stats: SharedMixingStats, + pub(crate) session_stats: SharedSessionStats, + pub(crate) verloc: SharedVerlocStats, } diff --git a/nym-node/nym-node-http-api/src/state/mod.rs b/nym-node/nym-node-http-api/src/state/mod.rs index 077ca782b6..67f16d1bd3 100644 --- a/nym-node/nym-node-http-api/src/state/mod.rs +++ b/nym-node/nym-node-http-api/src/state/mod.rs @@ -1,7 +1,9 @@ // Copyright 2023-2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::state::metrics::{MetricsAppState, SharedMixingStats, SharedVerlocStats}; +use crate::state::metrics::{ + MetricsAppState, SharedMixingStats, SharedSessionStats, SharedVerlocStats, +}; use tokio::time::Instant; pub mod metrics; @@ -32,6 +34,12 @@ impl AppState { self } + #[must_use] + pub fn with_sessions_stats(mut self, session_stats: SharedSessionStats) -> Self { + self.metrics.session_stats = session_stats; + self + } + #[must_use] pub fn with_verloc_stats(mut self, verloc_stats: SharedVerlocStats) -> Self { self.metrics.verloc = verloc_stats; diff --git a/nym-node/nym-node-requests/src/api/client.rs b/nym-node/nym-node-requests/src/api/client.rs index 87257e2835..11ec66a4b9 100644 --- a/nym-node/nym-node-requests/src/api/client.rs +++ b/nym-node/nym-node-requests/src/api/client.rs @@ -2,7 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::api::v1::gateway::models::WebSockets; -use crate::api::v1::node::models::{AuxiliaryDetails, SignedHostInformation}; +use crate::api::v1::node::models::{ + AuxiliaryDetails, NodeDescription, NodeRoles, SignedHostInformation, +}; use crate::api::ErrorResponse; use crate::routes; use async_trait::async_trait; @@ -32,6 +34,11 @@ pub trait NymNodeApiClientExt: ApiClient { .await } + async fn get_description(&self) -> Result { + self.get_json_from(routes::api::v1::description_absolute()) + .await + } + async fn get_build_information( &self, ) -> Result { @@ -39,6 +46,10 @@ pub trait NymNodeApiClientExt: ApiClient { .await } + async fn get_roles(&self) -> Result { + self.get_json_from(routes::api::v1::roles_absolute()).await + } + async fn get_auxiliary_details(&self) -> Result { self.get_json_from(routes::api::v1::auxiliary_absolute()) .await diff --git a/nym-node/nym-node-requests/src/api/mod.rs b/nym-node/nym-node-requests/src/api/mod.rs index 7a6c61b436..6bf586f71b 100644 --- a/nym-node/nym-node-requests/src/api/mod.rs +++ b/nym-node/nym-node-requests/src/api/mod.rs @@ -1,7 +1,9 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::api::v1::node::models::{HostInformation, LegacyHostInformation}; +use crate::api::v1::node::models::{ + HostInformation, LegacyHostInformation, LegacyHostInformationV2, +}; use crate::error::Error; use nym_crypto::asymmetric::identity; use schemars::JsonSchema; @@ -46,6 +48,7 @@ impl SignedData { let Ok(plaintext) = serde_json::to_string(&self.data) else { return false; }; + let Ok(signature) = identity::Signature::from_base58_string(&self.signature) else { return false; }; @@ -56,21 +59,25 @@ impl SignedData { impl SignedHostInformation { pub fn verify_host_information(&self) -> bool { - let Ok(pub_key) = identity::PublicKey::from_base58_string(&self.keys.ed25519_identity) - else { - return false; + if self.verify(&self.keys.ed25519_identity) { + return true; + } + + // attempt to verify legacy signatures + let legacy_v2 = SignedData { + data: LegacyHostInformationV2::from(self.data.clone()), + signature: self.signature.clone(), }; - if self.verify(&pub_key) { + if legacy_v2.verify(&self.keys.ed25519_identity) { return true; } - // attempt to verify legacy signature SignedData { - data: LegacyHostInformation::from(self.data.clone()), + data: LegacyHostInformation::from(legacy_v2.data), signature: self.signature.clone(), } - .verify(&pub_key) + .verify(&self.keys.ed25519_identity) } } @@ -105,20 +112,111 @@ mod tests { let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); let ed22519 = ed25519::KeyPair::new(&mut rng); let x25519_sphinx = x25519::KeyPair::new(&mut rng); + let x25519_noise = x25519::KeyPair::new(&mut rng); let host_info = crate::api::v1::node::models::HostInformation { ip_address: vec!["1.1.1.1".parse().unwrap()], hostname: Some("foomp.com".to_string()), keys: crate::api::v1::node::models::HostKeys { + ed25519_identity: *ed22519.public_key(), + x25519_sphinx: *x25519_sphinx.public_key(), + x25519_noise: None, + }, + }; + + let signed_info = SignedHostInformation::new(host_info, ed22519.private_key()).unwrap(); + assert!(signed_info.verify(ed22519.public_key())); + assert!(signed_info.verify_host_information()); + + let host_info_with_noise = crate::api::v1::node::models::HostInformation { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::HostKeys { + ed25519_identity: *ed22519.public_key(), + x25519_sphinx: *x25519_sphinx.public_key(), + x25519_noise: Some(*x25519_noise.public_key()), + }, + }; + + let signed_info = + SignedHostInformation::new(host_info_with_noise, ed22519.private_key()).unwrap(); + assert!(signed_info.verify(ed22519.public_key())); + assert!(signed_info.verify_host_information()); + } + + #[test] + fn dummy_legacy_v2_signed_host_verification() { + let mut rng = rand_chacha::ChaCha20Rng::from_seed([0u8; 32]); + let ed22519 = ed25519::KeyPair::new(&mut rng); + let x25519_sphinx = x25519::KeyPair::new(&mut rng); + let x25519_noise = x25519::KeyPair::new(&mut rng); + + let legacy_info_no_noise = crate::api::v1::node::models::LegacyHostInformationV2 { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::LegacyHostKeysV2 { ed25519_identity: ed22519.public_key().to_base58_string(), x25519_sphinx: x25519_sphinx.public_key().to_base58_string(), x25519_noise: "".to_string(), }, }; - let signed_info = SignedHostInformation::new(host_info, ed22519.private_key()).unwrap(); - assert!(signed_info.verify(ed22519.public_key())); - assert!(signed_info.verify_host_information()) + let legacy_info_noise = crate::api::v1::node::models::LegacyHostInformationV2 { + ip_address: vec!["1.1.1.1".parse().unwrap()], + hostname: Some("foomp.com".to_string()), + keys: crate::api::v1::node::models::LegacyHostKeysV2 { + ed25519_identity: ed22519.public_key().to_base58_string(), + x25519_sphinx: x25519_sphinx.public_key().to_base58_string(), + x25519_noise: x25519_noise.public_key().to_base58_string(), + }, + }; + + let host_info_no_noise = crate::api::v1::node::models::HostInformation { + ip_address: legacy_info_no_noise.ip_address.clone(), + hostname: legacy_info_no_noise.hostname.clone(), + keys: crate::api::v1::node::models::HostKeys { + ed25519_identity: legacy_info_no_noise.keys.ed25519_identity.parse().unwrap(), + x25519_sphinx: legacy_info_no_noise.keys.x25519_sphinx.parse().unwrap(), + x25519_noise: None, + }, + }; + + let host_info_noise = crate::api::v1::node::models::HostInformation { + ip_address: legacy_info_noise.ip_address.clone(), + hostname: legacy_info_noise.hostname.clone(), + keys: crate::api::v1::node::models::HostKeys { + ed25519_identity: legacy_info_noise.keys.ed25519_identity.parse().unwrap(), + x25519_sphinx: legacy_info_noise.keys.x25519_sphinx.parse().unwrap(), + x25519_noise: Some(legacy_info_noise.keys.x25519_noise.parse().unwrap()), + }, + }; + + // signature on legacy data + let signature_no_noise = SignedData::new(legacy_info_no_noise, ed22519.private_key()) + .unwrap() + .signature; + + let signature_noise = SignedData::new(legacy_info_noise, ed22519.private_key()) + .unwrap() + .signature; + + // signed blob with the 'current' structure + let current_struct_no_noise = SignedData { + data: host_info_no_noise, + signature: signature_no_noise, + }; + + let current_struct_noise = SignedData { + data: host_info_noise, + signature: signature_noise, + }; + + assert!(!current_struct_no_noise.verify(ed22519.public_key())); + assert!(current_struct_no_noise.verify_host_information()); + + // if noise key is present, the signature is actually valid + assert!(current_struct_noise.verify(ed22519.public_key())); + assert!(current_struct_noise.verify_host_information()) } #[test] @@ -140,9 +238,9 @@ mod tests { ip_address: legacy_info.ip_address.clone(), hostname: legacy_info.hostname.clone(), keys: crate::api::v1::node::models::HostKeys { - ed25519_identity: legacy_info.keys.ed25519.clone(), - x25519_sphinx: legacy_info.keys.x25519.clone(), - x25519_noise: "".to_string(), + ed25519_identity: legacy_info.keys.ed25519.parse().unwrap(), + x25519_sphinx: legacy_info.keys.x25519.parse().unwrap(), + x25519_noise: None, }, }; diff --git a/nym-node/nym-node-requests/src/api/v1/metrics/models.rs b/nym-node/nym-node-requests/src/api/v1/metrics/models.rs index 3374b1b0c4..d353596e11 100644 --- a/nym-node/nym-node-requests/src/api/v1/metrics/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/metrics/models.rs @@ -1,7 +1,7 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use nym_crypto::asymmetric::identity::{self, serde_helpers::bs58_pubkey}; +use nym_crypto::asymmetric::identity::{self, serde_helpers::bs58_ed25519_pubkey}; use serde::{Deserialize, Serialize}; use std::cmp::Ordering; use std::fmt; @@ -35,6 +35,28 @@ pub struct MixingStats { pub dropped_since_last_update: u64, } +#[derive(Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct Session { + pub duration_ms: u64, + pub typ: String, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct SessionStats { + #[serde(with = "time::serde::rfc3339")] + pub update_time: OffsetDateTime, + + pub unique_active_users: u32, + + pub sessions: Vec, + + pub sessions_started: u32, + + pub sessions_finished: u32, +} + #[derive(Serialize, Deserialize, Default, Debug, Clone)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct VerlocStats { @@ -86,7 +108,7 @@ impl VerlocResultData { #[derive(Serialize, Deserialize, Debug, Clone, Copy, Eq, PartialEq)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct VerlocNodeResult { - #[serde(with = "bs58_pubkey")] + #[serde(with = "bs58_ed25519_pubkey")] pub node_identity: identity::PublicKey, pub latest_measurement: Option, diff --git a/nym-node/nym-node-requests/src/api/v1/node/models.rs b/nym-node/nym-node-requests/src/api/v1/node/models.rs index b3c4a03931..d0f1e3df03 100644 --- a/nym-node/nym-node-requests/src/api/v1/node/models.rs +++ b/nym-node/nym-node-requests/src/api/v1/node/models.rs @@ -2,6 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use celes::Country; +use nym_crypto::asymmetric::ed25519::{self, serde_helpers::bs58_ed25519_pubkey}; +use nym_crypto::asymmetric::x25519::{ + self, + serde_helpers::{bs58_x25519_pubkey, option_bs58_x25519_pubkey}, +}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::net::IpAddr; @@ -18,7 +23,28 @@ pub struct NodeRoles { pub ip_packet_router_enabled: bool, } -#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] +impl NodeRoles { + pub fn can_operate_mixnode(&self) -> bool { + self.mixnode_enabled + } + + pub fn can_operate_entry_gateway(&self) -> bool { + self.gateway_enabled + } + + pub fn can_operate_exit_gateway(&self) -> bool { + self.gateway_enabled && self.network_requester_enabled && self.ip_packet_router_enabled + } +} + +#[derive(Clone, Copy, Default, Debug, Serialize, Deserialize, JsonSchema)] +#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] +pub struct AnnouncePorts { + pub verloc_port: Option, + pub mix_port: Option, +} + +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct HostInformation { /// Ip address(es) of this host, such as `1.1.1.1`. @@ -33,6 +59,24 @@ pub struct HostInformation { pub keys: HostKeys, } +impl HostInformation { + pub fn check_ips(&self) -> bool { + for ip in &self.ip_address { + if ip.is_unspecified() || ip.is_loopback() || ip.is_multicast() { + return false; + } + } + true + } +} + +#[derive(Serialize)] +pub struct LegacyHostInformationV2 { + pub ip_address: Vec, + pub hostname: Option, + pub keys: LegacyHostKeysV2, +} + #[derive(Serialize)] pub struct LegacyHostInformation { pub ip_address: Vec, @@ -40,8 +84,18 @@ pub struct LegacyHostInformation { pub keys: LegacyHostKeys, } -impl From for LegacyHostInformation { +impl From for LegacyHostInformationV2 { fn from(value: HostInformation) -> Self { + LegacyHostInformationV2 { + ip_address: value.ip_address, + hostname: value.hostname, + keys: value.keys.into(), + } + } +} + +impl From for LegacyHostInformation { + fn from(value: LegacyHostInformationV2) -> Self { LegacyHostInformation { ip_address: value.ip_address, hostname: value.hostname, @@ -50,25 +104,57 @@ impl From for LegacyHostInformation { } } -#[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct HostKeys { /// Base58-encoded ed25519 public key of this node. Currently, it corresponds to either mixnode's or gateway's identity. #[serde(alias = "ed25519")] - pub ed25519_identity: String, + #[serde(with = "bs58_ed25519_pubkey")] + #[schemars(with = "String")] + pub ed25519_identity: ed25519::PublicKey, /// Base58-encoded x25519 public key of this node used for sphinx/outfox packet creation. /// Currently, it corresponds to either mixnode's or gateway's key. #[serde(alias = "x25519")] - pub x25519_sphinx: String, + #[serde(with = "bs58_x25519_pubkey")] + #[schemars(with = "String")] + pub x25519_sphinx: x25519::PublicKey, /// Base58-encoded x25519 public key of this node used for the noise protocol. #[serde(default)] + #[serde(with = "option_bs58_x25519_pubkey")] + #[schemars(with = "Option")] + pub x25519_noise: Option, +} + +#[derive(Serialize)] +pub struct LegacyHostKeysV2 { + pub ed25519_identity: String, + pub x25519_sphinx: String, pub x25519_noise: String, } -impl From for LegacyHostKeys { +#[derive(Serialize)] +pub struct LegacyHostKeys { + pub ed25519: String, + pub x25519: String, +} + +impl From for LegacyHostKeysV2 { fn from(value: HostKeys) -> Self { + LegacyHostKeysV2 { + ed25519_identity: value.ed25519_identity.to_base58_string(), + x25519_sphinx: value.x25519_sphinx.to_base58_string(), + x25519_noise: value + .x25519_noise + .map(|k| k.to_base58_string()) + .unwrap_or_default(), + } + } +} + +impl From for LegacyHostKeys { + fn from(value: LegacyHostKeysV2) -> Self { LegacyHostKeys { ed25519: value.ed25519_identity, x25519: value.x25519_sphinx, @@ -76,12 +162,6 @@ impl From for LegacyHostKeys { } } -#[derive(Serialize)] -pub struct LegacyHostKeys { - pub ed25519: String, - pub x25519: String, -} - #[derive(Clone, Default, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))] pub struct HostSystem { @@ -175,6 +255,9 @@ pub struct AuxiliaryDetails { #[schemars(length(equal = 2))] pub location: Option, + #[serde(default)] + pub announce_ports: AnnouncePorts, + /// Specifies whether this node operator has agreed to the terms and conditions /// as defined at // make sure to include the default deserialisation as this field hasn't existed when the struct was first created diff --git a/nym-node/nym-node-requests/src/lib.rs b/nym-node/nym-node-requests/src/lib.rs index 7b4e0ee9b7..983b1a6aec 100644 --- a/nym-node/nym-node-requests/src/lib.rs +++ b/nym-node/nym-node-requests/src/lib.rs @@ -65,10 +65,12 @@ pub mod routes { use super::*; pub const MIXING: &str = "/mixing"; + pub const SESSIONS: &str = "/sessions"; pub const VERLOC: &str = "/verloc"; pub const PROMETHEUS: &str = "/prometheus"; absolute_route!(mixing_absolute, metrics_absolute(), MIXING); + absolute_route!(sessions_absolute, metrics_absolute(), SESSIONS); absolute_route!(verloc_absolute, metrics_absolute(), VERLOC); absolute_route!(prometheus_absolute, metrics_absolute(), PROMETHEUS); } diff --git a/nym-node/src/cli/commands/migrate.rs b/nym-node/src/cli/commands/migrate.rs index 08d9d0b2e0..ec8cdaeeea 100644 --- a/nym-node/src/cli/commands/migrate.rs +++ b/nym-node/src/cli/commands/migrate.rs @@ -235,6 +235,7 @@ async fn migrate_mixnode(mut args: Args) -> Result<(), NymNodeError> { .with_mixnode(args.mixnode.override_config_section(config::MixnodeConfig { verloc: config::mixnode::Verloc { bind_address: SocketAddr::new(ip, cfg.mixnode.verloc_port), + announce_port: None, debug: config::mixnode::VerlocDebug { packets_per_node: cfg.verloc.packets_per_node, connection_timeout: cfg.verloc.connection_timeout, @@ -386,6 +387,7 @@ async fn migrate_gateway(mut args: Args) -> Result<(), NymNodeError> { })) .with_mixnet(args.mixnet.override_config_section(config::Mixnet { bind_address: SocketAddr::new(ip, cfg.gateway.mix_port), + announce_port: None, nym_api_urls: cfg.gateway.nym_api_urls.clone(), nyxd_urls: cfg.gateway.nyxd_urls.clone(), debug: config::MixnetDebug { diff --git a/nym-node/src/cli/helpers.rs b/nym-node/src/cli/helpers.rs index 02a2b9ce6c..cabbd6d621 100644 --- a/nym-node/src/cli/helpers.rs +++ b/nym-node/src/cli/helpers.rs @@ -188,6 +188,15 @@ pub(crate) struct MixnetArgs { )] pub(crate) mixnet_bind_address: Option, + /// If applicable, custom port announced in the self-described API that other clients and nodes + /// will use. + /// Useful when the node is behind a proxy. + #[clap( + long, + env = NYMNODE_MIXNET_ANNOUNCE_PORT_ARG + )] + pub(crate) mixnet_announce_port: Option, + /// Addresses to nym APIs from which the node gets the view of the network. #[clap( long, @@ -223,6 +232,9 @@ impl MixnetArgs { if let Some(bind_address) = self.mixnet_bind_address { section.bind_address = bind_address } + if let Some(mixnet_announce_port) = self.mixnet_announce_port { + section.announce_port = Some(mixnet_announce_port) + } if let Some(nym_api_urls) = self.nym_api_urls { section.nym_api_urls = nym_api_urls } @@ -321,6 +333,15 @@ pub(crate) struct MixnodeArgs { env = NYMNODE_VERLOC_BIND_ADDRESS_ARG )] pub(crate) verloc_bind_address: Option, + + /// If applicable, custom port announced in the self-described API that other clients and nodes + /// will use. + /// Useful when the node is behind a proxy. + #[clap( + long, + env = NYMNODE_VERLOC_ANNOUNCE_PORT_ARG + )] + pub(crate) verloc_announce_port: Option, } impl MixnodeArgs { @@ -336,6 +357,9 @@ impl MixnodeArgs { if let Some(bind_address) = self.verloc_bind_address { section.verloc.bind_address = bind_address } + if let Some(announce_port) = self.verloc_announce_port { + section.verloc.announce_port = Some(announce_port) + } section } } diff --git a/nym-node/src/config/entry_gateway.rs b/nym-node/src/config/entry_gateway.rs index 6807e55d58..f7c0a51c06 100644 --- a/nym-node/src/config/entry_gateway.rs +++ b/nym-node/src/config/entry_gateway.rs @@ -154,7 +154,7 @@ pub fn ephemeral_entry_gateway_config( config: Config, mnemonic: &bip39::Mnemonic, ) -> Result { - let auth_opts = LocalAuthenticatorOpts { + let mut auth_opts = LocalAuthenticatorOpts { config: nym_authenticator::Config { base: nym_client_core_config_types::Config { client: base_client_config(&config), @@ -173,6 +173,10 @@ pub fn ephemeral_entry_gateway_config( custom_mixnet_path: None, }; + if config.authenticator.debug.disable_poisson_rate { + auth_opts.config.base.set_no_poisson_process(); + } + let wg_opts = LocalWireguardOpts { config: super::Wireguard { enabled: config.wireguard.enabled, diff --git a/nym-node/src/config/exit_gateway.rs b/nym-node/src/config/exit_gateway.rs index 4c3c55fad2..0fc6b9e8ee 100644 --- a/nym-node/src/config/exit_gateway.rs +++ b/nym-node/src/config/exit_gateway.rs @@ -243,7 +243,7 @@ pub fn ephemeral_exit_gateway_config( ipr_opts.config.base.set_no_poisson_process() } - let auth_opts = LocalAuthenticatorOpts { + let mut auth_opts = LocalAuthenticatorOpts { config: nym_authenticator::Config { base: nym_client_core_config_types::Config { client: base_client_config(&config), @@ -262,6 +262,10 @@ pub fn ephemeral_exit_gateway_config( custom_mixnet_path: None, }; + if config.authenticator.debug.disable_poisson_rate { + auth_opts.config.base.set_no_poisson_process(); + } + let pub_id_path = config .storage_paths .keys diff --git a/nym-node/src/config/mixnode.rs b/nym-node/src/config/mixnode.rs index 684ea7129b..c938610255 100644 --- a/nym-node/src/config/mixnode.rs +++ b/nym-node/src/config/mixnode.rs @@ -7,6 +7,7 @@ use crate::error::MixnodeError; use clap::crate_version; use nym_config::defaults::DEFAULT_VERLOC_LISTENING_PORT; use nym_config::helpers::inaddr_any; +use nym_config::serde_helpers::de_maybe_port; use serde::{Deserialize, Serialize}; use std::net::SocketAddr; use std::time::Duration; @@ -41,6 +42,13 @@ pub struct Verloc { /// default: `0.0.0.0:1790` pub bind_address: SocketAddr, + /// If applicable, custom port announced in the self-described API that other clients and nodes + /// will use. + /// Useful when the node is behind a proxy. + #[serde(deserialize_with = "de_maybe_port")] + #[serde(default)] + pub announce_port: Option, + #[serde(default)] pub debug: VerlocDebug, } @@ -49,6 +57,7 @@ impl Default for Verloc { fn default() -> Self { Verloc { bind_address: SocketAddr::new(inaddr_any(), DEFAULT_VERLOC_PORT), + announce_port: None, debug: Default::default(), } } diff --git a/nym-node/src/config/mod.rs b/nym-node/src/config/mod.rs index 5da49419cf..65ca8983d7 100644 --- a/nym-node/src/config/mod.rs +++ b/nym-node/src/config/mod.rs @@ -12,6 +12,7 @@ use nym_config::defaults::{ mainnet, var_names, DEFAULT_MIX_LISTENING_PORT, DEFAULT_NYM_NODE_HTTP_PORT, WG_PORT, }; use nym_config::helpers::inaddr_any; +use nym_config::serde_helpers::de_maybe_port; use nym_config::serde_helpers::de_maybe_stringified; use nym_config::{ must_get_home, parse_urls, read_config_from_toml_file, save_formatted_config_to_file, @@ -416,6 +417,13 @@ pub struct Mixnet { /// default: `0.0.0.0:1789` pub bind_address: SocketAddr, + /// If applicable, custom port announced in the self-described API that other clients and nodes + /// will use. + /// Useful when the node is behind a proxy. + #[serde(deserialize_with = "de_maybe_port")] + #[serde(default)] + pub announce_port: Option, + /// Addresses to nym APIs from which the node gets the view of the network. pub nym_api_urls: Vec, @@ -492,6 +500,7 @@ impl Default for Mixnet { Mixnet { bind_address: SocketAddr::new(inaddr_any(), DEFAULT_MIXNET_PORT), + announce_port: None, nym_api_urls, nyxd_urls, debug: Default::default(), diff --git a/nym-node/src/config/old_configs/old_config_v3.rs b/nym-node/src/config/old_configs/old_config_v3.rs index 809955e5ee..f13cfdf4a0 100644 --- a/nym-node/src/config/old_configs/old_config_v3.rs +++ b/nym-node/src/config/old_configs/old_config_v3.rs @@ -992,6 +992,7 @@ pub async fn try_upgrade_config_v3>( }, mixnet: Mixnet { bind_address: old_cfg.mixnet.bind_address, + announce_port: None, nym_api_urls: old_cfg.mixnet.nym_api_urls, nyxd_urls: old_cfg.mixnet.nyxd_urls, debug: MixnetDebug { @@ -1066,6 +1067,7 @@ pub async fn try_upgrade_config_v3>( storage_paths: MixnodePaths {}, verloc: Verloc { bind_address: old_cfg.mixnode.verloc.bind_address, + announce_port: None, debug: VerlocDebug { packets_per_node: old_cfg.mixnode.verloc.debug.packets_per_node, connection_timeout: old_cfg.mixnode.verloc.debug.connection_timeout, diff --git a/nym-node/src/config/template.rs b/nym-node/src/config/template.rs index f1576f2fe2..83588a0639 100644 --- a/nym-node/src/config/template.rs +++ b/nym-node/src/config/template.rs @@ -41,6 +41,12 @@ location = '{{ host.location }}' # default: `0.0.0.0:1789` bind_address = '{{ mixnet.bind_address }}' +# If applicable, custom port announced in the self-described API that other clients and nodes +# will use. +# Useful when the node is behind a proxy. +# (default: 0 - disabled) +announce_port ={{#if mixnet.announce_port }} {{ mixnet.announce_port }} {{else}} 0 {{/if}} + # Addresses to nym APIs from which the node gets the view of the network. nym_api_urls = [ {{#each mixnet.nym_api_urls }}'{{this}}',{{/each}} @@ -144,6 +150,12 @@ public_diffie_hellman_key_file = '{{ wireguard.storage_paths.public_diffie_hellm # default: `0.0.0.0:1790` bind_address = '{{ mixnode.verloc.bind_address }}' +# If applicable, custom port announced in the self-described API that other clients and nodes +# will use. +# Useful when the node is behind a proxy. +# (default: 0 - disabled) +announce_port ={{#if mixnode.verloc.announce_port }} {{ mixnode.verloc.announce_port }} {{else}} 0 {{/if}} + [mixnode.storage_paths] # currently empty diff --git a/nym-node/src/env.rs b/nym-node/src/env.rs index 6067c94aa7..d9b03a4b3e 100644 --- a/nym-node/src/env.rs +++ b/nym-node/src/env.rs @@ -35,6 +35,7 @@ pub mod vars { // mixnet: pub const NYMNODE_MIXNET_BIND_ADDRESS_ARG: &str = "NYMNODE_MIXNET_BIND_ADDRESS"; + pub const NYMNODE_MIXNET_ANNOUNCE_PORT_ARG: &str = "NYMNODE_MIXNET_ANNOUNCE_PORT"; pub const NYMNODE_NYM_APIS_ARG: &str = "NYMNODE_NYM_APIS"; pub const NYMNODE_NYXD_URLS_ARG: &str = "NYMNODE_NYXD"; pub const NYMNODE_UNSAFE_DISABLE_NOISE: &str = "UNSAFE_DISABLE_NOISE"; @@ -48,6 +49,7 @@ pub mod vars { // mixnode: pub const NYMNODE_VERLOC_BIND_ADDRESS_ARG: &str = "NYMNODE_VERLOC_BIND_ADDRESS"; + pub const NYMNODE_VERLOC_ANNOUNCE_PORT_ARG: &str = "NYMNODE_VERLOC_ANNOUNCE_PORT"; // entry gateway: pub const NYMNODE_ENTRY_BIND_ADDRESS_ARG: &str = "NYMNODE_ENTRY_BIND_ADDRESS"; diff --git a/nym-node/src/node/helpers.rs b/nym-node/src/node/helpers.rs index 9fc7c310c9..3055578757 100644 --- a/nym-node/src/node/helpers.rs +++ b/nym-node/src/node/helpers.rs @@ -38,6 +38,7 @@ pub(crate) struct DisplayDetails { pub(crate) exit_network_requester_address: String, pub(crate) exit_ip_packet_router_address: String, + pub(crate) exit_authenticator_address: String, } impl Display for DisplayDetails { @@ -64,6 +65,11 @@ impl Display for DisplayDetails { "exit ip packet router address: {}", self.exit_ip_packet_router_address )?; + writeln!( + f, + "exit authenticator address: {}", + self.exit_authenticator_address + )?; Ok(()) } } diff --git a/nym-node/src/node/http/mod.rs b/nym-node/src/node/http/mod.rs index b90398d6be..defdd9628e 100644 --- a/nym-node/src/node/http/mod.rs +++ b/nym-node/src/node/http/mod.rs @@ -17,17 +17,17 @@ pub(crate) fn sign_host_details( ed22519_identity: &ed25519::KeyPair, ) -> Result { let x25519_noise = if config.mixnet.debug.unsafe_disable_noise { - String::new() + None } else { - x25519_noise.to_base58_string() + Some(*x25519_noise) }; let host_info = api_requests::v1::node::models::HostInformation { ip_address: config.host.public_ips.clone(), hostname: config.host.hostname.clone(), keys: api_requests::v1::node::models::HostKeys { - ed25519_identity: ed22519_identity.public_key().to_base58_string(), - x25519_sphinx: x22519_sphinx.to_base58_string(), + ed25519_identity: *ed22519_identity.public_key(), + x25519_sphinx: *x22519_sphinx, x25519_noise, }, }; diff --git a/nym-node/src/node/mod.rs b/nym-node/src/node/mod.rs index ce832617aa..e2abef5970 100644 --- a/nym-node/src/node/mod.rs +++ b/nym-node/src/node/mod.rs @@ -25,20 +25,25 @@ use nym_node::config::{ }; use nym_node::error::{EntryGatewayError, ExitGatewayError, MixnodeError, NymNodeError}; use nym_node_http_api::api::api_requests; -use nym_node_http_api::api::api_requests::v1::node::models::NodeDescription; -use nym_node_http_api::state::metrics::{SharedMixingStats, SharedVerlocStats}; +use nym_node_http_api::api::api_requests::v1::node::models::{AnnouncePorts, NodeDescription}; +use nym_node_http_api::state::metrics::{SharedMixingStats, SharedSessionStats, SharedVerlocStats}; use nym_node_http_api::state::AppState; use nym_node_http_api::{NymNodeHTTPServer, NymNodeRouter}; use nym_sphinx_acknowledgements::AckKey; use nym_sphinx_addressing::Recipient; use nym_task::{TaskClient, TaskManager}; +use nym_validator_client::client::NymApiClientExt; +use nym_validator_client::models::NodeRefreshBody; +use nym_validator_client::NymApiClient; use nym_wireguard::{peer_controller::PeerControlRequest, WireguardGatewayData}; use rand::rngs::OsRng; use rand::{CryptoRng, RngCore}; use std::path::Path; use std::sync::Arc; +use std::time::Duration; use tokio::sync::mpsc; -use tracing::{debug, error, info, trace}; +use tokio::time::timeout; +use tracing::{debug, error, info, trace, warn}; use zeroize::Zeroizing; use self::helpers::load_x25519_wireguard_keypair; @@ -67,6 +72,7 @@ impl MixnodeData { pub struct EntryGatewayData { mnemonic: Zeroizing, client_storage: nym_gateway::node::PersistentStorage, + sessions_stats: SharedSessionStats, } impl EntryGatewayData { @@ -93,6 +99,7 @@ impl EntryGatewayData { ) .await .map_err(nym_gateway::GatewayError::from)?, + sessions_stats: SharedSessionStats::new(), }) } } @@ -522,6 +529,7 @@ impl NymNode { x25519_wireguard_key: self.x25519_wireguard_key().to_base58_string(), exit_network_requester_address: self.exit_network_requester_address().to_string(), exit_ip_packet_router_address: self.exit_ip_packet_router_address().to_string(), + exit_authenticator_address: self.exit_authenticator_address().to_string(), } } @@ -580,6 +588,7 @@ impl NymNode { ); entry_gateway.disable_http_server(); entry_gateway.set_task_client(task_client); + entry_gateway.set_session_stats(self.entry_gateway.sessions_stats.clone()); if self.config.wireguard.enabled { entry_gateway.set_wireguard_data(self.wireguard.into()); } @@ -609,6 +618,7 @@ impl NymNode { ); exit_gateway.disable_http_server(); exit_gateway.set_task_client(task_client); + exit_gateway.set_session_stats(self.entry_gateway.sessions_stats.clone()); //Weird naming I'll give you that, but Andrew is gonna rework it anyway if self.config.wireguard.enabled { exit_gateway.set_wireguard_data(self.wireguard.into()); } @@ -631,6 +641,10 @@ impl NymNode { let auxiliary_details = api_requests::v1::node::models::AuxiliaryDetails { location: self.config.host.location, + announce_ports: AnnouncePorts { + verloc_port: self.config.mixnode.verloc.announce_port, + mix_port: self.config.mixnet.announce_port, + }, accepted_operator_terms_and_conditions: self.accepted_operator_terms_and_conditions, }; @@ -723,6 +737,7 @@ impl NymNode { let app_state = AppState::new() .with_mixing_stats(self.mixnode.mixing_stats.clone()) + .with_sessions_stats(self.entry_gateway.sessions_stats.clone()) .with_verloc_stats(self.verloc_stats.clone()) .with_metrics_key(self.config.http.access_token.clone()); @@ -731,6 +746,38 @@ impl NymNode { .await?) } + async fn try_refresh_remote_nym_api_cache(&self) { + info!("attempting to request described cache request from nym-api..."); + if self.config.mixnet.nym_api_urls.is_empty() { + warn!("no nym-api urls available"); + return; + } + + for nym_api in &self.config.mixnet.nym_api_urls { + info!("trying {nym_api}..."); + let client = NymApiClient::new_with_user_agent(nym_api.clone(), bin_info_owned!()); + + // make new request every time in case previous one takes longer and invalidates the signature + let request = NodeRefreshBody::new(self.ed25519_identity_keys.private_key()); + match timeout( + Duration::from_secs(10), + client.nym_api.force_refresh_describe_cache(&request), + ) + .await + { + Ok(Ok(_)) => { + info!("managed to refresh own self-described data cache") + } + Ok(Err(request_failure)) => { + warn!("failed to resolve the refresh request: {request_failure}") + } + Err(_timeout) => { + warn!("timed out while attempting to resolve the request. the cache might be stale") + } + }; + } + } + pub(crate) async fn run(self) -> Result<(), NymNodeError> { let mut task_manager = TaskManager::default().named("NymNode"); let http_server = self @@ -745,6 +792,8 @@ impl NymNode { } }); + self.try_refresh_remote_nym_api_cache().await; + match self.config.mode { NodeMode::Mixnode => { self.start_mixnode(task_manager.subscribe_named("mixnode"))?; diff --git a/nym-validator-rewarder/src/cli/mod.rs b/nym-validator-rewarder/src/cli/mod.rs index 9fd6cf4ba8..e986b5f4d2 100644 --- a/nym-validator-rewarder/src/cli/mod.rs +++ b/nym-validator-rewarder/src/cli/mod.rs @@ -13,6 +13,8 @@ use url::Url; pub mod build_info; pub mod init; +pub mod process_block; +pub mod process_until; pub mod run; pub mod upgrade_helpers; @@ -42,6 +44,8 @@ impl Cli { match self.command { Commands::Init(args) => init::execute(args), Commands::Run(args) => run::execute(args).await, + Commands::ProcessBlock(args) => process_block::execute(args).await, + Commands::ProcessUntil(args) => process_until::execute(args).await, Commands::BuildInfo(args) => build_info::execute(args), } } @@ -97,6 +101,12 @@ pub(crate) enum Commands { /// Run the validator rewarder with the preconfigured settings. Run(run::Args), + /// Attempt to process a single block. + ProcessBlock(process_block::Args), + + /// Attempt to process multiple blocks until the provided height. + ProcessUntil(process_until::Args), + /// Show build information of this binary BuildInfo(build_info::Args), } diff --git a/nym-validator-rewarder/src/cli/process_block.rs b/nym-validator-rewarder/src/cli/process_block.rs new file mode 100644 index 0000000000..7f395bca25 --- /dev/null +++ b/nym-validator-rewarder/src/cli/process_block.rs @@ -0,0 +1,32 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::{try_load_current_config, ConfigOverridableArgs}; +use crate::error::NymRewarderError; +use nyxd_scraper::NyxdScraper; +use std::path::PathBuf; + +#[derive(Debug, clap::Args)] +pub struct Args { + #[command(flatten)] + config_override: ConfigOverridableArgs, + + /// Height of the block we want to process + #[clap(long)] + height: u32, + + /// Specifies custom location for the configuration file of nym validators rewarder. + #[clap(long)] + custom_config_path: Option, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NymRewarderError> { + let config = + try_load_current_config(&args.custom_config_path)?.with_override(args.config_override); + + NyxdScraper::new(config.scraper_config()) + .await? + .process_single_block(args.height) + .await?; + Ok(()) +} diff --git a/nym-validator-rewarder/src/cli/process_until.rs b/nym-validator-rewarder/src/cli/process_until.rs new file mode 100644 index 0000000000..f1c15470d0 --- /dev/null +++ b/nym-validator-rewarder/src/cli/process_until.rs @@ -0,0 +1,45 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: GPL-3.0-only + +use crate::cli::{try_load_current_config, ConfigOverridableArgs}; +use crate::error::NymRewarderError; +use nyxd_scraper::NyxdScraper; +use std::path::PathBuf; + +#[derive(Debug, clap::Args)] +pub struct Args { + #[command(flatten)] + config_override: ConfigOverridableArgs, + + /// Optional starting height for processing the blocks. + /// If none is provided, the default behaviour will be applied. + #[clap(long)] + start_height: Option, + + /// Height of until we want to be processing the blocks. + /// If none is provided, the currrent block height will be used + #[clap(long)] + stop_height: Option, + + /// Specifies custom location for the configuration file of nym validators rewarder. + #[clap(long)] + custom_config_path: Option, +} + +pub(crate) async fn execute(args: Args) -> Result<(), NymRewarderError> { + if let (Some(start), Some(end)) = (args.start_height, args.stop_height) { + if start > end { + eprintln!("the start height can't be larger than the stop height!"); + return Ok(()); + } + } + + let config = + try_load_current_config(&args.custom_config_path)?.with_override(args.config_override); + + NyxdScraper::new(config.scraper_config()) + .await? + .process_block_range(args.start_height, args.stop_height) + .await?; + Ok(()) +} diff --git a/nym-validator-rewarder/src/error.rs b/nym-validator-rewarder/src/error.rs index 4c72c6ef37..5e9de91cfb 100644 --- a/nym-validator-rewarder/src/error.rs +++ b/nym-validator-rewarder/src/error.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::config::RewardingRatios; +use crate::rewarder::Epoch; use nym_compact_ecash::error::CompactEcashError; use nym_crypto::asymmetric::ed25519; use nym_validator_client::nym_api::error::NymAPIError; @@ -178,6 +179,9 @@ pub enum NymRewarderError { #[error("pruning.keep_recent must not be smaller than {min_to_keep}. got: {keep_recent}")] TooSmallKeepRecent { min_to_keep: u32, keep_recent: u32 }, + + #[error("there were no blocks processed within the epoch {epoch}")] + NoBlocksProcessedInEpoch { epoch: Epoch }, } #[derive(Debug)] diff --git a/nym-validator-rewarder/src/main.rs b/nym-validator-rewarder/src/main.rs index 9b6654c3d6..e8945316a5 100644 --- a/nym-validator-rewarder/src/main.rs +++ b/nym-validator-rewarder/src/main.rs @@ -14,7 +14,7 @@ use nym_network_defaults::setup_env; pub mod cli; pub mod config; pub mod error; -mod rewarder; +pub mod rewarder; #[tokio::main] async fn main() -> anyhow::Result<()> { diff --git a/nym-validator-rewarder/src/rewarder/block_signing/mod.rs b/nym-validator-rewarder/src/rewarder/block_signing/mod.rs index d9747a9f7a..27f423c089 100644 --- a/nym-validator-rewarder/src/rewarder/block_signing/mod.rs +++ b/nym-validator-rewarder/src/rewarder/block_signing/mod.rs @@ -49,7 +49,12 @@ impl EpochSigning { ) -> Result, NymRewarderError> { // first attempt to get it via the historical info. // if that fails, attempt to use current block information to at least get **something** - if let Some(validators) = self.nyxd_client.historical_info(height).await?.hist { + if let Ok(Some(validators)) = self + .nyxd_client + .historical_info(height) + .await + .map(|v| v.hist) + { Ok(validators.valset) } else { let mut page_request = None; @@ -63,6 +68,10 @@ impl EpochSigning { break; }; + if pagination.next_key.is_empty() { + break; + } + page_request = Some(PageRequest { key: pagination.next_key, offset: 0, @@ -92,18 +101,28 @@ impl EpochSigning { let epoch_start = current_epoch.start_time; let epoch_end = current_epoch.end_time; - let first_block = self + + let Some(first_block) = self .nyxd_scraper .storage .get_first_block_height_after(epoch_start) .await? - .unwrap_or_default(); - let last_block = self + else { + return Err(NymRewarderError::NoBlocksProcessedInEpoch { + epoch: current_epoch, + }); + }; + + let Some(last_block) = self .nyxd_scraper .storage .get_last_block_height_before(epoch_end) .await? - .unwrap_or_default(); + else { + return Err(NymRewarderError::NoBlocksProcessedInEpoch { + epoch: current_epoch, + }); + }; // each validator MUST be online at some point during the first 20 blocks, otherwise they're not getting anything. let vp_range_end = min(first_block + 20, last_block); diff --git a/nym-validator-rewarder/src/rewarder/epoch.rs b/nym-validator-rewarder/src/rewarder/epoch.rs index bf74fc748c..c164a1735f 100644 --- a/nym-validator-rewarder/src/rewarder/epoch.rs +++ b/nym-validator-rewarder/src/rewarder/epoch.rs @@ -3,6 +3,7 @@ use crate::error::NymRewarderError; use sqlx::FromRow; +use std::fmt::{Display, Formatter}; use std::ops::Add; use std::time::Duration; use time::format_description::well_known::Rfc3339; @@ -34,6 +35,11 @@ impl Epoch { }) } + pub fn has_finished(&self) -> bool { + let now = OffsetDateTime::now_utc(); + self.end_time < now + } + pub fn until_end(&self) -> Duration { let now = OffsetDateTime::now_utc(); (self.end_time - now).try_into().unwrap_or_default() @@ -60,3 +66,15 @@ impl Epoch { self.end_time.format(&Rfc3339).unwrap() } } + +impl Display for Epoch { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}: {} - {}", + self.id, + self.start_rfc3339(), + self.end_rfc3339() + ) + } +} diff --git a/nym-validator-rewarder/src/rewarder/mod.rs b/nym-validator-rewarder/src/rewarder/mod.rs index 4829091f5c..514262c53b 100644 --- a/nym-validator-rewarder/src/rewarder/mod.rs +++ b/nym-validator-rewarder/src/rewarder/mod.rs @@ -7,7 +7,6 @@ use crate::rewarder::block_signing::types::EpochSigningResults; use crate::rewarder::block_signing::EpochSigning; use crate::rewarder::credential_issuance::types::CredentialIssuanceResults; use crate::rewarder::credential_issuance::CredentialIssuance; -use crate::rewarder::epoch::Epoch; use crate::rewarder::nyxd_client::NyxdClient; use crate::rewarder::storage::RewarderStorage; use futures::future::{FusedFuture, OptionFuture}; @@ -28,6 +27,8 @@ mod nyxd_client; mod storage; mod tasks; +pub(crate) use crate::rewarder::epoch::Epoch; + pub struct RewardingResult { pub total_spent: Coin, pub rewarding_tx: Hash, @@ -47,22 +48,30 @@ impl EpochRewards { pub fn amounts(&self) -> Result)>, NymRewarderError> { let mut amounts = Vec::new(); - if let Ok(Some(signing)) = &self.signing { - for (account, signing_amount) in signing.rewarding_amounts(&self.signing_budget) { - if signing_amount[0].amount != 0 { - amounts.push((account, signing_amount)) + match &self.signing { + Ok(Some(signing)) => { + for (account, signing_amount) in signing.rewarding_amounts(&self.signing_budget) { + if signing_amount[0].amount != 0 { + amounts.push((account, signing_amount)) + } } } + Err(err) => error!("failed to determine rewards for block signing: {err}"), + _ => (), } - if let Ok(Some(credentials)) = &self.credentials { - for (account, credential_amount) in - credentials.rewarding_amounts(&self.credentials_budget) - { - if credential_amount[0].amount != 0 { - amounts.push((account, credential_amount)) + match &self.credentials { + Ok(Some(credentials)) => { + for (account, credential_amount) in + credentials.rewarding_amounts(&self.credentials_budget) + { + if credential_amount[0].amount != 0 { + amounts.push((account, credential_amount)) + } } } + Err(err) => error!("failed to determine rewards for credential issuance: {err}"), + _ => (), } Ok(amounts) @@ -279,6 +288,58 @@ impl Rewarder { self.current_epoch = self.current_epoch.next(); } + async fn ensure_has_epoch_blocks(&self) -> Result<(), NymRewarderError> { + // make sure we at least have a single block processed within the epoch + let epoch_start = self.current_epoch.start_time; + let epoch_end = self.current_epoch.end_time; + + if let Some(epoch_signing) = &self.epoch_signing { + if epoch_signing + .nyxd_scraper + .storage + .get_first_block_height_after(epoch_start) + .await? + .is_none() + { + return Err(NymRewarderError::NoBlocksProcessedInEpoch { + epoch: self.current_epoch, + }); + } + + if epoch_signing + .nyxd_scraper + .storage + .get_last_block_height_before(epoch_end) + .await? + .is_none() + { + return Err(NymRewarderError::NoBlocksProcessedInEpoch { + epoch: self.current_epoch, + }); + } + } + + Ok(()) + } + + async fn startup_resync(&mut self) -> Result<(), NymRewarderError> { + // no sync required + if !self.current_epoch.has_finished() { + return Ok(()); + } + + info!("attempting to distribute missed rewards"); + while self.current_epoch.has_finished() { + info!("processing epoch {}", self.current_epoch); + self.ensure_has_epoch_blocks().await?; + + // we need to perform rewarding from the 'current' epoch until the actual current epoch + self.handle_epoch_end().await + } + + Ok(()) + } + pub async fn run(mut self) -> Result<(), NymRewarderError> { info!("Starting nym validators rewarder"); @@ -306,6 +367,20 @@ impl Rewarder { let until_end = self.current_epoch.until_end(); + if let Err(err) = self.startup_resync().await { + error!("failed to perform startup sync: {err}"); + error!("if the failure was due to insufficient number of blocks, your course of action is as follows:"); + error!("(ideally it would have been automatically resolved in this very method, but that'd require some serious refactoring)"); + error!( + "1. determine height of the first block of the epoch (doesn't have to be exact)" + ); + error!("2. run the following subcommand of the rewarder: `nym-validator-rewarder process-until --start-height=$STARTING_BLOCK"); + error!("3. !!IMPORTANT!! go to config.toml and temporarily disable block pruning, i.e. `pruning.strategy=nothing`"); + error!("4. restart nym-validator-rewarder as normal until it sends missing rewards"); + error!("5. re-enable pruning and restart the nym-validator rewarder"); + return Err(err); + } + info!( "the initial epoch (id: {}) will finish in {} secs", self.current_epoch.id, diff --git a/nym-validator-rewarder/src/rewarder/storage/mod.rs b/nym-validator-rewarder/src/rewarder/storage/mod.rs index b002ab46e5..9557a3708d 100644 --- a/nym-validator-rewarder/src/rewarder/storage/mod.rs +++ b/nym-validator-rewarder/src/rewarder/storage/mod.rs @@ -1,17 +1,19 @@ // Copyright 2023 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use crate::error::NymRewarderError; -use crate::rewarder::credential_issuance::types::CredentialIssuer; -use crate::rewarder::epoch::Epoch; -use crate::rewarder::storage::manager::StorageManager; -use crate::rewarder::{EpochRewards, RewardingResult}; -use nym_validator_client::nym_api::IssuedTicketbookBody; -use nym_validator_client::nyxd::contract_traits::ecash_query_client::DepositId; -use nym_validator_client::nyxd::Coin; +use crate::{ + error::NymRewarderError, + rewarder::{ + credential_issuance::types::CredentialIssuer, epoch::Epoch, + storage::manager::StorageManager, EpochRewards, RewardingResult, + }, +}; +use nym_validator_client::{ + nym_api::IssuedTicketbookBody, + nyxd::{contract_traits::ecash_query_client::DepositId, Coin}, +}; use sqlx::ConnectOptions; -use std::fmt::Debug; -use std::path::Path; +use std::{fmt::Debug, path::Path}; use tracing::{error, info, instrument}; mod manager; @@ -24,14 +26,13 @@ pub struct RewarderStorage { impl RewarderStorage { #[instrument] pub async fn init + Debug>(database_path: P) -> Result { - let mut opts = sqlx::sqlite::SqliteConnectOptions::new() + let opts = sqlx::sqlite::SqliteConnectOptions::new() .filename(database_path) - .create_if_missing(true); + .create_if_missing(true) + .disable_statement_logging(); // TODO: do we want auto_vacuum ? - opts.disable_statement_logging(); - let connection_pool = match sqlx::SqlitePool::connect_with(opts).await { Ok(db) => db, Err(err) => { diff --git a/nym-wallet/CHANGELOG.md b/nym-wallet/CHANGELOG.md index 146e6174ed..aba29abef0 100644 --- a/nym-wallet/CHANGELOG.md +++ b/nym-wallet/CHANGELOG.md @@ -2,6 +2,16 @@ ## [Unreleased] +## [2024.13-magura] (2024-11-18) + +- bugfix: [wallet] displaying delegations for native nymnodes ([#5087]) +- bugfix: wallet backend fixes ([#5070]) +- Feature/wallet bonding fixes ([#5064]) + +[#5087]: https://github.com/nymtech/nym/pull/5087 +[#5070]: https://github.com/nymtech/nym/pull/5070 +[#5064]: https://github.com/nymtech/nym/pull/5064 + ## [v1.2.13] (2024-05-08) - Bug fix: wallet delegations list is empty when RPC node doesn't hold block ([#4565]) diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index 3813f1ba4c..0d1cedc3b2 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" - [[package]] name = "addr2line" version = "0.20.0" @@ -173,13 +167,13 @@ checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -664,7 +658,7 @@ dependencies = [ "heck 0.4.1", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -1028,7 +1022,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" dependencies = [ "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -1089,7 +1083,7 @@ checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -1637,9 +1631,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.33" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide 0.8.0", @@ -1757,7 +1751,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -1937,7 +1931,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -2630,15 +2624,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - [[package]] name = "itertools" version = "0.13.0" @@ -3096,10 +3081,12 @@ dependencies = [ "nym-crypto", "nym-ecash-time", "nym-mixnet-contract-common", + "nym-network-defaults", "nym-node-requests", "nym-serde-helpers", "schemars", "serde", + "serde_json", "sha2 0.10.8", "tendermint 0.37.0", "thiserror", @@ -3154,12 +3141,13 @@ dependencies = [ "digest 0.9.0", "ff", "group", - "itertools 0.12.1", + "itertools 0.13.0", "nym-network-defaults", "nym-pemstore", "rand 0.8.5", "serde", "sha2 0.9.9", + "subtle 2.5.0", "thiserror", "zeroize", ] @@ -3289,6 +3277,7 @@ dependencies = [ "cosmwasm-schema", "cosmwasm-std", "cw-controllers", + "cw-storage-plus", "humantime-serde", "log", "nym-contracts-common", @@ -3332,6 +3321,7 @@ dependencies = [ name = "nym-node-requests" version = "0.1.0" dependencies = [ + "async-trait", "base64 0.22.1", "celes", "humantime 2.1.0", @@ -3339,6 +3329,7 @@ dependencies = [ "nym-bin-common", "nym-crypto", "nym-exit-policy", + "nym-http-api-client", "nym-wireguard-types", "schemars", "serde", @@ -3361,6 +3352,7 @@ version = "0.1.0" dependencies = [ "base64 0.22.1", "bs58", + "hex", "serde", "time", ] @@ -3436,7 +3428,6 @@ dependencies = [ "flate2", "futures", "itertools 0.13.0", - "log", "nym-api-requests", "nym-coconut-bandwidth-contract-common", "nym-coconut-dkg-common", @@ -3449,6 +3440,7 @@ dependencies = [ "nym-mixnet-contract-common", "nym-multisig-contract-common", "nym-network-defaults", + "nym-serde-helpers", "nym-vesting-contract-common", "prost", "reqwest 0.12.4", @@ -3459,6 +3451,7 @@ dependencies = [ "thiserror", "time", "tokio", + "tracing", "url", "wasmtimer", "zeroize", @@ -3525,7 +3518,7 @@ dependencies = [ [[package]] name = "nym_wallet" -version = "1.2.14" +version = "1.2.15" dependencies = [ "async-trait", "base64 0.13.1", @@ -3546,6 +3539,7 @@ dependencies = [ "nym-contracts-common", "nym-crypto", "nym-mixnet-contract-common", + "nym-node-requests", "nym-store-cipher", "nym-types", "nym-validator-client", @@ -3673,7 +3667,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -3860,7 +3854,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -3989,7 +3983,7 @@ checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -4146,7 +4140,7 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -4157,9 +4151,9 @@ checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" [[package]] name = "proc-macro2" -version = "1.0.79" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -4184,7 +4178,7 @@ dependencies = [ "itertools 0.11.0", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -4710,7 +4704,7 @@ dependencies = [ "proc-macro2", "quote", "serde_derive_internals", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -4804,9 +4798,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5" dependencies = [ "serde_derive", ] @@ -4831,13 +4825,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.210" +version = "1.0.214" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -4848,14 +4842,14 @@ checksum = "18d26a20a969b9e3fdf2fc2d9f21eda6c40e2de84c9408bb5d3b05d499aae711" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] name = "serde_json" -version = "1.0.128" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ "itoa 1.0.9", "memchr", @@ -4871,14 +4865,14 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" dependencies = [ "serde", ] @@ -5225,7 +5219,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -5268,9 +5262,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.55" +version = "2.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "002a1b3dbf967edfafc32655d0f377ab0bb7b994aa1d32c8cc7e9b8bf3ebb8f0" +checksum = "5023162dfcd14ef8f32034d8bcd4cc5ddc61ef7a247c024a33e24e1f24d21b56" dependencies = [ "proc-macro2", "quote", @@ -5760,22 +5754,22 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -5862,7 +5856,7 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -5975,7 +5969,7 @@ dependencies = [ "serde", "serde_spanned", "toml_datetime", - "winnow 0.6.20", + "winnow 0.6.19", ] [[package]] @@ -6027,7 +6021,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -6086,24 +6080,24 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "ts-rs" -version = "7.0.0" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1ff1f8c90369bc172200013ac17ae86e7b5def580687df4e6127883454ff2b0" +checksum = "3a2f31991cee3dce1ca4f929a8a04fdd11fd8801aac0f2030b0fa8a0a3fef6b9" dependencies = [ + "lazy_static", "thiserror", "ts-rs-macros", ] [[package]] name = "ts-rs-macros" -version = "7.0.0" +version = "10.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f41cc0aeb7a4a55730188e147d3795a7349b501f8334697fd37629b896cdc2" +checksum = "0ea0b99e8ec44abd6f94a18f28f7934437809dd062820797c52401298116f70e" dependencies = [ - "Inflector", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", "termcolor", ] @@ -6207,7 +6201,7 @@ dependencies = [ "proc-macro-error", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] @@ -6322,7 +6316,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", "wasm-bindgen-shared", ] @@ -6356,7 +6350,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -6851,9 +6845,9 @@ dependencies = [ [[package]] name = "winnow" -version = "0.6.20" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" +checksum = "c52ac009d615e79296318c1bcce2d422aaca15ad08515e344feeda07df67a587" dependencies = [ "memchr", ] @@ -6985,7 +6979,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.55", + "syn 2.0.85", ] [[package]] diff --git a/nym-wallet/nym-wallet-types/Cargo.toml b/nym-wallet/nym-wallet-types/Cargo.toml index 9bfd3717b9..720027178b 100644 --- a/nym-wallet/nym-wallet-types/Cargo.toml +++ b/nym-wallet/nym-wallet-types/Cargo.toml @@ -10,7 +10,7 @@ hex-literal = "0.3.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" strum = { version = "0.23", features = ["derive"] } -ts-rs = "7.0.0" +ts-rs = "10.0.0" cosmwasm-std = "1.4.3" cosmrs = "=0.15.0" diff --git a/nym-wallet/nym-wallet-types/src/admin.rs b/nym-wallet/nym-wallet-types/src/admin.rs index 0aac089ee2..4808f20b40 100644 --- a/nym-wallet/nym-wallet-types/src/admin.rs +++ b/nym-wallet/nym-wallet-types/src/admin.rs @@ -13,13 +13,12 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "nym-wallet/src/types/rust/StateParams.ts") + ts(export, export_to = "nym-wallet/src/types/rust/StateParams.ts") )] #[derive(Serialize, Deserialize, Debug)] pub struct TauriContractStateParams { - minimum_mixnode_pledge: DecCoin, - minimum_gateway_pledge: DecCoin, - minimum_mixnode_delegation: Option, + minimum_pledge: DecCoin, + minimum_delegation: Option, operating_cost: TauriOperatingCostRange, profit_margin: TauriProfitMarginRange, @@ -28,7 +27,7 @@ pub struct TauriContractStateParams { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "nym-wallet/src/types/rust/OperatingCostRange.ts") + ts(export, export_to = "nym-wallet/src/types/rust/OperatingCostRange.ts") )] #[derive(Serialize, Deserialize, Debug)] pub struct TauriOperatingCostRange { @@ -39,7 +38,7 @@ pub struct TauriOperatingCostRange { #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "nym-wallet/src/types/rust/ProfitMarginRange.ts") + ts(export, export_to = "nym-wallet/src/types/rust/ProfitMarginRange.ts") )] #[derive(Serialize, Deserialize, Debug)] pub struct TauriProfitMarginRange { @@ -52,7 +51,7 @@ impl TauriContractStateParams { state_params: ContractStateParams, reg: &RegisteredCoins, ) -> Result { - let rewarding_denom = &state_params.minimum_mixnode_pledge.denom; + let rewarding_denom = &state_params.minimum_pledge.denom; let min_operating_cost_c = Coin { denom: rewarding_denom.into(), amount: state_params.interval_operating_cost.minimum, @@ -63,12 +62,10 @@ impl TauriContractStateParams { }; Ok(TauriContractStateParams { - minimum_mixnode_pledge: reg - .attempt_convert_to_display_dec_coin(state_params.minimum_mixnode_pledge.into())?, - minimum_gateway_pledge: reg - .attempt_convert_to_display_dec_coin(state_params.minimum_gateway_pledge.into())?, - minimum_mixnode_delegation: state_params - .minimum_mixnode_delegation + minimum_pledge: reg + .attempt_convert_to_display_dec_coin(state_params.minimum_pledge.into())?, + minimum_delegation: state_params + .minimum_delegation .map(|min_del| reg.attempt_convert_to_display_dec_coin(min_del.into())) .transpose()?, @@ -96,16 +93,13 @@ impl TauriContractStateParams { let max_operating_cost_c = reg.attempt_convert_to_base_coin(self.operating_cost.maximum)?; Ok(ContractStateParams { - minimum_mixnode_delegation: self - .minimum_mixnode_delegation + minimum_delegation: self + .minimum_delegation .map(|min_del| reg.attempt_convert_to_base_coin(min_del)) .transpose()? .map(Into::into), - minimum_mixnode_pledge: reg - .attempt_convert_to_base_coin(self.minimum_mixnode_pledge)? - .into(), - minimum_gateway_pledge: reg - .attempt_convert_to_base_coin(self.minimum_gateway_pledge)? + minimum_pledge: reg + .attempt_convert_to_base_coin(self.minimum_pledge)? .into(), profit_margin: ContractProfitMarginRange { diff --git a/nym-wallet/nym-wallet-types/src/interval.rs b/nym-wallet/nym-wallet-types/src/interval.rs index 875f343043..3d17ae4813 100644 --- a/nym-wallet/nym-wallet-types/src/interval.rs +++ b/nym-wallet/nym-wallet-types/src/interval.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "nym-wallet/src/types/rust/Interval.ts") + ts(export, export_to = "nym-wallet/src/types/rust/Interval.ts") )] #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Eq, PartialOrd, Serialize)] pub struct Interval { diff --git a/nym-wallet/nym-wallet-types/src/network.rs b/nym-wallet/nym-wallet-types/src/network.rs index 08b3da69b1..97948a4248 100644 --- a/nym-wallet/nym-wallet-types/src/network.rs +++ b/nym-wallet/nym-wallet-types/src/network.rs @@ -14,7 +14,7 @@ mod sandbox; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "nym-wallet/src/types/rust/Network.ts") + ts(export, export_to = "nym-wallet/src/types/rust/Network.ts") )] #[derive(Copy, Clone, Debug, Deserialize, EnumIter, Eq, Hash, PartialEq, Serialize)] pub enum Network { diff --git a/nym-wallet/nym-wallet-types/src/network_config.rs b/nym-wallet/nym-wallet-types/src/network_config.rs index 8d867d1768..06e68d7f28 100644 --- a/nym-wallet/nym-wallet-types/src/network_config.rs +++ b/nym-wallet/nym-wallet-types/src/network_config.rs @@ -5,7 +5,7 @@ use std::fmt; #[cfg_attr(feature = "generate-ts", derive(ts_rs::TS))] #[cfg_attr( feature = "generate-ts", - ts(export_to = "nym-wallet/src/types/rust/ValidatorUrls.ts") + ts(export, export_to = "nym-wallet/src/types/rust/ValidatorUrls.ts") )] #[derive(Debug, Serialize, Deserialize)] pub struct ValidatorUrls { diff --git a/nym-wallet/package.json b/nym-wallet/package.json index 3d97eefd26..341da85fb1 100644 --- a/nym-wallet/package.json +++ b/nym-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/nym-wallet-app", - "version": "1.2.14", + "version": "1.2.15", "license": "MIT", "main": "index.js", "scripts": { diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 8e995a784f..fcf0f6417e 100644 --- a/nym-wallet/src-tauri/Cargo.toml +++ b/nym-wallet/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "nym_wallet" -version = "1.2.14" +version = "1.2.15" description = "Nym Native Wallet" authors = ["Nym Technologies SA"] license = "" @@ -52,6 +52,7 @@ zeroize = { version = "1.5", features = ["zeroize_derive", "serde"] } cosmwasm-std = "1.3.0" cosmrs = { git = "https://github.com/cosmos/cosmos-rust", rev = "4b1332e6d8258ac845cef71589c8d362a669675a" } +nym-node-requests = { path = "../../nym-node/nym-node-requests" } nym-validator-client = { path = "../../common/client-libs/validator-client" } nym-crypto = { path = "../../common/crypto", features = ["asymmetric"] } nym-contracts-common = { path = "../../common/cosmwasm-smart-contracts/contracts-common" } @@ -66,7 +67,7 @@ nym-store-cipher = { path = "../../common/store-cipher", features = ["json"] } nym-crypto = { path = "../../common/crypto", features = ["rand"] } rand_chacha = "0.3" tempfile = "3.3.0" -ts-rs = "7.0.0" +ts-rs = "10.0.0" [features] default = ["custom-protocol"] diff --git a/nym-wallet/src-tauri/src/error.rs b/nym-wallet/src-tauri/src/error.rs index 9d879c706e..26f7bea95d 100644 --- a/nym-wallet/src-tauri/src/error.rs +++ b/nym-wallet/src-tauri/src/error.rs @@ -1,5 +1,6 @@ use nym_contracts_common::signing::SigningAlgorithm; use nym_crypto::asymmetric::identity::Ed25519RecoveryError; +use nym_node_requests::api::client::NymNodeApiClientError; use nym_types::error::TypesError; use nym_validator_client::nym_api::error::NymAPIError; use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWalletError; @@ -12,17 +13,17 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum BackendError { - #[error("{source}")] + #[error(transparent)] TypesError { #[from] source: TypesError, }, - #[error("{source}")] + #[error(transparent)] Bip39Error { #[from] source: bip39::Error, }, - #[error("{source}")] + #[error(transparent)] TendermintError { #[from] source: cosmrs::rpc::Error, @@ -33,42 +34,47 @@ pub enum BackendError { #[source] source: NyxdError, }, - #[error("{source}")] + #[error(transparent)] CosmwasmStd { #[from] source: cosmwasm_std::StdError, }, - #[error("{source}")] + #[error(transparent)] ErrorReport { #[from] source: eyre::Report, }, - #[error("{source}")] + #[error(transparent)] NymApiError { #[from] source: NymAPIError, }, - #[error("{source}")] + #[error(transparent)] + NymNodeApiError { + #[from] + source: NymNodeApiClientError, + }, + #[error(transparent)] IOError { #[from] source: io::Error, }, - #[error("{source}")] + #[error(transparent)] SerdeJsonError { #[from] source: serde_json::Error, }, - #[error("{source}")] + #[error(transparent)] MalformedUrlProvided { #[from] source: url::ParseError, }, - #[error("{source}")] + #[error(transparent)] ReqwestError { #[from] source: reqwest::Error, }, - #[error("{source}")] + #[error(transparent)] K256Error { #[from] source: k256::ecdsa::Error, @@ -158,6 +164,9 @@ pub enum BackendError { #[error("there aren't any vesting delegations to migrate")] NoVestingDelegations, + // + // #[error("this operation is no longer allowed to be performed with vesting tokens. please move them to your liquid balance and try again")] + // DisabledVestingOperation, } impl Serialize for BackendError { diff --git a/nym-wallet/src-tauri/src/main.rs b/nym-wallet/src-tauri/src/main.rs index 623ab63e45..6824afbc15 100644 --- a/nym-wallet/src-tauri/src/main.rs +++ b/nym-wallet/src-tauri/src/main.rs @@ -73,6 +73,16 @@ fn main() { mixnet::bond::get_number_of_mixnode_delegators, mixnet::bond::get_mix_node_description, mixnet::bond::get_mixnode_avg_uptime, + mixnet::bond::bond_nymnode, + mixnet::bond::unbond_nymnode, + mixnet::bond::nym_node_bond_details, + mixnet::bond::get_nym_node_description, + mixnet::bond::migrate_legacy_mixnode, + mixnet::bond::migrate_legacy_gateway, + mixnet::bond::update_nymnode_config, + mixnet::bond::get_nymnode_performance, + mixnet::bond::get_nymnode_uptime, + mixnet::bond::get_nymnode_stake_saturation, mixnet::delegate::delegate_to_mixnode, mixnet::delegate::get_pending_delegator_rewards, mixnet::delegate::get_pending_delegation_events, @@ -103,8 +113,9 @@ fn main() { state::save_config_to_files, utils::owns_gateway, utils::owns_mixnode, + utils::owns_nym_node, utils::get_env, - utils::try_convert_pubkey_to_mix_id, + utils::try_convert_pubkey_to_node_id, utils::default_mixnode_cost_params, nym_api::status::compute_mixnode_reward_estimation, nym_api::status::gateway_core_node_status, @@ -114,6 +125,8 @@ fn main() { nym_api::status::mixnode_stake_saturation, nym_api::status::mixnode_status, nym_api::status::gateway_report, + nym_api::status::get_nymnode_role, + nym_api::status::get_nymnode_annotation, vesting::rewards::vesting_claim_delegator_reward, vesting::rewards::vesting_claim_operator_reward, vesting::bond::vesting_bond_gateway, @@ -165,8 +178,8 @@ fn main() { simulate::mixnet::simulate_update_mixnode_config, simulate::mixnet::simulate_update_mixnode_cost_params, simulate::mixnet::simulate_update_gateway_config, - simulate::mixnet::simulate_delegate_to_mixnode, - simulate::mixnet::simulate_undelegate_from_mixnode, + simulate::mixnet::simulate_delegate_to_node, + simulate::mixnet::simulate_undelegate_from_node, simulate::vesting::simulate_vesting_delegate_to_mixnode, simulate::vesting::simulate_vesting_undelegate_from_mixnode, simulate::vesting::simulate_vesting_bond_gateway, @@ -188,6 +201,7 @@ fn main() { signatures::ed25519_signing_payload::generate_mixnode_bonding_msg_payload, signatures::ed25519_signing_payload::vesting_generate_mixnode_bonding_msg_payload, signatures::ed25519_signing_payload::generate_gateway_bonding_msg_payload, + signatures::ed25519_signing_payload::generate_nym_node_bonding_msg_payload, signatures::ed25519_signing_payload::vesting_generate_gateway_bonding_msg_payload, help::log::help_log_toggle_window, app::window::create_main_window, diff --git a/nym-wallet/src-tauri/src/operations/helpers.rs b/nym-wallet/src-tauri/src/operations/helpers.rs index da0093e293..c2267ccb20 100644 --- a/nym-wallet/src-tauri/src/operations/helpers.rs +++ b/nym-wallet/src-tauri/src/operations/helpers.rs @@ -10,7 +10,8 @@ use nym_contracts_common::signing::{ use nym_crypto::asymmetric::identity; use nym_mixnet_contract_common::{ construct_legacy_mixnode_bonding_sign_payload, Gateway, GatewayBondingPayload, MixNode, - MixNodeCostParams, SignableGatewayBondingMsg, SignableLegacyMixNodeBondingMsg, + NodeCostParams, NymNode, NymNodeBondingPayload, SignableGatewayBondingMsg, + SignableLegacyMixNodeBondingMsg, SignableNymNodeBondingMsg, }; use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_validator_client::nyxd::error::NyxdError; @@ -39,7 +40,7 @@ impl AddressAndNonceProvider for DirectSigningHttpRpcValidatorClient { pub(crate) async fn create_mixnode_bonding_sign_payload( client: &P, mix_node: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, pledge: Coin, vesting: bool, ) -> Result { @@ -61,7 +62,7 @@ pub(crate) async fn create_mixnode_bonding_sign_payload( client: &P, mix_node: &MixNode, - cost_params: &MixNodeCostParams, + cost_params: &NodeCostParams, pledge: &Coin, vesting: bool, msg_signature: &MessageSignature, @@ -143,11 +144,58 @@ pub(crate) async fn verify_gateway_bonding_sign_payload( + client: &P, + nym_node: NymNode, + cost_params: NodeCostParams, + pledge: Coin, +) -> Result { + let payload = NymNodeBondingPayload::new(nym_node, cost_params); + let sender = client.cw_address(); + let content = ContractMessageContent::new(sender, vec![pledge.into()], payload); + let nonce = client.get_signing_nonce().await?; + + Ok(SignableMessage::new(nonce, content)) +} + +pub(crate) async fn verify_nym_node_bonding_sign_payload( + client: &P, + nym_node: &NymNode, + cost_params: &NodeCostParams, + pledge: &Coin, + msg_signature: &MessageSignature, +) -> Result<(), BackendError> { + let identity_key = identity::PublicKey::from_base58_string(&nym_node.identity_key)?; + let signature = identity::Signature::from_bytes(msg_signature.as_ref())?; + + // recreate the plaintext + let msg = create_nym_node_bonding_sign_payload( + client, + nym_node.clone(), + cost_params.clone(), + pledge.clone(), + ) + .await?; + let plaintext = msg.to_plaintext()?; + + if !msg.algorithm.is_ed25519() { + return Err(BackendError::UnexpectedSigningAlgorithm { + received: msg.algorithm, + expected: SigningAlgorithm::Ed25519, + }); + } + + // TODO: possibly provide better error message if this check fails + identity_key.verify(plaintext, &signature)?; + Ok(()) +} + #[cfg(test)] mod tests { use super::*; use cosmwasm_std::coin; use nym_contracts_common::Percent; + use nym_mixnet_contract_common::NodeCostParams; use rand_chacha::rand_core::SeedableRng; use rand_chacha::ChaCha20Rng; @@ -186,7 +234,7 @@ mod tests { identity_key: identity_keypair.public_key().to_base58_string(), version: "v1.2.3".to_string(), }; - let dummy_cost_params = MixNodeCostParams { + let dummy_cost_params = NodeCostParams { profit_margin_percent: Percent::from_percentage_value(42).unwrap(), interval_operating_cost: coin(1111111, "unym"), }; diff --git a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs index 1cf3a7bd69..e624f036d4 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/bond.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/bond.rs @@ -4,15 +4,22 @@ use crate::error::BackendError; use crate::operations::helpers::{ verify_gateway_bonding_sign_payload, verify_mixnode_bonding_sign_payload, + verify_nym_node_bonding_sign_payload, }; use crate::state::WalletState; use crate::{nyxd_client, Gateway, MixNode}; +use log::info; use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::gateway::GatewayConfigUpdate; -use nym_mixnet_contract_common::{MixId, MixNodeConfigUpdate}; +use nym_mixnet_contract_common::nym_node::{NodeConfigUpdate, StakeSaturationResponse}; +use nym_mixnet_contract_common::{MixNodeConfigUpdate, NodeId, NymNode}; +use nym_node_requests::api::client::NymNodeApiClientExt; +use nym_node_requests::api::v1::node::models::NodeDescription; +use nym_node_requests::api::ErrorResponse; use nym_types::currency::DecCoin; use nym_types::gateway::GatewayBond; -use nym_types::mixnode::{MixNodeCostParams, MixNodeDetails}; +use nym_types::mixnode::{MixNodeDetails, NodeCostParams}; +use nym_types::nym_node::NymNodeDetails; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::client::NymApiClientExt; use nym_validator_client::nyxd::contract_traits::{MixnetQueryClient, MixnetSigningClient}; @@ -22,7 +29,7 @@ use std::cmp::Ordering; use std::time::Duration; #[derive(Debug, Serialize, Deserialize)] -pub struct NodeDescription { +pub struct LegacyNodeDescription { name: String, description: String, link: String, @@ -89,7 +96,7 @@ pub async fn unbond_gateway( #[tauri::command] pub async fn bond_mixnode( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, msg_signature: MessageSignature, pledge: DecCoin, fee: Option, @@ -135,6 +142,54 @@ pub async fn bond_mixnode( )?) } +#[tauri::command] +pub async fn bond_nymnode( + nymnode: NymNode, + cost_params: NodeCostParams, + msg_signature: MessageSignature, + pledge: DecCoin, + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.write().await; + let reg = guard.registered_coins()?; + let pledge_base = guard.attempt_convert_to_base_coin(pledge.clone())?; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + let cost_params = cost_params.try_convert_to_mixnet_contract_cost_params(reg)?; + log::info!( + ">>> Bond NymNode: identity_key = {}, pledge_display = {}, pledge_base = {}, fee = {:?}", + nymnode.identity_key, + pledge, + pledge_base, + fee, + ); + + let client = guard.current_client()?; + // check the signature to make sure the user copied it correctly + if let Err(err) = verify_nym_node_bonding_sign_payload( + client, + &nymnode, + &cost_params, + &pledge_base, + &msg_signature, + ) + .await + { + log::warn!("failed to verify provided nymnode bonding signature: {err}"); + return Err(err); + } + + let res = client + .nyxd + .bond_nymnode(nymnode, cost_params, msg_signature, pledge_base, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + #[tauri::command] pub async fn update_pledge( current_pledge: DecCoin, @@ -254,9 +309,26 @@ pub async fn unbond_mixnode( )?) } +#[tauri::command] +pub async fn unbond_nymnode( + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.write().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!(">>> Unbond NymNode, fee = {fee:?}"); + let res = guard.current_client()?.nyxd.unbond_nymnode(fee).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + #[tauri::command] pub async fn update_mixnode_cost_params( - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { @@ -272,7 +344,7 @@ pub async fn update_mixnode_cost_params( let res = guard .current_client()? .nyxd - .update_mixnode_cost_params(cost_params, fee) + .update_cost_params(cost_params, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -331,6 +403,8 @@ pub async fn update_gateway_config( )?) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn get_mixnode_avg_uptime( state: tauri::State<'_, WalletState>, @@ -420,6 +494,37 @@ pub async fn gateway_bond_details( Ok(res) } +#[tauri::command] +pub async fn nym_node_bond_details( + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + log::info!(">>> Get nym-node bond details"); + let guard = state.read().await; + let client = guard.current_client()?; + let res = client + .nyxd + .get_owned_nymnode(&client.nyxd.address()) + .await?; + let details = res + .details + .map(|details| { + guard + .registered_coins() + .map(|reg| NymNodeDetails::from_mixnet_contract_nym_node_details(details, reg)) + }) + .transpose()? + .transpose()?; + log::info!( + "<<< node_id/identity_key = {:?}", + details.as_ref().map(|r| ( + r.bond_information.node_id, + &r.bond_information.node.identity_key + )) + ); + log::trace!("<<< {:?}", details); + Ok(details) +} + #[tauri::command] pub async fn get_pending_operator_rewards( address: String, @@ -459,7 +564,7 @@ pub async fn get_pending_operator_rewards( #[tauri::command] pub async fn get_number_of_mixnode_delegators( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(nyxd_client!(state) @@ -474,7 +579,7 @@ pub async fn get_number_of_mixnode_delegators( pub async fn get_mix_node_description( host: &str, port: u16, -) -> Result { +) -> Result { Ok(reqwest::Client::builder() .timeout(Duration::from_millis(1000)) .build()? @@ -485,9 +590,28 @@ pub async fn get_mix_node_description( .await?) } +#[tauri::command] +pub async fn get_nym_node_description( + host: &str, + port: u16, +) -> Result { + Ok( + nym_node_requests::api::Client::builder::<_, ErrorResponse>(format!( + "http://{host}:{port}" + ))? + .with_timeout(Duration::from_millis(1000)) + .with_user_agent(format!("nym-wallet/{}", env!("CARGO_PKG_VERSION"))) + .build::()? + .get_description() + .await?, + ) +} + +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn get_mixnode_uptime( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { log::info!(">>> Get mixnode uptime"); @@ -499,3 +623,110 @@ pub async fn get_mixnode_uptime( log::info!(">>> Uptime response: {}", uptime.avg_uptime); Ok(uptime.avg_uptime) } + +#[tauri::command] +pub async fn migrate_legacy_mixnode( + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.write().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + + info!(">>> migrate to NymNode, fee = {fee:?}"); + let client = guard.current_client()?; + + let res = client.nyxd.migrate_legacy_mixnode(fee).await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + +#[tauri::command] +pub async fn migrate_legacy_gateway( + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.write().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + + info!(">>> migrate to NymNode, fee = {fee:?}"); + let client = guard.current_client()?; + + let res = client.nyxd.migrate_legacy_gateway(None, fee).await?; + + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + +#[tauri::command] +pub async fn update_nymnode_config( + update: NodeConfigUpdate, + fee: Option, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.write().await; + let fee_amount = guard.convert_tx_fee(fee.as_ref()); + log::info!(">>> update nym node config: update = {update:?}, fee {fee:?}",); + let res = guard + .current_client()? + .nyxd + .update_nymnode_config(update, fee) + .await?; + log::info!("<<< tx hash = {}", res.transaction_hash); + log::trace!("<<< {:?}", res); + Ok(TransactionExecuteResult::from_execute_result( + res, fee_amount, + )?) +} + +#[tauri::command] +pub async fn get_nymnode_performance( + node_id: NodeId, + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + log::trace!(" >>> Get node performance: node_id = {node_id}"); + let guard = state.read().await; + let res = guard + .current_client()? + .nym_api + .get_current_node_performance(node_id) + .await?; + log::trace!(" <<< {res:?}"); + + Ok(res.performance) +} + +#[tauri::command] +pub async fn get_nymnode_uptime( + node_id: NodeId, + state: tauri::State<'_, WalletState>, +) -> Result { + log::info!(">>> Get legacy nymnode uptime"); + + let performance = get_nymnode_performance(node_id, state).await?; + + // convert value in range 0.0 - 1.0 into 0-100 + Ok(performance + .map(|p| (p * 100.).floor() as u8) + .unwrap_or_default()) +} + +#[tauri::command] +pub async fn get_nymnode_stake_saturation( + node_id: NodeId, + state: tauri::State<'_, WalletState>, +) -> Result { + log::trace!(" >>> Get node stake saturation: node_id = {node_id}"); + + let res = nyxd_client!(state) + .get_node_stake_saturation(node_id) + .await?; + log::trace!(" <<< {res:?}"); + + Ok(res) +} diff --git a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs index 7a705f6d03..744aeb1bb7 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/delegate.rs @@ -4,14 +4,16 @@ use crate::error::BackendError; use crate::state::WalletState; use crate::vesting::delegate::vesting_undelegate_from_mixnode; -use nym_mixnet_contract_common::mixnode::StakeSaturationResponse; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::mixnode::MixStakeSaturationResponse; +use nym_mixnet_contract_common::NodeId; use nym_types::currency::DecCoin; -use nym_types::delegation::{Delegation, DelegationWithEverything, DelegationsSummaryResponse}; +use nym_types::delegation::{ + Delegation, DelegationWithEverything, DelegationsSummaryResponse, NodeInformation, +}; use nym_types::deprecated::{ convert_to_delegation_events, DelegationEvent, WrappedDelegationEvent, }; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; use nym_types::pending_events::PendingEpochEvent; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::client::NymApiClientExt; @@ -19,6 +21,7 @@ use nym_validator_client::nyxd::contract_traits::{ MixnetQueryClient, MixnetSigningClient, NymContractsProvider, PagedMixnetQueryClient, }; use nym_validator_client::nyxd::Fee; +use nym_validator_client::DirectSigningHttpRpcValidatorClient; use tap::TapFallible; #[tauri::command] @@ -69,7 +72,7 @@ pub async fn get_pending_delegation_events( #[tauri::command] pub async fn delegate_to_mixnode( - mix_id: MixId, + mix_id: NodeId, amount: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, @@ -86,10 +89,7 @@ pub async fn delegate_to_mixnode( delegation_base, fee, ); - let res = client - .nyxd - .delegate_to_mixnode(mix_id, delegation_base, fee) - .await?; + let res = client.nyxd.delegate(mix_id, delegation_base, fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( @@ -99,7 +99,7 @@ pub async fn delegate_to_mixnode( #[tauri::command] pub async fn undelegate_from_mixnode( - mix_id: MixId, + mix_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { @@ -111,11 +111,7 @@ pub async fn undelegate_from_mixnode( mix_id, fee ); - let res = guard - .current_client()? - .nyxd - .undelegate_from_mixnode(mix_id, fee) - .await?; + let res = guard.current_client()?.nyxd.undelegate(mix_id, fee).await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( @@ -125,7 +121,7 @@ pub async fn undelegate_from_mixnode( #[tauri::command] pub async fn undelegate_all_from_mixnode( - mix_id: MixId, + mix_id: NodeId, uses_vesting_contract_tokens: bool, fee_liquid: Option, fee_vesting: Option, @@ -148,6 +144,60 @@ pub async fn undelegate_all_from_mixnode( Ok(res) } +pub(crate) async fn get_node_information( + client: &DirectSigningHttpRpcValidatorClient, + node_id: NodeId, + error_strings: &mut Vec, +) -> Result, BackendError> { + let native_nymnode = client + .nyxd + .get_nymnode_details(node_id) + .await + .inspect_err(|err| { + let str_err = + format!("Failed to get nymnode details for node_id = {node_id}. Error: {err}"); + log::error!(" <<< {str_err}",); + error_strings.push(str_err); + })?; + + if let Some(native) = native_nymnode.details { + return Ok(Some(NodeInformation { + is_unbonding: native.is_unbonding(), + owner: native.bond_information.owner.to_string(), + mix_id: node_id, + node_identity: native.bond_information.node.identity_key, + rewarding_details: native.rewarding_details, + })); + } + + let legacy_mixnode = client + .nyxd + .get_mixnode_details(node_id) + .await + .inspect_err(|err| { + let str_err = format!( + "Failed to get legacy mixnode details for node_id = {node_id}. Error: {err}", + ); + log::error!(" <<< {}", str_err); + error_strings.push(str_err); + })? + .mixnode_details; + + if let Some(legacy) = legacy_mixnode { + return Ok(Some(NodeInformation { + is_unbonding: legacy.is_unbonding(), + owner: legacy.bond_information.owner.to_string(), + mix_id: node_id, + node_identity: legacy.bond_information.mix_node.identity_key, + rewarding_details: legacy.rewarding_details, + })); + } + + Ok(None) +} + +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn get_all_mix_delegations( state: tauri::State<'_, WalletState>, @@ -195,8 +245,8 @@ pub async fn get_all_mix_delegations( let d = Delegation::from_mixnet_contract(delegation.clone(), reg).tap_err(|err| { log::error!( - " <<< Failed to get delegation for mix id {} from contract. Error: {}", - delegation.mix_id, + " <<< Failed to get delegation for node id {} from contract. Error: {}", + delegation.node_id, err ); })?; @@ -213,21 +263,9 @@ pub async fn get_all_mix_delegations( d.amount ); - let mixnode = client - .nyxd - .get_mixnode_details(d.mix_id) - .await - .tap_err(|err| { - let str_err = format!( - "Failed to get mixnode details for mix_id = {}. Error: {}", - d.mix_id, err - ); - log::error!(" <<< {}", str_err); - error_strings.push(str_err); - })? - .mixnode_details; + let node_details = get_node_information(client, d.mix_id, &mut error_strings).await?; - let accumulated_by_operator = mixnode + let accumulated_by_operator = node_details .as_ref() .map(|m| { guard.display_coin_from_base_decimal(&base_mix_denom, m.rewarding_details.operator) @@ -243,10 +281,13 @@ pub async fn get_all_mix_delegations( }) .unwrap_or_default(); - let accumulated_by_delegates = mixnode + let accumulated_by_delegates = node_details .as_ref() .map(|m| { - guard.display_coin_from_base_decimal(&base_mix_denom, m.rewarding_details.delegates) + reg.attempt_create_display_coin_from_base_dec_amount( + &base_mix_denom, + m.rewarding_details.delegates, + ) }) .transpose() .tap_err(|err| { @@ -259,10 +300,10 @@ pub async fn get_all_mix_delegations( }) .unwrap_or_default(); - let cost_params = mixnode + let cost_params = node_details .as_ref() .map(|m| { - MixNodeCostParams::from_mixnet_contract_mixnode_cost_params( + NodeCostParams::from_mixnet_contract_mixnode_cost_params( m.rewarding_details.cost_params.clone(), reg, ) @@ -329,7 +370,7 @@ pub async fn get_all_mix_delegations( log::error!(" <<< {}", str_err); error_strings.push(str_err); }) - .unwrap_or(StakeSaturationResponse { + .unwrap_or(MixStakeSaturationResponse { mix_id: d.mix_id, uncapped_saturation: None, current_saturation: None, @@ -340,21 +381,26 @@ pub async fn get_all_mix_delegations( " >>> Get average uptime percentage: mix_iid = {}", d.mix_id ); - let avg_uptime_percent = client + + let current_performance = client .nym_api - .get_mixnode_avg_uptime(d.mix_id) + .get_current_node_performance(d.mix_id) .await - .tap_err(|err| { + .inspect_err(|err| { let str_err = format!( - "Failed to get average uptime percentage for mix_id = {}. Error: {}", - d.mix_id, err + "Failed to get current node performance for node_id = {}. Error: {err}", + d.mix_id ); log::error!(" <<< {}", str_err); error_strings.push(str_err); }) .ok() - .map(|r| r.avg_uptime); - log::trace!(" <<< {:?}", avg_uptime_percent); + .and_then(|r| r.performance); + + // convert to old u8 + let current_uptime = current_performance.map(|p| (p * 100.) as u8); + + log::trace!(" <<< {:?}", current_uptime); log::trace!( " >>> Convert delegated on block height to timestamp: block_height = {}", @@ -382,9 +428,9 @@ pub async fn get_all_mix_delegations( pending_events.len() ); - let mixnode_is_unbonding = mixnode.as_ref().map(|m| m.is_unbonding()); + let mixnode_is_unbonding = node_details.as_ref().map(|m| m.is_unbonding); log::trace!( - " >>> mixnode with mix_id: {} is unbonding: {:?}", + " >>> node with mix_id: {} is unbonding: {:?}", d.mix_id, mixnode_is_unbonding ); @@ -392,16 +438,14 @@ pub async fn get_all_mix_delegations( with_everything.push(DelegationWithEverything { owner: d.owner, mix_id: d.mix_id, - node_identity: mixnode - .map(|m| m.bond_information.mix_node.identity_key) - .unwrap_or_default(), + node_identity: node_details.map(|m| m.node_identity).unwrap_or_default(), amount: d.amount, block_height: d.height, uses_vesting_contract_tokens, delegated_on_iso_datetime, stake_saturation: stake_saturation.uncapped_saturation, accumulated_by_operator, - avg_uptime_percent, + avg_uptime_percent: current_uptime, accumulated_by_delegates, cost_params, unclaimed_rewards: accumulated_rewards, @@ -420,7 +464,7 @@ pub async fn get_all_mix_delegations( } fn filter_pending_events( - mix_id: MixId, + mix_id: NodeId, pending_events: &[WrappedDelegationEvent], ) -> Vec { pending_events @@ -434,7 +478,7 @@ fn filter_pending_events( #[tauri::command] pub async fn get_pending_delegator_rewards( address: String, - mix_id: MixId, + mix_id: NodeId, proxy: Option, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs index 266295c6e1..4563005b88 100644 --- a/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/mixnet/rewards.rs @@ -1,7 +1,7 @@ use crate::error::BackendError; use crate::state::WalletState; use crate::vesting::rewards::vesting_claim_delegator_reward; -use nym_mixnet_contract_common::{MixId, RewardingParams}; +use nym_mixnet_contract_common::{NodeId, RewardingParams}; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::nyxd::contract_traits::{ MixnetQueryClient, MixnetSigningClient, NymContractsProvider, PagedMixnetQueryClient, @@ -31,17 +31,17 @@ pub async fn claim_operator_reward( #[tauri::command] pub async fn claim_delegator_reward( - mix_id: MixId, + node_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { - log::info!(">>> Withdraw delegator reward: mix_id = {}", mix_id); + log::info!(">>> Withdraw delegator reward: node_id = {node_id}"); let guard = state.read().await; let fee_amount = guard.convert_tx_fee(fee.as_ref()); let res = guard .current_client()? .nyxd - .withdraw_delegator_reward(mix_id, fee) + .withdraw_delegator_reward(node_id, fee) .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); @@ -52,19 +52,16 @@ pub async fn claim_delegator_reward( #[tauri::command] pub async fn claim_locked_and_unlocked_delegator_reward( - mix_id: MixId, + node_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result, BackendError> { - log::info!( - ">>> Claim delegator reward (locked and unlocked): mix_id = {}", - mix_id - ); + log::info!(">>> Claim delegator reward (locked and unlocked): node_id = {node_id}",); let guard = state.read().await; let client = guard.current_client()?; - log::trace!(">>> Get delegations: mix_id = {}", mix_id); + log::trace!(">>> Get delegations: node_id = {node_id}"); let address = client.nyxd.address(); let delegations = client.nyxd.get_all_delegator_delegations(&address).await?; log::trace!("<<< {} delegations", delegations.len()); @@ -76,11 +73,11 @@ pub async fn claim_locked_and_unlocked_delegator_reward( .to_string(); let liquid_delegation = client .nyxd - .get_delegation_details(mix_id, &address, None) + .get_delegation_details(node_id, &address, None) .await?; let vesting_delegation = client .nyxd - .get_delegation_details(mix_id, &address, Some(vesting_contract)) + .get_delegation_details(node_id, &address, Some(vesting_contract)) .await?; drop(guard); @@ -97,10 +94,10 @@ pub async fn claim_locked_and_unlocked_delegator_reward( let mut res: Vec = vec![]; if did_delegate_with_mixnet_contract { - res.push(claim_delegator_reward(mix_id, fee.clone(), state.clone()).await?); + res.push(claim_delegator_reward(node_id, fee.clone(), state.clone()).await?); } if did_delegate_with_vesting_contract { - res.push(vesting_claim_delegator_reward(mix_id, fee, state).await?); + res.push(vesting_claim_delegator_reward(node_id, fee, state).await?); } log::trace!("<<< {:?}", res); Ok(res) diff --git a/nym-wallet/src-tauri/src/operations/nym_api/status.rs b/nym-wallet/src-tauri/src/operations/nym_api/status.rs index 096615b3b5..a370c8ffd2 100644 --- a/nym-wallet/src-tauri/src/operations/nym_api/status.rs +++ b/nym-wallet/src-tauri/src/operations/nym_api/status.rs @@ -5,18 +5,20 @@ use crate::api_client; use crate::error::BackendError; use crate::state::WalletState; use nym_mixnet_contract_common::{ - reward_params::Performance, Coin, IdentityKeyRef, MixId, Percent, + reward_params::Performance, Coin, IdentityKeyRef, NodeId, Percent, }; use nym_validator_client::client::NymApiClientExt; use nym_validator_client::models::{ - ComputeRewardEstParam, GatewayCoreStatusResponse, GatewayStatusReportResponse, - InclusionProbabilityResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, - RewardEstimationResponse, StakeSaturationResponse, + AnnotationResponse, ComputeRewardEstParam, DisplayRole, GatewayCoreStatusResponse, + GatewayStatusReportResponse, InclusionProbabilityResponse, MixnodeCoreStatusResponse, + MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, }; +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn mixnode_core_node_status( - mix_id: MixId, + mix_id: NodeId, since: Option, state: tauri::State<'_, WalletState>, ) -> Result { @@ -25,6 +27,8 @@ pub async fn mixnode_core_node_status( .await?) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn gateway_core_node_status( identity: IdentityKeyRef<'_>, @@ -36,6 +40,8 @@ pub async fn gateway_core_node_status( .await?) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn gateway_report( identity: IdentityKeyRef<'_>, @@ -44,17 +50,21 @@ pub async fn gateway_report( Ok(api_client!(state).get_gateway_report(identity).await?) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn mixnode_status( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state).get_mixnode_status(mix_id).await?) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn mixnode_reward_estimation( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) @@ -62,6 +72,8 @@ pub async fn mixnode_reward_estimation( .await?) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn compute_mixnode_reward_estimation( mix_id: u32, @@ -85,9 +97,11 @@ pub async fn compute_mixnode_reward_estimation( .await?) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn mixnode_stake_saturation( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) @@ -95,12 +109,31 @@ pub async fn mixnode_stake_saturation( .await?) } +// TODO: fix later (yeah...) +#[allow(deprecated)] #[tauri::command] pub async fn mixnode_inclusion_probability( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { Ok(api_client!(state) .get_mixnode_inclusion_probability(mix_id) .await?) } + +#[tauri::command] +pub async fn get_nymnode_role( + node_id: NodeId, + state: tauri::State<'_, WalletState>, +) -> Result, BackendError> { + let annotation = get_nymnode_annotation(node_id, state).await?; + Ok(annotation.annotation.and_then(|n| n.current_role)) +} + +#[tauri::command] +pub async fn get_nymnode_annotation( + node_id: NodeId, + state: tauri::State<'_, WalletState>, +) -> Result { + Ok(api_client!(state).get_node_annotation(node_id).await?) +} diff --git a/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs b/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs index d539cf5eb3..aa59205169 100644 --- a/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs +++ b/nym-wallet/src-tauri/src/operations/signatures/ed25519_signing_payload.rs @@ -4,15 +4,16 @@ use crate::error::BackendError; use crate::operations::helpers::{ create_gateway_bonding_sign_payload, create_mixnode_bonding_sign_payload, + create_nym_node_bonding_sign_payload, }; use crate::state::WalletState; -use nym_mixnet_contract_common::{Gateway, MixNode}; +use nym_mixnet_contract_common::{Gateway, MixNode, NymNode}; use nym_types::currency::DecCoin; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; async fn mixnode_bonding_msg_payload( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, pledge: DecCoin, vesting: bool, state: tauri::State<'_, WalletState>, @@ -61,10 +62,33 @@ async fn gateway_bonding_msg_payload( Ok(msg.to_base58_string()?) } +async fn nym_node_bonding_msg_payload( + nym_node: NymNode, + cost_params: NodeCostParams, + pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + let guard = state.read().await; + let reg = guard.registered_coins()?; + + let pledge_base = guard.attempt_convert_to_base_coin(pledge.clone())?; + let cost_params = cost_params.try_convert_to_mixnet_contract_cost_params(reg)?; + log::info!( + ">>> Bond nym_node bonding signature: identity_key = {}, pledge_display = {pledge}, pledge_base = {pledge_base}", + nym_node.identity_key, + ); + + let client = guard.current_client()?; + + let msg = + create_nym_node_bonding_sign_payload(client, nym_node, cost_params, pledge_base).await?; + Ok(msg.to_base58_string()?) +} + #[tauri::command] pub async fn generate_mixnode_bonding_msg_payload( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, pledge: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { @@ -74,7 +98,7 @@ pub async fn generate_mixnode_bonding_msg_payload( #[tauri::command] pub async fn vesting_generate_mixnode_bonding_msg_payload( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, pledge: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { @@ -90,6 +114,16 @@ pub async fn generate_gateway_bonding_msg_payload( gateway_bonding_msg_payload(gateway, pledge, false, state).await } +#[tauri::command] +pub async fn generate_nym_node_bonding_msg_payload( + nymnode: NymNode, + cost_params: NodeCostParams, + pledge: DecCoin, + state: tauri::State<'_, WalletState>, +) -> Result { + nym_node_bonding_msg_payload(nymnode, cost_params, pledge, state).await +} + #[tauri::command] pub async fn vesting_generate_gateway_bonding_msg_payload( gateway: Gateway, diff --git a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs index c64fcc53af..733f8cb5f5 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/mixnet.rs @@ -1,17 +1,16 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::cmp::Ordering; - use crate::error::BackendError; use crate::operations::simulate::FeeDetails; use crate::WalletState; use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::{ExecuteMsg, Gateway, MixId, MixNode}; +use nym_mixnet_contract_common::{ExecuteMsg, Gateway, MixNode, NodeId}; use nym_mixnet_contract_common::{GatewayConfigUpdate, MixNodeConfigUpdate}; use nym_types::currency::DecCoin; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; use nym_validator_client::nyxd::contract_traits::NymContractsProvider; +use std::cmp::Ordering; async fn simulate_mixnet_operation( msg: ExecuteMsg, @@ -70,7 +69,7 @@ pub async fn simulate_unbond_gateway( #[tauri::command] pub async fn simulate_bond_mixnode( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, msg_signature: MessageSignature, pledge: DecCoin, state: tauri::State<'_, WalletState>, @@ -148,19 +147,14 @@ pub async fn simulate_unbond_mixnode( #[tauri::command] pub async fn simulate_update_mixnode_cost_params( - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; let reg = guard.registered_coins()?; let new_costs = new_costs.try_convert_to_mixnet_contract_cost_params(reg)?; - simulate_mixnet_operation( - ExecuteMsg::UpdateMixnodeCostParams { new_costs }, - None, - &state, - ) - .await + simulate_mixnet_operation(ExecuteMsg::UpdateCostParams { new_costs }, None, &state).await } #[tauri::command] @@ -190,25 +184,20 @@ pub async fn simulate_update_gateway_config( } #[tauri::command] -pub async fn simulate_delegate_to_mixnode( - mix_id: MixId, +pub async fn simulate_delegate_to_node( + node_id: NodeId, amount: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { - simulate_mixnet_operation( - ExecuteMsg::DelegateToMixnode { mix_id }, - Some(amount), - &state, - ) - .await + simulate_mixnet_operation(ExecuteMsg::Delegate { node_id }, Some(amount), &state).await } #[tauri::command] -pub async fn simulate_undelegate_from_mixnode( - mix_id: MixId, +pub async fn simulate_undelegate_from_node( + node_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { - simulate_mixnet_operation(ExecuteMsg::UndelegateFromMixnode { mix_id }, None, &state).await + simulate_mixnet_operation(ExecuteMsg::Undelegate { node_id }, None, &state).await } #[tauri::command] @@ -220,8 +209,13 @@ pub async fn simulate_claim_operator_reward( #[tauri::command] pub async fn simulate_claim_delegator_reward( - mix_id: MixId, + node_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { - simulate_mixnet_operation(ExecuteMsg::WithdrawDelegatorReward { mix_id }, None, &state).await + simulate_mixnet_operation( + ExecuteMsg::WithdrawDelegatorReward { node_id }, + None, + &state, + ) + .await } diff --git a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs index 9a33e2a3ed..9578575c3f 100644 --- a/nym-wallet/src-tauri/src/operations/simulate/vesting.rs +++ b/nym-wallet/src-tauri/src/operations/simulate/vesting.rs @@ -1,18 +1,17 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::cmp::Ordering; - use crate::error::BackendError; use crate::operations::simulate::FeeDetails; use crate::WalletState; use nym_contracts_common::signing::MessageSignature; -use nym_mixnet_contract_common::{Gateway, MixId, MixNode}; +use nym_mixnet_contract_common::{Gateway, MixNode, NodeId}; use nym_mixnet_contract_common::{GatewayConfigUpdate, MixNodeConfigUpdate}; use nym_types::currency::DecCoin; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; use nym_validator_client::nyxd::contract_traits::NymContractsProvider; use nym_vesting_contract_common::ExecuteMsg; +use std::cmp::Ordering; async fn simulate_vesting_operation( msg: ExecuteMsg, @@ -75,7 +74,7 @@ pub async fn simulate_vesting_unbond_gateway( #[tauri::command] pub async fn simulate_vesting_bond_mixnode( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, msg_signature: MessageSignature, pledge: DecCoin, state: tauri::State<'_, WalletState>, @@ -173,7 +172,7 @@ pub async fn simulate_vesting_unbond_mixnode( #[tauri::command] pub async fn simulate_vesting_update_mixnode_cost_params( - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, state: tauri::State<'_, WalletState>, ) -> Result { let guard = state.read().await; @@ -216,7 +215,7 @@ pub async fn simulate_vesting_update_gateway_config( #[tauri::command] pub async fn simulate_vesting_delegate_to_mixnode( - mix_id: MixId, + mix_id: NodeId, amount: DecCoin, state: tauri::State<'_, WalletState>, ) -> Result { @@ -237,7 +236,7 @@ pub async fn simulate_vesting_delegate_to_mixnode( #[tauri::command] pub async fn simulate_vesting_undelegate_from_mixnode( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { simulate_vesting_operation( @@ -270,7 +269,7 @@ pub async fn simulate_vesting_claim_operator_reward( #[tauri::command] pub async fn simulate_vesting_claim_delegator_reward( - mix_id: MixId, + mix_id: NodeId, state: tauri::State<'_, WalletState>, ) -> Result { simulate_vesting_operation(ExecuteMsg::ClaimDelegatorReward { mix_id }, None, &state).await diff --git a/nym-wallet/src-tauri/src/operations/vesting/bond.rs b/nym-wallet/src-tauri/src/operations/vesting/bond.rs index 8e47953109..dfa7aec1f9 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/bond.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/bond.rs @@ -8,7 +8,7 @@ use crate::{Gateway, MixNode}; use nym_contracts_common::signing::MessageSignature; use nym_mixnet_contract_common::{GatewayConfigUpdate, MixNodeConfigUpdate}; use nym_types::currency::DecCoin; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::nyxd::{contract_traits::VestingSigningClient, Fee}; use std::cmp::Ordering; @@ -76,7 +76,7 @@ pub async fn vesting_unbond_gateway( #[tauri::command] pub async fn vesting_bond_mixnode( mixnode: MixNode, - cost_params: MixNodeCostParams, + cost_params: NodeCostParams, msg_signature: MessageSignature, pledge: DecCoin, fee: Option, @@ -284,7 +284,7 @@ pub async fn withdraw_vested_coins( #[tauri::command] pub async fn vesting_update_mixnode_cost_params( - new_costs: MixNodeCostParams, + new_costs: NodeCostParams, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs index 64ac3ed736..bbb1d8df11 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/delegate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/delegate.rs @@ -3,14 +3,14 @@ use crate::error::BackendError; use crate::state::WalletState; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_types::currency::DecCoin; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::nyxd::{contract_traits::VestingSigningClient, Fee}; #[tauri::command] pub async fn vesting_delegate_to_mixnode( - mix_id: MixId, + mix_id: NodeId, amount: DecCoin, fee: Option, state: tauri::State<'_, WalletState>, @@ -40,7 +40,7 @@ pub async fn vesting_delegate_to_mixnode( #[tauri::command] pub async fn vesting_undelegate_from_mixnode( - mix_id: MixId, + mix_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/operations/vesting/migrate.rs b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs index e5624cfbf4..00257464b1 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/migrate.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/migrate.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::BackendError; -use crate::nyxd_client; use crate::state::WalletState; use nym_mixnet_contract_common::ExecuteMsg; use nym_types::transaction::TransactionExecuteResult; @@ -20,7 +19,11 @@ pub async fn migrate_vested_mixnode( let fee_amount = guard.convert_tx_fee(fee.as_ref()); log::info!(">>> migrate vested mixnode, fee = {fee:?}"); - let res = nyxd_client!(state).migrate_vested_mixnode(fee).await?; + let res = guard + .current_client()? + .nyxd + .migrate_vested_mixnode(fee) + .await?; log::info!("<<< tx hash = {}", res.transaction_hash); log::trace!("<<< {:?}", res); Ok(TransactionExecuteResult::from_execute_result( @@ -68,7 +71,7 @@ pub async fn migrate_vested_delegations( for delegation in &vesting_delegations { migrate_msgs.push(( ExecuteMsg::MigrateVestedDelegation { - mix_id: delegation.mix_id, + mix_id: delegation.node_id, }, Vec::new(), )); diff --git a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs index d990b2c1c5..7cf2155cdb 100644 --- a/nym-wallet/src-tauri/src/operations/vesting/rewards.rs +++ b/nym-wallet/src-tauri/src/operations/vesting/rewards.rs @@ -3,7 +3,7 @@ use crate::error::BackendError; use crate::state::WalletState; -use nym_mixnet_contract_common::MixId; +use nym_mixnet_contract_common::NodeId; use nym_types::transaction::TransactionExecuteResult; use nym_validator_client::nyxd::contract_traits::VestingSigningClient; use nym_validator_client::nyxd::Fee; @@ -30,7 +30,7 @@ pub async fn vesting_claim_operator_reward( #[tauri::command] pub async fn vesting_claim_delegator_reward( - mix_id: MixId, + mix_id: NodeId, fee: Option, state: tauri::State<'_, WalletState>, ) -> Result { diff --git a/nym-wallet/src-tauri/src/utils.rs b/nym-wallet/src-tauri/src/utils.rs index 2a340cc5dd..1c18d0206a 100644 --- a/nym-wallet/src-tauri/src/utils.rs +++ b/nym-wallet/src-tauri/src/utils.rs @@ -5,9 +5,9 @@ use crate::error::BackendError; use crate::nyxd_client; use crate::state::WalletState; use cosmwasm_std::Decimal; -use nym_mixnet_contract_common::{IdentityKey, MixId, Percent}; +use nym_mixnet_contract_common::{IdentityKey, NodeId, Percent}; use nym_types::currency::DecCoin; -use nym_types::mixnode::MixNodeCostParams; +use nym_types::mixnode::NodeCostParams; use nym_validator_client::nyxd::contract_traits::MixnetQueryClient; use nym_wallet_types::app::AppEnv; @@ -46,23 +46,50 @@ pub async fn owns_gateway(state: tauri::State<'_, WalletState>) -> Result) -> Result { + Ok(nyxd_client!(state) + .get_owned_nymnode(&nyxd_client!(state).address()) + .await? + .details + .is_some()) +} + +#[tauri::command] +pub async fn try_convert_pubkey_to_node_id( state: tauri::State<'_, WalletState>, mix_identity: IdentityKey, -) -> Result, BackendError> { - let res = nyxd_client!(state) - .get_mixnode_details_by_identity(mix_identity) - .await?; - Ok(res +) -> Result, BackendError> { + let guard = state.read().await; + let client = guard.current_client()?; + + // first try native nym-node + if let Some(node) = client + .nyxd + .get_nymnode_details_by_identity(mix_identity.clone()) + .await? + .details + { + return Ok(Some(node.node_id())); + } + + // fallback to legacy mixnode + if let Some(node) = client + .nyxd + .get_mixnode_details_by_identity(mix_identity.clone()) + .await? .mixnode_details - .map(|mixnode_details| mixnode_details.mix_id())) + { + return Ok(Some(node.mix_id())); + } + + Ok(None) } #[tauri::command] pub async fn default_mixnode_cost_params( state: tauri::State<'_, WalletState>, profit_margin_percent: Percent, -) -> Result { +) -> Result { // attaches the old pre-update default operating cost of 40 nym per interval let guard = state.read().await; @@ -71,7 +98,7 @@ pub async fn default_mixnode_cost_params( let current_network = guard.current_network(); let denom = current_network.mix_denom().display; - Ok(MixNodeCostParams { + Ok(NodeCostParams { profit_margin_percent, interval_operating_cost: DecCoin { denom: denom.into(), diff --git a/nym-wallet/src-tauri/tauri.conf.json b/nym-wallet/src-tauri/tauri.conf.json index 0067868c37..e493efdfed 100644 --- a/nym-wallet/src-tauri/tauri.conf.json +++ b/nym-wallet/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "package": { "productName": "nym-wallet", - "version": "1.2.14" + "version": "1.2.15" }, "build": { "distDir": "../dist", diff --git a/nym-wallet/src/components/Balance/VestingTimeline.tsx b/nym-wallet/src/components/Balance/VestingTimeline.tsx index 52e137f4c8..cfb489056d 100644 --- a/nym-wallet/src/components/Balance/VestingTimeline.tsx +++ b/nym-wallet/src/components/Balance/VestingTimeline.tsx @@ -26,7 +26,7 @@ export const VestingTimeline: FCWithChildren<{ percentageComplete: number }> = ( const nextPeriod = typeof currentVestingPeriod === 'object' && !!vestingAccountInfo?.periods - ? Number(vestingAccountInfo?.periods[currentVestingPeriod.in + 1]?.start_time) + ? Number(vestingAccountInfo?.periods[currentVestingPeriod.In + 1]?.start_time) : undefined; return ( diff --git a/nym-wallet/src/components/Balance/cards/VestingSchedule.tsx b/nym-wallet/src/components/Balance/cards/VestingSchedule.tsx index e713565680..b26b999970 100644 --- a/nym-wallet/src/components/Balance/cards/VestingSchedule.tsx +++ b/nym-wallet/src/components/Balance/cards/VestingSchedule.tsx @@ -23,7 +23,7 @@ const columnsHeaders: Array<{ title: string; align: TableCellProps['align'] }> = const vestingPeriod = (current?: Period, original?: number) => { if (current === 'After') return 'Complete'; - if (typeof current === 'object' && typeof original === 'number') return `${current.in + 1}/${original}`; + if (typeof current === 'object' && typeof original === 'number') return `${current.In + 1}/${original}`; return 'N/A'; }; diff --git a/nym-wallet/src/components/Bonding/Bond.tsx b/nym-wallet/src/components/Bonding/Bond.tsx index d240b31035..79a19c3396 100644 --- a/nym-wallet/src/components/Bonding/Bond.tsx +++ b/nym-wallet/src/components/Bonding/Bond.tsx @@ -20,7 +20,7 @@ export const Bond = ({ }} > - Bond a mix node or a gateway. Learn how to set up and run a node{' '} + Bond a nym node. Learn how to set up and run a Nym node{' '} here diff --git a/nym-wallet/src/components/Bonding/BondUpdateCard.tsx b/nym-wallet/src/components/Bonding/BondUpdateCard.tsx new file mode 100644 index 0000000000..9cd24d0af8 --- /dev/null +++ b/nym-wallet/src/components/Bonding/BondUpdateCard.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Box, Button, Stack, Tooltip, Typography } from '@mui/material'; +import { NymCard } from 'src/components'; + +export const BondUpdateCard = ({ setSuccesfullUpdate }: { setSuccesfullUpdate: (staus: boolean) => void }) => ( + + + Upgrade your node! + + } + subheader={ + + + It seems like your node is running outdated binaries. + + Update to the latest stable Nym node binary now* + The update takes less than a minute! + + *Without updating, legacy node settings can be changed in the Nym CLI. + + + } + Action={ + + + + + + + + } + /> + +); diff --git a/nym-wallet/src/components/Bonding/BondedGateway.tsx b/nym-wallet/src/components/Bonding/BondedGateway.tsx index 2c1c5f6127..53d0a06b16 100644 --- a/nym-wallet/src/components/Bonding/BondedGateway.tsx +++ b/nym-wallet/src/components/Bonding/BondedGateway.tsx @@ -1,11 +1,13 @@ import React from 'react'; -import { Box, Button, Stack, Typography } from '@mui/material'; +import { Box, Button, Stack, Tooltip, Typography } from '@mui/material'; import { Link } from '@nymproject/react/link/Link'; -import { TBondedGateway, urls } from 'src/context'; +import { urls } from 'src/context'; import { NymCard } from 'src/components'; import { Network } from 'src/types'; import { IdentityKey } from 'src/components/IdentityKey'; import { useNavigate } from 'react-router-dom'; +import { UpgradeRounded } from '@mui/icons-material'; +import { TBondedGateway } from 'src/requests/gatewayDetails'; import { Node as NodeIcon } from '../../svg-icons/node'; import { Cell, Header, NodeTable } from './NodeTable'; import { BondedGatewayActions, TBondedGatwayActions } from './BondedGatewayAction'; @@ -39,10 +41,12 @@ const headers: Header[] = [ export const BondedGateway = ({ gateway, network, + onShowMigrateToNymNodeModal, onActionSelect, }: { gateway: TBondedGateway; network?: Network; + onShowMigrateToNymNodeModal: () => void; onActionSelect: (action: TBondedGatwayActions) => void; }) => { const { name, bond, ip, identityKey, routingScore } = gateway; @@ -91,16 +95,30 @@ export const BondedGateway = ({ } Action={ - + + + + + + - + } > diff --git a/nym-wallet/src/components/Bonding/BondedMixnode.tsx b/nym-wallet/src/components/Bonding/BondedMixnode.tsx index e569cd7aec..6b2977b48e 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnode.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnode.tsx @@ -2,12 +2,14 @@ import React, { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Box, Button, Chip, Stack, Tooltip, Typography } from '@mui/material'; import { Link } from '@nymproject/react/link/Link'; -import { isMixnode, Network } from 'src/types'; -import { TBondedMixnode, urls } from 'src/context'; +import { Network } from 'src/types'; +import { urls } from 'src/context'; import { NymCard } from 'src/components'; import { IdentityKey } from 'src/components/IdentityKey'; import { NodeStatus } from 'src/components/NodeStatus'; import { getIntervalAsDate } from 'src/utils'; +import { UpgradeRounded } from '@mui/icons-material'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; import { Node as NodeIcon } from '../../svg-icons/node'; import { Cell, Header, NodeTable } from './NodeTable'; import { BondedMixnodeActions, TBondedMixnodeActions } from './BondedMixnodeActions'; @@ -60,10 +62,12 @@ const headers: Header[] = [ export const BondedMixnode = ({ mixnode, network, + onShowMigrateToNymNodeModal, onActionSelect, }: { mixnode: TBondedMixnode; network?: Network; + onShowMigrateToNymNodeModal: () => void; onActionSelect: (action: TBondedMixnodeActions) => void; }) => { const [nextEpoch, setNextEpoch] = useState(); @@ -81,6 +85,7 @@ export const BondedMixnode = ({ status, identityKey, host, + isUnbonding, } = mixnode; const getNextInterval = async () => { @@ -165,9 +170,13 @@ export const BondedMixnode = ({ } Action={ - {isMixnode(mixnode) && ( + - )} + + + + + + + {nextEpoch instanceof Error ? null : ( Next epoch starts at {nextEpoch} diff --git a/nym-wallet/src/components/Bonding/BondedNymNode.tsx b/nym-wallet/src/components/Bonding/BondedNymNode.tsx new file mode 100644 index 0000000000..6186891e7f --- /dev/null +++ b/nym-wallet/src/components/Bonding/BondedNymNode.tsx @@ -0,0 +1,206 @@ +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Box, Button, Chip, Stack, Tooltip, Typography } from '@mui/material'; +import { Link } from '@nymproject/react/link/Link'; +import { Network } from 'src/types'; +import { urls } from 'src/context'; +import { NymCard } from 'src/components'; +import { IdentityKey } from 'src/components/IdentityKey'; +import { getIntervalAsDate } from 'src/utils'; +import { TBondedNymNode } from 'src/requests/nymNodeDetails'; +import { Node as NodeIcon } from '../../svg-icons/node'; +import { Cell, Header, NodeTable } from './NodeTable'; +import { BondedNymNodeActions, TBondedNymNodeActions } from './BondedNymNodeActions'; + +const textWhenNotName = 'This node has not yet set a name'; + +const headers: Header[] = [ + { + header: 'Stake', + id: 'stake', + sx: { pl: 0 }, + }, + { + header: 'Bond', + id: 'bond', + }, + { + header: 'Stake saturation', + id: 'stake-saturation', + }, + { + header: 'PM', + id: 'profit-margin', + tooltipText: + 'The percentage of the node rewards that you as the node operator will take before the rest of the reward is shared between you and the delegators.', + }, + { + header: 'Operating cost', + id: 'operator-cost', + tooltipText: + 'Monthly operational costs of running your node. The cost also influences how the rewards are split between you and your delegators. ', + }, + { + header: 'Operator rewards', + id: 'operator-rewards', + tooltipText: + 'This is your (operator) rewards including the PM and cost. Rewards are automatically compounded every epoch. You can redeem your rewards at any time.', + }, + { + header: 'No. delegators', + id: 'delegators', + }, + { + id: 'menu-button', + sx: { width: 34, maxWidth: 34 }, + }, +]; + +export const BondedNymNode = ({ + nymnode, + network, + onActionSelect, +}: { + nymnode: TBondedNymNode; + network?: Network; + onActionSelect: (action: TBondedNymNodeActions) => void; +}) => { + const [nextEpoch, setNextEpoch] = useState(); + const navigate = useNavigate(); + const { + name, + nodeId, + stake, + bond, + stakeSaturation, + profitMargin, + operatorRewards, + operatorCost, + delegators, + identityKey, + host, + } = nymnode; + + const getNextInterval = async () => { + try { + const { nextEpoch: newNextEpoch } = await getIntervalAsDate(); + setNextEpoch(newNextEpoch); + } catch { + setNextEpoch(Error()); + } + }; + const cells: Cell[] = [ + { + cell: `${stake.amount} ${stake.denom}`, + id: 'stake-cell', + }, + { + cell: `${bond.amount} ${bond.denom}`, + id: 'bond-cell', + }, + { + cell: `${stakeSaturation}%`, + id: 'stake-saturation-cell', + }, + { + cell: `${profitMargin}%`, + id: 'pm-cell', + }, + { + cell: operatorCost ? `${operatorCost.amount} ${operatorCost.denom}` : '-', + id: 'operator-cost-cell', + }, + { + cell: operatorRewards ? `${operatorRewards.amount} ${operatorRewards.denom}` : '-', + id: 'operator-rewards-cell', + }, + { + cell: delegators, + id: 'delegators-cell', + }, + { + cell: nymnode.isUnbonding ? ( + + ) : ( + + ), + id: 'actions-cell', + align: 'right', + }, + ]; + + useEffect(() => { + getNextInterval(); + }, []); + + return ( + + + + + Nym node + + + {name?.includes(textWhenNotName) ? null : ( + + {name} + + )} + + + + + + + } + Action={ + + + + + + + + + + {nextEpoch instanceof Error ? null : ( + + Next epoch starts at {nextEpoch} + + )} + + } + > + + {network && ( + + Check more stats of your node on the{' '} + + explorer + + + )} + + {/* */} + + ); +}; diff --git a/nym-wallet/src/components/Bonding/BondedNymNodeActions.tsx b/nym-wallet/src/components/Bonding/BondedNymNodeActions.tsx new file mode 100644 index 0000000000..3125bbd2e1 --- /dev/null +++ b/nym-wallet/src/components/Bonding/BondedNymNodeActions.tsx @@ -0,0 +1,49 @@ +import React, { useState } from 'react'; +import { Typography } from '@mui/material'; +import { ActionsMenu, ActionsMenuItem } from 'src/components/ActionsMenu'; +import { Unbond as UnbondIcon, Bond as BondIcon } from '../../svg-icons'; + +export type TBondedNymNodeActions = 'nodeSettings' | 'updateBond' | 'unbond' | 'redeem'; + +export const BondedNymNodeActions = ({ + onActionSelect, + disabledRedeemAndCompound, + disabledUpdateBond, +}: { + onActionSelect: (action: TBondedNymNodeActions) => void; + disabledRedeemAndCompound: boolean; + disabledUpdateBond?: boolean; +}) => { + const [isOpen, setIsOpen] = useState(false); + + const handleOpen = () => setIsOpen(true); + const handleClose = () => setIsOpen(false); + + const handleActionClick = (action: TBondedNymNodeActions) => { + onActionSelect(action); + handleClose(); + }; + + return ( + + {!disabledUpdateBond && ( + } + onClick={() => handleActionClick('updateBond')} + /> + )} + R} + onClick={() => handleActionClick('redeem')} + disabled={disabledRedeemAndCompound} + /> + } + onClick={() => handleActionClick('unbond')} + /> + + ); +}; diff --git a/nym-wallet/src/components/Bonding/NodeStats.tsx b/nym-wallet/src/components/Bonding/NodeStats.tsx index eb22b50727..8c492968c7 100644 --- a/nym-wallet/src/components/Bonding/NodeStats.tsx +++ b/nym-wallet/src/components/Bonding/NodeStats.tsx @@ -1,9 +1,8 @@ import React from 'react'; -import { Stack, Typography, Box, useTheme, Grid, LinearProgress, LinearProgressProps, Button } from '@mui/material'; -import { useNavigate } from 'react-router-dom'; -import { TBondedMixnode } from 'src/context'; +import { Stack, Typography, Box, useTheme, Grid, LinearProgress, LinearProgressProps } from '@mui/material'; import { Cell, Pie, PieChart, Legend, ResponsiveContainer } from 'recharts'; import { SelectionChance } from '@nymproject/types'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; import { NymCard } from '../NymCard'; import { InfoTooltip } from '../InfoToolTip'; @@ -50,10 +49,9 @@ const StatRow = ({ export const NodeStats = ({ mixnode }: { mixnode: TBondedMixnode }) => { const { activeSetProbability, routingScore } = mixnode; const theme = useTheme(); - const navigate = useNavigate(); // clamp routing score to [0-100] - const score = Math.min(Math.max(routingScore, 0), 100); + const score = Math.min(Math.max(routingScore || 0, 0), 100); const data = [ { key: 'routingScore', value: score }, @@ -74,10 +72,6 @@ export const NodeStats = ({ mixnode }: { mixnode: TBondedMixnode }) => { } }; - const handleGoToTestNode = () => { - navigate('/bonding/node-settings', { state: 'test-node' }); - }; - const renderLegend = () => ( { Node stats } - Action={ - - } > diff --git a/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx b/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx deleted file mode 100644 index 274878310f..0000000000 --- a/nym-wallet/src/components/Bonding/forms/BondMixnodeForm.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import { Box } from '@mui/material'; -import { CurrencyDenom, TNodeType } from '@nymproject/types'; -import { NodeTypeSelector } from 'src/components'; -import { MixnodeAmount, MixnodeData, Signature } from 'src/pages/bonding/types'; -import MixnodeInitForm from './MixnodeInitForm'; -import MixnodeAmountForm from './MixnodeAmountForm'; -import MixnodeSignatureForm from './MixnodeSignatureForm'; - -export const BondMixnodeForm = ({ - step, - denom, - mixnodeData, - amountData, - hasVestingTokens, - onSelectNodeType, - onValidateMixnodeData, - onValidateAmountData, - onValidateSignature, -}: { - step: 1 | 2 | 3 | 4; - mixnodeData: MixnodeData; - amountData: MixnodeAmount; - denom: CurrencyDenom; - hasVestingTokens: boolean; - onSelectNodeType: (nodeType: TNodeType) => void; - onValidateMixnodeData: (data: MixnodeData) => void; - onValidateAmountData: (data: MixnodeAmount) => Promise; - onValidateSignature: (signature: Signature) => void; -}) => ( - <> - {step === 1 && ( - <> - - - - - - )} - {step === 2 && ( - - )} - {step === 3 && } - -); diff --git a/nym-wallet/src/components/Bonding/forms/GatewaySignatureForm.tsx b/nym-wallet/src/components/Bonding/forms/GatewaySignatureForm.tsx deleted file mode 100644 index fd7c25fcea..0000000000 --- a/nym-wallet/src/components/Bonding/forms/GatewaySignatureForm.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Stack, TextField, Typography } from '@mui/material'; -import { useForm } from 'react-hook-form'; -import { gatewayToTauri } from '../utils'; -import { CopyToClipboard } from '../../CopyToClipboard'; -import { useBondingContext } from '../../../context'; -import { Console } from '../../../utils/console'; -import { ErrorModal } from '../../Modals/ErrorModal'; -import { GatewayData, GatewayAmount, Signature } from '../../../pages/bonding/types'; - -const GatewaySignatureForm = ({ - gateway, - amount, - onNext, -}: { - gateway: GatewayData; - amount: GatewayAmount; - onNext: (data: Signature) => void; -}) => { - const [message, setMessage] = useState(); - const [error, setError] = useState(); - const { generateGatewayMsgPayload } = useBondingContext(); - - const { register, handleSubmit } = useForm(); - - const handleOnNext = (event: { detail: { step: number } }) => { - if (event.detail.step === 3) { - handleSubmit(onNext)(); - } - }; - - useEffect(() => { - window.addEventListener('validate_bond_gateway_step' as any, handleOnNext); - return () => window.removeEventListener('validate_bond_gateway_step' as any, handleOnNext); - }, []); - - const generateMessage = async () => { - try { - setMessage( - await generateGatewayMsgPayload({ - pledge: amount.amount, - gateway: gatewayToTauri(gateway), - tokenPool: amount.tokenPool as 'balance' | 'locked', - }), - ); - } catch (e) { - Console.error(e); - setError('Something went wrong while generating the payload signature'); - } - }; - - useEffect(() => { - generateMessage(); - }, [gateway, amount]); - - if (error) { - return {}} />; - } - - return ( - - - Copy the message below and sign it: -
- If you are using a nym-gateway: -
- nym-gateway sign --id <your-node-id> --contract-msg <payload-generated-by-the-wallet> -
- If you are using a nym-node: -
- nym-node sign --id <your-node-id> --contract-msg <payload-generated-by-the-wallet> -
- Then paste the signature in the next field. -
- - - Copy Message - {message && } - - -
- ); -}; - -export default GatewaySignatureForm; diff --git a/nym-wallet/src/components/Bonding/forms/MixnodeSignatureForm.tsx b/nym-wallet/src/components/Bonding/forms/MixnodeSignatureForm.tsx deleted file mode 100644 index 1cce8d2ccd..0000000000 --- a/nym-wallet/src/components/Bonding/forms/MixnodeSignatureForm.tsx +++ /dev/null @@ -1,95 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Stack, TextField, Typography } from '@mui/material'; -import { useForm } from 'react-hook-form'; -import { costParamsToTauri, mixnodeToTauri } from '../utils'; -import { CopyToClipboard } from '../../CopyToClipboard'; -import { useBondingContext } from '../../../context'; -import { Console } from '../../../utils/console'; -import { ErrorModal } from '../../Modals/ErrorModal'; -import { MixnodeAmount, MixnodeData, Signature } from '../../../pages/bonding/types'; - -const MixnodeSignatureForm = ({ - mixnode, - amount, - onNext, -}: { - mixnode: MixnodeData; - amount: MixnodeAmount; - onNext: (data: Signature) => void; -}) => { - const [message, setMessage] = useState(''); - const [error, setError] = useState(); - const { generateMixnodeMsgPayload } = useBondingContext(); - - const { register, handleSubmit } = useForm(); - - const handleOnNext = (event: { detail: { step: number } }) => { - if (event.detail.step === 3) { - handleSubmit(onNext)(); - } - }; - - useEffect(() => { - window.addEventListener('validate_bond_mixnode_step' as any, handleOnNext); - return () => window.removeEventListener('validate_bond_mixnode_step' as any, handleOnNext); - }, []); - - const generateMessage = async () => { - try { - setMessage( - (await generateMixnodeMsgPayload({ - pledge: amount.amount, - mixnode: mixnodeToTauri(mixnode), - costParams: costParamsToTauri(amount), - tokenPool: amount.tokenPool as 'balance' | 'locked', - })) as string, - ); - } catch (e) { - Console.error(e); - setError('Something went wrong while generating the payload signature'); - } - }; - - useEffect(() => { - generateMessage(); - }, [mixnode, amount]); - - if (error) { - return {}} />; - } - - return ( - - - Copy the message below and sign it: -
- If you are using a nym-mixnode: -
- nym-mixnode sign --id <your-node-id> --contract-msg <payload-generated-by-the-wallet> -
- If you are using a nym-node: -
- nym-node sign --id <your-node-id> --contract-msg <payload-generated-by-the-wallet> -
- Then paste the signature in the next field. -
- - - Copy Message - {message && } - - -
- ); -}; - -export default MixnodeSignatureForm; diff --git a/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx b/nym-wallet/src/components/Bonding/forms/legacyForms/BondGatewayForm.tsx similarity index 78% rename from nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx rename to nym-wallet/src/components/Bonding/forms/legacyForms/BondGatewayForm.tsx index efb812d969..843d604053 100644 --- a/nym-wallet/src/components/Bonding/forms/BondGatewayForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/legacyForms/BondGatewayForm.tsx @@ -2,10 +2,9 @@ import React from 'react'; import { Box } from '@mui/material'; import { NodeTypeSelector } from 'src/components'; import { CurrencyDenom, TNodeType } from '@nymproject/types'; -import { GatewayAmount, GatewayData, Signature } from 'src/pages/bonding/types'; +import { GatewayAmount, GatewayData } from 'src/pages/bonding/types'; import GatewayInitForm from './GatewayInitForm'; import GatewayAmountForm from './GatewayAmountForm'; -import GatewaySignatureForm from './GatewaySignatureForm'; export const BondGatewayForm = ({ step, @@ -16,7 +15,6 @@ export const BondGatewayForm = ({ onSelectNodeType, onValidateGatewayData, onValidateAmountData, - onValidateSignature, }: { step: 1 | 2 | 3 | 4; gatewayData: GatewayData; @@ -26,7 +24,6 @@ export const BondGatewayForm = ({ onSelectNodeType: (nodeType: TNodeType) => void; onValidateGatewayData: (data: GatewayData) => void; onValidateAmountData: (data: GatewayAmount) => Promise; - onValidateSignature: (signature: Signature) => void; }) => ( <> {step === 1 && ( @@ -45,6 +42,5 @@ export const BondGatewayForm = ({ onNext={onValidateAmountData} /> )} - {step === 3 && } ); diff --git a/nym-wallet/src/components/Bonding/forms/GatewayAmountForm.tsx b/nym-wallet/src/components/Bonding/forms/legacyForms/GatewayAmountForm.tsx similarity index 94% rename from nym-wallet/src/components/Bonding/forms/GatewayAmountForm.tsx rename to nym-wallet/src/components/Bonding/forms/legacyForms/GatewayAmountForm.tsx index 2cef9a59da..7135d2b154 100644 --- a/nym-wallet/src/components/Bonding/forms/GatewayAmountForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/legacyForms/GatewayAmountForm.tsx @@ -5,9 +5,9 @@ import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; import { Box, Stack } from '@mui/material'; import { amountSchema } from './gatewayValidationSchema'; -import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../utils'; -import { GatewayAmount } from '../../../pages/bonding/types'; -import { TokenPoolSelector } from '../../TokenPoolSelector'; +import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../../utils'; +import { GatewayAmount } from '../../../../pages/bonding/types'; +import { TokenPoolSelector } from '../../../TokenPoolSelector'; const GatewayAmountForm = ({ denom, diff --git a/nym-wallet/src/components/Bonding/forms/GatewayInitForm.tsx b/nym-wallet/src/components/Bonding/forms/legacyForms/GatewayInitForm.tsx similarity index 98% rename from nym-wallet/src/components/Bonding/forms/GatewayInitForm.tsx rename to nym-wallet/src/components/Bonding/forms/legacyForms/GatewayInitForm.tsx index 670c5a9e0a..5a891700c9 100644 --- a/nym-wallet/src/components/Bonding/forms/GatewayInitForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/legacyForms/GatewayInitForm.tsx @@ -4,7 +4,7 @@ import { clean } from 'semver'; import { Checkbox, FormControlLabel, Stack, TextField } from '@mui/material'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; -import { GatewayData } from '../../../pages/bonding/types'; +import { GatewayData } from '../../../../pages/bonding/types'; import { gatewayValidationSchema } from './gatewayValidationSchema'; const GatewayInitForm = ({ diff --git a/nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx b/nym-wallet/src/components/Bonding/forms/legacyForms/MixnodeAmountForm.tsx similarity index 93% rename from nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx rename to nym-wallet/src/components/Bonding/forms/legacyForms/MixnodeAmountForm.tsx index f24f9138b7..13176dadf6 100644 --- a/nym-wallet/src/components/Bonding/forms/MixnodeAmountForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/legacyForms/MixnodeAmountForm.tsx @@ -5,11 +5,11 @@ import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; import { CurrencyDenom } from '@nymproject/types'; import { amountSchema } from './mixnodeValidationSchema'; -import { MixnodeAmount } from '../../../pages/bonding/types'; -import { AppContext } from '../../../context'; -import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../utils'; -import { TokenPoolSelector } from '../../TokenPoolSelector'; -import { ModalListItem } from '../../Modals/ModalListItem'; +import { MixnodeAmount } from '../../../../pages/bonding/types'; +import { AppContext } from '../../../../context'; +import { checkHasEnoughFunds, checkHasEnoughLockedTokens } from '../../../../utils'; +import { TokenPoolSelector } from '../../../TokenPoolSelector'; +import { ModalListItem } from '../../../Modals/ModalListItem'; const MixnodeAmountForm = ({ amountData, diff --git a/nym-wallet/src/components/Bonding/forms/MixnodeInitForm.tsx b/nym-wallet/src/components/Bonding/forms/legacyForms/MixnodeInitForm.tsx similarity index 98% rename from nym-wallet/src/components/Bonding/forms/MixnodeInitForm.tsx rename to nym-wallet/src/components/Bonding/forms/legacyForms/MixnodeInitForm.tsx index 3c97bb22ce..0032bb3364 100644 --- a/nym-wallet/src/components/Bonding/forms/MixnodeInitForm.tsx +++ b/nym-wallet/src/components/Bonding/forms/legacyForms/MixnodeInitForm.tsx @@ -5,7 +5,7 @@ import { Checkbox, FormControlLabel, Stack, TextField } from '@mui/material'; import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; import { yupResolver } from '@hookform/resolvers/yup/dist/yup'; import { mixnodeValidationSchema } from './mixnodeValidationSchema'; -import { MixnodeData } from '../../../pages/bonding/types'; +import { MixnodeData } from '../../../../pages/bonding/types'; const MixnodeInitForm = ({ mixnodeData, onNext }: { mixnodeData: MixnodeData; onNext: (data: any) => void }) => { const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); diff --git a/nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/legacyForms/gatewayValidationSchema.ts similarity index 100% rename from nym-wallet/src/components/Bonding/forms/gatewayValidationSchema.ts rename to nym-wallet/src/components/Bonding/forms/legacyForms/gatewayValidationSchema.ts diff --git a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/legacyForms/mixnodeValidationSchema.ts similarity index 98% rename from nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts rename to nym-wallet/src/components/Bonding/forms/legacyForms/mixnodeValidationSchema.ts index a72364377c..c11324457c 100644 --- a/nym-wallet/src/components/Bonding/forms/mixnodeValidationSchema.ts +++ b/nym-wallet/src/components/Bonding/forms/legacyForms/mixnodeValidationSchema.ts @@ -8,7 +8,7 @@ import { validateRawPort, validateVersion, } from 'src/utils'; -import { TauriContractStateParams } from '../../../types'; +import { TauriContractStateParams } from '../../../../types'; export const mixnodeValidationSchema = Yup.object().shape({ identityKey: Yup.string() diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx b/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx new file mode 100644 index 0000000000..d3f95006f6 --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/nym-node/FormContext.tsx @@ -0,0 +1,97 @@ +import React, { createContext, useContext, useMemo, useState } from 'react'; +import { CurrencyDenom } from '@nymproject/types'; +import { TBondNymNodeArgs, TBondMixNodeArgs } from 'src/types'; + +const defaultNymNodeValues: TBondNymNodeArgs['nymnode'] = { + identity_key: '', + custom_http_port: null, + host: '1.1.1.1', +}; + +const defaultCostParams = (denom: CurrencyDenom): TBondNymNodeArgs['costParams'] => ({ + interval_operating_cost: { amount: '40', denom }, + profit_margin_percent: '40', +}); + +const defaultAmount = (denom: CurrencyDenom): TBondMixNodeArgs['pledge'] => ({ + amount: '100', + denom, +}); + +interface FormContextType { + step: 1 | 2 | 3 | 4; + setStep: React.Dispatch>; + nymNodeData: TBondNymNodeArgs['nymnode']; + setNymNodeData: React.Dispatch>; + costParams: TBondNymNodeArgs['costParams']; + setCostParams: React.Dispatch>; + amountData: TBondMixNodeArgs['pledge']; + setAmountData: React.Dispatch>; + signature: string; + setSignature: React.Dispatch>; + onError: (e: string) => void; +} + +const FormContext = createContext({ + step: 1, + setStep: () => {}, + nymNodeData: defaultNymNodeValues, + setNymNodeData: () => {}, + costParams: defaultCostParams('nym'), + setCostParams: () => {}, + amountData: defaultAmount('nym'), + setAmountData: () => {}, + signature: '', + setSignature: () => {}, + onError: () => {}, +}); + +const FormContextProvider = ({ children }: { children: React.ReactNode }) => { + // TODO - Make denom dynamic + const denom = 'nym'; + + const [step, setStep] = useState<1 | 2 | 3 | 4>(1); + const [nymNodeData, setNymNodeData] = useState(defaultNymNodeValues); + const [costParams, setCostParams] = useState(defaultCostParams(denom)); + const [amountData, setAmountData] = useState(defaultAmount(denom)); + const [signature, setSignature] = useState(''); + + const onError = (e: string) => { + console.error(e); + }; + + const value = useMemo( + () => ({ + step, + setStep, + nymNodeData, + setNymNodeData, + costParams, + setCostParams, + amountData, + setAmountData, + signature, + setSignature, + onError, + }), + [ + step, + setStep, + nymNodeData, + setNymNodeData, + costParams, + setCostParams, + amountData, + setAmountData, + signature, + setSignature, + onError, + ], + ); + + return {children}; +}; + +export const useFormContext = () => useContext(FormContext); + +export default FormContextProvider; diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeAmount.tsx b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeAmount.tsx new file mode 100644 index 0000000000..e95d0ae934 --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeAmount.tsx @@ -0,0 +1,116 @@ +import React from 'react'; +import { Stack, TextField, Box, FormHelperText } from '@mui/material'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; +import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; +import { checkHasEnoughFunds } from 'src/utils'; +import { nymNodeAmountSchema } from './amountValidationSchema'; +import { useFormContext } from './FormContext'; + +type NymNodeDataProps = { + onClose: () => void; + onBack: () => void; + onNext: () => Promise; + step: number; +}; + +const NymNodeAmount = ({ onClose, onBack, onNext, step }: NymNodeDataProps) => { + const { setAmountData, setCostParams, amountData, costParams } = useFormContext(); + const { + formState: { errors }, + register, + getValues, + setValue, + setError, + handleSubmit, + } = useForm({ + mode: 'all', + defaultValues: { + pledge: amountData, + ...costParams, + }, + resolver: yupResolver(nymNodeAmountSchema()), + }); + + const handleRequestValidation = async () => { + const values = getValues(); + + const hasSufficientTokens = await checkHasEnoughFunds(values.pledge.amount); + + if (hasSufficientTokens) { + handleSubmit((args) => { + setAmountData(args.pledge); + setCostParams({ + profit_margin_percent: args.profit_margin_percent, + interval_operating_cost: args.interval_operating_cost, + }); + onNext(); + })(); + } else { + setError('pledge.amount', { message: 'Not enough tokens' }); + } + }; + + return ( + 0} + > + + { + setValue('pledge.amount', newValue.amount, { shouldValidate: true }); + }} + validationError={errors.pledge?.amount?.message} + denom={amountData.denom} + initialValue={amountData.amount} + /> + + + { + setValue('interval_operating_cost', newValue, { shouldValidate: true }); + }} + validationError={errors.interval_operating_cost?.amount?.message} + denom={costParams.interval_operating_cost.denom} + initialValue={costParams.interval_operating_cost.amount} + /> + + Monthly operational costs of running your node. If your node is in the active set the amount will be paid + back to you from the rewards. + + + + + + The percentage of node rewards that you as the node operator take before rewards are distributed to operator + and delegators. + + + + + ); +}; + +export default NymNodeAmount; diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeData.tsx b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeData.tsx new file mode 100644 index 0000000000..4fc8d7fc98 --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeData.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import * as Yup from 'yup'; +import { Stack, TextField, FormControlLabel, Checkbox } from '@mui/material'; +import { useForm } from 'react-hook-form'; +import { IdentityKeyFormField } from '@nymproject/react/mixnodes/IdentityKeyFormField'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; +import { useFormContext } from './FormContext'; +import { settingsValidationSchema } from './settingsValidationSchema'; + +type NymNodeDataProps = { + onClose: () => void; + onBack: () => void; + onNext: () => Promise; + step: number; +}; + +const validationSchema = Yup.object().shape({ + identity_key: Yup.string().required('Identity key is required'), + ...settingsValidationSchema.fields, +}); + +const NymNodeData = ({ onClose, onNext, step }: NymNodeDataProps) => { + const { setNymNodeData, nymNodeData } = useFormContext(); + const { + formState: { errors }, + register, + setValue, + handleSubmit, + } = useForm({ + mode: 'all', + defaultValues: nymNodeData, + resolver: yupResolver(validationSchema), + }); + + const [showAdvancedOptions, setShowAdvancedOptions] = React.useState(false); + + const handleNext = async () => { + handleSubmit((args) => { + setNymNodeData(args); + onNext(); + })(); + }; + + return ( + 0} + > + + setValue('identity_key', value, { shouldValidate: true })} + showTickOnValid={false} + /> + + + + setShowAdvancedOptions((show) => !show)} checked={showAdvancedOptions} />} + label="Show advanced options" + /> + {showAdvancedOptions && ( + + + + )} + + + ); +}; + +export default NymNodeData; diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeSignature.tsx b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeSignature.tsx new file mode 100644 index 0000000000..c4c809ca82 --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/nym-node/NymNodeSignature.tsx @@ -0,0 +1,124 @@ +import React, { useEffect, useState } from 'react'; +import * as yup from 'yup'; +import { Stack, TextField, Typography } from '@mui/material'; +import { useForm } from 'react-hook-form'; +import { CopyToClipboard } from 'src/components/CopyToClipboard'; +import { ErrorModal } from 'src/components/Modals/ErrorModal'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; +import { useBondingContext } from 'src/context'; +import { TBondNymNodeArgs } from 'src/types'; +import { Signature } from 'src/pages/bonding/types'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { useFormContext } from './FormContext'; + +const NymNodeSignature = ({ + nymnode, + pledge, + costParams, + step, + onNext, + onClose, + onBack, +}: { + nymnode: TBondNymNodeArgs['nymnode']; + pledge: TBondNymNodeArgs['pledge']; + costParams: TBondNymNodeArgs['costParams']; + step: number; + onNext: () => void; + onClose: () => void; + onBack: () => void; +}) => { + const [message, setMessage] = useState(''); + const [error, setError] = useState(); + const { generateNymNodeMsgPayload } = useBondingContext(); + const { signature, setSignature } = useFormContext(); + + const yupValidationSchema = yup.object().shape({ + signature: yup.string().required('Signature is required'), + }); + + const { + register, + formState: { errors }, + handleSubmit, + } = useForm({ + defaultValues: { + signature, + }, + resolver: yupResolver(yupValidationSchema), + }); + + const generateMessage = async () => { + try { + const msg = await generateNymNodeMsgPayload({ + nymnode, + pledge, + costParams, + }); + + if (msg) { + setMessage(msg); + } + } catch (e) { + console.error(e); + setError('Something went wrong while generating the payload signature'); + } + }; + + useEffect(() => { + generateMessage(); + }, []); + + const handleNext = async () => { + handleSubmit(onNext)(); + }; + + if (error) { + return {}} />; + } + + return ( + 0} + > + + + Copy the message below and sign it: +
+ If you are using a nym-node: +
+ nym-node sign --id <your-node-id> --contract-msg <payload-generated-by-the-wallet> +
+ Then paste the signature in the next field. +
+ + + Copy Message + {message && } + + setSignature(e.target.value)} + id="outlined-multiline-static" + name="signature" + rows={3} + placeholder="Paste Signature" + helperText={errors.signature?.message} + error={Boolean(errors.signature)} + multiline + fullWidth + required + /> +
+
+ ); +}; + +export default NymNodeSignature; diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/amountValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/nym-node/amountValidationSchema.ts new file mode 100644 index 0000000000..93ab8f1478 --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/nym-node/amountValidationSchema.ts @@ -0,0 +1,58 @@ +import * as Yup from 'yup'; +import { TauriContractStateParams } from 'src/types'; +import { isLessThan, isGreaterThan, validateAmount } from 'src/utils'; + +const operatingCostAndPmValidation = (params?: TauriContractStateParams) => { + const defaultParams = { + profit_margin_percent: { + minimum: parseFloat(params?.profit_margin.minimum || '20%'), + maximum: parseFloat(params?.profit_margin.maximum || '50%'), + }, + + interval_operating_cost: { + minimum: parseFloat(params?.operating_cost.minimum.amount || '0'), + maximum: parseFloat(params?.operating_cost.maximum.amount || '1000000000'), + }, + }; + + return { + profit_margin_percent: Yup.number() + .required('Profit Percentage is required') + .min(defaultParams.profit_margin_percent.minimum) + .max(defaultParams.profit_margin_percent.maximum), + interval_operating_cost: Yup.object().shape({ + amount: Yup.string() + .required('An operating cost is required') + // eslint-disable-next-line prefer-arrow-callback + .test('valid-operating-cost', 'A valid amount is required', async function isValidAmount(this, value) { + if ( + value && + (!Number(value) || + isLessThan(+value, defaultParams.interval_operating_cost.minimum) || + isGreaterThan(+value, Number(defaultParams.interval_operating_cost.maximum))) + ) { + return this.createError({ + message: `A valid amount is required (min ${defaultParams?.interval_operating_cost.minimum} - max ${defaultParams?.interval_operating_cost.maximum})`, + }); + } + return true; + }), + }), + }; +}; + +export const nymNodeAmountSchema = (params?: TauriContractStateParams) => + Yup.object().shape({ + pledge: Yup.object().shape({ + amount: Yup.string() + .required('An amount is required') + .test('valid-amount', 'Pledge error', async function isValidAmount(this, value) { + const isValid = await validateAmount(value || '', '100'); + if (!isValid) { + return this.createError({ message: 'A valid amount is required (min 100)' }); + } + return true; + }), + }), + ...operatingCostAndPmValidation(params), + }); diff --git a/nym-wallet/src/components/Bonding/forms/nym-node/settingsValidationSchema.ts b/nym-wallet/src/components/Bonding/forms/nym-node/settingsValidationSchema.ts new file mode 100644 index 0000000000..eef46e37c7 --- /dev/null +++ b/nym-wallet/src/components/Bonding/forms/nym-node/settingsValidationSchema.ts @@ -0,0 +1,29 @@ +import { isValidHostname, validateRawPort } from 'src/utils'; +import * as Yup from 'yup'; + +const settingsValidationSchema = Yup.object().shape({ + host: Yup.string() + .required('A host is required') + .test('no-whitespace', 'Host cannot contain whitespace', (value) => !/\s/.test(value || '')) + .test('valid-host', 'A valid host is required', (value) => (value ? isValidHostname(value) : false)), + + custom_http_port: Yup.number() + .nullable() + .transform((numberVal, stringVal) => { + if (stringVal === '') { + return null; + } + if (!Number(stringVal)) { + return stringVal; + } + return numberVal; + }) + .test('valid-http', 'A valid http port is required', (value) => { + if (value === null) { + return true; + } + return value ? validateRawPort(value) : false; + }), +}); + +export { settingsValidationSchema }; diff --git a/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx b/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx index defdac3f95..1060b7c533 100644 --- a/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/BondGatewayModal.tsx @@ -4,15 +4,11 @@ import { CurrencyDenom, TNodeType } from '@nymproject/types'; import { ConfirmTx } from 'src/components/ConfirmTX'; import { ModalListItem } from 'src/components/Modals/ModalListItem'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; -import { TPoolOption } from 'src/components/TokenPoolSelector'; import { useGetFee } from 'src/hooks/useGetFee'; -import { GatewayAmount, GatewayData, Signature } from 'src/pages/bonding/types'; -import { simulateBondGateway, simulateVestingBondGateway } from 'src/requests'; -import { TBondGatewayArgs } from 'src/types'; +import { GatewayAmount, GatewayData } from 'src/pages/bonding/types'; import { BalanceWarning } from 'src/components/FeeWarning'; import { AppContext } from 'src/context'; -import { BondGatewayForm } from '../forms/BondGatewayForm'; -import { gatewayToTauri } from '../utils'; +import { BondGatewayForm } from '../forms/legacyForms/BondGatewayForm'; const defaultGatewayValues: GatewayData = { identityKey: '', @@ -34,14 +30,12 @@ const defaultAmountValues = (denom: CurrencyDenom) => ({ export const BondGatewayModal = ({ denom, hasVestingTokens, - onBondGateway, onSelectNodeType, onClose, onError, }: { denom: CurrencyDenom; hasVestingTokens: boolean; - onBondGateway: (data: TBondGatewayArgs, tokenPool: TPoolOption) => void; onSelectNodeType: (type: TNodeType) => void; onClose: () => void; onError: (e: string) => void; @@ -49,9 +43,8 @@ export const BondGatewayModal = ({ const [step, setStep] = useState<1 | 2 | 3>(1); const [gatewayData, setGatewayData] = useState(defaultGatewayValues); const [amountData, setAmountData] = useState(defaultAmountValues(denom)); - const [signature, setSignature] = useState(); - const { fee, getFee, resetFeeState, feeError } = useGetFee(); + const { fee, resetFeeState, feeError } = useGetFee(); const { userBalance } = useContext(AppContext); useEffect(() => { @@ -83,32 +76,7 @@ export const BondGatewayModal = ({ setStep(3); }; - const handleUpdateSignature = async (data: Signature) => { - setSignature(data.signature); - - const payload = { - pledge: amountData.amount, - msgSignature: data.signature, - gateway: gatewayToTauri(gatewayData), - }; - - if (amountData.tokenPool === 'balance') { - await getFee(simulateBondGateway, payload); - } else { - await getFee(simulateVestingBondGateway, payload); - } - }; - - const handleConfirm = async () => { - await onBondGateway( - { - pledge: amountData.amount, - msgSignature: signature as string, - gateway: gatewayToTauri(gatewayData), - }, - amountData.tokenPool as TPoolOption, - ); - }; + const handleConfirm = async () => {}; if (fee) { return ( @@ -154,7 +122,6 @@ export const BondGatewayModal = ({ hasVestingTokens={hasVestingTokens} onValidateGatewayData={handleUpdateGatwayData} onValidateAmountData={handleUpdateAmountData} - onValidateSignature={handleUpdateSignature} onSelectNodeType={onSelectNodeType} />
diff --git a/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx b/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx deleted file mode 100644 index 4e4c39f9cf..0000000000 --- a/nym-wallet/src/components/Bonding/modals/BondMixnodeModal.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import React, { useContext, useEffect, useState } from 'react'; -import { CurrencyDenom, TNodeType } from '@nymproject/types'; -import { ConfirmTx } from 'src/components/ConfirmTX'; -import { ModalListItem } from 'src/components/Modals/ModalListItem'; -import { SimpleModal } from 'src/components/Modals/SimpleModal'; -import { TPoolOption } from 'src/components/TokenPoolSelector'; -import { useGetFee } from 'src/hooks/useGetFee'; -import { MixnodeAmount, MixnodeData, Signature } from 'src/pages/bonding/types'; -import { simulateBondMixnode, simulateVestingBondMixnode } from 'src/requests'; -import { TBondMixNodeArgs } from 'src/types'; -import { BalanceWarning } from 'src/components/FeeWarning'; -import { AppContext } from 'src/context'; -import { BondMixnodeForm } from '../forms/BondMixnodeForm'; -import { costParamsToTauri, mixnodeToTauri } from '../utils'; - -const defaultMixnodeValues: MixnodeData = { - identityKey: '', - sphinxKey: '', - ownerSignature: '', - host: '', - version: '', - mixPort: 1789, - verlocPort: 1790, - httpApiPort: 8000, -}; - -const defaultAmountValues = (denom: CurrencyDenom) => ({ - amount: { amount: '100', denom }, - operatorCost: { amount: '40', denom }, - profitMargin: '10', - tokenPool: 'balance', -}); - -export const BondMixnodeModal = ({ - denom, - hasVestingTokens, - onBondMixnode, - onSelectNodeType, - onClose, - onError, -}: { - denom: CurrencyDenom; - hasVestingTokens: boolean; - onBondMixnode: (data: TBondMixNodeArgs, tokenPool: TPoolOption) => void; - onSelectNodeType: (type: TNodeType) => void; - onClose: () => void; - onError: (e: string) => void; -}) => { - const [step, setStep] = useState<1 | 2 | 3 | 4>(1); - const [mixnodeData, setMixnodeData] = useState(defaultMixnodeValues); - const [amountData, setAmountData] = useState(defaultAmountValues(denom)); - const [signature, setSignature] = useState(); - - const { fee, getFee, resetFeeState, feeError } = useGetFee(); - const { userBalance } = useContext(AppContext); - - useEffect(() => { - if (feeError) { - onError(feeError); - } - }, [feeError]); - - const validateStep = async (s: number) => { - const event = new CustomEvent('validate_bond_mixnode_step', { detail: { step: s } }); - window.dispatchEvent(event); - }; - - const handleBack = () => { - if (step === 2) { - setStep(1); - } else if (step === 3) { - setStep(2); - } - }; - - const handleUpdateMixnodeData = (data: MixnodeData) => { - setMixnodeData(data); - setStep(2); - }; - - const handleUpdateAmountData = async (data: MixnodeAmount) => { - setAmountData({ ...data }); - setStep(3); - }; - - const handleUpdateSignature = async (data: Signature) => { - setSignature(data.signature); - - const payload = { - pledge: amountData.amount, - msgSignature: data.signature, - mixnode: mixnodeToTauri(mixnodeData), - costParams: costParamsToTauri(amountData), - }; - - if (amountData.tokenPool === 'balance') { - await getFee(simulateBondMixnode, payload); - } else { - await getFee(simulateVestingBondMixnode, payload); - } - }; - - const handleConfirm = async () => { - await onBondMixnode( - { - pledge: amountData.amount, - msgSignature: signature as string, - mixnode: mixnodeToTauri(mixnodeData), - costParams: costParamsToTauri(amountData), - }, - amountData.tokenPool as TPoolOption, - ); - }; - - if (fee) { - return ( - - - - {fee.amount?.amount && userBalance.balance && ( - - )} - - ); - } - - return ( - { - await validateStep(step); - }} - onBack={step === 2 || step === 3 ? handleBack : undefined} - onClose={onClose} - header="Bond mixnode" - subHeader={`Step ${step}/3`} - okLabel="Next" - > - - - ); -}; diff --git a/nym-wallet/src/components/Bonding/modals/BondNymNodeModal.tsx b/nym-wallet/src/components/Bonding/modals/BondNymNodeModal.tsx new file mode 100644 index 0000000000..f6288889ab --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/BondNymNodeModal.tsx @@ -0,0 +1,107 @@ +import React, { useContext, useEffect } from 'react'; +import { ConfirmTx } from 'src/components/ConfirmTX'; +import { ModalListItem } from 'src/components/Modals/ModalListItem'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { BalanceWarning } from 'src/components/FeeWarning'; +import { AppContext } from 'src/context'; +import { TBondNymNodeArgs } from 'src/types'; +import FormContextProvider, { useFormContext } from '../forms/nym-node/FormContext'; +import NymNodeData from '../forms/nym-node/NymNodeData'; +import NymNodeAmount from '../forms/nym-node/NymNodeAmount'; +import NymNodeSignature from '../forms/nym-node/NymNodeSignature'; + +export const BondNymNodeModal = ({ + onClose, + onBond, +}: { + onClose: () => void; + onBond: (data: TBondNymNodeArgs) => Promise; +}) => { + const { fee, resetFeeState, feeError } = useGetFee(); + const { userBalance } = useContext(AppContext); + const { setStep, step, onError, signature, amountData, costParams, nymNodeData } = useFormContext(); + + useEffect(() => { + if (feeError) { + onError(feeError); + } + }, [feeError]); + + const handleUpdateNymnodeData = async () => { + setStep(2); + }; + + const handleBond = async () => { + onBond({ + nymnode: nymNodeData, + pledge: amountData, + costParams, + msgSignature: signature, + }); + }; + + const handleConfirm = async () => {}; + + if (fee) { + return ( + + + + {fee.amount?.amount && userBalance.balance && ( + + )} + + ); + } + + if (step === 1) { + return ; + } + + if (step === 2) { + return setStep(1)} onNext={async () => setStep(3)} step={step} />; + } + + if (step === 3) { + return ( + setStep(2)} + step={step} + /> + ); + } + + return null; +}; + +export const BondNymNode = ({ + open, + onClose, + onBond, +}: { + open: boolean; + onClose: () => void; + onBond: (data: TBondNymNodeArgs) => Promise; +}) => { + if (!open) { + return null; + } + + return ( + + + + ); +}; diff --git a/nym-wallet/src/components/Bonding/modals/MigrateLegacyNode.tsx b/nym-wallet/src/components/Bonding/modals/MigrateLegacyNode.tsx new file mode 100644 index 0000000000..1a04601200 --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/MigrateLegacyNode.tsx @@ -0,0 +1,28 @@ +import React from 'react'; +import { Box, Typography } from '@mui/material'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; + +const MigrateLegacyNode = ({ + open, + onClose, + handleMigrate, +}: { + open: boolean; + onClose: () => void; + handleMigrate: () => Promise; +}) => ( + Migrate Legacy Node} + onOk={handleMigrate} + okLabel="Migrate" + onClose={onClose} + sx={{ maxWidth: 500 }} + > + + You have a legacy node that needs to be migrated to the new system. + + +); + +export default MigrateLegacyNode; diff --git a/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx b/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx index 0aa53e7368..8d87405d13 100644 --- a/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/NodeSettingsModal.tsx @@ -2,7 +2,6 @@ import React, { useEffect, useState } from 'react'; import { Box, Button, FormHelperText, TextField, Typography } from '@mui/material'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; import { Node as NodeIcon } from 'src/svg-icons/node'; -import { TBondedMixnode } from 'src/context'; import { Tabs } from 'src/components/Tabs'; import { ModalListItem } from 'src/components/Modals/ModalListItem'; import { attachDefaultOperatingCost, isDecimal, toPercentFloatString } from 'src/utils'; @@ -11,6 +10,7 @@ import { ConfirmTx } from 'src/components/ConfirmTX'; import { simulateUpdateMixnodeCostParams, simulateVestingUpdateMixnodeCostParams } from 'src/requests'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { FeeDetails } from '@nymproject/types'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; // Now we are using the node setting page instead of this modal export const NodeSettings = ({ diff --git a/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx b/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx index 11279278f6..0243e765f3 100644 --- a/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/RedeemRewardsModal.tsx @@ -4,10 +4,10 @@ import { ModalListItem } from 'src/components/Modals/ModalListItem'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; import { ModalFee } from 'src/components/Modals/ModalFee'; import { useGetFee } from 'src/hooks/useGetFee'; -import { simulateClaimOperatorReward, simulateVestingClaimOperatorReward } from 'src/requests'; -import { AppContext, TBondedMixnode } from 'src/context'; +import { AppContext } from 'src/context'; import { BalanceWarning } from 'src/components/FeeWarning'; import { Box } from '@mui/material'; +import { TBondedNymNode } from 'src/requests/nymNodeDetails'; export const RedeemRewardsModal = ({ node, @@ -15,23 +15,18 @@ export const RedeemRewardsModal = ({ onError, onClose, }: { - node: TBondedMixnode; + node: TBondedNymNode; onConfirm: (fee?: FeeDetails) => Promise; onError: (err: string) => void; onClose: () => void; }) => { - const { fee, getFee, isFeeLoading, feeError } = useGetFee(); + const { fee, isFeeLoading, feeError } = useGetFee(); const { userBalance } = useContext(AppContext); useEffect(() => { if (feeError) onError(feeError); }, [feeError]); - useEffect(() => { - if (node.proxy) getFee(simulateVestingClaimOperatorReward, {}); - else getFee(simulateClaimOperatorReward, {}); - }, []); - const handleOnOK = async () => onConfirm(fee); return ( diff --git a/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx b/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx index 41eeb45211..de2fbdd1ca 100644 --- a/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/UnbondModal.tsx @@ -1,7 +1,7 @@ import * as React from 'react'; import { useEffect } from 'react'; import { Typography } from '@mui/material'; -import { TBondedGateway, TBondedMixnode } from 'src/context'; +import { TBondedNode } from 'src/context'; import { useGetFee } from 'src/hooks/useGetFee'; import { isGateway, isMixnode } from 'src/types'; import { ModalFee } from '../../Modals/ModalFee'; @@ -15,7 +15,7 @@ import { } from '../../../requests'; interface Props { - node: TBondedMixnode | TBondedGateway; + node: TBondedNode; onConfirm: () => Promise; onClose: () => void; onError: (e: string) => void; diff --git a/nym-wallet/src/components/Bonding/modals/UpdateBondAmountModal.tsx b/nym-wallet/src/components/Bonding/modals/UpdateBondAmountModal.tsx index 2148313b70..978675bd9b 100644 --- a/nym-wallet/src/components/Bonding/modals/UpdateBondAmountModal.tsx +++ b/nym-wallet/src/components/Bonding/modals/UpdateBondAmountModal.tsx @@ -10,8 +10,9 @@ import { useGetFee } from 'src/hooks/useGetFee'; import { decCoinToDisplay, validateAmount } from 'src/utils'; import { simulateUpdateBond, simulateVestingUpdateBond } from 'src/requests'; import { TSimulateUpdateBondArgs, TUpdateBondArgs } from 'src/types'; -import { AppContext, TBondedMixnode } from 'src/context'; +import { AppContext } from 'src/context'; import { BalanceWarning } from 'src/components/FeeWarning'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; import { TPoolOption } from '../../TokenPoolSelector'; export const UpdateBondAmountModal = ({ diff --git a/nym-wallet/src/components/Bonding/modals/UpdateBondAmountNymNode.tsx b/nym-wallet/src/components/Bonding/modals/UpdateBondAmountNymNode.tsx new file mode 100644 index 0000000000..e634b1c354 --- /dev/null +++ b/nym-wallet/src/components/Bonding/modals/UpdateBondAmountNymNode.tsx @@ -0,0 +1,153 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { Box, Stack } from '@mui/material'; +import Big from 'big.js'; +import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; +import { ModalListItem } from 'src/components/Modals/ModalListItem'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; +import { DecCoin } from '@nymproject/types'; +import { ConfirmTx } from 'src/components/ConfirmTX'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { decCoinToDisplay, validateAmount } from 'src/utils'; +import { simulateUpdateBond } from 'src/requests'; +import { TSimulateUpdateBondArgs, TUpdateBondArgs } from 'src/types'; +import { AppContext } from 'src/context'; +import { BalanceWarning } from 'src/components/FeeWarning'; +import { TBondedNymNode } from 'src/requests/nymNodeDetails'; +import { TPoolOption } from '../../TokenPoolSelector'; + +export const UpdateBondAmountNymNode = ({ + node, + onUpdateBond, + onClose, + onError, +}: { + node: TBondedNymNode; + onUpdateBond: (data: TUpdateBondArgs, tokenPool: TPoolOption) => Promise; + onClose: () => void; + onError: (e: string) => void; +}) => { + const { bond: currentBond, stakeSaturation, uncappedStakeSaturation } = node; + + const { fee, getFee, resetFeeState, feeError } = useGetFee(); + const [newBond, setNewBond] = useState(); + const [errorAmount, setErrorAmount] = useState(false); + + const { printBalance, userBalance } = useContext(AppContext); + + useEffect(() => { + if (feeError) { + onError(feeError); + } + }, [feeError]); + + const handleConfirm = async () => { + if (!newBond) { + return; + } + + await onUpdateBond( + { + currentPledge: currentBond, + newPledge: newBond, + fee: fee?.fee, + }, + 'balance', + ); + }; + + const handleAmountChanged = async (value: DecCoin) => { + const { amount } = value; + setNewBond(value); + if (!amount || !Number(amount)) { + setErrorAmount(true); + } else if (Big(amount).eq(currentBond.amount)) { + setErrorAmount(true); + } else { + const validAmount = await validateAmount(amount, '1'); + if (!validAmount) { + setErrorAmount(true); + return; + } + setErrorAmount(false); + } + }; + + const handleOnOk = async () => { + if (!newBond) { + return; + } + + await getFee(simulateUpdateBond, { + currentPledge: currentBond, + newPledge: newBond, + }); + }; + + const newBondToDisplay = () => { + const coin = decCoinToDisplay(newBond as DecCoin); + return `${coin.amount} ${coin.denom}`; + }; + + if (fee) + return ( + + + + {userBalance.balance?.amount.amount && fee?.amount?.amount && ( + + + + )} + + ); + + return ( + + + + { + handleAmountChanged(value); + }} + fullWidth + validationError={errorAmount ? 'Please enter a valid amount' : undefined} + /> + + + + + + {uncappedStakeSaturation ? ( + + ) : ( + + )} + + + + + ); +}; diff --git a/nym-wallet/src/components/Bonding/utils.ts b/nym-wallet/src/components/Bonding/utils.ts index 6e7b05a651..bb3be55e60 100644 --- a/nym-wallet/src/components/Bonding/utils.ts +++ b/nym-wallet/src/components/Bonding/utils.ts @@ -1,4 +1,4 @@ -import { Gateway, MixNode, MixNodeCostParams } from '@nymproject/types'; +import { Gateway, MixNode, NodeCostParams } from '@nymproject/types'; import { GatewayData, MixnodeAmount, MixnodeData } from '../../pages/bonding/types'; import { toPercentFloatString } from '../../utils'; @@ -14,7 +14,7 @@ export function mixnodeToTauri(data: MixnodeData): MixNode { }; } -export function costParamsToTauri(data: MixnodeAmount): MixNodeCostParams { +export function costParamsToTauri(data: MixnodeAmount): NodeCostParams { return { profit_margin_percent: toPercentFloatString(data.profitMargin), interval_operating_cost: { diff --git a/nym-wallet/src/components/Delegation/DelegateModal.tsx b/nym-wallet/src/components/Delegation/DelegateModal.tsx index 211b4ae2b9..88b91627c7 100644 --- a/nym-wallet/src/components/Delegation/DelegateModal.tsx +++ b/nym-wallet/src/components/Delegation/DelegateModal.tsx @@ -5,7 +5,7 @@ import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField' import { CurrencyDenom, FeeDetails, DecCoin, decimalToFloatApproximation } from '@nymproject/types'; import { Console } from 'src/utils/console'; import { useGetFee } from 'src/hooks/useGetFee'; -import { simulateDelegateToMixnode, simulateVestingDelegateToMixnode, tryConvertIdentityToMixId } from 'src/requests'; +import { simulateDelegateToNode, simulateVestingDelegateToMixnode, tryConvertIdentityToNodeId } from 'src/requests'; import { debounce } from 'lodash'; import { AppContext } from 'src/context'; import { SimpleModal } from '../Modals/SimpleModal'; @@ -152,7 +152,7 @@ export const DelegateModal: FCWithChildren<{ } if (tokenPool === 'balance') { - getFee(simulateDelegateToMixnode, { mixId: id, amount: value }); + getFee(simulateDelegateToNode, { nodeId: id, amount: value }); } if (tokenPool === 'locked') { @@ -187,16 +187,16 @@ export const DelegateModal: FCWithChildren<{ } let res; try { - res = await tryConvertIdentityToMixId(idKey); + res = await tryConvertIdentityToNodeId(idKey); } catch (e) { - Console.warn(`failed to resolve mix_id for "${idKey}": ${e}`); + Console.warn(`failed to resolve node_id for "${idKey}": ${e}`); return; } if (res) { setMixId(res); setMixIdError(undefined); } else { - setMixIdError('Mixnode with this identity does not seem to be currently bonded'); + setMixIdError('Node with this identity does not seem to be currently bonded'); } }, 500), [], diff --git a/nym-wallet/src/components/Delegation/UndelegateModal.tsx b/nym-wallet/src/components/Delegation/UndelegateModal.tsx index 65820eb2bb..1038dbe341 100644 --- a/nym-wallet/src/components/Delegation/UndelegateModal.tsx +++ b/nym-wallet/src/components/Delegation/UndelegateModal.tsx @@ -2,7 +2,7 @@ import React, { useContext, useEffect } from 'react'; import { Box, SxProps } from '@mui/material'; import { FeeDetails } from '@nymproject/types'; import { useGetFee } from 'src/hooks/useGetFee'; -import { simulateUndelegateFromMixnode, simulateVestingUndelegateFromMixnode } from 'src/requests'; +import { simulateUndelegateFromNode, simulateVestingUndelegateFromMixnode } from 'src/requests'; import { AppContext } from 'src/context'; import { ModalFee } from '../Modals/ModalFee'; import { ModalListItem } from '../Modals/ModalListItem'; @@ -27,7 +27,7 @@ export const UndelegateModal: FCWithChildren<{ useEffect(() => { if (usesVestingContractTokens) getFee(simulateVestingUndelegateFromMixnode, { mixId }); else { - getFee(simulateUndelegateFromMixnode, mixId); + getFee(simulateUndelegateFromNode, mixId); } }, []); diff --git a/nym-wallet/src/components/NodeStatus.tsx b/nym-wallet/src/components/NodeStatus.tsx index 00ec32197d..99f92bcd18 100644 --- a/nym-wallet/src/components/NodeStatus.tsx +++ b/nym-wallet/src/components/NodeStatus.tsx @@ -32,5 +32,4 @@ export const NodeStatus = ({ status }: { status: MixnodeStatus }) => { default: return null; } - return null; }; diff --git a/nym-wallet/src/components/NymCard.tsx b/nym-wallet/src/components/NymCard.tsx index 768f0811ab..ae3d84fe23 100644 --- a/nym-wallet/src/components/NymCard.tsx +++ b/nym-wallet/src/components/NymCard.tsx @@ -20,7 +20,7 @@ export const NymCard: FCWithChildren<{ dataTestid?: string; sx?: SxProps; sxTitle?: SxProps; - children: React.ReactNode; + children?: React.ReactNode; }> = ({ title, subheader, Action, Icon, noPadding, borderless, children, dataTestid, sx, sxTitle }) => ( Promise; - bondMixnode: (data: TBondMixNodeArgs, tokenPool: TokenPool) => Promise; - bondGateway: (data: TBondGatewayArgs, tokenPool: TokenPool) => Promise; + bondedNode?: TBondedNode | null; + isVestingAccount: boolean; + refresh: () => void; unbond: (fee?: FeeDetails) => Promise; - updateBondAmount: (data: TUpdateBondArgs, tokenPool: TokenPool) => Promise; + bond: (args: TBondNymNodeArgs) => Promise; + updateBondAmount: (data: TUpdateBondArgs) => Promise; + updateNymNodeConfig: (data: NodeConfigUpdate) => Promise; redeemRewards: (fee?: FeeDetails) => Promise; - updateMixnode: (pm: string, fee?: FeeDetails) => Promise; - generateMixnodeMsgPayload: (data: TBondMixnodeSignatureArgs) => Promise; - generateGatewayMsgPayload: (data: TBondGatewaySignatureArgs) => Promise; - isVestingAccount: boolean; + generateNymNodeMsgPayload: (data: TNymNodeSignatureArgs) => Promise; migrateVestedMixnode: () => Promise; + migrateLegacyNode: () => Promise; }; export const BondingContext = createContext({ isLoading: true, refresh: async () => undefined, - bondMixnode: async () => { - throw new Error('Not implemented'); - }, - bondGateway: async () => { + bond: async () => { throw new Error('Not implemented'); }, unbond: async () => { @@ -147,19 +54,19 @@ export const BondingContext = createContext({ updateBondAmount: async () => { throw new Error('Not implemented'); }, - redeemRewards: async () => { + updateNymNodeConfig: async () => { throw new Error('Not implemented'); }, - updateMixnode: async () => { + redeemRewards: async () => { throw new Error('Not implemented'); }, - generateMixnodeMsgPayload: async () => { + generateNymNodeMsgPayload: async () => { throw new Error('Not implemented'); }, - generateGatewayMsgPayload: async () => { + migrateVestedMixnode: async () => { throw new Error('Not implemented'); }, - migrateVestedMixnode: async () => { + migrateLegacyNode: async () => { throw new Error('Not implemented'); }, isVestingAccount: false, @@ -168,11 +75,16 @@ export const BondingContext = createContext({ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Element => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(); - const [bondedNode, setBondedNode] = useState(); + const [isVestingAccount, setIsVestingAccount] = useState(false); - const { userBalance, clientDetails } = useContext(AppContext); - const { ownership, isLoading: isOwnershipLoading } = useCheckOwnership(); + const { userBalance, clientDetails, network } = useContext(AppContext); + + const { + bondedNode, + isLoading: isBondedNodeLoading, + getNodeDetails, + } = useGetNodeDetails(clientDetails?.client_address, network); useEffect(() => { userBalance.fetchBalance(); @@ -186,317 +98,43 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen const resetState = () => { setError(undefined); - setBondedNode(undefined); - }; - - /** - * Fetch mixnode **optional** data. - * ⚠ The underlying queries are allowed to fail. - */ - const fetchMixnodeDetails = async (mixId: number, host: string, port: number) => { - const details: { - status: MixnodeStatus; - stakeSaturation: string; - estimatedRewards?: DecCoin; - uptime: number; - averageUptime?: number; - setProbability?: InclusionProbabilityResponse; - nodeDescription?: TNodeDescription | undefined; - operatorRewards?: DecCoin; - uncappedSaturation?: number; - } = { - status: 'not_found', - stakeSaturation: '0', - uptime: 0, - }; - - const statusReq: TauriReq = { - name: 'getMixnodeStatus', - request: () => getMixnodeStatus(mixId), - onFulfilled: (value) => { - details.status = value.status; - }, - }; - - const uptimeReq: TauriReq = { - name: 'getMixnodeUptime', - request: () => getMixnodeUptime(mixId), - onFulfilled: (value) => { - details.uptime = value; - }, - }; - - const stakeSaturationReq: TauriReq = { - name: 'getMixnodeStakeSaturation', - request: () => getMixnodeStakeSaturation(mixId), - onFulfilled: (value) => { - details.stakeSaturation = decimalToPercentage(value.saturation); - const rawUncappedSaturation = decimalToFloatApproximation(value.uncapped_saturation); - if (rawUncappedSaturation && rawUncappedSaturation > 1) { - details.uncappedSaturation = Math.round(rawUncappedSaturation * 100); - } - }, - }; - - const rewardReq: TauriReq = { - name: 'getMixnodeRewardEstimation', - request: () => getMixnodeRewardEstimation(mixId), - onFulfilled: (value) => { - const estimatedRewards = unymToNym(value.estimation.total_node_reward); - if (estimatedRewards) { - details.estimatedRewards = { - amount: estimatedRewards, - denom: 'nym', - }; - } - }, - }; - - const inclusionReq: TauriReq = { - name: 'getInclusionProbability', - request: () => getInclusionProbability(mixId), - onFulfilled: (value) => { - details.setProbability = value; - }, - }; - - const avgUptimeReq: TauriReq = { - name: 'getMixnodeAvgUptime', - request: () => getMixnodeAvgUptime(), - onFulfilled: (value) => { - details.averageUptime = value as number | undefined; - }, - }; - - const nodeDescReq: TauriReq = { - name: 'getNodeDescription', - request: () => getNodeDescriptionRequest(host, port), - onFulfilled: (value) => { - details.nodeDescription = value; - }, - }; - - const operatorRewardsReq: TauriReq = { - name: 'getPendingOperatorRewards', - request: () => getPendingOperatorRewards(clientDetails?.client_address || ''), - onFulfilled: (value) => { - details.operatorRewards = decCoinToDisplay(value); - }, - }; - - await fireRequests([ - statusReq, - uptimeReq, - stakeSaturationReq, - rewardReq, - inclusionReq, - avgUptimeReq, - nodeDescReq, - operatorRewardsReq, - ]); - - return details; - }; - - /** - * Fetch gateway **optional** data. - * ⚠ The underlying queries are allowed to fail. - */ - const fetchGatewayDetails = async (identityKey: string, host: string, port: number) => { - const details: { - routingScore?: { current: number; average: number } | undefined; - nodeDescription?: TNodeDescription | undefined; - } = {}; - - const reportReq: TauriReq = { - name: 'getGatewayReport', - request: () => getGatewayReport(identityKey), - onFulfilled: (value) => { - details.routingScore = { current: value.most_recent, average: value.last_day }; - }, - }; - - const nodeDescReq: TauriReq = { - name: 'getNodeDescription', - request: () => getNodeDescriptionRequest(host, port), - onFulfilled: (value) => { - details.nodeDescription = value; - }, - }; - - await fireRequests([reportReq, nodeDescReq]); - - return details; + setIsLoading(false); }; - const calculateStake = (pledge: string, delegations: string) => { - let stake; - try { - stake = unymToNym(Big(pledge).plus(delegations)); - } catch (e: any) { - Console.warn(`not a valid decimal number: ${e}`); - } - return stake; + const refresh = () => { + resetState(); }; - const refresh = useCallback(async () => { - setIsLoading(true); - setError(undefined); - - if (ownership.hasOwnership && ownership.nodeType === EnumNodeType.mixnode && clientDetails) { - try { - const data = await getMixnodeBondDetails(); - if (data) { - const { - bond_information, - rewarding_details, - bond_information: { mix_id }, - } = data; - - const { - status, - stakeSaturation, - uncappedSaturation: uncappedStakeSaturation, - estimatedRewards, - uptime, - operatorRewards, - averageUptime, - nodeDescription, - setProbability, - } = await fetchMixnodeDetails( - mix_id, - bond_information.mix_node.host, - bond_information.mix_node.http_api_port, - ); - - setBondedNode({ - id: data.bond_information.mix_id, - name: nodeDescription?.name, - mixId: mix_id, - identityKey: bond_information.mix_node.identity_key, - stake: { - amount: calculateStake(rewarding_details.operator, rewarding_details.delegates), - denom: bond_information.original_pledge.denom, - }, - bond: decCoinToDisplay(bond_information.original_pledge), - profitMargin: toPercentIntegerString(rewarding_details.cost_params.profit_margin_percent), - delegators: rewarding_details.unique_delegations, - proxy: bond_information.proxy, - operatorRewards, - uptime, - status, - stakeSaturation, - uncappedStakeSaturation, - operatorCost: decCoinToDisplay(rewarding_details.cost_params.interval_operating_cost), - host: bond_information.mix_node.host.replace(/\s/g, ''), - routingScore: averageUptime, - activeSetProbability: setProbability?.in_active, - standbySetProbability: setProbability?.in_reserve, - estimatedRewards, - httpApiPort: bond_information.mix_node.http_api_port, - mixPort: bond_information.mix_node.mix_port, - verlocPort: bond_information.mix_node.verloc_port, - version: bond_information.mix_node.version, - isUnbonding: bond_information.is_unbonding, - } as TBondedMixnode); - } - } catch (e: any) { - Console.warn(e); - setError(`While fetching current bond state, an error occurred: ${e}`); - } - } - - if (ownership.hasOwnership && ownership.nodeType === EnumNodeType.gateway) { - try { - const data = await getGatewayBondDetails(); - if (data) { - const { gateway, proxy } = data; - const { nodeDescription, routingScore } = await fetchGatewayDetails( - gateway.identity_key, - data.gateway.host, - data.gateway.clients_port, - ); - setBondedNode({ - name: nodeDescription?.name, - identityKey: gateway.identity_key, - mixPort: gateway.mix_port, - httpApiPort: gateway.clients_port, - host: gateway.host, - ip: gateway.host, - location: gateway.location, - bond: decCoinToDisplay(data.pledge_amount), - proxy, - routingScore, - version: gateway.version, - } as TBondedGateway); - } - } catch (e: any) { - Console.warn(e); - setError(`While fetching current bond state, an error occurred: ${e}`); - } - } - - if (!ownership.hasOwnership) { - resetState(); - } - setIsLoading(false); - }, [ownership]); - - useEffect(() => { - refresh(); - }, [ownership, refresh]); - - const bondMixnode = async (data: TBondMixNodeArgs, tokenPool: TokenPool) => { - let tx: TransactionExecuteResult | undefined; + const bond = async (data: TBondNymNodeArgs) => { + let tx; setIsLoading(true); - try { - if (tokenPool === 'balance') { - tx = await bondMixNodeRequest(data); - await userBalance.fetchBalance(); - } - if (tokenPool === 'locked') { - tx = await vestingBondMixNode(data); - await userBalance.fetchTokenAllocation(); - } - return tx; - } catch (e: any) { - Console.warn(e); - setError(`an error occurred: ${e}`); - } finally { - setIsLoading(false); - } - return undefined; - }; - const bondGateway = async (data: TBondGatewayArgs, tokenPool: TokenPool) => { - let tx: TransactionExecuteResult | undefined; - setIsLoading(true); try { - if (tokenPool === 'balance') { - tx = await bondGatewayRequest(data); - await userBalance.fetchBalance(); - } - if (tokenPool === 'locked') { - tx = await vestingBondGateway(data); - await userBalance.fetchTokenAllocation(); + tx = await bondNymNode({ + ...data, + costParams: { + ...data.costParams, + profit_margin_percent: toPercentFloatString(data.costParams.profit_margin_percent), + }, + }); + if (clientDetails?.client_address) { + await getNodeDetails(clientDetails?.client_address); } - return tx; - } catch (e: any) { + } catch (e) { Console.warn(e); - setError(`an error occurred: ${e}`); + setError(`an error occurred: ${e as string}`); } finally { setIsLoading(false); } - return undefined; + return tx; }; const unbond = async (fee?: FeeDetails) => { let tx; setIsLoading(true); try { - if (bondedNode && isMixnode(bondedNode) && bondedNode.proxy) tx = await vestingUnbondMixnode(fee?.fee); + if (bondedNode && isNymNode(bondedNode)) tx = await unbondNymNodeRequest(fee?.fee); if (bondedNode && isMixnode(bondedNode) && !bondedNode.proxy) tx = await unbondMixnodeRequest(fee?.fee); - if (bondedNode && isGateway(bondedNode) && bondedNode.proxy) tx = await vestingUnbondGateway(fee?.fee); if (bondedNode && isGateway(bondedNode) && !bondedNode.proxy) tx = await unbondGatewayRequest(fee?.fee); } catch (e) { Console.warn(e); @@ -507,22 +145,15 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen return tx; }; - const updateMixnode = async (pm: string, fee?: FeeDetails) => { + const updateNymNodeConfig = async (data: NodeConfigUpdate) => { let tx; setIsLoading(true); - - // TODO: this will have to be updated with allowing users to provide their operating cost in the form - const defaultCostParams = await attachDefaultOperatingCost(toPercentFloatString(pm)); - try { - // JS: this check is not entirely valid. you can have proxy field set whilst not using the vesting contract, - // you have to check if proxy exists AND if it matches the known vesting contract address! - if (bondedNode?.proxy) { - tx = await updateMixnodeVestingCostParamsRequest(defaultCostParams, fee?.fee); - } else { - tx = await updateMixnodeCostParamsRequest(defaultCostParams, fee?.fee); + tx = await updateNymNodeConfigReq(data); + if (clientDetails?.client_address) { + await getNodeDetails(clientDetails?.client_address); } - } catch (e: any) { + } catch (e) { Console.warn(e); setError(`an error occurred: ${e}`); } finally { @@ -535,7 +166,7 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen let tx; setIsLoading(true); try { - if (bondedNode?.proxy) tx = await vestingClaimOperatorReward(fee?.fee); + if (bondedNode && !isNymNode(bondedNode)) tx = await vestingClaimOperatorReward(fee?.fee); else tx = await claimOperatorReward(fee?.fee); } catch (e: any) { setError(`an error occurred: ${e}`); @@ -545,18 +176,12 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen return tx; }; - const updateBondAmount = async (data: TUpdateBondArgs, tokenPool: TokenPool) => { + const updateBondAmount = async (data: TUpdateBondArgs) => { let tx: TransactionExecuteResult | undefined; setIsLoading(true); try { - if (tokenPool === 'balance') { - tx = await updateBondReq(data); - await userBalance.fetchBalance(); - } - if (tokenPool === 'locked') { - tx = await vestingUpdateBondReq(data); - await userBalance.fetchTokenAllocation(); - } + tx = await updateBondReq(data); + await userBalance.fetchBalance(); return tx; } catch (e: any) { @@ -568,67 +193,77 @@ export const BondingContextProvider: FCWithChildren = ({ children }): JSX.Elemen return undefined; }; - const generateMixnodeMsgPayload = async (data: TBondMixnodeSignatureArgs) => { - let message; + const generateNymNodeMsgPayload = async (data: TNymNodeSignatureArgs) => { setIsLoading(true); + try { - if (data.tokenPool === 'locked') { - message = await vestingGenerateMixnodeMsgPayloadReq(data); - } else { - message = await generateMixnodeMsgPayloadReq(data); - } + const message = await generateNymNodeMsgPayloadReq({ + nymnode: data.nymnode, + pledge: data.pledge, + costParams: { + ...data.costParams, + profit_margin_percent: toPercentFloatString(data.costParams.profit_margin_percent), + }, + }); + return message; } catch (e) { Console.warn(e); setError(`an error occurred: ${e}`); } finally { setIsLoading(false); } - return message; + return undefined; }; - const generateGatewayMsgPayload = async (data: TBondGatewaySignatureArgs) => { - let message; + const migrateVestedMixnode = async () => { setIsLoading(true); try { - if (data.tokenPool === 'locked') { - message = await vestingGenerateGatewayMsgPayloadReq(data); - } else { - message = await generateGatewayMsgPayloadReq(data); - } + const tx = await tauriMigrateVestedMixnode(); + setIsLoading(false); + return tx; } catch (e) { - Console.warn(e); + Console.error(e); setError(`an error occurred: ${e}`); - } finally { - setIsLoading(false); } - return message; }; - const migrateVestedMixnode = async () => { + const migrateLegacyNode = async () => { setIsLoading(true); - const tx = await tauriMigrateVestedMixnode(); + try { + let tx: TransactionExecuteResult | undefined; + + if (bondedNode && isMixnode(bondedNode)) { + tx = await migrateLegacyMixnodeReq(); + } + if (bondedNode && isGateway(bondedNode)) { + tx = await migrateLegacyGatewayReq(); + } + return tx; + } catch (e) { + Console.error(e); + setError(`an error occurred: ${e}`); + } + setIsLoading(false); - return tx; }; const memoizedValue = useMemo( () => ({ - isLoading: isLoading || isOwnershipLoading, + isLoading: isLoading || isBondedNodeLoading, error, - bondMixnode, bondedNode, - bondGateway, + bond, unbond, - updateMixnode, refresh, redeemRewards, updateBondAmount, - generateMixnodeMsgPayload, - generateGatewayMsgPayload, + updateNymNodeConfig, + generateNymNodeMsgPayload, migrateVestedMixnode, + migrateLegacyNode, isVestingAccount, }), - [isLoading, isOwnershipLoading, error, bondedNode, isVestingAccount], + [isLoading, error, bondedNode, isVestingAccount, isBondedNodeLoading], ); return {children}; diff --git a/nym-wallet/src/context/mocks/bonding.tsx b/nym-wallet/src/context/mocks/bonding.tsx index e961272c66..0fd08a871f 100644 --- a/nym-wallet/src/context/mocks/bonding.tsx +++ b/nym-wallet/src/context/mocks/bonding.tsx @@ -1,9 +1,10 @@ import { FeeDetails, TransactionExecuteResult } from '@nymproject/types'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import type { Network } from 'src/types'; -import { BondingContext, TBondedGateway, TBondedMixnode } from '../bonding'; +import type { Network, TNymNodeSignatureArgs } from 'src/types'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; +import { TBondedGateway } from 'src/requests/gatewayDetails'; +import { BondingContext } from '../bonding'; import { mockSleep } from './utils'; -import { TBondGatewaySignatureArgs, TBondMixnodeSignatureArgs } from '../../types'; const SLEEP_MS = 1000; @@ -30,10 +31,11 @@ const bondedMixnodeMock: TBondedMixnode = { version: '1.0.2', isUnbonding: false, uptime: 1, + proxy: null, + uncappedStakeSaturation: 100, }; const bondedGatewayMock: TBondedGateway = { - id: 1, name: 'Monster node', identityKey: 'WayM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F', ip: '112.43.234.57', @@ -41,17 +43,18 @@ const bondedGatewayMock: TBondedGateway = { host: '1.2.34.5 ', httpApiPort: 8000, mixPort: 1789, - verlocPort: 1790, version: '1.0.2', routingScore: { average: 100, current: 100, }, + location: 'Germany', + proxy: null, }; const TxResultMock: TransactionExecuteResult = { logs_json: '', - data_json: '', + msg_responses_json: '', transaction_hash: '55303CD4B91FAC4C2715E40EBB52BB3B92829D9431B3A279D37B5CC58432E354', gas_info: { gas_wanted: { gas_units: BigInt(1) }, @@ -130,6 +133,14 @@ export const MockBondingContextProvider = ({ return TxResultMock; }; + const bond = async (): Promise => { + setIsLoading(true); + await mockSleep(SLEEP_MS); + setBondedData(bondedMixnodeMock); + setIsLoading(false); + return TxResultMock; + }; + const unbond = async (): Promise => { setIsLoading(true); await mockSleep(SLEEP_MS); @@ -138,6 +149,14 @@ export const MockBondingContextProvider = ({ return TxResultMock; }; + const updateNymNodeConfig = async (): Promise => { + setIsLoading(true); + await mockSleep(SLEEP_MS); + triggerStateUpdate(); + setIsLoading(false); + return TxResultMock; + }; + const redeemRewards = async (): Promise => { setIsLoading(true); await mockSleep(SLEEP_MS); @@ -170,15 +189,7 @@ export const MockBondingContextProvider = ({ return feeMock; }; - const generateMixnodeMsgPayload = async (_data: TBondMixnodeSignatureArgs) => { - setIsLoading(true); - await mockSleep(SLEEP_MS); - triggerStateUpdate(); - setIsLoading(false); - return '77dcaba7f41409984f4ebce4a386f59b10f1e65ed5514d1acdccae30174bd84b'; - }; - - const generateGatewayMsgPayload = async (_data: TBondGatewaySignatureArgs) => { + const generateNymNodeMsgPayload = async (_data: TNymNodeSignatureArgs) => { setIsLoading(true); await mockSleep(SLEEP_MS); triggerStateUpdate(); @@ -194,6 +205,7 @@ export const MockBondingContextProvider = ({ error, bondMixnode, bondGateway, + bond, unbond, refresh, redeemRewards, @@ -204,10 +216,11 @@ export const MockBondingContextProvider = ({ updateMixnode, updateBondAmount, checkOwnership, - generateMixnodeMsgPayload, - generateGatewayMsgPayload, + generateNymNodeMsgPayload, isVestingAccount: false, migrateVestedMixnode: async () => undefined, + migrateLegacyNode: async () => undefined, + updateNymNodeConfig, }), [isLoading, error, bondedMixnode, bondedGateway, trigger, fee], ); diff --git a/nym-wallet/src/context/mocks/delegations.tsx b/nym-wallet/src/context/mocks/delegations.tsx index 0c984d7369..f2aea600bc 100644 --- a/nym-wallet/src/context/mocks/delegations.tsx +++ b/nym-wallet/src/context/mocks/delegations.tsx @@ -108,7 +108,7 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => { return { logs_json: '', - data_json: '', + msg_responses_json: '', gas_info: { gas_wanted: { gas_units: BigInt(1) }, gas_used: { gas_units: BigInt(1) }, @@ -183,7 +183,7 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => { return { logs_json: '', - data_json: '', + msg_responses_json: '', transaction_hash: '', gas_info: { gas_wanted: { gas_units: BigInt(1) }, @@ -195,8 +195,9 @@ export const MockDelegationContextProvider: FCWithChildren = ({ children }) => { // eslint-disable-next-line @typescript-eslint/no-unused-vars const undelegateVesting = async (mix_id: number, _fee?: FeeDetails) => ({ - logs_json: '', + msg_responses_json: '', data_json: '', + logs_json: '', transaction_hash: '', gas_info: { gas_wanted: { gas_units: BigInt(1) }, diff --git a/nym-wallet/src/context/mocks/rewards.tsx b/nym-wallet/src/context/mocks/rewards.tsx index c89fdb00b3..fe84e5cfe2 100644 --- a/nym-wallet/src/context/mocks/rewards.tsx +++ b/nym-wallet/src/context/mocks/rewards.tsx @@ -65,8 +65,8 @@ export const MockRewardsContextProvider: FCWithChildren = ({ children }) => { amount: '1', denom: 'nym', }, - data_json: '[]', logs_json: '[]', + msg_responses_json: '[]', gas_info: { gas_wanted: { gas_units: BigInt(1) }, gas_used: { gas_units: BigInt(1) }, @@ -78,7 +78,7 @@ export const MockRewardsContextProvider: FCWithChildren = ({ children }) => { amount: '1', denom: 'nym', }, - data_json: '[]', + msg_responses_json: '[]', logs_json: '[]', gas_info: { gas_wanted: { gas_units: BigInt(1) }, diff --git a/nym-wallet/src/hooks/useGetNodeDetails.ts b/nym-wallet/src/hooks/useGetNodeDetails.ts new file mode 100644 index 0000000000..1c9608a351 --- /dev/null +++ b/nym-wallet/src/hooks/useGetNodeDetails.ts @@ -0,0 +1,70 @@ +import { useEffect, useState } from 'react'; +import { TBondedNode } from 'src/context'; +import { getGatewayDetails } from 'src/requests/gatewayDetails'; +import { getMixnodeDetails } from 'src/requests/mixnodeDetails'; +import { getNymNodeDetails } from 'src/requests/nymNodeDetails'; +import { fireRequests, TauriReq } from 'src/utils'; + +const useGetNodeDetails = (clientAddress?: string, network?: string) => { + const [bondedNode, setBondedNode] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [isError, setIsError] = useState(false); + + const getNodeDetails = async (address: string) => { + setIsError(false); + setBondedNode(null); + setIsLoading(true); + + // Check if the address has a Nym node bonded + const nymnode: TauriReq = { + name: 'getNymNodeBondDetails', + request: () => getNymNodeDetails(address), + onFulfilled: (value) => { + if (value) { + setBondedNode(value); + } + }, + }; + + // Check if the address has a Mix node bonded + const mixnode: TauriReq = { + name: 'getMixnodeDetails', + request: () => getMixnodeDetails(address), + onFulfilled: (value) => { + if (value) { + setBondedNode(value); + } + }, + }; + + // Check if the address has a Gateway bonded + const gateway: TauriReq = { + name: 'getGatewayDetails', + request: () => getGatewayDetails(), + onFulfilled: (value) => { + if (value) { + setBondedNode(value); + } + }, + }; + + await fireRequests([nymnode, mixnode, gateway]); + + setIsLoading(false); + }; + + useEffect(() => { + if (clientAddress) { + getNodeDetails(clientAddress); + } + }, [clientAddress, network]); + + return { + bondedNode, + isLoading, + isError, + getNodeDetails, + }; +}; + +export default useGetNodeDetails; diff --git a/nym-wallet/src/pages/Admin/index.tsx b/nym-wallet/src/pages/Admin/index.tsx index 978360f254..7a0876c5a3 100644 --- a/nym-wallet/src/pages/Admin/index.tsx +++ b/nym-wallet/src/pages/Admin/index.tsx @@ -26,29 +26,15 @@ const AdminForm: FCWithChildren<{ - - - diff --git a/nym-wallet/src/pages/bonding/Bonding.tsx b/nym-wallet/src/pages/bonding/Bonding.tsx index 3ab472d1e4..57dc05bbe4 100644 --- a/nym-wallet/src/pages/bonding/Bonding.tsx +++ b/nym-wallet/src/pages/bonding/Bonding.tsx @@ -2,57 +2,91 @@ import React, { useContext, useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { FeeDetails } from '@nymproject/types'; import { Alert, AlertTitle, Box, Button, Typography } from '@mui/material'; -import { TPoolOption } from 'src/components'; import { Bond } from 'src/components/Bonding/Bond'; import { BondedMixnode } from 'src/components/Bonding/BondedMixnode'; import { TBondedMixnodeActions } from 'src/components/Bonding/BondedMixnodeActions'; -import { BondGatewayModal } from 'src/components/Bonding/modals/BondGatewayModal'; -import { BondMixnodeModal } from 'src/components/Bonding/modals/BondMixnodeModal'; import { UpdateBondAmountModal } from 'src/components/Bonding/modals/UpdateBondAmountModal'; import { BondOversaturatedModal } from 'src/components/Bonding/modals/BondOversaturatedModal'; import { ConfirmationDetailProps, ConfirmationDetailsModal } from 'src/components/Bonding/modals/ConfirmationModal'; import { ErrorModal } from 'src/components/Modals/ErrorModal'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { AppContext, urls } from 'src/context/main'; -import { isGateway, isMixnode, TBondGatewayArgs, TBondMixNodeArgs, TUpdateBondArgs } from 'src/types'; +import { isGateway, isMixnode, isNymNode, TBondNymNodeArgs, TUpdateBondArgs } from 'src/types'; import { BondedGateway } from 'src/components/Bonding/BondedGateway'; import { RedeemRewardsModal } from 'src/components/Bonding/modals/RedeemRewardsModal'; import { VestingWarningModal } from 'src/components/VestingWarningModal'; +import MigrateLegacyNode from 'src/components/Bonding/modals/MigrateLegacyNode'; +import { BondedNymNode } from 'src/components/Bonding/BondedNymNode'; +import { UpdateBondAmountNymNode } from 'src/components/Bonding/modals/UpdateBondAmountNymNode'; +import { BondNymNode } from 'src/components/Bonding/modals/BondNymNodeModal'; import { BondingContextProvider, useBondingContext } from '../../context'; export const Bonding = () => { const [showModal, setShowModal] = useState< - 'bond-mixnode' | 'bond-gateway' | 'update-bond' | 'update-bond-oversaturated' | 'unbond' | 'redeem' + | 'bond-mixnode' + | 'bond-nymnode' + | 'bond-gateway' + | 'update-bond' + | 'update-bond-oversaturated' + | 'unbond' + | 'redeem' + | 'update-bond-nymnode' >(); - const [confirmationDetails, setConfirmationDetails] = useState(); - const [uncappedSaturation, setUncappedSaturation] = useState(); - const [showMigrationModal, setShowMigrationModal] = useState(false); - const { - network, - clientDetails, - userBalance: { originalVesting }, - } = useContext(AppContext); + + const { network } = useContext(AppContext); const navigate = useNavigate(); const { bondedNode, - bondMixnode, - bondGateway, - redeemRewards, isLoading, - updateBondAmount, error, + redeemRewards, + updateBondAmount, refresh, + bond, migrateVestedMixnode, + migrateLegacyNode, } = useBondingContext(); + const shouldShowMigrateLegacyNodeModal = () => { + if (!bondedNode) { + return false; + } + if (isMixnode(bondedNode) && !bondedNode.isUnbonding && !bondedNode.proxy) { + return true; + } + if (isGateway(bondedNode)) { + return true; + } + return false; + }; + + const [confirmationDetails, setConfirmationDetails] = useState(); + const [uncappedSaturation, setUncappedSaturation] = useState(); + const [showMigrationModal, setShowMigrationModal] = useState(false); + const [showMigrateLegacyNodeModal, setShowMigrateLegacyNodeModal] = useState(false); + useEffect(() => { if (bondedNode && isMixnode(bondedNode) && bondedNode.uncappedStakeSaturation) { setUncappedSaturation(bondedNode.uncappedStakeSaturation); } + + setShowMigrateLegacyNodeModal(shouldShowMigrateLegacyNodeModal()); }, [bondedNode]); + const handleBondNymNode = async (data: TBondNymNodeArgs) => { + setShowModal(undefined); + const tx = await bond(data); + if (tx) { + setConfirmationDetails({ + status: 'success', + title: 'Bonding successful', + txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, + }); + } + }; + const handleMigrateVestedMixnode = async () => { setShowMigrationModal(false); const tx = await migrateVestedMixnode(); @@ -65,6 +99,18 @@ export const Bonding = () => { } }; + const handleMigrateLegacyNode = async () => { + setShowMigrateLegacyNodeModal(false); + const tx = await migrateLegacyNode(); + if (tx) { + setConfirmationDetails({ + status: 'success', + title: 'Migration successful', + txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, + }); + } + }; + const handleCloseModal = async () => { setShowModal(undefined); refresh(); @@ -79,35 +125,10 @@ export const Bonding = () => { }); }; - const handleBondMixnode = async (data: TBondMixNodeArgs, tokenPool: TPoolOption) => { - setShowModal(undefined); - const tx = await bondMixnode(data, tokenPool); - if (tx) { - setConfirmationDetails({ - status: 'success', - title: 'Bond successful', - txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, - }); - } - return undefined; - }; - - const handleBondGateway = async (data: TBondGatewayArgs, tokenPool: TPoolOption) => { - setShowModal(undefined); - const tx = await bondGateway(data, tokenPool); - if (tx) { - setConfirmationDetails({ - status: 'success', - title: 'Bond successful', - txUrl: `${urls(network).blockExplorer}/transaction/${tx?.transaction_hash}`, - }); - } - }; - - const handleUpdateBond = async (data: TUpdateBondArgs, tokenPool: TPoolOption) => { + const handleUpdateBond = async (data: TUpdateBondArgs) => { setShowModal(undefined); - const tx = await updateBondAmount(data, tokenPool); + const tx = await updateBondAmount(data); if (tx) { setConfirmationDetails({ status: 'success', @@ -154,13 +175,41 @@ export const Bonding = () => { return undefined; }; + const handleBondedNymNodeAction = async (action: TBondedMixnodeActions) => { + switch (action) { + case 'unbond': { + navigate('/bonding/node-settings', { state: 'unbond' }); + break; + } + case 'updateBond': { + setShowModal('update-bond-nymnode'); + break; + } + case 'redeem': { + setShowModal('redeem'); + break; + } + default: { + return undefined; + } + } + return undefined; + }; + if (error) { - return refresh()} />; + return ( + refresh()} + /> + ); } return ( - {bondedNode?.proxy && ( + {bondedNode && !isNymNode(bondedNode) && bondedNode?.proxy && ( Your bonded node is using tokens from the vesting contract! @@ -187,41 +236,41 @@ export const Bonding = () => { }} /> - {!bondedNode && setShowModal('bond-mixnode')} />} + setShowMigrateLegacyNodeModal(false)} + handleMigrate={handleMigrateLegacyNode} + /> + + {!bondedNode && setShowModal('bond-nymnode')} />} + + {bondedNode && isNymNode(bondedNode) && ( + handleBondedNymNodeAction(action)} + /> + )} {bondedNode && isMixnode(bondedNode) && ( setShowMigrateLegacyNodeModal(true)} onActionSelect={(action) => handleBondedMixnodeAction(action)} /> )} {bondedNode && isGateway(bondedNode) && ( - - )} - - {showModal === 'bond-mixnode' && ( - setShowModal('bond-gateway')} - onClose={() => setShowModal(undefined)} - onError={handleError} + setShowMigrateLegacyNodeModal(true)} + onActionSelect={handleBondedMixnodeAction} /> )} - {showModal === 'bond-gateway' && ( - setShowModal('bond-mixnode')} - onClose={() => setShowModal(undefined)} - onError={handleError} - /> - )} + {showModal === 'update-bond-oversaturated' && uncappedSaturation && ( { /> )} - {showModal === 'redeem' && bondedNode && isMixnode(bondedNode) && ( + {showModal === 'update-bond-nymnode' && bondedNode && isNymNode(bondedNode) && ( + setShowModal(undefined)} + onError={handleError} + /> + )} + + {showModal === 'redeem' && bondedNode && isNymNode(bondedNode) && ( setShowModal(undefined)} diff --git a/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx index b41e09d826..bf7eb7a648 100644 --- a/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/NodeSettings.tsx @@ -10,11 +10,11 @@ import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { NymCard } from 'src/components'; import { PageLayout } from 'src/layouts'; import { Tabs } from 'src/components/Tabs'; -import { useBondingContext, BondingContextProvider, TBondedMixnode } from 'src/context'; +import { useBondingContext, BondingContextProvider } from 'src/context'; import { AppContext, urls } from 'src/context/main'; -import { isMixnode } from 'src/types'; import { getIntervalAsDate } from 'src/utils'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; import { NodeGeneralSettings } from './settings-pages/general-settings'; import { NodeUnbondPage } from './settings-pages/NodeUnbondPage'; import { NavItems, makeNavItems } from './node-settings.constant'; @@ -39,9 +39,9 @@ export const NodeSettings = () => { if (location.state === 'unbond') { setValue('Unbond'); } - if (location.state === 'test-node') { - setValue('Test my node'); - } + // if (location.state === 'test-node') { + // setValue('Test my node'); + // } }, [location]); const handleUnbond = async (fee?: FeeDetails) => { @@ -86,13 +86,13 @@ export const NodeSettings = () => { - {isMixnode(bondedNode) ? 'Node' : 'Gateway'} Settings + Nym Node Settings { > {value === 'General' && bondedNode && } - {value === 'Test my node' && } + {/* {value === 'Test my node' && } */} {value === 'Unbond' && bondedNode && ( )} - {value === 'Playground' && bondedNode && } + {/* {value === 'Playground' && bondedNode && } */} {confirmationDetails && confirmationDetails.status === 'success' && ( { }} > - You should NOT shutdown your {isMixnode(bondedNode) ? 'mix node' : 'gateway'} until the unbond process is - complete + You should NOT shutdown your node until the unbond process is complete )} diff --git a/nym-wallet/src/pages/bonding/node-settings/apy-playground/index.tsx b/nym-wallet/src/pages/bonding/node-settings/apy-playground/index.tsx index 65edb35bc5..c288dc96e7 100644 --- a/nym-wallet/src/pages/bonding/node-settings/apy-playground/index.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/apy-playground/index.tsx @@ -4,10 +4,10 @@ import { ResultsTable } from 'src/components/RewardsPlayground/ResultsTable'; import { getDelegationSummary } from 'src/requests'; import { NodeDetails } from 'src/components/RewardsPlayground/NodeDetail'; import { CalculateArgs, Inputs } from 'src/components/RewardsPlayground/Inputs'; -import { TBondedMixnode } from 'src/context'; import { useSnackbar } from 'notistack'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { Console } from 'src/utils/console'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; import { computeEstimate, computeStakeSaturation, handleCalculatePeriodRewards } from './utils'; export type DefaultInputValues = { diff --git a/nym-wallet/src/pages/bonding/node-settings/node-settings.constant.tsx b/nym-wallet/src/pages/bonding/node-settings/node-settings.constant.tsx index 74e8d7cf5b..8daf8174ab 100644 --- a/nym-wallet/src/pages/bonding/node-settings/node-settings.constant.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/node-settings.constant.tsx @@ -1,9 +1,19 @@ -export const makeNavItems = (isMixnode: boolean) => { - const navItems: NavItems[] = ['General', 'Unbond']; +import { TBondedNode } from 'src/context'; +import { isNymNode } from 'src/types'; - if (isMixnode) navItems.splice(1, 0, 'Test my node', 'Playground'); +export const makeNavItems = (bondedNode: TBondedNode) => { + const navItems: NavItems[] = ['Unbond']; + + if (isNymNode(bondedNode)) { + // Add these items to the beginning of the array "General", "Test my node", "Playground" + // Temporarily removed , 'Test my node due to wasm issues which we need to fix + // 'Playground' due to freezing issues + navItems.unshift('General'); + } return navItems; }; -export type NavItems = 'General' | 'Unbond' | 'Test my node' | 'Playground'; +// And these back in once fixed. +// 'Playground' | 'Test my node' include in array at a later point +export type NavItems = 'General' | 'Unbond' ; diff --git a/nym-wallet/src/pages/bonding/node-settings/node-test/index.tsx b/nym-wallet/src/pages/bonding/node-settings/node-test/index.tsx index ba9bf522f3..2e1dc533f8 100644 --- a/nym-wallet/src/pages/bonding/node-settings/node-test/index.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/node-test/index.tsx @@ -10,6 +10,7 @@ import { ErrorModal } from 'src/components/Modals/ErrorModal'; import { PrintResults } from 'src/components/TestNode/PrintResults'; import { MAINNET_VALIDATOR_URL, QA_VALIDATOR_URL } from 'src/constants'; import { TestStatus } from 'src/components/TestNode/types'; +import { isMixnode } from 'src/types'; export const NodeTestPage = () => { const [nodeTestClient, setNodeTestClient] = useState(); @@ -37,7 +38,7 @@ export const NodeTestPage = () => { }; const handleTestNode = async () => { - if (nodeTestClient && bondedNode) { + if (nodeTestClient && bondedNode && isMixnode(bondedNode)) { setResults(undefined); setTestDate(format(new Date(), 'dd/MM/yyyy HH:mm')); setIsLoading(true); @@ -87,7 +88,7 @@ export const NodeTestPage = () => { {isLoading && } {error && setError(undefined)} />} - {printResults && results && ( + {printResults && results && bondedNode && isMixnode(bondedNode) && ( Promise; onError: (e: string) => void; } @@ -15,6 +14,9 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => { const [confirmField, setConfirmField] = useState(''); const [isConfirmed, setIsConfirmed] = useState(false); // TODO: Check what happens with a gateway + + const shouldDisplayWarning = isMixnode(bondedNode) || isNymNode(bondedNode); + return ( @@ -24,7 +26,7 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => { Unbond - {isMixnode(bondedNode) && ( + {shouldDisplayWarning && ( theme.palette.nym.text.muted }}> Remember you should only unbond if you want to remove your node from the network for good. @@ -35,7 +37,7 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => { - {isMixnode(bondedNode) && ( + {shouldDisplayWarning && ( )} diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralGatewaySettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralGatewaySettings.tsx index 892c1e245c..7f8bdb6d08 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralGatewaySettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralGatewaySettings.tsx @@ -10,16 +10,17 @@ import { updateGatewayConfig, vestingUpdateGatewayConfig, } from 'src/requests'; -import { TBondedGateway, useBondingContext } from 'src/context/bonding'; +import { useBondingContext } from 'src/context/bonding'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; import { Console } from 'src/utils/console'; import { Alert } from 'src/components/Alert'; import { ConfirmTx } from 'src/components/ConfirmTX'; import { useGetFee } from 'src/hooks/useGetFee'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; -import { updateGatewayValidationSchema } from 'src/components/Bonding/forms/gatewayValidationSchema'; +import { updateGatewayValidationSchema } from 'src/components/Bonding/forms/legacyForms/gatewayValidationSchema'; import { BalanceWarning } from 'src/components/FeeWarning'; import { AppContext } from 'src/context'; +import { TBondedGateway } from 'src/requests/gatewayDetails'; export const GeneralGatewaySettings = ({ bondedNode }: { bondedNode: TBondedGateway }) => { const [openConfirmationModal, setOpenConfirmationModal] = useState(false); @@ -56,7 +57,6 @@ export const GeneralGatewaySettings = ({ bondedNode }: { bondedNode: TBondedGate location, version: clean(version) as string, clients_port: httpApiPort, - verloc_port: bondedNode.verlocPort, }; if (bondedNode.proxy) { @@ -206,7 +206,6 @@ export const GeneralGatewaySettings = ({ bondedNode }: { bondedNode: TBondedGate clients_port: data.httpApiPort, location: bondedNode.location!, version: data.version, - verloc_port: bondedNode.verlocPort, }), )} sx={{ m: 3 }} diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralMixnodeSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralMixnodeSettings.tsx index 509c605174..08304622c7 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralMixnodeSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralMixnodeSettings.tsx @@ -6,9 +6,8 @@ import { Box, Button, Divider, Grid, Stack, TextField, Typography } from '@mui/m import { useTheme } from '@mui/material/styles'; import { isMixnode } from 'src/types'; import { simulateUpdateMixnodeConfig, simulateVestingUpdateMixnodeConfig, updateMixnodeConfig } from 'src/requests'; -import { TBondedGateway, TBondedMixnode } from 'src/context/bonding'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; -import { bondedInfoParametersValidationSchema } from 'src/components/Bonding/forms/mixnodeValidationSchema'; +import { bondedInfoParametersValidationSchema } from 'src/components/Bonding/forms/legacyForms/mixnodeValidationSchema'; import { Console } from 'src/utils/console'; import { Alert } from 'src/components/Alert'; import { vestingUpdateMixnodeConfig } from 'src/requests/vesting'; @@ -17,8 +16,9 @@ import { useGetFee } from 'src/hooks/useGetFee'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { BalanceWarning } from 'src/components/FeeWarning'; import { AppContext } from 'src/context'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; -export const GeneralMixnodeSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }) => { +export const GeneralMixnodeSettings = ({ bondedNode }: { bondedNode: TBondedMixnode }) => { const [openConfirmationModal, setOpenConfirmationModal] = useState(false); const { getFee, fee, resetFeeState } = useGetFee(); const { userBalance } = useContext(AppContext); diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx new file mode 100644 index 0000000000..9578de69db --- /dev/null +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/GeneralNymNodeSettings.tsx @@ -0,0 +1,180 @@ +import React, { useContext, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { yupResolver } from '@hookform/resolvers/yup'; +import { Box, Button, Divider, Grid, Stack, TextField, Typography } from '@mui/material'; +import { useTheme } from '@mui/material/styles'; +import { SimpleModal } from 'src/components/Modals/SimpleModal'; +import { Console } from 'src/utils/console'; +import { Alert } from 'src/components/Alert'; +import { ConfirmTx } from 'src/components/ConfirmTX'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { BalanceWarning } from 'src/components/FeeWarning'; +import { AppContext, useBondingContext } from 'src/context'; +import { TBondedNymNode } from 'src/requests/nymNodeDetails'; +import { settingsValidationSchema } from 'src/components/Bonding/forms/nym-node/settingsValidationSchema'; + +export const GeneralNymNodeSettings = ({ bondedNode }: { bondedNode: TBondedNymNode }) => { + const [openConfirmationModal, setOpenConfirmationModal] = useState(false); + const { fee, resetFeeState } = useGetFee(); + const { userBalance } = useContext(AppContext); + const { updateNymNodeConfig } = useBondingContext(); + + const theme = useTheme(); + + const { + register, + handleSubmit, + formState: { errors, isSubmitting, isDirty, isValid }, + } = useForm({ + resolver: yupResolver(settingsValidationSchema), + mode: 'onChange', + defaultValues: { + host: bondedNode.host, + custom_http_port: bondedNode.customHttpPort, + }, + }); + + const onSubmit = async ({ host, custom_http_port }: { host: string; custom_http_port: number | null }) => { + resetFeeState(); + + try { + const NymNodeConfigParams = { + host, + custom_http_port, + restore_default_http_port: custom_http_port === null, + }; + await updateNymNodeConfig(NymNodeConfigParams); + + setOpenConfirmationModal(true); + } catch (error) { + Console.error(error); + } + }; + + return ( + + {fee && ( + + {fee.amount?.amount && userBalance?.balance?.amount.amount && ( + + + + )} + + )} + + + Changing these values will ONLY change the data about your node on the blockchain. + + Remember to change your node’s config file with the same values too. + + } + bgColor={`${theme.palette.nym.nymWallet.text.blue}0D !important`} + dismissable + /> + + + + + Port + + + + + + + + + + + + + Host + + + + + + + + + + + + + + + + + + { + setOpenConfirmationModal(false); + }} + buttonFullWidth + sx={{ + width: '450px', + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }} + headerStyles={{ + width: '100%', + mb: 1, + textAlign: 'center', + color: theme.palette.nym.nymWallet.text.blue, + fontSize: 16, + }} + subHeaderStyles={{ + width: '100%', + mb: 1, + textAlign: 'center', + color: 'main', + fontSize: 14, + }} + /> + + ); +}; diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx index 4f2032ac30..6f8ed79ec9 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/ParametersSettings.tsx @@ -14,7 +14,7 @@ import { Typography, } from '@mui/material'; import { useTheme } from '@mui/material/styles'; -import { CurrencyDenom, MixNodeCostParams } from '@nymproject/types'; +import { CurrencyDenom, NodeCostParams } from '@nymproject/types'; import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; import { isMixnode } from 'src/types'; import { @@ -22,11 +22,9 @@ import { simulateUpdateMixnodeCostParams, simulateVestingUpdateMixnodeCostParams, updateMixnodeCostParams, - vestingUpdateMixnodeCostParams, } from 'src/requests'; -import { TBondedMixnode } from 'src/context/bonding'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; -import { bondedNodeParametersValidationSchema } from 'src/components/Bonding/forms/mixnodeValidationSchema'; +import { bondedNodeParametersValidationSchema } from 'src/components/Bonding/forms/legacyForms/mixnodeValidationSchema'; import { Console } from 'src/utils/console'; import { getIntervalAsDate } from 'src/utils'; import { Alert } from 'src/components/Alert'; @@ -36,6 +34,7 @@ import { useGetFee } from 'src/hooks/useGetFee'; import { ConfirmTx } from 'src/components/ConfirmTX'; import { LoadingModal } from 'src/components/Modals/LoadingModal'; import { InfoOutlined } from '@mui/icons-material'; +import { TBondedMixnode } from 'src/requests/mixnodeDetails'; const operatorCostHint = `This is your (operator) rewards including the PM and cost. Rewards are automatically compounded every epoch.You can redeem your rewards at any time. `; @@ -45,7 +44,7 @@ const profitMarginHint = export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode }): JSX.Element => { const [openConfirmationModal, setOpenConfirmationModal] = useState(false); const [intervalTime, setIntervalTime] = useState(); - const [pendingUpdates, setPendingUpdates] = useState(); + const [pendingUpdates, setPendingUpdates] = useState(); const { clientDetails } = useContext(AppContext); const theme = useTheme(); @@ -115,11 +114,8 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode }, }; try { - if (bondedNode.proxy) { - await vestingUpdateMixnodeCostParams(mixNodeCostParams); - } else { - await updateMixnodeCostParams(mixNodeCostParams); - } + await updateMixnodeCostParams(mixNodeCostParams); + await getPendingEvents(); reset(); setOpenConfirmationModal(true); diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/index.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/index.tsx index b49958ae42..7c1f4568b4 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/index.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/index.tsx @@ -1,12 +1,13 @@ import React, { useState } from 'react'; import { Box, Button, Divider, Grid } from '@mui/material'; -import { isGateway, isMixnode } from 'src/types'; -import { TBondedMixnode, TBondedGateway } from 'src/context/bonding'; +import { isGateway, isMixnode, isNymNode } from 'src/types'; +import { TBondedNode } from 'src/context/bonding'; import { GeneralMixnodeSettings } from './GeneralMixnodeSettings'; import { ParametersSettings } from './ParametersSettings'; import { GeneralGatewaySettings } from './GeneralGatewaySettings'; +import { GeneralNymNodeSettings } from './GeneralNymNodeSettings'; -const makeGeneralNav = (bondedNode: TBondedMixnode | TBondedGateway) => { +const makeGeneralNav = (bondedNode: TBondedNode) => { const navItems = ['Info']; if (isMixnode(bondedNode)) { navItems.push('Parameters'); @@ -15,7 +16,7 @@ const makeGeneralNav = (bondedNode: TBondedMixnode | TBondedGateway) => { return navItems; }; -export const NodeGeneralSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }) => { +export const NodeGeneralSettings = ({ bondedNode }: { bondedNode: TBondedNode }) => { const [navSelection, setNavSelection] = useState(0); const getSettings = () => { @@ -23,10 +24,12 @@ export const NodeGeneralSettings = ({ bondedNode }: { bondedNode: TBondedMixnode case 0: { if (isMixnode(bondedNode)) return ; if (isGateway(bondedNode)) return ; + if (isNymNode(bondedNode)) return ; break; } case 1: { if (isMixnode(bondedNode)) return ; + if (isNymNode(bondedNode)) return null; break; } default: diff --git a/nym-wallet/src/pages/bonding/types.ts b/nym-wallet/src/pages/bonding/types.ts index 0e2b4a6c8d..d2c8d3ae9f 100644 --- a/nym-wallet/src/pages/bonding/types.ts +++ b/nym-wallet/src/pages/bonding/types.ts @@ -1,4 +1,4 @@ -import { DecCoin, MixNodeCostParams, TNodeType, TransactionExecuteResult } from '@nymproject/types'; +import { DecCoin, NodeCostParams, TNodeType, TransactionExecuteResult } from '@nymproject/types'; import { TPoolOption } from 'src/components'; export type FormStep = 1 | 2 | 3 | 4; @@ -61,5 +61,5 @@ export interface BondState { export interface ChangeMixCostParams { mix_id: number; - new_costs: MixNodeCostParams; + new_costs: NodeCostParams; } diff --git a/nym-wallet/src/pages/node-settings/system-variables.tsx b/nym-wallet/src/pages/node-settings/system-variables.tsx index 761d98444c..13fb9a60c6 100644 --- a/nym-wallet/src/pages/node-settings/system-variables.tsx +++ b/nym-wallet/src/pages/node-settings/system-variables.tsx @@ -7,8 +7,7 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { InclusionProbabilityResponse, SelectionChance } from '@nymproject/types'; import { validationSchema } from './validationSchema'; import { InfoTooltip } from '../../components'; -import { useCheckOwnership } from '../../hooks/useCheckOwnership'; -import { updateMixnodeCostParams, vestingUpdateMixnodeCostParams } from '../../requests'; +import { updateMixnodeCostParams } from '../../requests'; import { AppContext } from '../../context'; import { Console } from '../../utils/console'; import { attachDefaultOperatingCost, toPercentFloatString } from '../../utils'; @@ -74,7 +73,6 @@ export const SystemVariables = ({ }) => { const [nodeUpdateResponse, setNodeUpdateResponse] = useState<'success' | 'failed'>(); const { mixnodeDetails } = useContext(AppContext); - const { ownership } = useCheckOwnership(); const { register, @@ -106,12 +104,6 @@ export const SystemVariables = ({ await updateMixnodeCostParams(defaultCostParams); }; - const vestingUpdateMixnodeProfitMargin = async (profitMarginPercent: string) => { - // TODO: this will have to be updated with allowing users to provide their operating cost in the form - const defaultCostParams = await attachDefaultOperatingCost(toPercentFloatString(profitMarginPercent)); - await vestingUpdateMixnodeCostParams(defaultCostParams); - }; - if (!mixnodeDetails) return null; return ( @@ -175,12 +167,7 @@ export const SystemVariables = ({