diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0777368574..ad1f3d4d54 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,6 +29,12 @@ jobs: override: true components: rustfmt, clippy + - name: Check formatting + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + - name: Build all binaries uses: actions-rs/cargo@v1 with: @@ -48,12 +54,6 @@ jobs: command: test args: --workspace --all-features -- --ignored - - name: Check formatting - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check - - uses: actions-rs/clippy-check@v1 name: Clippy checks with: @@ -66,6 +66,8 @@ jobs: command: clippy args: --workspace -- -D warnings + # COCONUT stuff + - name: Build all binaries with coconut enabled uses: actions-rs/cargo@v1 with: @@ -82,4 +84,4 @@ jobs: uses: actions-rs/cargo@v1 with: command: clippy - args: --features=coconut -- -D warnings + args: --all-targets --features=coconut -- -D warnings diff --git a/.github/workflows/contracts-build.yml b/.github/workflows/contracts-build.yml index 84428b01cd..3921bd3a6d 100644 --- a/.github/workflows/contracts-build.yml +++ b/.github/workflows/contracts-build.yml @@ -1,16 +1,21 @@ name: Build release of Nym smart contracts on: workflow_dispatch: - -defaults: - run: - working-directory: contracts + release: + types: [created] jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 + + - name: Check the release tag starts with `nym-contracts-` + if: startsWith(github.ref, 'refs/tags/nym-contracts-') == false && github.event_name != 'workflow_dispatch' + uses: actions/github-script@v3 + with: + script: | + core.setFailed('Release tag did not start with nym-contracts-...') - name: Install Rust stable uses: actions-rs/toolchain@v1 @@ -21,7 +26,7 @@ jobs: components: rustfmt, clippy - name: Build release contracts - run: RUSTFLAGS='-C link-arg=-s' cargo build --release --target wasm32-unknown-unknown + run: make wasm - name: Upload Mixnet Contract Artifact uses: actions/upload-artifact@v3 @@ -36,3 +41,11 @@ jobs: name: vesting_contract.wasm path: contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm retention-days: 5 + + - name: Upload to release based on tag name + uses: softprops/action-gh-release@v1 + if: github.event_name == 'release' + with: + files: | + contracts/target/wasm32-unknown-unknown/release/vesting_contract.wasm + contracts/target/wasm32-unknown-unknown/release/mixnet_contract.wasm diff --git a/.github/workflows/network-explorer-api.yml b/.github/workflows/network-explorer-api.yml index 6464d32b96..2ad184be72 100644 --- a/.github/workflows/network-explorer-api.yml +++ b/.github/workflows/network-explorer-api.yml @@ -19,6 +19,9 @@ jobs: steps: - uses: actions/checkout@v3 + - name: Install Dependencies (Linux) + run: sudo apt-get update && sudo apt-get -y install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools + - name: Check the release tag starts with `nym-explorer-api-` if: startsWith(github.ref, 'refs/tags/nym-explorer-api-') == false && github.event_name != 'workflow_dispatch' uses: actions/github-script@v3 diff --git a/.github/workflows/nightly_build_release.yml b/.github/workflows/nightly_build_release.yml index 0347d1f755..4474b399b6 100644 --- a/.github/workflows/nightly_build_release.yml +++ b/.github/workflows/nightly_build_release.yml @@ -1,31 +1,34 @@ name: Nightly builds on latest release -on: workflow_dispatch +on: + schedule: + - cron: '14 2 * * *' jobs: matrix_prep: runs-on: ubuntu-latest outputs: matrix: ${{ steps.set-matrix.outputs.matrix }} steps: - # creates the matrix strategy from nightly_build_release_matrix.json + # creates the matrix strategy from nightly_build_matrix_includes.json - uses: actions/checkout@v3 - id: set-matrix uses: JoshuaTheMiller/conditional-build-matrix@main with: - inputFile: '.github/workflows/nightly_build_release_matrix.json' + inputFile: '.github/workflows/nightly_build_matrix_includes.json' filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]' get_release: runs-on: ubuntu-latest needs: matrix_prep outputs: - output1: ${{ steps.step2.outputs.lastest_release }} + output1: ${{ steps.step2.outputs.latest_release }} steps: - name: Check out repository code uses: actions/checkout@v3 - + - name: Fetch all branches + run: git fetch --all - name: Set output variable to latest release branch id: step2 - run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 1)" >> $GITHUB_OUTPUT + run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 1 | sed 's/ origin\///')" >> $GITHUB_OUTPUT build: needs: [get_release,matrix_prep] strategy: @@ -40,7 +43,7 @@ jobs: - name: Check out latest release branch uses: actions/checkout@v3 with: - ref: release/v1.1.1 + ref: ${{needs.get_release.outputs.output1}} - name: Install rust toolchain uses: actions-rs/toolchain@v1 @@ -171,7 +174,7 @@ jobs: args: --manifest-path nym-wallet/Cargo.toml --workspace --all-targets -- -D warnings notification: - needs: build + needs: [build,get_release] runs-on: ubuntu-latest steps: - name: Collect jobs status @@ -189,7 +192,7 @@ jobs: NYM_PROJECT_NAME: "Nym nightly build on latest release" GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" - GIT_BRANCH: "${GITHUB_REF##*/}" + GIT_BRANCH: "https://github.com/nymtech/nym/tree/${{needs.get_release.outputs.output1}}" KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}" KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}" KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}" diff --git a/.github/workflows/nightly_build_release2.yml b/.github/workflows/nightly_build_release2.yml new file mode 100644 index 0000000000..71f89948c9 --- /dev/null +++ b/.github/workflows/nightly_build_release2.yml @@ -0,0 +1,203 @@ +name: Nightly builds on second latest release + +on: + schedule: + - cron: '24 2 * * *' +jobs: + matrix_prep: + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + steps: + # creates the matrix strategy from nightly_build_matrix_includes.json + - uses: actions/checkout@v3 + - id: set-matrix + uses: JoshuaTheMiller/conditional-build-matrix@main + with: + inputFile: '.github/workflows/nightly_build_matrix_includes.json' + filter: '[?runOnEvent==`${{ github.event_name }}` || runOnEvent==`always`]' + get_release: + runs-on: ubuntu-latest + needs: matrix_prep + outputs: + output1: ${{ steps.step2.outputs.latest_release }} + steps: + - name: Check out repository code + uses: actions/checkout@v3 + - name: Fetch all branches + run: git fetch --all + - name: Set output variable to latest release branch + id: step2 + run: echo "latest_release=$(git branch -r | grep -E 'release/v[0-9]+\.[0-9]+\.[0-9]+' | tail -n 2 | head -n 1 | sed 's/ origin\///')" >> $GITHUB_OUTPUT + build: + needs: [get_release,matrix_prep] + strategy: + matrix: ${{fromJson(needs.matrix_prep.outputs.matrix)}} + runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.rust == 'nightly' || matrix.rust == 'beta' || matrix.rust == 'stable' }} + steps: + - name: Install Dependencies (Linux) + run: sudo apt-get update && sudo apt-get install libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libudev-dev squashfs-tools + if: matrix.os == 'ubuntu-latest' + + - name: Check out latest release branch + uses: actions/checkout@v3 + with: + ref: ${{needs.get_release.outputs.output1}} + + - name: Install rust toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: ${{ matrix.rust }} + override: true + components: rustfmt, clippy + + - name: Build all binaries + uses: actions-rs/cargo@v1 + with: + command: build + args: --workspace + + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + + - name: Run all tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --workspace + + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + + - name: Run expensive tests + if: github.ref == 'refs/heads/develop' || github.event.pull_request.base.ref == 'develop' || github.event.pull_request.base.ref == 'master' + uses: actions-rs/cargo@v1 + with: + command: test + args: --workspace --all-features -- --ignored + + - name: Check formatting + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + + - uses: actions-rs/clippy-check@v1 + name: Clippy checks + with: + token: ${{ secrets.GITHUB_TOKEN }} + args: --all-features + + - name: Run clippy + uses: actions-rs/cargo@v1 + if: ${{ matrix.rust != 'nightly' }} + with: + command: clippy + args: --workspace --all-targets -- -D warnings + + - name: Reclaim some disk space + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' || matrix.os == 'ubuntu-latest' }} + with: + command: clean + + # COCONUT stuff + - name: Build all binaries with coconut enabled + uses: actions-rs/cargo@v1 + with: + command: build + args: --workspace --features=coconut + + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + + - name: Run all tests with coconut enabled + uses: actions-rs/cargo@v1 + with: + command: test + args: --workspace --features=coconut + + - name: Reclaim some disk space (because Windows is being annoying) + uses: actions-rs/cargo@v1 + if: ${{ matrix.os == 'windows-latest' }} + with: + command: clean + + - name: Run clippy with coconut enabled + uses: actions-rs/cargo@v1 + if: ${{ matrix.rust != 'nightly' }} + with: + command: clippy + args: --workspace --all-targets --features=coconut -- -D warnings + + # nym-wallet (the rust part) + - name: Build nym-wallet rust code + uses: actions-rs/cargo@v1 + with: + command: build + args: --manifest-path nym-wallet/Cargo.toml --workspace + + - name: Run nym-wallet tests + uses: actions-rs/cargo@v1 + with: + command: test + args: --manifest-path nym-wallet/Cargo.toml --workspace + + - name: Check nym-wallet formatting + uses: actions-rs/cargo@v1 + with: + command: fmt + args: --manifest-path nym-wallet/Cargo.toml --all -- --check + + - name: Run clippy for nym-wallet + uses: actions-rs/cargo@v1 + if: ${{ matrix.rust != 'nightly' }} + with: + command: clippy + args: --manifest-path nym-wallet/Cargo.toml --workspace --all-targets -- -D warnings + + notification: + needs: [build,get_release] + runs-on: ubuntu-latest + steps: + - name: Collect jobs status + uses: technote-space/workflow-conclusion-action@v2 + - name: Check out repository code + uses: actions/checkout@v3 + - name: Keybase - Node Install + if: env.WORKFLOW_CONCLUSION == 'failure' + run: npm install + working-directory: .github/workflows/support-files + - name: Keybase - Send Notification + if: env.WORKFLOW_CONCLUSION == 'failure' + env: + NYM_NOTIFICATION_KIND: nightly + NYM_PROJECT_NAME: "Nym nightly build on latest release" + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" + GIT_BRANCH: "https://github.com/nymtech/nym/tree/${{needs.get_release.outputs.output1}}" + KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}" + KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}" + KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}" + KEYBASE_NYM_CHANNEL: "ci-nightly-release" + IS_SUCCESS: "${{ env.WORKFLOW_CONCLUSION == 'success' }}" + uses: docker://keybaseio/client:stable-node + with: + args: .github/workflows/support-files/notifications/entry_point.sh diff --git a/.github/workflows/nightly_build_release_matrix.json b/.github/workflows/nightly_build_release_matrix.json deleted file mode 100644 index c292817b6a..0000000000 --- a/.github/workflows/nightly_build_release_matrix.json +++ /dev/null @@ -1,50 +0,0 @@ -[ - { - "os":"ubuntu-latest", - "rust":"stable", - "runOnEvent":"workflow_dispatch" - }, - - { - "os":"windows-latest", - "rust":"stable", - "runOnEvent":"workflow_dispatch" - }, - { - "os":"macos-latest", - "rust":"stable", - "runOnEvent":"workflow_dispatch" - }, - - { - "os":"ubuntu-latest", - "rust":"beta", - "runOnEvent":"workflow_dispatch" - }, - { - "os":"windows-latest", - "rust":"beta", - "runOnEvent":"workflow_dispatch" - }, - { - "os":"macos-latest", - "rust":"beta", - "runOnEvent":"workflow_dispatch" - }, - - { - "os":"ubuntu-latest", - "rust":"nightly", - "runOnEvent":"workflow_dispatch" - }, - { - "os":"windows-latest", - "rust":"nightly", - "runOnEvent":"workflow_dispatch" - }, - { - "os":"macos-latest", - "rust":"nightly", - "runOnEvent":"workflow_dispatch" - } -] diff --git a/.github/workflows/nym-connect.yml b/.github/workflows/nym-connect.yml index 3a08744fc9..5a652e92d2 100644 --- a/.github/workflows/nym-connect.yml +++ b/.github/workflows/nym-connect.yml @@ -41,19 +41,19 @@ jobs: - name: Keybase - Node Install run: npm install working-directory: .github/workflows/support-files -# - name: Keybase - Send Notification -# env: -# NYM_NOTIFICATION_KIND: nym-connect -# NYM_PROJECT_NAME: "nym-connect" -# NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}" -# NYM_CI_WWW_LOCATION: "nym-connect-${{ env.GITHUB_REF_SLUG }}" -# GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" -# GIT_BRANCH: "${GITHUB_REF##*/}" -# KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}" -# KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}" -# KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}" -# KEYBASE_NYM_CHANNEL: "ci-nym-connect" -# IS_SUCCESS: "${{ job.status == 'success' }}" -# uses: docker://keybaseio/client:stable-node -# with: -# args: .github/workflows/support-files/notifications/entry_point.sh + - name: Keybase - Send Notification + env: + NYM_NOTIFICATION_KIND: nym-connect + NYM_PROJECT_NAME: "nym-connect" + NYM_CI_WWW_BASE: "${{ secrets.NYM_CI_WWW_BASE }}" + NYM_CI_WWW_LOCATION: "nym-connect-${{ env.GITHUB_REF_SLUG }}" + GIT_COMMIT_MESSAGE: "${{ github.event.head_commit.message }}" + GIT_BRANCH: "${GITHUB_REF##*/}" + KEYBASE_NYMBOT_USERNAME: "${{ secrets.KEYBASE_NYMBOT_USERNAME }}" + KEYBASE_NYMBOT_PAPERKEY: "${{ secrets.KEYBASE_NYMBOT_PAPERKEY }}" + KEYBASE_NYMBOT_TEAM: "${{ secrets.KEYBASE_NYMBOT_TEAM }}" + KEYBASE_NYM_CHANNEL: "ci-nym-connect" + IS_SUCCESS: "${{ job.status == 'success' }}" + uses: docker://keybaseio/client:stable-node + with: + args: .github/workflows/support-files/notifications/entry_point.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 369c290aab..d32a204ccb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,12 +7,34 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// ### Added - binaries: add `-c` shortform for `--config-env-file` +- websocket-requests: add server response signalling current packet queue length in the client +- contracts: DKG contract that handles coconut key generation ([#1678][#1708][#1747]) +- validator-api: generate coconut keys interactively, using DKG and multisig contracts ([#1678][#1708][#1747]) ### Changed - clients: add concept of transmission lanes to better handle multiple data streams ([#1720]) +- clients,validator-api: take coconut signers from the chain instead of specifying them via CLI ([#1747]) +- multisig contract: add DKG contract to the list of addresses that can create proposals ([#1747]) +- socks5-client: wait closing inbound connection until data is sent, and throttle incoming data in general ([#1783]) +- nym-cli: improve error reporting/handling and changed `vesting-schedule` queries to use query client instead of signing client +### Fixed + +- gateway-client: fix decrypting stored messages on reconnect ([#1786]) + +### Fixed + +- gateway-client: fix decrypting stored messages on reconnect ([#1786]) +- socks5-client: fix shutting down all tasks if anyone of them panics or errors out ([#1805]) + +[#1678]: https://github.com/nymtech/nym/pull/1678 +[#1708]: https://github.com/nymtech/nym/pull/1708 [#1720]: https://github.com/nymtech/nym/pull/1720 +[#1747]: https://github.com/nymtech/nym/pull/1747 +[#1783]: https://github.com/nymtech/nym/pull/1783 +[#1786]: https://github.com/nymtech/nym/pull/1786 +[#1805]: https://github.com/nymtech/nym/pull/1805 ## [v1.1.0](https://github.com/nymtech/nym/tree/v1.1.0) (2022-11-09) diff --git a/Cargo.lock b/Cargo.lock index 2da304990a..cafc9e6695 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,6 +581,7 @@ name = "client-connections" version = "0.1.0" dependencies = [ "futures", + "log", ] [[package]] @@ -635,6 +636,18 @@ dependencies = [ "serde", ] +[[package]] +name = "coconut-dkg-common" +version = "0.1.0" +dependencies = [ + "contracts-common", + "cosmwasm-std", + "cw-utils", + "multisig-contract-common", + "schemars", + "serde", +] + [[package]] name = "coconut-interface" version = "0.1.0" @@ -744,7 +757,9 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" name = "contracts-common" version = "0.1.0" dependencies = [ + "bs58", "cosmwasm-std", + "dkg", "schemars", "serde", "serde_json", @@ -950,7 +965,6 @@ dependencies = [ "crypto", "rand 0.7.3", "thiserror", - "url", "validator-api-requests", "validator-client", ] @@ -1229,9 +1243,9 @@ dependencies = [ [[package]] name = "cw-utils" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "babd2c090f39d07ce5bf2556962305e795daa048ce20a93709eb591476e4a29e" +checksum = "9dbaecb78c8e8abfd6b4258c7f4fbeb5c49a5e45ee4d910d3240ee8e1d714e1b" dependencies = [ "cosmwasm-std", "schemars", @@ -1240,10 +1254,22 @@ dependencies = [ ] [[package]] -name = "cw3" -version = "0.13.2" +name = "cw2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f871854338a54c7bb094d16ffe17212b93b146d9659dbce4c9402a9b77e240ef" +checksum = "04cf4639517490dd36b333bbd6c4fbd92e325fd0acf4683b41753bc5eb63bfc1" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", +] + +[[package]] +name = "cw3" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe19462a7f644ba60c19d3443cb90d00c50d9b6b3b0a3a7fca93df8261af979b" dependencies = [ "cosmwasm-std", "cw-utils", @@ -1252,10 +1278,26 @@ dependencies = [ ] [[package]] -name = "cw4" -version = "0.13.2" +name = "cw3-fixed-multisig" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4476d6a7c13c46ed9ff260bd0e1cf648dc37b13f483822e1ff2a431f0f6ee52" +checksum = "df54aa54c13f405ec4ab36b6217538bc957d439eee58f89312db05a79caf6706" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw3", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw4" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0acc3549d5ce11c6901b3a676f2e2628684722197054d97cd0101ea174ed5cbd" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -1410,6 +1452,7 @@ dependencies = [ "ff 0.11.0", "group 0.11.0", "lazy_static", + "pemstore", "rand 0.8.5", "rand_chacha 0.3.1", "rand_core 0.6.3", @@ -1461,9 +1504,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "1.4.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d5c4b5e5959dc2c2b89918d8e2cc40fcdd623cef026ed09d2f0ee05199dc8e4" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" dependencies = [ "serde", "signature", @@ -2938,6 +2981,7 @@ dependencies = [ "cosmwasm-std", "cw-utils", "cw3", + "cw3-fixed-multisig", "cw4", "schemars", "serde", @@ -3095,6 +3139,7 @@ dependencies = [ "pretty_env_logger", "serde", "serde_json", + "tap", "tokio", "validator-client", ] @@ -3120,6 +3165,7 @@ dependencies = [ "rand 0.6.5", "serde", "serde_json", + "tap", "thiserror", "time 0.3.14", "toml", @@ -3155,6 +3201,7 @@ dependencies = [ "serde", "serde_json", "sled", + "tap", "task", "thiserror", "tokio", @@ -3334,6 +3381,7 @@ dependencies = [ "serde", "snafu 0.6.10", "socks5-requests", + "tap", "task", "thiserror", "tokio", @@ -3376,9 +3424,11 @@ version = "1.1.0" dependencies = [ "anyhow", "async-trait", + "bs58", "cfg-if 1.0.0", "clap 3.2.8", "coconut-bandwidth-contract-common", + "coconut-dkg-common", "coconut-interface", "config", "console-subscriber", @@ -3390,6 +3440,7 @@ dependencies = [ "cw-utils", "cw3", "dirs", + "dkg", "dotenv", "futures", "gateway-client", @@ -3403,6 +3454,7 @@ dependencies = [ "nymcoconut", "nymsphinx", "okapi", + "pemstore", "pin-project", "pretty_env_logger", "rand 0.7.3", @@ -3455,16 +3507,19 @@ name = "nymcoconut" version = "0.5.0" dependencies = [ "bincode", - "bls12_381 0.5.0", + "bls12_381 0.6.0", "bs58", "criterion", "digest 0.9.0", + "dkg", "doc-comment", - "ff 0.10.1", + "ff 0.11.0", "getrandom 0.2.6", - "group 0.10.0", + "group 0.11.0", "itertools", + "pemstore", "rand 0.8.5", + "rand_chacha 0.3.1", "serde", "serde_derive", "sha2 0.9.9", @@ -5640,7 +5695,9 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" name = "task" version = "0.1.0" dependencies = [ + "futures", "log", + "thiserror", "tokio", ] @@ -5780,18 +5837,18 @@ checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" [[package]] name = "thiserror" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", @@ -6419,6 +6476,7 @@ dependencies = [ "base64", "bip39", "coconut-bandwidth-contract-common", + "coconut-dkg-common", "coconut-interface", "colored", "config", diff --git a/Cargo.toml b/Cargo.toml index 4ab19fc70e..e03bd46aeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ members = [ "common/commands", "common/config", "common/cosmwasm-smart-contracts/coconut-bandwidth-contract", + "common/cosmwasm-smart-contracts/coconut-dkg", "common/cosmwasm-smart-contracts/contracts-common", "common/cosmwasm-smart-contracts/mixnet-contract", "common/cosmwasm-smart-contracts/multisig-contract", diff --git a/clients/client-core/src/client/cover_traffic_stream.rs b/clients/client-core/src/client/cover_traffic_stream.rs index ee3b7851cb..228234663a 100644 --- a/clients/client-core/src/client/cover_traffic_stream.rs +++ b/clients/client-core/src/client/cover_traffic_stream.rs @@ -228,7 +228,9 @@ impl LoopCoverTrafficStream { } } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("LoopCoverTrafficStream: Exiting"); }) } diff --git a/clients/client-core/src/client/inbound_messages.rs b/clients/client-core/src/client/inbound_messages.rs index 3cc71d4460..909e3720f9 100644 --- a/clients/client-core/src/client/inbound_messages.rs +++ b/clients/client-core/src/client/inbound_messages.rs @@ -1,10 +1,9 @@ use client_connections::TransmissionLane; -use futures::channel::mpsc; use nymsphinx::addressing::clients::Recipient; use nymsphinx::anonymous_replies::ReplySurb; -pub type InputMessageSender = mpsc::UnboundedSender; -pub type InputMessageReceiver = mpsc::UnboundedReceiver; +pub type InputMessageSender = tokio::sync::mpsc::Sender; +pub type InputMessageReceiver = tokio::sync::mpsc::Receiver; #[derive(Debug)] pub enum InputMessage { diff --git a/clients/client-core/src/client/mix_traffic.rs b/clients/client-core/src/client/mix_traffic.rs index d5702cc0a8..06846316cc 100644 --- a/clients/client-core/src/client/mix_traffic.rs +++ b/clients/client-core/src/client/mix_traffic.rs @@ -69,6 +69,8 @@ impl MixTrafficController { #[cfg(not(target_arch = "wasm32"))] pub fn start_with_shutdown(mut self, mut shutdown: task::ShutdownListener) { + use std::time::Duration; + spawn_future(async move { debug!("Started MixTrafficController with graceful shutdown support"); @@ -88,7 +90,9 @@ impl MixTrafficController { } } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("MixTrafficController: Exiting"); }) } diff --git a/clients/client-core/src/client/mod.rs b/clients/client-core/src/client/mod.rs index bce1a089dc..309be529f1 100644 --- a/clients/client-core/src/client/mod.rs +++ b/clients/client-core/src/client/mod.rs @@ -1,5 +1,3 @@ -use std::sync::atomic::AtomicBool; - pub mod cover_traffic_stream; pub mod inbound_messages; pub mod key_manager; @@ -9,10 +7,3 @@ pub mod received_buffer; #[cfg(feature = "reply-surb")] pub mod reply_key_storage; pub mod topology_control; - -// This is *NOT* used to signal shutdown. -// It's critical that we don't have any tasks finishing early, this is an additional safety check -// that tasks exiting are doing so because shutdown has been signalled, and no other reason. -// In particular for tasks that rely on their associated channel being closed to signal shutdown, -// and don't have access to a shutdown listener channel. -pub static SHUTDOWN_HAS_BEEN_SIGNALLED: AtomicBool = AtomicBool::new(false); diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs index 9b285d8444..dff476ad3c 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/acknowledgement_listener.rs @@ -72,6 +72,8 @@ impl AcknowledgementListener { #[cfg(not(target_arch = "wasm32"))] pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + use std::time::Duration; + debug!("Started AcknowledgementListener with graceful shutdown support"); while !shutdown.is_shutdown() { @@ -88,7 +90,9 @@ impl AcknowledgementListener { } } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("AcknowledgementListener: Exiting"); } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs index 2b21639230..822cea0a2f 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/action_controller.rs @@ -272,7 +272,9 @@ impl ActionController { } } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("ActionController: Exiting"); } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs index 20cbc10de5..c58967104c 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/input_message_listener.rs @@ -9,7 +9,6 @@ use crate::client::{ topology_control::TopologyAccessor, }; use client_connections::TransmissionLane; -use futures::StreamExt; use log::*; use nymsphinx::anonymous_replies::ReplySurb; use nymsphinx::preparer::MessagePreparer; @@ -189,18 +188,21 @@ where if let Some(real_messages) = real_messages { // tells real message sender (with the poisson timer) to send this to the mix network self.real_message_sender - .unbounded_send((real_messages, lane)) - .unwrap(); + .send((real_messages, lane)) + .await + .expect("BatchRealMessageReceiver has stopped receiving!"); } } #[cfg(not(target_arch = "wasm32"))] pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + use std::time::Duration; + debug!("Started InputMessageListener with graceful shutdown support"); while !shutdown.is_shutdown() { tokio::select! { - input_msg = self.input_receiver.next() => match input_msg { + input_msg = self.input_receiver.recv() => match input_msg { Some(input_msg) => { self.on_input_message(input_msg).await; }, @@ -214,14 +216,16 @@ where } } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("InputMessageListener: Exiting"); } #[cfg(target_arch = "wasm32")] pub(super) async fn run(&mut self) { debug!("Started InputMessageListener without graceful shutdown support"); - while let Some(input_msg) = self.input_receiver.next().await { + while let Some(input_msg) = self.input_receiver.recv().await { self.on_input_message(input_msg).await; } } diff --git a/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs b/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs index 8a26809c79..d0fd805d22 100644 --- a/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs +++ b/clients/client-core/src/client/real_messages_control/acknowledgement_control/retransmission_request_listener.rs @@ -117,15 +117,18 @@ where // send to `OutQueueControl` to eventually send to the mix network self.real_message_sender - .unbounded_send(( + .send(( vec![RealMessage::new(prepared_fragment.mix_packet, frag_id)], TransmissionLane::Retransmission, )) - .unwrap(); + .await + .expect("BatchRealMessageReceiver has stopped receiving!"); } #[cfg(not(target_arch = "wasm32"))] pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + use std::time::Duration; + debug!("Started RetransmissionRequestListener with graceful shutdown support"); while !shutdown.is_shutdown() { @@ -142,7 +145,9 @@ where } } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("RetransmissionRequestListener: Exiting"); } diff --git a/clients/client-core/src/client/real_messages_control/mod.rs b/clients/client-core/src/client/real_messages_control/mod.rs index a7c03dc1c0..ffeeea61e3 100644 --- a/clients/client-core/src/client/real_messages_control/mod.rs +++ b/clients/client-core/src/client/real_messages_control/mod.rs @@ -8,13 +8,15 @@ use self::{ acknowledgement_control::AcknowledgementController, real_traffic_stream::OutQueueControl, }; -use crate::client::real_messages_control::acknowledgement_control::AcknowledgementControllerConnectors; -use crate::client::{ - inbound_messages::InputMessageReceiver, mix_traffic::BatchMixMessageSender, - topology_control::TopologyAccessor, +use crate::{ + client::{ + inbound_messages::InputMessageReceiver, mix_traffic::BatchMixMessageSender, + real_messages_control::acknowledgement_control::AcknowledgementControllerConnectors, + topology_control::TopologyAccessor, + }, + spawn_future, }; -use crate::spawn_future; -use client_connections::ClosedConnectionReceiver; +use client_connections::{ConnectionCommandReceiver, LaneQueueLengths}; use futures::channel::mpsc; use gateway_client::AcknowledgementReceiver; use log::*; @@ -104,6 +106,7 @@ where // obviously when we finally make shared rng that is on 'higher' level, this should become // generic `R` impl RealMessagesController { + #[allow(clippy::too_many_arguments)] pub fn new( config: Config, ack_receiver: AcknowledgementReceiver, @@ -111,11 +114,12 @@ impl RealMessagesController { mix_sender: BatchMixMessageSender, topology_access: TopologyAccessor, #[cfg(feature = "reply-surb")] reply_key_storage: ReplyKeyStorage, - closed_connection_rx: ClosedConnectionReceiver, + lane_queue_lengths: LaneQueueLengths, + client_connection_rx: ConnectionCommandReceiver, ) -> Self { let rng = OsRng; - let (real_message_sender, real_message_receiver) = mpsc::unbounded(); + let (real_message_sender, real_message_receiver) = tokio::sync::mpsc::channel(1); let (sent_notifier_tx, sent_notifier_rx) = mpsc::unbounded(); let ack_controller_connectors = AcknowledgementControllerConnectors::new( @@ -161,7 +165,8 @@ impl RealMessagesController { rng, config.self_recipient, topology_access, - closed_connection_rx, + lane_queue_lengths, + client_connection_rx, ); RealMessagesController { diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs index a42c24892e..5b9bf6ed5b 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream.rs @@ -4,8 +4,9 @@ use crate::client::mix_traffic::BatchMixMessageSender; use crate::client::real_messages_control::acknowledgement_control::SentPacketNotificationSender; use crate::client::topology_control::TopologyAccessor; -use client_connections::{ClosedConnectionReceiver, ConnectionId, TransmissionLane}; -use futures::channel::mpsc; +use client_connections::{ + ConnectionCommand, ConnectionCommandReceiver, ConnectionId, LaneQueueLengths, TransmissionLane, +}; use futures::task::{Context, Poll}; use futures::{Future, Stream, StreamExt}; use log::*; @@ -133,9 +134,13 @@ where /// Incoming channel for being notified of closed connections, so that we can close lanes /// corresponding to connections. To avoid sending traffic unnecessary - closed_connection_rx: ClosedConnectionReceiver, + client_connection_rx: ConnectionCommandReceiver, + + /// Report queue lengths so that upstream can backoff sending data, and keep connections open. + lane_queue_lengths: LaneQueueLengths, } +#[derive(Debug)] pub(crate) struct RealMessage { mix_packet: MixPacket, fragment_id: FragmentIdentifier, @@ -153,8 +158,8 @@ impl RealMessage { // messages are already prepared, etc. the real point of it is to forward it to mix_traffic // after sufficient delay pub(crate) type BatchRealMessageSender = - mpsc::UnboundedSender<(Vec, TransmissionLane)>; -type BatchRealMessageReceiver = mpsc::UnboundedReceiver<(Vec, TransmissionLane)>; + tokio::sync::mpsc::Sender<(Vec, TransmissionLane)>; +type BatchRealMessageReceiver = tokio::sync::mpsc::Receiver<(Vec, TransmissionLane)>; pub(crate) enum StreamMessage { Cover, @@ -177,7 +182,8 @@ where rng: R, our_full_destination: Recipient, topology_access: TopologyAccessor, - closed_connection_rx: ClosedConnectionReceiver, + lane_queue_lengths: LaneQueueLengths, + client_connection_rx: ConnectionCommandReceiver, ) -> Self { OutQueueControl { config, @@ -191,7 +197,8 @@ where rng, topology_access, transmission_buffer: Default::default(), - closed_connection_rx, + client_connection_rx, + lane_queue_lengths, } } @@ -247,7 +254,7 @@ where }; if let Err(err) = self.mix_tx.send(vec![next_message]).await { - log::error!("Failed to send - channel closed: {}", err); + log::error!("Failed to send: {}", err); } // notify ack controller about sending our message only after we actually managed to push it @@ -256,7 +263,7 @@ where self.sent_notify(fragment_id); } - // In addition to closing connections on receiving messages throught closed_connection_rx, + // In addition to closing connections on receiving messages throught client_connection_rx, // also close connections when sufficiently stale. self.transmission_buffer.prune_stale_connections(); @@ -309,6 +316,17 @@ where } } + fn pop_next_message(&mut self) -> Option { + // Pop the next message from the transmission buffer + let (lane, real_next) = self.transmission_buffer.pop_next_message_at_random()?; + + // Update the published queue length + let lane_length = self.transmission_buffer.lane_length(&lane); + self.lane_queue_lengths.set(&lane, lane_length); + + Some(real_next) + } + fn poll_poisson(&mut self, cx: &mut Context<'_>) -> Poll> { // The average delay could change depending on if backpressure in the downstream channel // (mix_tx) was detected. @@ -318,8 +336,11 @@ where // Start by checking if we have any incoming messages about closed connections // NOTE: this feels a bit iffy, the `OutQueueControl` is getting ripe for a rewrite to // something simpler. - if let Poll::Ready(Some(id)) = Pin::new(&mut self.closed_connection_rx).poll_next(cx) { - self.on_close_connection(id); + if let Poll::Ready(Some(id)) = Pin::new(&mut self.client_connection_rx).poll_next(cx) { + match id { + ConnectionCommand::Close(id) => self.on_close_connection(id), + ConnectionCommand::ActiveConnections(_) => panic!(), + } } if let Some(ref mut next_delay) = &mut self.next_delay { @@ -350,7 +371,7 @@ where // in `Vec`, this ensures that on average we will fetch messages faster than we can // send, which is a condition for being able to multiplex sphinx packets from multiple // data streams. - match Pin::new(&mut self.real_receiver).poll_next(cx) { + match Pin::new(&mut self.real_receiver).poll_recv(cx) { // in the case our real message channel stream was closed, we should also indicate we are closed // (and whoever is using the stream should panic) Poll::Ready(None) => Poll::Ready(None), @@ -359,16 +380,13 @@ where log::trace!("handling real_messages: size: {}", real_messages.len()); self.transmission_buffer.store(&conn_id, real_messages); - let real_next = self - .transmission_buffer - .pop_next_message_at_random() - .expect("we just added one"); + let real_next = self.pop_next_message().expect("Just stored one"); Poll::Ready(Some(StreamMessage::Real(Box::new(real_next)))) } Poll::Pending => { - if let Some(real_next) = self.transmission_buffer.pop_next_message_at_random() { + if let Some(real_next) = self.pop_next_message() { Poll::Ready(Some(StreamMessage::Real(Box::new(real_next)))) } else { // otherwise construct a dummy one @@ -397,11 +415,14 @@ where fn poll_immediate(&mut self, cx: &mut Context<'_>) -> Poll> { // Start by checking if we have any incoming messages about closed connections - if let Poll::Ready(Some(id)) = Pin::new(&mut self.closed_connection_rx).poll_next(cx) { - self.on_close_connection(id); + if let Poll::Ready(Some(id)) = Pin::new(&mut self.client_connection_rx).poll_next(cx) { + match id { + ConnectionCommand::Close(id) => self.on_close_connection(id), + ConnectionCommand::ActiveConnections(_) => panic!(), + } } - match Pin::new(&mut self.real_receiver).poll_next(cx) { + match Pin::new(&mut self.real_receiver).poll_recv(cx) { // in the case our real message channel stream was closed, we should also indicate we are closed // (and whoever is using the stream should panic) Poll::Ready(None) => Poll::Ready(None), @@ -411,16 +432,13 @@ where // First store what we got for the given connection id self.transmission_buffer.store(&conn_id, real_messages); - let real_next = self - .transmission_buffer - .pop_next_message_at_random() - .expect("we just added one"); + let real_next = self.pop_next_message().expect("we just added one"); Poll::Ready(Some(StreamMessage::Real(Box::new(real_next)))) } Poll::Pending => { - if let Some(real_next) = self.transmission_buffer.pop_next_message_at_random() { + if let Some(real_next) = self.pop_next_message() { Poll::Ready(Some(StreamMessage::Real(Box::new(real_next)))) } else { Poll::Pending @@ -447,16 +465,32 @@ where let lanes = self.transmission_buffer.num_lanes(); let mult = self.sending_delay_controller.current_multiplier(); let delay = self.current_average_message_sending_delay().as_millis(); - if self.config.disable_poisson_packet_distribution { - log::info!( + let status_str = if self.config.disable_poisson_packet_distribution { + format!( "Status: {lanes} lanes, backlog: {:.2} kiB ({packets}), no delay", backlog - ); + ) } else { - log::info!( + format!( "Status: {lanes} lanes, backlog: {:.2} kiB ({packets}), avg delay: {}ms ({mult})", - backlog, - delay + backlog, delay + ) + }; + if packets > 1000 { + log::warn!("{status_str}"); + } else if packets > 0 { + log::info!("{status_str}"); + } else { + log::debug!("{status_str}"); + } + } + + #[cfg(not(target_arch = "wasm32"))] + fn log_status_infrequent(&self) { + if self.sending_delay_controller.current_multiplier() > 1 { + log::warn!( + "Unable to send packets fast enough - sending delay multiplier set to: {}", + self.sending_delay_controller.current_multiplier() ); } } @@ -465,7 +499,8 @@ where pub(super) async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { debug!("Started OutQueueControl with graceful shutdown support"); - let mut status_timer = tokio::time::interval(Duration::from_secs(1)); + let mut status_timer = tokio::time::interval(Duration::from_secs(5)); + let mut infrequent_status_timer = tokio::time::interval(Duration::from_secs(60)); while !shutdown.is_shutdown() { tokio::select! { @@ -476,6 +511,9 @@ where _ = status_timer.tick() => { self.log_status(); } + _ = infrequent_status_timer.tick() => { + self.log_status_infrequent(); + } next_message = self.next() => if let Some(next_message) = next_message { self.on_message(next_message).await; } else { @@ -484,7 +522,9 @@ where } } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("OutQueueControl: Exiting"); } diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs index c90f7b4983..b22ffae7ff 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream/sending_delay_controller.rs @@ -82,7 +82,7 @@ impl SendingDelayController { self.current_multiplier = (self.current_multiplier + 1).clamp(self.lower_bound, self.upper_bound); self.time_when_changed = get_time_now(); - log::debug!( + log::warn!( "Increasing sending delay multiplier to: {}", self.current_multiplier ); diff --git a/clients/client-core/src/client/real_messages_control/real_traffic_stream/transmission_buffer.rs b/clients/client-core/src/client/real_messages_control/real_traffic_stream/transmission_buffer.rs index 2510a65e05..b47aaf470e 100644 --- a/clients/client-core/src/client/real_messages_control/real_traffic_stream/transmission_buffer.rs +++ b/clients/client-core/src/client/real_messages_control/real_traffic_stream/transmission_buffer.rs @@ -41,6 +41,10 @@ impl TransmissionBuffer { self.buffer.keys().count() } + pub(crate) fn lane_length(&self, lane: &TransmissionLane) -> Option { + self.buffer.get(lane).map(LaneBufferEntry::len) + } + #[allow(unused)] pub(crate) fn connections(&self) -> HashSet { self.buffer @@ -127,7 +131,7 @@ impl TransmissionBuffer { Some(real_next) } - pub(crate) fn pop_next_message_at_random(&mut self) -> Option { + pub(crate) fn pop_next_message_at_random(&mut self) -> Option<(TransmissionLane, RealMessage)> { if self.buffer.is_empty() { return None; } @@ -142,8 +146,9 @@ impl TransmissionBuffer { *self.pick_random_lane()? }; + let msg = self.pop_front_from_lane(&lane)?; log::trace!("picking to send from lane: {:?}", lane); - self.pop_front_from_lane(&lane) + Some((lane, msg)) } pub(crate) fn prune_stale_connections(&mut self) { @@ -196,7 +201,6 @@ impl LaneBufferEntry { > Duration::from_secs(MSG_CONSIDERED_STALE_AFTER_SECS) } - #[cfg(not(target_arch = "wasm32"))] fn len(&self) -> usize { self.real_messages.len() } diff --git a/clients/client-core/src/client/received_buffer.rs b/clients/client-core/src/client/received_buffer.rs index fa61b9b9a0..7e5a0d7db8 100644 --- a/clients/client-core/src/client/received_buffer.rs +++ b/clients/client-core/src/client/received_buffer.rs @@ -322,6 +322,8 @@ impl RequestReceiver { #[cfg(not(target_arch = "wasm32"))] async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + use std::time::Duration; + debug!("Started RequestReceiver with graceful shutdown support"); while !shutdown.is_shutdown() { tokio::select! { @@ -340,7 +342,9 @@ impl RequestReceiver { }, } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("RequestReceiver: Exiting"); } @@ -372,6 +376,8 @@ impl FragmentedMessageReceiver { #[cfg(not(target_arch = "wasm32"))] async fn run_with_shutdown(&mut self, mut shutdown: task::ShutdownListener) { + use std::time::Duration; + debug!("Started FragmentedMessageReceiver with graceful shutdown support"); while !shutdown.is_shutdown() { tokio::select! { @@ -389,7 +395,9 @@ impl FragmentedMessageReceiver { } } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("FragmentedMessageReceiver: Exiting"); } diff --git a/clients/client-core/src/client/reply_key_storage.rs b/clients/client-core/src/client/reply_key_storage.rs index a717f44ea2..bfe1c43b9b 100644 --- a/clients/client-core/src/client/reply_key_storage.rs +++ b/clients/client-core/src/client/reply_key_storage.rs @@ -8,10 +8,13 @@ use nymsphinx::anonymous_replies::{ }; use std::path::Path; -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum ReplyKeyStorageError { + #[error("DB Read Error: {0}")] DbReadError(sled::Error), + #[error("DB Write Error: {0}")] DbWriteError(sled::Error), + #[error("DB Open Error: {0}")] DbOpenError(sled::Error), } diff --git a/clients/client-core/src/client/topology_control.rs b/clients/client-core/src/client/topology_control.rs index 3b0fc4afc3..b685cfb8b4 100644 --- a/clients/client-core/src/client/topology_control.rs +++ b/clients/client-core/src/client/topology_control.rs @@ -138,7 +138,7 @@ impl TopologyRefresherConfig { } pub struct TopologyRefresher { - validator_client: validator_client::ApiClient, + validator_client: validator_client::client::ApiClient, client_version: String, validator_api_urls: Vec, @@ -154,7 +154,9 @@ impl TopologyRefresher { cfg.validator_api_urls.shuffle(&mut thread_rng()); TopologyRefresher { - validator_client: validator_client::ApiClient::new(cfg.validator_api_urls[0].clone()), + validator_client: validator_client::client::ApiClient::new( + cfg.validator_api_urls[0].clone(), + ), client_version: cfg.client_version, validator_api_urls: cfg.validator_api_urls, topology_accessor, @@ -309,7 +311,9 @@ impl TopologyRefresher { }, } } - assert!(shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("TopologyRefresher: Exiting"); }) } diff --git a/clients/client-core/src/config/mod.rs b/clients/client-core/src/config/mod.rs index 678b70ef54..277fff30b9 100644 --- a/clients/client-core/src/config/mod.rs +++ b/clients/client-core/src/config/mod.rs @@ -125,6 +125,10 @@ impl Config { self.client.gateway_endpoint.gateway_id = id.into(); } + pub fn set_custom_validators(&mut self, validator_urls: Vec) { + self.client.validator_urls = validator_urls; + } + pub fn set_custom_validator_apis(&mut self, validator_api_urls: Vec) { self.client.validator_api_urls = validator_api_urls; } @@ -179,6 +183,10 @@ impl Config { self.client.ack_key_file.clone() } + pub fn get_validator_endpoints(&self) -> Vec { + self.client.validator_urls.clone() + } + pub fn get_validator_api_endpoints(&self) -> Vec { self.client.validator_api_urls.clone() } @@ -306,6 +314,10 @@ pub struct Client { #[serde(default)] disabled_credentials_mode: bool, + /// Addresses to nymd validators via which the client can communicate with the chain. + #[serde(default)] + validator_urls: Vec, + /// Addresses to APIs running on validator from which the client gets the view of the network. validator_api_urls: Vec, @@ -354,6 +366,7 @@ impl Default for Client { version: env!("CARGO_PKG_VERSION").to_string(), id: "".to_string(), disabled_credentials_mode: true, + validator_urls: vec![], validator_api_urls: vec![], private_identity_key_file: Default::default(), public_identity_key_file: Default::default(), diff --git a/clients/client-core/src/error.rs b/clients/client-core/src/error.rs index 24e846087f..0313db076b 100644 --- a/clients/client-core/src/error.rs +++ b/clients/client-core/src/error.rs @@ -26,4 +26,7 @@ pub enum ClientCoreError { CouldNotLoadExistingGatewayConfiguration(std::io::Error), #[error("The current network topology seem to be insufficient to route any packets through")] InsufficientNetworkTopology, + + #[error("Unexpected exit")] + UnexpectedExit, } diff --git a/clients/client-core/src/init.rs b/clients/client-core/src/init.rs index 98cc42df64..be9758d363 100644 --- a/clients/client-core/src/init.rs +++ b/clients/client-core/src/init.rs @@ -31,7 +31,7 @@ pub async fn query_gateway_details( let validator_api = validator_servers .choose(&mut thread_rng()) .ok_or(ClientCoreError::ListOfValidatorApisIsEmpty)?; - let validator_client = validator_client::ApiClient::new(validator_api.clone()); + let validator_client = validator_client::client::ApiClient::new(validator_api.clone()); log::trace!("Fetching list of gateways from: {}", validator_api); let gateways = validator_client.get_cached_gateways().await?; diff --git a/clients/credential/src/commands.rs b/clients/credential/src/commands.rs index c281332810..7f9d3ae2e1 100644 --- a/clients/credential/src/commands.rs +++ b/clients/credential/src/commands.rs @@ -14,8 +14,9 @@ use credential_storage::PersistentStorage; use credentials::coconut::bandwidth::{BandwidthVoucher, TOTAL_ATTRIBUTES}; use credentials::coconut::utils::obtain_aggregate_signature; use crypto::asymmetric::{encryption, identity}; -use network_defaults::VOUCHER_INFO; +use network_defaults::{NymNetworkDetails, VOUCHER_INFO}; use validator_client::nymd::tx::Hash; +use validator_client::{CoconutApiClient, Config}; use crate::client::Client; use crate::error::{CredentialClientError, Result}; @@ -107,10 +108,9 @@ pub(crate) struct GetCredential { /// The hash of a successful deposit transaction #[clap(long)] tx_hash: String, - /// The URLs to the validator-api endpoints the are run as coconut signer authorities, separated - /// by comma (,) + /// The nymd URL that should be used #[clap(long)] - signer_authorities: String, + nymd_url: String, /// If we want to get the signature without attaching a blind sign request; it is expected that /// there is already a signature stored on the signer #[clap(long, parse(from_flag))] @@ -124,7 +124,10 @@ impl Execute for GetCredential { .get::(&self.tx_hash) .ok_or(CredentialClientError::NoDeposit)?; - let urls = config::parse_validators(&self.signer_authorities); + let network_details = NymNetworkDetails::new_from_env(); + let config = Config::try_from_nym_network_details(&network_details)?; + let client = validator_client::Client::new_query(config)?; + let coconut_api_clients = CoconutApiClient::all_coconut_api_clients(&client).await?; let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap(); let bandwidth_credential_attributes = if self.__no_request { @@ -178,8 +181,12 @@ impl Execute for GetCredential { )?); db.set(&self.tx_hash, &state).unwrap(); - let signature = - obtain_aggregate_signature(¶ms, &bandwidth_credential_attributes, &urls).await?; + let signature = obtain_aggregate_signature( + ¶ms, + &bandwidth_credential_attributes, + &coconut_api_clients, + ) + .await?; shared_storage .insert_coconut_credential( state.amount.to_string(), diff --git a/clients/credential/src/error.rs b/clients/credential/src/error.rs index 830e4d9e16..08bd0001e9 100644 --- a/clients/credential/src/error.rs +++ b/clients/credential/src/error.rs @@ -8,6 +8,7 @@ use credentials::error::Error as CredentialError; use crypto::asymmetric::encryption::KeyRecoveryError; use crypto::asymmetric::identity::Ed25519RecoveryError; use validator_client::nymd::error::NymdError; +use validator_client::ValidatorClientError; pub type Result = std::result::Result; @@ -16,6 +17,9 @@ pub enum CredentialClientError { #[error("Nymd error: {0}")] Nymd(#[from] NymdError), + #[error("Validator client error: {0}")] + ValidatorClientError(#[from] ValidatorClientError), + #[error("Credential error: {0}")] Credential(#[from] CredentialError), diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index 5da490d504..ab22e76a9d 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -4,7 +4,7 @@ version = "1.1.0" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] description = "Implementation of the Nym Client" edition = "2021" -rust-version = "1.56" +rust-version = "1.65" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -51,6 +51,7 @@ topology = { path = "../../common/topology" } validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] } version-checker = { path = "../../common/version-checker" } websocket-requests = { path = "websocket-requests" } +tap = "1.0.1" [features] coconut = ["coconut-interface", "credentials", "credentials/coconut", "gateway-requests/coconut", "gateway-client/coconut", "client-core/coconut"] diff --git a/clients/native/examples/websocket_binarysend.rs b/clients/native/examples/websocket_binarysend.rs index 160a9bf009..f103815af2 100644 --- a/clients/native/examples/websocket_binarysend.rs +++ b/clients/native/examples/websocket_binarysend.rs @@ -43,7 +43,7 @@ async fn send_file_with_reply() { recipient, message: read_data, with_reply_surb: true, - connection_id: 0, + connection_id: Some(0), }; println!("sending content of 'dummy_file' over the mix network..."); @@ -92,7 +92,7 @@ async fn send_file_without_reply() { recipient, message: read_data, with_reply_surb: false, - connection_id: 0, + connection_id: Some(0), }; println!("sending content of 'dummy_file' over the mix network..."); diff --git a/clients/native/src/client/config/template.rs b/clients/native/src/client/config/template.rs index ca25e6c794..c327d286ff 100644 --- a/clients/native/src/client/config/template.rs +++ b/clients/native/src/client/config/template.rs @@ -23,6 +23,13 @@ id = '{{ client.id }}' # to claim bandwidth without presenting bandwidth credentials. disabled_credentials_mode = {{ client.disabled_credentials_mode }} +# Addresses to nymd validators via which the client can communicate with the chain. +validator_urls = [ + {{#each client.validator_urls }} + '{{this}}', + {{/each}} +] + # Addresses to APIs running on validator from which the client gets the view of the network. validator_api_urls = [ {{#each client.validator_api_urls }} diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index df77a7fb77..4d60e3ad54 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -1,7 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use client_connections::{ClosedConnectionReceiver, ClosedConnectionSender, TransmissionLane}; +use client_connections::{ + ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths, TransmissionLane, +}; use client_core::client::cover_traffic_stream::LoopCoverTrafficStream; use client_core::client::inbound_messages::{ InputMessage, InputMessageReceiver, InputMessageSender, @@ -32,6 +34,7 @@ use nymsphinx::addressing::clients::Recipient; use nymsphinx::addressing::nodes::NodeIdentity; use nymsphinx::anonymous_replies::ReplySurb; use nymsphinx::receiver::ReconstructedMessage; +use tap::TapFallible; use task::{wait_for_signal, ShutdownListener, ShutdownNotifier}; use crate::client::config::{Config, SocketType}; @@ -119,7 +122,8 @@ impl NymClient { ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, - closed_connection_rx: ClosedConnectionReceiver, + lane_queue_lengths: LaneQueueLengths, + client_connection_rx: ConnectionCommandReceiver, shutdown: ShutdownListener, ) { let mut controller_config = real_messages_control::Config::new( @@ -149,7 +153,8 @@ impl NymClient { mix_sender, topology_accessor, reply_key_storage, - closed_connection_rx, + lane_queue_lengths, + client_connection_rx, ) .start_with_shutdown(shutdown); } @@ -196,11 +201,22 @@ impl NymClient { .expect("provided gateway id is invalid!"); #[cfg(feature = "coconut")] - let bandwidth_controller = BandwidthController::new( - credential_storage::initialise_storage(self.config.get_base().get_database_path()) - .await, - self.config.get_base().get_validator_api_endpoints(), - ); + let bandwidth_controller = { + let details = network_defaults::NymNetworkDetails::new_from_env(); + let client_config = validator_client::Config::try_from_nym_network_details(&details) + .expect("failed to construct validator client config"); + let client = validator_client::Client::new_query(client_config) + .expect("Could not construct query client"); + let coconut_api_clients = + validator_client::CoconutApiClient::all_coconut_api_clients(&client) + .await + .expect("Could not query api clients"); + BandwidthController::new( + credential_storage::initialise_storage(self.config.get_base().get_database_path()) + .await, + coconut_api_clients, + ) + }; #[cfg(not(feature = "coconut"))] let bandwidth_controller = BandwidthController::new( credential_storage::initialise_storage(self.config.get_base().get_database_path()) @@ -283,15 +299,17 @@ impl NymClient { &self, buffer_requester: ReceivedBufferRequestSender, msg_input: InputMessageSender, - closed_connection_tx: ClosedConnectionSender, + shared_lane_queue_lengths: LaneQueueLengths, + client_connection_tx: ConnectionCommandSender, ) { info!("Starting websocket listener..."); let websocket_handler = websocket::Handler::new( msg_input, - closed_connection_tx, + client_connection_tx, buffer_requester, - self.as_mix_recipient(), + &self.as_mix_recipient(), + shared_lane_queue_lengths, ); websocket::Listener::new(self.config.get_listening_port()).start(websocket_handler); @@ -300,28 +318,35 @@ impl NymClient { /// EXPERIMENTAL DIRECT RUST API /// It's untested and there are absolutely no guarantees about it (but seems to have worked /// well enough in local tests) - pub fn send_message(&mut self, recipient: Recipient, message: Vec, with_reply_surb: bool) { + pub async fn send_message( + &mut self, + recipient: Recipient, + message: Vec, + with_reply_surb: bool, + ) { let lane = TransmissionLane::General; let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb, lane); self.input_tx .as_ref() .expect("start method was not called before!") - .unbounded_send(input_msg) - .unwrap(); + .send(input_msg) + .await + .expect("InputMessageReceiver has stopped receiving!"); } /// EXPERIMENTAL DIRECT RUST API /// It's untested and there are absolutely no guarantees about it (but seems to have worked /// well enough in local tests) - pub fn send_reply(&mut self, reply_surb: ReplySurb, message: Vec) { + pub async fn send_reply(&mut self, reply_surb: ReplySurb, message: Vec) { let input_msg = InputMessage::new_reply(reply_surb, message); self.input_tx .as_ref() .expect("start method was not called before!") - .unbounded_send(input_msg) - .unwrap(); + .send(input_msg) + .await + .expect("InputMessageReceiver has stopped receiving!"); } /// EXPERIMENTAL DIRECT RUST API @@ -378,7 +403,7 @@ impl NymClient { let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded(); // channels responsible for controlling real messages - let (input_sender, input_receiver) = mpsc::unbounded::(); + let (input_sender, input_receiver) = tokio::sync::mpsc::channel::(1); // channels responsible for controlling ack messages let (ack_sender, ack_receiver) = mpsc::unbounded(); @@ -386,7 +411,10 @@ impl NymClient { let reply_key_storage = ReplyKeyStorage::load(self.config.get_base().get_reply_encryption_key_store_path()) - .expect("Failed to load reply key storage!"); + .tap_err(|err| { + log::error!("Failed to load reply key storage - is it perhaps already in use?"); + log::error!("{}", err); + })?; // Shutdown notifier for signalling tasks to stop let shutdown = ShutdownNotifier::default(); @@ -415,7 +443,11 @@ impl NymClient { // Channels that the websocket listener can use to signal downstream to the real traffic // controller that connections are closed. - let (closed_connection_tx, closed_connection_rx) = mpsc::unbounded(); + let (client_connection_tx, client_connection_rx) = mpsc::unbounded(); + + // Shared queue length data. Published by the `OutQueueController` in the client, and used + // primarily to throttle incoming connections (e.g socks5 for attached network-requesters) + let shared_lane_queue_lengths = LaneQueueLengths::new(); self.start_real_traffic_controller( shared_topology_accessor.clone(), @@ -423,7 +455,8 @@ impl NymClient { ack_receiver, input_receiver, sphinx_message_sender.clone(), - closed_connection_rx, + shared_lane_queue_lengths.clone(), + client_connection_rx, shutdown.subscribe(), ); @@ -443,7 +476,8 @@ impl NymClient { SocketType::WebSocket => self.start_websocket_listener( received_buffer_request_sender, input_sender, - closed_connection_tx, + shared_lane_queue_lengths, + client_connection_tx, ), SocketType::None => { // if we did not start the socket, it means we're running (supposedly) in the native mode diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 208a026959..6a38770eec 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -25,9 +25,13 @@ pub(crate) struct Init { #[clap(long)] force_register_gateway: bool, - /// Comma separated list of rest endpoints of the validators + /// Comma separated list of rest endpoints of the nymd validators #[clap(long)] - validators: Option, + nymd_validators: Option, + + /// Comma separated list of rest endpoints of the API validators + #[clap(long)] + api_validators: Option, /// Whether to not start the websocket #[clap(long)] @@ -52,7 +56,8 @@ pub(crate) struct Init { impl From for OverrideConfig { fn from(init_config: Init) -> Self { OverrideConfig { - validators: init_config.validators, + nymd_validators: init_config.nymd_validators, + api_validators: init_config.api_validators, disable_socket: init_config.disable_socket, port: init_config.port, fastmode: init_config.fastmode, diff --git a/clients/native/src/commands/mod.rs b/clients/native/src/commands/mod.rs index 08323a6454..648a1d6002 100644 --- a/clients/native/src/commands/mod.rs +++ b/clients/native/src/commands/mod.rs @@ -75,7 +75,8 @@ pub(crate) enum Commands { // Configuration that can be overridden. pub(crate) struct OverrideConfig { - validators: Option, + nymd_validators: Option, + api_validators: Option, disable_socket: bool, port: Option, fastmode: bool, @@ -98,7 +99,18 @@ pub(crate) async fn execute(args: &Cli) -> Result<(), ClientError> { } pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config { - if let Some(raw_validators) = args.validators { + if let Some(raw_validators) = args.nymd_validators { + config + .get_base_mut() + .set_custom_validators(config::parse_validators(&raw_validators)); + } else if std::env::var(network_defaults::var_names::CONFIGURED).is_ok() { + let raw_validators = std::env::var(network_defaults::var_names::NYMD_VALIDATOR) + .expect("nymd validator not set"); + config + .get_base_mut() + .set_custom_validators(config::parse_validators(&raw_validators)); + } + if let Some(raw_validators) = args.api_validators { config .get_base_mut() .set_custom_validator_apis(config::parse_validators(&raw_validators)); diff --git a/clients/native/src/commands/run.rs b/clients/native/src/commands/run.rs index 7332f81448..d5c892fadc 100644 --- a/clients/native/src/commands/run.rs +++ b/clients/native/src/commands/run.rs @@ -18,9 +18,13 @@ pub(crate) struct Run { #[clap(long)] id: String, - /// Comma separated list of rest endpoints of the validators + /// Comma separated list of rest endpoints of the nymd validators #[clap(long)] - validators: Option, + nymd_validators: Option, + + /// Comma separated list of rest endpoints of the API validators + #[clap(long)] + api_validators: Option, /// Id of the gateway we want to connect to. If overridden, it is user's responsibility to /// ensure prior registration happened @@ -45,7 +49,8 @@ pub(crate) struct Run { impl From for OverrideConfig { fn from(run_config: Run) -> Self { OverrideConfig { - validators: run_config.validators, + nymd_validators: run_config.nymd_validators, + api_validators: run_config.api_validators, disable_socket: run_config.disable_socket, port: run_config.port, fastmode: false, diff --git a/clients/native/src/error.rs b/clients/native/src/error.rs index a32a44d3b6..6ffdc86e99 100644 --- a/clients/native/src/error.rs +++ b/clients/native/src/error.rs @@ -1,4 +1,4 @@ -use client_core::error::ClientCoreError; +use client_core::{client::reply_key_storage::ReplyKeyStorageError, error::ClientCoreError}; use crypto::asymmetric::identity::Ed25519RecoveryError; use gateway_client::error::GatewayClientError; use validator_client::ValidatorClientError; @@ -15,6 +15,8 @@ pub enum ClientError { ValidatorClientError(#[from] ValidatorClientError), #[error("client-core error: {0}")] ClientCoreError(#[from] ClientCoreError), + #[error("Reply key storage error: {0}")] + ReplyKeyStorageError(#[from] ReplyKeyStorageError), #[error("Failed to load config for: {0}")] FailedToLoadConfig(String), diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index f43fbca4c8..11d0dda55f 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -1,7 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use client_connections::{ClosedConnectionSender, TransmissionLane}; +use client_connections::{ + ConnectionCommand, ConnectionCommandSender, LaneQueueLengths, TransmissionLane, +}; use client_core::client::{ inbound_messages::{InputMessage, InputMessageSender}, received_buffer::{ @@ -35,11 +37,12 @@ impl Default for ReceivedResponseType { pub(crate) struct Handler { msg_input: InputMessageSender, - closed_connection_tx: ClosedConnectionSender, + client_connection_tx: ConnectionCommandSender, buffer_requester: ReceivedBufferRequestSender, self_full_address: Recipient, socket: Option>, received_response_type: ReceivedResponseType, + lane_queue_lengths: LaneQueueLengths, } // clone is used to use handler on a new connection, which initially is `None` @@ -47,11 +50,12 @@ impl Clone for Handler { fn clone(&self) -> Self { Handler { msg_input: self.msg_input.clone(), - closed_connection_tx: self.closed_connection_tx.clone(), + client_connection_tx: self.client_connection_tx.clone(), buffer_requester: self.buffer_requester.clone(), self_full_address: self.self_full_address, socket: None, received_response_type: Default::default(), + lane_queue_lengths: self.lane_queue_lengths.clone(), } } } @@ -67,42 +71,85 @@ impl Drop for Handler { impl Handler { pub(crate) fn new( msg_input: InputMessageSender, - closed_connection_tx: ClosedConnectionSender, + client_connection_tx: ConnectionCommandSender, buffer_requester: ReceivedBufferRequestSender, - self_full_address: Recipient, + self_full_address: &Recipient, + lane_queue_lengths: LaneQueueLengths, ) -> Self { Handler { msg_input, - closed_connection_tx, + client_connection_tx, buffer_requester, - self_full_address, + self_full_address: *self_full_address, socket: None, received_response_type: Default::default(), + lane_queue_lengths, } } - fn handle_send( + async fn handle_send( &mut self, - recipient: Recipient, + recipient: &Recipient, message: Vec, with_reply_surb: bool, - connection_id: u64, + connection_id: Option, ) -> Option { - // the ack control is now responsible for chunking, etc. - let lane = TransmissionLane::ConnectionId(connection_id); - let input_msg = InputMessage::new_fresh(recipient, message, with_reply_surb, lane); - self.msg_input.unbounded_send(input_msg).unwrap(); + // We map the absence of a connection id as going into the general lane. + let lane = connection_id.map_or(TransmissionLane::General, |id| { + TransmissionLane::ConnectionId(id) + }); - None + // the ack control is now responsible for chunking, etc. + let input_msg = InputMessage::new_fresh(*recipient, message, with_reply_surb, lane); + self.msg_input + .send(input_msg) + .await + .expect("InputMessageReceiver has stopped receiving!"); + + // Only reply back with a `LaneQueueLength` if the sender providided a connection id + let connection_id = match lane { + TransmissionLane::General + | TransmissionLane::Reply + | TransmissionLane::Retransmission + | TransmissionLane::Control => return None, + TransmissionLane::ConnectionId(id) => id, + }; + + // on receiving a send, we reply back the current lane queue length for that connection id. + // Note that this does _NOT_ take into account the packets that have been received but not + // yet reach `OutQueueControl`, so it might be a tad low. + let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() else { + log::warn!( + "Failed to get the lane queue length lock, \ + not responding back with the current queue length" + ); + return None; + }; + + let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); + Some(ServerResponse::LaneQueueLength(connection_id, queue_length)) } - fn handle_reply(&mut self, reply_surb: ReplySurb, message: Vec) -> Option { + async fn handle_reply( + &mut self, + reply_surb: ReplySurb, + message: Vec, + ) -> Option { if message.len() > ReplySurb::max_msg_len(Default::default()) { - return Some(ServerResponse::new_error(format!("too long message to put inside a reply SURB. Received: {} bytes and maximum is {} bytes", message.len(), ReplySurb::max_msg_len(Default::default())))); + return Some( + ServerResponse::new_error( + format!( + "too long message to put inside a reply SURB. Received: {} bytes and maximum is {} bytes", + message.len(), ReplySurb::max_msg_len(Default::default())) + ) + ); } let input_msg = InputMessage::new_reply(reply_surb, message); - self.msg_input.unbounded_send(input_msg).unwrap(); + self.msg_input + .send(input_msg) + .await + .expect("InputMessageReceiver has stopped receiving!"); None } @@ -112,30 +159,47 @@ impl Handler { } fn handle_closed_connection(&self, connection_id: u64) -> Option { - self.closed_connection_tx - .unbounded_send(connection_id) + self.client_connection_tx + .unbounded_send(ConnectionCommand::Close(connection_id)) .unwrap(); None } - fn handle_request(&mut self, request: ClientRequest) -> Option { + fn handle_get_lane_queue_length(&self, connection_id: u64) -> Option { + let Ok(lane_queue_lengths) = self.lane_queue_lengths.lock() else { + log::warn!( + "Failed to get the lane queue length lock, not responding back with the current queue length" + ); + return None; + }; + + let lane = TransmissionLane::ConnectionId(connection_id); + let queue_length = lane_queue_lengths.get(&lane).unwrap_or(0); + Some(ServerResponse::LaneQueueLength(connection_id, queue_length)) + } + + async fn handle_request(&mut self, request: ClientRequest) -> Option { match request { ClientRequest::Send { recipient, message, with_reply_surb, connection_id, - } => self.handle_send(recipient, message, with_reply_surb, connection_id), + } => { + self.handle_send(&recipient, message, with_reply_surb, connection_id) + .await + } ClientRequest::Reply { message, reply_surb, - } => self.handle_reply(reply_surb, message), + } => self.handle_reply(reply_surb, message).await, ClientRequest::SelfAddress => Some(self.handle_self_address()), ClientRequest::ClosedConnection(id) => self.handle_closed_connection(id), + ClientRequest::GetLaneQueueLength(id) => self.handle_get_lane_queue_length(id), } } - fn handle_text_message(&mut self, msg: String) -> Option { + async fn handle_text_message(&mut self, msg: String) -> Option { debug!("Handling text message request"); trace!("Content: {:?}", msg); @@ -144,13 +208,13 @@ impl Handler { let response = match client_request { Err(err) => Some(ServerResponse::Error(err)), - Ok(req) => self.handle_request(req), + Ok(req) => self.handle_request(req).await, }; response.map(|resp| WsMessage::text(resp.into_text())) } - fn handle_binary_message(&mut self, msg: Vec) -> Option { + async fn handle_binary_message(&mut self, msg: &[u8]) -> Option { debug!("Handling binary message request"); self.received_response_type = ReceivedResponseType::Binary; @@ -158,49 +222,23 @@ impl Handler { let response = match client_request { Err(err) => Some(ServerResponse::Error(err)), - Ok(req) => self.handle_request(req), + Ok(req) => self.handle_request(req).await, }; response.map(|resp| WsMessage::Binary(resp.into_binary())) } - fn handle_ws_request(&mut self, raw_request: WsMessage) -> Option { + async fn handle_ws_request(&mut self, raw_request: WsMessage) -> Option { // apparently tungstenite auto-handles ping/pong/close messages so for now let's ignore // them and let's test that claim. If that's not the case, just copy code from // old version of this file. match raw_request { - WsMessage::Text(text_message) => self.handle_text_message(text_message), - WsMessage::Binary(binary_message) => self.handle_binary_message(binary_message), + WsMessage::Text(text_message) => self.handle_text_message(text_message).await, + WsMessage::Binary(binary_message) => self.handle_binary_message(&binary_message).await, _ => None, } } - // I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but - // let's just play along for now - fn prepare_reconstructed_binary( - &self, - reconstructed_messages: Vec, - ) -> Vec> { - reconstructed_messages - .into_iter() - .map(ServerResponse::Received) - .map(|resp| Ok(WsMessage::Binary(resp.into_binary()))) - .collect() - } - - // I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but - // let's just play along for now - fn prepare_reconstructed_text( - &self, - reconstructed_messages: Vec, - ) -> Vec> { - reconstructed_messages - .into_iter() - .map(ServerResponse::Received) - .map(|resp| Ok(WsMessage::Text(resp.into_text()))) - .collect() - } - async fn push_websocket_received_plaintexts( &mut self, reconstructed_messages: Vec, @@ -209,10 +247,8 @@ impl Handler { // if it's text or binary, but for time being we use the naive assumption that if // client is sending Message::Text it expects text back. Same for Message::Binary let response_messages = match self.received_response_type { - ReceivedResponseType::Binary => { - self.prepare_reconstructed_binary(reconstructed_messages) - } - ReceivedResponseType::Text => self.prepare_reconstructed_text(reconstructed_messages), + ReceivedResponseType::Binary => prepare_reconstructed_binary(reconstructed_messages), + ReceivedResponseType::Text => prepare_reconstructed_text(reconstructed_messages), }; let mut send_stream = futures::stream::iter(response_messages); @@ -260,7 +296,7 @@ impl Handler { break; } - if let Some(response) = self.handle_ws_request(socket_msg) { + if let Some(response) = self.handle_ws_request(socket_msg).await { if let Err(err) = self.send_websocket_response(response).await { warn!( "Failed to send message over websocket: {}. Assuming the connection is dead.", @@ -307,3 +343,27 @@ impl Handler { self.listen_for_requests(reconstructed_receiver).await; } } + +// I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but +// let's just play along for now +fn prepare_reconstructed_binary( + reconstructed_messages: Vec, +) -> Vec> { + reconstructed_messages + .into_iter() + .map(ServerResponse::Received) + .map(|resp| Ok(WsMessage::Binary(resp.into_binary()))) + .collect() +} + +// I'm still not entirely sure why `send_all` requires `TryStream` rather than `Stream`, but +// let's just play along for now +fn prepare_reconstructed_text( + reconstructed_messages: Vec, +) -> Vec> { + reconstructed_messages + .into_iter() + .map(ServerResponse::Received) + .map(|resp| Ok(WsMessage::Text(resp.into_text()))) + .collect() +} diff --git a/clients/native/websocket-requests/src/requests.rs b/clients/native/websocket-requests/src/requests.rs index ea44059250..315399ae3d 100644 --- a/clients/native/websocket-requests/src/requests.rs +++ b/clients/native/websocket-requests/src/requests.rs @@ -23,6 +23,9 @@ pub const SELF_ADDRESS_REQUEST_TAG: u8 = 0x02; /// Value tag representing [`ClosedConnection`] variant of the [`ClientRequest`] pub const CLOSED_CONNECTION_REQUEST_TAG: u8 = 0x03; +/// Value tag representing [`GetLaneQueueLength`] variant of the [`ClientRequest`] +pub const GET_LANE_QUEUE_LENGHT_TAG: u8 = 0x04; + #[allow(non_snake_case)] #[derive(Debug)] pub enum ClientRequest { @@ -31,7 +34,7 @@ pub enum ClientRequest { message: Vec, // Perhaps we could change it to a number to indicate how many reply_SURBs we want to include? with_reply_surb: bool, - connection_id: u64, + connection_id: Option, }, Reply { message: Vec, @@ -39,6 +42,7 @@ pub enum ClientRequest { }, SelfAddress, ClosedConnection(u64), + GetLaneQueueLength(u64), } // we could have been parsing it directly TryFrom, but we want to retain @@ -49,10 +53,10 @@ impl ClientRequest { recipient: Recipient, data: Vec, with_reply_surb: bool, - connection_id: u64, + connection_id: Option, ) -> Vec { let data_len_bytes = (data.len() as u64).to_be_bytes(); - let conn_id_bytes = connection_id.to_be_bytes(); + let conn_id_bytes = connection_id.unwrap_or(0).to_be_bytes(); std::iter::once(SEND_REQUEST_TAG) .chain(std::iter::once(with_reply_surb as u8)) .chain(recipient.to_bytes().iter().cloned()) // will not be length prefixed because the length is constant @@ -62,10 +66,10 @@ impl ClientRequest { .collect() } - // SEND_REQUEST_TAG || with_reply || recipient || data_len || data + // SEND_REQUEST_TAG || with_reply || recipient || conn_id || data_len || data fn deserialize_send(b: &[u8]) -> Result { - // we need to have at least 1 (tag) + 1 (reply flag) + Recipient::LEN + sizeof bytes - if b.len() < 2 + Recipient::LEN + size_of::() { + // we need to have at least 1 (tag) + 1 (reply flag) + Recipient::LEN + 2*sizeof bytes + if b.len() < 2 + Recipient::LEN + 2 * size_of::() { return Err(error::Error::new( ErrorKind::TooShortRequest, "not enough data provided to recover 'send'".to_string(), @@ -102,6 +106,11 @@ impl ClientRequest { connection_id_bytes .copy_from_slice(&b[2 + Recipient::LEN..2 + Recipient::LEN + size_of::()]); let connection_id = u64::from_be_bytes(connection_id_bytes); + let connection_id = if connection_id == 0 { + None + } else { + Some(connection_id) + }; let data_len_bytes = &b[2 + Recipient::LEN + size_of::()..2 + Recipient::LEN + 2 * size_of::()]; @@ -127,7 +136,7 @@ impl ClientRequest { } // REPLY_REQUEST_TAG || surb_len || surb || message_len || message - fn serialize_reply(message: Vec, reply_surb: ReplySurb) -> Vec { + fn serialize_reply(message: Vec, reply_surb: &ReplySurb) -> Vec { let reply_surb_bytes = reply_surb.to_bytes(); let surb_len_bytes = (reply_surb_bytes.len() as u64).to_be_bytes(); let message_len_bytes = (message.len() as u64).to_be_bytes(); @@ -225,12 +234,19 @@ impl ClientRequest { fn serialize_closed_connection(connection_id: u64) -> Vec { let conn_id_bytes = connection_id.to_be_bytes(); std::iter::once(CLOSED_CONNECTION_REQUEST_TAG) - .chain(conn_id_bytes.iter().cloned()) + .chain(conn_id_bytes.iter().copied()) .collect() } // CLOSED_CONNECTION_REQUEST_TAG - fn deserialize_closed_connection(b: &[u8]) -> Self { + fn deserialize_closed_connection(b: &[u8]) -> Result { + if b.len() != 1 + size_of::() { + return Err(error::Error::new( + ErrorKind::MalformedRequest, + "the received closed connection has invalid length".to_string(), + )); + } + // this MUST match because it was called by 'deserialize' debug_assert_eq!(b[0], CLOSED_CONNECTION_REQUEST_TAG); @@ -238,7 +254,34 @@ impl ClientRequest { connection_id_bytes.copy_from_slice(&b[1..=size_of::()]); let connection_id = u64::from_be_bytes(connection_id_bytes); - ClientRequest::ClosedConnection(connection_id) + Ok(ClientRequest::ClosedConnection(connection_id)) + } + + // GET_LANE_QUEUE_LENGHT_TAG + fn serialize_get_lane_queue_lengths(connection_id: u64) -> Vec { + let conn_id_bytes = connection_id.to_be_bytes(); + std::iter::once(GET_LANE_QUEUE_LENGHT_TAG) + .chain(conn_id_bytes.iter().copied()) + .collect() + } + + // GET_LANE_QUEUE_LENGHT_TAG + fn deserialize_get_lane_queue_length(b: &[u8]) -> Result { + if b.len() != 1 + size_of::() { + return Err(error::Error::new( + ErrorKind::MalformedRequest, + "the received get lane queue length has invalid length".to_string(), + )); + } + + // this MUST match because it was called by 'deserialize' + debug_assert_eq!(b[0], GET_LANE_QUEUE_LENGHT_TAG); + + let mut connection_id_bytes = [0u8; size_of::()]; + connection_id_bytes.copy_from_slice(&b[1..=size_of::()]); + let connection_id = u64::from_be_bytes(connection_id_bytes); + + Ok(ClientRequest::GetLaneQueueLength(connection_id)) } pub fn serialize(self) -> Vec { @@ -253,11 +296,13 @@ impl ClientRequest { ClientRequest::Reply { message, reply_surb, - } => Self::serialize_reply(message, reply_surb), + } => Self::serialize_reply(message, &reply_surb), ClientRequest::SelfAddress => Self::serialize_self_address(), ClientRequest::ClosedConnection(id) => Self::serialize_closed_connection(id), + + ClientRequest::GetLaneQueueLength(id) => Self::serialize_get_lane_queue_lengths(id), } } @@ -287,16 +332,17 @@ impl ClientRequest { SEND_REQUEST_TAG => Self::deserialize_send(b), REPLY_REQUEST_TAG => Self::deserialize_reply(b), SELF_ADDRESS_REQUEST_TAG => Ok(Self::deserialize_self_address(b)), - CLOSED_CONNECTION_REQUEST_TAG => Ok(Self::deserialize_closed_connection(b)), + CLOSED_CONNECTION_REQUEST_TAG => Self::deserialize_closed_connection(b), + GET_LANE_QUEUE_LENGHT_TAG => Self::deserialize_get_lane_queue_length(b), n => Err(error::Error::new( ErrorKind::UnknownRequest, - format!("type {}", n), + format!("type {n}"), )), } } - pub fn try_from_binary(raw_req: Vec) -> Result { - Self::deserialize(&raw_req) + pub fn try_from_binary(raw_req: &[u8]) -> Result { + Self::deserialize(raw_req) } pub fn try_from_text(raw_req: String) -> Result { @@ -323,7 +369,7 @@ mod tests { recipient, message: b"foomp".to_vec(), with_reply_surb: false, - connection_id: 42, + connection_id: Some(42), }; let bytes = send_request_no_surb.serialize(); @@ -338,7 +384,7 @@ mod tests { assert_eq!(recipient.to_string(), recipient_string); assert_eq!(message, b"foomp".to_vec()); assert!(!with_reply_surb); - assert_eq!(connection_id, 42) + assert_eq!(connection_id, Some(42)) } _ => unreachable!(), } @@ -347,7 +393,7 @@ mod tests { recipient, message: b"foomp".to_vec(), with_reply_surb: true, - connection_id: 213, + connection_id: None, }; let bytes = send_request_surb.serialize(); @@ -362,7 +408,7 @@ mod tests { assert_eq!(recipient.to_string(), recipient_string); assert_eq!(message, b"foomp".to_vec()); assert!(with_reply_surb); - assert_eq!(connection_id, 213) + assert_eq!(connection_id, None) } _ => unreachable!(), } diff --git a/clients/native/websocket-requests/src/responses.rs b/clients/native/websocket-requests/src/responses.rs index 603650f0b8..7a32645957 100644 --- a/clients/native/websocket-requests/src/responses.rs +++ b/clients/native/websocket-requests/src/responses.rs @@ -23,10 +23,14 @@ pub const RECEIVED_RESPONSE_TAG: u8 = 0x01; /// Value tag representing [`SelfAddress`] variant of the [`ServerResponse`] pub const SELF_ADDRESS_RESPONSE_TAG: u8 = 0x02; +/// Value tag representing [`LaneQueueLength`] variant of the [`ServerResponse`] +pub const LANE_QUEUE_LENGTH_RESPONSE_TAG: u8 = 0x03; + #[derive(Debug)] pub enum ServerResponse { Received(ReconstructedMessage), SelfAddress(Recipient), + LaneQueueLength(u64, usize), Error(error::Error), } @@ -193,6 +197,31 @@ impl ServerResponse { Ok(ServerResponse::SelfAddress(recipient)) } + // LANE_QUEUE_LENGTH_RESPONSE_TAG || lane || queue_length + fn serialize_lane_queue_length(lane: u64, queue_length: usize) -> Vec { + std::iter::once(LANE_QUEUE_LENGTH_RESPONSE_TAG) + .chain(lane.to_be_bytes().iter().cloned()) + .chain(queue_length.to_be_bytes().iter().cloned()) + .collect() + } + + // LANE_QUEUE_LENGTH_RESPONSE_TAG || lane || queue_length + fn deserialize_lane_queue_length(b: &[u8]) -> Result { + // this MUST match because it was called by 'deserialize' + debug_assert_eq!(b[0], LANE_QUEUE_LENGTH_RESPONSE_TAG); + + let mut lane_bytes = [0u8; size_of::()]; + lane_bytes.copy_from_slice(&b[1..=size_of::()]); + let lane = u64::from_be_bytes(lane_bytes); + + let mut queue_length_bytes = [0u8; size_of::()]; + queue_length_bytes + .copy_from_slice(&b[1 + size_of::()..1 + size_of::() + size_of::()]); + let queue_length = usize::from_be_bytes(queue_length_bytes); + + Ok(ServerResponse::LaneQueueLength(lane, queue_length)) + } + // ERROR_RESPONSE_TAG || err_code || msg_len || msg fn serialize_error(error: error::Error) -> Vec { let message_len_bytes = (error.message.len() as u64).to_be_bytes(); @@ -272,6 +301,9 @@ impl ServerResponse { Self::serialize_received(reconstructed_message) } ServerResponse::SelfAddress(address) => Self::serialize_self_address(address), + ServerResponse::LaneQueueLength(lane, queue_length) => { + Self::serialize_lane_queue_length(lane, queue_length) + } ServerResponse::Error(err) => Self::serialize_error(err), } } @@ -302,6 +334,7 @@ impl ServerResponse { match response_tag { RECEIVED_RESPONSE_TAG => Self::deserialize_received(b), SELF_ADDRESS_RESPONSE_TAG => Self::deserialize_self_address(b), + LANE_QUEUE_LENGTH_RESPONSE_TAG => Self::deserialize_lane_queue_length(b), ERROR_RESPONSE_TAG => Self::deserialize_error(b), n => Err(error::Error::new( ErrorKind::UnknownResponse, @@ -378,6 +411,20 @@ mod tests { } } + #[test] + fn lane_queue_length_response_serialization_works() { + let lane_queue_length_response = ServerResponse::LaneQueueLength(13, 42); + let bytes = lane_queue_length_response.serialize(); + let recovered = ServerResponse::deserialize(&bytes).unwrap(); + match recovered { + ServerResponse::LaneQueueLength(lane, queue_length) => { + assert_eq!(lane, 13); + assert_eq!(queue_length, 42) + } + _ => unreachable!(), + } + } + #[test] fn error_response_serialization_works() { let dummy_error = error::Error::new(ErrorKind::UnknownRequest, "foomp message".to_string()); diff --git a/clients/native/websocket-requests/src/text.rs b/clients/native/websocket-requests/src/text.rs index 5a9f9dcf01..1083ecd9a8 100644 --- a/clients/native/websocket-requests/src/text.rs +++ b/clients/native/websocket-requests/src/text.rs @@ -20,7 +20,7 @@ pub(super) enum ClientRequestText { message: String, recipient: String, with_reply_surb: bool, - connection_id: u64, + connection_id: Option, }, SelfAddress, #[serde(rename_all = "camelCase")] @@ -94,6 +94,10 @@ pub(super) enum ServerResponseText { SelfAddress { address: String, }, + LaneQueueLength { + lane: u64, + queue_length: usize, + }, Error { message: String, }, @@ -135,6 +139,9 @@ impl From for ServerResponseText { ServerResponse::SelfAddress(recipient) => ServerResponseText::SelfAddress { address: recipient.to_string(), }, + ServerResponse::LaneQueueLength(lane, queue_length) => { + ServerResponseText::LaneQueueLength { lane, queue_length } + } ServerResponse::Error(err) => ServerResponseText::Error { message: err.to_string(), }, diff --git a/clients/socks5/Cargo.toml b/clients/socks5/Cargo.toml index ee103e7fda..859bacfe62 100644 --- a/clients/socks5/Cargo.toml +++ b/clients/socks5/Cargo.toml @@ -46,6 +46,7 @@ task = { path = "../../common/task" } topology = { path = "../../common/topology" } validator-client = { path = "../../common/client-libs/validator-client", features = ["nymd-client"] } version-checker = { path = "../../common/version-checker" } +tap = "1.0.1" [features] coconut = ["coconut-interface", "credentials", "gateway-requests/coconut", "gateway-client/coconut", "credentials/coconut", "client-core/coconut"] diff --git a/clients/socks5/src/client/config/template.rs b/clients/socks5/src/client/config/template.rs index 4168d44e85..11d3185f56 100644 --- a/clients/socks5/src/client/config/template.rs +++ b/clients/socks5/src/client/config/template.rs @@ -23,6 +23,13 @@ id = '{{ client.id }}' # to claim bandwidth without presenting bandwidth credentials. disabled_credentials_mode = {{ client.disabled_credentials_mode }} +# Addresses to nymd validators via which the client can communicate with the chain. +validator_urls = [ + {{#each client.validator_urls }} + '{{this}}', + {{/each}} +] + # Addresses to APIs running on validator from which the client gets the view of the network. validator_api_urls = [ {{#each client.validator_api_urls }} diff --git a/clients/socks5/src/client/mod.rs b/clients/socks5/src/client/mod.rs index 5b8530e6d0..399254c0d1 100644 --- a/clients/socks5/src/client/mod.rs +++ b/clients/socks5/src/client/mod.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::sync::atomic::Ordering; +use std::error::Error; use crate::client::config::Config; use crate::error::Socks5ClientError; @@ -9,7 +9,7 @@ use crate::socks::{ authentication::{AuthenticationMethods, Authenticator, User}, server::SphinxSocksServer, }; -use client_connections::{ClosedConnectionReceiver, ClosedConnectionSender}; +use client_connections::{ConnectionCommandReceiver, ConnectionCommandSender, LaneQueueLengths}; use client_core::client::cover_traffic_stream::LoopCoverTrafficStream; use client_core::client::inbound_messages::{ InputMessage, InputMessageReceiver, InputMessageSender, @@ -37,7 +37,9 @@ use gateway_client::{ use log::*; use nymsphinx::addressing::clients::Recipient; use nymsphinx::addressing::nodes::NodeIdentity; -use task::{wait_for_signal, ShutdownListener, ShutdownNotifier}; +use tap::TapFallible; +use task::signal::wait_for_signal_and_error; +use task::{ShutdownListener, ShutdownNotifier}; pub mod config; @@ -119,7 +121,8 @@ impl NymClient { ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, - closed_connection_rx: ClosedConnectionReceiver, + client_connection_rx: ConnectionCommandReceiver, + lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) { let mut controller_config = client_core::client::real_messages_control::Config::new( @@ -149,7 +152,8 @@ impl NymClient { mix_sender, topology_accessor, reply_key_storage, - closed_connection_rx, + lane_queue_lengths, + client_connection_rx, ) .start_with_shutdown(shutdown); } @@ -196,11 +200,22 @@ impl NymClient { .expect("provided gateway id is invalid!"); #[cfg(feature = "coconut")] - let bandwidth_controller = BandwidthController::new( - credential_storage::initialise_storage(self.config.get_base().get_database_path()) - .await, - self.config.get_base().get_validator_api_endpoints(), - ); + let bandwidth_controller = { + let details = network_defaults::NymNetworkDetails::new_from_env(); + let client_config = validator_client::Config::try_from_nym_network_details(&details) + .expect("failed to construct validator client config"); + let client = validator_client::Client::new_query(client_config) + .expect("Could not construct query client"); + let coconut_api_clients = + validator_client::CoconutApiClient::all_coconut_api_clients(&client) + .await + .expect("Could not query api clients"); + BandwidthController::new( + credential_storage::initialise_storage(self.config.get_base().get_database_path()) + .await, + coconut_api_clients, + ) + }; #[cfg(not(feature = "coconut"))] let bandwidth_controller = BandwidthController::new( credential_storage::initialise_storage(self.config.get_base().get_database_path()) @@ -283,8 +298,9 @@ impl NymClient { &self, buffer_requester: ReceivedBufferRequestSender, msg_input: InputMessageSender, - closed_connection_tx: ClosedConnectionSender, - shutdown: ShutdownListener, + client_connection_tx: ConnectionCommandSender, + lane_queue_lengths: LaneQueueLengths, + mut shutdown: ShutdownListener, ) { info!("Starting socks5 listener..."); let auth_methods = vec![AuthenticationMethods::NoAuth as u8]; @@ -296,38 +312,58 @@ impl NymClient { authenticator, self.config.get_provider_mix_address(), self.as_mix_recipient(), - shutdown, + lane_queue_lengths, + shutdown.clone(), ); tokio::spawn(async move { - sphinx_socks - .serve(msg_input, buffer_requester, closed_connection_tx) + // Ideally we should have a fully fledged task manager to check for errors in all + // tasks. + // However, pragmatically, we start out by at least reporting errors for some of the + // tasks that interact with the outside world and can fail in normal operation, such as + // network issues. + // TODO: replace this by a generic solution, such as a task manager that stores all + // JoinHandles of all spawned tasks. + if let Err(res) = sphinx_socks + .serve(msg_input, buffer_requester, client_connection_tx) .await + { + shutdown.send_we_stopped(Box::new(res)); + } }); } /// blocking version of `start` method. Will run forever (or until SIGINT is sent) - pub async fn run_forever(&mut self) -> Result<(), Socks5ClientError> { - let mut shutdown = self.start().await?; - wait_for_signal().await; + pub async fn run_forever(&mut self) -> Result<(), Box> { + let mut shutdown = self + .start() + .await + .map_err(|err| Box::new(err) as Box)?; + + let res = wait_for_signal_and_error(&mut shutdown).await; log::info!("Sending shutdown"); - client_core::client::SHUTDOWN_HAS_BEEN_SIGNALLED.store(true, Ordering::Relaxed); shutdown.signal_shutdown().ok(); log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); shutdown.wait_for_shutdown().await; log::info!("Stopping nym-socks5-client"); - Ok(()) + res } // Variant of `run_forever` that listends for remote control messages pub async fn run_and_listen( &mut self, mut receiver: Socks5ControlMessageReceiver, - ) -> Result<(), Socks5ClientError> { - let mut shutdown = self.start().await?; - tokio::select! { + ) -> Result<(), Box> { + // Start the main task + let mut shutdown = self + .start() + .await + .map_err(|err| Box::new(err) as Box)?; + + let res = tokio::select! { + biased; message = receiver.next() => { log::debug!("Received message: {:?}", message); match message { @@ -338,21 +374,26 @@ impl NymClient { log::info!("Channel closed, stopping"); } } + Ok(()) + } + Some(msg) = shutdown.wait_for_error() => { + log::info!("Task error: {:?}", msg); + Err(msg) } _ = tokio::signal::ctrl_c() => { log::info!("Received SIGINT"); + Ok(()) }, - } + }; log::info!("Sending shutdown"); - client_core::client::SHUTDOWN_HAS_BEEN_SIGNALLED.store(true, Ordering::Relaxed); shutdown.signal_shutdown().ok(); log::info!("Waiting for tasks to finish... (Press ctrl-c to force)"); shutdown.wait_for_shutdown().await; log::info!("Stopping nym-socks5-client"); - Ok(()) + res } pub async fn start(&mut self) -> Result { @@ -370,7 +411,7 @@ impl NymClient { let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded(); // channels responsible for controlling real messages - let (input_sender, input_receiver) = mpsc::unbounded::(); + let (input_sender, input_receiver) = tokio::sync::mpsc::channel::(1); // channels responsible for controlling ack messages let (ack_sender, ack_receiver) = mpsc::unbounded(); @@ -378,7 +419,10 @@ impl NymClient { let reply_key_storage = ReplyKeyStorage::load(self.config.get_base().get_reply_encryption_key_store_path()) - .expect("Failed to load reply key storage!"); + .tap_err(|err| { + log::error!("Failed to load reply key storage - is it perhaps already in use?"); + log::error!("{}", err); + })?; // Shutdown notifier for signalling tasks to stop let shutdown = ShutdownNotifier::default(); @@ -407,7 +451,11 @@ impl NymClient { // Channel for announcing closed (socks5) connections by the controller. // This will be forwarded to `OutQueueControl` - let (closed_connection_tx, closed_connection_rx) = mpsc::unbounded(); + let (client_connection_tx, client_connection_rx) = mpsc::unbounded(); + + // Shared queue length data. Published by the `OutQueueController` in the client, and used + // primarily to throttle incoming connections + let shared_lane_queue_lengths = LaneQueueLengths::new(); self.start_real_traffic_controller( shared_topology_accessor.clone(), @@ -415,7 +463,8 @@ impl NymClient { ack_receiver, input_receiver, sphinx_message_sender.clone(), - closed_connection_rx, + client_connection_rx, + shared_lane_queue_lengths.clone(), shutdown.subscribe(), ); @@ -434,7 +483,8 @@ impl NymClient { self.start_socks5_listener( received_buffer_request_sender, input_sender, - closed_connection_tx, + client_connection_tx, + shared_lane_queue_lengths, shutdown.subscribe(), ); diff --git a/clients/socks5/src/commands/init.rs b/clients/socks5/src/commands/init.rs index fb93e4e30c..58b10002c4 100644 --- a/clients/socks5/src/commands/init.rs +++ b/clients/socks5/src/commands/init.rs @@ -29,9 +29,13 @@ pub(crate) struct Init { #[clap(long)] force_register_gateway: bool, - /// Comma separated list of rest endpoints of the validators + /// Comma separated list of rest endpoints of the nymd validators #[clap(long)] - validators: Option, + nymd_validators: Option, + + /// Comma separated list of rest endpoints of the API validators + #[clap(long)] + api_validators: Option, /// Port for the socket to listen on in all subsequent runs #[clap(short, long)] @@ -52,7 +56,8 @@ pub(crate) struct Init { impl From for OverrideConfig { fn from(init_config: Init) -> Self { OverrideConfig { - validators: init_config.validators, + nymd_validators: init_config.nymd_validators, + api_validators: init_config.api_validators, port: init_config.port, fastmode: init_config.fastmode, #[cfg(feature = "coconut")] diff --git a/clients/socks5/src/commands/mod.rs b/clients/socks5/src/commands/mod.rs index 192e905b62..e1d42e0fb1 100644 --- a/clients/socks5/src/commands/mod.rs +++ b/clients/socks5/src/commands/mod.rs @@ -1,8 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::error::Error; + use crate::client::config::Config; -use crate::error::Socks5ClientError; use clap::CommandFactory; use clap::{Parser, Subcommand}; use completions::{fig_generate, ArgShell}; @@ -78,7 +79,8 @@ pub(crate) enum Commands { // Configuration that can be overridden. pub(crate) struct OverrideConfig { - validators: Option, + nymd_validators: Option, + api_validators: Option, port: Option, fastmode: bool, @@ -86,7 +88,7 @@ pub(crate) struct OverrideConfig { enabled_credentials_mode: bool, } -pub(crate) async fn execute(args: &Cli) -> Result<(), Socks5ClientError> { +pub(crate) async fn execute(args: &Cli) -> Result<(), Box> { let bin_name = "nym-socks5-client"; match &args.command { @@ -100,7 +102,16 @@ pub(crate) async fn execute(args: &Cli) -> Result<(), Socks5ClientError> { } pub(crate) fn override_config(mut config: Config, args: OverrideConfig) -> Config { - if let Some(raw_validators) = args.validators { + if let Some(raw_validators) = args.nymd_validators { + config + .get_base_mut() + .set_custom_validators(parse_validators(&raw_validators)); + } else if let Ok(raw_validators) = std::env::var(network_defaults::var_names::NYMD_VALIDATOR) { + config + .get_base_mut() + .set_custom_validators(parse_validators(&raw_validators)); + } + if let Some(raw_validators) = args.api_validators { config .get_base_mut() .set_custom_validator_apis(parse_validators(&raw_validators)); diff --git a/clients/socks5/src/commands/run.rs b/clients/socks5/src/commands/run.rs index af7a22b078..d74e6678fe 100644 --- a/clients/socks5/src/commands/run.rs +++ b/clients/socks5/src/commands/run.rs @@ -31,9 +31,13 @@ pub(crate) struct Run { #[clap(long)] gateway: Option, - /// Comma separated list of rest endpoints of the validators + /// Comma separated list of rest endpoints of the nymd validators #[clap(long)] - validators: Option, + nymd_validators: Option, + + /// Comma separated list of rest endpoints of the API validators + #[clap(long)] + api_validators: Option, /// Port for the socket to listen on #[clap(short, long)] @@ -49,7 +53,8 @@ pub(crate) struct Run { impl From for OverrideConfig { fn from(run_config: Run) -> Self { OverrideConfig { - validators: run_config.validators, + nymd_validators: run_config.nymd_validators, + api_validators: run_config.api_validators, port: run_config.port, fastmode: false, @@ -81,14 +86,16 @@ fn version_check(cfg: &Config) -> bool { } } -pub(crate) async fn execute(args: &Run) -> Result<(), Socks5ClientError> { +pub(crate) async fn execute(args: &Run) -> Result<(), Box> { let id = &args.id; let mut config = match Config::load_from_file(Some(id)) { Ok(cfg) => cfg, Err(err) => { error!("Failed to load config for {}. Are you sure you have run `init` before? (Error was: {})", id, err); - return Err(Socks5ClientError::FailedToLoadConfig(id.to_string())); + return Err(Box::new(Socks5ClientError::FailedToLoadConfig( + id.to_string(), + ))); } }; @@ -97,7 +104,7 @@ pub(crate) async fn execute(args: &Run) -> Result<(), Socks5ClientError> { if !version_check(&config) { error!("failed the local version check"); - return Err(Socks5ClientError::FailedLocalVersionCheck); + return Err(Box::new(Socks5ClientError::FailedLocalVersionCheck)); } NymClient::new(config).run_forever().await diff --git a/clients/socks5/src/error.rs b/clients/socks5/src/error.rs index 8037d7f23e..6b67e6a339 100644 --- a/clients/socks5/src/error.rs +++ b/clients/socks5/src/error.rs @@ -1,8 +1,10 @@ -use client_core::error::ClientCoreError; +use client_core::{client::reply_key_storage::ReplyKeyStorageError, error::ClientCoreError}; use crypto::asymmetric::identity::Ed25519RecoveryError; use gateway_client::error::GatewayClientError; use validator_client::ValidatorClientError; +use crate::socks::types::SocksProxyError; + #[derive(thiserror::Error, Debug)] pub enum Socks5ClientError { #[error("I/O error: {0}")] @@ -15,9 +17,16 @@ pub enum Socks5ClientError { ValidatorClientError(#[from] ValidatorClientError), #[error("client-core error: {0}")] ClientCoreError(#[from] ClientCoreError), + #[error("Reply key storage error: {0}")] + ReplyKeyStorageError(#[from] ReplyKeyStorageError), + + #[error("SOCKS proxy error")] + SocksProxyError(SocksProxyError), #[error("Failed to load config for: {0}")] FailedToLoadConfig(String), #[error("Failed local version check, client and config mismatch")] FailedLocalVersionCheck, + #[error("Fail to bind address")] + FailToBindAddress, } diff --git a/clients/socks5/src/main.rs b/clients/socks5/src/main.rs index d7a0fdf01f..d41e5e4bd8 100644 --- a/clients/socks5/src/main.rs +++ b/clients/socks5/src/main.rs @@ -1,8 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::error::Error; + use clap::{crate_version, Parser}; -use error::Socks5ClientError; use logging::setup_logging; use network_defaults::setup_env; @@ -12,7 +13,7 @@ pub mod error; pub mod socks; #[tokio::main] -async fn main() -> Result<(), Socks5ClientError> { +async fn main() -> Result<(), Box> { setup_logging(); println!("{}", banner()); diff --git a/clients/socks5/src/socks/client.rs b/clients/socks5/src/socks/client.rs index 5188f7a15d..3e3c24347c 100644 --- a/clients/socks5/src/socks/client.rs +++ b/clients/socks5/src/socks/client.rs @@ -4,7 +4,7 @@ use super::authentication::{AuthenticationMethods, Authenticator, User}; use super::request::{SocksCommand, SocksRequest}; use super::types::{ResponseCode, SocksProxyError}; use super::{RESERVED, SOCKS_VERSION}; -use client_connections::TransmissionLane; +use client_connections::{LaneQueueLengths, TransmissionLane}; use client_core::client::inbound_messages::{InputMessage, InputMessageSender}; use futures::channel::mpsc; use futures::task::{Context, Poll}; @@ -141,6 +141,7 @@ pub(crate) struct SocksClient { service_provider: Recipient, self_address: Recipient, started_proxy: bool, + lane_queue_lengths: LaneQueueLengths, shutdown_listener: ShutdownListener, } @@ -158,6 +159,7 @@ impl Drop for SocksClient { impl SocksClient { /// Create a new SOCKClient + #[allow(clippy::too_many_arguments)] pub fn new( stream: TcpStream, authenticator: Authenticator, @@ -165,6 +167,7 @@ impl SocksClient { service_provider: Recipient, controller_sender: ControllerSender, self_address: Recipient, + lane_queue_lengths: LaneQueueLengths, shutdown_listener: ShutdownListener, ) -> Self { let connection_id = Self::generate_random(); @@ -179,6 +182,7 @@ impl SocksClient { service_provider, self_address, started_proxy: false, + lane_queue_lengths, shutdown_listener, } } @@ -198,6 +202,7 @@ impl SocksClient { pub async fn shutdown(&mut self) -> Result<(), SocksProxyError> { info!("client is shutting down its TCP stream"); self.stream.shutdown().await?; + self.shutdown_listener.mark_as_success(); Ok(()) } @@ -226,7 +231,7 @@ impl SocksClient { } } - fn send_connect_to_mixnet(&mut self, remote_address: RemoteAddress) { + async fn send_connect_to_mixnet(&mut self, remote_address: RemoteAddress) { let req = Request::new_connect(self.connection_id, remote_address, self.self_address); let msg = Message::Request(req); @@ -236,11 +241,15 @@ impl SocksClient { false, TransmissionLane::ConnectionId(self.connection_id), ); - self.input_sender.unbounded_send(input_message).unwrap(); + self.input_sender + .send(input_message) + .await + .expect("InputMessageReceiver has stopped receiving!"); } async fn run_proxy(&mut self, conn_receiver: ConnectionReceiver, remote_proxy_target: String) { - self.send_connect_to_mixnet(remote_proxy_target.clone()); + self.send_connect_to_mixnet(remote_proxy_target.clone()) + .await; let stream = self.stream.run_proxy(); let local_stream_remote = stream @@ -258,6 +267,7 @@ impl SocksClient { conn_receiver, input_sender, connection_id, + Some(self.lane_queue_lengths.clone()), self.shutdown_listener.clone(), ) .run(move |conn_id, read_data, socket_closed| { @@ -309,6 +319,7 @@ impl SocksClient { SocksCommand::UdpAssociate => unimplemented!(), // not handled }; + self.shutdown_listener.mark_as_success(); Ok(()) } diff --git a/clients/socks5/src/socks/mixnet_responses.rs b/clients/socks5/src/socks/mixnet_responses.rs index 5bd83bee85..fa9ad1abe8 100644 --- a/clients/socks5/src/socks/mixnet_responses.rs +++ b/clients/socks5/src/socks/mixnet_responses.rs @@ -1,3 +1,5 @@ +use std::time::Duration; + use futures::channel::mpsc; use futures::StreamExt; use log::*; @@ -18,9 +20,16 @@ pub(crate) struct MixnetResponseListener { impl Drop for MixnetResponseListener { fn drop(&mut self) { - self.buffer_requester + if let Err(err) = self + .buffer_requester .unbounded_send(ReceivedBufferMessage::ReceiverDisconnect) - .expect("the buffer request failed!") + { + if self.shutdown.is_shutdown_poll() { + log::debug!("The buffer request failed: {}", err); + } else { + log::error!("The buffer request failed: {}", err); + } + } } } @@ -96,7 +105,9 @@ impl MixnetResponseListener { } } } - assert!(self.shutdown.is_shutdown_poll()); + tokio::time::timeout(Duration::from_secs(5), self.shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); log::debug!("MixnetResponseListener: Exiting"); } } diff --git a/clients/socks5/src/socks/server.rs b/clients/socks5/src/socks/server.rs index 11912d0a0d..551f82b788 100644 --- a/clients/socks5/src/socks/server.rs +++ b/clients/socks5/src/socks/server.rs @@ -1,17 +1,17 @@ +use crate::error::Socks5ClientError; + use super::authentication::Authenticator; use super::client::SocksClient; -use super::{ - mixnet_responses::MixnetResponseListener, - types::{ResponseCode, SocksProxyError}, -}; -use client_connections::ClosedConnectionSender; +use super::{mixnet_responses::MixnetResponseListener, types::ResponseCode}; +use client_connections::{ConnectionCommandSender, LaneQueueLengths}; use client_core::client::{ inbound_messages::InputMessageSender, received_buffer::ReceivedBufferRequestSender, }; use log::*; use nymsphinx::addressing::clients::Recipient; -use proxy_helpers::connection_controller::Controller; +use proxy_helpers::connection_controller::{BroadcastActiveConnections, Controller}; use std::net::SocketAddr; +use tap::TapFallible; use task::ShutdownListener; use tokio::net::TcpListener; @@ -21,6 +21,7 @@ pub struct SphinxSocksServer { listening_address: SocketAddr, service_provider: Recipient, self_address: Recipient, + lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, } @@ -31,6 +32,7 @@ impl SphinxSocksServer { authenticator: Authenticator, service_provider: Recipient, self_address: Recipient, + lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) -> Self { // hardcode ip as we (presumably) ONLY want to listen locally. If we change it, we can @@ -42,6 +44,7 @@ impl SphinxSocksServer { listening_address: format!("{}:{}", ip, port).parse().unwrap(), service_provider, self_address, + lane_queue_lengths, shutdown, } } @@ -52,14 +55,19 @@ impl SphinxSocksServer { &mut self, input_sender: InputMessageSender, buffer_requester: ReceivedBufferRequestSender, - closed_connection_tx: ClosedConnectionSender, - ) -> Result<(), SocksProxyError> { - let listener = TcpListener::bind(self.listening_address).await.unwrap(); + client_connection_tx: ConnectionCommandSender, + ) -> Result<(), Socks5ClientError> { + let listener = TcpListener::bind(self.listening_address) + .await + .tap_err(|err| log::error!("Failed to bind to address: {err}"))?; info!("Serving Connections..."); // controller for managing all active connections - let (mut active_streams_controller, controller_sender) = - Controller::new(closed_connection_tx, self.shutdown.clone()); + let (mut active_streams_controller, controller_sender) = Controller::new( + client_connection_tx, + BroadcastActiveConnections::Off, + self.shutdown.clone(), + ); tokio::spawn(async move { active_streams_controller.run().await; }); @@ -85,6 +93,7 @@ impl SphinxSocksServer { self.service_provider, controller_sender.clone(), self.self_address, + self.lane_queue_lengths.clone(), self.shutdown.clone(), ); diff --git a/clients/webassembly/Cargo.toml b/clients/webassembly/Cargo.toml index b296e29959..2833598a16 100644 --- a/clients/webassembly/Cargo.toml +++ b/clients/webassembly/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "nym-client-wasm" authors = ["Dave Hrycyszyn ", "Jedrzej Stuczynski "] -version = "1.0.1" +version = "1.1.0" edition = "2021" keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"] license = "Apache-2.0" @@ -19,13 +19,14 @@ coconut = ["coconut-interface", "credentials", "gateway-client/coconut"] [dependencies] futures = "0.3" -serde = { version = "1.0", features = ["derive"] } -serde-wasm-bindgen = "0.4" -wasm-bindgen = { version = "=0.2.83", features = ["serde-serialize"] } -wasm-bindgen-futures = "0.4" js-sys = "0.3" rand = { version = "0.7.3", features = ["wasm-bindgen"] } +serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = "0.4" +tokio = { version = "1.21.2", features = ["sync"] } url = "2.2" +wasm-bindgen = { version = "=0.2.83", features = ["serde-serialize"] } +wasm-bindgen-futures = "0.4" # internal client-core = { path = "../client-core", default-features = false, features = ["wasm"] } diff --git a/clients/webassembly/src/client/mod.rs b/clients/webassembly/src/client/mod.rs index 7aed6c9a3b..c64dbe69e0 100644 --- a/clients/webassembly/src/client/mod.rs +++ b/clients/webassembly/src/client/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use self::config::Config; -use client_connections::{ClosedConnectionReceiver, TransmissionLane}; +use client_connections::{ConnectionCommandReceiver, LaneQueueLengths, TransmissionLane}; use client_core::client::{ cover_traffic_stream::LoopCoverTrafficStream, inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender}, @@ -128,7 +128,8 @@ impl NymClient { ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, mix_sender: BatchMixMessageSender, - closed_connection_rx: ClosedConnectionReceiver, + client_connection_rx: ConnectionCommandReceiver, + lane_queue_lengths: LaneQueueLengths, ) { let mut controller_config = real_messages_control::Config::new( self.key_manager.ack_key(), @@ -153,7 +154,8 @@ impl NymClient { input_receiver, mix_sender, topology_accessor, - closed_connection_rx, + lane_queue_lengths, + client_connection_rx, ) .start(); } @@ -324,7 +326,7 @@ impl NymClient { let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded(); // channels responsible for controlling real messages - let (input_sender, input_receiver) = mpsc::unbounded::(); + let (input_sender, input_receiver) = tokio::sync::mpsc::channel::(1); // channels responsible for controlling ack messages let (ack_sender, ack_receiver) = mpsc::unbounded(); @@ -332,7 +334,7 @@ impl NymClient { // Channel that the real traffix controller can listed to for closing connections. // Currently unused in the wasm client. - let (_closed_connection_tx, closed_connection_rx) = mpsc::unbounded(); + let (_client_connection_tx, client_connection_rx) = mpsc::unbounded(); // the components are started in very specific order. Unless you know what you are doing, // do not change that. @@ -353,12 +355,17 @@ impl NymClient { // The MixTrafficController then sends the actual traffic let sphinx_message_sender = Self::start_mix_traffic_controller(gateway_client); + // Shared queue length data. Published by the `OutQueueController` in the client, and used + // primarily to throttle incoming connections + let shared_lane_queue_lengths = LaneQueueLengths::new(); + self.start_real_traffic_controller( shared_topology_accessor.clone(), ack_receiver, input_receiver, sphinx_message_sender.clone(), - closed_connection_rx, + client_connection_rx, + shared_lane_queue_lengths, ); if !self.config.debug.disable_loop_cover_traffic_stream { @@ -391,8 +398,9 @@ impl NymClient { self.input_tx .as_ref() .expect("start method was not called before!") - .unbounded_send(input_msg) - .unwrap(); + .send(input_msg) + .await + .expect("InputMessageReceiver has stopped receiving!"); self } diff --git a/clients/webassembly/src/gateway_selector.rs b/clients/webassembly/src/gateway_selector.rs index f20ca90fbb..7e15252b8a 100644 --- a/clients/webassembly/src/gateway_selector.rs +++ b/clients/webassembly/src/gateway_selector.rs @@ -6,7 +6,7 @@ use wasm_bindgen::prelude::*; #[wasm_bindgen] pub async fn get_gateway(api_server: String, preferred: Option) -> GatewayEndpoint { - let validator_client = validator_client::ApiClient::new(api_server.parse().unwrap()); + let validator_client = validator_client::client::ApiClient::new(api_server.parse().unwrap()); let gateways = match validator_client.get_cached_gateways().await { Err(err) => panic!("failed to obtain list of all gateways - {}", err), diff --git a/common/client-connections/Cargo.toml b/common/client-connections/Cargo.toml index 067621c28f..7438be79ba 100644 --- a/common/client-connections/Cargo.toml +++ b/common/client-connections/Cargo.toml @@ -7,3 +7,4 @@ edition = "2021" [dependencies] futures = "0.3" +log = "0.4.17" diff --git a/common/client-connections/src/lib.rs b/common/client-connections/src/lib.rs index 2aabce78b0..515938ed54 100644 --- a/common/client-connections/src/lib.rs +++ b/common/client-connections/src/lib.rs @@ -1,6 +1,8 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use std::collections::HashMap; + use futures::channel::mpsc; pub type ConnectionId = u64; @@ -14,8 +16,96 @@ pub enum TransmissionLane { ConnectionId(ConnectionId), } -/// Announce connections that are closed, for whoever is interested. -/// One usecase is that the network-requester and socks5-client wants to know about this, so that -/// they can forward this to the `OutQueueControl` (via `ClientRequest` for the network-requester) -pub type ClosedConnectionSender = mpsc::UnboundedSender; -pub type ClosedConnectionReceiver = mpsc::UnboundedReceiver; +/// Used by the connection controller to report current state for client connections. +pub type ConnectionCommandSender = mpsc::UnboundedSender; +pub type ConnectionCommandReceiver = mpsc::UnboundedReceiver; + +pub enum ConnectionCommand { + // Announce that at a connection was closed. E.g the `OutQueueControl` uses this to discard + // transmission lanes. + Close(ConnectionId), + + // In the network requester for example, we usually want to broadcast active connections + // regularly, so we know what connections we need to request lane queue lengths for from the + // client. + // In the socks5-client, this is not needed since have direct access to the lane queue lengths. + ActiveConnections(Vec), +} + +// The `OutQueueControl` publishes the backlog per lane, primarily so that upstream can slow down +// if needed. +#[derive(Clone, Debug)] +pub struct LaneQueueLengths(std::sync::Arc>); + +impl LaneQueueLengths { + pub fn new() -> Self { + LaneQueueLengths(std::sync::Arc::new(std::sync::Mutex::new( + LaneQueueLengthsInner { + map: HashMap::new(), + }, + ))) + } + + pub fn set(&mut self, lane: &TransmissionLane, lane_length: Option) { + match self.0.lock() { + Ok(mut inner) => { + if let Some(length) = lane_length { + inner + .map + .entry(*lane) + .and_modify(|e| *e = length) + .or_insert(length); + } else { + inner.map.remove(lane); + } + } + Err(err) => log::warn!("Failed to set lane queue length: {err}"), + } + } + + pub fn get(&self, lane: &TransmissionLane) -> Option { + match self.0.lock() { + Ok(inner) => inner.get(lane), + Err(err) => { + log::warn!("Failed to get lane queue length: {err}"); + None + } + } + } +} + +impl Default for LaneQueueLengths { + fn default() -> Self { + Self::new() + } +} + +impl std::ops::Deref for LaneQueueLengths { + type Target = std::sync::Arc>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug)] +pub struct LaneQueueLengthsInner { + pub map: HashMap, +} + +impl LaneQueueLengthsInner { + pub fn get(&self, lane: &TransmissionLane) -> Option { + self.map.get(lane).copied() + } + + pub fn values(&self) -> impl Iterator { + self.map.values() + } + + pub fn modify(&mut self, lane: &TransmissionLane, f: F) + where + F: FnOnce(&mut usize), + { + self.map.entry(*lane).and_modify(f); + } +} diff --git a/common/client-libs/gateway-client/src/bandwidth.rs b/common/client-libs/gateway-client/src/bandwidth.rs index 77916876f1..6f3b7722dc 100644 --- a/common/client-libs/gateway-client/src/bandwidth.rs +++ b/common/client-libs/gateway-client/src/bandwidth.rs @@ -17,6 +17,8 @@ use credential_storage::error::StorageError; #[cfg(feature = "coconut")] use std::str::FromStr; #[cfg(feature = "coconut")] +use validator_client::client::CoconutApiClient; +#[cfg(feature = "coconut")] use { coconut_interface::Base58, credentials::coconut::{ @@ -29,7 +31,7 @@ pub struct BandwidthController { #[allow(dead_code)] storage: St, #[cfg(feature = "coconut")] - validator_endpoints: Vec, + coconut_api_clients: Vec, } impl BandwidthController @@ -37,10 +39,10 @@ where St: Storage + Clone + 'static, { #[cfg(feature = "coconut")] - pub fn new(storage: St, validator_endpoints: Vec) -> Self { + pub fn new(storage: St, coconut_api_clients: Vec) -> Self { BandwidthController { storage, - validator_endpoints, + coconut_api_clients, } } @@ -53,7 +55,7 @@ where pub async fn prepare_coconut_credential( &self, ) -> Result { - let verification_key = obtain_aggregate_verification_key(&self.validator_endpoints).await?; + let verification_key = obtain_aggregate_verification_key(&self.coconut_api_clients).await?; let bandwidth_credential = self.storage.get_next_coconut_credential().await?; let voucher_value = u64::from_str(&bandwidth_credential.voucher_value) .map_err(|_| StorageError::InconsistentData)?; diff --git a/common/client-libs/gateway-client/src/client.rs b/common/client-libs/gateway-client/src/client.rs index 9e17f55ff8..e6b89c13f9 100644 --- a/common/client-libs/gateway-client/src/client.rs +++ b/common/client-libs/gateway-client/src/client.rs @@ -2,25 +2,19 @@ // SPDX-License-Identifier: Apache-2.0 use crate::bandwidth::BandwidthController; -use crate::cleanup_socket_message; use crate::error::GatewayClientError; use crate::packet_router::PacketRouter; pub use crate::packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, }; use crate::socket_state::{PartiallyDelegated, SocketState}; -#[cfg(target_arch = "wasm32")] -use crate::wasm_storage::PersistentStorage; -#[cfg(feature = "coconut")] -use coconut_interface::Credential; -#[cfg(not(target_arch = "wasm32"))] -use credential_storage::PersistentStorage; +use crate::{cleanup_socket_message, try_decrypt_binary_message}; use crypto::asymmetric::identity; use futures::{FutureExt, SinkExt, StreamExt}; use gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; use gateway_requests::iv::IV; use gateway_requests::registration::handshake::{client_handshake, SharedKeys}; -use gateway_requests::{BinaryRequest, ClientControlRequest, ServerResponse}; +use gateway_requests::{BinaryRequest, ClientControlRequest, ServerResponse, PROTOCOL_VERSION}; use log::*; use network_defaults::{REMAINING_BANDWIDTH_THRESHOLD, TOKENS_TO_BURN}; use nymsphinx::forwarding::packet::MixPacket; @@ -28,13 +22,20 @@ use rand::rngs::OsRng; use std::convert::TryFrom; use std::sync::Arc; use std::time::Duration; -#[cfg(not(target_arch = "wasm32"))] -use task::ShutdownListener; use tungstenite::protocol::Message; +#[cfg(feature = "coconut")] +use coconut_interface::Credential; + +#[cfg(not(target_arch = "wasm32"))] +use credential_storage::PersistentStorage; +#[cfg(not(target_arch = "wasm32"))] +use task::ShutdownListener; #[cfg(not(target_arch = "wasm32"))] use tokio_tungstenite::connect_async; +#[cfg(target_arch = "wasm32")] +use crate::wasm_storage::PersistentStorage; #[cfg(target_arch = "wasm32")] use wasm_timer; #[cfg(target_arch = "wasm32")] @@ -306,6 +307,8 @@ impl GatewayClient { let m_shutdown = self.shutdown.clone(); async { if let Some(mut s) = m_shutdown { + // TODO: fix this by marking as success _after_ the select + s.mark_as_success(); s.recv().await } else { std::future::pending::<()>().await @@ -336,7 +339,15 @@ impl GatewayClient { }; match ws_msg { Message::Binary(bin_msg) => { - if let Err(err) = self.packet_router.route_received(vec![bin_msg]) { + // if we have established the shared key already, attempt to use it for decryption + // otherwise there's not much we can do apart from just routing what we have on hand + if let Some(shared_keys) = &self.shared_key { + if let Some(plaintext) = try_decrypt_binary_message(bin_msg, shared_keys) { + if let Err(err) = self.packet_router.route_received(vec![plaintext]) { + log::warn!("Route received failed: {:?}", err); + } + } + } else if let Err(err) = self.packet_router.route_received(vec![bin_msg]) { log::warn!("Route received failed: {:?}", err); } } @@ -436,6 +447,33 @@ impl GatewayClient { } } + fn check_gateway_protocol( + &self, + gateway_protocol: Option, + ) -> Result<(), GatewayClientError> { + // right now there are no failure cases here, but this might change in the future + match gateway_protocol { + None => { + warn!("the gateway we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0"); + // note: in 1.2.0 we will have to return a hard error here + Ok(()) + } + Some(v) if v != PROTOCOL_VERSION => { + let err = GatewayClientError::IncompatibleProtocol { + gateway: Some(v), + current: PROTOCOL_VERSION, + }; + error!("{err}"); + Err(err) + } + + Some(_) => { + info!("the gateway is using exactly the same protocol version as we are. We're good to continue!"); + Ok(()) + } + } + } + async fn register(&mut self) -> Result<(), GatewayClientError> { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); @@ -458,11 +496,20 @@ impl GatewayClient { .map_err(GatewayClientError::RegistrationFailure), _ => unreachable!(), }?; - self.authenticated = match self.read_control_response().await? { - ServerResponse::Register { status } => Ok(status), - ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), - _ => Err(GatewayClientError::UnexpectedResponse), - }?; + let (authentication_status, gateway_protocol) = match self.read_control_response().await? { + ServerResponse::Register { + protocol_version, + status, + } => (status, protocol_version), + ServerResponse::Error { message } => { + return Err(GatewayClientError::GatewayError(message)) + } + _ => return Err(GatewayClientError::UnexpectedResponse), + }; + + self.check_gateway_protocol(gateway_protocol)?; + self.authenticated = authentication_status; + if self.authenticated { self.shared_key = Some(Arc::new(shared_key)); } @@ -501,9 +548,11 @@ impl GatewayClient { match self.send_websocket_message(msg).await? { ServerResponse::Authenticate { + protocol_version, status, bandwidth_remaining, } => { + self.check_gateway_protocol(protocol_version)?; self.authenticated = status; self.bandwidth_remaining = bandwidth_remaining; Ok(()) diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index bb6f71f564..8a8104658b 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -85,6 +85,9 @@ pub enum GatewayClientError { #[error("Failed to send mixnet message")] MixnetMsgSenderFailedToSend, + + #[error("Attempted to negotiate connection with gateway using incompatible protocol version. Ours is {current} and the gateway reports {gateway:?}")] + IncompatibleProtocol { gateway: Option, current: u8 }, } impl GatewayClientError { diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index 1becf01b4a..8671a7363b 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -3,6 +3,9 @@ use crate::error::GatewayClientError; pub use client::GatewayClient; +use gateway_requests::registration::handshake::SharedKeys; +use gateway_requests::BinaryResponse; +use log::warn; pub use packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, }; @@ -37,3 +40,21 @@ pub(crate) fn cleanup_socket_messages( None => Err(GatewayClientError::ConnectionAbruptlyClosed), } } + +pub(crate) fn try_decrypt_binary_message( + bin_msg: Vec, + shared_keys: &SharedKeys, +) -> Option> { + match BinaryResponse::try_from_encrypted_tagged_bytes(bin_msg, shared_keys) { + Ok(bin_response) => match bin_response { + BinaryResponse::PushedMixMessage(plaintext) => Some(plaintext), + }, + Err(err) => { + warn!( + "message received from the gateway was malformed! - {:?}", + err + ); + None + } + } +} diff --git a/common/client-libs/gateway-client/src/socket_state.rs b/common/client-libs/gateway-client/src/socket_state.rs index df1b95c0ae..2d66e782e8 100644 --- a/common/client-libs/gateway-client/src/socket_state.rs +++ b/common/client-libs/gateway-client/src/socket_state.rs @@ -1,14 +1,13 @@ // Copyright 2021-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::cleanup_socket_messages; use crate::error::GatewayClientError; use crate::packet_router::PacketRouter; +use crate::{cleanup_socket_messages, try_decrypt_binary_message}; use futures::channel::oneshot; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use gateway_requests::registration::handshake::SharedKeys; -use gateway_requests::BinaryResponse; use log::*; use std::sync::Arc; #[cfg(not(target_arch = "wasm32"))] @@ -50,21 +49,9 @@ impl PartiallyDelegated { match ws_msg { Message::Binary(bin_msg) => { // this function decrypts the request and checks the MAC - let plaintext = match BinaryResponse::try_from_encrypted_tagged_bytes( - bin_msg, shared_key, - ) { - Ok(bin_response) => match bin_response { - BinaryResponse::PushedMixMessage(plaintext) => plaintext, - }, - Err(err) => { - warn!( - "message received from the gateway was malformed! - {:?}", - err - ); - continue; - } - }; - plaintexts.push(plaintext) + if let Some(plaintext) = try_decrypt_binary_message(bin_msg, shared_key) { + plaintexts.push(plaintext) + } } // I think that in the future we should perhaps have some sequence number system, i.e. // so each request/response pair can be easily identified, so that if messages are diff --git a/common/client-libs/validator-client/Cargo.toml b/common/client-libs/validator-client/Cargo.toml index 2b45232238..ec842828e7 100644 --- a/common/client-libs/validator-client/Cargo.toml +++ b/common/client-libs/validator-client/Cargo.toml @@ -10,10 +10,11 @@ rust-version = "1.56" [dependencies] base64 = "0.13" colored = "2.0" -cw3 = "0.13.1" + +coconut-dkg-common = { path = "../../cosmwasm-smart-contracts/coconut-dkg" } +contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common" } mixnet-contract-common = { path= "../../cosmwasm-smart-contracts/mixnet-contract" } vesting-contract-common = { path= "../../cosmwasm-smart-contracts/vesting-contract" } -contracts-common = { path = "../../cosmwasm-smart-contracts/contracts-common" } coconut-bandwidth-contract-common = { path= "../../cosmwasm-smart-contracts/coconut-bandwidth-contract" } multisig-contract-common = { path = "../../cosmwasm-smart-contracts/multisig-contract" } vesting-contract = { path = "../../../contracts/vesting" } @@ -37,6 +38,7 @@ async-trait = { version = "0.1.51", optional = true } bip39 = { version = "1", features = ["rand"], optional = true } config = { path = "../../config", optional = true } cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support", features = ["rpc", "bip32", "cosmwasm"], optional = true} +cw3 = { version = "0.13.4", optional = true } prost = { version = "0.10", default-features = false, optional = true } flate2 = { version = "1.0.20", optional = true } sha2 = { version = "0.9.5", optional = true } @@ -53,6 +55,7 @@ nymd-client = [ "bip39", "config", "cosmrs", + "cw3", "prost", "flate2", "sha2", diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index a4d0a22f05..8ddce33a4f 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -2,13 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 use crate::{validator_api, ValidatorClientError}; +use coconut_dkg_common::types::NodeIndex; +#[cfg(feature = "nymd-client")] +use coconut_dkg_common::{ + dealer::ContractDealing, types::DealerDetails, verification_key::ContractVKShare, +}; +#[cfg(feature = "nymd-client")] +use coconut_interface::Base58; +use coconut_interface::VerificationKey; use mixnet_contract_common::mixnode::MixNodeDetails; use mixnet_contract_common::MixId; use mixnet_contract_common::{GatewayBond, IdentityKeyRef}; -use url::Url; +#[cfg(feature = "nymd-client")] +use std::str::FromStr; use validator_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, - VerifyCredentialBody, VerifyCredentialResponse, + BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use validator_api_requests::models::{ GatewayCoreStatusResponse, MixnodeCoreStatusResponse, MixnodeStatusResponse, @@ -16,10 +24,12 @@ use validator_api_requests::models::{ }; #[cfg(feature = "nymd-client")] -use crate::nymd::traits::MixnetQueryClient; +use crate::nymd::traits::{DkgQueryClient, MixnetQueryClient, MultisigQueryClient}; #[cfg(feature = "nymd-client")] use crate::nymd::{self, CosmWasmClient, NymdClient, QueryNymdClient, SigningNymdClient}; #[cfg(feature = "nymd-client")] +use cw3::ProposalResponse; +#[cfg(feature = "nymd-client")] use mixnet_contract_common::{ mixnode::MixNodeBond, pending_events::{PendingEpochEvent, PendingIntervalEvent}, @@ -27,6 +37,7 @@ use mixnet_contract_common::{ }; #[cfg(feature = "nymd-client")] use network_defaults::NymNetworkDetails; +use url::Url; #[cfg(feature = "nymd-client")] use validator_api_requests::models::MixNodeBondAnnotated; @@ -43,6 +54,9 @@ pub struct Config { gateway_page_limit: Option, mixnode_delegations_page_limit: Option, rewarded_set_page_limit: Option, + dealers_page_limit: Option, + verification_key_page_limit: Option, + proposals_page_limit: Option, } #[cfg(feature = "nymd-client")] @@ -72,6 +86,9 @@ impl Config { gateway_page_limit: None, mixnode_delegations_page_limit: None, rewarded_set_page_limit: None, + dealers_page_limit: None, + verification_key_page_limit: None, + proposals_page_limit: None, }) } @@ -119,6 +136,9 @@ pub struct Client { gateway_page_limit: Option, mixnode_delegations_page_limit: Option, rewarded_set_page_limit: Option, + dealers_page_limit: Option, + verification_key_page_limit: Option, + proposals_page_limit: Option, // ideally they would have been read-only, but unfortunately rust doesn't have such features pub validator_api: validator_api::Client, @@ -145,6 +165,9 @@ impl Client { gateway_page_limit: config.gateway_page_limit, mixnode_delegations_page_limit: config.mixnode_delegations_page_limit, rewarded_set_page_limit: config.rewarded_set_page_limit, + dealers_page_limit: config.dealers_page_limit, + verification_key_page_limit: config.verification_key_page_limit, + proposals_page_limit: config.proposals_page_limit, validator_api: validator_api_client, nymd: nymd_client, }) @@ -178,6 +201,9 @@ impl Client { gateway_page_limit: config.gateway_page_limit, mixnode_delegations_page_limit: config.mixnode_delegations_page_limit, rewarded_set_page_limit: config.rewarded_set_page_limit, + dealers_page_limit: config.dealers_page_limit, + verification_key_page_limit: config.verification_key_page_limit, + proposals_page_limit: config.proposals_page_limit, validator_api: validator_api_client, nymd: nymd_client, }) @@ -519,6 +545,135 @@ impl Client { Ok(events) } + + pub async fn get_all_nymd_current_dealers( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut dealers = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_current_dealers_paged(start_after.take(), self.dealers_page_limit) + .await?; + dealers.append(&mut paged_response.dealers); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res.into_string()) + } else { + break; + } + } + + Ok(dealers) + } + + pub async fn get_all_nymd_past_dealers( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut dealers = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_past_dealers_paged(start_after.take(), self.dealers_page_limit) + .await?; + dealers.append(&mut paged_response.dealers); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res.into_string()) + } else { + break; + } + } + + Ok(dealers) + } + + pub async fn get_all_nymd_epoch_dealings( + &self, + idx: usize, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut dealings = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_dealings_paged(idx, start_after.take(), self.dealers_page_limit) + .await?; + dealings.append(&mut paged_response.dealings); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res.into_string()) + } else { + break; + } + } + + Ok(dealings) + } + + pub async fn get_all_nymd_verification_key_shares( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut shares = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_vk_shares_paged(start_after.take(), self.verification_key_page_limit) + .await?; + shares.append(&mut paged_response.shares); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res.into_string()) + } else { + break; + } + } + + Ok(shares) + } + + pub async fn get_all_nymd_proposals( + &self, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + let mut proposals = Vec::new(); + let mut start_after = None; + + loop { + let mut paged_response = self + .nymd + .list_proposals(start_after.take(), self.proposals_page_limit) + .await?; + + let last_id = paged_response.proposals.last().map(|prop| prop.id); + proposals.append(&mut paged_response.proposals); + + if let Some(start_after_res) = last_id { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(proposals) + } } // validator-api wrappers @@ -572,14 +727,53 @@ impl Client { ) -> Result { Ok(self.validator_api.blind_sign(request_body).await?) } +} - pub async fn get_coconut_verification_key( - &self, - ) -> Result { - Ok(self.validator_api.get_coconut_verification_key().await?) +#[derive(Clone)] +pub struct CoconutApiClient { + pub api_client: ApiClient, + pub verification_key: VerificationKey, + pub node_id: NodeIndex, + #[cfg(feature = "nymd-client")] + pub cosmos_address: cosmrs::AccountId, +} + +#[cfg(feature = "nymd-client")] +impl CoconutApiClient { + pub async fn all_coconut_api_clients( + nymd_client: &Client, + ) -> Result, ValidatorClientError> + where + C: CosmWasmClient + Sync + Send, + { + Ok(nymd_client + .get_all_nymd_verification_key_shares() + .await? + .into_iter() + .filter_map(Self::try_from) + .collect()) + } + + fn try_from(share: ContractVKShare) -> Option { + if share.verified { + if let Ok(url_address) = Url::parse(&share.announce_address) { + if let Ok(verification_key) = VerificationKey::try_from_bs58(&share.share) { + if let Ok(cosmos_address) = cosmrs::AccountId::from_str(share.owner.as_str()) { + return Some(CoconutApiClient { + api_client: ApiClient::new(url_address), + verification_key, + node_id: share.node_index, + cosmos_address, + }); + } + } + } + } + None } } +#[derive(Clone)] pub struct ApiClient { pub validator_api: validator_api::Client, // TODO: perhaps if we really need it at some (currently I don't see any reasons for it) @@ -685,16 +879,6 @@ impl ApiClient { .await?) } - pub async fn get_coconut_verification_key( - &self, - ) -> Result { - Ok(self.validator_api.get_coconut_verification_key().await?) - } - - pub async fn get_cosmos_address(&self) -> Result { - Ok(self.validator_api.get_cosmos_address().await?) - } - pub async fn verify_bandwidth_credential( &self, request_body: &VerifyCredentialBody, diff --git a/common/client-libs/validator-client/src/lib.rs b/common/client-libs/validator-client/src/lib.rs index 3e11d3fcb6..2a5e010338 100644 --- a/common/client-libs/validator-client/src/lib.rs +++ b/common/client-libs/validator-client/src/lib.rs @@ -9,7 +9,8 @@ mod error; pub mod nymd; pub mod validator_api; -pub use crate::client::ApiClient; +#[cfg(feature = "nymd-client")] +pub use crate::client::{ApiClient, CoconutApiClient}; pub use crate::error::ValidatorClientError; pub use validator_api_requests::*; diff --git a/common/client-libs/validator-client/src/nymd/cosmwasm_client/logs.rs b/common/client-libs/validator-client/src/nymd/cosmwasm_client/logs.rs index 7ddf3ef1dd..6c68c306f6 100644 --- a/common/client-libs/validator-client/src/nymd/cosmwasm_client/logs.rs +++ b/common/client-libs/validator-client/src/nymd/cosmwasm_client/logs.rs @@ -6,6 +6,9 @@ use cosmrs::tendermint::abci; use itertools::Itertools; use serde::{Deserialize, Serialize}; +pub use coconut_bandwidth_contract_common::event_attributes::*; +pub use coconut_dkg_common::event_attributes::*; + // it seems that currently validators just emit stringified events (which are also returned as part of deliverTx response) // as theirs logs #[derive(Debug, Serialize, Deserialize)] diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 481cb021be..dabe4b0819 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -65,6 +65,7 @@ pub struct Config { pub(crate) bandwidth_claim_contract_address: Option, pub(crate) coconut_bandwidth_contract_address: Option, pub(crate) multisig_contract_address: Option, + pub(crate) coconut_dkg_contract_address: Option, // TODO: add this in later commits // pub(crate) gas_price: GasPrice, } @@ -118,6 +119,10 @@ impl Config { details.contracts.multisig_contract_address.as_ref(), prefix, )?, + coconut_dkg_contract_address: Self::parse_optional_account( + details.contracts.coconut_dkg_contract_address.as_ref(), + prefix, + )?, }) } } @@ -275,6 +280,14 @@ impl NymdClient { self.config.multisig_contract_address.as_ref().unwrap() } + // TODO: this should get changed into Result<&AccountId, NymdError> (or Option<&AccountId> in future commits + // note: what unwrap is doing here is just moving a failure that would have normally + // occurred in `connect` when attempting to parse an empty address, + // so it's not introducing new source of failure (just moves it) + pub fn coconut_dkg_contract_address(&self) -> &AccountId { + self.config.coconut_dkg_contract_address.as_ref().unwrap() + } + pub fn set_simulated_gas_multiplier(&mut self, multiplier: f32) { self.simulated_gas_multiplier = multiplier; } diff --git a/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs b/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs new file mode 100644 index 0000000000..561b9f44ef --- /dev/null +++ b/common/client-libs/validator-client/src/nymd/traits/dkg_query_client.rs @@ -0,0 +1,126 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nymd::error::NymdError; +use crate::nymd::{CosmWasmClient, NymdClient}; +use async_trait::async_trait; +use coconut_dkg_common::dealer::{ + DealerDetailsResponse, PagedDealerResponse, PagedDealingsResponse, +}; +use coconut_dkg_common::msg::QueryMsg as DkgQueryMsg; +use coconut_dkg_common::types::EpochState; +use coconut_dkg_common::verification_key::PagedVKSharesResponse; +use cosmrs::AccountId; + +#[async_trait] +pub trait DkgQueryClient { + async fn get_current_epoch_state(&self) -> Result; + async fn get_dealer_details( + &self, + address: &AccountId, + ) -> Result; + async fn get_current_dealers_paged( + &self, + start_after: Option, + page_limit: Option, + ) -> Result; + async fn get_past_dealers_paged( + &self, + start_after: Option, + page_limit: Option, + ) -> Result; + + async fn get_dealings_paged( + &self, + idx: usize, + start_after: Option, + page_limit: Option, + ) -> Result; + async fn get_vk_shares_paged( + &self, + start_after: Option, + page_limit: Option, + ) -> Result; +} + +#[async_trait] +impl DkgQueryClient for NymdClient +where + C: CosmWasmClient + Send + Sync, +{ + async fn get_current_epoch_state(&self) -> Result { + let request = DkgQueryMsg::GetCurrentEpochState {}; + self.client + .query_contract_smart(self.coconut_dkg_contract_address(), &request) + .await + } + async fn get_dealer_details( + &self, + address: &AccountId, + ) -> Result { + let request = DkgQueryMsg::GetDealerDetails { + dealer_address: address.to_string(), + }; + self.client + .query_contract_smart(self.coconut_dkg_contract_address(), &request) + .await + } + + async fn get_current_dealers_paged( + &self, + start_after: Option, + page_limit: Option, + ) -> Result { + let request = DkgQueryMsg::GetCurrentDealers { + start_after, + limit: page_limit, + }; + self.client + .query_contract_smart(self.coconut_dkg_contract_address(), &request) + .await + } + + async fn get_past_dealers_paged( + &self, + start_after: Option, + page_limit: Option, + ) -> Result { + let request = DkgQueryMsg::GetPastDealers { + start_after, + limit: page_limit, + }; + self.client + .query_contract_smart(self.coconut_dkg_contract_address(), &request) + .await + } + + async fn get_dealings_paged( + &self, + idx: usize, + start_after: Option, + page_limit: Option, + ) -> Result { + let request = DkgQueryMsg::GetDealing { + idx: idx as u64, + limit: page_limit, + start_after, + }; + self.client + .query_contract_smart(self.coconut_dkg_contract_address(), &request) + .await + } + + async fn get_vk_shares_paged( + &self, + start_after: Option, + page_limit: Option, + ) -> Result { + let request = DkgQueryMsg::GetVerificationKeys { + limit: page_limit, + start_after, + }; + self.client + .query_contract_smart(self.coconut_dkg_contract_address(), &request) + .await + } +} diff --git a/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs new file mode 100644 index 0000000000..b0d1dfaf08 --- /dev/null +++ b/common/client-libs/validator-client/src/nymd/traits/dkg_signing_client.rs @@ -0,0 +1,100 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::nymd::cosmwasm_client::types::ExecuteResult; +use crate::nymd::error::NymdError; +use crate::nymd::{Fee, NymdClient, SigningCosmWasmClient}; +use async_trait::async_trait; +use coconut_dkg_common::msg::ExecuteMsg as DkgExecuteMsg; +use coconut_dkg_common::types::EncodedBTEPublicKeyWithProof; +use coconut_dkg_common::verification_key::VerificationKeyShare; +use contracts_common::dealings::ContractSafeBytes; + +#[async_trait] +pub trait DkgSigningClient { + async fn register_dealer( + &self, + bte_key: EncodedBTEPublicKeyWithProof, + announce_address: String, + fee: Option, + ) -> Result; + + async fn submit_dealing_bytes( + &self, + commitment: ContractSafeBytes, + fee: Option, + ) -> Result; + + async fn submit_verification_key_share( + &self, + share: VerificationKeyShare, + fee: Option, + ) -> Result; +} + +#[async_trait] +impl DkgSigningClient for NymdClient +where + C: SigningCosmWasmClient + Send + Sync, +{ + async fn register_dealer( + &self, + bte_key: EncodedBTEPublicKeyWithProof, + announce_address: String, + fee: Option, + ) -> Result { + let req = DkgExecuteMsg::RegisterDealer { + bte_key_with_proof: bte_key, + announce_address, + }; + + self.client + .execute( + self.address(), + self.coconut_dkg_contract_address(), + &req, + fee.unwrap_or_default(), + format!("registering {} as a dealer", self.address()), + vec![], + ) + .await + } + + async fn submit_dealing_bytes( + &self, + dealing_bytes: ContractSafeBytes, + fee: Option, + ) -> Result { + let req = DkgExecuteMsg::CommitDealing { dealing_bytes }; + + self.client + .execute( + self.address(), + self.coconut_dkg_contract_address(), + &req, + fee.unwrap_or_default(), + "dealing commitment", + vec![], + ) + .await + } + + async fn submit_verification_key_share( + &self, + share: VerificationKeyShare, + fee: Option, + ) -> Result { + let req = DkgExecuteMsg::CommitVerificationKeyShare { share }; + + self.client + .execute( + self.address(), + self.coconut_dkg_contract_address(), + &req, + fee.unwrap_or_default(), + "verification key share commitment", + vec![], + ) + .await + } +} diff --git a/common/client-libs/validator-client/src/nymd/traits/mod.rs b/common/client-libs/validator-client/src/nymd/traits/mod.rs index 61a562a5b3..11e70cd484 100644 --- a/common/client-libs/validator-client/src/nymd/traits/mod.rs +++ b/common/client-libs/validator-client/src/nymd/traits/mod.rs @@ -3,6 +3,8 @@ mod coconut_bandwidth_query_client; mod coconut_bandwidth_signing_client; +mod dkg_query_client; +mod dkg_signing_client; mod mixnet_query_client; mod mixnet_signing_client; mod multisig_query_client; @@ -12,6 +14,8 @@ mod vesting_signing_client; pub use coconut_bandwidth_query_client::CoconutBandwidthQueryClient; pub use coconut_bandwidth_signing_client::CoconutBandwidthSigningClient; +pub use dkg_query_client::DkgQueryClient; +pub use dkg_signing_client::DkgSigningClient; pub use mixnet_query_client::MixnetQueryClient; pub use mixnet_signing_client::MixnetSigningClient; pub use multisig_query_client::MultisigQueryClient; diff --git a/common/client-libs/validator-client/src/nymd/traits/multisig_query_client.rs b/common/client-libs/validator-client/src/nymd/traits/multisig_query_client.rs index 9166897b18..89643e8136 100644 --- a/common/client-libs/validator-client/src/nymd/traits/multisig_query_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/multisig_query_client.rs @@ -4,13 +4,19 @@ use crate::nymd::error::NymdError; use crate::nymd::{CosmWasmClient, NymdClient}; -use multisig_contract_common::msg::{ProposalResponse, QueryMsg}; +use cw3::{ProposalListResponse, ProposalResponse}; +use multisig_contract_common::msg::QueryMsg; use async_trait::async_trait; #[async_trait] pub trait MultisigQueryClient { async fn get_proposal(&self, proposal_id: u64) -> Result; + async fn list_proposals( + &self, + start_after: Option, + limit: Option, + ) -> Result; } #[async_trait] @@ -21,4 +27,15 @@ impl MultisigQueryClient for NymdClient { .query_contract_smart(self.multisig_contract_address(), &request) .await } + + async fn list_proposals( + &self, + start_after: Option, + limit: Option, + ) -> Result { + let request = QueryMsg::ListProposals { start_after, limit }; + self.client + .query_contract_smart(self.multisig_contract_address(), &request) + .await + } } diff --git a/common/client-libs/validator-client/src/nymd/traits/multisig_signing_client.rs b/common/client-libs/validator-client/src/nymd/traits/multisig_signing_client.rs index 46bb118ef0..a715c85075 100644 --- a/common/client-libs/validator-client/src/nymd/traits/multisig_signing_client.rs +++ b/common/client-libs/validator-client/src/nymd/traits/multisig_signing_client.rs @@ -7,11 +7,11 @@ use crate::nymd::error::NymdError; use crate::nymd::{Fee, NymdClient}; use coconut_bandwidth_contract_common::msg::ExecuteMsg as CoconutBandwidthExecuteMsg; +use cw3::Vote; use multisig_contract_common::msg::ExecuteMsg; use async_trait::async_trait; use cosmwasm_std::{to_binary, Coin, CosmosMsg, WasmMsg}; -use cw3::Vote; #[async_trait] pub trait MultisigSigningClient { diff --git a/common/client-libs/validator-client/src/validator_api/mod.rs b/common/client-libs/validator-client/src/validator_api/mod.rs index 0f4ebc60db..4a7b7af3cd 100644 --- a/common/client-libs/validator-client/src/validator_api/mod.rs +++ b/common/client-libs/validator-client/src/validator_api/mod.rs @@ -9,8 +9,7 @@ use reqwest::Response; use serde::{Deserialize, Serialize}; use url::Url; use validator_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, - VerifyCredentialBody, VerifyCredentialResponse, + BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use validator_api_requests::models::{ GatewayCoreStatusResponse, GatewayStatusReportResponse, GatewayUptimeHistoryResponse, @@ -27,6 +26,7 @@ type Params<'a, K, V> = &'a [(K, V)]; const NO_PARAMS: Params<'_, &'_ str, &'_ str> = &[]; +#[derive(Clone)] pub struct Client { url: Url, reqwest_client: reqwest::Client, @@ -443,34 +443,6 @@ impl Client { .await } - pub async fn get_coconut_verification_key( - &self, - ) -> Result { - self.query_validator_api( - &[ - routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_VERIFICATION_KEY, - ], - NO_PARAMS, - ) - .await - } - - pub async fn get_cosmos_address(&self) -> Result { - self.query_validator_api( - &[ - routes::API_VERSION, - routes::COCONUT_ROUTES, - routes::BANDWIDTH, - routes::COCONUT_COSMOS_ADDRESS, - ], - NO_PARAMS, - ) - .await - } - pub async fn verify_bandwidth_credential( &self, request_body: &VerifyCredentialBody, diff --git a/common/client-libs/validator-client/src/validator_api/routes.rs b/common/client-libs/validator-client/src/validator_api/routes.rs index 64aa1b8cd9..66a822b4fd 100644 --- a/common/client-libs/validator-client/src/validator_api/routes.rs +++ b/common/client-libs/validator-client/src/validator_api/routes.rs @@ -15,8 +15,6 @@ pub const BANDWIDTH: &str = "bandwidth"; pub const COCONUT_BLIND_SIGN: &str = "blind-sign"; pub const COCONUT_PARTIAL_BANDWIDTH_CREDENTIAL: &str = "partial-bandwidth-credential"; -pub const COCONUT_VERIFICATION_KEY: &str = "verification-key"; -pub const COCONUT_COSMOS_ADDRESS: &str = "cosmos-address"; pub const COCONUT_VERIFY_BANDWIDTH_CREDENTIAL: &str = "verify-bandwidth-credential"; pub const STATUS_ROUTES: &str = "status"; diff --git a/common/commands/Cargo.toml b/common/commands/Cargo.toml index 35b38512fa..ce0195cce4 100644 --- a/common/commands/Cargo.toml +++ b/common/commands/Cargo.toml @@ -22,6 +22,7 @@ thiserror = "1" time = { version = "0.3.6", features = ["parsing", "formatting"] } toml = "0.5.6" url = "2.2" +tap = "1" cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support" } cosmwasm-std = { version = "1.0.0" } diff --git a/common/commands/src/context/errors.rs b/common/commands/src/context/errors.rs index 5c3d24a2bb..44a99f49ed 100644 --- a/common/commands/src/context/errors.rs +++ b/common/commands/src/context/errors.rs @@ -15,4 +15,10 @@ pub enum ContextError { // TODO: improve this to return known errors #[error("failed to create client - {0}")] NymdError(String), + + #[error("{0}")] + NymdErrorPassthrough(#[from] validator_client::nymd::error::NymdError), + + #[error("{0}")] + ValidatorClientError(#[from] validator_client::ValidatorClientError), } diff --git a/common/commands/src/context/mod.rs b/common/commands/src/context/mod.rs index 856a03e62a..4f55aa1847 100644 --- a/common/commands/src/context/mod.rs +++ b/common/commands/src/context/mod.rs @@ -6,6 +6,7 @@ use network_defaults::{ var_names::{API_VALIDATOR, MIXNET_CONTRACT_ADDRESS, NYMD_VALIDATOR, VESTING_CONTRACT_ADDRESS}, NymNetworkDetails, }; +use tap::prelude::*; use validator_client::nymd::{self, AccountId, NymdClient, QueryNymdClient, SigningNymdClient}; pub use validator_client::validator_api::Client as ValidatorApiClient; @@ -58,7 +59,7 @@ pub fn create_signing_client( network_details: &NymNetworkDetails, ) -> Result { let client_config = nymd::Config::try_from_nym_network_details(network_details) - .expect("failed to construct valid validator client config with the provided network"); + .tap_err(|e| log::error!("Failed to get client config - {:?}", e))?; // get mnemonic let mnemonic = match std::env::var("MNEMONIC") { @@ -87,7 +88,7 @@ pub fn create_query_client( network_details: &NymNetworkDetails, ) -> Result { let client_config = nymd::Config::try_from_nym_network_details(network_details) - .expect("failed to construct valid validator client config with the provided network"); + .tap_err(|e| log::error!("Failed to get client config - {:?}", e))?; let nymd_url = network_details .endpoints @@ -107,7 +108,7 @@ pub fn create_signing_client_with_validator_api( network_details: &NymNetworkDetails, ) -> Result { let client_config = validator_client::Config::try_from_nym_network_details(network_details) - .expect("failed to construct valid validator client config with the provided network"); + .tap_err(|e| log::error!("Failed to get client config - {:?}", e))?; // get mnemonic let mnemonic = match std::env::var("MNEMONIC") { @@ -129,7 +130,7 @@ pub fn create_query_client_with_validator_api( network_details: &NymNetworkDetails, ) -> Result { let client_config = validator_client::Config::try_from_nym_network_details(network_details) - .expect("failed to construct valid validator client config with the provided network"); + .tap_err(|e| log::error!("Failed to get client config - {:?}", e))?; match validator_client::client::Client::new_query(client_config) { Ok(client) => Ok(client), diff --git a/common/commands/src/validator/signature/sign.rs b/common/commands/src/validator/signature/sign.rs index d26302aeba..cd4c169ae1 100644 --- a/common/commands/src/validator/signature/sign.rs +++ b/common/commands/src/validator/signature/sign.rs @@ -13,7 +13,7 @@ use validator_client::nymd::wallet::DirectSecp256k1HdWallet; pub struct SignatureOutputJson { pub account_id: String, pub public_key: PublicKey, - pub signature: String, + pub signature_as_hex: String, } #[derive(Debug, Parser)] @@ -46,7 +46,7 @@ pub fn sign(args: Args, prefix: &str, mnemonic: Option) { let output = SignatureOutputJson { account_id: account.address().to_string(), public_key: account.public_key(), - signature: signature.to_string(), + signature_as_hex: signature.to_string(), }; println!("{}", json!(output)); } diff --git a/common/commands/src/validator/vesting/balance.rs b/common/commands/src/validator/vesting/balance.rs index 45b2f7866d..0dff560b9f 100644 --- a/common/commands/src/validator/vesting/balance.rs +++ b/common/commands/src/validator/vesting/balance.rs @@ -3,11 +3,11 @@ use clap::Parser; use cosmrs::AccountId; -use log::info; +use log::{error, info}; use validator_client::nymd::{Coin, VestingQueryClient}; -use crate::context::SigningClient; +use crate::context::QueryClient; use crate::utils::show_error; use crate::utils::{pretty_coin, pretty_cosmwasm_coin}; @@ -18,8 +18,16 @@ pub struct Args { pub address: Option, } -pub async fn balance(args: Args, client: SigningClient) { - let account_id = args.address.unwrap_or_else(|| client.address().clone()); +pub async fn balance(args: Args, client: QueryClient, address_from_mnemonic: Option) { + if args.address.is_none() && address_from_mnemonic.is_none() { + error!("Please specify an account address or a mnemonic to get the balance for"); + return; + } + + let account_id = args + .address + .unwrap_or_else(|| address_from_mnemonic.expect("please provide a mnemonic")); + let vesting_address = account_id.to_string(); let denom = client.current_chain_details().mix_denom.base.as_str(); diff --git a/common/commands/src/validator/vesting/query_vesting_schedule.rs b/common/commands/src/validator/vesting/query_vesting_schedule.rs index 23118c1bf5..10a4dc41d3 100644 --- a/common/commands/src/validator/vesting/query_vesting_schedule.rs +++ b/common/commands/src/validator/vesting/query_vesting_schedule.rs @@ -4,11 +4,11 @@ use clap::Parser; use cosmrs::AccountId; use cosmwasm_std::Coin as CosmWasmCoin; -use log::info; +use log::{error, info}; use validator_client::nymd::{Coin, VestingQueryClient}; -use crate::context::SigningClient; +use crate::context::QueryClient; use crate::utils::show_error; use crate::utils::{pretty_coin, pretty_cosmwasm_coin}; @@ -19,8 +19,18 @@ pub struct Args { pub address: Option, } -pub async fn query(args: Args, client: SigningClient) { - let account_id = args.address.unwrap_or_else(|| client.address().clone()); +pub async fn query(args: Args, client: QueryClient, address_from_mnemonic: Option) { + if args.address.is_none() && address_from_mnemonic.is_none() { + error!("Please specify an account address or a mnemonic to get the balance for"); + return; + } + + let account_id = args + .address + .unwrap_or_else(|| address_from_mnemonic.expect("please provide a mnemonic")); + + info!("Checking account {} for a vesting schedule...", account_id); + let vesting_address = account_id.to_string(); let denom = client.current_chain_details().mix_denom.base.as_str(); diff --git a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/event_attributes.rs b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/event_attributes.rs new file mode 100644 index 0000000000..80f5daf68a --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/event_attributes.rs @@ -0,0 +1,4 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub const BANDWIDTH_PROPOSAL_ID: &str = "proposal_id"; diff --git a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/lib.rs b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/lib.rs index 34b5556745..56cb54e81d 100644 --- a/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/lib.rs +++ b/common/cosmwasm-smart-contracts/coconut-bandwidth-contract/src/lib.rs @@ -1,4 +1,5 @@ pub mod deposit; +pub mod event_attributes; pub mod events; pub mod msg; pub mod spend_credential; diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/Cargo.toml b/common/cosmwasm-smart-contracts/coconut-dkg/Cargo.toml new file mode 100644 index 0000000000..cffca1ac3d --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-dkg/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "coconut-dkg-common" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +cosmwasm-std = "1.0.0" +cw-utils = "0.13.4" +schemars = "0.8" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } + +contracts-common = { path = "../contracts-common" } +multisig-contract-common = { path = "../multisig-contract" } \ No newline at end of file diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs new file mode 100644 index 0000000000..7b24be4869 --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/dealer.rs @@ -0,0 +1,102 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::{ContractSafeBytes, EncodedBTEPublicKeyWithProof, NodeIndex}; +use cosmwasm_std::Addr; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] +#[serde(rename_all = "snake_case")] +pub struct DealerDetails { + pub address: Addr, + pub bte_public_key_with_proof: EncodedBTEPublicKeyWithProof, + pub announce_address: String, + pub assigned_index: NodeIndex, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum DealerType { + Current, + Past, + Unknown, +} + +impl DealerType { + pub fn is_current(&self) -> bool { + matches!(&self, DealerType::Current) + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct DealerDetailsResponse { + pub details: Option, + pub dealer_type: DealerType, +} + +impl DealerDetailsResponse { + pub fn new(details: Option, dealer_type: DealerType) -> Self { + DealerDetailsResponse { + details, + dealer_type, + } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct PagedDealerResponse { + pub dealers: Vec, + pub per_page: usize, + pub start_next_after: Option, +} + +impl PagedDealerResponse { + pub fn new( + dealers: Vec, + per_page: usize, + start_next_after: Option, + ) -> Self { + PagedDealerResponse { + dealers, + per_page, + start_next_after, + } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct ContractDealing { + pub dealing: ContractSafeBytes, + pub dealer: Addr, +} + +impl ContractDealing { + pub fn new(dealing: ContractSafeBytes, dealer: Addr) -> Self { + ContractDealing { dealing, dealer } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct PagedDealingsResponse { + pub dealings: Vec, + pub per_page: usize, + pub start_next_after: Option, +} + +impl PagedDealingsResponse { + pub fn new( + dealings: Vec, + per_page: usize, + start_next_after: Option, + ) -> Self { + PagedDealingsResponse { + dealings, + per_page, + start_next_after, + } + } +} diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/event_attributes.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/event_attributes.rs new file mode 100644 index 0000000000..28166677af --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/event_attributes.rs @@ -0,0 +1,5 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub const NODE_INDEX: &str = "node_index"; +pub const DKG_PROPOSAL_ID: &str = "proposal_id"; diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/lib.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/lib.rs new file mode 100644 index 0000000000..545451eeb6 --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/lib.rs @@ -0,0 +1,5 @@ +pub mod dealer; +pub mod event_attributes; +pub mod msg; +pub mod types; +pub mod verification_key; diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs new file mode 100644 index 0000000000..357e56b968 --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/msg.rs @@ -0,0 +1,69 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::types::{ContractSafeBytes, EncodedBTEPublicKeyWithProof}; +use crate::verification_key::VerificationKeyShare; +use cosmwasm_std::Addr; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +pub struct InstantiateMsg { + pub group_addr: String, + pub multisig_addr: String, + pub admin: String, + pub mix_denom: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ExecuteMsg { + RegisterDealer { + bte_key_with_proof: EncodedBTEPublicKeyWithProof, + announce_address: String, + }, + + CommitDealing { + dealing_bytes: ContractSafeBytes, + }, + + CommitVerificationKeyShare { + share: VerificationKeyShare, + }, + + VerifyVerificationKeyShare { + owner: Addr, + }, + + AdvanceEpochState {}, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum QueryMsg { + GetCurrentEpochState {}, + GetDealerDetails { + dealer_address: String, + }, + GetCurrentDealers { + limit: Option, + start_after: Option, + }, + GetPastDealers { + limit: Option, + start_after: Option, + }, + GetDealing { + idx: u64, + limit: Option, + start_after: Option, + }, + GetVerificationKeys { + limit: Option, + start_after: Option, + }, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub struct MigrateMsg {} diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs new file mode 100644 index 0000000000..ffb1653919 --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/types.rs @@ -0,0 +1,81 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use serde::{Deserialize, Serialize}; +use std::fmt::{Display, Formatter}; + +pub use crate::dealer::{DealerDetails, PagedDealerResponse}; +pub use contracts_common::dealings::ContractSafeBytes; +pub use cosmwasm_std::{Addr, Coin}; + +pub type EncodedBTEPublicKeyWithProof = String; +pub type EncodedBTEPublicKeyWithProofRef<'a> = &'a str; +pub type NodeIndex = u64; + +// 2 public attributes, 2 private attributes, 1 fixed for coconut credential +pub const TOTAL_DEALINGS: usize = 2 + 2 + 1; + +// currently (it is still extremely likely to change, we might be able to get rid of verification key-related complaints), +// the epoch can be in the following states (in order): +// 1. PublicKeySubmission -> potential dealers are submitting their BTE and ed25519 public keys to participate in dealing exchange +// 2. DealingExchange -> the actual (off-chain) dealing exchange is happening +// 3. ComplaintSubmission -> receivers submitting evidence of other dealers sending malformed data +// 4. ComplaintVoting -> (if any complaints were submitted) receivers voting on the validity of the evidence provided +// 5. VerificationKeySubmission -> receivers submitting their partial (and master) verification keys +// 6. VerificationKeyMismatchSubmission -> receivers / watchers raising issue that the submitted VK are mismatched with their local derivations +// 7. VerificationKeyMismatchVoting -> (if any complaints were submitted) receivers voting on received mismatches +// 8. InProgress -> all receivers have all their secrets derived and all is good +// +// Note: It's important that the variant ordering is not changed otherwise it would mess up the derived `PartialOrd` +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd)] +#[serde(rename_all = "snake_case")] +pub enum EpochState { + PublicKeySubmission, + DealingExchange, + VerificationKeySubmission, + VerificationKeyValidation, + VerificationKeyFinalization, + InProgress, +} + +impl Default for EpochState { + fn default() -> Self { + Self::PublicKeySubmission + } +} + +impl Display for EpochState { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + EpochState::PublicKeySubmission => write!(f, "PublicKeySubmission"), + EpochState::DealingExchange => write!(f, "DealingExchange"), + EpochState::VerificationKeySubmission => write!(f, "VerificationKeySubmission"), + EpochState::VerificationKeyValidation => write!(f, "VerificationKeyValidation"), + EpochState::VerificationKeyFinalization => write!(f, "VerificationKeyFinalization"), + EpochState::InProgress => write!(f, "InProgress"), + } + } +} + +impl EpochState { + pub fn next(self) -> Option { + match self { + EpochState::PublicKeySubmission => Some(EpochState::DealingExchange), + EpochState::DealingExchange => Some(EpochState::VerificationKeySubmission), + EpochState::VerificationKeySubmission => Some(EpochState::VerificationKeyValidation), + EpochState::VerificationKeyValidation => Some(EpochState::VerificationKeyFinalization), + EpochState::VerificationKeyFinalization => Some(EpochState::InProgress), + EpochState::InProgress => None, + } + } + + pub fn all_until(&self, end: Self) -> Vec { + let mut states = vec![*self]; + while states.last().unwrap() != &end { + let next_state = states.last().unwrap().next().expect("somehow reached the end of state diff -> this should be impossible under any circumstances!"); + states.push(next_state); + } + + states + } +} diff --git a/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs b/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs new file mode 100644 index 0000000000..cfc3ffae03 --- /dev/null +++ b/common/cosmwasm-smart-contracts/coconut-dkg/src/verification_key.rs @@ -0,0 +1,70 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::msg::ExecuteMsg; +use crate::types::NodeIndex; +use cosmwasm_std::{from_binary, to_binary, Addr, CosmosMsg, StdResult, Timestamp, WasmMsg}; +use cw_utils::Expiration; +use multisig_contract_common::msg::ExecuteMsg as MultisigExecuteMsg; +use serde::{Deserialize, Serialize}; + +pub type VerificationKeyShare = String; + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub struct ContractVKShare { + pub share: VerificationKeyShare, + pub announce_address: String, + pub node_index: NodeIndex, + pub owner: Addr, + pub verified: bool, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub struct PagedVKSharesResponse { + pub shares: Vec, + pub per_page: usize, + pub start_next_after: Option, +} + +pub fn to_cosmos_msg( + owner: Addr, + coconut_dkg_addr: String, + multisig_addr: String, + expiration_time: Timestamp, +) -> StdResult { + let verify_vk_share_req = ExecuteMsg::VerifyVerificationKeyShare { owner }; + let verify_vk_share_msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: coconut_dkg_addr, + msg: to_binary(&verify_vk_share_req)?, + funds: vec![], + }); + let req = MultisigExecuteMsg::Propose { + title: String::from("Verify VK share, as ordered by Coconut DKG Contract"), + description: String::new(), + msgs: vec![verify_vk_share_msg], + latest: Some(Expiration::AtTime(expiration_time)), + }; + let msg = CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: multisig_addr, + msg: to_binary(&req)?, + funds: vec![], + }); + + Ok(msg) +} + +pub fn owner_from_cosmos_msgs(msgs: &[CosmosMsg]) -> Option { + if let Some(CosmosMsg::Wasm(WasmMsg::Execute { + contract_addr: _, + msg, + funds: _, + })) = msgs.get(0) + { + if let Ok(ExecuteMsg::VerifyVerificationKeyShare { owner }) = from_binary::(msg) + { + return Some(owner); + } + } + None +} diff --git a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml index 4f98451100..351830cfe3 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml +++ b/common/cosmwasm-smart-contracts/contracts-common/Cargo.toml @@ -7,10 +7,15 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +bs58 = "0.4.0" cosmwasm-std = "1.0.0" -serde = { version = "1.0", features = ["derive"] } +dkg = { path = "../../../common/crypto/dkg", optional = true } schemars = "0.8" +serde = { version = "1.0", features = ["derive"] } thiserror = "1" [dev-dependencies] serde_json = "1.0.0" + +[features] +coconut = ["dkg"] \ No newline at end of file diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs b/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs new file mode 100644 index 0000000000..8beedd4a42 --- /dev/null +++ b/common/cosmwasm-smart-contracts/contracts-common/src/dealings.rs @@ -0,0 +1,87 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +#[cfg(feature = "coconut")] +use dkg::{error::DkgError, Dealing}; +use schemars::JsonSchema; +use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use std::fmt::{Display, Formatter}; +use std::ops::Deref; + +// some sane upper-bound size on byte sizes +// currently set to 128 bytes +pub const MAX_DISPLAY_SIZE: usize = 128; + +// TODO: if we are to use this for different types, it might make sense to introduce something like +// CommitmentTypeId field on the below for distinguishing different ones. it would somehow become part of the trait +#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, JsonSchema)] +pub struct ContractSafeBytes(Vec); + +impl Deref for ContractSafeBytes { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for ContractSafeBytes { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + if !self.0.is_empty() { + write!(f, "0x")?; + } + for byte in self.0.iter().take(MAX_DISPLAY_SIZE) { + write!(f, "{:02X}", byte)?; + } + // just some sanity safeguards + if self.0.len() > MAX_DISPLAY_SIZE { + write!(f, "...")?; + } + Ok(()) + } +} + +// since cosmwasm stores everything with byte representation of stringified json, it's actually more efficient +// to serialize this as a string as opposed to keeping it as vector of bytes. +// for example vec![255,255] would have string representation of "[255,255]" and will be serialized to +// [91, 50, 53, 53, 44, 50, 53, 53, 93]. the equivalent base58 encoded string `"LUv"` will be serialized to +// [34, 76, 85, 118, 34] +// +// the difference between base58 and base64 is rather minimal and I've gone with base58 for consistency sake +impl Serialize for ContractSafeBytes { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&bs58::encode(&self.0).into_string()) + } +} + +impl<'de> Deserialize<'de> for ContractSafeBytes { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = ::deserialize(deserializer)?; + let bytes = bs58::decode(&s) + .into_vec() + .map_err(serde::de::Error::custom)?; + Ok(ContractSafeBytes(bytes)) + } +} + +#[cfg(feature = "coconut")] +impl<'a> From<&'a Dealing> for ContractSafeBytes { + fn from(dealing: &'a Dealing) -> Self { + ContractSafeBytes(dealing.to_bytes()) + } +} + +#[cfg(feature = "coconut")] +impl<'a> TryFrom<&'a ContractSafeBytes> for Dealing { + type Error = DkgError; + + fn try_from(value: &'a ContractSafeBytes) -> Result { + Dealing::try_from_bytes(&value.0) + } +} diff --git a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs index f4d3906558..4c351a0ccf 100644 --- a/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs +++ b/common/cosmwasm-smart-contracts/contracts-common/src/lib.rs @@ -4,6 +4,7 @@ #![warn(clippy::expect_used)] #![warn(clippy::unwrap_used)] +pub mod dealings; pub mod events; pub mod types; diff --git a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs index c6fb28063e..1c00c64a9c 100644 --- a/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/mixnet-contract/src/msg.rs @@ -413,4 +413,6 @@ pub enum QueryMsg { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "snake_case")] -pub struct MigrateMsg {} +pub struct MigrateMsg { + pub vesting_contract_address: Option, +} diff --git a/common/cosmwasm-smart-contracts/multisig-contract/Cargo.toml b/common/cosmwasm-smart-contracts/multisig-contract/Cargo.toml index cb3c2c0cb1..f493cb6635 100644 --- a/common/cosmwasm-smart-contracts/multisig-contract/Cargo.toml +++ b/common/cosmwasm-smart-contracts/multisig-contract/Cargo.toml @@ -6,9 +6,10 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -cw-utils = { version = "0.13.1" } -cw3 = { version = "0.13.1" } -cw4 = { version = "0.13.1" } +cw-utils = { version = "0.13.4" } +cw3 = { version = "0.13.4" } +cw3-fixed-multisig = { version = "0.13.4", features = ["library"] } +cw4 = { version = "0.13.4" } cosmwasm-std = "1.0.0" schemars = "0.8" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/common/cosmwasm-smart-contracts/multisig-contract/src/msg.rs b/common/cosmwasm-smart-contracts/multisig-contract/src/msg.rs index 93fd9f4839..db37835f64 100644 --- a/common/cosmwasm-smart-contracts/multisig-contract/src/msg.rs +++ b/common/cosmwasm-smart-contracts/multisig-contract/src/msg.rs @@ -5,7 +5,6 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use cosmwasm_std::{CosmosMsg, Empty}; -pub use cw3::ProposalResponse; use cw3::Vote; use cw4::MemberChangedHookMsg; use cw_utils::{Duration, Expiration, Threshold}; @@ -15,6 +14,7 @@ pub struct InstantiateMsg { // this is the group contract that contains the member list pub group_addr: String, pub coconut_bandwidth_contract_address: String, + pub coconut_dkg_contract_address: String, pub threshold: Threshold, pub max_voting_period: Duration, } @@ -82,4 +82,5 @@ pub enum QueryMsg { #[serde(rename_all = "snake_case")] pub struct MigrateMsg { pub coconut_bandwidth_address: String, + pub coconut_dkg_address: String, } diff --git a/common/credentials/Cargo.toml b/common/credentials/Cargo.toml index 4ff6b60d2b..58d43af03f 100644 --- a/common/credentials/Cargo.toml +++ b/common/credentials/Cargo.toml @@ -9,7 +9,6 @@ edition = "2021" bls12_381 = { version = "0.5", default-features = false, features = ["pairings", "alloc", "experimental"] } cosmrs = { git = "https://github.com/neacsu/cosmos-rust", branch = "neacsu/feegrant_support", optional = true } thiserror = "1.0" -url = "2.2" # I guess temporarily until we get serde support in coconut up and running coconut-interface = { path = "../coconut-interface" } diff --git a/common/credentials/src/coconut/utils.rs b/common/credentials/src/coconut/utils.rs index 537ea801ff..f701938d59 100644 --- a/common/credentials/src/coconut/utils.rs +++ b/common/credentials/src/coconut/utils.rs @@ -8,8 +8,8 @@ use coconut_interface::{ use crypto::asymmetric::encryption::PublicKey; use crypto::shared_key::recompute_shared_key; use crypto::symmetric::stream_cipher; -use url::Url; use validator_api_requests::coconut::BlindSignRequestBody; +use validator_client::client::CoconutApiClient; use crate::coconut::bandwidth::{BandwidthVoucher, PRIVATE_ATTRIBUTES, PUBLIC_ATTRIBUTES}; use crate::coconut::params::{ @@ -17,46 +17,21 @@ use crate::coconut::params::{ }; use crate::error::Error; -/// Contacts all provided validators and then aggregate their verification keys. -/// -/// # Arguments -/// -/// * `validators`: list of validators to obtain verification keys from. -/// -/// Note: list of validators must be correctly ordered by the polynomial coordinates used -/// during key generation and it is responsibility of the caller to ensure that correct -/// number of them is provided -/// -/// # Examples -/// -/// ```no_run -/// use url::{Url, ParseError}; -/// use credentials::obtain_aggregate_verification_key; -/// -/// async fn example() -> Result<(), ParseError> { -/// let validators = vec!["https://sandbox-validator1.nymtech.net/api".parse()?, "https://sandbox-validator2.nymtech.net/api".parse()?]; -/// let aggregated_key = obtain_aggregate_verification_key(&validators).await; -/// // deal with the obtained Result -/// Ok(()) -/// } -/// ``` pub async fn obtain_aggregate_verification_key( - validators: &[Url], + api_clients: &[CoconutApiClient], ) -> Result { - if validators.is_empty() { + if api_clients.is_empty() { return Err(Error::NoValidatorsAvailable); } - let mut indices = Vec::with_capacity(validators.len()); - let mut shares = Vec::with_capacity(validators.len()); - - let mut client = validator_client::ApiClient::new(validators[0].clone()); - for (id, validator_url) in validators.iter().enumerate() { - client.change_validator_api(validator_url.clone()); - let response = client.get_coconut_verification_key().await?; - indices.push((id + 1) as u64); - shares.push(response.key); - } + let indices: Vec<_> = api_clients + .iter() + .map(|api_client| api_client.node_id) + .collect(); + let shares: Vec<_> = api_clients + .iter() + .map(|api_client| api_client.verification_key.clone()) + .collect(); Ok(aggregate_verification_keys(&shares, Some(&indices))?) } @@ -64,7 +39,7 @@ pub async fn obtain_aggregate_verification_key( async fn obtain_partial_credential( params: &Parameters, attributes: &BandwidthVoucher, - client: &validator_client::ApiClient, + client: &validator_client::client::ApiClient, validator_vk: &VerificationKey, ) -> Result { let public_attributes = attributes.get_public_attributes(); @@ -118,26 +93,33 @@ async fn obtain_partial_credential( pub async fn obtain_aggregate_signature( params: &Parameters, attributes: &BandwidthVoucher, - validators: &[Url], + coconut_api_clients: &[CoconutApiClient], ) -> Result { - if validators.is_empty() { + if coconut_api_clients.is_empty() { return Err(Error::NoValidatorsAvailable); } let public_attributes = attributes.get_public_attributes(); let private_attributes = attributes.get_private_attributes(); - let mut shares = Vec::with_capacity(validators.len()); - let mut validators_partial_vks: Vec = Vec::with_capacity(validators.len()); + let mut shares = Vec::with_capacity(coconut_api_clients.len()); + let validators_partial_vks: Vec<_> = coconut_api_clients + .iter() + .map(|api_client| api_client.verification_key.clone()) + .collect(); + let indices: Vec<_> = coconut_api_clients + .iter() + .map(|api_client| api_client.node_id) + .collect(); - let mut client = validator_client::ApiClient::new(validators[0].clone()); - for (id, validator_url) in validators.iter().enumerate() { - client.change_validator_api(validator_url.clone()); - let validator_partial_vk = client.get_coconut_verification_key().await?; - validators_partial_vks.push(validator_partial_vk.key.clone()); - let signature = - obtain_partial_credential(params, attributes, &client, &validator_partial_vk.key) - .await?; - let share = SignatureShare::new(signature, (id + 1) as u64); + for coconut_api_client in coconut_api_clients.iter() { + let signature = obtain_partial_credential( + params, + attributes, + &coconut_api_client.api_client, + &coconut_api_client.verification_key, + ) + .await?; + let share = SignatureShare::new(signature, coconut_api_client.node_id); shares.push(share) } @@ -145,10 +127,6 @@ pub async fn obtain_aggregate_signature( attributes.extend_from_slice(&private_attributes); attributes.extend_from_slice(&public_attributes); - let mut indices: Vec = Vec::with_capacity(validators_partial_vks.len()); - for i in 0..validators_partial_vks.len() { - indices.push((i + 1) as u64); - } let verification_key = aggregate_verification_keys(&validators_partial_vks, Some(indices.as_ref()))?; diff --git a/common/crypto/dkg/Cargo.toml b/common/crypto/dkg/Cargo.toml index c6e3edefd4..ea20508321 100644 --- a/common/crypto/dkg/Cargo.toml +++ b/common/crypto/dkg/Cargo.toml @@ -25,6 +25,8 @@ serde_derive = "1.0" thiserror = "1.0" zeroize = { version = "1.4", features = ["zeroize_derive"] } +pemstore = { path = "../../pemstore" } + [dependencies.group] version = "0.11" default-features = false diff --git a/common/crypto/dkg/benches/benchmarks.rs b/common/crypto/dkg/benches/benchmarks.rs index 4f226144ab..7b85d70356 100644 --- a/common/crypto/dkg/benches/benchmarks.rs +++ b/common/crypto/dkg/benches/benchmarks.rs @@ -9,7 +9,7 @@ use dkg::bte::proof_discrete_log::ProofOfDiscreteLog; use dkg::bte::proof_sharing::ProofOfSecretSharing; use dkg::bte::{ decrypt_share, encrypt_shares, keygen, proof_chunking, proof_sharing, setup, DecryptionKey, - Epoch, PublicKey, + PublicKey, }; use dkg::interpolation::polynomial::Polynomial; use dkg::{Dealing, NodeIndex, Share}; @@ -54,7 +54,6 @@ pub fn creating_dealing_for_3_parties(c: &mut Criterion) { let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); let threshold = 2; - let epoch = Epoch::new(2); let (receivers, _) = prepare_keys(&mut rng, 3); @@ -66,7 +65,6 @@ pub fn creating_dealing_for_3_parties(c: &mut Criterion) { ¶ms, receivers.keys().next().copied().unwrap(), threshold, - epoch, &receivers, None, ) @@ -80,7 +78,6 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); let threshold = 2; - let epoch = Epoch::new(2); let (receivers, mut dks) = prepare_keys(&mut rng, 3); let (dealing, _) = Dealing::create( @@ -88,22 +85,18 @@ pub fn verifying_dealing_made_for_3_parties_and_recovering_share(c: &mut Criteri ¶ms, receivers.keys().next().copied().unwrap(), threshold, - epoch, &receivers, None, ); let first_key = dks.get_mut(0).unwrap(); - first_key.try_update_to(epoch, ¶ms, &mut rng).unwrap(); c.bench_function( "verifying single dealing made for 3 parties (threshold 2) and recovering share", |b| { b.iter(|| { - assert!(dealing - .verify(¶ms, epoch, threshold, &receivers, None) - .is_ok()); - black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap()); + assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok()); + black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap()); }) }, ); @@ -114,7 +107,6 @@ pub fn creating_dealing_for_20_parties(c: &mut Criterion) { let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); let threshold = 14; - let epoch = Epoch::new(2); let (receivers, _) = prepare_keys(&mut rng, 20); @@ -128,7 +120,6 @@ pub fn creating_dealing_for_20_parties(c: &mut Criterion) { ¶ms, receivers.keys().next().copied().unwrap(), threshold, - epoch, &receivers, None, ) @@ -143,7 +134,6 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); let threshold = 14; - let epoch = Epoch::new(2); let (receivers, mut dks) = prepare_keys(&mut rng, 20); let (dealing, _) = Dealing::create( @@ -151,22 +141,18 @@ pub fn verifying_dealing_made_for_20_parties_and_recovering_share(c: &mut Criter ¶ms, receivers.keys().next().copied().unwrap(), threshold, - epoch, &receivers, None, ); let first_key = dks.get_mut(0).unwrap(); - first_key.try_update_to(epoch, ¶ms, &mut rng).unwrap(); c.bench_function( "verifying single dealing made for 20 parties (threshold 14) and recovering share", |b| { b.iter(|| { - assert!(dealing - .verify(¶ms, epoch, threshold, &receivers, None) - .is_ok()); - black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap()); + assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok()); + black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap()); }) }, ); @@ -177,7 +163,6 @@ pub fn creating_dealing_for_100_parties(c: &mut Criterion) { let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); let threshold = 67; - let epoch = Epoch::new(2); let (receivers, _) = prepare_keys(&mut rng, 100); @@ -191,7 +176,6 @@ pub fn creating_dealing_for_100_parties(c: &mut Criterion) { ¶ms, receivers.keys().next().copied().unwrap(), threshold, - epoch, &receivers, None, ) @@ -206,7 +190,6 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); let threshold = 67; - let epoch = Epoch::new(2); let (receivers, mut dks) = prepare_keys(&mut rng, 100); let (dealing, _) = Dealing::create( @@ -214,22 +197,18 @@ pub fn verifying_dealing_made_for_100_parties_and_recovering_share(c: &mut Crite ¶ms, receivers.keys().next().copied().unwrap(), threshold, - epoch, &receivers, None, ); let first_key = dks.get_mut(0).unwrap(); - first_key.try_update_to(epoch, ¶ms, &mut rng).unwrap(); c.bench_function( "verifying single dealing made for 100 parties (threshold 67) and recovering share", |b| { b.iter(|| { - assert!(dealing - .verify(¶ms, epoch, threshold, &receivers, None) - .is_ok()); - black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, epoch, None).unwrap()); + assert!(dealing.verify(¶ms, threshold, &receivers, None).is_ok()); + black_box(decrypt_share(first_key, 0, &dealing.ciphertexts, None).unwrap()); }) }, ); @@ -266,7 +245,6 @@ pub fn creating_proof_of_chunking_for_100_parties(c: &mut Criterion) { let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); - let epoch = Epoch::new(2); let (receivers, _) = prepare_keys(&mut rng, 100); @@ -283,7 +261,7 @@ pub fn creating_proof_of_chunking_for_100_parties(c: &mut Criterion) { .collect::>(); let ordered_public_keys = receivers.values().copied().collect::>(); - let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, ¶ms, &mut rng); + let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng); c.bench_function("creating proof of chunking for 100 parties", |b| { b.iter(|| { @@ -301,7 +279,6 @@ pub fn verifying_proof_of_chunking_for_100_parties(c: &mut Criterion) { let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); - let epoch = Epoch::new(2); let (receivers, _) = prepare_keys(&mut rng, 100); @@ -318,7 +295,7 @@ pub fn verifying_proof_of_chunking_for_100_parties(c: &mut Criterion) { .collect::>(); let ordered_public_keys = receivers.values().copied().collect::>(); - let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, ¶ms, &mut rng); + let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng); let chunking_instance = proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts); let proof_of_chunking = @@ -338,7 +315,6 @@ pub fn creating_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) { let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); - let epoch = Epoch::new(2); let (receivers, _) = prepare_keys(&mut rng, 100); @@ -354,7 +330,7 @@ pub fn creating_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) { .map(|(share, key)| (share, key)) .collect::>(); - let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, ¶ms, &mut rng); + let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng); let combined_ciphertexts = ciphertexts.combine_ciphertexts(); let combined_r = hazmat.combine_rs(); @@ -381,7 +357,6 @@ pub fn verifying_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) { let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); - let epoch = Epoch::new(2); let (receivers, _) = prepare_keys(&mut rng, 100); @@ -397,7 +372,7 @@ pub fn verifying_proof_of_secret_sharing_for_100_parties(c: &mut Criterion) { .map(|(share, key)| (share, key)) .collect::>(); - let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, epoch, ¶ms, &mut rng); + let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng); let combined_ciphertexts = ciphertexts.combine_ciphertexts(); let combined_r = hazmat.combine_rs(); @@ -430,7 +405,6 @@ pub fn single_share_encryption(c: &mut Criterion) { let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); - let epoch = Epoch::new(2); let (_, pk) = keygen(¶ms, &mut rng); let polynomial = Polynomial::new_random(&mut rng, 3); @@ -440,7 +414,6 @@ pub fn single_share_encryption(c: &mut Criterion) { b.iter(|| { black_box(encrypt_shares( &[(&share, pk.public_key())], - epoch, ¶ms, &mut rng, )) @@ -452,7 +425,6 @@ pub fn share_encryption_100(c: &mut Criterion) { let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); - let epoch = Epoch::new(2); let (receivers, _) = prepare_keys(&mut rng, 100); let polynomial = Polynomial::new_random(&mut rng, 3); @@ -468,14 +440,7 @@ pub fn share_encryption_100(c: &mut Criterion) { .collect::>(); c.bench_function("100 shares encryption", |b| { - b.iter(|| { - black_box(encrypt_shares( - &remote_share_key_pairs, - epoch, - ¶ms, - &mut rng, - )) - }) + b.iter(|| black_box(encrypt_shares(&remote_share_key_pairs, ¶ms, &mut rng))) }); } @@ -483,16 +448,14 @@ pub fn share_decryption(c: &mut Criterion) { let dummy_seed = [42u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); - let epoch = Epoch::new(2); - let (mut dk, pk) = keygen(¶ms, &mut rng); + let (dk, pk) = keygen(¶ms, &mut rng); let polynomial = Polynomial::new_random(&mut rng, 3); let share: Share = polynomial.evaluate_at(&Scalar::from(42)).into(); - let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], epoch, ¶ms, &mut rng); - dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); + let (ciphertexts, _) = encrypt_shares(&[(&share, pk.public_key())], ¶ms, &mut rng); c.bench_function("single share decryption", |b| { - b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, epoch, None))) + b.iter(|| black_box(decrypt_share(&dk, 0, &ciphertexts, None))) }); } diff --git a/common/crypto/dkg/src/bte/encryption.rs b/common/crypto/dkg/src/bte/encryption.rs index 7d7f4d4819..22fd509677 100644 --- a/common/crypto/dkg/src/bte/encryption.rs +++ b/common/crypto/dkg/src/bte/encryption.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::bte::keys::{DecryptionKey, PublicKey}; -use crate::bte::{Epoch, Params, CHUNK_SIZE, G2_GENERATOR_PREPARED, NUM_CHUNKS, PAIRING_BASE}; +use crate::bte::{evaluate_f, Params, CHUNK_SIZE, G2_GENERATOR_PREPARED, NUM_CHUNKS, PAIRING_BASE}; use crate::error::DkgError; use crate::utils::{combine_g1_chunks, combine_scalar_chunks, deserialize_g1, deserialize_g2}; use crate::{Chunk, ChunkedShare, Share}; @@ -24,7 +24,7 @@ pub struct Ciphertexts { } impl Ciphertexts { - pub fn verify_integrity(&self, params: &Params, epoch: Epoch) -> bool { + pub fn verify_integrity(&self, params: &Params) -> bool { // if this checks fails it means the ciphertext is undefined as values // in `r`, `s` and `z` are meaningless since technically this ciphertext // has been created for 0 parties @@ -33,9 +33,7 @@ impl Ciphertexts { } let g1_neg = G1Affine::generator().neg(); - let f = epoch - .as_extended_tau(&self.rr, &self.ss, &self.ciphertext_chunks) - .evaluate_f(params); + let f = evaluate_f(params); // we have to use `f` in up to `NUM_CHUNKS` pairings (if everything is valid), // so perform some precomputation on it @@ -192,7 +190,6 @@ impl HazmatRandomness { pub fn encrypt_shares( shares: &[(&Share, &PublicKey)], - epoch: Epoch, params: &Params, mut rng: impl RngCore, ) -> (Ciphertexts, HazmatRandomness) { @@ -242,7 +239,7 @@ pub fn encrypt_shares( let rr = rr.try_into().unwrap(); let ss = ss.try_into().unwrap(); - let f = epoch.as_extended_tau(&rr, &ss, &cc).evaluate_f(params); + let f = evaluate_f(params); let mut zz = Vec::with_capacity(NUM_CHUNKS); for i in 0..NUM_CHUNKS { @@ -269,35 +266,22 @@ pub fn decrypt_share( // in the case of multiple receivers, specifies which index of ciphertext chunks should be used i: usize, ciphertext: &Ciphertexts, - epoch: Epoch, lookup_table: Option<&BabyStepGiantStepLookup>, ) -> Result { let mut plaintext = ChunkedShare::default(); - let decryption_node = dk.try_get_compatible_node(epoch)?; - let extended_tau = epoch.as_extended_tau( - &ciphertext.rr, - &ciphertext.ss, - &ciphertext.ciphertext_chunks, - ); - if i >= ciphertext.ciphertext_chunks.len() { return Err(DkgError::UnavailableCiphertext(i)); } - let height = decryption_node.tau.height(); - let b_neg = decryption_node - .ds + let b_neg = dk + .dh .iter() - .chain(decryption_node.dh.iter()) - .zip(extended_tau.0.iter().by_vals().skip(height)) - .filter(|(_, i)| *i) - .map(|(d_i, _)| d_i) - .fold(decryption_node.b, |acc, d_i| acc + d_i) + .fold(dk.b, |acc, d_i| acc + d_i) .neg() .to_affine(); - let e_neg = decryption_node.e.neg().to_affine(); + let e_neg = dk.e.neg().to_affine(); for j in 0..NUM_CHUNKS { let rr_j = &ciphertext.rr[j]; @@ -308,7 +292,7 @@ pub fn decrypt_share( let miller = bls12_381::multi_miller_loop(&[ (&cc_ij.to_affine(), &G2_GENERATOR_PREPARED), (&rr_j.to_affine(), &G2Prepared::from(b_neg)), - (&decryption_node.a.to_affine(), &G2Prepared::from(zz_j)), + (&dk.a.to_affine(), &G2Prepared::from(zz_j)), (&ss_j.to_affine(), &G2Prepared::from(e_neg)), ]); let m = miller.final_exponentiation(); @@ -466,7 +450,6 @@ mod tests { let (decryption_key1, public_key1) = keygen(¶ms, &mut rng); let (decryption_key2, public_key2) = keygen(¶ms, &mut rng); - let epoch = Epoch::new(0); let lookup_table = &DEFAULT_BSGS_TABLE; @@ -475,13 +458,13 @@ mod tests { let m2 = Share::random(&mut rng); let shares = &[(&m1, &public_key1.key), (&m2, &public_key2.key)]; - let (ciphertext, hazmat) = encrypt_shares(shares, epoch, ¶ms, &mut rng); + let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng); verify_hazmat_rand(&ciphertext, &hazmat); let recovered1 = - decrypt_share(&decryption_key1, 0, &ciphertext, epoch, Some(lookup_table)).unwrap(); + decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap(); let recovered2 = - decrypt_share(&decryption_key2, 1, &ciphertext, epoch, Some(lookup_table)).unwrap(); + decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap(); assert_eq!(m1, recovered1); assert_eq!(m2, recovered2); } @@ -494,15 +477,8 @@ mod tests { let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); let params = setup(); - let (mut decryption_key1, public_key1) = keygen(¶ms, &mut rng); - let (mut decryption_key2, public_key2) = keygen(¶ms, &mut rng); - let epoch = Epoch::new(12345); - decryption_key1 - .try_update_to(epoch, ¶ms, &mut rng) - .unwrap(); - decryption_key2 - .try_update_to(epoch, ¶ms, &mut rng) - .unwrap(); + let (decryption_key1, public_key1) = keygen(¶ms, &mut rng); + let (decryption_key2, public_key2) = keygen(¶ms, &mut rng); let lookup_table = &DEFAULT_BSGS_TABLE; @@ -511,121 +487,18 @@ mod tests { let m2 = Share::random(&mut rng); let shares = &[(&m1, &public_key1.key), (&m2, &public_key2.key)]; - let (ciphertext, hazmat) = encrypt_shares(shares, epoch, ¶ms, &mut rng); + let (ciphertext, hazmat) = encrypt_shares(shares, ¶ms, &mut rng); verify_hazmat_rand(&ciphertext, &hazmat); let recovered1 = - decrypt_share(&decryption_key1, 0, &ciphertext, epoch, Some(lookup_table)).unwrap(); + decrypt_share(&decryption_key1, 0, &ciphertext, Some(lookup_table)).unwrap(); let recovered2 = - decrypt_share(&decryption_key2, 1, &ciphertext, epoch, Some(lookup_table)).unwrap(); + decrypt_share(&decryption_key2, 1, &ciphertext, Some(lookup_table)).unwrap(); assert_eq!(m1, recovered1); assert_eq!(m2, recovered2); } } - #[test] - fn decryption_with_root_key() { - let dummy_seed = [42u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - let params = setup(); - - let (root_key, public_key) = keygen(¶ms, &mut rng); - - let share = Share::random(&mut rng); - - let epoch0 = Epoch::new(0); - let epoch42 = Epoch::new(42); - let epoch_big = Epoch::new(3292547435); - - let (ciphertext1, hazmat1) = - encrypt_shares(&[(&share, &public_key.key)], epoch0, ¶ms, &mut rng); - verify_hazmat_rand(&ciphertext1, &hazmat1); - - let (ciphertext2, hazmat2) = - encrypt_shares(&[(&share, &public_key.key)], epoch42, ¶ms, &mut rng); - verify_hazmat_rand(&ciphertext2, &hazmat2); - - let (ciphertext3, hazmat3) = - encrypt_shares(&[(&share, &public_key.key)], epoch_big, ¶ms, &mut rng); - verify_hazmat_rand(&ciphertext3, &hazmat3); - - let recovered1 = decrypt_share(&root_key, 0, &ciphertext1, epoch0, None).unwrap(); - let recovered2 = decrypt_share(&root_key, 0, &ciphertext2, epoch42, None).unwrap(); - let recovered3 = decrypt_share(&root_key, 0, &ciphertext3, epoch_big, None).unwrap(); - - assert_eq!(share, recovered1); - assert_eq!(share, recovered2); - assert_eq!(share, recovered3); - } - - #[test] - #[ignore] // expensive test - fn update_and_decrypt_10() { - let dummy_seed = [1u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - let params = setup(); - - let (mut decryption_key, public_key) = keygen(¶ms, &mut rng); - - for epoch_value in 0..10 { - let epoch = Epoch::new(epoch_value); - let share = Share::random(&mut rng); - decryption_key - .try_update_to(epoch, ¶ms, &mut rng) - .unwrap(); - - let (ciphertext, hazmat) = - encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng); - verify_hazmat_rand(&ciphertext, &hazmat); - - let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch, None).unwrap(); - assert_eq!(share, recovered); - } - } - - #[test] - #[ignore] // expensive test - fn reblinding_node_doesnt_affect_decryption() { - let dummy_seed = [1u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - let params = setup(); - - let (mut decryption_key, public_key) = keygen(¶ms, &mut rng); - - let epoch = Epoch::new(12345); - decryption_key - .try_update_to(epoch, ¶ms, &mut rng) - .unwrap(); - for node in decryption_key.nodes.iter_mut() { - node.reblind(¶ms, &mut rng); - } - let share = Share::random(&mut rng); - - let (ciphertext, hazmat) = - encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng); - verify_hazmat_rand(&ciphertext, &hazmat); - - let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch, None).unwrap(); - assert_eq!(share, recovered); - - // attempt to update the key again so we have to derive fresh nodes using previous reblinded results - let epoch2 = Epoch::new(67890); - decryption_key - .try_update_to(epoch2, ¶ms, &mut rng) - .unwrap(); - for node in decryption_key.nodes.iter_mut() { - node.reblind(¶ms, &mut rng); - } - let share2 = Share::random(&mut rng); - - let (ciphertext, hazmat) = - encrypt_shares(&[(&share2, &public_key.key)], epoch2, ¶ms, &mut rng); - verify_hazmat_rand(&ciphertext, &hazmat); - - let recovered = decrypt_share(&decryption_key, 0, &ciphertext, epoch2, None).unwrap(); - assert_eq!(share2, recovered); - } - #[test] #[ignore] // expensive test fn ciphertext_integrity_check_passes_for_valid_data() { @@ -634,14 +507,11 @@ mod tests { let dummy_seed = [1u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - let (mut dk, public_key) = keygen(¶ms, &mut rng); - let epoch = Epoch::new(1); + let (_, public_key) = keygen(¶ms, &mut rng); - dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); let share = Share::random(&mut rng); - let (ciphertext, _) = - encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng); - assert!(ciphertext.verify_integrity(¶ms, epoch)) + let (ciphertext, _) = encrypt_shares(&[(&share, &public_key.key)], ¶ms, &mut rng); + assert!(ciphertext.verify_integrity(¶ms)) } #[test] @@ -652,45 +522,22 @@ mod tests { let dummy_seed = [1u8; 32]; let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - let (mut dk, public_key) = keygen(¶ms, &mut rng); - let epoch = Epoch::new(1); + let (_, public_key) = keygen(¶ms, &mut rng); - dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); let share = Share::random(&mut rng); - let (ciphertext, _) = - encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng); + let (ciphertext, _) = encrypt_shares(&[(&share, &public_key.key)], ¶ms, &mut rng); let mut bad_cipher1 = ciphertext.clone(); bad_cipher1.rr[4] = G1Projective::generator(); - assert!(!bad_cipher1.verify_integrity(¶ms, epoch)); + assert!(!bad_cipher1.verify_integrity(¶ms)); let mut bad_cipher2 = ciphertext.clone(); bad_cipher2.ss[4] = G1Projective::generator(); - assert!(!bad_cipher2.verify_integrity(¶ms, epoch)); + assert!(!bad_cipher2.verify_integrity(¶ms)); let mut bad_cipher3 = ciphertext; bad_cipher3.zz[4] = G2Projective::generator(); - assert!(!bad_cipher3.verify_integrity(¶ms, epoch)); - } - - #[test] - #[ignore] // expensive test - fn ciphertext_integrity_check_passes_fails_for_wrong_epoch() { - let params = setup(); - - let dummy_seed = [1u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - - let (mut dk, public_key) = keygen(¶ms, &mut rng); - let epoch = Epoch::new(1); - - dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); - let share = Share::random(&mut rng); - let (ciphertext, _) = - encrypt_shares(&[(&share, &public_key.key)], epoch, ¶ms, &mut rng); - - let another_epoch = Epoch::new(2); - assert!(!ciphertext.verify_integrity(¶ms, another_epoch)) + assert!(!bad_cipher3.verify_integrity(¶ms)); } #[test] @@ -711,7 +558,7 @@ mod tests { } let refs = shares.iter().zip(public_keys.iter()).collect::>(); - let (ciphertext, hazmat) = encrypt_shares(&refs, Epoch::new(42), ¶ms, &mut rng); + let (ciphertext, hazmat) = encrypt_shares(&refs, ¶ms, &mut rng); let combined_r = combine_scalar_chunks(hazmat.r()); let combined_rr = ciphertext.combine_rs(); diff --git a/common/crypto/dkg/src/bte/keys.rs b/common/crypto/dkg/src/bte/keys.rs index 857b01c5b0..9412790db8 100644 --- a/common/crypto/dkg/src/bte/keys.rs +++ b/common/crypto/dkg/src/bte/keys.rs @@ -2,306 +2,16 @@ // SPDX-License-Identifier: Apache-2.0 use crate::bte::proof_discrete_log::ProofOfDiscreteLog; -use crate::bte::{Epoch, Params, Tau}; +use crate::bte::Params; use crate::error::DkgError; use crate::utils::{deserialize_g1, deserialize_g2, deserialize_scalar}; use bls12_381::{G1Projective, G2Projective, Scalar}; use ff::Field; use group::GroupEncoding; +use pemstore::traits::{PemStorableKey, PemStorableKeyPair}; use rand_core::RngCore; use zeroize::Zeroize; -#[derive(Debug, Zeroize)] -#[zeroize(drop)] -#[cfg_attr(test, derive(Clone, PartialEq))] -pub(crate) struct Node { - pub(crate) tau: Tau, - - // g1^rho - pub(crate) a: G1Projective, - - // g2^x - pub(crate) b: G2Projective, - - // f_i^rho, up to lambda_t elements - pub(crate) ds: Vec, - - // fh_i^rho, always lambda_h elements - pub(crate) dh: Vec, - - // h^rho - pub(crate) e: G2Projective, -} - -impl Node { - fn new_root( - a: G1Projective, - b: G2Projective, - ds: Vec, - dh: Vec, - e: G2Projective, - ) -> Self { - Node { - tau: Tau::new_root(), - a, - b, - ds, - dh, - e, - } - } - - fn is_root(&self) -> bool { - self.tau.0.is_empty() - } - - pub(crate) fn reblind(&mut self, params: &Params, mut rng: impl RngCore) { - let delta = Scalar::random(&mut rng); - self.a += G1Projective::generator() * delta; - - // TODO: or do we have to do full tau evaluation here? - self.b += self.tau.evaluate_partial_f(params) * delta; - self.ds - .iter_mut() - .zip(params.fs.iter().skip(self.tau.height())) - .for_each(|(d_i, f_i)| *d_i += f_i * delta); - self.dh - .iter_mut() - .zip(params.fh.iter()) - .for_each(|(d_i, f_i)| *d_i += f_i * delta); - - self.e += params.h * delta; - } - - // note: it's unsafe to use this method outside `try_update_to` as - // we have guaranteed there that `self` is parent of the target - // and that `self.tau != target_tau` - /// Given `self` with `Tau1` and `target_tau` with `Tau2`, such that `Tau1` prefixes `Tau2`, - /// i.e. `Tau2 == Tau1 || SUFFIX`, and `Tau2` is a leaf node, derive all required crypto material - /// for its construction. - fn derive_target_child_with_partials( - &self, - params: &Params, - target_tau: Tau, - partial_b: &G2Projective, - partial_f: &G2Projective, - mut rng: impl RngCore, - ) -> Self { - debug_assert!(self.tau.is_parent_of(&target_tau)); - debug_assert_ne!(self.tau, target_tau); - - let delta = Scalar::random(&mut rng); - let a = self.a + G1Projective::generator() * delta; - let b = partial_b + partial_f * delta; - let ds = self - .ds - .iter() - .zip(params.fs.iter()) - .skip(target_tau.height()) - .map(|(d_i, f_i)| d_i + f_i * delta) - .collect(); - let dh = self - .dh - .iter() - .zip(params.fh.iter()) - .map(|(dh_i, fh_i)| dh_i + fh_i * delta) - .collect(); - let e = self.e + params.h * delta; - - Node { - tau: target_tau, - a, - b, - ds, - dh, - e, - } - } - - // note: it's unsafe to use this method outside `try_update_to` as - // we have guaranteed there that `self` is parent of the target - // and that `self.tau != target_tau` - /// Given `self` with `Tau1` and `most_direct_parent` with `Tau2`, such that `Tau1` prefixes `Tau2`, - /// i.e. `Tau2 == Tau1 || SUFFIX`, derive node with `Tau3 = Tau2 || 1` - fn derive_right_nonfinal_child_of_with_partials( - &self, - params: &Params, - most_direct_parent: Tau, - partial_b: &G2Projective, - partial_f: &G2Projective, - mut rng: impl RngCore, - ) -> Self { - let right_branch = most_direct_parent.right_child(); - - debug_assert!(self.tau.is_parent_of(&most_direct_parent)); - debug_assert!(self.tau.is_parent_of(&right_branch)); - debug_assert_ne!(self.tau, right_branch); - - // n is height difference between self and the child - let n = right_branch.height() - self.tau.height(); - - // i is the index of the last bit we just added - let i = right_branch.height() - 1; - - let delta = Scalar::random(&mut rng); - let a = self.a + G1Projective::generator() * delta; - let d0 = self.ds[n - 1]; - let b = partial_b + d0 + (partial_f + params.fs[i]) * delta; - let ds = self - .ds - .iter() - .skip(n) - .zip(params.fs.iter().skip(right_branch.height())) - .map(|(d_i, f_i)| d_i + f_i * delta) - .collect(); - let dh = self - .dh - .iter() - .zip(params.fh.iter()) - .map(|(dh_i, fh_i)| dh_i + fh_i * delta) - .collect(); - - let e = self.e + params.h * delta; - - Node { - tau: right_branch, - a, - b, - ds, - dh, - e, - } - } - - // tau_bytes_len || tau || a || b || len_ds || ds || len_dh || dh || e - pub(crate) fn to_bytes(&self) -> Vec { - let g1_elements = 1; - let g2_elements = self.ds.len() + self.dh.len() + 2; - - let tau_bytes = self.tau.to_bytes(); - - // the extra 12 comes from the triple u32 we use for encoding lengths of tau, ds and dh - let mut bytes = - Vec::with_capacity(tau_bytes.len() + g1_elements * 48 + g2_elements * 96 + 12); - - bytes.extend_from_slice(&((tau_bytes.len() as u32).to_be_bytes())); - bytes.extend_from_slice(&tau_bytes); - bytes.extend_from_slice(self.a.to_bytes().as_ref()); - bytes.extend_from_slice(self.b.to_bytes().as_ref()); - bytes.extend_from_slice(&((self.ds.len() as u32).to_be_bytes())); - for d_i in &self.ds { - bytes.extend_from_slice(d_i.to_bytes().as_ref()); - } - bytes.extend_from_slice(&((self.dh.len() as u32).to_be_bytes())); - for dh_i in &self.dh { - bytes.extend_from_slice(dh_i.to_bytes().as_ref()); - } - bytes.extend_from_slice(self.e.to_bytes().as_ref()); - - bytes - } - - pub fn try_from_bytes(bytes: &[u8]) -> Result { - // at the very least we require bytes for: - // - tau_len ( 4 ) - // - tau ( could be 0 for root node ) - // - a ( 48 ) - // - b ( 96 ) - // - length indication of ds ( 4 ) - // - length indication of dh ( 4 ) - // - e ( 96 ) - if bytes.len() < 4 + 48 + 96 + 4 + 4 + 96 { - return Err(DkgError::new_deserialization_failure( - "Node", - "insufficient number of bytes provided", - )); - } - - let tau_len = u32::from_be_bytes((&bytes[..4]).try_into().unwrap()) as usize; - let mut i = 4; - - let tau = Tau::try_from_bytes(&bytes[i..i + tau_len])?; - i += tau_len; - - // perform another length check to account for bytes consumed by tau - if bytes[i..].len() < 48 + 96 + 4 + 4 + 96 { - return Err(DkgError::new_deserialization_failure( - "Node", - "insufficient number of bytes provided", - )); - } - - let a = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| { - DkgError::new_deserialization_failure("Node.a", "invalid curve point") - })?; - i += 48; - - let b = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| { - DkgError::new_deserialization_failure("Node.b", "invalid curve point") - })?; - i += 96; - - let ds_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; - i += 4; - - if bytes[i..].len() < ds_len * 96 + 4 { - return Err(DkgError::new_deserialization_failure( - "Node", - "insufficient number of bytes provided (ds)", - )); - } - - let mut ds = Vec::with_capacity(ds_len); - for j in 0..ds_len { - let d_i = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| { - DkgError::new_deserialization_failure( - format!("Node.ds_{}", j), - "invalid curve point", - ) - })?; - - ds.push(d_i); - i += 96; - } - - let dh_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; - i += 4; - - if bytes[i..].len() != (dh_len + 1) * 96 { - return Err(DkgError::new_deserialization_failure( - "Node", - "insufficient number of bytes provided (dh)", - )); - } - - let mut dh = Vec::with_capacity(dh_len); - for j in 0..dh_len { - let dh_i = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| { - DkgError::new_deserialization_failure( - format!("Node.dh_{}", j), - "invalid curve point", - ) - })?; - - dh.push(dh_i); - i += 96; - } - - let e = deserialize_g2(&bytes[i..]).ok_or_else(|| { - DkgError::new_deserialization_failure("Node.h", "invalid curve point") - })?; - - Ok(Node { - tau, - a, - b, - ds, - dh, - e, - }) - } -} - // produces public key and a decryption key for the root of the tree pub fn keygen(params: &Params, mut rng: impl RngCore) -> (DecryptionKey, PublicKeyWithProof) { let g1 = G1Projective::generator(); @@ -317,11 +27,10 @@ pub fn keygen(params: &Params, mut rng: impl RngCore) -> (DecryptionKey, PublicK let a = g1 * rho; let b = g2 * x + params.f0 * rho; - let ds = params.fs.iter().map(|f_i| f_i * rho).collect(); let dh = params.fh.iter().map(|fh_i| fh_i * rho).collect(); let e = params.h * rho; - let dk = DecryptionKey::new_root(Node::new_root(a, b, ds, dh, e)); + let dk = DecryptionKey::new_root(a, b, dh, e); let public_key = PublicKey(y); let key_with_proof = PublicKeyWithProof { @@ -351,6 +60,20 @@ pub struct PublicKeyWithProof { pub(crate) proof: ProofOfDiscreteLog, } +impl PemStorableKey for PublicKeyWithProof { + type Error = DkgError; + + fn pem_type() -> &'static str { + "DKG PUBLIC KEY WITH PROOF" + } + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + fn from_bytes(bytes: &[u8]) -> Result { + Self::try_from_bytes(bytes) + } +} + impl PublicKeyWithProof { pub fn verify(&self) -> bool { self.key.verify(&self.proof) @@ -412,233 +135,159 @@ impl PublicKeyWithProof { #[derive(Debug, Zeroize)] #[zeroize(drop)] -#[cfg_attr(test, derive(PartialEq))] +#[cfg_attr(test, derive(PartialEq, Eq))] pub struct DecryptionKey { - // note that the nodes are ordered from "right" to "left" - pub(crate) nodes: Vec, + // g1^rho + pub(crate) a: G1Projective, + + // g2^x * f0^rho + pub(crate) b: G2Projective, + + // fh_i^rho, always lambda_h elements + pub(crate) dh: Vec, + + // h^rho + pub(crate) e: G2Projective, +} + +impl PemStorableKey for DecryptionKey { + type Error = DkgError; + + fn pem_type() -> &'static str { + "DKG DECRYPTION KEY" + } + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + fn from_bytes(bytes: &[u8]) -> Result { + Self::try_from_bytes(bytes) + } } impl DecryptionKey { - fn new_root(root_node: Node) -> Self { - DecryptionKey { - nodes: vec![root_node], - } - } - - fn current(&self) -> Result<&Node, DkgError> { - // we must have at least a single node, otherwise we have a malformed key - self.nodes.last().ok_or(DkgError::MalformedDecryptionKey) - } - - pub fn current_epoch(&self, params: &Params) -> Result, DkgError> { - let current_node = self.current()?; - if current_node.is_root() { - Ok(None) - } else { - Epoch::try_from_tau(¤t_node.tau, params).map(Option::Some) - } - } - - pub(crate) fn try_get_compatible_node(&self, epoch: Epoch) -> Result<&Node, DkgError> { - let tau = epoch.as_tau(); - self.nodes - .iter() - .rev() - .find(|node| node.tau.is_parent_of(&tau)) - .ok_or(DkgError::ExpiredKey) - } - - pub fn try_update_to_next_epoch( - &mut self, - params: &Params, - mut rng: impl RngCore, - ) -> Result<(), DkgError> { - if self.nodes.is_empty() { - return Err(DkgError::MalformedDecryptionKey); - } - - let mut target_epoch = Epoch::new(0); - if self.nodes.len() == 1 && self.nodes[0].is_root() { - return self.try_update_to(target_epoch, params, &mut rng); - } - - // unwrap is fine as we have asserted self.nodes is not empty - self.nodes.pop().unwrap(); - - if let Some(tail) = self.nodes.last() { - target_epoch = tail.tau.lowest_valid_epoch_child(params)?; - } else { - // essentially our key consisted of only a single node and it wasn't a root, - // so either it was malformed or we somehow reached the final epoch and wanted to update - // beyond that. Either way, update to l + 1 is impossible - return Err(DkgError::MalformedDecryptionKey); - } - - self.try_update_to(target_epoch, params, &mut rng) - } - - /// Attempts to update `self` to the provided `epoch`. If the update is not possible, - /// because the target was in the past or the key is malformed, an error is returned. - /// - /// Note that this method mutates the key in place and if the original key was malformed, - /// there are no guarantees about its internal state post-call. - pub fn try_update_to( - &mut self, - target_epoch: Epoch, - params: &Params, - mut rng: impl RngCore, - ) -> Result<(), DkgError> { - if self.nodes.is_empty() { - // somehow we have an empty decryption key - return Err(DkgError::MalformedDecryptionKey); - } - - // makes it easier to work with since we will be generating non-leaf nodes - let target_tau = target_epoch.as_tau(); - let current_tau = &self.current()?.tau; - - if current_tau == &target_tau { - // our key is already updated to the target - return Ok(()); - } - - if current_tau > &target_tau { - // we cannot derive keys for past epochs - return Err(DkgError::TargetEpochUpdateInThePast); - } - - // drop the nodes that are no longer required and get the most direct parent for the target epoch available - let mut parent = loop { - // if pop() fails the key is malformed since we checked that the target_epoch > current_epoch, - // hence the update should have been possible - let tail = self.nodes.pop().ok_or(DkgError::MalformedDecryptionKey)?; - if tail.tau.is_parent_of(&target_tau) { - break tail; - } - }; - - // essentially the case of updating epoch n to n + 1, where n is even; - // in that case the last two nodes are [..., epoch_{n+1}, epoch_n] - // so we just have to reblind the n+1 node and we're done - if parent.tau == target_tau { - parent.reblind(params, &mut rng); - self.nodes.push(parent); - return Ok(()); - } - - // accumulators, note that the previous elements have already been included by the parent, - // i.e. for example for parent at height l <= n, b = g2^x * f0^rho * d1^{tau_1} * ... * dl^{tau_l} - // new_b_accumulator = b * d1^{tau_1} * d2^{tau_2} * ... * dn^{tau_n} - // new_f_accumulator = f0 * f1^{tau_1} * f2^{tau_2} * ... * fn^{tau_n} (up to lambda_t) - let mut new_b_accumulator = parent.b; - let mut new_f_accumulator = parent.tau.evaluate_partial_f(params); - - let parent_height = parent.tau.height(); - - // path from the parent to the child - for (n, bit) in target_tau - .0 - .iter() - .by_vals() - .skip(parent.tau.height()) - .enumerate() - { - // ith bit of the [child] epoch - // note that n represents height difference between parent and the current bit - let i = n + parent_height; - - // if the bit is NOT set, push the right '1' subtree (for future keys) - // so for example if given parent with some `PREFIX` tau and target_epoch being `PREFIX || 010`, - // in the first loop iteration we're going to look at bit `0` and - // derive child node `PREFIX || 1` so that in the future we could derive keys for all other epochs starting with `PREFIX || 1` - // in the next loop iteration we're going to look at bit `1` and simply update the accumulators, - // as we don't need to generate any "left" nodes as all of them would have constructed epochs that are already in the past - // finally, in the last iteration, we look at the bit `0` and derive node `PREFIX || 011`, - // i.e. the one that FOLLOWS the target node. - if !bit { - let direct_parent = target_tau.try_get_parent_at_height(i)?; - - self.nodes - .push(parent.derive_right_nonfinal_child_of_with_partials( - params, - direct_parent, - &new_b_accumulator, - &new_f_accumulator, - &mut rng, - )); - } else { - // only update the accumulators when the bit is set, as d^0 == identity, so there's - // no point in doing anything else; - // note that we don't have to generate any new nodes when going into the right branch - // of the tree as everything on the left would have been in the past, so we don't care about them - new_b_accumulator += parent.ds[n]; // add d0 - new_f_accumulator += params.fs[i]; // f_i - } - } - - self.nodes.push(parent.derive_target_child_with_partials( - params, - target_epoch.as_tau(), - &new_b_accumulator, - &new_f_accumulator, - &mut rng, - )); - - Ok(()) + fn new_root(a: G1Projective, b: G2Projective, dh: Vec, e: G2Projective) -> Self { + DecryptionKey { a, b, dh, e } } pub fn to_bytes(&self) -> Vec { - let num_nodes = self.nodes.len() as u32; + let g1_elements = 1; + let g2_elements = self.dh.len() + 2; - // unfortunately we're not going to know the expected capacity - let mut bytes = Vec::new(); - bytes.extend_from_slice(&num_nodes.to_be_bytes()); + // the extra 8 comes from the triple u32 we use for encoding lengths of ds and dh + let mut bytes = Vec::with_capacity(g1_elements * 48 + g2_elements * 96 + 8); - for node in &self.nodes { - let mut node_bytes = node.to_bytes(); - bytes.extend_from_slice(&((node_bytes.len() as u32).to_be_bytes())); - bytes.append(&mut node_bytes) + bytes.extend_from_slice(self.a.to_bytes().as_ref()); + bytes.extend_from_slice(self.b.to_bytes().as_ref()); + bytes.extend_from_slice(&((self.dh.len() as u32).to_be_bytes())); + for dh_i in &self.dh { + bytes.extend_from_slice(dh_i.to_bytes().as_ref()); } + bytes.extend_from_slice(self.e.to_bytes().as_ref()); bytes } - pub fn try_from_bytes(b: &[u8]) -> Result { - // we have to be able to read the length of nodes - if b.len() < 4 { + pub fn try_from_bytes(bytes: &[u8]) -> Result { + // at the very least we require bytes for: + // - a ( 48 ) + // - b ( 96 ) + // - length indication of dh ( 4 ) + // - e ( 96 ) + if bytes.len() < 48 + 96 + 4 + 96 { return Err(DkgError::new_deserialization_failure( - "DecryptionKey", + "Node", "insufficient number of bytes provided", )); } - let nodes_len = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize; - let mut nodes = Vec::with_capacity(nodes_len); - let mut i = 4; - for _ in 0..nodes_len { - // check if we can actually read the length... - if b[i..].len() < 4 { - return Err(DkgError::new_deserialization_failure( - "DecryptionKey.Node", - "insufficient number of bytes provided for BTE Node recovery", - )); - } + let mut i = 0; + let a = deserialize_g1(&bytes[i..i + 48]).ok_or_else(|| { + DkgError::new_deserialization_failure("Node.a", "invalid curve point") + })?; + i += 48; - let node_bytes = u32::from_be_bytes([b[i], b[i + 1], b[i + 2], b[i + 3]]) as usize; - if b[i + 4..].len() < node_bytes { - return Err(DkgError::new_deserialization_failure( - "DecryptionKey.Node", - "insufficient number of bytes provided for BTE Node recovery", - )); - } - i += 4; + let b = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| { + DkgError::new_deserialization_failure("Node.b", "invalid curve point") + })?; + i += 96; - let node = Node::try_from_bytes(&b[i..i + node_bytes])?; - nodes.push(node); - i += node_bytes; + let dh_len = u32::from_be_bytes((&bytes[i..i + 4]).try_into().unwrap()) as usize; + i += 4; + + if bytes[i..].len() != (dh_len + 1) * 96 { + return Err(DkgError::new_deserialization_failure( + "Node", + "insufficient number of bytes provided (dh)", + )); } - Ok(DecryptionKey { nodes }) + let mut dh = Vec::with_capacity(dh_len); + for j in 0..dh_len { + let dh_i = deserialize_g2(&bytes[i..i + 96]).ok_or_else(|| { + DkgError::new_deserialization_failure( + format!("Node.dh_{}", j), + "invalid curve point", + ) + })?; + + dh.push(dh_i); + i += 96; + } + + let e = deserialize_g2(&bytes[i..]).ok_or_else(|| { + DkgError::new_deserialization_failure("Node.h", "invalid curve point") + })?; + + Ok(Self { a, b, dh, e }) + } +} + +pub struct KeyPair { + pub(crate) private_key: DecryptionKey, + pub(crate) public_key: PublicKeyWithProof, +} + +impl KeyPair { + pub fn new(params: &Params, rng: impl RngCore) -> Self { + let (dk, pk) = keygen(params, rng); + Self { + private_key: dk, + public_key: pk, + } + } + pub fn private_key(&self) -> &DecryptionKey { + &self.private_key + } + + pub fn public_key(&self) -> &PublicKeyWithProof { + &self.public_key + } + + pub fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Result { + Ok(KeyPair { + private_key: DecryptionKey::try_from_bytes(priv_bytes)?, + public_key: PublicKeyWithProof::try_from_bytes(pub_bytes)?, + }) + } +} + +impl PemStorableKeyPair for KeyPair { + type PrivatePemKey = DecryptionKey; + type PublicPemKey = PublicKeyWithProof; + + fn private_key(&self) -> &Self::PrivatePemKey { + self.private_key() + } + + fn public_key(&self) -> &Self::PublicPemKey { + self.public_key() + } + + fn from_keys(private_key: Self::PrivatePemKey, public_key: Self::PublicPemKey) -> Self { + KeyPair { + private_key, + public_key, + } } } @@ -646,163 +295,8 @@ impl DecryptionKey { mod tests { use super::*; use crate::bte::setup; - use bitvec::bitvec; - use bitvec::order::Msb0; use rand_core::SeedableRng; - #[test] - #[ignore] // expensive test - fn basic_coverage_nodes() { - // it's some basic test I've been performing when writing the update function, but figured - // might as well put it into a unit test. note that it doesn't check the entire structure, - // but just the few last nodes of low height - - let params = setup(); - - let dummy_seed = [1u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - - let (mut dk, _) = keygen(¶ms, &mut rng); - - let root_node_copy = dk.nodes.clone(); - - // this is a root node - assert_eq!(dk.nodes.len(), 1); - assert!(dk.nodes[0].is_root()); - - // we have to have a node for right branch on each height (1, 01, 001, ... etc) - // plus an additional one for the two left-most leaves (epochs "0" and "1") - dk.try_update_to(Epoch::new(0), ¶ms, &mut rng).unwrap(); - assert_eq!(dk.nodes.len(), 33); - - let expected_last = Tau::new(0); - // (and yes, I had to look up those names in a thesaurus) - let expected_penultimate = Tau::new(1); - // note that this value is 31bit long - let expected_antepenultimate = Tau(bitvec![u32, Msb0; - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 1 - ]); - - let mut nodes_iter = dk.nodes.iter().rev(); - assert_eq!(expected_last, nodes_iter.next().unwrap().tau); - assert_eq!(expected_penultimate, nodes_iter.next().unwrap().tau); - assert_eq!(expected_antepenultimate, nodes_iter.next().unwrap().tau); - - let mut epoch_zero_nodes = dk.nodes.clone(); - - // nodes for epoch1 should be identical for those for epoch0 minus the 00..00 leaf - dk.try_update_to(Epoch::new(1), ¶ms, &mut rng).unwrap(); - assert_eq!(dk.nodes.len(), 32); - epoch_zero_nodes.pop().unwrap(); - assert_eq!( - epoch_zero_nodes - .iter() - .map(|node| node.tau.clone()) - .collect::>(), - dk.nodes - .iter() - .map(|node| node.tau.clone()) - .collect::>() - ); - - dk.try_update_to(Epoch::new(2), ¶ms, &mut rng).unwrap(); - dk.try_update_to(Epoch::new(3), ¶ms, &mut rng).unwrap(); - dk.try_update_to(Epoch::new(4), ¶ms, &mut rng).unwrap(); - - let expected_last = Tau::new(4); - let expected_penultimate = Tau::new(5); - let expected_antepenultimate = Tau(bitvec![u32, Msb0; - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1 - ]); - let expected_preantepenultimate = Tau(bitvec![u32, Msb0; - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 - ]); - assert_eq!(dk.nodes.len(), 32); - let mut nodes_iter = dk.nodes.iter().rev(); - assert_eq!(expected_last, nodes_iter.next().unwrap().tau); - assert_eq!(expected_penultimate, nodes_iter.next().unwrap().tau); - assert_eq!(expected_antepenultimate, nodes_iter.next().unwrap().tau); - assert_eq!(expected_preantepenultimate, nodes_iter.next().unwrap().tau); - - // the result should be the same of regardless if we update incrementally or go to the target immediately - let mut new_root = DecryptionKey { - nodes: root_node_copy, - }; - new_root - .try_update_to(Epoch::new(4), ¶ms, &mut rng) - .unwrap(); - assert_eq!( - dk.nodes - .iter() - .map(|node| node.tau.clone()) - .collect::>(), - new_root - .nodes - .iter() - .map(|node| node.tau.clone()) - .collect::>() - ); - - // getting expected nodes for those epochs is non-trivial for test purposes, but the last node - // should ALWAYS be equal to the target epoch - dk.try_update_to(Epoch::new(42), ¶ms, &mut rng).unwrap(); - assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(42)); - dk.try_update_to(Epoch::new(123456), ¶ms, &mut rng) - .unwrap(); - assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(123456)); - dk.try_update_to(Epoch::new(3292547435), ¶ms, &mut rng) - .unwrap(); - assert_eq!(dk.nodes.last().unwrap().tau, Tau::new(3292547435)); - - // trying to go to past epochs fails - assert!(dk - .try_update_to(Epoch::new(531), ¶ms, &mut rng) - .is_err()) - } - - #[test] - #[ignore] // expensive test - fn updating_to_next_epoch() { - let params = setup(); - - let dummy_seed = [1u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - - let (mut dk, _) = keygen(¶ms, &mut rng); - - // for root node current epoch is `None` - assert_eq!(None, dk.current_epoch(¶ms).unwrap()); - - // for root node it should result in epoch 0 - dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); - assert_eq!(Some(Epoch::new(0)), dk.current_epoch(¶ms).unwrap()); - - dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); - assert_eq!(Some(Epoch::new(1)), dk.current_epoch(¶ms).unwrap()); - - dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); - assert_eq!(Some(Epoch::new(2)), dk.current_epoch(¶ms).unwrap()); - - // if we start from some non-root epoch, it should result in l + 1 - dk.try_update_to(Epoch::new(42), ¶ms, &mut rng).unwrap(); - dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); - assert_eq!(Some(Epoch::new(43)), dk.current_epoch(¶ms).unwrap()); - - dk.try_update_to(Epoch::new(12345), ¶ms, &mut rng) - .unwrap(); - dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); - assert_eq!(Some(Epoch::new(12346)), dk.current_epoch(¶ms).unwrap()); - - dk.try_update_to(Epoch::new(3292547435), ¶ms, &mut rng) - .unwrap(); - dk.try_update_to_next_epoch(¶ms, &mut rng).unwrap(); - assert_eq!( - Some(Epoch::new(3292547436)), - dk.current_epoch(¶ms).unwrap() - ); - } - #[test] fn public_key_with_proof_roundtrip() { let params = setup(); @@ -816,64 +310,4 @@ mod tests { assert_eq!(pk, recovered) } - - #[test] - #[ignore] // expensive test - fn bte_node_roundtrip() { - let params = setup(); - - let dummy_seed = [1u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - - let (mut dk, _) = keygen(¶ms, &mut rng); - - let root_node = dk.nodes[0].clone(); - let bytes = root_node.to_bytes(); - let recovered = Node::try_from_bytes(&bytes).unwrap(); - assert_eq!(root_node, recovered); - - dk.try_update_to(Epoch::new(3292547435), ¶ms, &mut rng) - .unwrap(); - for node in &dk.nodes { - let bytes = node.to_bytes(); - let recovered = Node::try_from_bytes(&bytes).unwrap(); - assert_eq!(node, &recovered); - } - } - - #[test] - #[ignore] // expensive test - fn decryption_key_node_roundtrip() { - let params = setup(); - - let dummy_seed = [1u8; 32]; - let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); - - let (mut dk, _) = keygen(¶ms, &mut rng); - - let bytes = dk.to_bytes(); - let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap(); - assert_eq!(dk, recovered); - - dk.try_update_to(Epoch::new(0), ¶ms, &mut rng).unwrap(); - let bytes = dk.to_bytes(); - let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap(); - assert_eq!(dk, recovered); - - dk.try_update_to(Epoch::new(1), ¶ms, &mut rng).unwrap(); - let bytes = dk.to_bytes(); - let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap(); - assert_eq!(dk, recovered); - - dk.try_update_to(Epoch::new(42), ¶ms, &mut rng).unwrap(); - let bytes = dk.to_bytes(); - let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap(); - assert_eq!(dk, recovered); - - dk.try_update_to(Epoch::new(3292547435), ¶ms, &mut rng) - .unwrap(); - let bytes = dk.to_bytes(); - let recovered = DecryptionKey::try_from_bytes(&bytes).unwrap(); - assert_eq!(dk, recovered); - } } diff --git a/common/crypto/dkg/src/bte/mod.rs b/common/crypto/dkg/src/bte/mod.rs index cb3dd07fa4..2eca744cf1 100644 --- a/common/crypto/dkg/src/bte/mod.rs +++ b/common/crypto/dkg/src/bte/mod.rs @@ -1,17 +1,11 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::error::DkgError; -use crate::utils::{hash_g2, RandomOracleBuilder}; +use crate::utils::hash_g2; use crate::{Chunk, Share}; -use bitvec::field::BitField; -use bitvec::order::Msb0; -use bitvec::vec::BitVec; -use bitvec::view::BitView; -use bls12_381::{G1Affine, G1Projective, G2Affine, G2Prepared, G2Projective, Gt}; +use bls12_381::{G1Affine, G2Affine, G2Prepared, G2Projective, Gt}; use group::Curve; use lazy_static::lazy_static; -use zeroize::Zeroize; pub mod encryption; pub mod keys; @@ -35,10 +29,6 @@ lazy_static! { // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-hash-to-curve-11#section-3.1 const SETUP_DOMAIN: &[u8] = b"NYM_COCONUT_NIDKG_V01_CS01_WITH_BLS12381G2_XMD:SHA-256_SSWU_RO_SETUP"; -// this particular domain is not for curve hashing, but might as well also follow the same naming pattern -const TREE_TAU_EXTENSION_DOMAIN: &[u8] = b"NYM_COCONUT_NIDKG_V01_CS01_SHA-256_TREE_EXTENSION"; - -const MAX_EPOCHS_EXP: usize = 32; const HASH_SECURITY_PARAM: usize = 256; // note: CHUNK_BYTES * NUM_CHUNKS must equal to SCALAR_SIZE @@ -49,253 +39,17 @@ pub const SCALAR_SIZE: usize = 32; /// In paper B; number of distinct chunks pub const CHUNK_SIZE: usize = 1 << (CHUNK_BYTES << 3); -pub(crate) type EpochStore = u32; - -#[derive(Clone, Debug, PartialEq, PartialOrd)] -// None empty bitvec implies this is a root node -pub(crate) struct Tau(BitVec); - -impl Tau { - pub fn new_root() -> Self { - Tau(BitVec::new()) - } - - // TODO: perhaps this should be explicitly moved to some test module - #[cfg(test)] - pub(crate) fn new(epoch: EpochStore) -> Self { - Tau(epoch.view_bits().to_bitvec()) - } - - #[allow(unused)] - pub fn left_child(&self) -> Self { - let mut child = self.0.clone(); - child.push(false); - Tau(child) - } - - pub fn right_child(&self) -> Self { - let mut child = self.0.clone(); - child.push(true); - Tau(child) - } - - pub fn is_leaf(&self, params: &Params) -> bool { - self.height() == params.lambda_t - } - - pub fn try_get_parent_at_height(&self, height: usize) -> Result { - if height > self.0.len() { - return Err(DkgError::NotAValidParent); - } - - Ok(Tau(self.0[..height].to_bitvec())) - } - - // essentially is this tau prefixing the other - pub fn is_parent_of(&self, other: &Tau) -> bool { - if self.0.len() > other.0.len() { - return false; - } - - for (i, b) in self.0.iter().enumerate() { - if b != other.0[i] { - return false; - } - } - - true - } - - pub fn lowest_valid_epoch_child(&self, params: &Params) -> Result { - if self.0.len() > params.lambda_t { - // this node is already BELOW a valid leaf-epoch node. it can only happen - // if either some invariant was broken or additional data was pushed to `tau` - // in order compute some intermediate results, but in that case this method should have - // never been called anyway. tl;dr: if this is called, the underlying key is malformed - return Err(DkgError::NotAValidParent); - } - let mut child = self.0.clone(); - for _ in 0..(params.lambda_t - self.0.len()) { - child.push(false) - } - - // the unwrap here is fine as we ensure we have exactly `params.tree_height` bits here - // (we could just propagate the error instead of unwraping and putting it behind an `Ok` anyway - // but I'd prefer to just blow up since this would be a serious error - Ok(Epoch::try_from_tau(&Tau(child), params).unwrap()) - } - - pub fn height(&self) -> usize { - self.0.len() - } - - fn extend( - &self, - rr: &[G1Projective; NUM_CHUNKS], - ss: &[G1Projective; NUM_CHUNKS], - cc: &[[G1Projective; NUM_CHUNKS]], - ) -> Self { - let mut random_oracle_builder = RandomOracleBuilder::new(TREE_TAU_EXTENSION_DOMAIN); - random_oracle_builder.update_with_g1_elements(rr.iter()); - random_oracle_builder.update_with_g1_elements(ss.iter()); - for ciphertext_chunks in cc { - random_oracle_builder.update_with_g1_elements(ciphertext_chunks.iter()); - } - - let tau_mem = self.0.as_raw_slice(); - assert_eq!(tau_mem.len(), 1, "tau length invariant was broken"); - random_oracle_builder.update(tau_mem[0].to_be_bytes()); - - let oracle_output = random_oracle_builder.finalize(); - debug_assert_eq!(oracle_output.len() * 8, HASH_SECURITY_PARAM); - - let mut extended_tau = self.clone(); - for byte in oracle_output { - extended_tau - .0 - .extend_from_bitslice(byte.view_bits::()) - } - - extended_tau - } - - // considers all lambda_t + lambda_h bits - fn evaluate_f(&self, params: &Params) -> G2Projective { - self.0 - .iter() - .by_vals() - .zip(params.fs.iter().chain(params.fh.iter())) - .filter(|(i, _)| *i) - .map(|(_, f_i)| f_i) - .fold(params.f0, |acc, f_i| acc + f_i) - } - - // only considers up to lambda_t bits - fn evaluate_partial_f(&self, params: &Params) -> G2Projective { - self.0 - .iter() - .by_vals() - .zip(params.fs.iter()) - .filter(|(i, _)| *i) - .map(|(_, f_i)| f_i) - .fold(params.f0, |acc, f_i| acc + f_i) - } - - pub(crate) fn to_bytes(&self) -> Vec { - let len_bytes = (self.0.len() as u32).to_be_bytes(); - len_bytes - .into_iter() - .chain(self.0.chunks(8).map(BitField::load_be)) - .collect() - } - - pub(crate) fn try_from_bytes(b: &[u8]) -> Result { - if b.len() < 4 { - return Err(DkgError::new_deserialization_failure( - "Tau", - "insufficient number of bytes provided", - )); - } - let tau_len = u32::from_be_bytes([b[0], b[1], b[2], b[3]]) as usize; - - // maximum theoretical length - if tau_len > MAX_EPOCHS_EXP + HASH_SECURITY_PARAM { - return Err(DkgError::new_deserialization_failure( - "Tau", - format!( - "malformed length {} is greater than maximum {}", - tau_len, - MAX_EPOCHS_EXP + HASH_SECURITY_PARAM - ), - )); - } - - if tau_len == 0 { - if b.len() != 4 { - Err(DkgError::new_deserialization_failure( - "Tau", - "malformed bytes", - )) - } else { - Ok(Tau::new_root()) - } - } else if b.len() == 4 { - Err(DkgError::new_deserialization_failure( - "Tau", - "insufficient number of bytes provided", - )) - } else { - let mut inner = BitVec::repeat(false, tau_len); - for (slot, &byte) in inner.chunks_mut(8).zip(b[4..].iter()) { - slot.store_be(byte); - } - - Ok(Tau(inner)) - } - } -} - -impl Zeroize for Tau { - fn zeroize(&mut self) { - for v in self.0.as_raw_mut_slice() { - v.zeroize() - } - } -} - -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd)] -pub struct Epoch(EpochStore); - -impl Epoch { - pub fn new(value: EpochStore) -> Self { - Epoch(value) - } - - pub(crate) fn as_tau(&self) -> Tau { - (*self).into() - } - - pub(crate) fn as_extended_tau( - &self, - rr: &[G1Projective; NUM_CHUNKS], - ss: &[G1Projective; NUM_CHUNKS], - cc: &[[G1Projective; NUM_CHUNKS]], - ) -> Tau { - self.as_tau().extend(rr, ss, cc) - } - - pub(crate) fn try_from_tau(tau: &Tau, params: &Params) -> Result { - if !tau.is_leaf(params) { - Err(DkgError::MalformedEpoch) - } else { - Ok(Epoch(tau.0.load_be())) - } - } -} - -impl From for Tau { - fn from(epoch: Epoch) -> Self { - Tau(epoch.0.view_bits().to_bitvec()) - } -} - -impl From for Epoch { - fn from(epoch: EpochStore) -> Self { - Epoch(epoch) - } +// considers all lambda_h bits +pub fn evaluate_f(params: &Params) -> G2Projective { + params.fh.iter().fold(params.f0, |acc, f_i| acc + f_i) } pub struct Params { - /// Maximum size of an epoch, in bits. - pub lambda_t: usize, - /// Security parameter of our $H_{\Lamda_H}$ hash function pub lambda_h: usize, - // keeping f0 separate from the rest of the curve points makes it easier to work with tau f0: G2Projective, - fs: Vec, // f_1, f_2, .... f_{lambda_t} in the paper - fh: Vec, // f_{lambda_t+1}, f_{lambda_t+1}, .... f_{lambda_t+lambda_h} in the paper + fh: Vec, // f_{lambda_h}, f_{lambda_h+1}, .... f_{lambda_h} in the paper h: G2Projective, /// Precomputed `h` used for the miller loop @@ -305,10 +59,6 @@ pub struct Params { pub fn setup() -> Params { let f0 = hash_g2(b"f0", SETUP_DOMAIN); - let fs = (1..=MAX_EPOCHS_EXP) - .map(|i| hash_g2(format!("f{}", i), SETUP_DOMAIN)) - .collect(); - let fh = (0..HASH_SECURITY_PARAM) .map(|i| hash_g2(format!("fh{}", i), SETUP_DOMAIN)) .collect(); @@ -316,148 +66,10 @@ pub fn setup() -> Params { let h = hash_g2(b"h", SETUP_DOMAIN); Params { - lambda_t: MAX_EPOCHS_EXP, lambda_h: HASH_SECURITY_PARAM, f0, - fs, fh, h, _h_prepared: G2Prepared::from(h.to_affine()), } } - -#[cfg(test)] -mod tests { - use super::*; - use bitvec::bitvec; - use bitvec::order::Msb0; - - #[test] - fn creating_tau_from_epoch() { - assert!(Tau::new_root().0.is_empty()); - - let zero = Tau::new(0); - assert!(zero.0.iter().by_vals().all(|b| !b)); - - let one = Tau::new(1); - let mut iter = one.0.iter().by_vals(); - // first 31 bits are 0, the last one is 1 - for _ in 0..31 { - assert!(!iter.next().unwrap()) - } - assert!(iter.next().unwrap()); - - // 101010 in binary - let forty_two = Tau::new(42); - // first 26 bits are not set - let mut iter = forty_two.0.iter().by_vals(); - for _ in 0..26 { - assert!(!iter.next().unwrap()) - } - assert!(iter.next().unwrap()); - assert!(!iter.next().unwrap()); - assert!(iter.next().unwrap()); - assert!(!iter.next().unwrap()); - assert!(iter.next().unwrap()); - assert!(!iter.next().unwrap()); - - // value that requires an actual u32 (i.e. takes 4 bytes to represent) - // 11000100_01000000_01001001_01101011 in binary - let big_val = Tau::new(3292547435); - let expected = bitvec![u32, Msb0; - 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, - 0, 1, 1 - ]; - assert_eq!(expected, big_val.0) - } - - #[test] - fn getting_parent_at_height() { - let tau = Tau(bitvec![u32, Msb0; 1,0,1,1,0,0,1]); - - let expected_0 = Tau(BitVec::new()); - let expected_1 = Tau(bitvec![u32, Msb0; 1]); - let expected_5 = Tau(bitvec![u32, Msb0; 1,0,1,1,0]); - - assert_eq!(expected_0, tau.try_get_parent_at_height(0).unwrap()); - assert_eq!(expected_1, tau.try_get_parent_at_height(1).unwrap()); - assert_eq!(expected_5, tau.try_get_parent_at_height(5).unwrap()); - assert_eq!(tau, tau.try_get_parent_at_height(7).unwrap()); - assert!(tau.try_get_parent_at_height(8).is_err()) - } - - #[test] - fn converting_tau_to_epoch() { - let params = setup(); - - let tau0: Tau = Epoch::new(0).into(); - let tau1: Tau = Epoch::new(1).into(); - let tau42: Tau = Epoch::new(42).into(); - let tau_big: Tau = Epoch::new(3292547435).into(); - - assert_eq!(Epoch::new(0), Epoch::try_from_tau(&tau0, ¶ms).unwrap()); - assert_eq!(Epoch::new(1), Epoch::try_from_tau(&tau1, ¶ms).unwrap()); - assert_eq!( - Epoch::new(42), - Epoch::try_from_tau(&tau42, ¶ms).unwrap() - ); - assert_eq!( - Epoch::new(3292547435), - Epoch::try_from_tau(&tau_big, ¶ms).unwrap() - ); - - assert!(Epoch::try_from_tau(&Tau(BitVec::new()), ¶ms).is_err()); - assert!(Epoch::try_from_tau(&Tau(bitvec![u32, Msb0; 1,0,1,1,0]), ¶ms).is_err()); - let _31bit_tau = Tau(bitvec![u32, Msb0; - 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, - 0, 1 - ]); - assert!(Epoch::try_from_tau(&_31bit_tau, ¶ms).is_err()); - - let _33bit_tau = Tau(bitvec![u32, Msb0; - 1, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 1, - 0, 1, 1, 0 - ]); - assert!(Epoch::try_from_tau(&_33bit_tau, ¶ms).is_err()); - } - - #[test] - fn tau_roundtrip() { - let good_taus = vec![ - Tau::new_root(), - Tau::new(0), - Tau::new(1), - Tau::new(2), - Tau::new(42), - Tau::new(123456), - Tau::new(3292547435), - Tau::new(u32::MAX), - ]; - - for tau in good_taus { - let bytes = tau.to_bytes(); - let recovered = Tau::try_from_bytes(&bytes).unwrap(); - assert_eq!(tau, recovered); - } - - // more valid variants - let mut another_tau = Tau::new(u32::MAX); - another_tau.0.push(true); - another_tau.0.push(false); - another_tau.0.push(true); - - let bytes = another_tau.to_bytes(); - let recovered = Tau::try_from_bytes(&bytes).unwrap(); - assert_eq!(another_tau, recovered); - - // ensure there are no panics - let big_length_bytes = [255, 255, 255, 255, 42]; - assert!(Tau::try_from_bytes(&big_length_bytes).is_err()); - - assert!(Tau::try_from_bytes(&[]).is_err()); - assert!(Tau::try_from_bytes(&[1, 1, 1, 1]).is_err()); - assert!(Tau::try_from_bytes(&[0, 0, 0, 1]).is_err()); - assert!(Tau::try_from_bytes(&[1, 0, 0, 0]).is_err()); - assert!(Tau::try_from_bytes(&[1, 0, 0]).is_err()); - } -} diff --git a/common/crypto/dkg/src/dealing.rs b/common/crypto/dkg/src/dealing.rs index bf471d7e37..817bffdf12 100644 --- a/common/crypto/dkg/src/dealing.rs +++ b/common/crypto/dkg/src/dealing.rs @@ -3,9 +3,7 @@ use crate::bte::proof_chunking::ProofOfChunking; use crate::bte::proof_sharing::ProofOfSecretSharing; -use crate::bte::{ - encrypt_shares, proof_chunking, proof_sharing, Ciphertexts, Epoch, Params, PublicKey, -}; +use crate::bte::{encrypt_shares, proof_chunking, proof_sharing, Ciphertexts, Params, PublicKey}; use crate::error::DkgError; use crate::interpolation::polynomial::{Polynomial, PublicCoefficients}; use crate::interpolation::{ @@ -17,6 +15,13 @@ use rand_core::RngCore; use std::collections::BTreeMap; use zeroize::Zeroize; +#[derive(Debug)] +#[cfg_attr(test, derive(PartialEq, Eq))] +pub struct RecoveredVerificationKeys { + pub recovered_master: G2Projective, + pub recovered_partials: Vec, +} + #[derive(Debug)] #[cfg_attr(test, derive(PartialEq, Eq))] pub struct Dealing { @@ -34,7 +39,6 @@ impl Dealing { params: &Params, dealer_index: NodeIndex, threshold: Threshold, - epoch: Epoch, // BTreeMap ensures the keys are sorted by their indices receivers: &BTreeMap, prior_resharing_secret: Option, @@ -58,8 +62,7 @@ impl Dealing { .collect::>(); let ordered_public_keys = receivers.values().copied().collect::>(); - let (ciphertexts, hazmat) = - encrypt_shares(&remote_share_key_pairs, epoch, params, &mut rng); + let (ciphertexts, hazmat) = encrypt_shares(&remote_share_key_pairs, params, &mut rng); // create proofs of knowledge let chunking_instance = proof_chunking::Instance::new(&ordered_public_keys, &ciphertexts); @@ -108,7 +111,6 @@ impl Dealing { pub fn verify( &self, params: &Params, - epoch: Epoch, threshold: Threshold, receivers: &BTreeMap, prior_resharing_public: Option, @@ -134,7 +136,7 @@ impl Dealing { }); } - if !self.ciphertexts.verify_integrity(params, epoch) { + if !self.ciphertexts.verify_integrity(params) { return Err(DkgError::FailedCiphertextIntegrityCheck); } @@ -250,7 +252,7 @@ pub fn try_recover_verification_keys( dealings: &[Dealing], threshold: Threshold, receivers: &BTreeMap, -) -> Result<(G2Projective, Vec), DkgError> { +) -> Result { if dealings.is_empty() { return Err(DkgError::NoDealingsAvailable); } @@ -297,7 +299,10 @@ pub fn try_recover_verification_keys( .map(|index| interpolated_coefficients.evaluate_at(&Scalar::from(*index))) .collect(); - Ok((master_verification_key, verification_key_shares)) + Ok(RecoveredVerificationKeys { + recovered_master: master_verification_key, + recovered_partials: verification_key_shares, + }) } pub fn verify_verification_keys( @@ -369,32 +374,18 @@ mod tests { full_keys.push((dk, pk)) } - // start off in a defined epoch (i.e. not root); - let epoch = Epoch::new(2); - let dealings = node_indices .iter() .map(|&dealer_index| { - Dealing::create( - &mut rng, - ¶ms, - dealer_index, - threshold, - epoch, - &receivers, - None, - ) - .0 + Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0 }) .collect::>(); let mut derived_secrets = Vec::new(); for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { - dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); - let shares = dealings .iter() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap()) + .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); derived_secrets.push( combine_shares(shares, &receivers.keys().copied().collect::>()).unwrap(), @@ -408,8 +399,10 @@ mod tests { .unwrap(); // END OF SETUP - let (recovered_master, recovered_partials) = - try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); + let RecoveredVerificationKeys { + recovered_master, + recovered_partials, + } = try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); let g2 = G2Projective::generator(); assert_eq!(g2 * master_secret, recovered_master); @@ -437,27 +430,17 @@ mod tests { full_keys.push((dk, pk)) } - // start off in a defined epoch (i.e. not root); - let epoch = Epoch::new(2); - let dealings = node_indices .iter() .map(|&dealer_index| { - Dealing::create( - &mut rng, - ¶ms, - dealer_index, - threshold, - epoch, - &receivers, - None, - ) - .0 + Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0 }) .collect::>(); - let (recovered_master, recovered_partials) = - try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); + let RecoveredVerificationKeys { + recovered_master, + recovered_partials, + } = try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); assert!(verify_verification_keys( &recovered_master, @@ -478,7 +461,6 @@ mod tests { let parties = 5; let threshold = ((parties as f32 * 2.) / 3. + 1.) as Threshold; let node_indices = (1..=parties).collect::>(); - let epoch = Epoch::new(2); let mut receivers = BTreeMap::new(); for index in &node_indices { @@ -491,7 +473,6 @@ mod tests { ¶ms, node_indices[0], threshold, - epoch, &receivers, None, ); diff --git a/common/crypto/dkg/tests/integration.rs b/common/crypto/dkg/tests/integration.rs index 8e6c001385..2d25c31d40 100644 --- a/common/crypto/dkg/tests/integration.rs +++ b/common/crypto/dkg/tests/integration.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use bls12_381::{G2Projective, Scalar}; -use dkg::bte::{decrypt_share, keygen, setup, Epoch}; +use dkg::bte::{decrypt_share, keygen, setup}; +use dkg::dealing::RecoveredVerificationKeys; use dkg::interpolation::perform_lagrangian_interpolation_at_origin; use dkg::{combine_shares, try_recover_verification_keys, Dealing}; use rand_core::SeedableRng; @@ -32,9 +33,6 @@ fn single_sender() { full_keys.push((dk, pk)) } - // start off in a defined epoch (i.e. not root); - let epoch = Epoch::new(2); - // TODO: HERE BE SERIALIZATION / DESERIALIZATION THAT'S NOT IMPLEMENTED YET // verify remote proofs of key possession for key in full_keys.iter() { @@ -46,23 +44,20 @@ fn single_sender() { ¶ms, node_indices[0], threshold, - epoch, &receivers, None, ); dealing - .verify(¶ms, epoch, threshold, &receivers, None) + .verify(¶ms, threshold, &receivers, None) .unwrap(); // make sure each share is actually decryptable (even though proofs say they must be, perform this sanity check) for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { - dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); - let _recovered = decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap(); + let _recovered = decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap(); } // and for good measure, check that the dealer's share matches decryption result - let recovered_dealer = - decrypt_share(&full_keys[0].0, 0, &dealing.ciphertexts, epoch, None).unwrap(); + let recovered_dealer = decrypt_share(&full_keys[0].0, 0, &dealing.ciphertexts, None).unwrap(); assert_eq!(recovered_dealer, dealer_share.unwrap()); } @@ -87,9 +82,6 @@ fn full_threshold_secret_sharing() { full_keys.push((dk, pk)) } - // start off in a defined epoch (i.e. not root); - let epoch = Epoch::new(2); - // TODO: HERE BE SERIALIZATION / DESERIALIZATION THAT'S NOT IMPLEMENTED YET // verify remote proofs of key possession for key in full_keys.iter() { @@ -99,37 +91,28 @@ fn full_threshold_secret_sharing() { let dealings = node_indices .iter() .map(|&dealer_index| { - Dealing::create( - &mut rng, - ¶ms, - dealer_index, - threshold, - epoch, - &receivers, - None, - ) - .0 + Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0 }) .collect::>(); for dealing in dealings.iter() { dealing - .verify(¶ms, epoch, threshold, &receivers, None) + .verify(¶ms, threshold, &receivers, None) .unwrap(); } // recover verification keys - let (recovered_master, recovered_partials) = - try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); + let RecoveredVerificationKeys { + recovered_master, + recovered_partials, + } = try_recover_verification_keys(&dealings, threshold, &receivers).unwrap(); let g2 = G2Projective::generator(); let mut derived_secrets = Vec::new(); for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { - dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); - let shares = dealings .iter() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap()) + .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); // we know dealer_share matches, but it would be inconvenient to try to put them in here, @@ -183,36 +166,24 @@ fn full_threshold_secret_resharing() { full_keys.push((dk, pk)) } - // start off in a defined epoch (i.e. not root); - let epoch = Epoch::new(2); - let first_dealings = node_indices .iter() .map(|&dealer_index| { - Dealing::create( - &mut rng, - ¶ms, - dealer_index, - threshold, - epoch, - &receivers, - None, - ) - .0 + Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0 }) .collect::>(); // recover verification keys - let (public_original_master, recovered_partials) = - try_recover_verification_keys(&first_dealings, threshold, &receivers).unwrap(); + let RecoveredVerificationKeys { + recovered_master: public_original_master, + recovered_partials, + } = try_recover_verification_keys(&first_dealings, threshold, &receivers).unwrap(); let mut derived_secrets = Vec::new(); for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { - dk.try_update_to(epoch, ¶ms, &mut rng).unwrap(); - let shares = first_dealings .iter() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, epoch, None).unwrap()) + .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = @@ -227,8 +198,6 @@ fn full_threshold_secret_resharing() { ]) .unwrap(); - let next_epoch = Epoch::new(3); - // attempt to create resharing dealings! let resharing_dealings = node_indices .iter() @@ -239,7 +208,6 @@ fn full_threshold_secret_resharing() { ¶ms, dealer_index, threshold, - next_epoch, &receivers, Some(*prior_secret), ) @@ -249,21 +217,21 @@ fn full_threshold_secret_resharing() { for (reshared_dealing, prior_vk) in resharing_dealings.iter().zip(recovered_partials.iter()) { reshared_dealing - .verify(¶ms, next_epoch, threshold, &receivers, Some(*prior_vk)) + .verify(¶ms, threshold, &receivers, Some(*prior_vk)) .unwrap(); } // recover verification keys - let (public_reshared_master, reshared_partials) = - try_recover_verification_keys(&resharing_dealings, threshold, &receivers).unwrap(); + let RecoveredVerificationKeys { + recovered_master: public_reshared_master, + recovered_partials: reshared_partials, + } = try_recover_verification_keys(&resharing_dealings, threshold, &receivers).unwrap(); let mut reshared_secrets = Vec::new(); for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { - dk.try_update_to(next_epoch, ¶ms, &mut rng).unwrap(); - let shares = resharing_dealings .iter() - .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, next_epoch, None).unwrap()) + .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) .collect(); let recovered_secret = diff --git a/common/network-defaults/envs/mainnet.env b/common/network-defaults/envs/mainnet.env deleted file mode 100644 index dce1c3c9c0..0000000000 --- a/common/network-defaults/envs/mainnet.env +++ /dev/null @@ -1,20 +0,0 @@ -CONFIGURED=true - -RUST_LOG=info -RUST_BACKTRACE=1 - -BECH32_PREFIX=n -MIX_DENOM=unym -MIX_DENOM_DISPLAY=nym -STAKE_DENOM=unyx -STAKE_DENOM_DISPLAY=nyx -DENOMS_EXPONENT=6 -MIXNET_CONTRACT_ADDRESS=n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g -VESTING_CONTRACT_ADDRESS=n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw -BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 -MULTISIG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 -REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy -STATISTICS_SERVICE_DOMAIN_ADDRESS="http://127.0.0.1:8090" -NYMD_VALIDATOR="https://rpc.nyx.nodes.guru/" -API_VALIDATOR="https://validator.nymtech.net/api/" diff --git a/common/network-defaults/envs/qa.env b/common/network-defaults/envs/qa.env deleted file mode 100644 index 842fb54c4d..0000000000 --- a/common/network-defaults/envs/qa.env +++ /dev/null @@ -1,20 +0,0 @@ -CONFIGURED=true - -RUST_LOG=info -RUST_BACKTRACE=1 - -BECH32_PREFIX=n -MIX_DENOM=unym -MIX_DENOM_DISPLAY=nym -STAKE_DENOM=unyx -STAKE_DENOM_DISPLAY=nyx -DENOMS_EXPONENT=6 -MIXNET_CONTRACT_ADDRESS=n1suhgf5svhu4usrurvxzlgn54ksxmn8gljarjtxqnapv8kjnp4nrsd3qaep -VESTING_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sjkxkav -BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 -COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw -MULTISIG_CONTRACT_ADDRESS=n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs -REWARDING_VALIDATOR_ADDRESS=n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq -STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" -NYMD_VALIDATOR="https://qa-validator.nymtech.net" -API_VALIDATOR="https://qa-validator-api.nymtech.net/api" diff --git a/common/network-defaults/src/lib.rs b/common/network-defaults/src/lib.rs index c600199140..d7b01622b1 100644 --- a/common/network-defaults/src/lib.rs +++ b/common/network-defaults/src/lib.rs @@ -36,6 +36,7 @@ pub struct NymContracts { pub bandwidth_claim_contract_address: Option, pub coconut_bandwidth_contract_address: Option, pub multisig_contract_address: Option, + pub coconut_dkg_contract_address: Option, } // I wanted to use the simpler `NetworkDetails` name, but there's a clash @@ -96,6 +97,9 @@ impl NymNetworkDetails { .with_multisig_contract(Some( var(var_names::MULTISIG_CONTRACT_ADDRESS).expect("multisig contract not set"), )) + .with_coconut_dkg_contract(Some( + var(var_names::COCONUT_DKG_CONTRACT_ADDRESS).expect("coconut dkg contract not set"), + )) } pub fn new_mainnet() -> Self { @@ -121,6 +125,9 @@ impl NymNetworkDetails { mainnet::COCONUT_BANDWIDTH_CONTRACT_ADDRESS, ), multisig_contract_address: parse_optional_str(mainnet::MULTISIG_CONTRACT_ADDRESS), + coconut_dkg_contract_address: parse_optional_str( + mainnet::COCONUT_DKG_CONTRACT_ADDRESS, + ), }, } } @@ -190,6 +197,12 @@ impl NymNetworkDetails { self.contracts.multisig_contract_address = contract.map(Into::into); self } + + #[must_use] + pub fn with_coconut_dkg_contract>(mut self, contract: Option) -> Self { + self.contracts.coconut_dkg_contract_address = contract.map(Into::into); + self + } } #[derive(Debug, Copy, Serialize, Deserialize, Clone, PartialEq, Eq)] diff --git a/common/network-defaults/src/mainnet.rs b/common/network-defaults/src/mainnet.rs index ed415e2622..59242376f9 100644 --- a/common/network-defaults/src/mainnet.rs +++ b/common/network-defaults/src/mainnet.rs @@ -10,7 +10,7 @@ pub const MIX_DENOM: DenomDetails = DenomDetails::new("unym", "nym", 6); pub const STAKE_DENOM: DenomDetails = DenomDetails::new("unyx", "nyx", 6); pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = - "n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g"; + "n17srjznxl9dvzdkpwpw24gg668wc73val88a6m5ajg6ankwvz9wtst0cznr"; pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw"; pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = @@ -18,6 +18,7 @@ pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; +pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] = hex_literal::hex!("0000000000000000000000000000000000000000"); pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = @@ -84,6 +85,10 @@ pub fn export_to_env() { var_names::MULTISIG_CONTRACT_ADDRESS, MULTISIG_CONTRACT_ADDRESS, ); + set_var_to_default( + var_names::COCONUT_DKG_CONTRACT_ADDRESS, + COCONUT_DKG_CONTRACT_ADDRESS, + ); set_var_to_default( var_names::REWARDING_VALIDATOR_ADDRESS, REWARDING_VALIDATOR_ADDRESS, @@ -124,6 +129,10 @@ pub fn export_to_env_if_not_set() { var_names::MULTISIG_CONTRACT_ADDRESS, MULTISIG_CONTRACT_ADDRESS, ); + set_var_conditionally_to_default( + var_names::COCONUT_DKG_CONTRACT_ADDRESS, + COCONUT_DKG_CONTRACT_ADDRESS, + ); set_var_conditionally_to_default( var_names::REWARDING_VALIDATOR_ADDRESS, REWARDING_VALIDATOR_ADDRESS, diff --git a/common/network-defaults/src/var_names.rs b/common/network-defaults/src/var_names.rs index d1f3cf2736..4e62e444cc 100644 --- a/common/network-defaults/src/var_names.rs +++ b/common/network-defaults/src/var_names.rs @@ -15,6 +15,7 @@ pub const VESTING_CONTRACT_ADDRESS: &str = "VESTING_CONTRACT_ADDRESS"; pub const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = "BANDWIDTH_CLAIM_CONTRACT_ADDRESS"; pub const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "COCONUT_BANDWIDTH_CONTRACT_ADDRESS"; pub const MULTISIG_CONTRACT_ADDRESS: &str = "MULTISIG_CONTRACT_ADDRESS"; +pub const COCONUT_DKG_CONTRACT_ADDRESS: &str = "COCONUT_DKG_CONTRACT_ADDRESS"; pub const REWARDING_VALIDATOR_ADDRESS: &str = "REWARDING_VALIDATOR_ADDRESS"; pub const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "STATISTICS_SERVICE_DOMAIN_ADDRESS"; pub const NYMD_VALIDATOR: &str = "NYMD_VALIDATOR"; diff --git a/common/nymcoconut/Cargo.toml b/common/nymcoconut/Cargo.toml index 60b916e58f..5dd70dbd2c 100644 --- a/common/nymcoconut/Cargo.toml +++ b/common/nymcoconut/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bls12_381 = { version = "0.5", default-features = false, features = ["pairings", "alloc", "experimental"] } +bls12_381 = { git = "https://github.com/jstuczyn/bls12_381", branch ="gt-serialisation", default-features = false, features = ["pairings", "alloc", "experimental"] } itertools = "0.10" digest = "0.9" rand = "0.8" @@ -17,17 +17,21 @@ serde_derive = "1.0" bs58 = "0.4.0" sha2 = "0.9" +dkg = { path = "../crypto/dkg" } +pemstore = { path = "../pemstore" } + [dependencies.ff] -version = "0.10" +version = "0.11" default-features = false [dependencies.group] -version = "0.10" +version = "0.11" default-features = false [dev-dependencies] criterion = { version="0.3", features=["html_reports"] } doc-comment = "0.3" +rand_chacha = "0.3" [dev-dependencies.bincode] version = "1" diff --git a/common/nymcoconut/src/lib.rs b/common/nymcoconut/src/lib.rs index ab1cab7cf7..a98b546fc1 100644 --- a/common/nymcoconut/src/lib.rs +++ b/common/nymcoconut/src/lib.rs @@ -17,9 +17,11 @@ pub use scheme::issuance::prepare_blind_sign; pub use scheme::issuance::BlindSignRequest; pub use scheme::keygen::ttp_keygen; pub use scheme::keygen::KeyPair; +pub use scheme::keygen::SecretKey; pub use scheme::keygen::VerificationKey; pub use scheme::setup::setup; pub use scheme::setup::Parameters; +pub use scheme::verification::check_vk_pairing; pub use scheme::verification::prove_bandwidth_credential; pub use scheme::verification::verify_credential; pub use scheme::verification::Theta; diff --git a/common/nymcoconut/src/scheme/keygen.rs b/common/nymcoconut/src/scheme/keygen.rs index bd7dd6cf85..ea9c88aa65 100644 --- a/common/nymcoconut/src/scheme/keygen.rs +++ b/common/nymcoconut/src/scheme/keygen.rs @@ -9,6 +9,7 @@ use std::convert::TryInto; use bls12_381::{G1Projective, G2Projective, Scalar}; use group::Curve; +use pemstore::traits::{PemStorableKey, PemStorableKeyPair}; use serde_derive::{Deserialize, Serialize}; use crate::error::{CoconutError, Result}; @@ -29,6 +30,22 @@ pub struct SecretKey { pub(crate) ys: Vec, } +impl PemStorableKey for SecretKey { + type Error = CoconutError; + + fn pem_type() -> &'static str { + "COCONUT SECRET KEY" + } + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn from_bytes(bytes: &[u8]) -> std::result::Result { + Self::from_bytes(bytes) + } +} + impl TryFrom<&[u8]> for SecretKey { type Error = CoconutError; @@ -71,6 +88,12 @@ impl TryFrom<&[u8]> for SecretKey { } impl SecretKey { + /// Following a (distributed) key generation process, scalar values can be obtained + /// outside of the normal key generation process. + pub fn create_from_raw(x: Scalar, ys: Vec) -> Self { + Self { x, ys } + } + /// Derive verification key using this secret key. pub fn verification_key(&self, params: &Parameters) -> VerificationKey { let g1 = params.gen1(); @@ -122,6 +145,22 @@ pub struct VerificationKey { pub(crate) beta_g2: Vec, } +impl PemStorableKey for VerificationKey { + type Error = CoconutError; + + fn pem_type() -> &'static str { + "COCONUT VERIFICATION KEY" + } + + fn to_bytes(&self) -> Vec { + self.to_bytes() + } + + fn from_bytes(bytes: &[u8]) -> std::result::Result { + Self::from_bytes(bytes) + } +} + impl TryFrom<&[u8]> for VerificationKey { type Error = CoconutError; @@ -360,9 +399,34 @@ pub struct KeyPair { pub index: Option, } +impl PemStorableKeyPair for KeyPair { + type PrivatePemKey = SecretKey; + type PublicPemKey = VerificationKey; + + fn private_key(&self) -> &Self::PrivatePemKey { + &self.secret_key + } + + fn public_key(&self) -> &Self::PublicPemKey { + &self.verification_key + } + + fn from_keys(secret_key: Self::PrivatePemKey, verification_key: Self::PublicPemKey) -> Self { + Self::from_keys(secret_key, verification_key) + } +} + impl KeyPair { const MARKER_BYTES: &'static [u8] = b"coconutkeypair"; + pub fn from_keys(secret_key: SecretKey, verification_key: VerificationKey) -> Self { + Self { + secret_key, + verification_key, + index: None, + } + } + pub fn secret_key(&self) -> SecretKey { self.secret_key.clone() } diff --git a/common/nymcoconut/src/scheme/verification.rs b/common/nymcoconut/src/scheme/verification.rs index 9f7aba12b5..96ef28e9ca 100644 --- a/common/nymcoconut/src/scheme/verification.rs +++ b/common/nymcoconut/src/scheme/verification.rs @@ -212,6 +212,39 @@ pub fn check_bilinear_pairing(p: &G1Affine, q: &G2Prepared, r: &G1Affine, s: &G2 multi_miller.final_exponentiation().is_identity().into() } +pub fn check_vk_pairing( + params: &Parameters, + dkg_values: &[G2Projective], + vk: &VerificationKey, +) -> bool { + let values_len = dkg_values.len(); + if values_len == 0 || values_len - 1 != vk.beta_g1.len() || values_len - 1 != vk.beta_g2.len() { + return false; + } + if vk.alpha != *dkg_values.last().unwrap() { + return false; + } + if dkg_values + .iter() + .zip(vk.beta_g2.iter()) + .any(|(dkg_beta, vk_beta)| dkg_beta != vk_beta) + { + return false; + } + if vk.beta_g1.iter().zip(vk.beta_g2.iter()).any(|(g1, g2)| { + !check_bilinear_pairing( + params.gen1(), + &G2Prepared::from(g2.to_affine()), + &g1.to_affine(), + params.prepared_miller_g2(), + ) + }) { + return false; + } + + true +} + pub fn verify_credential( params: &Parameters, verification_key: &VerificationKey, @@ -284,6 +317,15 @@ mod tests { use super::*; + #[test] + fn vk_pairing() { + let params = setup(2).unwrap(); + let vk = keygen(¶ms).verification_key(); + let mut dkg_values = vk.beta_g2.clone(); + dkg_values.push(vk.alpha); + assert!(check_vk_pairing(¶ms, &dkg_values, &vk)); + } + #[test] fn theta_bytes_roundtrip() { let params = setup(2).unwrap(); diff --git a/common/nymcoconut/src/tests/e2e.rs b/common/nymcoconut/src/tests/e2e.rs index ce4fe2ab57..235b2adbab 100644 --- a/common/nymcoconut/src/tests/e2e.rs +++ b/common/nymcoconut/src/tests/e2e.rs @@ -1,11 +1,16 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::tests::helpers::tests::generate_dkg_keys; use crate::{ - aggregate_verification_keys, setup, tests::helpers::theta_from_keys_and_attributes, ttp_keygen, - verify_credential, CoconutError, VerificationKey, + aggregate_verification_keys, setup, tests::helpers::*, ttp_keygen, verify_credential, + CoconutError, VerificationKey, }; #[test] -fn main() -> Result<(), CoconutError> { +fn keygen() -> Result<(), CoconutError> { let params = setup(5)?; + let node_indices = vec![15u64, 248, 33521]; let public_attributes = params.n_random_scalars(2); @@ -18,10 +23,52 @@ fn main() -> Result<(), CoconutError> { .collect(); // aggregate verification keys - let verification_key = aggregate_verification_keys(&verification_keys, Some(&[1, 2, 3]))?; + let verification_key = aggregate_verification_keys(&verification_keys, Some(&node_indices))?; // Generate cryptographic material to verify them - let theta = theta_from_keys_and_attributes(¶ms, &coconut_keypairs, &public_attributes)?; + let theta = theta_from_keys_and_attributes( + ¶ms, + &coconut_keypairs, + &node_indices, + &public_attributes, + )?; + + // Verify credentials + assert!(verify_credential( + ¶ms, + &verification_key, + &theta, + &public_attributes, + )); + + Ok(()) +} + +#[test] +fn dkg() -> Result<(), CoconutError> { + let params = setup(5)?; + let node_indices = vec![15u64, 248, 33521]; + + let public_attributes = params.n_random_scalars(2); + + // generate_keys + let coconut_keypairs = generate_dkg_keys(5, &node_indices); + + let verification_keys: Vec = coconut_keypairs + .iter() + .map(|keypair| keypair.verification_key()) + .collect(); + + // aggregate verification keys + let verification_key = aggregate_verification_keys(&verification_keys, Some(&node_indices))?; + + // Generate cryptographic material to verify them + let theta = theta_from_keys_and_attributes( + ¶ms, + &coconut_keypairs, + &node_indices, + &public_attributes, + )?; // Verify credentials assert!(verify_credential( diff --git a/common/nymcoconut/src/tests/helpers.rs b/common/nymcoconut/src/tests/helpers.rs index 7d9c7098b9..ee7e418788 100644 --- a/common/nymcoconut/src/tests/helpers.rs +++ b/common/nymcoconut/src/tests/helpers.rs @@ -3,10 +3,12 @@ use crate::*; use itertools::izip; +use std::fmt::Debug; pub fn theta_from_keys_and_attributes( params: &Parameters, coconut_keypairs: &Vec, + indices: &[scheme::SignerIndex], public_attributes: &Vec, ) -> Result { let serial_number = params.random_scalar(); @@ -23,12 +25,7 @@ pub fn theta_from_keys_and_attributes( .collect(); // aggregate verification keys - let indices: Vec = coconut_keypairs - .iter() - .enumerate() - .map(|(idx, _)| (idx + 1) as u64) - .collect(); - let verification_key = aggregate_verification_keys(&verification_keys, Some(&indices))?; + let verification_key = aggregate_verification_keys(&verification_keys, Some(indices))?; // generate blinded signatures let mut blinded_signatures = Vec::new(); @@ -44,26 +41,31 @@ pub fn theta_from_keys_and_attributes( } // Unblind - let unblinded_signatures: Vec = - izip!(blinded_signatures.iter(), verification_keys.iter()) - .map(|(s, vk)| { - s.unblind( - params, - vk, - &private_attributes, - public_attributes, - &blind_sign_request.get_commitment_hash(), - &commitments_openings, - ) - .unwrap() - }) - .collect(); + let unblinded_signatures: Vec<(scheme::SignerIndex, Signature)> = izip!( + indices.iter(), + blinded_signatures.iter(), + verification_keys.iter() + ) + .map(|(idx, s, vk)| { + ( + *idx, + s.unblind( + params, + vk, + &private_attributes, + public_attributes, + &blind_sign_request.get_commitment_hash(), + &commitments_openings, + ) + .unwrap(), + ) + }) + .collect(); // Aggregate signatures let signature_shares: Vec = unblinded_signatures .iter() - .enumerate() - .map(|(idx, signature)| SignatureShare::new(*signature, (idx + 1) as u64)) + .map(|(idx, signature)| SignatureShare::new(*signature, *idx)) .collect(); let mut attributes = Vec::with_capacity(private_attributes.len() + public_attributes.len()); @@ -85,3 +87,79 @@ pub fn theta_from_keys_and_attributes( Ok(theta) } + +pub fn transpose_matrix(matrix: Vec>) -> Vec> { + let len = matrix[0].len(); + let mut iters: Vec<_> = matrix.into_iter().map(|d| d.into_iter()).collect(); + (0..len) + .map(|_| { + iters + .iter_mut() + .map(|it| it.next().unwrap()) + .collect::>() + }) + .collect::>() +} + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::{KeyPair, Parameters, SecretKey}; + use bls12_381::Scalar; + use dkg::{bte::decrypt_share, combine_shares, Dealing, NodeIndex}; + use rand_chacha::rand_core::SeedableRng; + + pub fn generate_dkg_secrets(node_indices: &[NodeIndex]) -> Vec { + let dummy_seed = [42u8; 32]; + let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed); + let params = dkg::bte::setup(); + + // the simplest possible case + let threshold = 2; + + let mut receivers = std::collections::BTreeMap::new(); + let mut full_keys = Vec::new(); + for index in node_indices { + let (dk, pk) = dkg::bte::keygen(¶ms, &mut rng); + receivers.insert(*index, *pk.public_key()); + full_keys.push((dk, pk)) + } + let dealings = node_indices + .iter() + .map(|&dealer_index| { + Dealing::create(&mut rng, ¶ms, dealer_index, threshold, &receivers, None).0 + }) + .collect::>(); + let mut derived_secrets = Vec::new(); + for (i, (ref mut dk, _)) in full_keys.iter_mut().enumerate() { + let shares = dealings + .iter() + .map(|dealing| decrypt_share(dk, i, &dealing.ciphertexts, None).unwrap()) + .collect(); + + let recovered_secret = + combine_shares(shares, &receivers.keys().copied().collect::>()).unwrap(); + + derived_secrets.push(recovered_secret) + } + derived_secrets + } + pub fn generate_dkg_keys(num_attributes: u32, node_indices: &[NodeIndex]) -> Vec { + let params = Parameters::new(num_attributes).unwrap(); + let mut all_secrets = vec![]; + for _ in 0..num_attributes { + let secrets = generate_dkg_secrets(node_indices); + all_secrets.push(secrets); + } + let signers = transpose_matrix(all_secrets); + signers + .into_iter() + .map(|mut secrets| { + let x = secrets.pop().unwrap(); + let sk = SecretKey::create_from_raw(x, secrets); + let vk = sk.verification_key(¶ms); + KeyPair::from_keys(sk, vk) + }) + .collect() + } +} diff --git a/common/nymcoconut/src/utils.rs b/common/nymcoconut/src/utils.rs index 764cfe090c..9fec3b334c 100644 --- a/common/nymcoconut/src/utils.rs +++ b/common/nymcoconut/src/utils.rs @@ -32,7 +32,7 @@ impl Polynomial { Scalar::zero() // if x is zero then we can ignore most of the expensive computation and // just return the last term of the polynomial - } else if x.is_zero() { + } else if x.is_zero().into() { // we checked that coefficients are not empty so unwrap here is fine *self.coefficients.first().unwrap() } else { diff --git a/common/nymsphinx/forwarding/src/packet.rs b/common/nymsphinx/forwarding/src/packet.rs index 62df1028e3..8c22b43f5c 100644 --- a/common/nymsphinx/forwarding/src/packet.rs +++ b/common/nymsphinx/forwarding/src/packet.rs @@ -5,7 +5,7 @@ use nymsphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressEr use nymsphinx_params::{PacketMode, PacketSize}; use nymsphinx_types::SphinxPacket; use std::convert::TryFrom; -use std::fmt::{self, Display, Formatter}; +use std::fmt::{self, Debug, Display, Formatter}; #[derive(Debug)] pub enum MixPacketFormattingError { @@ -50,6 +50,19 @@ pub struct MixPacket { packet_mode: PacketMode, } +impl Debug for MixPacket { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + "MixPacket to {:?} with packet_mode {:?}. Sphinx header: {:?}, payload length: {}", + self.next_hop, + self.packet_mode, + self.sphinx_packet.header, + self.sphinx_packet.payload.len() + ) + } +} + impl MixPacket { pub fn new( next_hop: NymNodeRoutingAddress, diff --git a/common/socks5/proxy-helpers/src/connection_controller.rs b/common/socks5/proxy-helpers/src/connection_controller.rs index 16b370058d..f0f552d83c 100644 --- a/common/socks5/proxy-helpers/src/connection_controller.rs +++ b/common/socks5/proxy-helpers/src/connection_controller.rs @@ -1,14 +1,18 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use client_connections::ClosedConnectionSender; +use client_connections::{ConnectionCommand, ConnectionCommandSender}; use futures::channel::mpsc; use futures::StreamExt; use log::*; use ordered_buffer::{OrderedMessage, OrderedMessageBuffer, ReadContiguousData}; use socks5_requests::ConnectionId; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + time::Duration, +}; use task::ShutdownListener; +use tokio::time; /// A generic message produced after reading from a socket/connection. It includes data that was /// actually read alongside boolean indicating whether the connection got closed so that @@ -64,6 +68,12 @@ impl ActiveConnection { } } +#[derive(PartialEq, Eq)] +pub enum BroadcastActiveConnections { + On, + Off, +} + /// Controller represents a way of managing multiple open connections that are used for socks5 /// proxy. pub struct Controller { @@ -75,7 +85,11 @@ pub struct Controller { recently_closed: HashSet, // Broadcast closed connections - closed_connection_tx: ClosedConnectionSender, + client_connection_tx: ConnectionCommandSender, + + // The controller can broadcast active connections. This is useful in the network-requester + // where its used to query the client for lane queue lengths + broadcast_connections: BroadcastActiveConnections, // TODO: this can potentially be abused to ddos and kill provider. Not sure at this point // how to handle it more gracefully @@ -89,7 +103,8 @@ pub struct Controller { impl Controller { pub fn new( - closed_connection_tx: ClosedConnectionSender, + client_connection_tx: ConnectionCommandSender, + broadcast_connections: BroadcastActiveConnections, shutdown: ShutdownListener, ) -> (Self, ControllerSender) { let (sender, receiver) = mpsc::unbounded(); @@ -98,7 +113,8 @@ impl Controller { active_connections: HashMap::new(), receiver, recently_closed: HashSet::new(), - closed_connection_tx, + client_connection_tx, + broadcast_connections, pending_messages: HashMap::new(), shutdown, }, @@ -137,7 +153,25 @@ impl Controller { self.recently_closed.insert(conn_id); // Announce closed connections, currently used by the `OutQueueControl`. - self.closed_connection_tx.unbounded_send(conn_id).unwrap(); + if let Err(err) = self + .client_connection_tx + .unbounded_send(ConnectionCommand::Close(conn_id)) + { + if self.shutdown.is_shutdown_poll() { + log::debug!("Failed to send: {}", err); + } else { + log::error!("Failed to send: {}", err); + } + } + } + + fn broadcast_active_connections(&mut self) { + // What about the recently closed ones? Hopefully we can ignore them ... + let conn_ids = self.active_connections.keys().copied().collect(); + + self.client_connection_tx + .unbounded_send(ConnectionCommand::ActiveConnections(conn_ids)) + .unwrap(); } fn send_to_connection(&mut self, conn_id: ConnectionId, payload: Vec, is_closed: bool) { @@ -196,6 +230,8 @@ impl Controller { } pub async fn run(&mut self) { + let mut interval = time::interval(Duration::from_millis(500)); + loop { tokio::select! { command = self.receiver.next() => match command { @@ -210,9 +246,17 @@ impl Controller { log::trace!("SOCKS5 Controller: Stopping since channel closed"); break; } - } + }, + _ = interval.tick() => { + if self.broadcast_connections == BroadcastActiveConnections::On { + self.broadcast_active_connections(); + } + }, } } + tokio::time::timeout(Duration::from_secs(5), self.shutdown.recv()) + .await + .expect("Task stopped without shutdown called"); assert!(self.shutdown.is_shutdown_poll()); log::debug!("SOCKS5 Controller: Exiting"); } diff --git a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs index e116ba1dc5..f2ebc2cd9b 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/inbound.rs @@ -5,31 +5,38 @@ use super::MixProxySender; use super::SHUTDOWN_TIMEOUT; use crate::available_reader::AvailableReader; use bytes::Bytes; +use client_connections::LaneQueueLengths; +use client_connections::TransmissionLane; use futures::FutureExt; use futures::StreamExt; use log::*; use ordered_buffer::OrderedMessageSender; use socks5_requests::ConnectionId; +use std::fmt::Debug; +use std::time::Duration; use std::{io, sync::Arc}; use task::ShutdownListener; use tokio::select; use tokio::{net::tcp::OwnedReadHalf, sync::Notify, time::sleep}; -fn send_empty_close( +async fn send_empty_close( connection_id: ConnectionId, message_sender: &mut OrderedMessageSender, mix_sender: &MixProxySender, adapter_fn: F, ) where F: Fn(ConnectionId, Vec, bool) -> S, + S: Debug, { let ordered_msg = message_sender.wrap_message(Vec::new()).into_bytes(); mix_sender - .unbounded_send(adapter_fn(connection_id, ordered_msg, true)) - .unwrap(); + .send(adapter_fn(connection_id, ordered_msg, true)) + .await + .expect("BatchRealMessageReceiver has stopped receiving!"); } -fn deal_with_data( +#[allow(clippy::too_many_arguments)] +async fn deal_with_data( read_data: Option>, local_destination_address: &str, remote_source_address: &str, @@ -37,9 +44,11 @@ fn deal_with_data( message_sender: &mut OrderedMessageSender, mix_sender: &MixProxySender, adapter_fn: F, + lane_queue_lengths: Option, ) -> bool where F: Fn(ConnectionId, Vec, bool) -> S, + S: Debug, { let (read_data, is_finished) = match read_data { Some(data) => match data { @@ -67,9 +76,23 @@ where "pushing data down the input sender: size: {}", ordered_msg.len() ); + + // If we are closing the socket, wait until the data has passed `OutQueueControl` and the lane + // is empty, otherwise just wait until we are reasonably close to finish sending as a way to + // throttle the incoming data. + if let Some(lane_queue_lengths) = lane_queue_lengths { + if is_finished { + wait_until_lane_empty(lane_queue_lengths, connection_id).await; + } else { + // We allow a bit of slack when this is not the last msg + wait_until_lane_almost_empty(lane_queue_lengths, connection_id).await; + } + } + mix_sender - .unbounded_send(adapter_fn(connection_id, ordered_msg, is_finished)) - .unwrap(); + .send(adapter_fn(connection_id, ordered_msg, is_finished)) + .await + .expect("InputMessageReceiver has stopped receiving!"); if is_finished { // technically we already informed it when we sent the message to mixnet above @@ -79,6 +102,55 @@ where is_finished } +async fn wait_until_lane_empty(lane_queue_lengths: LaneQueueLengths, connection_id: u64) { + if tokio::time::timeout( + Duration::from_secs(2 * 60), + wait_for_lane( + lane_queue_lengths, + connection_id, + 0, + Duration::from_millis(500), + ), + ) + .await + .is_err() + { + log::warn!("Wait until lane empty timed out"); + } +} + +async fn wait_until_lane_almost_empty(lane_queue_lengths: LaneQueueLengths, connection_id: u64) { + if tokio::time::timeout( + Duration::from_secs(2 * 60), + wait_for_lane( + lane_queue_lengths, + connection_id, + 10, + Duration::from_millis(100), + ), + ) + .await + .is_err() + { + log::debug!("Wait until lane almost empty timed out"); + } +} + +async fn wait_for_lane( + lane_queue_lengths: LaneQueueLengths, + connection_id: u64, + queue_length_threshold: usize, + sleep_duration: Duration, +) { + while let Some(queue) = lane_queue_lengths.get(&TransmissionLane::ConnectionId(connection_id)) { + if queue > queue_length_threshold { + sleep(sleep_duration).await; + } else { + break; + } + } +} + #[allow(clippy::too_many_arguments)] pub(super) async fn run_inbound( mut reader: OwnedReadHalf, @@ -88,10 +160,12 @@ pub(super) async fn run_inbound( mix_sender: MixProxySender, adapter_fn: F, shutdown_notify: Arc, + lane_queue_lengths: Option, mut shutdown_listener: ShutdownListener, ) -> OwnedReadHalf where F: Fn(ConnectionId, Vec, bool) -> S + Send + 'static, + S: Debug, { let mut available_reader = AvailableReader::new(&mut reader); let mut message_sender = OrderedMessageSender::new(); @@ -102,15 +176,27 @@ where loop { select! { read_data = &mut available_reader.next() => { - if deal_with_data(read_data, &local_destination_address, &remote_source_address, connection_id, &mut message_sender, &mix_sender, &adapter_fn) { + if deal_with_data( + read_data, + &local_destination_address, + &remote_source_address, + connection_id, + &mut message_sender, + &mix_sender, + &adapter_fn, + lane_queue_lengths.clone() + ).await { break } } _ = &mut shutdown_future => { - debug!("closing inbound proxy after outbound was closed {:?} ago", SHUTDOWN_TIMEOUT); + debug!( + "closing inbound proxy after outbound was closed {:?} ago", + SHUTDOWN_TIMEOUT + ); // inform remote just in case it was closed because of lack of heartbeat. // worst case the remote will just have couple of false negatives - send_empty_close(connection_id, &mut message_sender, &mix_sender, &adapter_fn); + send_empty_close(connection_id, &mut message_sender, &mix_sender, &adapter_fn).await; break; } _ = shutdown_listener.recv() => { @@ -122,5 +208,6 @@ where trace!("{} - inbound closed", connection_id); shutdown_notify.notify_one(); + shutdown_listener.mark_as_success(); reader } diff --git a/common/socks5/proxy-helpers/src/proxy_runner/mod.rs b/common/socks5/proxy-helpers/src/proxy_runner/mod.rs index 1d5b493ced..defcbf4f5f 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/mod.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/mod.rs @@ -2,8 +2,9 @@ // SPDX-License-Identifier: Apache-2.0 use crate::connection_controller::ConnectionReceiver; -use futures::channel::mpsc; +use client_connections::LaneQueueLengths; use socks5_requests::ConnectionId; +use std::fmt::Debug; use std::{sync::Arc, time::Duration}; use task::ShutdownListener; use tokio::{net::TcpStream, sync::Notify}; @@ -29,7 +30,8 @@ impl From<(Vec, bool)> for ProxyMessage { } } -pub type MixProxySender = mpsc::UnboundedSender; +pub type MixProxySender = tokio::sync::mpsc::Sender; +pub type MixProxyReader = tokio::sync::mpsc::Receiver; // TODO: when we finally get to implementing graceful shutdown, // on Drop this guy should tell the remote that it's closed now @@ -45,6 +47,7 @@ pub struct ProxyRunner { local_destination_address: String, remote_source_address: String, connection_id: ConnectionId, + lane_queue_lengths: Option, // Listens to shutdown commands from higher up shutdown_listener: ShutdownListener, @@ -52,8 +55,9 @@ pub struct ProxyRunner { impl ProxyRunner where - S: Send + 'static, + S: Debug + Send + 'static, { + #[allow(clippy::too_many_arguments)] pub fn new( socket: TcpStream, local_destination_address: String, // addresses are provided for better logging @@ -61,6 +65,7 @@ where mix_receiver: ConnectionReceiver, mix_sender: MixProxySender, connection_id: ConnectionId, + lane_queue_lengths: Option, shutdown_listener: ShutdownListener, ) -> Self { ProxyRunner { @@ -70,6 +75,7 @@ where local_destination_address, remote_source_address, connection_id, + lane_queue_lengths, shutdown_listener, } } @@ -78,7 +84,7 @@ where // request/response as required by entity running particular side of the proxy. pub async fn run(mut self, adapter_fn: F) -> Self where - F: Fn(ConnectionId, Vec, bool) -> S + Send + 'static, + F: Fn(ConnectionId, Vec, bool) -> S + Send + Sync + 'static, { let (read_half, write_half) = self.socket.take().unwrap().into_split(); let shutdown_notify = Arc::new(Notify::new()); @@ -92,6 +98,7 @@ where self.mix_sender.clone(), adapter_fn, Arc::clone(&shutdown_notify), + self.lane_queue_lengths.clone(), self.shutdown_listener.clone(), ); @@ -127,6 +134,7 @@ where } pub fn into_inner(mut self) -> (TcpStream, ConnectionReceiver) { + self.shutdown_listener.mark_as_success(); ( self.socket.take().unwrap(), self.mix_receiver.take().unwrap(), diff --git a/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs b/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs index 8fad665785..78db73afb6 100644 --- a/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs +++ b/common/socks5/proxy-helpers/src/proxy_runner/outbound.rs @@ -90,5 +90,6 @@ pub(super) async fn run_outbound( trace!("{} - outbound closed", connection_id); shutdown_notify.notify_one(); + shutdown_listener.mark_as_success(); (writer, mix_receiver) } diff --git a/common/socks5/requests/src/msg.rs b/common/socks5/requests/src/msg.rs index b13f346f35..2b688d8214 100644 --- a/common/socks5/requests/src/msg.rs +++ b/common/socks5/requests/src/msg.rs @@ -25,6 +25,7 @@ pub enum MessageError { UnknownMessageType, } +#[derive(Debug)] pub enum Message { Request(Request), Response(Response), diff --git a/common/task/Cargo.toml b/common/task/Cargo.toml index 4c0c088d67..f074cd2abd 100644 --- a/common/task/Cargo.toml +++ b/common/task/Cargo.toml @@ -6,7 +6,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +futures = "0.3" log = "0.4" +thiserror = "1.0.37" tokio = { version = "1.21.2", features = ["macros", "signal", "time", "sync"] } [dev-dependencies] diff --git a/common/task/src/shutdown.rs b/common/task/src/shutdown.rs index 21ea12569d..5d4f2c77bb 100644 --- a/common/task/src/shutdown.rs +++ b/common/task/src/shutdown.rs @@ -1,27 +1,62 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use std::time::Duration; +use std::{error::Error, time::Duration}; -use tokio::sync::watch::{self, error::SendError}; +use futures::FutureExt; +use tokio::{ + sync::{ + mpsc, + watch::{self, error::SendError}, + }, + time::sleep, +}; const DEFAULT_SHUTDOWN_TIMER_SECS: u64 = 5; +pub(crate) type SentError = Box; +type ErrorSender = mpsc::UnboundedSender; +type ErrorReceiver = mpsc::UnboundedReceiver; + +#[derive(thiserror::Error, Debug)] +enum TaskError { + #[error("Task halted unexpectedly")] + UnexpectedHalt, +} + /// Used to notify other tasks to gracefully shutdown #[derive(Debug)] pub struct ShutdownNotifier { + // These channels have the dual purpose of signalling it's time to shutdown, but also to keep + // track of which tasks we are still waiting for. notify_tx: watch::Sender<()>, notify_rx: Option>, shutdown_timer_secs: u64, + + // If any task failed, it needs to report separately + task_return_error_tx: ErrorSender, + task_return_error_rx: Option, + + // Also signal when the notifier is dropped, in case the task exits unexpectedly. + // Why are we not reusing the return error channel? Well, let me tell you kids, it's because I + // didn't manage to reliably get the explicitly sent error (and not the error sent during drop) + task_drop_tx: ErrorSender, + task_drop_rx: Option, } impl Default for ShutdownNotifier { fn default() -> Self { let (notify_tx, notify_rx) = watch::channel(()); + let (task_halt_tx, task_halt_rx) = mpsc::unbounded_channel(); + let (task_drop_tx, task_drop_rx) = mpsc::unbounded_channel(); Self { notify_tx, notify_rx: Some(notify_rx), shutdown_timer_secs: DEFAULT_SHUTDOWN_TIMER_SECS, + task_return_error_tx: task_halt_tx, + task_return_error_rx: Some(task_halt_rx), + task_drop_tx, + task_drop_rx: Some(task_drop_rx), } } } @@ -40,6 +75,8 @@ impl ShutdownNotifier { .as_ref() .expect("Unable to subscribe to shutdown notifier that is already shutdown") .clone(), + self.task_return_error_tx.clone(), + self.task_drop_tx.clone(), ) } @@ -47,7 +84,31 @@ impl ShutdownNotifier { self.notify_tx.send(()) } + pub async fn wait_for_error(&mut self) -> Option { + let mut error_rx = self + .task_return_error_rx + .take() + .expect("Unable to wait for error: attempt to wait twice?"); + let mut drop_rx = self + .task_drop_rx + .take() + .expect("Unable to wait for error: attempt to wait twice?"); + + // During an error we are likely like to be swamped with drop notifications as well, this + // is a crude way to give priority to real errors (if there are any). + let drop_rx = drop_rx.recv().then(|msg| async move { + sleep(Duration::from_millis(50)).await; + msg + }); + + tokio::select! { + msg = error_rx.recv() => msg, + msg = drop_rx => msg + } + } + pub async fn wait_for_shutdown(&mut self) { + log::info!("Waiting for shutdown"); if let Some(notify_rx) = self.notify_rx.take() { drop(notify_rx); } @@ -56,7 +117,7 @@ impl ShutdownNotifier { _ = self.notify_tx.closed() => { log::info!("All registered tasks succesfully shutdown"); }, - _ = tokio::signal::ctrl_c() => { + _ = tokio::signal::ctrl_c() => { log::info!("Forcing shutdown"); } _ = tokio::time::sleep(Duration::from_secs(self.shutdown_timer_secs)) => { @@ -69,15 +130,36 @@ impl ShutdownNotifier { /// Listen for shutdown notifications #[derive(Clone, Debug)] pub struct ShutdownListener { + // If a shutdown notification has been registered shutdown: bool, + + // Listen for shutdown notifications, as well as a mechanism to report back that we have + // finished (the receiver is closed). notify: watch::Receiver<()>, + + // Send back error if we stopped + return_error: ErrorSender, + + // Also notify if we dropped without shutdown being registered + drop_error: ErrorSender, + + // Sometimes it's necessary to clone and drop the shutdown listener during normal operation, + // for those situations we need to explicitly not drop (and trigger shutdown). + set_not_drop: bool, } impl ShutdownListener { - fn new(notify: watch::Receiver<()>) -> ShutdownListener { + fn new( + notify: watch::Receiver<()>, + return_error: ErrorSender, + drop_error: ErrorSender, + ) -> ShutdownListener { ShutdownListener { shutdown: false, notify, + return_error, + drop_error, + set_not_drop: false, } } @@ -111,6 +193,29 @@ impl ShutdownListener { } } } + + pub fn send_we_stopped(&mut self, err: SentError) { + log::trace!("Notifying we stopped: {:?}", err); + if self.return_error.send(err).is_err() { + log::error!("Failed to send back error message"); + } + } + + pub fn mark_as_success(&mut self) { + self.set_not_drop = true; + } +} + +impl Drop for ShutdownListener { + fn drop(&mut self) { + if !self.set_not_drop && !self.is_shutdown_poll() { + log::trace!("Notifying stop on unexpected drop"); + // If we can't send, well then there is not much to do + self.drop_error + .send(Box::new(TaskError::UnexpectedHalt)) + .ok(); + } + } } #[cfg(test)] diff --git a/common/task/src/signal.rs b/common/task/src/signal.rs index 40ca122848..a580c5e293 100644 --- a/common/task/src/signal.rs +++ b/common/task/src/signal.rs @@ -17,6 +17,35 @@ pub async fn wait_for_signal() { } } +#[cfg(unix)] +pub async fn wait_for_signal_and_error( + shutdown: &mut crate::ShutdownNotifier, +) -> Result<(), crate::shutdown::SentError> { + use tokio::signal::unix::{signal, SignalKind}; + + let mut sigterm = signal(SignalKind::terminate()).expect("Failed to setup SIGTERM channel"); + let mut sigquit = signal(SignalKind::quit()).expect("Failed to setup SIGQUIT channel"); + + tokio::select! { + _ = tokio::signal::ctrl_c() => { + log::info!("Received SIGINT"); + Ok(()) + }, + _ = sigterm.recv() => { + log::info!("Received SIGTERM"); + Ok(()) + } + _ = sigquit.recv() => { + log::info!("Received SIGQUIT"); + Ok(()) + } + Some(msg) = shutdown.wait_for_error() => { + log::info!("Task error: {:?}", msg); + Err(msg) + } + } +} + #[cfg(not(unix))] pub async fn wait_for_signal() { tokio::select! { diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index ed6b7c71fb..6ef41c9aad 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -209,18 +209,14 @@ impl NymTopology { #[must_use] pub fn filter_system_version(&self, expected_version: &str) -> Self { - self.filter_node_versions(expected_version, expected_version) + self.filter_node_versions(expected_version) } #[must_use] - pub fn filter_node_versions( - &self, - expected_mix_version: &str, - expected_gateway_version: &str, - ) -> Self { + pub fn filter_node_versions(&self, expected_mix_version: &str) -> Self { NymTopology { mixes: self.mixes.filter_by_version(expected_mix_version), - gateways: self.gateways.filter_by_version(expected_gateway_version), + gateways: self.gateways.clone(), } } } diff --git a/contracts/CHANGELOG.md b/contracts/CHANGELOG.md index 3de60bd6f4..f57a633956 100644 --- a/contracts/CHANGELOG.md +++ b/contracts/CHANGELOG.md @@ -1,3 +1,12 @@ +## Unreleased + +## Added + +- Added migration code to the mixnet contract to allow updating stored vesting contract address to make it easier to deploy any future environments ([#1759],[#1769]) + +[#1759]: https://github.com/nymtech/nym/pull/1759 +[#1769]: https://github.com/nymtech/nym/pull/1769 + ## [nym-contracts-v1.1.0](https://github.com/nymtech/nym/tree/nym-contracts-v1.1.0) (2022-11-09) ### Changed diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 1671df0a3a..d2fc273d22 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -189,6 +189,33 @@ dependencies = [ "serde", ] +[[package]] +name = "coconut-dkg" +version = "0.1.0" +dependencies = [ + "coconut-dkg-common", + "cosmwasm-std", + "cosmwasm-storage", + "cw-controllers", + "cw-storage-plus", + "cw4", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "coconut-dkg-common" +version = "0.1.0" +dependencies = [ + "contracts-common", + "cosmwasm-std", + "cw-utils", + "multisig-contract-common", + "schemars", + "serde", +] + [[package]] name = "coconut-test" version = "0.1.0" @@ -196,6 +223,8 @@ dependencies = [ "bandwidth-claim-contract", "coconut-bandwidth", "coconut-bandwidth-contract-common", + "coconut-dkg", + "coconut-dkg-common", "cosmwasm-std", "cosmwasm-storage", "cw-controllers", @@ -203,6 +232,7 @@ dependencies = [ "cw-storage-plus", "cw-utils", "cw3-flex-multisig", + "cw4", "cw4-group", "multisig-contract-common", "schemars", @@ -220,6 +250,7 @@ checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" name = "contracts-common" version = "0.1.0" dependencies = [ + "bs58", "cosmwasm-std", "schemars", "serde", @@ -383,9 +414,9 @@ dependencies = [ [[package]] name = "cw-multi-test" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbea57e5be4a682268a5eca1a57efece57a54ff216bfd87603d5e864aad40e12" +checksum = "a3f9a8ab7c3c29ec93cb7a39ce4b14a05e053153b4a17ef7cf2246af1b7c087e" dependencies = [ "anyhow", "cosmwasm-std", @@ -497,7 +528,7 @@ dependencies = [ [[package]] name = "cw4-group" -version = "0.13.1" +version = "0.13.4" dependencies = [ "cosmwasm-schema", "cosmwasm-std", @@ -569,9 +600,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "1.4.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed12bbf7b5312f8da1c2722bc06d8c6b12c2d86a7fb35a194c7f3e6fc2bbe39" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" dependencies = [ "signature", ] @@ -972,6 +1003,7 @@ dependencies = [ "cosmwasm-std", "cw-utils", "cw3", + "cw3-fixed-multisig", "cw4", "schemars", "serde", @@ -1720,9 +1752,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "x25519-dalek" -version = "1.2.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2392b6b94a576b4e2bf3c5b2757d63f10ada8020a2e4d08ac849ebcf6ea8e077" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" dependencies = [ "curve25519-dalek", "rand_core 0.5.1", @@ -1731,9 +1763,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.3.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" dependencies = [ "zeroize_derive", ] diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index 1787d0ca52..de21d0fc92 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["coconut-bandwidth", "mixnet", "vesting", "multisig/cw3-flex-multisig", "multisig/cw4-group", "coconut-test"] +members = ["coconut-bandwidth", "mixnet", "vesting", "multisig/cw3-flex-multisig", "multisig/cw4-group", "coconut-test", "coconut-dkg"] [profile.release] opt-level = 3 diff --git a/contracts/coconut-dkg/Cargo.toml b/contracts/coconut-dkg/Cargo.toml new file mode 100644 index 0000000000..479187c370 --- /dev/null +++ b/contracts/coconut-dkg/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "coconut-dkg" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +coconut-dkg-common = { path = "../../common/cosmwasm-smart-contracts/coconut-dkg" } + +cosmwasm-std = "1.0.0" +cosmwasm-storage = "1.0.0" +cw-storage-plus = "0.13.4" +cw-controllers = "0.13.4" +cw4 = { version = "0.13.4" } + +schemars = "0.8" +serde = { version = "1.0.103", default-features = false, features = ["derive"] } +thiserror = "1.0.23" \ No newline at end of file diff --git a/contracts/coconut-dkg/src/constants.rs b/contracts/coconut-dkg/src/constants.rs new file mode 100644 index 0000000000..249a96c696 --- /dev/null +++ b/contracts/coconut-dkg/src/constants.rs @@ -0,0 +1,5 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +// Wait time for the verification to take place (currently 24h) +pub(crate) const BLOCK_TIME_FOR_VERIFICATION_SECS: u64 = 86400; diff --git a/contracts/coconut-dkg/src/dealers/mod.rs b/contracts/coconut-dkg/src/dealers/mod.rs new file mode 100644 index 0000000000..11f07ae776 --- /dev/null +++ b/contracts/coconut-dkg/src/dealers/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod queries; +pub mod storage; +pub mod transactions; diff --git a/contracts/coconut-dkg/src/dealers/queries.rs b/contracts/coconut-dkg/src/dealers/queries.rs new file mode 100644 index 0000000000..5221994dc0 --- /dev/null +++ b/contracts/coconut-dkg/src/dealers/queries.rs @@ -0,0 +1,67 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::dealers::storage::{self, IndexedDealersMap}; +use coconut_dkg_common::dealer::{DealerDetailsResponse, DealerType, PagedDealerResponse}; +use cosmwasm_std::{Deps, Order, StdResult}; +use cw_storage_plus::Bound; + +fn query_dealers( + deps: Deps<'_>, + start_after: Option, + limit: Option, + underlying_map: IndexedDealersMap<'_>, +) -> StdResult { + let limit = limit + .unwrap_or(storage::DEALERS_PAGE_DEFAULT_LIMIT) + .min(storage::DEALERS_PAGE_MAX_LIMIT) as usize; + + let addr = start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + + let start = addr.as_ref().map(Bound::exclusive); + + let dealers = underlying_map + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|item| item.1)) + .collect::>>()?; + + let start_next_after = dealers.last().map(|dealer| dealer.address.clone()); + + Ok(PagedDealerResponse::new(dealers, limit, start_next_after)) +} + +pub fn query_dealer_details( + deps: Deps<'_>, + dealer_address: String, +) -> StdResult { + let addr = deps.api.addr_validate(&dealer_address)?; + if let Some(current) = storage::current_dealers().may_load(deps.storage, &addr)? { + return Ok(DealerDetailsResponse::new( + Some(current), + DealerType::Current, + )); + } + if let Some(past) = storage::past_dealers().may_load(deps.storage, &addr)? { + return Ok(DealerDetailsResponse::new(Some(past), DealerType::Past)); + } + Ok(DealerDetailsResponse::new(None, DealerType::Unknown)) +} + +pub fn query_current_dealers_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + query_dealers(deps, start_after, limit, storage::current_dealers()) +} + +pub fn query_past_dealers_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + query_dealers(deps, start_after, limit, storage::past_dealers()) +} diff --git a/contracts/coconut-dkg/src/dealers/storage.rs b/contracts/coconut-dkg/src/dealers/storage.rs new file mode 100644 index 0000000000..398193de23 --- /dev/null +++ b/contracts/coconut-dkg/src/dealers/storage.rs @@ -0,0 +1,49 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use coconut_dkg_common::types::{DealerDetails, NodeIndex}; +use cosmwasm_std::{Addr, StdResult, Storage}; +use cw_storage_plus::{Index, IndexList, IndexedMap, Item, UniqueIndex}; + +const CURRENT_DEALERS_PK: &str = "crd"; +const PAST_DEALERS_PK: &str = "ptd"; +const DEALERS_NODE_INDEX_IDX_NAMESPACE: &str = "dni"; + +pub(crate) const DEALERS_PAGE_MAX_LIMIT: u32 = 75; +pub(crate) const DEALERS_PAGE_DEFAULT_LIMIT: u32 = 50; + +pub(crate) const NODE_INDEX_COUNTER: Item = Item::new("node_index_counter"); + +pub(crate) type IndexedDealersMap<'a> = IndexedMap<'a, &'a Addr, DealerDetails, DealersIndex<'a>>; + +pub(crate) struct DealersIndex<'a> { + pub(crate) node_index: UniqueIndex<'a, NodeIndex, DealerDetails>, +} + +impl<'a> IndexList for DealersIndex<'a> { + fn get_indexes(&'_ self) -> Box> + '_> { + let v: Vec<&dyn Index> = vec![&self.node_index]; + Box::new(v.into_iter()) + } +} + +pub(crate) fn current_dealers<'a>() -> IndexedDealersMap<'a> { + let indexes = DealersIndex { + node_index: UniqueIndex::new(|d| d.assigned_index, DEALERS_NODE_INDEX_IDX_NAMESPACE), + }; + IndexedMap::new(CURRENT_DEALERS_PK, indexes) +} + +pub(crate) fn past_dealers<'a>() -> IndexedDealersMap<'a> { + let indexes = DealersIndex { + node_index: UniqueIndex::new(|d| d.assigned_index, DEALERS_NODE_INDEX_IDX_NAMESPACE), + }; + IndexedMap::new(PAST_DEALERS_PK, indexes) +} + +pub(crate) fn next_node_index(store: &mut dyn Storage) -> StdResult { + // make sure we don't start from 0, otherwise all the crypto breaks (kinda) + let id: NodeIndex = NODE_INDEX_COUNTER.may_load(store)?.unwrap_or_default() + 1; + NODE_INDEX_COUNTER.save(store, &id)?; + Ok(id) +} diff --git a/contracts/coconut-dkg/src/dealers/transactions.rs b/contracts/coconut-dkg/src/dealers/transactions.rs new file mode 100644 index 0000000000..74e7d61aee --- /dev/null +++ b/contracts/coconut-dkg/src/dealers/transactions.rs @@ -0,0 +1,66 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::dealers::storage as dealers_storage; +use crate::epoch_state::utils::check_epoch_state; +use crate::{ContractError, State, STATE}; +use coconut_dkg_common::types::{DealerDetails, EncodedBTEPublicKeyWithProof, EpochState}; +use cosmwasm_std::{Addr, DepsMut, MessageInfo, Response}; + +// currently we only require that +// a) it's part of the signer group +// b) it isn't already a dealer +fn verify_dealer(deps: DepsMut<'_>, state: &State, dealer: &Addr) -> Result<(), ContractError> { + if dealers_storage::current_dealers() + .may_load(deps.storage, dealer)? + .is_some() + { + return Err(ContractError::AlreadyADealer); + } + + state + .group_addr + .is_voting_member(&deps.querier, dealer, None)? + .ok_or(ContractError::Unauthorized {})?; + + Ok(()) +} + +pub fn try_add_dealer( + mut deps: DepsMut<'_>, + info: MessageInfo, + bte_key_with_proof: EncodedBTEPublicKeyWithProof, + announce_address: String, +) -> Result { + check_epoch_state(deps.storage, EpochState::PublicKeySubmission)?; + let state = STATE.load(deps.storage)?; + + verify_dealer(deps.branch(), &state, &info.sender)?; + + // if it was already a dealer in the past, assign the same node index + let node_index = if let Some(prior_details) = + dealers_storage::past_dealers().may_load(deps.storage, &info.sender)? + { + // since this dealer is going to become active now, remove it from the past dealers + dealers_storage::past_dealers().replace( + deps.storage, + &info.sender, + None, + Some(&prior_details), + )?; + prior_details.assigned_index + } else { + dealers_storage::next_node_index(deps.storage)? + }; + + // save the dealer into the storage + let dealer_details = DealerDetails { + address: info.sender.clone(), + bte_public_key_with_proof: bte_key_with_proof, + announce_address, + assigned_index: node_index, + }; + dealers_storage::current_dealers().save(deps.storage, &info.sender, &dealer_details)?; + + Ok(Response::new().add_attribute("node_index", node_index.to_string())) +} diff --git a/contracts/coconut-dkg/src/dealings/mod.rs b/contracts/coconut-dkg/src/dealings/mod.rs new file mode 100644 index 0000000000..11f07ae776 --- /dev/null +++ b/contracts/coconut-dkg/src/dealings/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod queries; +pub mod storage; +pub mod transactions; diff --git a/contracts/coconut-dkg/src/dealings/queries.rs b/contracts/coconut-dkg/src/dealings/queries.rs new file mode 100644 index 0000000000..cf2321837c --- /dev/null +++ b/contracts/coconut-dkg/src/dealings/queries.rs @@ -0,0 +1,45 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::dealings::storage; +use crate::dealings::storage::DEALINGS_BYTES; +use coconut_dkg_common::dealer::{ContractDealing, PagedDealingsResponse}; +use coconut_dkg_common::types::TOTAL_DEALINGS; +use cosmwasm_std::{Deps, Order, StdResult}; +use cw_storage_plus::Bound; + +pub fn query_dealings_paged( + deps: Deps<'_>, + idx: u64, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(storage::DEALINGS_PAGE_DEFAULT_LIMIT) + .min(storage::DEALINGS_PAGE_MAX_LIMIT) as usize; + + let idx = idx as usize; + if idx >= TOTAL_DEALINGS { + return Ok(PagedDealingsResponse::new(vec![], limit, None)); + } + + let addr = start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + + let start = addr.as_ref().map(Bound::exclusive); + + let dealings = DEALINGS_BYTES[idx] + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|(dealer, dealing)| ContractDealing::new(dealing, dealer))) + .collect::>>()?; + + let start_next_after = dealings.last().map(|dealing| dealing.dealer.clone()); + + Ok(PagedDealingsResponse::new( + dealings, + limit, + start_next_after, + )) +} diff --git a/contracts/coconut-dkg/src/dealings/storage.rs b/contracts/coconut-dkg/src/dealings/storage.rs new file mode 100644 index 0000000000..26cb5b35db --- /dev/null +++ b/contracts/coconut-dkg/src/dealings/storage.rs @@ -0,0 +1,33 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use coconut_dkg_common::types::{ContractSafeBytes, TOTAL_DEALINGS}; +use cosmwasm_std::Addr; +use cw_storage_plus::Map; + +pub(crate) const DEALINGS_PAGE_MAX_LIMIT: u32 = 2; +pub(crate) const DEALINGS_PAGE_DEFAULT_LIMIT: u32 = 1; + +type DealingKey<'a> = &'a Addr; + +// Note to whoever is looking at this implementation and is thinking of using something similar +// for storing small commitments/hashes of data on chain: +// If there's a lot of entries you want to store thinking, "oh, this digest is only 32 bytes, it's not that much", +// the default cosmwasm' serializer will bloat it to around ~100B. So you really don't want to be using +// Buckets/Maps, etc. for that purpose. Instead you want to use `storage` directly (look into the actual implementation of +// `Map` or `Bucket` to see what I mean. Instead of using the `to_vec` method on serde_json_wasm, you'd +// provide your data directly yourself. +// but you must be extremely careful when doing so, as you might end up overwriting some existing data +// if you don't choose your prefixes wisely. +// I didn't have to do it here as I'm storing relatively little data and after just base58-encoding +// my bytes, I was fine with the json overhead. + +// if TOTAL_DEALINGS is modified to anything other then current value (5), this part will also need +// to be modified +pub(crate) const DEALINGS_BYTES: [Map<'_, DealingKey<'_>, ContractSafeBytes>; TOTAL_DEALINGS] = [ + Map::new("dbyt1"), + Map::new("dbyt2"), + Map::new("dbyt3"), + Map::new("dbyt4"), + Map::new("dbyt5"), +]; diff --git a/contracts/coconut-dkg/src/dealings/transactions.rs b/contracts/coconut-dkg/src/dealings/transactions.rs new file mode 100644 index 0000000000..73e067e0d6 --- /dev/null +++ b/contracts/coconut-dkg/src/dealings/transactions.rs @@ -0,0 +1,37 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::dealers::storage as dealers_storage; +use crate::dealings::storage::DEALINGS_BYTES; +use crate::epoch_state::utils::check_epoch_state; +use crate::ContractError; +use coconut_dkg_common::types::{ContractSafeBytes, EpochState}; +use cosmwasm_std::{DepsMut, MessageInfo, Response}; + +pub fn try_commit_dealings( + deps: DepsMut<'_>, + info: MessageInfo, + dealing_bytes: ContractSafeBytes, +) -> Result { + check_epoch_state(deps.storage, EpochState::DealingExchange)?; + // ensure the sender is a dealer + if dealers_storage::current_dealers() + .may_load(deps.storage, &info.sender)? + .is_none() + { + return Err(ContractError::NotADealer); + } + + // check if this dealer has already committed to all dealings + // (we don't want to allow overwriting anything) + for dealings in DEALINGS_BYTES { + if !dealings.has(deps.storage, &info.sender) { + dealings.save(deps.storage, &info.sender, &dealing_bytes)?; + return Ok(Response::default()); + } + } + + Err(ContractError::AlreadyCommitted { + commitment: String::from("dealing"), + }) +} diff --git a/contracts/coconut-dkg/src/epoch_state/mod.rs b/contracts/coconut-dkg/src/epoch_state/mod.rs new file mode 100644 index 0000000000..195ff89361 --- /dev/null +++ b/contracts/coconut-dkg/src/epoch_state/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod queries; +pub mod storage; +pub mod utils; diff --git a/contracts/coconut-dkg/src/epoch_state/queries.rs b/contracts/coconut-dkg/src/epoch_state/queries.rs new file mode 100644 index 0000000000..142ee4826a --- /dev/null +++ b/contracts/coconut-dkg/src/epoch_state/queries.rs @@ -0,0 +1,13 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use super::storage; +use crate::ContractError; +use coconut_dkg_common::types::EpochState; +use cosmwasm_std::Storage; + +pub(crate) fn query_current_epoch_state( + storage: &dyn Storage, +) -> Result { + storage::current_epoch_state(storage) +} diff --git a/contracts/coconut-dkg/src/epoch_state/storage.rs b/contracts/coconut-dkg/src/epoch_state/storage.rs new file mode 100644 index 0000000000..b0abe5fa5b --- /dev/null +++ b/contracts/coconut-dkg/src/epoch_state/storage.rs @@ -0,0 +1,29 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ContractError, ADMIN}; +use coconut_dkg_common::types::EpochState; +use cosmwasm_std::{DepsMut, MessageInfo, Response, Storage}; +use cw_storage_plus::Item; + +pub(crate) const CURRENT_EPOCH_STATE: Item<'_, EpochState> = Item::new("current_epoch_state"); + +pub(crate) fn current_epoch_state(storage: &dyn Storage) -> Result { + CURRENT_EPOCH_STATE + .load(storage) + .map_err(|_| ContractError::EpochNotInitialised) +} + +pub(crate) fn advance_epoch_state( + deps: DepsMut<'_>, + info: MessageInfo, +) -> Result { + ADMIN.assert_admin(deps.as_ref(), &info.sender)?; + CURRENT_EPOCH_STATE.update::<_, ContractError>(deps.storage, |mut epoch_state| { + // TODO: When defaulting to the first state, some action will probably need to be taken on the + // rest of the contract, as we're starting with a new set of signers + epoch_state = epoch_state.next().unwrap_or_default(); + Ok(epoch_state) + })?; + Ok(Response::default()) +} diff --git a/contracts/coconut-dkg/src/epoch_state/utils.rs b/contracts/coconut-dkg/src/epoch_state/utils.rs new file mode 100644 index 0000000000..d78fd49ae3 --- /dev/null +++ b/contracts/coconut-dkg/src/epoch_state/utils.rs @@ -0,0 +1,21 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ContractError, CURRENT_EPOCH_STATE}; +use coconut_dkg_common::types::EpochState; +use cosmwasm_std::Storage; + +pub(crate) fn check_epoch_state( + storage: &dyn Storage, + against: EpochState, +) -> Result<(), ContractError> { + let epoch_state = CURRENT_EPOCH_STATE.load(storage)?; + if epoch_state != against { + Err(ContractError::IncorrectEpochState { + current_state: epoch_state.to_string(), + expected_state: against.to_string(), + }) + } else { + Ok(()) + } +} diff --git a/contracts/coconut-dkg/src/error.rs b/contracts/coconut-dkg/src/error.rs new file mode 100644 index 0000000000..ad3a3e74ef --- /dev/null +++ b/contracts/coconut-dkg/src/error.rs @@ -0,0 +1,68 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::{Addr, StdError, VerificationError}; +use cw_controllers::AdminError; +use thiserror::Error; + +/// Custom errors for contract failure conditions. +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + Admin(#[from] AdminError), + + #[error("Group contract invalid address '{addr}'")] + InvalidGroup { addr: String }, + + #[error("No coin was sent for the deposit, you must send {denom}")] + NoDepositFound { denom: String }, + + #[error("Received multiple coin types")] + MultipleDenoms, + + #[error("Wrong coin denomination, you must send {denom}")] + WrongDenom { denom: String }, + + #[error("Not enough funds sent for deposit. (received {received}, minimum {minimum})")] + InsufficientDeposit { received: u128, minimum: u128 }, + + #[error("Failed to perform ed25519 signature verification - {0}. This dealer will be temporarily blacklisted now.")] + Ed25519VerificationError(#[from] VerificationError), + + #[error("Provided ed25519 signature did not verify correctly. This dealer will be temporarily blacklisted now.")] + InvalidEd25519Signature, + + #[error("This potential dealer is not in the coconut signer group")] + Unauthorized, + + #[error("This sender is already a dealer for the epoch")] + AlreadyADealer, + + #[error("Epoch hasn't been correctly initialised!")] + EpochNotInitialised, + + #[error( + "Requested action needs state to be {expected_state}, currently in state {current_state}, " + )] + IncorrectEpochState { + current_state: String, + expected_state: String, + }, + + // we should never ever see this error (famous last words in programming), therefore, I'd want to + // explicitly declare it so that when we ultimate do see it, it's gonna be more informative over "normal" panic + #[error("Somehow our validated address {address} is not using correct bech32 encoding")] + InvalidValidatedAddress { address: Addr }, + + #[error("This sender is not a dealer for the current epoch")] + NotADealer, + + #[error("This dealer has already committed {commitment}")] + AlreadyCommitted { commitment: String }, + + #[error("No verification key committed for owner {owner}")] + NoCommitForOwner { owner: String }, +} diff --git a/contracts/coconut-dkg/src/lib.rs b/contracts/coconut-dkg/src/lib.rs new file mode 100644 index 0000000000..7255b96ae8 --- /dev/null +++ b/contracts/coconut-dkg/src/lib.rs @@ -0,0 +1,146 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::dealers::queries::{ + query_current_dealers_paged, query_dealer_details, query_past_dealers_paged, +}; +use crate::dealings::queries::query_dealings_paged; +use crate::epoch_state::queries::query_current_epoch_state; +use crate::error::ContractError; +use crate::state::{State, ADMIN, MULTISIG, STATE}; +use crate::verification_key_shares::queries::query_vk_shares_paged; +use coconut_dkg_common::msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; +use coconut_dkg_common::types::EpochState; +use cosmwasm_std::{ + entry_point, to_binary, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, +}; +use cw4::Cw4Contract; +use epoch_state::storage::{advance_epoch_state, CURRENT_EPOCH_STATE}; + +mod constants; +mod dealers; +mod dealings; +mod epoch_state; +mod error; +mod state; +mod verification_key_shares; + +/// Instantiate the contract. +/// +/// `deps` contains Storage, API and Querier +/// `env` contains block, message and contract info +/// `msg` is the contract initialization message, sort of like a constructor call. +#[entry_point] +pub fn instantiate( + mut deps: DepsMut<'_>, + _env: Env, + _info: MessageInfo, + msg: InstantiateMsg, +) -> Result { + let admin_addr = deps.api.addr_validate(&msg.admin)?; + let multisig_addr = deps.api.addr_validate(&msg.multisig_addr)?; + ADMIN.set(deps.branch(), Some(admin_addr))?; + MULTISIG.set(deps.branch(), Some(multisig_addr.clone()))?; + + let group_addr = Cw4Contract(deps.api.addr_validate(&msg.group_addr).map_err(|_| { + ContractError::InvalidGroup { + addr: msg.group_addr.clone(), + } + })?); + + let state = State { + group_addr, + multisig_addr, + mix_denom: msg.mix_denom, + }; + STATE.save(deps.storage, &state)?; + + CURRENT_EPOCH_STATE.save(deps.storage, &EpochState::default())?; + + Ok(Response::default()) +} + +/// Handle an incoming message +#[entry_point] +pub fn execute( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> Result { + match msg { + ExecuteMsg::RegisterDealer { + bte_key_with_proof, + announce_address, + } => { + dealers::transactions::try_add_dealer(deps, info, bte_key_with_proof, announce_address) + } + ExecuteMsg::CommitDealing { dealing_bytes } => { + dealings::transactions::try_commit_dealings(deps, info, dealing_bytes) + } + ExecuteMsg::CommitVerificationKeyShare { share } => { + verification_key_shares::transactions::try_commit_verification_key_share( + deps, env, info, share, + ) + } + ExecuteMsg::VerifyVerificationKeyShare { owner } => { + verification_key_shares::transactions::try_verify_verification_key_share( + deps, info, owner, + ) + } + ExecuteMsg::AdvanceEpochState {} => advance_epoch_state(deps, info), + } +} + +#[entry_point] +pub fn query(deps: Deps<'_>, _env: Env, msg: QueryMsg) -> Result { + let response = match msg { + QueryMsg::GetCurrentEpochState {} => to_binary(&query_current_epoch_state(deps.storage)?)?, + QueryMsg::GetDealerDetails { dealer_address } => { + to_binary(&query_dealer_details(deps, dealer_address)?)? + } + QueryMsg::GetCurrentDealers { limit, start_after } => { + to_binary(&query_current_dealers_paged(deps, start_after, limit)?)? + } + QueryMsg::GetPastDealers { limit, start_after } => { + to_binary(&query_past_dealers_paged(deps, start_after, limit)?)? + } + QueryMsg::GetDealing { + idx, + limit, + start_after, + } => to_binary(&query_dealings_paged(deps, idx, start_after, limit)?)?, + QueryMsg::GetVerificationKeys { limit, start_after } => { + to_binary(&query_vk_shares_paged(deps, start_after, limit)?)? + } + }; + + Ok(response) +} + +#[entry_point] +pub fn migrate(_deps: DepsMut<'_>, _env: Env, _msg: MigrateMsg) -> Result { + Ok(Default::default()) +} + +#[cfg(test)] +mod tests { + use super::*; + use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; + + #[test] + fn initialize_contract() { + let mut deps = mock_dependencies(); + let env = mock_env(); + let msg = InstantiateMsg { + group_addr: "group_addr".to_string(), + multisig_addr: "multisig_addr".to_string(), + admin: "admin".to_string(), + mix_denom: "nym".to_string(), + }; + let info = mock_info("creator", &[]); + + let res = instantiate(deps.as_mut(), env.clone(), info, msg); + assert!(res.is_ok()) + } +} diff --git a/contracts/coconut-dkg/src/state/mod.rs b/contracts/coconut-dkg/src/state/mod.rs new file mode 100644 index 0000000000..729618ec60 --- /dev/null +++ b/contracts/coconut-dkg/src/state/mod.rs @@ -0,0 +1,21 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use cosmwasm_std::Addr; +use cw4::Cw4Contract; +use cw_controllers::Admin; +use cw_storage_plus::Item; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +// unique items +pub const STATE: Item = Item::new("state"); +pub const ADMIN: Admin = Admin::new("admin"); +pub const MULTISIG: Admin = Admin::new("multisig"); + +#[derive(Serialize, Deserialize, Clone, PartialEq, JsonSchema, Debug)] +pub struct State { + pub mix_denom: String, + pub multisig_addr: Addr, + pub group_addr: Cw4Contract, +} diff --git a/contracts/coconut-dkg/src/verification_key_shares/mod.rs b/contracts/coconut-dkg/src/verification_key_shares/mod.rs new file mode 100644 index 0000000000..11f07ae776 --- /dev/null +++ b/contracts/coconut-dkg/src/verification_key_shares/mod.rs @@ -0,0 +1,6 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod queries; +pub mod storage; +pub mod transactions; diff --git a/contracts/coconut-dkg/src/verification_key_shares/queries.rs b/contracts/coconut-dkg/src/verification_key_shares/queries.rs new file mode 100644 index 0000000000..6739577abb --- /dev/null +++ b/contracts/coconut-dkg/src/verification_key_shares/queries.rs @@ -0,0 +1,38 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::verification_key_shares::storage; +use crate::verification_key_shares::storage::VK_SHARES; +use coconut_dkg_common::verification_key::PagedVKSharesResponse; +use cosmwasm_std::{Deps, Order, StdResult}; +use cw_storage_plus::Bound; + +pub fn query_vk_shares_paged( + deps: Deps<'_>, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(storage::VERIFICATION_KEY_SHARES_PAGE_DEFAULT_LIMIT) + .min(storage::VERIFICATION_KEY_SHARES_PAGE_MAX_LIMIT) as usize; + + let addr = start_after + .map(|addr| deps.api.addr_validate(&addr)) + .transpose()?; + + let start = addr.as_ref().map(Bound::exclusive); + + let shares = VK_SHARES + .range(deps.storage, start, None, Order::Ascending) + .take(limit) + .map(|res| res.map(|(_, share)| share)) + .collect::>>()?; + + let start_next_after = shares.last().map(|share| share.owner.clone()); + + Ok(PagedVKSharesResponse { + shares, + per_page: limit, + start_next_after, + }) +} diff --git a/contracts/coconut-dkg/src/verification_key_shares/storage.rs b/contracts/coconut-dkg/src/verification_key_shares/storage.rs new file mode 100644 index 0000000000..761736367c --- /dev/null +++ b/contracts/coconut-dkg/src/verification_key_shares/storage.rs @@ -0,0 +1,15 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use coconut_dkg_common::verification_key::ContractVKShare; +use cosmwasm_std::Addr; +use cw_storage_plus::Map; + +pub(crate) const VERIFICATION_KEY_SHARES_PAGE_MAX_LIMIT: u32 = 75; +pub(crate) const VERIFICATION_KEY_SHARES_PAGE_DEFAULT_LIMIT: u32 = 50; + +type VKShareKey<'a> = &'a Addr; + +pub(crate) const VK_SHARES: Map<'_, VKShareKey<'_>, ContractVKShare> = Map::new("vks"); diff --git a/contracts/coconut-dkg/src/verification_key_shares/transactions.rs b/contracts/coconut-dkg/src/verification_key_shares/transactions.rs new file mode 100644 index 0000000000..d86748e7f4 --- /dev/null +++ b/contracts/coconut-dkg/src/verification_key_shares/transactions.rs @@ -0,0 +1,71 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::constants::BLOCK_TIME_FOR_VERIFICATION_SECS; +use crate::dealers::storage as dealers_storage; +use crate::epoch_state::utils::check_epoch_state; +use crate::error::ContractError; +use crate::state::{MULTISIG, STATE}; +use crate::verification_key_shares::storage::VK_SHARES; +use coconut_dkg_common::types::EpochState; +use coconut_dkg_common::verification_key::{to_cosmos_msg, ContractVKShare, VerificationKeyShare}; +use cosmwasm_std::{Addr, DepsMut, Env, MessageInfo, Response}; + +pub fn try_commit_verification_key_share( + deps: DepsMut<'_>, + env: Env, + info: MessageInfo, + share: VerificationKeyShare, +) -> Result { + check_epoch_state(deps.storage, EpochState::VerificationKeySubmission)?; + // ensure the sender is a dealer + let details = dealers_storage::current_dealers() + .load(deps.storage, &info.sender) + .map_err(|_| ContractError::NotADealer)?; + if VK_SHARES.may_load(deps.storage, &info.sender)?.is_some() { + return Err(ContractError::AlreadyCommitted { + commitment: String::from("verification key share"), + }); + } + + let data = ContractVKShare { + share, + node_index: details.assigned_index, + announce_address: details.announce_address, + owner: info.sender.clone(), + verified: false, + }; + VK_SHARES.save(deps.storage, &info.sender, &data)?; + + let msg = to_cosmos_msg( + info.sender, + env.contract.address.to_string(), + STATE.load(deps.storage)?.multisig_addr.to_string(), + env.block + .time + .plus_seconds(BLOCK_TIME_FOR_VERIFICATION_SECS), + )?; + + Ok(Response::new().add_message(msg)) +} + +pub fn try_verify_verification_key_share( + deps: DepsMut<'_>, + info: MessageInfo, + owner: Addr, +) -> Result { + check_epoch_state(deps.storage, EpochState::VerificationKeyFinalization)?; + MULTISIG.assert_admin(deps.as_ref(), &info.sender)?; + VK_SHARES.update(deps.storage, &owner, |vk_share| { + vk_share + .map(|mut share| { + share.verified = true; + share + }) + .ok_or(ContractError::NoCommitForOwner { + owner: owner.to_string(), + }) + })?; + + Ok(Response::default()) +} diff --git a/contracts/coconut-test/Cargo.toml b/contracts/coconut-test/Cargo.toml index 5955391250..3aff6fc368 100644 --- a/contracts/coconut-test/Cargo.toml +++ b/contracts/coconut-test/Cargo.toml @@ -8,10 +8,12 @@ edition = "2021" [dependencies] bandwidth-claim-contract = { path = "../../common/bandwidth-claim-contract" } coconut-bandwidth-contract-common = { path = "../../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" } +coconut-dkg-common = { path = "../../common/cosmwasm-smart-contracts/coconut-dkg" } multisig-contract-common = { path = "../../common/cosmwasm-smart-contracts/multisig-contract" } cosmwasm-std = "1.0.0" cosmwasm-storage = "1.0.0" +cw4 = "0.13.4" cw-storage-plus = "0.13.4" cw-controllers = "0.13.4" cw-utils = "0.13.4" @@ -21,7 +23,8 @@ serde = { version = "1.0.103", default-features = false, features = ["derive"] } thiserror = "1.0.23" coconut-bandwidth = { path = "../coconut-bandwidth" } -cw-multi-test = { version = "0.13.2" } +coconut-dkg = { path = "../coconut-dkg" } +cw-multi-test = { version = "0.13.4" } cw3-flex-multisig = { path = "../multisig/cw3-flex-multisig" } cw4-group = { path = "../multisig/cw4-group" } diff --git a/contracts/coconut-test/src/helpers.rs b/contracts/coconut-test/src/helpers.rs index 2a8c0b24ed..b8e8ad6d03 100644 --- a/contracts/coconut-test/src/helpers.rs +++ b/contracts/coconut-test/src/helpers.rs @@ -8,6 +8,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; pub const OWNER: &str = "admin0001"; +pub const MEMBER1: &str = "member1"; pub const MULTISIG_CONTRACT: &str = "multisig contract address"; pub const POOL_CONTRACT: &str = "mix pool contract address"; pub const RANDOM_ADDRESS: &str = "random address"; @@ -16,6 +17,7 @@ pub const RANDOM_ADDRESS: &str = "random address"; #[serde(rename_all = "snake_case")] pub struct MigrateMsg { pub coconut_bandwidth_address: String, + pub coconut_dkg_address: String, } #[entry_point] @@ -32,8 +34,20 @@ pub fn mock_app(init_funds: &[Coin]) -> App { .bank .init_balance(storage, &Addr::unchecked(OWNER), init_funds.to_vec()) .unwrap(); + router + .bank + .init_balance(storage, &Addr::unchecked(MEMBER1), init_funds.to_vec()) + .unwrap(); }) } +pub fn contract_dkg() -> Box> { + let contract = ContractWrapper::new( + coconut_dkg::execute, + coconut_dkg::instantiate, + coconut_dkg::query, + ); + Box::new(contract) +} pub fn contract_bandwidth() -> Box> { let contract = ContractWrapper::new( diff --git a/contracts/coconut-test/src/spend_credential_creates_proposal.rs b/contracts/coconut-test/src/spend_credential_creates_proposal.rs index 7d07905b97..3d382f493b 100644 --- a/contracts/coconut-test/src/spend_credential_creates_proposal.rs +++ b/contracts/coconut-test/src/spend_credential_creates_proposal.rs @@ -15,6 +15,7 @@ use multisig_contract_common::msg::InstantiateMsg as MultisigInstantiateMsg; pub const TEST_COIN_DENOM: &str = "unym"; pub const TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; +pub const TEST_COCONUT_DKG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; #[test] fn spend_credential_creates_proposal() { @@ -46,6 +47,7 @@ fn spend_credential_creates_proposal() { }, max_voting_period: Duration::Height(1000), coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), + coconut_dkg_contract_address: TEST_COCONUT_DKG_CONTRACT_ADDRESS.to_string(), }; let multisig_contract_addr = app .instantiate_contract( @@ -77,6 +79,7 @@ fn spend_credential_creates_proposal() { let msg = MigrateMsg { coconut_bandwidth_address: coconut_bandwidth_contract_addr.to_string(), + coconut_dkg_address: "".to_string(), }; app.migrate_contract( Addr::unchecked(OWNER), diff --git a/contracts/coconut-test/src/submit_vk_creates_proposal.rs b/contracts/coconut-test/src/submit_vk_creates_proposal.rs new file mode 100644 index 0000000000..dd8f59b2da --- /dev/null +++ b/contracts/coconut-test/src/submit_vk_creates_proposal.rs @@ -0,0 +1,142 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::helpers::{ + contract_dkg, contract_group, contract_multisig, mock_app, MigrateMsg, MEMBER1, OWNER, +}; +use crate::spend_credential_creates_proposal::{ + TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS, TEST_COCONUT_DKG_CONTRACT_ADDRESS, TEST_COIN_DENOM, +}; +use coconut_dkg_common::msg::ExecuteMsg::{ + AdvanceEpochState, CommitVerificationKeyShare, RegisterDealer, +}; +use coconut_dkg_common::msg::InstantiateMsg as DkgInstantiateMsg; +use cosmwasm_std::{coins, Addr, Decimal}; +use cw4::Member; +use cw4_group::msg::InstantiateMsg as GroupInstantiateMsg; +use cw_multi_test::Executor; +use cw_utils::{Duration, Threshold}; +use multisig_contract_common::msg::InstantiateMsg as MultisigInstantiateMsg; + +#[test] +fn dkg_create_proposal() { + let init_funds = coins(10000000000, TEST_COIN_DENOM); + let mut app = mock_app(&init_funds); + let member1 = Member { + addr: MEMBER1.to_string(), + weight: 10, + }; + + let group_code_id = app.store_code(contract_group()); + let msg = GroupInstantiateMsg { + admin: Some(OWNER.to_string()), + members: vec![member1], + }; + let group_contract_addr = app + .instantiate_contract( + group_code_id, + Addr::unchecked(OWNER), + &msg, + &[], + "group", + None, + ) + .unwrap(); + + let multisig_code_id = app.store_code(contract_multisig()); + let msg = MultisigInstantiateMsg { + group_addr: group_contract_addr.to_string(), + threshold: Threshold::AbsolutePercentage { + percentage: Decimal::from_ratio(2u128, 3u128), + }, + max_voting_period: Duration::Time(1000), + coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), + coconut_dkg_contract_address: TEST_COCONUT_DKG_CONTRACT_ADDRESS.to_string(), + }; + let multisig_contract_addr = app + .instantiate_contract( + multisig_code_id, + Addr::unchecked(OWNER), + &msg, + &[], + "multisig", + Some(OWNER.to_string()), + ) + .unwrap(); + + let coconut_dkg_code_id = app.store_code(contract_dkg()); + let msg = DkgInstantiateMsg { + group_addr: group_contract_addr.to_string(), + multisig_addr: multisig_contract_addr.to_string(), + admin: Addr::unchecked(OWNER).to_string(), + mix_denom: TEST_COIN_DENOM.to_string(), + }; + let coconut_dkg_contract_addr = app + .instantiate_contract( + coconut_dkg_code_id, + Addr::unchecked(OWNER), + &msg, + &[], + "coconut dkg", + None, + ) + .unwrap(); + + let msg = MigrateMsg { + coconut_bandwidth_address: coconut_dkg_contract_addr.to_string(), + coconut_dkg_address: coconut_dkg_contract_addr.to_string(), + }; + app.migrate_contract( + Addr::unchecked(OWNER), + multisig_contract_addr, + &msg, + multisig_code_id, + ) + .unwrap(); + + app.execute_contract( + Addr::unchecked(MEMBER1), + coconut_dkg_contract_addr.clone(), + &RegisterDealer { + bte_key_with_proof: "bte_key_with_proof".to_string(), + announce_address: "127.0.0.1:8000".to_string(), + }, + &vec![], + ) + .unwrap(); + + for _ in 0..2 { + app.execute_contract( + Addr::unchecked(OWNER), + coconut_dkg_contract_addr.clone(), + &AdvanceEpochState {}, + &vec![], + ) + .unwrap(); + } + + let msg = CommitVerificationKeyShare { + share: "share".to_string(), + }; + let res = app + .execute_contract( + Addr::unchecked(MEMBER1), + coconut_dkg_contract_addr.clone(), + &msg, + &vec![], + ) + .unwrap(); + let proposal_id = res + .events + .into_iter() + .find(|e| &e.ty == "wasm") + .unwrap() + .attributes + .into_iter() + .find(|attr| &attr.key == "proposal_id") + .unwrap() + .value + .parse::() + .unwrap(); + assert_eq!(1, proposal_id); +} diff --git a/contracts/coconut-test/src/tests.rs b/contracts/coconut-test/src/tests.rs index 575a2c2ac1..2ba5f4b2e8 100644 --- a/contracts/coconut-test/src/tests.rs +++ b/contracts/coconut-test/src/tests.rs @@ -4,3 +4,4 @@ mod deposit_and_release; mod helpers; mod spend_credential_creates_proposal; +mod submit_vk_creates_proposal; diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 1951c535de..e07b7ba32a 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -467,10 +467,20 @@ pub fn query( #[entry_point] pub fn migrate( - _deps: DepsMut<'_>, + deps: DepsMut<'_>, _env: Env, - _msg: MigrateMsg, + msg: MigrateMsg, ) -> Result { + // 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 + if let Some(vesting_contract_address) = msg.vesting_contract_address { + let mut current_state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?; + current_state.vesting_contract_address = + deps.api.addr_validate(&vesting_contract_address)?; + mixnet_params_storage::CONTRACT_STATE.save(deps.storage, ¤t_state)?; + } + Ok(Default::default()) } diff --git a/contracts/multisig/cw3-flex-multisig/Cargo.toml b/contracts/multisig/cw3-flex-multisig/Cargo.toml index 0168eec05f..8afa71f251 100644 --- a/contracts/multisig/cw3-flex-multisig/Cargo.toml +++ b/contracts/multisig/cw3-flex-multisig/Cargo.toml @@ -17,12 +17,12 @@ crate-type = ["cdylib", "rlib"] library = [] [dependencies] -cw-utils = { version = "0.13.2" } -cw2 = { version = "0.13.2" } -cw3 = { version = "0.13.2" } -cw3-fixed-multisig = { version = "0.13.2", features = ["library"] } -cw4 = { version = "0.13.2" } -cw-storage-plus = { version = "0.13.2" } +cw-utils = { version = "0.13.4" } +cw2 = { version = "0.13.4" } +cw3 = { version = "0.13.4" } +cw3-fixed-multisig = { version = "0.13.4", features = ["library"] } +cw4 = { version = "0.13.4" } +cw-storage-plus = { version = "0.13.4" } cosmwasm-std = { version = "1.0.0" } schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } @@ -32,5 +32,5 @@ multisig-contract-common = { path= "../../../common/cosmwasm-smart-contracts/mul [dev-dependencies] cosmwasm-schema = { version = "1.0.0" } -cw4-group = { path = "../cw4-group", version = "0.13.1" } -cw-multi-test = { version = "0.13.1" } +cw4-group = { path = "../cw4-group", version = "0.13.4" } +cw-multi-test = { version = "0.13.4" } diff --git a/contracts/multisig/cw3-flex-multisig/src/contract.rs b/contracts/multisig/cw3-flex-multisig/src/contract.rs index c85d1362a7..5a56ea9745 100644 --- a/contracts/multisig/cw3-flex-multisig/src/contract.rs +++ b/contracts/multisig/cw3-flex-multisig/src/contract.rs @@ -37,11 +37,12 @@ pub fn instantiate( addr: msg.group_addr.clone(), } })?); - // This might need to be changed via a migration, due to circular dependency + // Those might need to be changed via a migration, due to circular dependency // of deploying the two contracts let coconut_bandwidth_addr = deps .api .addr_validate(&msg.coconut_bandwidth_contract_address)?; + let coconut_dkg_addr = deps.api.addr_validate(&msg.coconut_dkg_contract_address)?; let total_weight = group_addr.total_weight(&deps.querier)?; msg.threshold.validate(total_weight)?; @@ -52,6 +53,7 @@ pub fn instantiate( max_voting_period: msg.max_voting_period, group_addr, coconut_bandwidth_addr, + coconut_dkg_addr, }; CONFIG.save(deps.storage, &cfg)?; @@ -62,6 +64,7 @@ pub fn instantiate( pub fn migrate(deps: DepsMut<'_>, _env: Env, msg: MigrateMsg) -> Result { let mut cfg = CONFIG.load(deps.storage)?; cfg.coconut_bandwidth_addr = deps.api.addr_validate(&msg.coconut_bandwidth_address)?; + cfg.coconut_dkg_addr = deps.api.addr_validate(&msg.coconut_dkg_address)?; CONFIG.save(deps.storage, &cfg)?; Ok(Default::default()) } @@ -102,8 +105,8 @@ pub fn execute_propose( // only members of the multisig can create a proposal let cfg = CONFIG.load(deps.storage)?; - // Only the coconut bandwidth contract can create proposals - if info.sender != cfg.coconut_bandwidth_addr { + // Only the coconut bandwidth or dkg contracts can create proposals + if info.sender != cfg.coconut_bandwidth_addr && info.sender != cfg.coconut_dkg_addr { return Err(ContractError::Unauthorized {}); } // The contract doesn't have any say in the voting outcome @@ -460,6 +463,7 @@ mod tests { const SOMEBODY: &str = "somebody"; const TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; + const TEST_COCONUT_DKG_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; fn member>(addr: T, weight: u64) -> Member { Member { @@ -539,6 +543,7 @@ mod tests { threshold, max_voting_period, coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), + coconut_dkg_contract_address: TEST_COCONUT_DKG_CONTRACT_ADDRESS.to_string(), }; app.instantiate_contract(flex_id, Addr::unchecked(OWNER), &msg, &[], "flex", None) .unwrap() @@ -655,6 +660,7 @@ mod tests { }, max_voting_period, coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), + coconut_dkg_contract_address: TEST_COCONUT_DKG_CONTRACT_ADDRESS.to_string(), }; let err = app .instantiate_contract( @@ -677,6 +683,7 @@ mod tests { threshold: Threshold::AbsoluteCount { weight: 100 }, max_voting_period, coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), + coconut_dkg_contract_address: TEST_COCONUT_DKG_CONTRACT_ADDRESS.to_string(), }; let err = app .instantiate_contract( @@ -699,6 +706,7 @@ mod tests { threshold: Threshold::AbsoluteCount { weight: 1 }, max_voting_period, coconut_bandwidth_contract_address: TEST_COCONUT_BANDWIDTH_CONTRACT_ADDRESS.to_string(), + coconut_dkg_contract_address: TEST_COCONUT_DKG_CONTRACT_ADDRESS.to_string(), }; let flex_addr = app .instantiate_contract( diff --git a/contracts/multisig/cw3-flex-multisig/src/state.rs b/contracts/multisig/cw3-flex-multisig/src/state.rs index 96e6146c2f..0826f1014e 100644 --- a/contracts/multisig/cw3-flex-multisig/src/state.rs +++ b/contracts/multisig/cw3-flex-multisig/src/state.rs @@ -13,6 +13,7 @@ pub struct Config { // Total weight and voters are queried from this contract pub group_addr: Cw4Contract, pub coconut_bandwidth_addr: Addr, + pub coconut_dkg_addr: Addr, } // unique items diff --git a/contracts/multisig/cw4-group/Cargo.toml b/contracts/multisig/cw4-group/Cargo.toml index ecf5ff366c..235cfeed70 100644 --- a/contracts/multisig/cw4-group/Cargo.toml +++ b/contracts/multisig/cw4-group/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cw4-group" -version = "0.13.1" +version = "0.13.4" authors = ["Ethan Frey "] edition = "2018" description = "Simple cw4 implementation of group membership controlled by admin " @@ -24,11 +24,11 @@ crate-type = ["cdylib", "rlib"] library = [] [dependencies] -cw-utils = { version = "0.13.2" } -cw2 = { version = "0.13.2" } -cw4 = { version = "0.13.2" } -cw-controllers = { version = "0.13.2" } -cw-storage-plus = { version = "0.13.2" } +cw-utils = { version = "0.13.4" } +cw2 = { version = "0.13.4" } +cw4 = { version = "0.13.4" } +cw-controllers = { version = "0.13.4" } +cw-storage-plus = { version = "0.13.4" } cosmwasm-std = { version = "1.0.0" } schemars = "0.8.1" serde = { version = "1.0.103", default-features = false, features = ["derive"] } diff --git a/deny.toml b/deny.toml index 38d21db403..4bed0dce56 100644 --- a/deny.toml +++ b/deny.toml @@ -76,6 +76,7 @@ notice = "warn" ignore = [ "RUSTSEC-2020-0159", "RUSTSEC-2020-0071", + "RUSTSEC-2021-0145", ] # Threshold for security vulnerabilities, any vulnerability with a CVSS score # lower than the range specified will be ignored. Note that ignored advisories diff --git a/envs/mainnet.env b/envs/mainnet.env index a8dc60dd44..d53b7f4b8b 100644 --- a/envs/mainnet.env +++ b/envs/mainnet.env @@ -14,6 +14,7 @@ VESTING_CONTRACT_ADDRESS=n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq7 BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 MULTISIG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 +COCONUT_DKG_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 REWARDING_VALIDATOR_ADDRESS=n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy STATISTICS_SERVICE_DOMAIN_ADDRESS="https://mainnet-stats.nymte.ch:8090" NYMD_VALIDATOR="https://rpc.nyx.nodes.guru/" diff --git a/envs/qa.env b/envs/qa.env index 0e791db172..64511fa922 100644 --- a/envs/qa.env +++ b/envs/qa.env @@ -14,6 +14,7 @@ VESTING_CONTRACT_ADDRESS=n1xr3rq8yvd7qplsw5yx90ftsr2zdhg4e9z60h5duusgxpv72hud3sj BANDWIDTH_CLAIM_CONTRACT_ADDRESS=n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0 COCONUT_BANDWIDTH_CONTRACT_ADDRESS=n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw MULTISIG_CONTRACT_ADDRESS=n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs +COCONUT_DKG_CONTRACT_ADDRESS=n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs REWARDING_VALIDATOR_ADDRESS=n1tfzd4qz3a45u8p4mr5zmzv66457uwjgcl05jdq STATISTICS_SERVICE_DOMAIN_ADDRESS="http://0.0.0.0" NYMD_VALIDATOR="https://qa-validator.nymtech.net" diff --git a/explorer-api/src/country_statistics/geolocate.rs b/explorer-api/src/country_statistics/geolocate.rs index 42fe9405be..c77a61a9a1 100644 --- a/explorer-api/src/country_statistics/geolocate.rs +++ b/explorer-api/src/country_statistics/geolocate.rs @@ -95,12 +95,12 @@ impl GeoLocateTask { .await; } }, - Err(e) => { - warn!( - "❌ Oh no! Location for {} failed. Error: {:#?}", - cache_item.mix_node().host, - e - ); + Err(_e) => { + // warn!( + // "❌ Oh no! Location for {} failed. Error: {:#?}", + // cache_item.mix_node().host, + // e + // ); } }; } diff --git a/explorer/src/context/mixnode.tsx b/explorer/src/context/mixnode.tsx index a17366645c..a710095811 100644 --- a/explorer/src/context/mixnode.tsx +++ b/explorer/src/context/mixnode.tsx @@ -36,16 +36,16 @@ export const useMixnodeContext = (): React.ContextType => React.useContext(MixnodeContext); interface MixnodeContextProviderProps { - mixNodeIdentityKey: string; + mixId: string; } /** * Provides a state context for a mixnode by identity - * @param mixNodeIdentityKey The identity key of the mixnode + * @param mixId The mixID of the mixnode */ -export const MixnodeContextProvider: React.FC = ({ mixNodeIdentityKey, children }) => { +export const MixnodeContextProvider: React.FC = ({ mixId, children }) => { const [mixNode, fetchMixnodeById, clearMixnodeById] = useApiState( - mixNodeIdentityKey, + mixId, Api.fetchMixnodeByID, 'Failed to fetch mixnode by id', ); @@ -53,44 +53,44 @@ export const MixnodeContextProvider: React.FC = ({ const [mixNodeRow, setMixnodeRow] = React.useState(); const [delegations, fetchDelegations, clearDelegations] = useApiState( - mixNodeIdentityKey, + mixId, Api.fetchDelegationsById, 'Failed to fetch delegations for mixnode', ); const [uniqDelegations, fetchUniqDelegations, clearUniqDelegations] = useApiState( - mixNodeIdentityKey, + mixId, Api.fetchUniqDelegationsById, 'Failed to fetch delegations for mixnode', ); const [status, fetchStatus, clearStatus] = useApiState( - mixNodeIdentityKey, + mixId, Api.fetchStatusById, 'Failed to fetch mixnode status', ); const [stats, fetchStats, clearStats] = useApiState( - mixNodeIdentityKey, + mixId, Api.fetchStatsById, 'Failed to fetch mixnode stats', ); const [description, fetchDescription, clearDescription] = useApiState( - mixNodeIdentityKey, + mixId, Api.fetchMixnodeDescriptionById, 'Failed to fetch mixnode description', ); const [economicDynamicsStats, fetchEconomicDynamicsStats, clearEconomicDynamicsStats] = useApiState( - mixNodeIdentityKey, + mixId, Api.fetchMixnodeEconomicDynamicsStatsById, 'Failed to fetch mixnode dynamics stats by id', ); const [uptimeStory, fetchUptimeHistory, clearUptimeHistory] = useApiState( - mixNodeIdentityKey, + mixId, Api.fetchUptimeStoryById, 'Failed to fetch mixnode uptime history', ); @@ -123,7 +123,7 @@ export const MixnodeContextProvider: React.FC = ({ fetchUptimeHistory(), ]); }); - }, [mixNodeIdentityKey]); + }, [mixId]); const state = React.useMemo( () => ({ diff --git a/explorer/src/pages/MixnodeDetail/index.tsx b/explorer/src/pages/MixnodeDetail/index.tsx index 266c13be3a..1c38f69a53 100644 --- a/explorer/src/pages/MixnodeDetail/index.tsx +++ b/explorer/src/pages/MixnodeDetail/index.tsx @@ -223,7 +223,7 @@ export const PageMixnodeDetail: React.FC = () => { } return ( - + ); diff --git a/gateway/gateway-requests/src/lib.rs b/gateway/gateway-requests/src/lib.rs index 98de45c377..d1c1861a8a 100644 --- a/gateway/gateway-requests/src/lib.rs +++ b/gateway/gateway-requests/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2020 - Nym Technologies SA +// Copyright 2020-2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 pub use crypto::generic_array; @@ -12,6 +12,10 @@ pub mod iv; pub mod registration; pub mod types; +/// Defines the current version of the communication protocol between gateway and clients. +/// It has to be incremented for any breaking change. +pub const PROTOCOL_VERSION: u8 = 1; + pub type GatewayMac = HmacOutput; // TODO: could using `Mac` trait here for OutputSize backfire? diff --git a/gateway/gateway-requests/src/registration/handshake/state.rs b/gateway/gateway-requests/src/registration/handshake/state.rs index 10c0ae9729..1560b5fd9f 100644 --- a/gateway/gateway-requests/src/registration/handshake/state.rs +++ b/gateway/gateway-requests/src/registration/handshake/state.rs @@ -210,7 +210,10 @@ impl<'a, S> State<'a, S> { match msg { WsMessage::Text(ws_msg) => match types::RegistrationHandshake::try_from(ws_msg) { Ok(reg_handshake_msg) => return match reg_handshake_msg { - types::RegistrationHandshake::HandshakePayload { data } => Ok(data), + // hehe, that's a bit disgusting that the type system requires we explicitly ignore the + // protocol_version field that we actually never attach at this point + // yet another reason for the overdue refactor + types::RegistrationHandshake::HandshakePayload { data, .. } => Ok(data), types::RegistrationHandshake::HandshakeError { message } => Err(HandshakeError::RemoteError(message)), }, Err(_) => error!("Received a non-handshake message during the registration handshake! It's getting dropped."), diff --git a/gateway/gateway-requests/src/types.rs b/gateway/gateway-requests/src/types.rs index bfa20a9273..f73f85bca5 100644 --- a/gateway/gateway-requests/src/types.rs +++ b/gateway/gateway-requests/src/types.rs @@ -4,7 +4,7 @@ use crate::authentication::encrypted_address::EncryptedAddressBytes; use crate::iv::IV; use crate::registration::handshake::SharedKeys; -use crate::GatewayMacSize; +use crate::{GatewayMacSize, PROTOCOL_VERSION}; use crypto::generic_array::typenum::Unsigned; use crypto::hmac::recompute_keyed_hmac_and_verify_tag; use crypto::symmetric::stream_cipher; @@ -28,13 +28,22 @@ use credentials::token::bandwidth::TokenCredential; #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type", rename_all = "camelCase")] pub enum RegistrationHandshake { - HandshakePayload { data: Vec }, - HandshakeError { message: String }, + HandshakePayload { + #[serde(default)] + protocol_version: Option, + data: Vec, + }, + HandshakeError { + message: String, + }, } impl RegistrationHandshake { pub fn new_payload(data: Vec) -> Self { - RegistrationHandshake::HandshakePayload { data } + RegistrationHandshake::HandshakePayload { + protocol_version: Some(PROTOCOL_VERSION), + data, + } } pub fn new_error>(message: S) -> Self { @@ -115,12 +124,16 @@ pub enum ClientControlRequest { // TODO: should this also contain a MAC considering that at this point we already // have the shared key derived? Authenticate { + #[serde(default)] + protocol_version: Option, address: String, enc_address: String, iv: String, }, #[serde(alias = "handshakePayload")] RegisterHandshakeInitRequest { + #[serde(default)] + protocol_version: Option, data: Vec, }, BandwidthCredential { @@ -137,6 +150,7 @@ impl ClientControlRequest { iv: IV, ) -> Self { ClientControlRequest::Authenticate { + protocol_version: Some(PROTOCOL_VERSION), address: address.as_base58_string(), enc_address: enc_address.to_base58_string(), iv: iv.to_base58_string(), @@ -223,10 +237,14 @@ impl TryInto for ClientControlRequest { #[serde(tag = "type", rename_all = "camelCase")] pub enum ServerResponse { Authenticate { + #[serde(default)] + protocol_version: Option, status: bool, bandwidth_remaining: i64, }, Register { + #[serde(default)] + protocol_version: Option, status: bool, }, Bandwidth { @@ -381,14 +399,37 @@ mod tests { #[test] fn handshake_payload_can_be_deserialized_into_register_handshake_init_request() { let handshake_data = vec![1, 2, 3, 4, 5, 6]; - let handshake_payload = RegistrationHandshake::HandshakePayload { + let handshake_payload_with_protocol = RegistrationHandshake::HandshakePayload { + protocol_version: Some(42), data: handshake_data.clone(), }; - let serialized = serde_json::to_string(&handshake_payload).unwrap(); + let serialized = serde_json::to_string(&handshake_payload_with_protocol).unwrap(); let deserialized = ClientControlRequest::try_from(serialized).unwrap(); match deserialized { - ClientControlRequest::RegisterHandshakeInitRequest { data } => { + ClientControlRequest::RegisterHandshakeInitRequest { + protocol_version, + data, + } => { + assert_eq!(protocol_version, Some(42)); + assert_eq!(data, handshake_data) + } + _ => unreachable!("this branch shouldn't have been reached!"), + } + + let handshake_payload_without_protocol = RegistrationHandshake::HandshakePayload { + protocol_version: None, + data: handshake_data.clone(), + }; + let serialized = serde_json::to_string(&handshake_payload_without_protocol).unwrap(); + let deserialized = ClientControlRequest::try_from(serialized).unwrap(); + + match deserialized { + ClientControlRequest::RegisterHandshakeInitRequest { + protocol_version, + data, + } => { + assert!(protocol_version.is_none()); assert_eq!(data, handshake_data) } _ => unreachable!("this branch shouldn't have been reached!"), diff --git a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs b/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs index 8f0ffcd098..e83c23e5e9 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/coconut.rs @@ -8,11 +8,11 @@ use log::*; use coconut_interface::{Credential, VerificationKey}; use validator_client::{ nymd::{ - cosmwasm_client::logs::find_attribute, + cosmwasm_client::logs::{find_attribute, BANDWIDTH_PROPOSAL_ID}, traits::{CoconutBandwidthSigningClient, MultisigQueryClient, MultisigSigningClient}, - Coin, Fee, NymdClient, SigningNymdClient, + Coin, Fee, SigningNymdClient, }, - ApiClient, + Client, CoconutApiClient, }; use super::authenticated::RequestHandlingError; @@ -21,16 +21,16 @@ const ONE_HOUR_SEC: u64 = 3600; const MAX_FEEGRANT_UNYM: u128 = 10000; pub(crate) struct CoconutVerifier { - api_clients: Vec, - nymd_client: NymdClient, + api_clients: Vec, + nymd_client: Client, mix_denom_base: String, aggregated_verification_key: VerificationKey, } impl CoconutVerifier { pub fn new( - api_clients: Vec, - nymd_client: NymdClient, + api_clients: Vec, + nymd_client: Client, mix_denom_base: String, aggregated_verification_key: VerificationKey, ) -> Result { @@ -59,17 +59,18 @@ impl CoconutVerifier { let res = self .nymd_client + .nymd .spend_credential( Coin::new( credential.voucher_value().into(), self.mix_denom_base.clone(), ), credential.blinded_serial_number(), - self.nymd_client.address().to_string(), + self.nymd_client.nymd.address().to_string(), None, ) .await?; - let proposal_id = find_attribute(&res.logs, "wasm", "proposal_id") + let proposal_id = find_attribute(&res.logs, "wasm", BANDWIDTH_PROPOSAL_ID) .ok_or(RequestHandlingError::ProposalIdError { reason: String::from("proposal id not found"), })? @@ -79,7 +80,7 @@ impl CoconutVerifier { reason: String::from("proposal id could not be parsed to u64"), })?; - let proposal = self.nymd_client.get_proposal(proposal_id).await?; + let proposal = self.nymd_client.nymd.get_proposal(proposal_id).await?; if !credential.has_blinded_serial_number(&proposal.description)? { return Err(RequestHandlingError::ProposalIdError { reason: String::from("proposal has different serial number"), @@ -89,13 +90,13 @@ impl CoconutVerifier { let req = validator_api_requests::coconut::VerifyCredentialBody::new( credential.clone(), proposal_id, - self.nymd_client.address().clone(), + self.nymd_client.nymd.address().clone(), ); for client in self.api_clients.iter() { - let api_cosmos_addr = client.get_cosmos_address().await?.addr; self.nymd_client + .nymd .grant_allowance( - &api_cosmos_addr, + &client.cosmos_address, vec![Coin::new(MAX_FEEGRANT_UNYM, self.mix_denom_base.clone())], SystemTime::now().checked_add(Duration::from_secs(ONE_HOUR_SEC)), // It would be nice to be able to filter deeper, but for now only the msg type filter is avaialable @@ -104,20 +105,24 @@ impl CoconutVerifier { None, ) .await?; - let ret = client.verify_bandwidth_credential(&req).await; + let ret = client.api_client.verify_bandwidth_credential(&req).await; self.nymd_client + .nymd .revoke_allowance( - &api_cosmos_addr, + &client.cosmos_address, "Cleanup the previous allowance for releasing funds".to_string(), revoke_fee.clone(), ) .await?; if !ret?.verification_result { - debug!("Validator {} didn't accept the credential. It will probably vote No on the spending proposal", client.validator_api.current_url()); + debug!("Validator {} didn't accept the credential. It will probably vote No on the spending proposal", client.api_client.validator_api.current_url()); } } - self.nymd_client.execute_proposal(proposal_id, None).await?; + self.nymd_client + .nymd + .execute_proposal(proposal_id, None) + .await?; Ok(()) } diff --git a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs index abe2f30a87..14116c948b 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler/fresh.rs @@ -18,7 +18,7 @@ use gateway_requests::iv::{IVConversionError, IV}; use gateway_requests::registration::handshake::error::HandshakeError; use gateway_requests::registration::handshake::{gateway_handshake, SharedKeys}; use gateway_requests::types::{ClientControlRequest, ServerResponse}; -use gateway_requests::BinaryResponse; +use gateway_requests::{BinaryResponse, PROTOCOL_VERSION}; use log::*; use mixnet_client::forwarder::MixForwardingSender; use nymsphinx::DestinationAddressBytes; @@ -55,6 +55,9 @@ enum InitialAuthenticationError { #[error("Experienced connection error - {0}")] ConnectionError(#[from] WsError), + + #[error("Attempted to negotiate connection with client using incompatible protocol version. Ours is {current} and the client reports {client:?}")] + IncompatibleProtocol { client: Option, current: u8 }, } impl InitialAuthenticationError { @@ -279,10 +282,7 @@ where // push them to the client if let Err(err) = self.push_packets_to_client(shared_keys, messages).await { - warn!( - "We failed to send stored messages to fresh client - {}", - err - ); + warn!("We failed to send stored messages to fresh client - {err}",); return Err(InitialAuthenticationError::ConnectionError(err)); } else { // if it was successful - remove them from the store @@ -342,6 +342,33 @@ where } } + fn check_client_protocol( + &self, + client_protocol: Option, + ) -> Result<(), InitialAuthenticationError> { + // right now there are no failure cases here, but this might change in the future + match client_protocol { + None => { + warn!("the client we're connected to has not specified its protocol version. It's probably running version < 1.1.X, but that's still fine for now. It will become a hard error in 1.2.0"); + // note: in 1.2.0 we will have to return a hard error here + Ok(()) + } + Some(v) if v != PROTOCOL_VERSION => { + let err = InitialAuthenticationError::IncompatibleProtocol { + client: Some(v), + current: PROTOCOL_VERSION, + }; + error!("{err}"); + Err(err) + } + + Some(_) => { + info!("the client is using exactly the same protocol version as we are. We're good to continue!"); + Ok(()) + } + } + } + /// Using the received challenge data, i.e. client's address as well the ciphertext of it plus /// a fresh IV, attempts to authenticate the client by checking whether the ciphertext matches /// the expected value if encrypted with the shared key. @@ -389,6 +416,7 @@ where /// * `iv`: fresh IV received with the request. async fn handle_authenticate( &mut self, + client_protocol_version: Option, address: String, enc_address: String, iv: String, @@ -396,6 +424,8 @@ where where S: AsyncRead + AsyncWrite + Unpin, { + self.check_client_protocol(client_protocol_version)?; + let address = DestinationAddressBytes::try_from_base58_string(address) .map_err(|err| InitialAuthenticationError::MalformedClientAddress(err.to_string()))?; let encrypted_address = EncryptedAddressBytes::try_from_base58_string(enc_address)?; @@ -420,6 +450,7 @@ where Ok(InitialAuthResult::new( client_details, ServerResponse::Authenticate { + protocol_version: Some(PROTOCOL_VERSION), status, bandwidth_remaining, }, @@ -474,11 +505,14 @@ where /// * `init_data`: init payload of the registration handshake. async fn handle_register( &mut self, + client_protocol_version: Option, init_data: Vec, ) -> Result where S: AsyncRead + AsyncWrite + Unpin + Send, { + self.check_client_protocol(client_protocol_version)?; + let remote_identity = Self::extract_remote_identity_from_register_init(&init_data)?; let remote_address = remote_identity.derive_destination_address(); @@ -493,7 +527,10 @@ where Ok(InitialAuthResult::new( Some(client_details), - ServerResponse::Register { status }, + ServerResponse::Register { + protocol_version: Some(PROTOCOL_VERSION), + status, + }, )) } @@ -513,13 +550,18 @@ where if let Ok(request) = ClientControlRequest::try_from(raw_request) { match request { ClientControlRequest::Authenticate { + protocol_version, address, enc_address, iv, - } => self.handle_authenticate(address, enc_address, iv).await, - ClientControlRequest::RegisterHandshakeInitRequest { data } => { - self.handle_register(data).await + } => { + self.handle_authenticate(protocol_version, address, enc_address, iv) + .await } + ClientControlRequest::RegisterHandshakeInitRequest { + protocol_version, + data, + } => self.handle_register(protocol_version, data).await, // won't accept anything else (like bandwidth) without prior authentication _ => Err(InitialAuthenticationError::InvalidRequest), } diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index fd31af1af7..8c2a541f12 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -25,9 +25,9 @@ use crate::config::persistence::pathfinder::GatewayPathfinder; #[cfg(feature = "coconut")] use crate::node::client_handling::websocket::connection_handler::coconut::CoconutVerifier; #[cfg(feature = "coconut")] -use credentials::obtain_aggregate_verification_key; +use credentials::coconut::utils::obtain_aggregate_verification_key; #[cfg(feature = "coconut")] -use validator_client::nymd; +use validator_client::{Client, CoconutApiClient}; use self::storage::PersistentStorage; @@ -242,32 +242,24 @@ where #[cfg(feature = "coconut")] fn random_nymd_client( &self, - ) -> validator_client::nymd::NymdClient { + ) -> validator_client::Client { let endpoints = self.config.get_validator_nymd_endpoints(); let validator_nymd = endpoints .choose(&mut thread_rng()) .expect("The list of validators is empty"); let network_details = NymNetworkDetails::new_from_env(); - let client_config = nymd::Config::try_from_nym_network_details(&network_details) - .expect("failed to construct valid validator client config with the provided network"); - - validator_client::nymd::NymdClient::connect_with_mnemonic( - client_config, - validator_nymd.as_ref(), - self.config.get_cosmos_mnemonic(), - None, + let client_config = validator_client::Config::try_from_nym_network_details( + &network_details, ) - .expect("Could not connect with mnemonic") - } + .expect("failed to construct valid validator client config with the provided network"); - #[cfg(feature = "coconut")] - fn all_api_clients(&self) -> Vec { - self.config - .get_validator_api_endpoints() - .into_iter() - .map(validator_client::ApiClient::new) - .collect() + let mut client = Client::new_signing(client_config, self.config.get_cosmos_mnemonic()) + .expect("Could not connect with mnemonic"); + client + .change_nymd(validator_nymd.clone()) + .expect("Could not use the random nymd URL"); + client } // TODO: ask DH whether this function still makes sense in ^0.10 @@ -306,20 +298,23 @@ where } #[cfg(feature = "coconut")] - let validators_verification_key = - obtain_aggregate_verification_key(&self.config.get_validator_api_endpoints()) + let coconut_verifier = { + let nymd_client = self.random_nymd_client(); + let api_clients = CoconutApiClient::all_coconut_api_clients(&nymd_client) + .await + .expect("Could not query all api clients"); + let validators_verification_key = obtain_aggregate_verification_key(&api_clients) .await .expect("failed to contact validators to obtain their verification keys"); - #[cfg(feature = "coconut")] - let nymd_client = self.random_nymd_client(); - #[cfg(feature = "coconut")] - let coconut_verifier = CoconutVerifier::new( - self.all_api_clients(), - nymd_client, - std::env::var(network_defaults::var_names::MIX_DENOM).expect("mix denom base not set"), - validators_verification_key, - ) - .expect("Could not create coconut verifier"); + CoconutVerifier::new( + api_clients, + nymd_client, + std::env::var(network_defaults::var_names::MIX_DENOM) + .expect("mix denom base not set"), + validators_verification_key, + ) + .expect("Could not create coconut verifier") + }; let mix_forwarding_channel = self.start_packet_forwarder(); diff --git a/nym-chitchat/Cargo.lock b/nym-chitchat/Cargo.lock deleted file mode 100644 index 4c396ed66d..0000000000 --- a/nym-chitchat/Cargo.lock +++ /dev/null @@ -1,1983 +0,0 @@ -# This file is automatically @generated by Cargo. -# 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" -dependencies = [ - "lazy_static", - "regex", -] - -[[package]] -name = "aead" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" -dependencies = [ - "generic-array", -] - -[[package]] -name = "aes" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", - "opaque-debug", -] - -[[package]] -name = "aes-gcm" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "aho-corasick" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" -dependencies = [ - "memchr", -] - -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - -[[package]] -name = "anyhow" -version = "1.0.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" - -[[package]] -name = "assert_cmd" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93ae1ddd39efd67689deb1979d80bad3bf7f2b09c6e6117c8d1f2443b5e2f83e" -dependencies = [ - "bstr", - "doc-comment", - "predicates", - "predicates-core", - "predicates-tree", - "wait-timeout", -] - -[[package]] -name = "async-trait" -version = "0.1.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "base64" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" - -[[package]] -name = "base64" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "block-buffer" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bstr" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" -dependencies = [ - "lazy_static", - "memchr", - "regex-automata", -] - -[[package]] -name = "bumpalo" -version = "3.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" - -[[package]] -name = "bytes" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" - -[[package]] -name = "bytes" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0b3de4a0c5e67e16066a0715723abd91edc2f9001d09c46e1dca929351e130e" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "chitchat" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e16bc6492a6744d6e322e22562c3ab89404ba326aedb65a570799d2e042687" -dependencies = [ - "anyhow", - "async-trait", - "bytes 1.2.0", - "rand", - "serde", - "tokio", - "tokio-stream", - "tracing", -] - -[[package]] -name = "chitchat-test" -version = "0.3.0" -dependencies = [ - "anyhow", - "assert_cmd", - "chitchat", - "cool-id-generator", - "env_logger", - "once_cell", - "poem", - "poem-openapi", - "predicates", - "reqwest", - "serde", - "serde_json", - "structopt", - "tokio", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "chrono" -version = "0.4.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" -dependencies = [ - "libc", - "num-integer", - "num-traits", - "time 0.1.44", - "winapi", -] - -[[package]] -name = "cipher" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" -dependencies = [ - "generic-array", -] - -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim 0.8.0", - "textwrap", - "unicode-width", - "vec_map", -] - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "cookie" -version = "0.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" -dependencies = [ - "aes-gcm", - "base64 0.13.0", - "hkdf", - "hmac", - "percent-encoding", - "rand", - "sha2", - "subtle", - "time 0.3.11", - "version_check", -] - -[[package]] -name = "cool-id-generator" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "946cf88fb171128bc025588b9b408982814ace1a1fb31ffb9bfa63d955f0b43d" -dependencies = [ - "rand", -] - -[[package]] -name = "cpufeatures" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "ctr" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" -dependencies = [ - "cipher", -] - -[[package]] -name = "darling" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim 0.10.0", - "syn", -] - -[[package]] -name = "darling_macro" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5" -dependencies = [ - "darling_core", - "quote", - "syn", -] - -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", -] - -[[package]] -name = "difflib" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" - -[[package]] -name = "digest" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - -[[package]] -name = "either" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" - -[[package]] -name = "encoding_rs" -version = "0.8.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "env_logger" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - -[[package]] -name = "fastrand" -version = "1.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" -dependencies = [ - "instant", -] - -[[package]] -name = "float-cmp" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" -dependencies = [ - "num-traits", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" -dependencies = [ - "matches", - "percent-encoding", -] - -[[package]] -name = "futures-channel" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" -dependencies = [ - "futures-core", -] - -[[package]] -name = "futures-core" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" - -[[package]] -name = "futures-io" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" - -[[package]] -name = "futures-macro" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" - -[[package]] -name = "futures-task" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" - -[[package]] -name = "futures-util" -version = "0.3.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" -dependencies = [ - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" -dependencies = [ - "cfg-if", - "libc", - "wasi 0.11.0+wasi-snapshot-preview1", -] - -[[package]] -name = "ghash" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "h2" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" -dependencies = [ - "bytes 1.2.0", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "headers" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" -dependencies = [ - "base64 0.13.0", - "bitflags", - "bytes 1.2.0", - "headers-core", - "http", - "httpdate", - "mime", - "sha-1", -] - -[[package]] -name = "headers-core" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" -dependencies = [ - "http", -] - -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hkdf" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "http" -version = "0.2.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" -dependencies = [ - "bytes 1.2.0", - "fnv", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" -dependencies = [ - "bytes 1.2.0", - "http", - "pin-project-lite", -] - -[[package]] -name = "httparse" -version = "1.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" - -[[package]] -name = "httpdate" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" - -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - -[[package]] -name = "hyper" -version = "0.14.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" -dependencies = [ - "bytes 1.2.0", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "indexmap" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" -dependencies = [ - "autocfg", - "hashbrown", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "ipnet" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" - -[[package]] -name = "itertools" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" - -[[package]] -name = "js-sys" -version = "0.3.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" - -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - -[[package]] -name = "lock_api" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - -[[package]] -name = "memchr" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" - -[[package]] -name = "mime" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" - -[[package]] -name = "mio" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" -dependencies = [ - "libc", - "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys", -] - -[[package]] -name = "multer" -version = "2.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a30ba6d97eb198c5e8a35d67d5779d6680cca35652a60ee90fc23dc431d4fde8" -dependencies = [ - "bytes 1.2.0", - "encoding_rs", - "futures-util", - "http", - "httparse", - "log", - "memchr", - "mime", - "spin", - "tokio", - "version_check", -] - -[[package]] -name = "normalize-line-endings" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" - -[[package]] -name = "num-integer" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" -dependencies = [ - "autocfg", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - -[[package]] -name = "once_cell" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" - -[[package]] -name = "opaque-debug" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" - -[[package]] -name = "parking_lot" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-sys", -] - -[[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - -[[package]] -name = "pin-project-lite" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "poem" -version = "1.3.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e479ed477ea11939bea754109eaa024e55bf1639e19c1753fb250026351da8" -dependencies = [ - "async-trait", - "bytes 1.2.0", - "chrono", - "cookie", - "futures-util", - "headers", - "http", - "hyper", - "mime", - "multer", - "parking_lot", - "percent-encoding", - "pin-project-lite", - "poem-derive", - "regex", - "rfc7239", - "serde", - "serde_json", - "serde_urlencoded", - "smallvec", - "tempfile", - "thiserror", - "time 0.3.11", - "tokio", - "tokio-stream", - "tokio-util", - "tracing", - "typed-headers", -] - -[[package]] -name = "poem-derive" -version = "1.3.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d454ac05b5299eb8e6261214fcb823f0c3c1b02f47167f8809c9dc2db0ee033" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "poem-openapi" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c27cd1c97d45a9552b0c80bab1d51f93a1050c3f7f91e9312239b3b87c336b0f" -dependencies = [ - "base64 0.13.0", - "bytes 1.2.0", - "derive_more", - "futures-util", - "mime", - "num-traits", - "poem", - "poem-openapi-derive", - "regex", - "serde", - "serde_json", - "serde_urlencoded", - "serde_yaml", - "thiserror", - "tokio", -] - -[[package]] -name = "poem-openapi-derive" -version = "2.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5105e426a8a747fefc4be06ab7705cb63bc3ab1b2db373867e1d83b1d95877a" -dependencies = [ - "Inflector", - "darling", - "http", - "indexmap", - "mime", - "proc-macro-crate", - "proc-macro2", - "quote", - "regex", - "syn", - "thiserror", -] - -[[package]] -name = "polyval" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" - -[[package]] -name = "predicates" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5aab5be6e4732b473071984b3164dbbfb7a3674d30ea5ff44410b6bcd960c3c" -dependencies = [ - "difflib", - "float-cmp", - "itertools", - "normalize-line-endings", - "predicates-core", - "regex", -] - -[[package]] -name = "predicates-core" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da1c2388b1513e1b605fcec39a95e0a9e8ef088f71443ef37099fa9ae6673fcb" - -[[package]] -name = "predicates-tree" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d86de6de25020a36c6d3643a86d9a6a9f552107c0559c60ea03551b5e16c032" -dependencies = [ - "predicates-core", - "termtree", -] - -[[package]] -name = "proc-macro-crate" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" -dependencies = [ - "thiserror", - "toml", -] - -[[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", - "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-macro2" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" -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", -] - -[[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", -] - -[[package]] -name = "rand_core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" -dependencies = [ - "getrandom", -] - -[[package]] -name = "redox_syscall" -version = "0.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" - -[[package]] -name = "regex-syntax" -version = "0.6.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" - -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - -[[package]] -name = "reqwest" -version = "0.11.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" -dependencies = [ - "base64 0.13.0", - "bytes 1.2.0", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "hyper", - "ipnet", - "js-sys", - "lazy_static", - "log", - "mime", - "percent-encoding", - "pin-project-lite", - "serde", - "serde_json", - "serde_urlencoded", - "tokio", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", - "winreg", -] - -[[package]] -name = "rfc7239" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "087317b3cf7eb481f13bd9025d729324b7cd068d6f470e2d76d049e191f5ba47" -dependencies = [ - "uncased", -] - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "ryu" -version = "1.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "semver" -version = "1.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1" - -[[package]] -name = "serde" -version = "1.0.139" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.139" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.82" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" -dependencies = [ - "itoa", - "ryu", - "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.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" -dependencies = [ - "indexmap", - "ryu", - "serde", - "yaml-rust", -] - -[[package]] -name = "sha-1" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sha2" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "sharded-slab" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "slab" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1" - -[[package]] -name = "socket2" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "spin" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09" - -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "structopt" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" -dependencies = [ - "clap", - "lazy_static", - "structopt-derive", -] - -[[package]] -name = "structopt-derive" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "subtle" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" - -[[package]] -name = "syn" -version = "1.0.98" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" -dependencies = [ - "cfg-if", - "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", -] - -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "termtree" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "507e9898683b6c43a9aa55b64259b721b52ba226e0f3779137e50ad114a4c90b" - -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "thiserror" -version = "1.0.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" -dependencies = [ - "once_cell", -] - -[[package]] -name = "time" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" -dependencies = [ - "itoa", - "libc", - "num_threads", - "time-macros", -] - -[[package]] -name = "time-macros" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" - -[[package]] -name = "tinyvec" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" - -[[package]] -name = "tokio" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57aec3cfa4c296db7255446efb4928a6be304b431a806216105542a67b6ca82e" -dependencies = [ - "autocfg", - "bytes 1.2.0", - "libc", - "memchr", - "mio", - "num_cpus", - "once_cell", - "pin-project-lite", - "socket2", - "tokio-macros", - "winapi", -] - -[[package]] -name = "tokio-macros" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-stream" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df54d54117d6fdc4e4fea40fe1e4e566b3505700e148a6827e59b34b0d2600d9" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-util" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" -dependencies = [ - "bytes 1.2.0", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", - "tracing", -] - -[[package]] -name = "toml" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" -dependencies = [ - "serde", -] - -[[package]] -name = "tower-service" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" - -[[package]] -name = "tracing" -version = "0.1.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" -dependencies = [ - "cfg-if", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" -dependencies = [ - "lazy_static", - "log", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a713421342a5a666b7577783721d3117f1b69a393df803ee17bb73b1e122a59" -dependencies = [ - "ansi_term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" - -[[package]] -name = "typed-headers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3179a61e9eccceead5f1574fd173cf2e162ac42638b9bf214c6ad0baf7efa24a" -dependencies = [ - "base64 0.11.0", - "bytes 0.5.6", - "chrono", - "http", - "mime", -] - -[[package]] -name = "typenum" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" - -[[package]] -name = "uncased" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b01702b0fd0b3fadcf98e098780badda8742d4f4a7676615cad90e8ac73622" -dependencies = [ - "version_check", -] - -[[package]] -name = "unicode-bidi" -version = "0.3.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" - -[[package]] -name = "unicode-ident" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" - -[[package]] -name = "unicode-normalization" -version = "0.1.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" - -[[package]] -name = "unicode-width" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" - -[[package]] -name = "universal-hash" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" -dependencies = [ - "generic-array", - "subtle", -] - -[[package]] -name = "url" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" -dependencies = [ - "form_urlencoded", - "idna", - "matches", - "percent-encoding", -] - -[[package]] -name = "valuable" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" - -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "wait-timeout" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" -dependencies = [ - "libc", -] - -[[package]] -name = "want" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" -dependencies = [ - "log", - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasm-bindgen" -version = "0.2.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" -dependencies = [ - "bumpalo", - "lazy_static", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" -dependencies = [ - "cfg-if", - "js-sys", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.81" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" - -[[package]] -name = "web-sys" -version = "0.3.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[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.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[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.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" -dependencies = [ - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" - -[[package]] -name = "windows_i686_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" - -[[package]] -name = "windows_i686_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.36.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" - -[[package]] -name = "winreg" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" -dependencies = [ - "winapi", -] - -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] diff --git a/nym-chitchat/Cargo.toml b/nym-chitchat/Cargo.toml deleted file mode 100644 index eae29e5738..0000000000 --- a/nym-chitchat/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "chitchat-test" -version = "0.3.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -chitchat = "0.4" -poem = "1" -poem-openapi = {version="2", features = ["swagger-ui"] } -structopt = "0.3" -tokio = { version = "1.14.0", features = ["net", "sync", "rt-multi-thread", "macros", "time"] } -serde = { version="1", features=["derive"] } -serde_json = "1" -anyhow = "1" -once_cell = "1" -tracing = "0.1" -tracing-subscriber = "0.3" -cool-id-generator = "1" -env_logger = "0.9" - -[dev-dependencies] -assert_cmd = "2" -predicates = "2" -reqwest = { version = "0.11", default-features=false, features = ["blocking", "json"] } - -[workspace] diff --git a/nym-chitchat/README.md b/nym-chitchat/README.md deleted file mode 100644 index 2fd61cd169..0000000000 --- a/nym-chitchat/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Chitchat test - -Runs simple chitchat servers, mostly copied over from https://github.com/quickwit-oss/chitchat - -## Example - -```bash -# Starts 5 servers and joins them into a cluster on localhost ports 10000-10004 -# All servers print cluster state on `/` ie 127.0.0.1:10000 -# `/docs` endpoint has an open api with a key value setter, set it on one node and observe how the state propagates to the other nodes -# NodeState is a regular BTreeMap -./run-servers.sh - -# run killall chitchat-test after you're done, as the servers will continue to run forever in the background -``` diff --git a/nym-chitchat/run-servers.sh b/nym-chitchat/run-servers.sh deleted file mode 100755 index 9e08abac3c..0000000000 --- a/nym-chitchat/run-servers.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/bin/bash - -killall chitchat-test - -cargo build --release - -for i in $(seq 10000 10004) -do - listen_addr="127.0.0.1:$i"; - echo ${listen_addr}; - cargo run --release -- --listen_addr ${listen_addr} --seed 127.0.0.1:10000 --node_id node_$i & -done; - -read -kill 0 diff --git a/nym-chitchat/src/main.rs b/nym-chitchat/src/main.rs deleted file mode 100644 index dc0899913d..0000000000 --- a/nym-chitchat/src/main.rs +++ /dev/null @@ -1,123 +0,0 @@ -use std::net::SocketAddr; -use std::sync::Arc; -use std::time::Duration; - -use chitchat::transport::UdpTransport; -use chitchat::{spawn_chitchat, Chitchat, ChitchatConfig, FailureDetectorConfig, NodeId}; -use cool_id_generator::Size; -use poem::listener::TcpListener; -use poem::{Route, Server}; -use poem_openapi::param::Query; -use poem_openapi::payload::Json; -use poem_openapi::{OpenApi, OpenApiService}; -use structopt::StructOpt; -use tokio::sync::Mutex; - -use chitchat::ClusterStateSnapshot; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug)] -pub struct ApiResponse { - pub cluster_id: String, - pub cluster_state: ClusterStateSnapshot, - pub live_nodes: Vec, - pub dead_nodes: Vec, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct SetKeyValueResponse { - pub status: bool, -} - -struct Api { - chitchat: Arc>, -} - -#[OpenApi] -impl Api { - /// Chitchat state - #[oai(path = "/", method = "get")] - async fn index(&self) -> Json { - let chitchat_guard = self.chitchat.lock().await; - let response = ApiResponse { - cluster_id: chitchat_guard.cluster_id().to_string(), - cluster_state: chitchat_guard.state_snapshot(), - live_nodes: chitchat_guard.live_nodes().cloned().collect::>(), - dead_nodes: chitchat_guard.dead_nodes().cloned().collect::>(), - }; - Json(serde_json::to_value(&response).unwrap()) - } - - /// Set a key & value on this node (with no validation). - #[oai(path = "/set_kv/", method = "get")] - async fn set_kv(&self, key: Query, value: Query) -> Json { - let mut chitchat_guard = self.chitchat.lock().await; - - let cc_state = chitchat_guard.self_node_state(); - cc_state.set(key.as_str(), value.as_str()); - - Json(serde_json::to_value(&SetKeyValueResponse { status: true }).unwrap()) - } -} - -#[derive(Debug, StructOpt)] -#[structopt(name = "chitchat", about = "Chitchat test server.")] -struct Opt { - /// Defines the socket addr on which we should listen to. - #[structopt(long = "listen_addr", default_value = "127.0.0.1:10000")] - listen_addr: SocketAddr, - /// Defines the socket_address (host:port) other servers should use to - /// reach this server. - /// - /// It defaults to the listen address, but this is only valid - /// when all server are running on the same server. - #[structopt(long = "public_addr")] - public_addr: Option, - - /// Node id. Has to be unique. If None, the node_id will be generated from - /// the public_addr and a random suffix. - #[structopt(long = "node_id")] - node_id: Option, - - #[structopt(long = "seed")] - seeds: Vec, - - #[structopt(long = "interval_ms", default_value = "500")] - interval: u64, -} - -fn generate_server_id(public_addr: SocketAddr) -> String { - let cool_id = cool_id_generator::get_id(Size::Medium); - format!("server:{}-{}", public_addr, cool_id) -} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - tracing_subscriber::fmt::init(); - let opt = Opt::from_args(); - println!("{:?}", opt); - let public_addr = opt.public_addr.unwrap_or(opt.listen_addr); - let node_id_str = opt - .node_id - .unwrap_or_else(|| generate_server_id(public_addr)); - let node_id = NodeId::new(node_id_str, public_addr); - let config = ChitchatConfig { - node_id, - cluster_id: "testing".to_string(), - gossip_interval: Duration::from_millis(opt.interval), - listen_addr: opt.listen_addr, - seed_nodes: opt.seeds.clone(), - failure_detector_config: FailureDetectorConfig::default(), - }; - let chitchat_handler = spawn_chitchat(config, Vec::new(), &UdpTransport).await?; - let chitchat = chitchat_handler.chitchat(); - let api = Api { chitchat }; - let api_service = OpenApiService::new(api, "Hello World", "1.0") - .server(&format!("http://{}/", opt.listen_addr)); - let docs = api_service.swagger_ui(); - let app = Route::new().nest("/", api_service).nest("/docs", docs); - Server::new(TcpListener::bind(&opt.listen_addr)) - .run(app) - .await?; - Ok(()) -} diff --git a/nym-connect/.storybook/preview.js b/nym-connect/.storybook/preview.js index 2dbe041198..9c14c3ec99 100644 --- a/nym-connect/.storybook/preview.js +++ b/nym-connect/.storybook/preview.js @@ -1,26 +1,26 @@ import { NymMixnetTheme } from '../src/theme'; -import { ClientContextProvider } from '../src/context/main'; import { Fonts } from './preview-fonts'; - -const withThemeProvider= (Story, context) =>{ +import { MockProvider } from '../src/context/mocks/main'; +const withThemeProvider = (Story, context) => { return ( - - + + - + - ) -} + ); +}; + export const decorators = [withThemeProvider]; export const parameters = { - actions: { argTypesRegex: "^on[A-Z].*" }, + actions: { argTypesRegex: '^on[A-Z].*' }, controls: { matchers: { color: /(background|color)$/i, date: /Date$/, }, }, -} \ No newline at end of file +}; diff --git a/nym-connect/Cargo.lock b/nym-connect/Cargo.lock index 40bcd330e4..22331cf94d 100644 --- a/nym-connect/Cargo.lock +++ b/nym-connect/Cargo.lock @@ -268,6 +268,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "blake2" version = "0.8.1" @@ -334,11 +346,25 @@ dependencies = [ "digest 0.9.0", "ff 0.10.1", "group 0.10.0", - "pairing", + "pairing 0.20.0", "rand_core 0.6.4", "subtle 2.4.1", ] +[[package]] +name = "bls12_381" +version = "0.6.0" +source = "git+https://github.com/jstuczyn/bls12_381?branch=gt-serialisation#10fb6f700bfda17c8475af3bfd31e3fec15f2278" +dependencies = [ + "digest 0.9.0", + "ff 0.11.1", + "group 0.11.0", + "pairing 0.21.0", + "rand_core 0.6.4", + "subtle 2.4.1", + "zeroize", +] + [[package]] name = "brotli" version = "3.3.4" @@ -599,6 +625,7 @@ name = "client-connections" version = "0.1.0" dependencies = [ "futures", + "log", ] [[package]] @@ -683,6 +710,18 @@ dependencies = [ "serde", ] +[[package]] +name = "coconut-dkg-common" +version = "0.1.0" +dependencies = [ + "contracts-common", + "cosmwasm-std", + "cw-utils", + "multisig-contract-common", + "schemars", + "serde", +] + [[package]] name = "coconut-interface" version = "0.1.0" @@ -759,6 +798,7 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" name = "contracts-common" version = "0.1.0" dependencies = [ + "bs58", "cosmwasm-std", "schemars", "serde", @@ -931,11 +971,10 @@ dependencies = [ name = "credentials" version = "0.1.0" dependencies = [ - "bls12_381", + "bls12_381 0.5.0", "coconut-interface", "crypto", "thiserror", - "url", "validator-api-requests", "validator-client", ] @@ -1125,9 +1164,9 @@ checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" [[package]] name = "curve25519-dalek" -version = "3.2.1" +version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" dependencies = [ "byteorder", "digest 0.9.0", @@ -1159,6 +1198,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw2" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cf4639517490dd36b333bbd6c4fbd92e325fd0acf4683b41753bc5eb63bfc1" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", +] + [[package]] name = "cw3" version = "0.13.4" @@ -1171,6 +1222,22 @@ dependencies = [ "serde", ] +[[package]] +name = "cw3-fixed-multisig" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df54aa54c13f405ec4ab36b6217538bc957d439eee58f89312db05a79caf6706" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw3", + "schemars", + "serde", + "thiserror", +] + [[package]] name = "cw4" version = "0.13.4" @@ -1326,6 +1393,27 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dkg" +version = "0.1.0" +dependencies = [ + "bitvec", + "bls12_381 0.6.0", + "bs58", + "ff 0.11.1", + "group 0.11.0", + "lazy_static", + "pemstore", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde", + "serde_derive", + "sha2 0.9.9", + "thiserror", + "zeroize", +] + [[package]] name = "doc-comment" version = "0.3.3" @@ -1649,6 +1737,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -2105,6 +2199,7 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" dependencies = [ + "byteorder", "ff 0.11.1", "rand_core 0.6.4", "subtle 2.4.1", @@ -3029,6 +3124,7 @@ dependencies = [ "cosmwasm-std", "cw-utils", "cw3", + "cw3-fixed-multisig", "cw4", "schemars", "serde", @@ -3281,6 +3377,7 @@ dependencies = [ "serde", "snafu", "socks5-requests", + "tap", "task", "thiserror", "tokio", @@ -3295,13 +3392,15 @@ dependencies = [ name = "nymcoconut" version = "0.5.0" dependencies = [ - "bls12_381", + "bls12_381 0.6.0", "bs58", "digest 0.9.0", - "ff 0.10.1", + "dkg", + "ff 0.11.1", "getrandom 0.2.7", - "group 0.10.0", + "group 0.11.0", "itertools", + "pemstore", "rand 0.8.5", "serde", "serde_derive", @@ -3559,6 +3658,15 @@ dependencies = [ "group 0.10.0", ] +[[package]] +name = "pairing" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2e415e349a3006dd7d9482cdab1c980a845bed1377777d768cb693a44540b42" +dependencies = [ + "group 0.11.0", +] + [[package]] name = "pango" version = "0.15.10" @@ -4074,6 +4182,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.6.5" @@ -5345,7 +5459,9 @@ dependencies = [ name = "task" version = "0.1.0" dependencies = [ + "futures", "log", + "thiserror", "tokio", ] @@ -5670,18 +5786,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c" [[package]] name = "thiserror" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" +checksum = "10deb33631e3c9018b9baf9dcbbc4f737320d2b576bac10f6aefa048fa407e3e" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.35" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" +checksum = "982d17546b47146b28f7c22e3d08465f6b8903d0ea13c1660d9d84a6e7adcdbb" dependencies = [ "proc-macro2", "quote", @@ -6096,6 +6212,7 @@ dependencies = [ "base64", "bip39", "coconut-bandwidth-contract-common", + "coconut-dkg-common", "coconut-interface", "colored", "config", @@ -6760,6 +6877,15 @@ dependencies = [ "windows-implement", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.20.0" @@ -6783,9 +6909,9 @@ dependencies = [ [[package]] name = "x25519-dalek" -version = "1.2.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2392b6b94a576b4e2bf3c5b2757d63f10ada8020a2e4d08ac849ebcf6ea8e077" +checksum = "5a0c105152107e3b96f6a00a65e86ce82d9b125230e1c4302940eca58ff71f4f" dependencies = [ "curve25519-dalek", "rand_core 0.5.1", @@ -6809,9 +6935,9 @@ checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" [[package]] name = "zeroize" -version = "1.3.0" +version = "1.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" dependencies = [ "zeroize_derive", ] diff --git a/nym-connect/Cargo.toml b/nym-connect/Cargo.toml index b9fb14e41b..3ee3f20be3 100644 --- a/nym-connect/Cargo.toml +++ b/nym-connect/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["src-tauri"] \ No newline at end of file +members = ["src-tauri"] diff --git a/nym-connect/README.md b/nym-connect/README.md index 5a247235ab..bbd9b55bbb 100644 --- a/nym-connect/README.md +++ b/nym-connect/README.md @@ -12,8 +12,8 @@ Nym Connects sets up a SOCKS5 proxy for local applications to use. ## Installation prerequisites - Linux / Mac - `Yarn` -- `NodeJS >= v16.8.0` -- `Rust & cargo >= v1.56` +- `NodeJS >= v16` +- `Rust & cargo` ## Installation prerequisites - Windows diff --git a/nym-connect/src-tauri/Cargo.toml b/nym-connect/src-tauri/Cargo.toml index 11f79849b7..dfaf452dd1 100644 --- a/nym-connect/src-tauri/Cargo.toml +++ b/nym-connect/src-tauri/Cargo.toml @@ -32,7 +32,7 @@ reqwest = { version = "0.11", features = ["json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" tap = "1.0.1" -tauri = { version = "^1.1.1", features = ["clipboard-write-text", "shell-open", "system-tray", "updater"] } +tauri = { version = "^1.1.1", features = ["clipboard-write-text", "macos-private-api", "shell-open", "system-tray", "updater", "window-close", "window-start-dragging"] } tendermint-rpc = "0.23.0" thiserror = "1.0" tokio = { version = "1.21.2", features = ["sync", "time"] } diff --git a/nym-connect/src-tauri/src/models/mod.rs b/nym-connect/src-tauri/src/models/mod.rs index ded6a479fc..e5076ac5c0 100644 --- a/nym-connect/src-tauri/src/models/mod.rs +++ b/nym-connect/src-tauri/src/models/mod.rs @@ -45,7 +45,7 @@ pub struct AppEventConnectionStatusChangedPayload { } #[cfg_attr(test, derive(ts_rs::TS))] -#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DirectoryService { pub id: String, pub description: String, @@ -53,7 +53,7 @@ pub struct DirectoryService { } #[cfg_attr(test, derive(ts_rs::TS))] -#[derive(Clone, serde::Serialize, serde::Deserialize)] +#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct DirectoryServiceProvider { pub id: String, pub description: String, diff --git a/nym-connect/src-tauri/src/operations/directory/mod.rs b/nym-connect/src-tauri/src/operations/directory/mod.rs index aaac2276d9..b6c45fa8c0 100644 --- a/nym-connect/src-tauri/src/operations/directory/mod.rs +++ b/nym-connect/src-tauri/src/operations/directory/mod.rs @@ -6,9 +6,11 @@ static SERVICE_PROVIDER_WELLKNOWN_URL: &str = #[tauri::command] pub async fn get_services() -> Result> { + log::trace!("Fetching services"); let res = reqwest::get(SERVICE_PROVIDER_WELLKNOWN_URL) .await? .json::>() .await?; + log::trace!("Received: {:#?}", res); Ok(res) } diff --git a/nym-connect/src-tauri/src/tasks.rs b/nym-connect/src-tauri/src/tasks.rs index 751f7c5566..086b0487b1 100644 --- a/nym-connect/src-tauri/src/tasks.rs +++ b/nym-connect/src-tauri/src/tasks.rs @@ -50,7 +50,7 @@ pub fn start_nym_socks5_client( .block_on(async move { socks5_client.run_and_listen(socks5_ctrl_rx).await }); if let Err(err) = result { - log::error!("SOCKS5 proxy failed to start: {err}"); + log::error!("SOCKS5 proxy failed: {err}"); socks5_status_tx .send(Socks5StatusMessage::FailedToStart) .expect("Failed to send status message back to main task"); @@ -66,6 +66,12 @@ pub fn start_nym_socks5_client( Ok((socks5_ctrl_tx, socks5_status_rx, used_gateway)) } +#[derive(Clone, serde::Serialize)] +struct Payload { + title: String, + message: String, +} + /// The disconnect listener listens to the channel setup between the socks5 proxy task and the main /// tauri task. Primarily it listens for shutdown messages, and updates the state accordingly. pub fn start_disconnect_listener( @@ -78,14 +84,39 @@ pub fn start_disconnect_listener( match status_receiver.await { Ok(Socks5StatusMessage::Stopped) => { log::info!("SOCKS5 task reported it has finished"); + window + .emit( + "socks5-event", + Payload { + title: "SOCKS5 finished".into(), + message: "SOCKS5 task reported it has finished".into(), + }, + ) + .unwrap(); } Ok(Socks5StatusMessage::FailedToStart) => { log::info!("SOCKS5 task reported it failed to start"); + window + .emit( + "socks5-event", + Payload { + title: "SOCKS5 error".into(), + message: "SOCKS5 failed to start".into(), + }, + ) + .unwrap(); } Err(_) => { log::info!("SOCKS5 task appears to have stopped abruptly"); - // TODO: we should probably generate some events here, or otherwise signal to the - // frontend. + window + .emit( + "socks5-event", + Payload { + title: "SOCKS5 error".into(), + message: "SOCKS5 stopped abruptly".into(), + }, + ) + .unwrap(); } } diff --git a/nym-connect/src-tauri/tauri.conf.json b/nym-connect/src-tauri/tauri.conf.json index c4fce76f06..b9cbfd8233 100644 --- a/nym-connect/src-tauri/tauri.conf.json +++ b/nym-connect/src-tauri/tauri.conf.json @@ -10,6 +10,7 @@ "beforeBuildCommand": "" }, "tauri": { + "macOSPrivateApi": true, "systemTray": { "iconPath": "icons/tray_icon.png", "iconAsTemplate": true @@ -18,13 +19,7 @@ "active": true, "targets": "all", "identifier": "net.nymtech.connect", - "icon": [ - "icons/32x32.png", - "icons/128x128.png", - "icons/128x128@2x.png", - "icons/icon.icns", - "icons/icon.ico" - ], + "icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"], "resources": [], "externalBin": [], "copyright": "Copyright © 2021-2022 Nym Technologies SA", @@ -49,9 +44,7 @@ }, "updater": { "active": true, - "endpoints": [ - "https://nymtech.net/.wellknown/connect/updater.json" - ], + "endpoints": ["https://nymtech.net/.wellknown/connect/updater.json"], "dialog": true, "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IENCNzQ2M0E5N0VFODE2NApSV1JrZ2U2WE9rYTNETTg1OTBKdE5uWUEra0hML2syOVUvQ2lxZmFZRzZ1T3NWbGM0eVRzUTVhVwo=" }, @@ -61,14 +54,20 @@ }, "clipboard": { "writeText": true + }, + "window": { + "startDragging": true, + "close": true } }, "windows": [ { "title": "NymConnect", "width": 240, - "height": 500, - "resizable": false + "height": 540, + "resizable": false, + "decorations": false, + "transparent": true } ], "security": { diff --git a/nym-connect/src/App.tsx b/nym-connect/src/App.tsx index 10a2995f34..90dd1a4230 100644 --- a/nym-connect/src/App.tsx +++ b/nym-connect/src/App.tsx @@ -1,37 +1,50 @@ -import React from 'react'; +import React, { useEffect } from 'react'; +import { DateTime } from 'luxon'; import { ConnectionStatusKind } from './types'; import { useClientContext } from './context/main'; import { DefaultLayout } from './layouts/DefaultLayout'; import { ConnectedLayout } from './layouts/ConnectedLayout'; +import { HelpGuideLayout } from './layouts/HelpGuideLayout'; export const App: React.FC = () => { const context = useClientContext(); const [busy, setBusy] = React.useState(); + const [showInfoModal, setShowInfoModal] = React.useState(false); const handleConnectClick = React.useCallback(async () => { - const oldStatus = context.connectionStatus; - if (oldStatus === ConnectionStatusKind.connected || oldStatus === ConnectionStatusKind.disconnected) { + const currentStatus = context.connectionStatus; + if (currentStatus === ConnectionStatusKind.connected || currentStatus === ConnectionStatusKind.disconnected) { setBusy(true); // eslint-disable-next-line default-case - switch (oldStatus) { + switch (currentStatus) { case ConnectionStatusKind.disconnected: await context.startConnecting(); + context.setConnectedSince(DateTime.now()); break; case ConnectionStatusKind.connected: await context.startDisconnecting(); + context.setConnectedSince(undefined); break; } setBusy(false); } }, [context.connectionStatus]); + useEffect(() => { + if (context.connectionStatus === ConnectionStatusKind.connected) setShowInfoModal(true); + }, [context.connectionStatus]); + + if (context.showHelp) return ; + if ( context.connectionStatus === ConnectionStatusKind.disconnected || context.connectionStatus === ConnectionStatusKind.connecting ) { return ( { return ( setShowInfoModal(false)} status={context.connectionStatus} busy={busy} onConnectClick={handleConnectClick} diff --git a/nym-connect/src/assets/help-step-four.png b/nym-connect/src/assets/help-step-four.png new file mode 100644 index 0000000000..5507962892 Binary files /dev/null and b/nym-connect/src/assets/help-step-four.png differ diff --git a/nym-connect/src/assets/help-step-one.png b/nym-connect/src/assets/help-step-one.png new file mode 100644 index 0000000000..5e91a29cff Binary files /dev/null and b/nym-connect/src/assets/help-step-one.png differ diff --git a/nym-connect/src/assets/help-step-three.png b/nym-connect/src/assets/help-step-three.png new file mode 100644 index 0000000000..932b6c1171 Binary files /dev/null and b/nym-connect/src/assets/help-step-three.png differ diff --git a/nym-connect/src/assets/help-step-two.png b/nym-connect/src/assets/help-step-two.png new file mode 100644 index 0000000000..f2910c98cf Binary files /dev/null and b/nym-connect/src/assets/help-step-two.png differ diff --git a/nym-connect/src/components/AppWindowFrame.tsx b/nym-connect/src/components/AppWindowFrame.tsx index fc30793a2d..5fc3d60ff6 100644 --- a/nym-connect/src/components/AppWindowFrame.tsx +++ b/nym-connect/src/components/AppWindowFrame.tsx @@ -1,21 +1,18 @@ import React from 'react'; import { Box } from '@mui/material'; -import { NymWordmark } from '@nymproject/react/logo/NymWordmark'; +import { CustomTitleBar } from './CustomTitleBar'; export const AppWindowFrame: React.FC = ({ children }) => ( t.palette.background.default, - borderRadius: '12px', - padding: '12px 16px', display: 'grid', - gridTemplateRows: '30px auto', - width: '240px', + borderRadius: '12px', + gridTemplateRows: '40px 1fr', + bgcolor: 'nym.background.dark', + height: '100vh', }} > - - - - {children} + + {children} ); diff --git a/nym-connect/src/components/ConnectionButton.tsx b/nym-connect/src/components/ConnectionButton.tsx index 0ab2ce1fa9..42903a80ea 100644 --- a/nym-connect/src/components/ConnectionButton.tsx +++ b/nym-connect/src/components/ConnectionButton.tsx @@ -19,16 +19,16 @@ const getStatusFillColor = (status: ConnectionStatusKind, hover: boolean, isErro switch (status) { case ConnectionStatusKind.disconnected: if (hover) { - return '#21D072'; + return '#FFFF33'; } - return '#F4B02D'; + return '#FFE600'; case ConnectionStatusKind.connecting: case ConnectionStatusKind.disconnecting: - return '#F4B02D'; + return '#FFE600'; default: // connected if (hover) { - return '#DA465B'; + return '#E43E3E'; } return '#21D072'; } @@ -81,69 +81,62 @@ export const ConnectionButton: React.FC<{ viewBox="0 0 208 208" fill="none" xmlns="http://www.w3.org/2000/svg" - onMouseEnter={() => !disabled && setHover(true)} - onMouseLeave={() => !disabled && setHover(false)} > - + !disabled && setHover(true)} + onMouseLeave={() => !disabled && setHover(false)} + > - - + + - - - - {busy && ( - - )} - + + + - + {status === ConnectionStatusKind.connected && hover ? ( ) : ( )} {statusText} - + - - + + - - + + diff --git a/nym-connect/src/components/ConnectionStatus.tsx b/nym-connect/src/components/ConnectionStatus.tsx index a866f8521d..c2bfcc82f0 100644 --- a/nym-connect/src/components/ConnectionStatus.tsx +++ b/nym-connect/src/components/ConnectionStatus.tsx @@ -1,12 +1,10 @@ import React from 'react'; import { Box, CircularProgress, Typography } from '@mui/material'; -import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline'; -import CircleOutlinedIcon from '@mui/icons-material/CircleOutlined'; import { DateTime } from 'luxon'; import { ConnectionStatusKind } from '../types'; import { ServiceProvider } from '../types/directory'; -const FONT_SIZE = '16px'; +const FONT_SIZE = '10px'; const FONT_WEIGHT = '600'; const FONT_STYLE = 'normal'; @@ -16,39 +14,41 @@ const ConnectionStatusContent: React.FC<{ switch (status) { case ConnectionStatusKind.connected: return ( - <> - - - Connected - - + + Connected + ); case ConnectionStatusKind.disconnecting: return ( - <> + Disconnecting... - + ); case ConnectionStatusKind.connecting: return ( - <> + Connecting... - + ); case ConnectionStatusKind.disconnected: return ( - <> - - - Disconnected - - + + You are not protected + ); default: return null; @@ -61,29 +61,22 @@ export const ConnectionStatus: React.FC<{ serviceProvider?: ServiceProvider; }> = ({ status, connectedSince, serviceProvider }) => { const color = - status === ConnectionStatusKind.connected || status === ConnectionStatusKind.disconnecting ? '#21D072' : '#888'; - const [duration, setDuration] = React.useState(); - React.useEffect(() => { - const intervalId = setInterval(() => { - if (connectedSince) { - setDuration(DateTime.now().diff(connectedSince).toFormat('hh:mm:ss')); - } - }, 500); - return () => { - clearInterval(intervalId); - }; - }, [status, connectedSince]); + status === ConnectionStatusKind.connected || status === ConnectionStatusKind.disconnecting + ? '#21D072' + : 'warning.main'; + return ( <> - - - - - - {status === ConnectionStatusKind.connected && duration} - + + + + + {serviceProvider && ( + + To {serviceProvider.description} + + )} - {serviceProvider && {serviceProvider.description}} ); }; diff --git a/nym-connect/src/components/ConntectionTimer.tsx b/nym-connect/src/components/ConntectionTimer.tsx new file mode 100644 index 0000000000..25e2af8d9c --- /dev/null +++ b/nym-connect/src/components/ConntectionTimer.tsx @@ -0,0 +1,26 @@ +import React, { useEffect } from 'react'; +import { Stack, Typography } from '@mui/material'; +import { DateTime } from 'luxon'; + +export const ConnectionTimer = ({ connectedSince }: { connectedSince?: DateTime }) => { + const [duration, setDuration] = React.useState(); + useEffect(() => { + const intervalId = setInterval(() => { + if (connectedSince) { + setDuration(DateTime.now().diff(connectedSince).toFormat('hh:mm:ss')); + } + }, 500); + return () => { + clearInterval(intervalId); + }; + }, [connectedSince]); + + return ( + + + Connection time + + {duration || '00:00:00'} + + ); +}; diff --git a/nym-connect/src/components/CustomTitleBar.tsx b/nym-connect/src/components/CustomTitleBar.tsx new file mode 100644 index 0000000000..dc4e8c0061 --- /dev/null +++ b/nym-connect/src/components/CustomTitleBar.tsx @@ -0,0 +1,41 @@ +import React from 'react'; +import { ArrowBack, Close, HelpOutline } from '@mui/icons-material'; +import { Box, IconButton } from '@mui/material'; +import { NymWordmark } from '@nymproject/react/logo/NymWordmark'; +import { appWindow } from '@tauri-apps/api/window'; +import { useClientContext } from 'src/context/main'; + +const customTitleBarStyles = { + titlebar: { + background: '#1D2125', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', + padding: '16px', + paddingBottom: '0px', + borderTopLeftRadius: '12px', + borderTopRightRadius: '12px', + }, +}; + +const CustomButton = ({ Icon, onClick }: { Icon: React.JSXElementConstructor; onClick: () => void }) => ( + + + +); + +export const CustomTitleBar = () => { + const { showHelp, handleShowHelp } = useClientContext(); + return ( + + { + handleShowHelp(); + }} + /> + + appWindow.close()} /> + + ); +}; diff --git a/nym-connect/src/components/HelpPage.tsx b/nym-connect/src/components/HelpPage.tsx new file mode 100644 index 0000000000..788f7264d5 --- /dev/null +++ b/nym-connect/src/components/HelpPage.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Stack, Typography } from '@mui/material'; +import { HelpPageActions } from './HelpPageActions'; +import { HelpImage } from './HelpPageImage'; +import { StepIndicator } from './HelpPageStepIndicator'; + +export const HelpPage = ({ + step, + description, + img, + onNext, + onPrev, +}: { + step: number; + description: string; + img: any; + onNext?: () => void; + onPrev?: () => void; +}) => ( + + + + + How to connect guide {step}/4 + + + {description} + + + + + +); diff --git a/nym-connect/src/components/HelpPageActions.tsx b/nym-connect/src/components/HelpPageActions.tsx new file mode 100644 index 0000000000..17e513120f --- /dev/null +++ b/nym-connect/src/components/HelpPageActions.tsx @@ -0,0 +1,20 @@ +import { ArrowBack, ArrowForward } from '@mui/icons-material'; +import { Box, Button } from '@mui/material'; +import React from 'react'; + +export const HelpPageActions = ({ onNext, onPrev }: { onNext?: () => void; onPrev?: () => void }) => ( + + {onPrev ? ( + + ) : ( +
+ )} + {onNext && ( + + )} + +); diff --git a/nym-connect/src/components/HelpPageImage.tsx b/nym-connect/src/components/HelpPageImage.tsx new file mode 100644 index 0000000000..27a8dcdca3 --- /dev/null +++ b/nym-connect/src/components/HelpPageImage.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export const HelpImage = ({ img, imageDescription }: { img: any; imageDescription: string }) => ( + {imageDescription} +); diff --git a/nym-connect/src/components/HelpPageStepIndicator.tsx b/nym-connect/src/components/HelpPageStepIndicator.tsx new file mode 100644 index 0000000000..b5dff42b34 --- /dev/null +++ b/nym-connect/src/components/HelpPageStepIndicator.tsx @@ -0,0 +1,15 @@ +import { Box } from '@mui/material'; +import React from 'react'; + +const Step = ({ highlight }: { highlight: boolean }) => ( + +); + +export const StepIndicator = ({ step }: { step: number }) => ( + + + = 2} /> + = 3} /> + = 4} /> + +); diff --git a/nym-connect/src/components/InfoModal.tsx b/nym-connect/src/components/InfoModal.tsx new file mode 100644 index 0000000000..ed628de7eb --- /dev/null +++ b/nym-connect/src/components/InfoModal.tsx @@ -0,0 +1,69 @@ +import { Close, ErrorOutline } from '@mui/icons-material'; +import { Box, IconButton, Modal, Theme, Typography } from '@mui/material'; +import React from 'react'; + +const styles = { + position: 'absolute', + top: '50%', + left: '50%', + transform: 'translate(-50%, -50%)', + width: 200, + bgcolor: '#292E34', + p: 1.5, + borderRadius: 0.5, + height: 'fit-content', + border: (theme: Theme) => `1px solid ${theme.palette.grey[700]}`, +}; + +const ModalTitle = ({ title, withCloseIcon }: { title: string; withCloseIcon: boolean }) => ( + + + + {title} + + +); + +const ModalBody = ({ description, children }: { description: string; children?: React.ReactElement }) => ( + + {children} + + {description} + + +); + +export const InfoModal = ({ + title, + description, + show, + children, + Action, + onClose, +}: { + title: string; + description: string; + show: boolean; + children?: React.ReactElement; + Action?: React.ReactNode; + onClose?: () => void; +}) => ( + + + {onClose && ( + + + + + + )} + + {children} + {Action && ( + + {Action} + + )} + + +); diff --git a/nym-connect/src/components/IpAddressAndPort.tsx b/nym-connect/src/components/IpAddressAndPort.tsx index 435cf926fb..8d7d79c1f2 100644 --- a/nym-connect/src/components/IpAddressAndPort.tsx +++ b/nym-connect/src/components/IpAddressAndPort.tsx @@ -37,8 +37,12 @@ export const IpAddressAndPort: React.FC<{ return ( - {label} - Port + + {label} + + + Port + void; +}) => ( + Done} + > + + + Socks5 address + + + {ipAddress} + + + + + Port + + + {port} + + + + +); diff --git a/nym-connect/src/components/ServiceProviderSelector.tsx b/nym-connect/src/components/ServiceProviderSelector.tsx index 08cda8eb8a..9dee82fbb4 100644 --- a/nym-connect/src/components/ServiceProviderSelector.tsx +++ b/nym-connect/src/components/ServiceProviderSelector.tsx @@ -1,11 +1,5 @@ import React, { useEffect, useMemo } from 'react'; -import IconButton from '@mui/material/IconButton'; -import Menu from '@mui/material/Menu'; -import MenuItem from '@mui/material/MenuItem'; -import KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownRounded'; -import KeyboardArrowUpRoundedIcon from '@mui/icons-material/KeyboardArrowUpRounded'; -import { Box, CircularProgress, Stack, Tooltip, Typography, ListItemIcon } from '@mui/material'; -import Check from '@mui/icons-material/Check'; +import { Box, CircularProgress, Stack, TextField, Tooltip, Typography, MenuItem, ListItemIcon } from '@mui/material'; import { ServiceProvider, Service, Services } from '../types/directory'; type ServiceWithRandomSp = { @@ -19,11 +13,8 @@ export const ServiceProviderSelector: React.FC<{ services?: Services; currentSp?: ServiceProvider; }> = ({ services, currentSp, onChange }) => { - const [service, setService] = React.useState(); + const [service, setService] = React.useState({ id: '', description: '', items: [] }); const [serviceProvider, setServiceProvider] = React.useState(currentSp); - const textEl = React.useRef(null); - const [anchorEl, setAnchorEl] = React.useState(null); - const open = Boolean(anchorEl); useEffect(() => { if (!serviceProvider && currentSp) { @@ -34,38 +25,34 @@ export const ServiceProviderSelector: React.FC<{ useEffect(() => { if (services && serviceProvider) { // retrieve the service corresponding to this service provider - setService( - services.find((s) => - s.items.some( - ({ id, address, gateway }) => - id === serviceProvider.id && address === serviceProvider.address && gateway === serviceProvider.gateway, - ), + + const match = services.find((s) => + s.items.some( + ({ id, address, gateway }) => + id === serviceProvider.id && address === serviceProvider.address && gateway === serviceProvider.gateway, ), ); + + if (match) { + setService(match); + } } }, [serviceProvider, services]); - const handleClick = () => { - setAnchorEl(textEl.current); - }; - const handleClose = (newServiceProvider?: ServiceProvider) => { + const handleSelectSp = (newServiceProvider?: ServiceProvider) => { if (newServiceProvider && newServiceProvider !== currentSp) { setServiceProvider(newServiceProvider); onChange?.(newServiceProvider); } - setAnchorEl(null); }; if (!services) { return ( - + theme.palette.common.white}> Loading services... - - {open ? : } - ); } @@ -80,68 +67,44 @@ export const ServiceProviderSelector: React.FC<{ [services], ); + if (!service) return null; + return ( - <> - `1px solid ${theme.palette.info.main}` }} - > - theme.palette.info.main}> - {!service ? 'Select a service' : service.description} - - - {open ? : } - - - handleClose()} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'right', - }} - transformOrigin={{ - vertical: 'top', - horizontal: 'left', - }} - PaperProps={{ + + {servicesWithRandomSp.map(({ id, description, sp }) => ( - handleClose(sp)} - > + handleSelectSp(sp)}> @@ -164,19 +127,9 @@ export const ServiceProviderSelector: React.FC<{ > {description} - {id === service?.id && ( - - - - )} ))} - - + + ); }; diff --git a/nym-connect/src/context/main.tsx b/nym-connect/src/context/main.tsx index 42bfed638e..5d2b049bf9 100644 --- a/nym-connect/src/context/main.tsx +++ b/nym-connect/src/context/main.tsx @@ -7,20 +7,26 @@ import { forage } from '@tauri-apps/tauri-forage'; import { ConnectionStatusKind } from '../types'; import { ConnectionStatsItem } from '../components/ConnectionStats'; import { ServiceProvider, Services } from '../types/directory'; +import { Error } from 'src/types/error'; +import { TauriEvent } from 'src/types/event'; const TAURI_EVENT_STATUS_CHANGED = 'app:connection-status-changed'; type ModeType = 'light' | 'dark'; -type TClientContext = { +export type TClientContext = { mode: ModeType; connectionStatus: ConnectionStatusKind; connectionStats?: ConnectionStatsItem[]; connectedSince?: DateTime; services?: Services; serviceProvider?: ServiceProvider; + showHelp: boolean; + error?: Error; setMode: (mode: ModeType) => void; + clearError: () => void; + handleShowHelp: () => void; setConnectionStatus: (connectionStatus: ConnectionStatusKind) => void; setConnectionStats: (connectionStats: ConnectionStatsItem[] | undefined) => void; setConnectedSince: (connectedSince: DateTime | undefined) => void; @@ -39,6 +45,8 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode const [connectedSince, setConnectedSince] = useState(); const [services, setServices] = React.useState([]); const [serviceProvider, setRawServiceProvider] = React.useState(); + const [showHelp, setShowHelp] = useState(false); + const [error, setError] = useState(); useEffect(() => { invoke('get_services').then((result) => { @@ -47,30 +55,45 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode }, []); useEffect(() => { - let unlisten: UnlistenFn | undefined; + const unlisten: UnlistenFn[] = []; // TODO: fix typings listen(TAURI_EVENT_STATUS_CHANGED, (event) => { const { status } = event.payload as any; console.log(TAURI_EVENT_STATUS_CHANGED, { status, event }); setConnectionStatus(status); + }) + .then((result) => { + unlisten.push(result); + }) + .catch((e) => console.log(e)); + + listen('socks5-event', (e: TauriEvent) => { + setError(e.payload); }).then((result) => { - unlisten = result; + unlisten.push(result); }); return () => { - if (unlisten) { - unlisten(); - } + unlisten.forEach((unsubscribe) => unsubscribe()); }; }, []); const startConnecting = useCallback(async () => { - await invoke('start_connecting'); + try { + await invoke('start_connecting'); + } catch (e) { + setError({ title: 'Could not connect', message: e as string }); + console.log(e); + } }, []); const startDisconnecting = useCallback(async () => { - await invoke('start_disconnecting'); + try { + await invoke('start_disconnecting'); + } catch (e) { + console.log(e); + } }, []); const setSpInStorage = async (sp: ServiceProvider) => { @@ -92,12 +115,17 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode const spFromStorage = await forage.getItem({ key: 'nym-connect-sp' })(); if (spFromStorage) { setRawServiceProvider(spFromStorage); + setServiceProvider(spFromStorage); } } catch (e) { console.warn(e); } }; + const handleShowHelp = () => setShowHelp((show) => !show); + + const clearError = () => setError(undefined); + useEffect(() => { const validityCheck = async () => { if (services.length > 0 && serviceProvider) { @@ -122,6 +150,8 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode () => ({ mode, setMode, + error, + clearError, connectionStatus, setConnectionStatus, connectionStats, @@ -133,8 +163,20 @@ export const ClientContextProvider = ({ children }: { children: React.ReactNode services, serviceProvider, setServiceProvider, + showHelp, + handleShowHelp, }), - [mode, connectedSince, connectionStatus, connectionStats, connectedSince, services, serviceProvider], + [ + mode, + error, + connectedSince, + showHelp, + connectionStatus, + connectionStats, + connectedSince, + services, + serviceProvider, + ], ); return {children}; diff --git a/nym-connect/src/context/mocks/main.tsx b/nym-connect/src/context/mocks/main.tsx new file mode 100644 index 0000000000..e2974413eb --- /dev/null +++ b/nym-connect/src/context/mocks/main.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { ConnectionStatusKind } from 'src/types'; +import { ClientContext, TClientContext } from '../main'; + +const mockValues: TClientContext = { + mode: 'dark', + connectionStatus: ConnectionStatusKind.disconnected, + services: [], + showHelp: false, + serviceProvider: { id: '1', description: 'Keybase service provider', gateway: 'abc123', address: '123abc' }, + setMode: () => {}, + clearError: () => {}, + handleShowHelp: () => {}, + setConnectedSince: () => {}, + setConnectionStats: () => {}, + setConnectionStatus: () => {}, + setServiceProvider: () => {}, + startConnecting: async () => {}, + startDisconnecting: async () => {}, +}; + +export const MockProvider = ({ children }: { children: React.ReactNode }) => { + return {children}; +}; diff --git a/nym-connect/src/index.tsx b/nym-connect/src/index.tsx index 2938840fff..66a3e11d17 100644 --- a/nym-connect/src/index.tsx +++ b/nym-connect/src/index.tsx @@ -4,16 +4,18 @@ import { ErrorBoundary } from 'react-error-boundary'; import { ClientContextProvider } from './context/main'; import { ErrorFallback } from './components/Error'; import { NymMixnetTheme } from './theme'; -import './fonts/fonts.css'; import { App } from './App'; +import { AppWindowFrame } from './components/AppWindowFrame'; const root = document.getElementById('root'); ReactDOM.render( - - + + + + , diff --git a/nym-connect/src/layouts/ConnectedLayout.tsx b/nym-connect/src/layouts/ConnectedLayout.tsx index a031f18a56..8e34415b39 100644 --- a/nym-connect/src/layouts/ConnectedLayout.tsx +++ b/nym-connect/src/layouts/ConnectedLayout.tsx @@ -1,11 +1,11 @@ import React from 'react'; -import { Box } from '@mui/material'; +import { Box, Divider } from '@mui/material'; import { DateTime } from 'luxon'; -import { AppWindowFrame } from '../components/AppWindowFrame'; +import { IpAddressAndPortModal } from 'src/components/IpAddressAndPortModal'; +import { ConnectionTimer } from 'src/components/ConntectionTimer'; import { ConnectionStatus } from '../components/ConnectionStatus'; import { ConnectionStatusKind } from '../types'; -import { ConnectionStats, ConnectionStatsItem } from '../components/ConnectionStats'; -import { NeedHelp } from '../components/NeedHelp'; +import { ConnectionStatsItem } from '../components/ConnectionStats'; import { ConnectionButton } from '../components/ConnectionButton'; import { IpAddressAndPort } from '../components/IpAddressAndPort'; import { ServiceProvider } from '../types/directory'; @@ -17,19 +17,32 @@ export const ConnectedLayout: React.FC<{ port: number; connectedSince?: DateTime; busy?: boolean; + showInfoModal: boolean; isError?: boolean; + handleCloseInfoModal: () => void; onConnectClick?: (status: ConnectionStatusKind) => void; serviceProvider?: ServiceProvider; -}> = ({ status, stats, ipAddress, port, connectedSince, busy, isError, serviceProvider, onConnectClick }) => ( - +}> = ({ + status, + showInfoModal, + handleCloseInfoModal, + ipAddress, + port, + connectedSince, + busy, + isError, + serviceProvider, + onConnectClick, +}) => ( + <> + - - - - + + + {/* */} + - - + ); diff --git a/nym-connect/src/layouts/DefaultLayout.tsx b/nym-connect/src/layouts/DefaultLayout.tsx index ba59b7f95d..ed42aad5fe 100644 --- a/nym-connect/src/layouts/DefaultLayout.tsx +++ b/nym-connect/src/layouts/DefaultLayout.tsx @@ -1,48 +1,48 @@ import React from 'react'; import { Typography } from '@mui/material'; -import { AppWindowFrame } from '../components/AppWindowFrame'; +import { Box } from '@mui/material'; +import { ConnectionStatus } from 'src/components/ConnectionStatus'; +import { ConnectionTimer } from 'src/components/ConntectionTimer'; +import { InfoModal } from 'src/components/InfoModal'; +import { Error } from 'src/types/error'; import { ConnectionButton } from '../components/ConnectionButton'; -import { ConnectionStatusKind } from '../types'; -import { NeedHelp } from '../components/NeedHelp'; import { ServiceProviderSelector } from '../components/ServiceProviderSelector'; -import { ServiceProvider, Services } from '../types/directory'; import { useClientContext } from '../context/main'; +import { ConnectionStatusKind } from '../types'; +import { ServiceProvider, Services } from '../types/directory'; export const DefaultLayout: React.FC<{ + error?: Error; status: ConnectionStatusKind; services?: Services; busy?: boolean; isError?: boolean; + clearError: () => void; onConnectClick?: (status: ConnectionStatusKind) => void; onServiceProviderChange?: (serviceProvider: ServiceProvider) => void; -}> = ({ status, services, busy, isError, onConnectClick, onServiceProviderChange }) => { - const [serviceProvider, setServiceProvider] = React.useState(); +}> = ({ status, error, services, busy, isError, onConnectClick, onServiceProviderChange, clearError }) => { const handleServiceProviderChange = (newServiceProvider: ServiceProvider) => { - setServiceProvider(newServiceProvider); onServiceProviderChange?.(newServiceProvider); }; + const { serviceProvider: currentSp } = useClientContext(); return ( - - - This is experimental software.
- Do not rely on it for strong anonymity (yet). -
- - Connect to the -
- Nym mixnet for privacy. + + {error && } + + + Connect to the Nym
mixnet for privacy.
+ - -
+
); }; diff --git a/nym-connect/src/layouts/HelpGuideLayout.tsx b/nym-connect/src/layouts/HelpGuideLayout.tsx new file mode 100644 index 0000000000..d56fd6b0ae --- /dev/null +++ b/nym-connect/src/layouts/HelpGuideLayout.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import { HelpPage } from 'src/components/HelpPage'; +import { Button, Link, Stack } from '@mui/material'; +import Image1 from '../assets/help-step-one.png'; +import Image2 from '../assets/help-step-two.png'; +import Image3 from '../assets/help-step-three.png'; +import Image4 from '../assets/help-step-four.png'; + +export const HelpGuideLayout = () => { + const [step, setStep] = useState(0); + + if (step === 1) + return ( + setStep(2)} + /> + ); + + if (step === 2) + return ( + setStep(1)} + onNext={() => setStep(3)} + /> + ); + + if (step === 3) + return ( + setStep(2)} + onNext={() => setStep(4)} + /> + ); + + if (step === 4) + return ( + setStep(3)} + /> + ); + + return ( + + + + + ); +}; diff --git a/nym-connect/src/stories/AppFlow.stories.tsx b/nym-connect/src/stories/AppFlow.stories.tsx index ef7a1ea434..34064e6202 100644 --- a/nym-connect/src/stories/AppFlow.stories.tsx +++ b/nym-connect/src/stories/AppFlow.stories.tsx @@ -67,20 +67,25 @@ export const Mock: ComponentStory = () => { context.connectionStatus === ConnectionStatusKind.connecting ) { return ( - + {}} /> - + ); } return ( - + { + return undefined; + }} status={context.connectionStatus} busy={busy} onConnectClick={handleConnectClick} @@ -101,6 +106,6 @@ export const Mock: ComponentStory = () => { }, ]} /> - + ); }; diff --git a/nym-connect/src/stories/ConnectedLayout.stories.tsx b/nym-connect/src/stories/ConnectedLayout.stories.tsx index 6063f83c28..53538457bf 100644 --- a/nym-connect/src/stories/ConnectedLayout.stories.tsx +++ b/nym-connect/src/stories/ConnectedLayout.stories.tsx @@ -11,8 +11,12 @@ export default { } as ComponentMeta; export const Default: ComponentStory = () => ( - + { + return undefined; + }} status={ConnectionStatusKind.connected} connectedSince={DateTime.now()} ipAddress="127.0.0.1" diff --git a/nym-connect/src/stories/DefaultLayout.stories.tsx b/nym-connect/src/stories/DefaultLayout.stories.tsx index 2c4ef513c2..ba311a410b 100644 --- a/nym-connect/src/stories/DefaultLayout.stories.tsx +++ b/nym-connect/src/stories/DefaultLayout.stories.tsx @@ -10,7 +10,24 @@ export default { } as ComponentMeta; export const Default: ComponentStory = () => ( - - + + {}} error={undefined} /> + +); + +export const WithServices: ComponentStory = () => ( + + {}} + error={undefined} + /> ); diff --git a/nym-connect/src/theme/index.tsx b/nym-connect/src/theme/index.tsx index 63f9db42fa..075aa7920a 100644 --- a/nym-connect/src/theme/index.tsx +++ b/nym-connect/src/theme/index.tsx @@ -3,12 +3,12 @@ import { createTheme, ThemeProvider } from '@mui/material/styles'; import { CssBaseline } from '@mui/material'; import { getDesignTokens } from './theme'; import { ClientContext } from '../context/main'; +import '../../../assets/fonts/non-variable/fonts.css'; /** - * Provides the theme for the Network Explorer by reacting to the light/dark mode choice stored in the app context. + * Provides the theme for Nym Connect by reacting to the light/dark mode choice stored in the app context. */ -export const NymMixnetTheme: React.FC = ({ children }) => { - const { mode } = useContext(ClientContext); +export const NymMixnetTheme: React.FC<{ mode: 'light' | 'dark' }> = ({ children, mode }) => { const theme = React.useMemo(() => createTheme(getDesignTokens(mode)), [mode]); return ( diff --git a/nym-connect/src/theme/mui-theme.d.ts b/nym-connect/src/theme/mui-theme.d.ts index a5b92073b3..ef042f6305 100644 --- a/nym-connect/src/theme/mui-theme.d.ts +++ b/nym-connect/src/theme/mui-theme.d.ts @@ -30,6 +30,7 @@ declare module '@mui/material/styles' { interface NymPalette { highlight: string; success: string; + warning: string; info: string; fee: string; background: { light: string; dark: string }; diff --git a/nym-connect/src/theme/theme.tsx b/nym-connect/src/theme/theme.tsx index e0153e6824..5b70f5118b 100644 --- a/nym-connect/src/theme/theme.tsx +++ b/nym-connect/src/theme/theme.tsx @@ -23,6 +23,7 @@ const nymPalette: NymPalette = { highlight: '#FB6E4E', success: '#21D073', info: '#60D7EF', + warning: '#FFE600', fee: '#967FF0', background: { light: '#F4F6F8', dark: '#1D2125' }, text: { @@ -93,6 +94,9 @@ const variantToMUIPalette = (variant: NymPaletteVariant): PaletteOptions => ({ info: { main: nymPalette.info, }, + warning: { + main: nymPalette.warning, + }, background: { default: variant.background.main, paper: variant.background.paper, diff --git a/nym-connect/src/types/error.ts b/nym-connect/src/types/error.ts new file mode 100644 index 0000000000..599b959c39 --- /dev/null +++ b/nym-connect/src/types/error.ts @@ -0,0 +1,4 @@ +export type Error = { + title: string; + message: string; +}; diff --git a/nym-connect/src/types/event.ts b/nym-connect/src/types/event.ts new file mode 100644 index 0000000000..66d1e514a6 --- /dev/null +++ b/nym-connect/src/types/event.ts @@ -0,0 +1,6 @@ +export type TauriEvent = { + payload: { + title: string; + message: string; + }; +}; diff --git a/nym-wallet/Cargo.lock b/nym-wallet/Cargo.lock index a0086ebb09..999378447f 100644 --- a/nym-wallet/Cargo.lock +++ b/nym-wallet/Cargo.lock @@ -257,10 +257,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] -name = "blake2" -version = "0.10.4" +name = "bitvec" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e5fd123190ce1c2e559308a94c9bacad77907d4c6005d9e58fe1a0689e55e" dependencies = [ "digest 0.10.3", ] @@ -319,16 +331,16 @@ checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" [[package]] name = "bls12_381" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54757888b09a69be70b5ec303e382a74227392086ba808cb01eeca29233a2397" +version = "0.6.0" +source = "git+https://github.com/jstuczyn/bls12_381?branch=gt-serialisation#10fb6f700bfda17c8475af3bfd31e3fec15f2278" dependencies = [ "digest 0.9.0", - "ff 0.10.1", - "group 0.10.0", + "ff", + "group", "pairing", "rand_core 0.6.3", "subtle", + "zeroize", ] [[package]] @@ -595,6 +607,18 @@ dependencies = [ "serde", ] +[[package]] +name = "coconut-dkg-common" +version = "0.1.0" +dependencies = [ + "contracts-common", + "cosmwasm-std", + "cw-utils", + "multisig-contract-common", + "schemars", + "serde", +] + [[package]] name = "coconut-interface" version = "0.1.0" @@ -667,6 +691,7 @@ checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" name = "contracts-common" version = "0.1.0" dependencies = [ + "bs58", "cosmwasm-std", "schemars", "serde", @@ -954,9 +979,9 @@ dependencies = [ [[package]] name = "cw-utils" -version = "0.13.2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "babd2c090f39d07ce5bf2556962305e795daa048ce20a93709eb591476e4a29e" +checksum = "9dbaecb78c8e8abfd6b4258c7f4fbeb5c49a5e45ee4d910d3240ee8e1d714e1b" dependencies = [ "cosmwasm-std", "schemars", @@ -965,10 +990,22 @@ dependencies = [ ] [[package]] -name = "cw3" -version = "0.13.2" +name = "cw2" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f871854338a54c7bb094d16ffe17212b93b146d9659dbce4c9402a9b77e240ef" +checksum = "04cf4639517490dd36b333bbd6c4fbd92e325fd0acf4683b41753bc5eb63bfc1" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "schemars", + "serde", +] + +[[package]] +name = "cw3" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe19462a7f644ba60c19d3443cb90d00c50d9b6b3b0a3a7fca93df8261af979b" dependencies = [ "cosmwasm-std", "cw-utils", @@ -977,10 +1014,26 @@ dependencies = [ ] [[package]] -name = "cw4" -version = "0.13.2" +name = "cw3-fixed-multisig" +version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4476d6a7c13c46ed9ff260bd0e1cf648dc37b13f483822e1ff2a431f0f6ee52" +checksum = "df54aa54c13f405ec4ab36b6217538bc957d439eee58f89312db05a79caf6706" +dependencies = [ + "cosmwasm-std", + "cw-storage-plus", + "cw-utils", + "cw2", + "cw3", + "schemars", + "serde", + "thiserror", +] + +[[package]] +name = "cw4" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0acc3549d5ce11c6901b3a676f2e2628684722197054d97cd0101ea174ed5cbd" dependencies = [ "cosmwasm-std", "cw-storage-plus", @@ -1131,6 +1184,27 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" +[[package]] +name = "dkg" +version = "0.1.0" +dependencies = [ + "bitvec", + "bls12_381", + "bs58", + "ff", + "group", + "lazy_static", + "pemstore", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_core 0.6.3", + "serde", + "serde_derive", + "sha2 0.9.9", + "thiserror", + "zeroize", +] + [[package]] name = "dotenv" version = "0.15.0" @@ -1172,9 +1246,9 @@ dependencies = [ [[package]] name = "ed25519" -version = "1.4.0" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed12bbf7b5312f8da1c2722bc06d8c6b12c2d86a7fb35a194c7f3e6fc2bbe39" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" dependencies = [ "signature", ] @@ -1221,9 +1295,9 @@ dependencies = [ "base16ct", "crypto-bigint", "der", - "ff 0.11.0", + "ff", "generic-array 0.14.5", - "group 0.11.0", + "group", "rand_core 0.6.3", "sec1", "subtle", @@ -1334,16 +1408,6 @@ dependencies = [ "log", ] -[[package]] -name = "ff" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f" -dependencies = [ - "rand_core 0.6.3", - "subtle", -] - [[package]] name = "ff" version = "0.11.0" @@ -1441,6 +1505,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futf" version = "0.1.5" @@ -1832,25 +1902,14 @@ dependencies = [ "system-deps 6.0.2", ] -[[package]] -name = "group" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" -dependencies = [ - "byteorder", - "ff 0.10.1", - "rand_core 0.6.3", - "subtle", -] - [[package]] name = "group" version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5ac374b108929de78460075f3dc439fa66df9d8fc77e8f12caa5165fcf0c89" dependencies = [ - "ff 0.11.0", + "byteorder", + "ff", "rand_core 0.6.3", "subtle", ] @@ -2646,6 +2705,7 @@ dependencies = [ "cosmwasm-std", "cw-utils", "cw3", + "cw3-fixed-multisig", "cw4", "schemars", "serde", @@ -2877,7 +2937,7 @@ dependencies = [ [[package]] name = "nym_wallet" -version = "1.1.0" +version = "1.1.1" dependencies = [ "aes-gcm", "argon2 0.3.4", @@ -2933,10 +2993,12 @@ dependencies = [ "bls12_381", "bs58", "digest 0.9.0", - "ff 0.10.1", + "dkg", + "ff", "getrandom 0.2.5", - "group 0.10.0", + "group", "itertools", + "pemstore", "rand 0.8.5", "serde", "serde_derive", @@ -3052,11 +3114,11 @@ checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" [[package]] name = "pairing" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de9d09263c9966e8196fe0380c9dbbc7ea114b5cf371ba29004bc1f9c6db7f3" +checksum = "f2e415e349a3006dd7d9482cdab1c980a845bed1377777d768cb693a44540b42" dependencies = [ - "group 0.10.0", + "group", ] [[package]] @@ -3208,6 +3270,24 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c719dcf55f09a3a7e764c6649ab594c18a177e3599c467983cdf644bfc0a4088" +[[package]] +name = "pem" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" +dependencies = [ + "base64", + "once_cell", + "regex", +] + +[[package]] +name = "pemstore" +version = "0.1.0" +dependencies = [ + "pem", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -3579,6 +3659,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.6.5" @@ -4637,6 +4723,12 @@ dependencies = [ "x11-dl", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tar" version = "0.4.38" @@ -5327,6 +5419,7 @@ dependencies = [ "base64", "bip39", "coconut-bandwidth-contract-common", + "coconut-dkg-common", "coconut-interface", "colored 2.0.0", "config", @@ -5887,6 +5980,15 @@ dependencies = [ "windows-implement", ] +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "x11" version = "2.19.1" diff --git a/nym-wallet/nym-wallet-types/src/network.rs b/nym-wallet/nym-wallet-types/src/network.rs index 1be3c80732..3a39bd6733 100644 --- a/nym-wallet/nym-wallet-types/src/network.rs +++ b/nym-wallet/nym-wallet-types/src/network.rs @@ -111,6 +111,8 @@ mod sandbox { "nymt1nz0r0au8aj6dc00wmm3ufy4g4k86rjzlgq608r"; pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "nymt1k8re7jwz6rnnwrktnejdwkwnncte7ek7kk6fvg"; + pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = + "nymt1k8re7jwz6rnnwrktnejdwkwnncte7ek7kk6fvg"; pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] = hex_literal::hex!("8e0DcFF7F3085235C32E845f3667aEB3f1e83133"); pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = @@ -144,6 +146,7 @@ mod sandbox { COCONUT_BANDWIDTH_CONTRACT_ADDRESS, ), multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), + coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), }, } } @@ -160,14 +163,16 @@ mod qa { pub(crate) const STAKE_DENOM: DenomDetails = DenomDetails::new("unyx", "nyx", 6); pub(crate) const MIXNET_CONTRACT_ADDRESS: &str = - "n1frq2hzkjtatsupc6jtyaz67ytydk9nya437q92qg76ny3y8fcnjsw806vg"; - pub(crate) const VESTING_CONTRACT_ADDRESS: &str = "n14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9sjyvg3g"; + pub(crate) const VESTING_CONTRACT_ADDRESS: &str = + "n1nc5tatafv6eyq7llkr2gv50ff9e22mnf70qgjlv737ktmt4eswrq73f2nw"; pub(crate) const BANDWIDTH_CLAIM_CONTRACT_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; pub(crate) const COCONUT_BANDWIDTH_CONTRACT_ADDRESS: &str = "n1ghd753shjuwexxywmgs4xz7x2q732vcn7ty4yw"; pub(crate) const MULTISIG_CONTRACT_ADDRESS: &str = "n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs"; + pub(crate) const COCONUT_DKG_CONTRACT_ADDRESS: &str = + "n17p9rzwnnfxcjp32un9ug7yhhzgtkhvl988qccs"; pub(crate) const _ETH_CONTRACT_ADDRESS: [u8; 20] = hex_literal::hex!("0000000000000000000000000000000000000000"); pub(crate) const _ETH_ERC20_CONTRACT_ADDRESS: [u8; 20] = @@ -177,8 +182,8 @@ mod qa { //pub(crate) const STATISTICS_SERVICE_DOMAIN_ADDRESS: &str = "http://0.0.0.0"; pub(crate) fn validators() -> Vec { vec![ValidatorDetails::new( - "https://adv-epoch-qa-validator.qa.nymte.ch/", - Some("https://adv-epoch-qa-val-api.qa.nymte.ch/api"), + "https://qwerty-validator.qa.nymte.ch/", + Some("https://qwerty-validator-api.qa.nymte.ch/api"), )] } @@ -200,6 +205,7 @@ mod qa { COCONUT_BANDWIDTH_CONTRACT_ADDRESS, ), multisig_contract_address: parse_optional_str(MULTISIG_CONTRACT_ADDRESS), + coconut_dkg_contract_address: parse_optional_str(COCONUT_DKG_CONTRACT_ADDRESS), }, } } diff --git a/nym-wallet/package.json b/nym-wallet/package.json index 973fc67ded..a9a55d2d0b 100644 --- a/nym-wallet/package.json +++ b/nym-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/nym-wallet-app", - "version": "1.0.0", + "version": "1.1.1", "main": "index.js", "license": "MIT", "scripts": { diff --git a/nym-wallet/src-tauri/Cargo.toml b/nym-wallet/src-tauri/Cargo.toml index 21dfa19030..835383188e 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.1.0" +version = "1.1.1" description = "Nym Native Wallet" authors = ["Nym Technologies SA"] license = "" diff --git a/nym-wallet/src-tauri/tauri.conf.json b/nym-wallet/src-tauri/tauri.conf.json index 11af64d0a0..b7b616b2e8 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.1.0" + "version": "1.1.1" }, "build": { "distDir": "../dist", diff --git a/nym-wallet/src/components/Bonding/BondedGateway.tsx b/nym-wallet/src/components/Bonding/BondedGateway.tsx index 8b2776f8a9..51523ef6ca 100644 --- a/nym-wallet/src/components/Bonding/BondedGateway.tsx +++ b/nym-wallet/src/components/Bonding/BondedGateway.tsx @@ -91,7 +91,7 @@ export const BondedGateway = ({ {network && ( Check more stats of your gateway on the{' '} - + explorer diff --git a/nym-wallet/src/components/Bonding/BondedMixnode.tsx b/nym-wallet/src/components/Bonding/BondedMixnode.tsx index a30e5557cb..5d63555ddb 100644 --- a/nym-wallet/src/components/Bonding/BondedMixnode.tsx +++ b/nym-wallet/src/components/Bonding/BondedMixnode.tsx @@ -42,7 +42,7 @@ const headers: Header[] = [ header: 'Operator rewards', id: 'operator-rewards', tooltipText: - 'This is your (operator) new rewards including the PM and cost. You can compound your rewards manually every epoch or unbond your node to redeem them.', + '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', @@ -66,6 +66,7 @@ export const BondedMixnode = ({ const navigate = useNavigate(); const { name, + mixId, stake, bond, stakeSaturation, @@ -165,7 +166,7 @@ export const BondedMixnode = ({ {network && ( Check more stats of your node on the{' '} - + explorer diff --git a/nym-wallet/src/components/Delegation/DelegationItem.tsx b/nym-wallet/src/components/Delegation/DelegationItem.tsx index 2906ed2abd..d800f9f487 100644 --- a/nym-wallet/src/components/Delegation/DelegationItem.tsx +++ b/nym-wallet/src/components/Delegation/DelegationItem.tsx @@ -45,7 +45,7 @@ export const DelegationItem = ({ ) : ( Promise; - undelegate: ( - mix_id: number, - usesVestingContractTokens: boolean, - fee?: FeeDetails, - ) => Promise; + undelegate: (mix_id: number, fee?: Fee) => Promise; + undelegateVesting: (mix_id: number) => Promise; }; export type TDelegationTransaction = { @@ -51,7 +54,10 @@ export const DelegationContext = createContext({ addDelegation: async () => { throw new Error('Not implemented'); }, - undelegate: async () => { + undelegate: () => { + throw new Error('Not implemented'); + }, + undelegateVesting: () => { throw new Error('Not implemented'); }, }); @@ -135,7 +141,8 @@ export const DelegationContextProvider: FC<{ totalRewards, refresh, addDelegation, - undelegate: undelegateAllFromMixnode, + undelegate: undelegateFromMixnode, + undelegateVesting: vestingUndelegateFromMixnode, }), [isLoading, error, delegations, pendingDelegations, totalDelegations], ); diff --git a/nym-wallet/src/context/mocks/bonding.tsx b/nym-wallet/src/context/mocks/bonding.tsx index 5fde300d75..8d97deb090 100644 --- a/nym-wallet/src/context/mocks/bonding.tsx +++ b/nym-wallet/src/context/mocks/bonding.tsx @@ -8,6 +8,7 @@ const SLEEP_MS = 1000; const bondedMixnodeMock: TBondedMixnode = { name: 'Monster node', + mixId: 1, identityKey: '7mjM2fYbtN6kxMwp1TrmQ4VwPks3URR5pBgWPWhzT98F', stake: { denom: 'nym', amount: '1234' }, bond: { denom: 'nym', amount: '1234' }, diff --git a/nym-wallet/src/context/mocks/delegations.tsx b/nym-wallet/src/context/mocks/delegations.tsx index 3674dc1494..bc216fd7bf 100644 --- a/nym-wallet/src/context/mocks/delegations.tsx +++ b/nym-wallet/src/context/mocks/delegations.tsx @@ -1,5 +1,12 @@ import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; -import { DelegationWithEverything, DecCoin, TransactionExecuteResult, FeeDetails } from '@nymproject/types'; +import { + DelegationWithEverything, + DecCoin, + TransactionExecuteResult, + FeeDetails, + Fee, + CurrencyDenom, +} from '@nymproject/types'; import { DelegationContext, TDelegationTransaction } from '../delegations'; import { mockSleep } from './utils'; @@ -154,11 +161,7 @@ export const MockDelegationContextProvider: FC<{}> = ({ children }) => { }; }; - const undelegate = async ( - mix_id: number, - _usesVestingContractTokens: boolean, - _fee?: FeeDetails, - ): Promise => { + const undelegate = async (mix_id: number, _fee?: Fee): Promise => { await mockSleep(SLEEP_MS); mockDelegations = mockDelegations.map((d) => { if (d.mix_id === mix_id) { @@ -175,18 +178,29 @@ export const MockDelegationContextProvider: FC<{}> = ({ children }) => { triggerStateUpdate(); }, 3000); - return [ - { - logs_json: '', - data_json: '', - transaction_hash: '', - gas_info: { - gas_wanted: { gas_units: BigInt(1) }, - gas_used: { gas_units: BigInt(1) }, - }, - fee: { amount: '1', denom: 'nym' }, + return { + logs_json: '', + data_json: '', + transaction_hash: '', + gas_info: { + gas_wanted: { gas_units: BigInt(1) }, + gas_used: { gas_units: BigInt(1) }, }, - ]; + fee: { amount: '1', denom: 'nym' as CurrencyDenom }, + }; + }; + + const undelegateVesting = async (mix_id: number, _fee?: FeeDetails) => { + return { + logs_json: '', + data_json: '', + transaction_hash: '', + gas_info: { + gas_wanted: { gas_units: BigInt(1) }, + gas_used: { gas_units: BigInt(1) }, + }, + fee: { amount: '1', denom: 'nym' as CurrencyDenom }, + }; }; const resetState = () => { @@ -226,6 +240,7 @@ export const MockDelegationContextProvider: FC<{}> = ({ children }) => { addDelegation, updateDelegation, undelegate, + undelegateVesting, }), [isLoading, error, delegations, totalDelegations, trigger], ); diff --git a/nym-wallet/src/pages/balance/vesting.tsx b/nym-wallet/src/pages/balance/vesting.tsx index a3a52a44b1..c58c1454dd 100644 --- a/nym-wallet/src/pages/balance/vesting.tsx +++ b/nym-wallet/src/pages/balance/vesting.tsx @@ -130,16 +130,10 @@ const TokenTransfer = () => { t.palette.nym.text.muted, mt: 2 }}> - Transferable tokens + Unlocked transferable tokens - + {userBalance.tokenAllocation?.spendable || 'n/a'} {clientDetails?.display_mix_denom.toUpperCase()} @@ -167,7 +161,6 @@ export const VestingCard = ({ onTransfer }: { onTransfer: () => Promise }) } Action={ { diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx index 4a6097b8da..f576afccd9 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/NodeUnbondPage.tsx @@ -33,10 +33,17 @@ export const NodeUnbondPage = ({ bondedNode, onConfirm, onError }: Props) => { + + + diff --git a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/InfoSettings.tsx b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/InfoSettings.tsx index 7b85f1c0c2..31e88a0f31 100644 --- a/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/InfoSettings.tsx +++ b/nym-wallet/src/pages/bonding/node-settings/settings-pages/general-settings/InfoSettings.tsx @@ -4,15 +4,20 @@ import { yupResolver } from '@hookform/resolvers/yup'; import { Button, Divider, Typography, TextField, Grid, CircularProgress, Box } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import { isMixnode } from 'src/types'; -import { updateMixnodeConfig } from 'src/requests'; +import { simulateUpdateMixnodeConfig, simulateVestingUpdateMixnodeConfig, updateMixnodeConfig } from 'src/requests'; import { TBondedMixnode, TBondedGateway } from 'src/context/bonding'; import { SimpleModal } from 'src/components/Modals/SimpleModal'; import { bondedInfoParametersValidationSchema } from 'src/components/Bonding/forms/mixnodeValidationSchema'; import { Console } from 'src/utils/console'; import { Alert } from 'src/components/Alert'; +import { vestingUpdateMixnodeConfig } from 'src/requests/vesting'; +import { ConfirmTx } from 'src/components/ConfirmTX'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { LoadingModal } from 'src/components/Modals/LoadingModal'; export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBondedGateway }) => { const [openConfirmationModal, setOpenConfirmationModal] = useState(false); + const { getFee, fee, resetFeeState } = useGetFee(); const theme = useTheme(); @@ -33,6 +38,7 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon verlocPort?: number; httpApiPort?: number; }) => { + resetFeeState(); const { host, version, mixPort, verlocPort, httpApiPort } = data; if (host && version && mixPort && verlocPort && httpApiPort) { const MixNodeConfigParams = { @@ -43,7 +49,11 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon version, }; try { - await updateMixnodeConfig(MixNodeConfigParams); + if (bondedNode.proxy) { + await vestingUpdateMixnodeConfig(MixNodeConfigParams); + } else { + await updateMixnodeConfig(MixNodeConfigParams); + } setOpenConfirmationModal(true); } catch (error) { Console.error(error); @@ -53,10 +63,22 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon return ( + {fee && ( + onSubmit(d))} + onPrev={resetFeeState} + onClose={resetFeeState} + /> + )} + {isSubmitting && } - Your changes will be ONLY saved on the display. Remember to change the values on your node’s config file too + 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 } dismissable @@ -67,16 +89,6 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon Port - (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'), - }} - > - Change profit margin of your node - @@ -120,16 +132,6 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon Host - (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'), - }} - > - Lock wallet after certain time - @@ -151,16 +153,6 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon Version - (t.palette.mode === 'light' ? t.palette.nym.text.muted : 'text.primary'), - }} - > - Lock wallet after certain time - @@ -182,18 +174,24 @@ export const InfoSettings = ({ bondedNode }: { bondedNode: TBondedMixnode | TBon size="large" variant="contained" disabled={isSubmitting || !isDirty || !isValid} - onClick={handleSubmit((d) => onSubmit(d))} - type="submit" - sx={{ m: 3, width: '320px' }} - endIcon={isSubmitting && } + onClick={handleSubmit((data) => + getFee(bondedNode.proxy ? simulateVestingUpdateMixnodeConfig : simulateUpdateMixnodeConfig, { + host: data.host, + mix_port: data.mixPort, + verloc_port: data.verlocPort, + http_api_port: data.httpApiPort, + version: data.version, + }), + )} + sx={{ m: 3 }} > - Save all display changes + Submit changes to the blockchain 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 ad9b3d03af..aebb21c8c1 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,17 +14,27 @@ import { } from '@mui/material'; import { useTheme } from '@mui/material/styles'; import { CurrencyDenom, MixNodeCostParams } from '@nymproject/types'; +import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; import { add, format, fromUnixTime } from 'date-fns'; import { isMixnode } from 'src/types'; -import { getCurrentInterval, getPendingIntervalEvents, updateMixnodeCostParams } from 'src/requests'; -import { TBondedMixnode, TBondedGateway } from 'src/context/bonding'; +import { + getCurrentInterval, + getPendingIntervalEvents, + 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 { Console } from 'src/utils/console'; import { Alert } from 'src/components/Alert'; import { ChangeMixCostParams } from 'src/pages/bonding/types'; import { AppContext } from 'src/context'; -import { CurrencyFormField } from '@nymproject/react/currency/CurrencyFormField'; +import { useGetFee } from 'src/hooks/useGetFee'; +import { ConfirmTx } from 'src/components/ConfirmTX'; +import { LoadingModal } from 'src/components/Modals/LoadingModal'; export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode }): JSX.Element => { const [openConfirmationModal, setOpenConfirmationModal] = useState(false); @@ -34,6 +44,8 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode const { clientDetails } = useContext(AppContext); const theme = useTheme(); + const { fee, getFee, resetFeeState } = useGetFee(); + const defaultValues = { operatorCost: bondedNode.operatorCost, profitMargin: bondedNode.profitMargin, @@ -96,6 +108,7 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode }, []); const onSubmit = async (data: { operatorCost: { amount: string; denom: CurrencyDenom }; profitMargin: string }) => { + resetFeeState(); if (data.operatorCost && data.profitMargin) { const MixNodeCostParams = { profit_margin_percent: (+data.profitMargin / 100).toString(), @@ -105,7 +118,11 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode }, }; try { - await updateMixnodeCostParams(MixNodeCostParams); + if (bondedNode.proxy) { + await vestingUpdateMixnodeCostParams(MixNodeCostParams); + } else { + await updateMixnodeCostParams(MixNodeCostParams); + } await getPendingEvents(); reset(); setOpenConfirmationModal(true); @@ -117,6 +134,17 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode return ( + {fee && ( + onSubmit(d))} + onPrev={resetFeeState} + onClose={resetFeeState} + /> + )} + {isSubmitting && } @@ -129,7 +157,7 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode /> - + Profit Margin @@ -223,12 +251,16 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode size="large" variant="contained" disabled={isSubmitting || !isDirty || !isValid} - onClick={handleSubmit(onSubmit)} + onClick={handleSubmit((data) => { + getFee(bondedNode.proxy ? simulateVestingUpdateMixnodeCostParams : simulateUpdateMixnodeCostParams, { + profit_margin_percent: (+data.profitMargin / 100).toString(), + interval_operating_cost: data.operatorCost, + }); + })} type="submit" - sx={{ m: 3, width: '320px' }} - endIcon={isSubmitting && } + sx={{ m: 3 }} > - Save all display changes + Submit changes to the blockchain @@ -236,7 +268,7 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode open={openConfirmationModal} header="Your changes will take place in the next interval" - okLabel="close" + okLabel="Close" hideCloseIcon displayInfoIcon onOk={async () => { @@ -256,7 +288,6 @@ export const ParametersSettings = ({ bondedNode }: { bondedNode: TBondedMixnode textAlign: 'center', color: theme.palette.nym.nymWallet.text.blue, fontSize: 16, - textTransform: 'capitalize', }} subHeaderStyles={{ m: 0, diff --git a/nym-wallet/src/pages/delegation/index.tsx b/nym-wallet/src/pages/delegation/index.tsx index 62418f0463..a70300388f 100644 --- a/nym-wallet/src/pages/delegation/index.tsx +++ b/nym-wallet/src/pages/delegation/index.tsx @@ -52,6 +52,7 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { isLoading, addDelegation, undelegate, + undelegateVesting, refresh: refreshDelegations, } = useDelegationContext(); @@ -206,7 +207,8 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { const handleUndelegate = async ( mixId: number, - identityKey: string, + // identityKey is no longer used + _: string, usesVestingContractTokens: boolean, fee?: FeeDetails, ) => { @@ -216,19 +218,27 @@ export const Delegation: FC<{ isStorybook?: boolean }> = ({ isStorybook }) => { }); setShowUndelegateModal(false); setCurrentDelegationListActionItem(undefined); - + let tx; try { - const txs = await undelegate(mixId, usesVestingContractTokens, fee); + if (usesVestingContractTokens) { + tx = await undelegateVesting(mixId); + } else { + tx = await undelegate(mixId, fee?.fee); + } + + // const txs = await undelegate(mixId, usesVestingContractTokens, fee); const balances = await getAllBalances(); setConfirmationModalProps({ status: 'success', action: 'undelegate', ...balances, - transactions: txs.map((tx) => ({ - url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`, - hash: tx.transaction_hash, - })), + transactions: [ + { + url: `${urls(network).blockExplorer}/transaction/${tx.transaction_hash}`, + hash: tx.transaction_hash, + }, + ], }); } catch (e) { Console.error('Failed to undelegate', e); diff --git a/sdk/typescript/packages/sdk/package.json b/sdk/typescript/packages/sdk/package.json index c31270a7cf..e2e4c5b008 100644 --- a/sdk/typescript/packages/sdk/package.json +++ b/sdk/typescript/packages/sdk/package.json @@ -1,6 +1,6 @@ { "name": "@nymproject/sdk", - "version": "1.1.2", + "version": "1.1.4", "license": "Apache-2.0", "author": "Nym Technologies SA", "main": "dist/index.js", diff --git a/sdk/typescript/packages/sdk/src/mixnet/wasm/types.ts b/sdk/typescript/packages/sdk/src/mixnet/wasm/types.ts index 8e984f66c2..36aacd5adb 100644 --- a/sdk/typescript/packages/sdk/src/mixnet/wasm/types.ts +++ b/sdk/typescript/packages/sdk/src/mixnet/wasm/types.ts @@ -27,6 +27,11 @@ export interface NymClientConfig { * Optional. The identity key of the preferred gateway to connect to. */ preferredGatewayIdentityKey?: string; + + /** + * Optional. The listener websocket of the preferred gateway to connect to. + */ + gatewayListener?: string; /** * Optional. Settings for the WASM client. diff --git a/sdk/typescript/packages/sdk/src/mixnet/wasm/worker.ts b/sdk/typescript/packages/sdk/src/mixnet/wasm/worker.ts index 61df19b408..cb1c8a32e4 100644 --- a/sdk/typescript/packages/sdk/src/mixnet/wasm/worker.ts +++ b/sdk/typescript/packages/sdk/src/mixnet/wasm/worker.ts @@ -133,7 +133,11 @@ wasm_bindgen(wasmUrl) config.validatorApiUrl, config.preferredGatewayIdentityKey, ); - + + // set a different gatewayListener in order to avoid workaround ws over https error + if (config.gatewayListener) + gatewayEndpoint.gateway_listener = config.gatewayListener; + // create the client, passing handlers for events wrapper.init( new wasm_bindgen.Config( diff --git a/service-providers/network-requester/Cargo.toml b/service-providers/network-requester/Cargo.toml index eb3cc1d229..8d81920af5 100644 --- a/service-providers/network-requester/Cargo.toml +++ b/service-providers/network-requester/Cargo.toml @@ -6,6 +6,7 @@ name = "nym-network-requester" version = "1.1.0" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] edition = "2021" +rust-version = "1.65" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/service-providers/network-requester/src/connection.rs b/service-providers/network-requester/src/connection.rs index 5c5fea04b4..95bdac06a3 100644 --- a/service-providers/network-requester/src/connection.rs +++ b/service-providers/network-requester/src/connection.rs @@ -1,10 +1,10 @@ // Copyright 2020 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use futures::channel::mpsc; +use client_connections::LaneQueueLengths; use nymsphinx::addressing::clients::Recipient; use proxy_helpers::connection_controller::ConnectionReceiver; -use proxy_helpers::proxy_runner::ProxyRunner; +use proxy_helpers::proxy_runner::{MixProxySender, ProxyRunner}; use socks5_requests::{ConnectionId, Message as Socks5Message, RemoteAddress, Response}; use std::io; use task::ShutdownListener; @@ -40,7 +40,8 @@ impl Connection { pub(crate) async fn run_proxy( &mut self, mix_receiver: ConnectionReceiver, - mix_sender: mpsc::UnboundedSender<(Socks5Message, Recipient)>, + mix_sender: MixProxySender<(Socks5Message, Recipient)>, + lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) { let stream = self.conn.take().unwrap(); @@ -54,6 +55,7 @@ impl Connection { mix_receiver, mix_sender, connection_id, + Some(lane_queue_lengths), shutdown, ) .run(move |conn_id, read_data, socket_closed| { diff --git a/service-providers/network-requester/src/core.rs b/service-providers/network-requester/src/core.rs index c9333d6bf6..870376d896 100644 --- a/service-providers/network-requester/src/core.rs +++ b/service-providers/network-requester/src/core.rs @@ -7,14 +7,19 @@ use crate::error::NetworkRequesterError; use crate::statistics::ServiceStatisticsCollector; use crate::websocket; use crate::websocket::TSWebsocketStream; -use client_connections::ClosedConnectionReceiver; +use client_connections::{ + ConnectionCommand, ConnectionCommandReceiver, LaneQueueLengths, TransmissionLane, +}; use futures::channel::mpsc; use futures::stream::{SplitSink, SplitStream}; use futures::{SinkExt, StreamExt}; use log::*; use nymsphinx::addressing::clients::Recipient; use nymsphinx::receiver::ReconstructedMessage; -use proxy_helpers::connection_controller::{Controller, ControllerCommand, ControllerSender}; +use proxy_helpers::connection_controller::{ + BroadcastActiveConnections, Controller, ControllerCommand, ControllerSender, +}; +use proxy_helpers::proxy_runner::{MixProxyReader, MixProxySender}; use socks5_requests::{ ConnectionId, Message as Socks5Message, NetworkRequesterResponse, Request, Response, }; @@ -67,14 +72,14 @@ impl ServiceProvider { /// via the `websocket_writer`. async fn mixnet_response_listener( mut websocket_writer: SplitSink, - mut mix_reader: mpsc::UnboundedReceiver<(Socks5Message, Recipient)>, + mut mix_reader: MixProxyReader<(Socks5Message, Recipient)>, stats_collector: Option, - mut closed_connection_rx: ClosedConnectionReceiver, + mut client_connection_rx: ConnectionCommandReceiver, ) { loop { tokio::select! { // TODO: wire SURBs in here once they're available - socks5_msg = mix_reader.next() => { + socks5_msg = mix_reader.recv() => { if let Some((msg, return_address)) = socks5_msg { if let Some(stats_collector) = stats_collector.as_ref() { if let Some(remote_addr) = stats_collector @@ -97,7 +102,7 @@ impl ServiceProvider { recipient: return_address, message: msg.into_bytes(), with_reply_surb: false, - connection_id: conn_id, + connection_id: Some(conn_id), }; let message = Message::Binary(response_message.serialize()); @@ -107,17 +112,49 @@ impl ServiceProvider { break; } }, - Some(id) = closed_connection_rx.next() => { - let msg = ClientRequest::ClosedConnection(id); - let ws_msg = Message::Binary(msg.serialize()); - websocket_writer.send(ws_msg).await.unwrap(); - } + Some(command) = client_connection_rx.next() => { + match command { + ConnectionCommand::Close(id) => { + let msg = ClientRequest::ClosedConnection(id); + let ws_msg = Message::Binary(msg.serialize()); + websocket_writer.send(ws_msg).await.unwrap(); + } + ConnectionCommand::ActiveConnections(ids) => { + // We can optimize this by sending a single request, but this is + // usually in the low single digits, max a few tens, so we leave that + // for a rainy day. + // Also that means fiddling with the currently manual + // serialize/deserialize we do with ClientRequests ... bleh + for id in ids { + log::trace!("Requesting lane queue length for: {}", id); + let msg = ClientRequest::GetLaneQueueLength(id); + let ws_msg = Message::Binary(msg.serialize()); + websocket_writer.send(ws_msg).await.unwrap(); + } + } + } + }, } } } + fn handle_lane_queue_length_response( + lane_queue_lengths: &LaneQueueLengths, + lane: u64, + queue_length: usize, + ) { + log::trace!("Received LaneQueueLength lane: {lane}, queue_length: {queue_length}"); + if let Ok(mut lane_queue_lengths) = lane_queue_lengths.lock() { + let lane = TransmissionLane::ConnectionId(lane); + lane_queue_lengths.map.insert(lane, queue_length); + } else { + log::warn!("Unable to lock lane queue lengths, skipping updating received lane length") + } + } + async fn read_websocket_message( websocket_reader: &mut SplitStream, + lane_queue_lengths: LaneQueueLengths, ) -> Option { while let Some(msg) = websocket_reader.next().await { let data = msg @@ -138,6 +175,14 @@ impl ServiceProvider { let received = match deserialized_message { ServerResponse::Received(received) => received, + ServerResponse::LaneQueueLength(lane, queue_length) => { + Self::handle_lane_queue_length_response( + &lane_queue_lengths, + lane, + queue_length, + ); + continue; + } ServerResponse::Error(err) => { panic!("received error from native client! - {}", err) } @@ -153,7 +198,8 @@ impl ServiceProvider { remote_addr: String, return_address: Recipient, controller_sender: ControllerSender, - mix_input_sender: mpsc::UnboundedSender<(Socks5Message, Recipient)>, + mix_input_sender: MixProxySender<(Socks5Message, Recipient)>, + lane_queue_lengths: LaneQueueLengths, shutdown: ShutdownListener, ) { let mut conn = match Connection::new(conn_id, remote_addr.clone(), return_address).await { @@ -167,11 +213,12 @@ impl ServiceProvider { // inform the remote that the connection is closed before it even was established mix_input_sender - .unbounded_send(( + .send(( Socks5Message::Response(Response::new(conn_id, Vec::new(), true)), return_address, )) - .unwrap(); + .await + .expect("InputMessageReceiver has stopped receiving!"); return; } @@ -191,7 +238,7 @@ impl ServiceProvider { ); // run the proxy on the connection - conn.run_proxy(mix_receiver, mix_input_sender, shutdown) + conn.run_proxy(mix_receiver, mix_input_sender, lane_queue_lengths, shutdown) .await; // proxy is done - remove the access channel from the controller @@ -207,10 +254,12 @@ impl ServiceProvider { ); } - fn handle_proxy_connect( + #[allow(clippy::too_many_arguments)] + async fn handle_proxy_connect( &mut self, controller_sender: &mut ControllerSender, - mix_input_sender: &mpsc::UnboundedSender<(Socks5Message, Recipient)>, + mix_input_sender: &MixProxySender<(Socks5Message, Recipient)>, + lane_queue_lengths: LaneQueueLengths, conn_id: ConnectionId, remote_addr: String, return_address: Recipient, @@ -220,13 +269,14 @@ impl ServiceProvider { let log_msg = format!("Domain {:?} failed filter check", remote_addr); log::info!("{}", log_msg); mix_input_sender - .unbounded_send(( + .send(( Socks5Message::NetworkRequesterResponse(NetworkRequesterResponse::new( conn_id, log_msg, )), return_address, )) - .unwrap(); + .await + .expect("InputMessageReceiver has stopped receiving!"); return; } @@ -241,6 +291,7 @@ impl ServiceProvider { return_address, controller_sender_clone, mix_input_sender_clone, + lane_queue_lengths, shutdown, ) .await @@ -248,7 +299,6 @@ impl ServiceProvider { } fn handle_proxy_send( - &self, controller_sender: &mut ControllerSender, conn_id: ConnectionId, data: Vec, @@ -263,7 +313,8 @@ impl ServiceProvider { &mut self, raw_request: &[u8], controller_sender: &mut ControllerSender, - mix_input_sender: &mpsc::UnboundedSender<(Socks5Message, Recipient)>, + mix_input_sender: &MixProxySender<(Socks5Message, Recipient)>, + lane_queue_lengths: LaneQueueLengths, stats_collector: Option, shutdown: ShutdownListener, ) { @@ -287,11 +338,13 @@ impl ServiceProvider { self.handle_proxy_connect( controller_sender, mix_input_sender, + lane_queue_lengths, req.conn_id, req.remote_addr, req.return_address, shutdown, ) + .await } Request::Send(conn_id, data, closed) => { @@ -309,7 +362,7 @@ impl ServiceProvider { .processed(remote_addr, data.len() as u32); } } - self.handle_proxy_send(controller_sender, conn_id, data, closed) + Self::handle_proxy_send(controller_sender, conn_id, data, closed) } }, Socks5Message::Response(_) | Socks5Message::NetworkRequesterResponse(_) => {} @@ -326,21 +379,28 @@ impl ServiceProvider { // channels responsible for managing messages that are to be sent to the mix network. The receiver is // going to be used by `mixnet_response_listener` let (mix_input_sender, mix_input_receiver) = - mpsc::unbounded::<(Socks5Message, Recipient)>(); + tokio::sync::mpsc::channel::<(Socks5Message, Recipient)>(1); // Used to notify tasks to shutdown. Not all tasks fully supports this (yet). let shutdown = task::ShutdownNotifier::default(); - // Channel for announcing closed (socks5) connections by the controller. - // The `mixnet_response_listener` will forward this info to the client using a - // `ClientRequest`. - let (closed_connection_tx, closed_connection_rx) = mpsc::unbounded(); + // Channel for announcing client connection state by the controller. + // The `mixnet_response_listener` will use this to either report closed connection to the + // client or request lane queue lengths. + let (client_connection_tx, client_connection_rx) = mpsc::unbounded(); + + // Shared queue length data. Published by the `OutQueueController` in the client, and used + // primarily to throttle incoming connections + let shared_lane_queue_lengths = LaneQueueLengths::new(); // Controller for managing all active connections. // We provide it with a ShutdownListener since it requires it, even though for the network // requester shutdown signalling is not yet fully implemented. - let (mut active_connections_controller, mut controller_sender) = - Controller::new(closed_connection_tx, shutdown.subscribe()); + let (mut active_connections_controller, mut controller_sender) = Controller::new( + client_connection_tx, + BroadcastActiveConnections::On, + shutdown.subscribe(), + ); tokio::spawn(async move { active_connections_controller.run().await; @@ -368,7 +428,7 @@ impl ServiceProvider { websocket_writer, mix_input_receiver, stats_collector_clone, - closed_connection_rx, + client_connection_rx, ) .await; }); @@ -376,12 +436,14 @@ impl ServiceProvider { println!("\nAll systems go. Press CTRL-C to stop the server."); // for each incoming message from the websocket... (which in 99.99% cases is going to be a mix message) loop { - let received = match Self::read_websocket_message(&mut websocket_reader).await { - Some(msg) => msg, - None => { - error!("The websocket stream has finished!"); - return Ok(()); - } + let Some(received) = Self::read_websocket_message( + &mut websocket_reader, + shared_lane_queue_lengths.clone() + ) + .await + else { + log::error!("The websocket stream has finished!"); + return Ok(()); }; let raw_message = received.message; @@ -391,6 +453,7 @@ impl ServiceProvider { &raw_message, &mut controller_sender, &mix_input_sender, + shared_lane_queue_lengths.clone(), stats_collector.clone(), shutdown.subscribe(), ) diff --git a/service-providers/network-requester/src/main.rs b/service-providers/network-requester/src/main.rs index 71046109da..e6cb277610 100644 --- a/service-providers/network-requester/src/main.rs +++ b/service-providers/network-requester/src/main.rs @@ -23,15 +23,19 @@ const ENABLE_STATISTICS: &str = "enable-statistics"; #[derive(Args)] struct Run { /// Specifies whether this network requester should run in 'open-proxy' mode + #[clap(long)] open_proxy: bool, /// Websocket port to bind to + #[clap(long)] websocket_port: Option, /// Enable service anonymized statistics that get sent to a statistics aggregator server + #[clap(long)] enable_statistics: bool, /// Mixnet client address where a statistics aggregator is running. The default value is a Nym aggregator client + #[clap(long)] statistics_recipient: Option, } diff --git a/service-providers/network-requester/src/statistics/collector.rs b/service-providers/network-requester/src/statistics/collector.rs index 96e7292d82..ac43252c33 100644 --- a/service-providers/network-requester/src/statistics/collector.rs +++ b/service-providers/network-requester/src/statistics/collector.rs @@ -2,8 +2,8 @@ // SPDX-License-Identifier: Apache-2.0 use async_trait::async_trait; -use futures::channel::mpsc; use log::*; +use proxy_helpers::proxy_runner::MixProxySender; use rand::RngCore; use serde::Deserialize; use sqlx::types::chrono::{DateTime, Utc}; @@ -77,13 +77,13 @@ pub struct ServiceStatisticsCollector { pub(crate) response_stats_data: Arc>, pub(crate) connected_services: Arc>>, stats_provider_addr: Recipient, - mix_input_sender: mpsc::UnboundedSender<(Socks5Message, Recipient)>, + mix_input_sender: MixProxySender<(Socks5Message, Recipient)>, } impl ServiceStatisticsCollector { pub async fn new( stats_provider_addr: Option, - mix_input_sender: mpsc::UnboundedSender<(Socks5Message, Recipient)>, + mix_input_sender: MixProxySender<(Socks5Message, Recipient)>, ) -> Result { let client = reqwest::Client::builder() .timeout(Duration::from_secs(3)) @@ -176,19 +176,21 @@ impl StatisticsCollector for ServiceStatisticsCollector { self.stats_provider_addr, ); self.mix_input_sender - .unbounded_send(( + .send(( Socks5Message::Request(connect_req), self.stats_provider_addr, )) - .unwrap(); + .await + .expect("MixProxyReader has stopped receiving!"); trace!("Sending data to statistics service"); let mut message_sender = OrderedMessageSender::new(); let ordered_msg = message_sender.wrap_message(msg).into_bytes(); let send_req = Request::new_send(conn_id, ordered_msg, true); self.mix_input_sender - .unbounded_send((Socks5Message::Request(send_req), self.stats_provider_addr)) - .unwrap(); + .send((Socks5Message::Request(send_req), self.stats_provider_addr)) + .await + .expect("MixProxyReader has stopped receiving!"); Ok(()) } diff --git a/tools/nym-cli/Cargo.toml b/tools/nym-cli/Cargo.toml index 2ec5af6404..67c3bda09a 100644 --- a/tools/nym-cli/Cargo.toml +++ b/tools/nym-cli/Cargo.toml @@ -18,6 +18,7 @@ serde_json = "1" tokio = { version = "1.11", features = [ "net", "rt-multi-thread", "macros", "signal"] } bip39 = "1.0.1" anyhow = "1" +tap = "1" nym-cli-commands = { path = "../../common/commands" } logging = { path = "../../common/logging"} diff --git a/tools/nym-cli/src/validator/vesting.rs b/tools/nym-cli/src/validator/vesting.rs index d87ca2c9ff..202011bc3c 100644 --- a/tools/nym-cli/src/validator/vesting.rs +++ b/tools/nym-cli/src/validator/vesting.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use network_defaults::NymNetworkDetails; -use nym_cli_commands::context::{create_signing_client, ClientArgs}; +use nym_cli_commands::context::{create_query_client, create_signing_client, ClientArgs}; pub(crate) async fn execute( global_args: ClientArgs, @@ -19,18 +19,22 @@ pub(crate) async fn execute( .await } Some(nym_cli_commands::validator::vesting::VestingScheduleCommands::Query(args)) => { + let address_from_args = args.address.clone(); nym_cli_commands::validator::vesting::query_vesting_schedule::query( args, - create_signing_client(global_args, network_details)?, + create_query_client(network_details)?, + address_from_args, ) .await } Some(nym_cli_commands::validator::vesting::VestingScheduleCommands::VestedBalance( args, )) => { + let address_from_args = args.address.clone(); nym_cli_commands::validator::vesting::balance::balance( args, - create_signing_client(global_args, network_details)?, + create_query_client(network_details)?, + address_from_args, ) .await } diff --git a/validator-api/Cargo.toml b/validator-api/Cargo.toml index 9b07010edc..4fb4413f99 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -16,6 +16,7 @@ rust-version = "1.56" [dependencies] async-trait = "0.1.52" +bs58 = {version = "0.4.0", optional = true } cfg-if = "1.0" clap = { version = "3.2", features = ["cargo"] } console-subscriber = { version = "0.1.1", optional = true } # validator-api needs to be built with RUSTFLAGS="--cfg tokio_unstable" @@ -63,6 +64,7 @@ schemars = { version = "0.8", features = ["preserve_order"] } ## internal coconut-bandwidth-contract-common = { path = "../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" } +coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg", optional = true } coconut-interface = { path = "../common/coconut-interface", optional = true } config = { path = "../common/config" } cosmwasm-std = "1.0.0" @@ -70,13 +72,16 @@ credential-storage = { path = "../common/credential-storage" } credentials = { path = "../common/credentials", optional = true } crypto = { path = "../common/crypto" } logging = { path = "../common/logging"} +cw3 = { version = "0.13.4", optional = true } +dkg = { path = "../common/crypto/dkg", optional = true } gateway-client = { path = "../common/client-libs/gateway-client" } inclusion-probability = { path = "../common/inclusion-probability" } mixnet-contract-common = { path = "../common/cosmwasm-smart-contracts/mixnet-contract" } -contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common" } +contracts-common = { path = "../common/cosmwasm-smart-contracts/contracts-common", features = ["coconut"] } multisig-contract-common = { path = "../common/cosmwasm-smart-contracts/multisig-contract" } nymcoconut = { path = "../common/nymcoconut", optional = true } nymsphinx = { path = "../common/nymsphinx" } +pemstore = { path = "../common/pemstore", optional = true } task = { path = "../common/task" } topology = { path = "../common/topology" } validator-api-requests = { path = "validator-api-requests" } @@ -89,10 +94,15 @@ version-checker = { path = "../common/version-checker" } coconut = [ "coconut-interface", "credentials", + "cw3", "gateway-client/coconut", "credentials/coconut", "validator-api-requests/coconut", "nymcoconut", + "coconut-dkg-common", + "dkg", + "bs58", + "pemstore", ] no-reward = [] generate-ts = ["ts-rs"] @@ -113,5 +123,5 @@ vergen = { version = "7", default-features = false, features = [ ] } [dev-dependencies] -cw3 = "0.13.2" -cw-utils = "0.13.2" +cw3 = "0.13.4" +cw-utils = "0.13.4" diff --git a/validator-api/src/coconut/client.rs b/validator-api/src/coconut/client.rs index 626a4b775b..41fffd4856 100644 --- a/validator-api/src/coconut/client.rs +++ b/validator-api/src/coconut/client.rs @@ -3,7 +3,12 @@ use crate::coconut::error::Result; use coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; -use multisig_contract_common::msg::ProposalResponse; +use coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}; +use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState}; +use coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; +use contracts_common::dealings::ContractSafeBytes; +use cw3::ProposalResponse; +use validator_client::nymd::cosmwasm_client::types::ExecuteResult; use validator_client::nymd::{AccountId, Fee, TxResponse}; #[async_trait] @@ -11,10 +16,27 @@ pub trait Client { async fn address(&self) -> AccountId; async fn get_tx(&self, tx_hash: &str) -> Result; async fn get_proposal(&self, proposal_id: u64) -> Result; + async fn list_proposals(&self) -> Result>; async fn get_spent_credential( &self, blinded_serial_number: String, ) -> Result; + async fn get_current_epoch_state(&self) -> Result; + async fn get_self_registered_dealer_details(&self) -> Result; + async fn get_current_dealers(&self) -> Result>; + async fn get_dealings(&self, idx: usize) -> Result>; + async fn get_verification_key_shares(&self) -> Result>; async fn vote_proposal(&self, proposal_id: u64, vote_yes: bool, fee: Option) -> Result<()>; + async fn execute_proposal(&self, proposal_id: u64) -> Result<()>; + async fn register_dealer( + &self, + bte_key: EncodedBTEPublicKeyWithProof, + announce_address: String, + ) -> Result; + async fn submit_dealing(&self, dealing_bytes: ContractSafeBytes) -> Result; + async fn submit_verification_key_share( + &self, + share: VerificationKeyShare, + ) -> Result; } diff --git a/validator-api/src/coconut/comm.rs b/validator-api/src/coconut/comm.rs index e6f7b8a1ac..42a428f153 100644 --- a/validator-api/src/coconut/comm.rs +++ b/validator-api/src/coconut/comm.rs @@ -2,28 +2,33 @@ // SPDX-License-Identifier: Apache-2.0 use crate::coconut::error::Result; +use crate::nymd_client::Client; use coconut_interface::VerificationKey; -use credentials::obtain_aggregate_verification_key; -use url::Url; +use credentials::coconut::utils::obtain_aggregate_verification_key; +use validator_client::nymd::SigningNymdClient; +use validator_client::CoconutApiClient; #[async_trait] pub trait APICommunicationChannel { async fn aggregated_verification_key(&self) -> Result; } -pub struct QueryCommunicationChannel { - validator_apis: Vec, +pub(crate) struct QueryCommunicationChannel { + nymd_client: Client, } impl QueryCommunicationChannel { - pub fn new(validator_apis: Vec) -> Self { - QueryCommunicationChannel { validator_apis } + pub fn new(nymd_client: Client) -> Self { + QueryCommunicationChannel { nymd_client } } } #[async_trait] impl APICommunicationChannel for QueryCommunicationChannel { async fn aggregated_verification_key(&self) -> Result { - Ok(obtain_aggregate_verification_key(&self.validator_apis).await?) + let client = self.nymd_client.0.read().await; + let coconut_api_clients = CoconutApiClient::all_coconut_api_clients(&client).await?; + let vk = obtain_aggregate_verification_key(&coconut_api_clients).await?; + Ok(vk) } } diff --git a/validator-api/src/coconut/dkg/client.rs b/validator-api/src/coconut/dkg/client.rs new file mode 100644 index 0000000000..6850dabc1c --- /dev/null +++ b/validator-api/src/coconut/dkg/client.rs @@ -0,0 +1,146 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::coconut::client::Client; +use crate::coconut::error::CoconutError; +use coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}; +use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState, NodeIndex}; +use coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; +use contracts_common::dealings::ContractSafeBytes; +use cw3::ProposalResponse; +use validator_client::nymd::cosmwasm_client::logs::{find_attribute, NODE_INDEX}; +use validator_client::nymd::cosmwasm_client::types::ExecuteResult; +use validator_client::nymd::AccountId; + +pub(crate) struct DkgClient { + inner: Box, +} + +impl DkgClient { + // Some queries simply don't work the first time + // Until we determine why that is, retry the query a few more times + const RETRIES: usize = 3; + + pub(crate) fn new(nymd_client: C) -> Self + where + C: Client + Send + Sync + 'static, + { + DkgClient { + inner: Box::new(nymd_client), + } + } + + pub(crate) async fn _get_address(&self) -> AccountId { + self.inner.address().await + } + + pub(crate) async fn get_current_epoch_state(&self) -> Result { + let mut ret = self.inner.get_current_epoch_state().await; + for _ in 0..Self::RETRIES { + if ret.is_ok() { + return ret; + } + ret = self.inner.get_current_epoch_state().await; + } + ret + } + + pub(crate) async fn get_self_registered_dealer_details( + &self, + ) -> Result { + self.inner.get_self_registered_dealer_details().await + } + + pub(crate) async fn get_current_dealers(&self) -> Result, CoconutError> { + self.inner.get_current_dealers().await + } + + pub(crate) async fn get_dealings( + &self, + idx: usize, + ) -> Result, CoconutError> { + let mut ret = self.inner.get_dealings(idx).await; + for _ in 0..Self::RETRIES { + if ret.is_ok() { + return ret; + } + ret = self.inner.get_dealings(idx).await; + } + ret + } + + pub(crate) async fn get_verification_key_shares( + &self, + ) -> Result, CoconutError> { + self.inner.get_verification_key_shares().await + } + + pub(crate) async fn list_proposals(&self) -> Result, CoconutError> { + self.inner.list_proposals().await + } + + pub(crate) async fn register_dealer( + &self, + bte_key: EncodedBTEPublicKeyWithProof, + announce_address: String, + ) -> Result { + let res = self + .inner + .register_dealer(bte_key, announce_address) + .await?; + let node_index = find_attribute(&res.logs, "wasm", NODE_INDEX) + .ok_or(CoconutError::NodeIndexRecoveryError { + reason: String::from("node index not found"), + })? + .value + .parse::() + .map_err(|_| CoconutError::NodeIndexRecoveryError { + reason: String::from("node index could not be parsed"), + })?; + + Ok(node_index) + } + + pub(crate) async fn submit_dealing( + &self, + dealing_bytes: ContractSafeBytes, + ) -> Result<(), CoconutError> { + self.inner.submit_dealing(dealing_bytes).await?; + Ok(()) + } + + pub(crate) async fn submit_verification_key_share( + &self, + share: VerificationKeyShare, + ) -> Result { + let mut ret = self + .inner + .submit_verification_key_share(share.clone()) + .await; + for _ in 0..Self::RETRIES { + if let Ok(res) = ret { + return Ok(res); + } + ret = self + .inner + .submit_verification_key_share(share.clone()) + .await; + } + ret + } + + pub(crate) async fn vote_verification_key_share( + &self, + proposal_id: u64, + vote_yes: bool, + ) -> Result<(), CoconutError> { + self.inner.vote_proposal(proposal_id, vote_yes, None).await + } + + pub(crate) async fn execute_verification_key_share( + &self, + proposal_id: u64, + ) -> Result<(), CoconutError> { + self.inner.execute_proposal(proposal_id).await + } +} diff --git a/validator-api/src/coconut/dkg/complaints.rs b/validator-api/src/coconut/dkg/complaints.rs new file mode 100644 index 0000000000..8a95645a30 --- /dev/null +++ b/validator-api/src/coconut/dkg/complaints.rs @@ -0,0 +1,11 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use dkg::error::DkgError; + +pub(crate) enum ComplaintReason { + MalformedBTEPublicKey, + MissingDealing, + MalformedDealing(DkgError), + DealingVerificationError(DkgError), +} diff --git a/validator-api/src/coconut/dkg/controller.rs b/validator-api/src/coconut/dkg/controller.rs new file mode 100644 index 0000000000..9058dc558b --- /dev/null +++ b/validator-api/src/coconut/dkg/controller.rs @@ -0,0 +1,133 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::coconut::dkg::client::DkgClient; +use crate::coconut::dkg::state::{ConsistentState, State}; +use crate::coconut::dkg::verification_key::{ + verification_key_finalization, verification_key_validation, +}; +use crate::coconut::dkg::{ + dealing::dealing_exchange, public_key::public_key_submission, + verification_key::verification_key_submission, +}; +use crate::coconut::keypair::KeyPair as CoconutKeyPair; +use crate::{nymd_client, Config}; +use anyhow::Result; +use coconut_dkg_common::types::EpochState; +use dkg::bte::keys::KeyPair as DkgKeyPair; +use rand::rngs::OsRng; +use rand::RngCore; +use std::path::PathBuf; +use std::time::Duration; +use task::ShutdownListener; +use tokio::time::interval; +use validator_client::nymd::SigningNymdClient; + +pub(crate) fn init_keypair(config: &Config) -> Result<()> { + let mut rng = OsRng; + let dkg_params = dkg::bte::setup(); + let kp = DkgKeyPair::new(&dkg_params, &mut rng); + pemstore::store_keypair( + &kp, + &pemstore::KeyPairPath::new( + config.decryption_key_path(), + config.public_key_with_proof_path(), + ), + )?; + Ok(()) +} + +pub(crate) struct DkgController { + dkg_client: DkgClient, + secret_key_path: PathBuf, + verification_key_path: PathBuf, + state: State, + rng: R, + polling_rate: Duration, +} + +impl DkgController { + pub(crate) async fn new( + config: &Config, + nymd_client: nymd_client::Client, + coconut_keypair: CoconutKeyPair, + rng: R, + ) -> Result { + let dkg_keypair = pemstore::load_keypair(&pemstore::KeyPairPath::new( + config.decryption_key_path(), + config.public_key_with_proof_path(), + ))?; + if let Ok(coconut_keypair_value) = pemstore::load_keypair(&pemstore::KeyPairPath::new( + config.secret_key_path(), + config.verification_key_path(), + )) { + coconut_keypair.set(coconut_keypair_value).await; + } + + Ok(DkgController { + dkg_client: DkgClient::new(nymd_client), + secret_key_path: config.secret_key_path(), + verification_key_path: config.verification_key_path(), + state: State::new(config.get_announce_address(), dkg_keypair, coconut_keypair), + rng, + polling_rate: config.get_dkg_contract_polling_rate(), + }) + } + + async fn handle_epoch_state(&mut self) { + match self.dkg_client.get_current_epoch_state().await { + Err(e) => warn!("Could not get current epoch state {}", e), + Ok(epoch_state) => { + if let Err(e) = self.state.is_consistent(epoch_state).await { + error!( + "Epoch state is corrupted - {}, the process should be terminated", + e + ); + } + let ret = match epoch_state { + EpochState::PublicKeySubmission => { + public_key_submission(&self.dkg_client, &mut self.state).await + } + EpochState::DealingExchange => { + dealing_exchange(&self.dkg_client, &mut self.state, self.rng.clone()).await + } + EpochState::VerificationKeySubmission => { + let keypair_path = pemstore::KeyPairPath::new( + self.secret_key_path.clone(), + self.verification_key_path.clone(), + ); + verification_key_submission( + &self.dkg_client, + &mut self.state, + &keypair_path, + ) + .await + } + EpochState::VerificationKeyValidation => { + verification_key_validation(&self.dkg_client, &mut self.state).await + } + EpochState::VerificationKeyFinalization => { + verification_key_finalization(&self.dkg_client, &mut self.state).await + } + // Just wait, in case we need to redo dkg at some point + EpochState::InProgress => Ok(()), + }; + if let Err(e) = ret { + warn!("Could not handle this iteration for the epoch state: {}", e); + } + } + } + } + + pub(crate) async fn run(mut self, mut shutdown: ShutdownListener) { + let mut interval = interval(self.polling_rate); + while !shutdown.is_shutdown() { + tokio::select! { + _ = interval.tick() => self.handle_epoch_state().await, + _ = shutdown.recv() => { + trace!("DkgController: Received shutdown"); + } + } + } + } +} diff --git a/validator-api/src/coconut/dkg/dealing.rs b/validator-api/src/coconut/dkg/dealing.rs new file mode 100644 index 0000000000..fea2d4e98f --- /dev/null +++ b/validator-api/src/coconut/dkg/dealing.rs @@ -0,0 +1,52 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::coconut::dkg::client::DkgClient; +use crate::coconut::dkg::state::{ConsistentState, State}; +use crate::coconut::error::CoconutError; +use coconut_dkg_common::types::TOTAL_DEALINGS; +use contracts_common::dealings::ContractSafeBytes; +use dkg::bte::setup; +use dkg::Dealing; +use rand::RngCore; + +pub(crate) async fn dealing_exchange( + dkg_client: &DkgClient, + state: &mut State, + rng: impl RngCore + Clone, +) -> Result<(), CoconutError> { + if state.receiver_index().is_some() { + return Ok(()); + } + + let dealers = dkg_client.get_current_dealers().await?; + // note: ceiling in integer division can be achieved via q = (x + y - 1) / y; + let threshold = (2 * dealers.len() as u64 + 3 - 1) / 3; + + state.set_dealers(dealers); + state.set_threshold(threshold); + let receivers = state.current_dealers_by_idx(); + let params = setup(); + let dealer_index = state.node_index_value()?; + let receiver_index = receivers + .keys() + .position(|node_index| *node_index == dealer_index); + for _ in 0..TOTAL_DEALINGS { + let (dealing, _) = Dealing::create( + rng.clone(), + ¶ms, + dealer_index, + threshold, + &receivers, + None, + ); + dkg_client + .submit_dealing(ContractSafeBytes::from(&dealing)) + .await?; + } + + info!("DKG: Finished submitting dealing"); + state.set_receiver_index(receiver_index); + + Ok(()) +} diff --git a/validator-api/src/coconut/dkg/mod.rs b/validator-api/src/coconut/dkg/mod.rs new file mode 100644 index 0000000000..4149a3e236 --- /dev/null +++ b/validator-api/src/coconut/dkg/mod.rs @@ -0,0 +1,10 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub(crate) mod client; +pub(crate) mod complaints; +pub(crate) mod controller; +pub(crate) mod dealing; +pub(crate) mod public_key; +pub(crate) mod state; +pub(crate) mod verification_key; diff --git a/validator-api/src/coconut/dkg/public_key.rs b/validator-api/src/coconut/dkg/public_key.rs new file mode 100644 index 0000000000..6669db09ca --- /dev/null +++ b/validator-api/src/coconut/dkg/public_key.rs @@ -0,0 +1,32 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::coconut::dkg::client::DkgClient; +use crate::coconut::dkg::state::State; +use crate::coconut::error::CoconutError; + +pub(crate) async fn public_key_submission( + dkg_client: &DkgClient, + state: &mut State, +) -> Result<(), CoconutError> { + if state.node_index().is_some() { + return Ok(()); + } + + let bte_key = bs58::encode(&state.dkg_keypair().public_key().to_bytes()).into_string(); + let index = if let Some(details) = dkg_client + .get_self_registered_dealer_details() + .await? + .details + { + details.assigned_index + } else { + dkg_client + .register_dealer(bte_key, state.announce_address().to_string()) + .await? + }; + state.set_node_index(index); + info!("DKG: Using node index {}", index); + + Ok(()) +} diff --git a/validator-api/src/coconut/dkg/state.rs b/validator-api/src/coconut/dkg/state.rs new file mode 100644 index 0000000000..f6be2bc8a7 --- /dev/null +++ b/validator-api/src/coconut/dkg/state.rs @@ -0,0 +1,258 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::coconut::dkg::complaints::ComplaintReason; +use crate::coconut::error::CoconutError; +use crate::coconut::keypair::KeyPair as CoconutKeyPair; +use coconut_dkg_common::dealer::DealerDetails; +use coconut_dkg_common::types::EpochState; +use cosmwasm_std::Addr; +use dkg::bte::{keys::KeyPair as DkgKeyPair, PublicKey, PublicKeyWithProof}; +use dkg::{NodeIndex, RecoveredVerificationKeys, Threshold}; +use std::collections::BTreeMap; +use url::Url; + +// note: each dealer is also a receiver which simplifies some logic significantly +#[derive(Debug)] +pub(crate) struct DkgParticipant { + pub(crate) _address: Addr, + pub(crate) bte_public_key_with_proof: PublicKeyWithProof, + pub(crate) assigned_index: NodeIndex, +} + +impl TryFrom for DkgParticipant { + type Error = ComplaintReason; + + fn try_from(dealer: DealerDetails) -> Result { + let bte_public_key_with_proof = bs58::decode(dealer.bte_public_key_with_proof) + .into_vec() + .map(|bytes| PublicKeyWithProof::try_from_bytes(&bytes)) + .map_err(|_| ComplaintReason::MalformedBTEPublicKey)? + .map_err(|_| ComplaintReason::MalformedBTEPublicKey)?; + + Ok(DkgParticipant { + _address: dealer.address, + bte_public_key_with_proof, + assigned_index: dealer.assigned_index, + }) + } +} + +#[async_trait] +pub(crate) trait ConsistentState { + fn node_index_value(&self) -> Result; + fn receiver_index_value(&self) -> Result; + fn threshold(&self) -> Result; + async fn coconut_keypair_is_some(&self) -> Result<(), CoconutError>; + fn proposal_id_value(&self) -> Result; + async fn is_consistent(&self, epoch_state: EpochState) -> Result<(), CoconutError> { + match epoch_state { + EpochState::PublicKeySubmission => {} + EpochState::DealingExchange => { + self.node_index_value()?; + } + EpochState::VerificationKeySubmission => { + self.receiver_index_value()?; + self.threshold()?; + } + EpochState::VerificationKeyValidation => { + self.coconut_keypair_is_some().await?; + } + EpochState::VerificationKeyFinalization => { + self.proposal_id_value()?; + } + EpochState::InProgress => {} + } + Ok(()) + } +} + +pub(crate) struct State { + announce_address: Url, + dkg_keypair: DkgKeyPair, + coconut_keypair: CoconutKeyPair, + node_index: Option, + dealers: BTreeMap>, + receiver_index: Option, + threshold: Option, + recovered_vks: Vec, + proposal_id: Option, + voted_vks: bool, + executed_proposal: bool, +} + +#[async_trait] +impl ConsistentState for State { + fn node_index_value(&self) -> Result { + self.node_index.ok_or(CoconutError::UnrecoverableState { + reason: String::from("Node index should have been set"), + }) + } + + fn receiver_index_value(&self) -> Result { + self.receiver_index.ok_or(CoconutError::UnrecoverableState { + reason: String::from("Receiver index should have been set"), + }) + } + + fn threshold(&self) -> Result { + let threshold = self.threshold.ok_or(CoconutError::UnrecoverableState { + reason: String::from("Threshold should have been set"), + })?; + if self.current_dealers_by_idx().len() < threshold as usize { + Err(CoconutError::UnrecoverableState { + reason: String::from( + "Not enough good dealers in the signer set to achieve threshold", + ), + }) + } else { + Ok(threshold) + } + } + + async fn coconut_keypair_is_some(&self) -> Result<(), CoconutError> { + if self.coconut_keypair_is_some().await { + Ok(()) + } else { + Err(CoconutError::UnrecoverableState { + reason: String::from("Coconut keypair should have been set"), + }) + } + } + + fn proposal_id_value(&self) -> Result { + self.proposal_id.ok_or(CoconutError::UnrecoverableState { + reason: String::from("Proposal id should have benn set"), + }) + } +} + +impl State { + pub fn new( + announce_address: Url, + dkg_keypair: DkgKeyPair, + coconut_keypair: CoconutKeyPair, + ) -> Self { + State { + announce_address, + dkg_keypair, + coconut_keypair, + node_index: None, + dealers: BTreeMap::new(), + receiver_index: None, + threshold: None, + recovered_vks: vec![], + proposal_id: None, + voted_vks: false, + executed_proposal: false, + } + } + + pub fn announce_address(&self) -> &Url { + &self.announce_address + } + + pub fn dkg_keypair(&self) -> &DkgKeyPair { + &self.dkg_keypair + } + + pub async fn coconut_keypair_is_some(&self) -> bool { + self.coconut_keypair.get().await.is_some() + } + + pub fn node_index(&self) -> Option { + self.node_index + } + + pub fn receiver_index(&self) -> Option { + self.receiver_index + } + + pub fn current_dealers_by_addr(&self) -> BTreeMap { + self.dealers + .iter() + .filter_map(|(addr, dealer)| { + dealer + .as_ref() + .ok() + .map(|participant| (addr.clone(), participant.assigned_index)) + }) + .collect() + } + + pub fn current_dealers_by_idx(&self) -> BTreeMap { + self.dealers + .iter() + .filter_map(|(_, dealer)| { + dealer.as_ref().ok().map(|participant| { + ( + participant.assigned_index, + *participant.bte_public_key_with_proof.public_key(), + ) + }) + }) + .collect() + } + + pub fn recovered_vks(&self) -> &Vec { + &self.recovered_vks + } + + pub fn voted_vks(&self) -> bool { + self.voted_vks + } + + pub fn executed_proposal(&self) -> bool { + self.executed_proposal + } + + pub fn set_recovered_vks(&mut self, recovered_vks: Vec) { + self.recovered_vks = recovered_vks; + } + + pub async fn set_coconut_keypair(&mut self, coconut_keypair: coconut_interface::KeyPair) { + self.coconut_keypair.set(coconut_keypair).await + } + + pub fn set_node_index(&mut self, node_index: NodeIndex) { + self.node_index = Some(node_index); + } + + pub fn set_dealers(&mut self, dealers: Vec) { + self.dealers = BTreeMap::from_iter( + dealers + .into_iter() + .map(|details| (details.address.clone(), DkgParticipant::try_from(details))), + ) + } + + pub fn mark_bad_dealer(&mut self, dealer_addr: &Addr, reason: ComplaintReason) { + if let Some((_, value)) = self + .dealers + .iter_mut() + .find(|(addr, _)| *addr == dealer_addr) + { + *value = Err(reason); + } + } + + pub fn set_receiver_index(&mut self, receiver_index: Option) { + self.receiver_index = receiver_index; + } + + pub fn set_threshold(&mut self, threshold: Threshold) { + self.threshold = Some(threshold); + } + + pub fn set_proposal_id(&mut self, proposal_id: u64) { + self.proposal_id = Some(proposal_id); + } + + pub fn set_voted_vks(&mut self) { + self.voted_vks = true; + } + + pub fn set_executed_proposal(&mut self) { + self.executed_proposal = true; + } +} diff --git a/validator-api/src/coconut/dkg/verification_key.rs b/validator-api/src/coconut/dkg/verification_key.rs new file mode 100644 index 0000000000..08c352255d --- /dev/null +++ b/validator-api/src/coconut/dkg/verification_key.rs @@ -0,0 +1,244 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::coconut::dkg::client::DkgClient; +use crate::coconut::dkg::complaints::ComplaintReason; +use crate::coconut::dkg::state::{ConsistentState, State}; +use crate::coconut::error::CoconutError; +use coconut_dkg_common::event_attributes::DKG_PROPOSAL_ID; +use coconut_dkg_common::types::{NodeIndex, TOTAL_DEALINGS}; +use coconut_dkg_common::verification_key::owner_from_cosmos_msgs; +use coconut_interface::KeyPair as CoconutKeyPair; +use cosmwasm_std::Addr; +use credentials::coconut::bandwidth::{PRIVATE_ATTRIBUTES, PUBLIC_ATTRIBUTES}; +use cw3::{ProposalResponse, Status}; +use dkg::bte::{decrypt_share, setup}; +use dkg::{combine_shares, try_recover_verification_keys, Dealing, Threshold}; +use nymcoconut::tests::helpers::transpose_matrix; +use nymcoconut::{check_vk_pairing, Base58, KeyPair, Parameters, SecretKey, VerificationKey}; +use pemstore::KeyPairPath; +use std::collections::BTreeMap; +use validator_client::nymd::cosmwasm_client::logs::find_attribute; + +// Filter the dealers based on what dealing they posted (or not) in the contract +async fn deterministic_filter_dealers( + dkg_client: &DkgClient, + state: &mut State, + threshold: Threshold, +) -> Result>, CoconutError> { + let mut dealings_maps = vec![]; + let initial_dealers_by_addr = state.current_dealers_by_addr(); + let initial_receivers = state.current_dealers_by_idx(); + let params = setup(); + + for idx in 0..TOTAL_DEALINGS { + let dealings = dkg_client.get_dealings(idx).await?; + let dealings_map = + BTreeMap::from_iter(dealings.into_iter().filter_map(|contract_dealing| { + match Dealing::try_from(&contract_dealing.dealing) { + Ok(dealing) => { + if let Err(err) = + dealing.verify(¶ms, threshold, &initial_receivers, None) + { + state.mark_bad_dealer( + &contract_dealing.dealer, + ComplaintReason::DealingVerificationError(err), + ); + None + } else if let Some(idx) = + initial_dealers_by_addr.get(&contract_dealing.dealer) + { + Some((*idx, (contract_dealing.dealer, dealing))) + } else { + None + } + } + Err(err) => { + state.mark_bad_dealer( + &contract_dealing.dealer, + ComplaintReason::MalformedDealing(err), + ); + None + } + } + })); + dealings_maps.push(dealings_map); + } + for (addr, _) in initial_dealers_by_addr.iter() { + for dealings_map in dealings_maps.iter() { + if !dealings_map.iter().any(|(_, (address, _))| address == addr) { + state.mark_bad_dealer(addr, ComplaintReason::MissingDealing); + break; + } + } + } + + Ok(dealings_maps) +} + +fn derive_partial_keypair( + state: &mut State, + threshold: Threshold, + dealings_maps: Vec>, +) -> Result { + let filtered_receivers_by_idx = state.current_dealers_by_idx(); + let filtered_dealers_by_addr = state.current_dealers_by_addr(); + let dk = state.dkg_keypair().private_key(); + let node_index_value = state.receiver_index_value()?; + let mut scalars = vec![]; + let mut recovered_vks = vec![]; + for dealings_map in dealings_maps.into_iter() { + let filtered_dealings: Vec<_> = dealings_map + .into_iter() + .filter_map(|(_, (addr, dealing))| { + if filtered_dealers_by_addr.keys().any(|a| addr == *a) { + Some(dealing) + } else { + None + } + }) + .collect(); + let recovered = try_recover_verification_keys( + &filtered_dealings, + threshold, + &filtered_receivers_by_idx, + )?; + recovered_vks.push(recovered); + + let shares = filtered_dealings + .iter() + .map(|dealing| decrypt_share(dk, node_index_value, &dealing.ciphertexts, None)) + .collect::>()?; + let scalar = combine_shares( + shares, + &filtered_receivers_by_idx + .keys() + .copied() + .collect::>(), + )?; + scalars.push(scalar); + } + state.set_recovered_vks(recovered_vks); + + let params = Parameters::new(PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES)?; + let x = scalars.pop().unwrap(); + let sk = SecretKey::create_from_raw(x, scalars); + let vk = sk.verification_key(¶ms); + + Ok(CoconutKeyPair::from_keys(sk, vk)) +} + +pub(crate) async fn verification_key_submission( + dkg_client: &DkgClient, + state: &mut State, + keypair_path: &KeyPairPath, +) -> Result<(), CoconutError> { + if state.coconut_keypair_is_some().await { + return Ok(()); + } + + let threshold = state.threshold()?; + let dealings_maps = deterministic_filter_dealers(dkg_client, state, threshold).await?; + let coconut_keypair = derive_partial_keypair(state, threshold, dealings_maps)?; + let vk_share = coconut_keypair.verification_key().to_bs58(); + pemstore::store_keypair(&coconut_keypair, keypair_path)?; + let res = dkg_client.submit_verification_key_share(vk_share).await?; + let proposal_id = find_attribute(&res.logs, "wasm", DKG_PROPOSAL_ID) + .ok_or(CoconutError::ProposalIdError { + reason: String::from("proposal id not found"), + })? + .value + .parse::() + .map_err(|_| CoconutError::ProposalIdError { + reason: String::from("proposal id could not be parsed to u64"), + })?; + state.set_proposal_id(proposal_id); + state.set_coconut_keypair(coconut_keypair).await; + info!("DKG: Submitted own verification key"); + + Ok(()) +} + +fn validate_proposal(proposal: &ProposalResponse) -> Option<(Addr, u64)> { + if proposal.status == Status::Open { + if let Some(owner) = owner_from_cosmos_msgs(&proposal.msgs) { + return Some((owner, proposal.id)); + } + } + None +} + +pub(crate) async fn verification_key_validation( + dkg_client: &DkgClient, + state: &mut State, +) -> Result<(), CoconutError> { + if state.voted_vks() { + return Ok(()); + } + + let vk_shares = dkg_client.get_verification_key_shares().await?; + let proposal_ids = BTreeMap::from_iter( + dkg_client + .list_proposals() + .await? + .iter() + .filter_map(validate_proposal), + ); + let filtered_receivers_by_idx: Vec<_> = + state.current_dealers_by_idx().keys().copied().collect(); + let recovered_partials: Vec<_> = state + .recovered_vks() + .iter() + .map(|recovered_vk| recovered_vk.recovered_partials.clone()) + .collect(); + let recovered_partials = transpose_matrix(recovered_partials); + let params = Parameters::new(PUBLIC_ATTRIBUTES + PRIVATE_ATTRIBUTES)?; + for contract_share in vk_shares { + if let Some(proposal_id) = proposal_ids.get(&contract_share.owner).copied() { + match VerificationKey::try_from_bs58(contract_share.share) { + Ok(vk) => { + if let Some(idx) = filtered_receivers_by_idx + .iter() + .position(|node_index| contract_share.node_index == *node_index) + { + if !check_vk_pairing(¶ms, &recovered_partials[idx], &vk) { + dkg_client + .vote_verification_key_share(proposal_id, false) + .await?; + } else { + dkg_client + .vote_verification_key_share(proposal_id, true) + .await?; + } + } + } + Err(_) => { + dkg_client + .vote_verification_key_share(proposal_id, false) + .await? + } + } + } + } + state.set_voted_vks(); + info!("DKG: Validated the other verification keys"); + Ok(()) +} + +pub(crate) async fn verification_key_finalization( + dkg_client: &DkgClient, + state: &mut State, +) -> Result<(), CoconutError> { + if state.executed_proposal() { + return Ok(()); + } + + let proposal_id = state.proposal_id_value()?; + dkg_client + .execute_verification_key_share(proposal_id) + .await?; + state.set_executed_proposal(); + info!("DKG: Finalized own verification key on chain"); + + Ok(()) +} diff --git a/validator-api/src/coconut/error.rs b/validator-api/src/coconut/error.rs index 7dfdb843c2..9b6fadf957 100644 --- a/validator-api/src/coconut/error.rs +++ b/validator-api/src/coconut/error.rs @@ -11,6 +11,7 @@ use crypto::asymmetric::{ encryption::KeyRecoveryError, identity::{Ed25519RecoveryError, SignatureError}, }; +use dkg::error::DkgError; use validator_client::nymd::error::NymdError; use crate::node_status_api::models::ValidatorApiStorageError; @@ -19,6 +20,9 @@ pub type Result = std::result::Result; #[derive(Debug, Error)] pub enum CoconutError { + #[error("{0}")] + IOError(#[from] std::io::Error), + #[error("Could not parse Ed25519 data")] Ed25519ParseError(#[from] Ed25519RecoveryError), @@ -31,6 +35,9 @@ pub enum CoconutError { #[error("Nymd error - {0}")] NymdError(#[from] NymdError), + #[error("Validator client error - {0}")] + ValidatorClientError(#[from] validator_client::ValidatorClientError), + #[error("Coconut internal error - {0}")] CoconutInternalError(#[from] nymcoconut::CoconutError), @@ -77,6 +84,21 @@ pub enum CoconutError { #[error("Invalid status of credential: {status}")] InvalidCredentialStatus { status: String }, + + #[error("DKG error: {0}")] + DkgError(#[from] DkgError), + + #[error("Failed to recover assigned node index: {reason}")] + NodeIndexRecoveryError { reason: String }, + + #[error("Unrecoverable state: {reason}. Process should be restarted")] + UnrecoverableState { reason: String }, + + #[error("DKG has not finished yet in order to derive the coconut key")] + KeyPairNotDerivedYet, + + #[error("There was a problem with the proposal id: {reason}")] + ProposalIdError { reason: String }, } impl<'r, 'o: 'r> Responder<'r, 'o> for CoconutError { diff --git a/validator-api/src/coconut/keypair.rs b/validator-api/src/coconut/keypair.rs new file mode 100644 index 0000000000..680f1c4d13 --- /dev/null +++ b/validator-api/src/coconut/keypair.rs @@ -0,0 +1,27 @@ +// Copyright 2022 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::Arc; +use tokio::sync::{RwLock, RwLockReadGuard}; + +#[derive(Clone, Debug)] +pub struct KeyPair { + inner: Arc>>, +} + +impl KeyPair { + pub fn new() -> Self { + Self { + inner: Arc::new(RwLock::new(None)), + } + } + + pub async fn get(&self) -> RwLockReadGuard<'_, Option> { + self.inner.read().await + } + + pub async fn set(&self, keypair: coconut_interface::KeyPair) { + let mut w_lock = self.inner.write().await; + *w_lock = Some(keypair); + } +} diff --git a/validator-api/src/coconut/mod.rs b/validator-api/src/coconut/mod.rs index 45de17a25d..94bc96608d 100644 --- a/validator-api/src/coconut/mod.rs +++ b/validator-api/src/coconut/mod.rs @@ -4,7 +4,9 @@ pub(crate) mod client; pub(crate) mod comm; mod deposit; +pub(crate) mod dkg; pub(crate) mod error; +pub(crate) mod keypair; #[cfg(test)] mod tests; @@ -16,8 +18,9 @@ use crate::ValidatorApiStorage; use coconut_bandwidth_contract_common::spend_credential::{ funds_from_cosmos_msgs, SpendCredentialStatus, }; +use coconut_interface::KeyPair as CoconutKeyPair; use coconut_interface::{ - Attribute, BlindSignRequest, BlindedSignature, KeyPair, Parameters, VerificationKey, + Attribute, BlindSignRequest, BlindedSignature, Parameters, VerificationKey, }; use config::defaults::VALIDATOR_API_VERSION; use credentials::coconut::params::{ @@ -26,9 +29,9 @@ use credentials::coconut::params::{ use crypto::asymmetric::encryption; use crypto::shared_key::new_ephemeral_shared_key; use crypto::symmetric::stream_cipher; +use keypair::KeyPair; use validator_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, - VerifyCredentialBody, VerifyCredentialResponse, + BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use validator_client::nymd::{Coin, Fee}; use validator_client::validator_api::routes::{BANDWIDTH, COCONUT_ROUTES}; @@ -182,8 +185,6 @@ impl InternalSignRequest { ), routes![ post_blind_sign, - get_verification_key, - get_cosmos_address, post_partial_bandwidth_credential, verify_bandwidth_credential ], @@ -192,7 +193,7 @@ impl InternalSignRequest { } } -fn blind_sign(request: InternalSignRequest, key_pair: &KeyPair) -> Result { +fn blind_sign(request: InternalSignRequest, key_pair: &CoconutKeyPair) -> Result { let params = Parameters::new(request.total_params())?; Ok(coconut_interface::blind_sign( ¶ms, @@ -225,7 +226,11 @@ pub async fn post_blind_sign( blind_sign_request_body.public_attributes(), blind_sign_request_body.blind_sign_request().clone(), ); - let blinded_signature = blind_sign(internal_request, &state.key_pair)?; + let blinded_signature = if let Some(keypair) = state.key_pair.get().await.as_ref() { + blind_sign(internal_request, keypair)? + } else { + return Err(CoconutError::KeyPairNotDerivedYet); + }; let response = state .encrypt_and_store( @@ -250,22 +255,6 @@ pub async fn post_partial_bandwidth_credential( Ok(Json(v)) } -#[get("/verification-key")] -pub async fn get_verification_key( - state: &RocketState, -) -> Result> { - Ok(Json(VerificationKeyResponse::new( - state.key_pair.verification_key(), - ))) -} - -#[get("/cosmos-address")] -pub async fn get_cosmos_address(state: &RocketState) -> Result> { - Ok(Json(CosmosAddressResponse::new( - state.client.address().await, - ))) -} - #[post("/verify-bandwidth-credential", data = "")] pub async fn verify_bandwidth_credential( verify_credential_body: Json, diff --git a/validator-api/src/coconut/tests.rs b/validator-api/src/coconut/tests.rs index 757eb9aa4a..842194b913 100644 --- a/validator-api/src/coconut/tests.rs +++ b/validator-api/src/coconut/tests.rs @@ -19,33 +19,36 @@ use credentials::coconut::params::{ }; use crypto::shared_key::recompute_shared_key; use crypto::symmetric::stream_cipher; -use multisig_contract_common::msg::ProposalResponse; use nymcoconut::tests::helpers::theta_from_keys_and_attributes; use nymcoconut::{ - prepare_blind_sign, ttp_keygen, Base58, BlindSignRequest, BlindedSignature, KeyPair, Parameters, + prepare_blind_sign, ttp_keygen, Base58, BlindSignRequest, BlindedSignature, Parameters, }; use validator_api_requests::coconut::{ - BlindSignRequestBody, BlindedSignatureResponse, CosmosAddressResponse, VerificationKeyResponse, - VerifyCredentialBody, VerifyCredentialResponse, + BlindSignRequestBody, BlindedSignatureResponse, VerifyCredentialBody, VerifyCredentialResponse, }; use validator_client::nymd::Coin; use validator_client::nymd::{tx::Hash, AccountId, DeliverTx, Event, Fee, Tag, TxResponse}; use validator_client::validator_api::routes::{ - API_VERSION, BANDWIDTH, COCONUT_BLIND_SIGN, COCONUT_COSMOS_ADDRESS, - COCONUT_PARTIAL_BANDWIDTH_CREDENTIAL, COCONUT_ROUTES, COCONUT_VERIFICATION_KEY, - COCONUT_VERIFY_BANDWIDTH_CREDENTIAL, + API_VERSION, BANDWIDTH, COCONUT_BLIND_SIGN, COCONUT_PARTIAL_BANDWIDTH_CREDENTIAL, + COCONUT_ROUTES, COCONUT_VERIFY_BANDWIDTH_CREDENTIAL, }; use crate::coconut::State; use crate::ValidatorApiStorage; use async_trait::async_trait; +use coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}; +use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState}; +use coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; +use contracts_common::dealings::ContractSafeBytes; use crypto::asymmetric::{encryption, identity}; +use cw3::ProposalResponse; use rand_07::rngs::OsRng; use rocket::http::Status; use rocket::local::asynchronous::Client; use std::collections::HashMap; use std::str::FromStr; use std::sync::{Arc, RwLock}; +use validator_client::nymd::cosmwasm_client::types::ExecuteResult; const TEST_COIN_DENOM: &str = "unym"; const TEST_REWARDING_VALIDATOR_ADDRESS: &str = "n19lc9u84cz0yz3fww5283nucc9yvr8gsjmgeul0"; @@ -103,6 +106,10 @@ impl super::client::Client for DummyClient { }) } + async fn list_proposals(&self) -> Result> { + todo!() + } + async fn get_spent_credential( &self, blinded_serial_number: String, @@ -117,6 +124,26 @@ impl super::client::Client for DummyClient { }) } + async fn get_current_epoch_state(&self) -> Result { + todo!() + } + + async fn get_self_registered_dealer_details(&self) -> Result { + todo!() + } + + async fn get_current_dealers(&self) -> Result> { + todo!() + } + + async fn get_dealings(&self, _idx: usize) -> Result> { + todo!() + } + + async fn get_verification_key_shares(&self) -> Result> { + todo!() + } + async fn vote_proposal( &self, proposal_id: u64, @@ -132,6 +159,29 @@ impl super::client::Client for DummyClient { } Ok(()) } + + async fn execute_proposal(&self, _proposal_id: u64) -> Result<()> { + todo!() + } + + async fn register_dealer( + &self, + _bte_key: EncodedBTEPublicKeyWithProof, + _announce_address: String, + ) -> Result { + todo!() + } + + async fn submit_dealing(&self, _dealing_bytes: ContractSafeBytes) -> Result { + todo!() + } + + async fn submit_verification_key_share( + &self, + _share: VerificationKeyShare, + ) -> Result { + todo!() + } } #[derive(Clone, Debug)] @@ -174,63 +224,6 @@ pub fn tx_entry_fixture(tx_hash: &str) -> TxResponse { } } -async fn check_signer_verif_key(key_pair: KeyPair) { - let verification_key = key_pair.verification_key(); - - let mut db_dir = std::env::temp_dir(); - db_dir.push(&verification_key.to_bs58()[..8]); - let storage = ValidatorApiStorage::init(db_dir).await.unwrap(); - let nymd_client = DummyClient::new( - AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(), - &Arc::new(RwLock::new(HashMap::new())), - &Arc::new(RwLock::new(HashMap::new())), - &Arc::new(RwLock::new(HashMap::new())), - ); - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); - - let rocket = rocket::build().attach(InternalSignRequest::stage( - nymd_client, - TEST_COIN_DENOM.to_string(), - key_pair, - comm_channel, - storage, - )); - - let client = Client::tracked(rocket) - .await - .expect("valid rocket instance"); - - let response = client - .get(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_VERIFICATION_KEY - )) - .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::() - // .await - // .is_some()); - let verification_key_response = - serde_json::from_str::(&response.into_string().await.unwrap()) - .unwrap(); - assert_eq!(verification_key_response.key, verification_key); -} - -#[tokio::test] -async fn multiple_verification_key() { - let params = Parameters::new(4).unwrap(); - let num_authorities = 4; - - let key_pairs = ttp_keygen(¶ms, num_authorities, num_authorities).unwrap(); - for key_pair in key_pairs.into_iter() { - check_signer_verif_key(key_pair).await; - } -} - #[tokio::test] async fn signed_before() { let tx_hash = @@ -281,11 +274,13 @@ async fn signed_before() { &Arc::new(RwLock::new(HashMap::new())), ); let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let staged_key_pair = crate::coconut::KeyPair::new(); + staged_key_pair.set(key_pair).await; let rocket = rocket::build().attach(InternalSignRequest::stage( nymd_client, TEST_COIN_DENOM.to_string(), - key_pair, + staged_key_pair, comm_channel, storage.clone(), )); @@ -351,10 +346,12 @@ async fn state_functions() { db_dir.push(&key_pair.verification_key().to_bs58()[..8]); let storage = ValidatorApiStorage::init(db_dir).await.unwrap(); let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let staged_key_pair = crate::coconut::KeyPair::new(); + staged_key_pair.set(key_pair).await; let state = State::new( nymd_client, TEST_COIN_DENOM.to_string(), - key_pair, + staged_key_pair, comm_channel, storage.clone(), ); @@ -521,11 +518,13 @@ async fn blind_sign_correct() { &Arc::new(RwLock::new(HashMap::new())), ); let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let staged_key_pair = crate::coconut::KeyPair::new(); + staged_key_pair.set(key_pair).await; let rocket = rocket::build().attach(InternalSignRequest::stage( nymd_client, TEST_COIN_DENOM.to_string(), - key_pair, + staged_key_pair, comm_channel, storage.clone(), )); @@ -600,11 +599,13 @@ async fn signature_test() { &Arc::new(RwLock::new(HashMap::new())), ); let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let staged_key_pair = crate::coconut::KeyPair::new(); + staged_key_pair.set(key_pair).await; let rocket = rocket::build().attach(InternalSignRequest::stage( nymd_client, TEST_COIN_DENOM.to_string(), - key_pair, + staged_key_pair, comm_channel, storage.clone(), )); @@ -656,47 +657,6 @@ async fn signature_test() { ); } -#[tokio::test] -async fn get_cosmos_address() { - let validator_address = AccountId::from_str(TEST_REWARDING_VALIDATOR_ADDRESS).unwrap(); - let nymd_client = DummyClient::new( - validator_address.clone(), - &Arc::new(RwLock::new(HashMap::new())), - &Arc::new(RwLock::new(HashMap::new())), - &Arc::new(RwLock::new(HashMap::new())), - ); - let mut db_dir = std::env::temp_dir(); - let key_pair = ttp_keygen(&Parameters::new(4).unwrap(), 1, 1) - .unwrap() - .remove(0); - db_dir.push(&key_pair.verification_key().to_bs58()[..8]); - let storage = ValidatorApiStorage::init(db_dir).await.unwrap(); - let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); - let rocket = rocket::build().attach(InternalSignRequest::stage( - nymd_client, - TEST_COIN_DENOM.to_string(), - key_pair, - comm_channel, - storage.clone(), - )); - let client = Client::tracked(rocket) - .await - .expect("valid rocket instance"); - - let response = client - .get(format!( - "/{}/{}/{}/{}", - API_VERSION, COCONUT_ROUTES, BANDWIDTH, COCONUT_COSMOS_ADDRESS - )) - .dispatch() - .await; - assert_eq!(response.status(), Status::Ok); - let cosmos_addr_response = - serde_json::from_str::(&response.into_string().await.unwrap()) - .unwrap(); - assert_eq!(validator_address, cosmos_addr_response.addr); -} - #[tokio::test] async fn verification_of_bandwidth_credential() { // Setup variables @@ -718,15 +678,23 @@ async fn verification_of_bandwidth_credential() { hash_to_scalar(voucher_value.to_string()), hash_to_scalar(voucher_info), ]; - let theta = theta_from_keys_and_attributes(¶ms, &key_pairs, &public_attributes).unwrap(); + let indices: Vec = key_pairs + .iter() + .enumerate() + .map(|(idx, _)| (idx + 1) as u64) + .collect(); + let theta = + theta_from_keys_and_attributes(¶ms, &key_pairs, &indices, &public_attributes).unwrap(); let key_pair = key_pairs.remove(0); db_dir.push(&key_pair.verification_key().to_bs58()[..8]); let storage1 = ValidatorApiStorage::init(db_dir).await.unwrap(); let comm_channel = DummyCommunicationChannel::new(key_pair.verification_key()); + let staged_key_pair = crate::coconut::KeyPair::new(); + staged_key_pair.set(key_pair).await; let rocket = rocket::build().attach(InternalSignRequest::stage( nymd_client.clone(), TEST_COIN_DENOM.to_string(), - key_pair, + staged_key_pair, comm_channel.clone(), storage1.clone(), )); diff --git a/validator-api/src/config/mod.rs b/validator-api/src/config/mod.rs index 3676e5ae3f..e08b3bb20a 100644 --- a/validator-api/src/config/mod.rs +++ b/validator-api/src/config/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::config::template::config_template; +use config::defaults::DEFAULT_VALIDATOR_API_PORT; use config::NymConfig; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -12,6 +13,8 @@ mod template; pub const DEFAULT_LOCAL_VALIDATOR: &str = "http://localhost:26657"; +pub const DEFAULT_DKG_CONTRACT_POLLING_RATE: Duration = Duration::from_secs(10); + const DEFAULT_GATEWAY_SENDING_RATE: usize = 200; const DEFAULT_MAX_CONCURRENT_GATEWAY_CLIENTS: usize = 50; const DEFAULT_PACKET_DELIVERY_TIMEOUT: Duration = Duration::from_secs(20); @@ -91,6 +94,11 @@ pub struct Base { local_validator: Url, + /// Address announced to the directory server for the clients to connect to. + // It is useful, say, in NAT scenarios or wanting to more easily update actual IP address + // later on by using name resolvable with a DNS query, such as `nymtech.net`. + announce_address: Url, + /// Address of the validator contract managing the network mixnet_contract_address: String, @@ -100,11 +108,17 @@ pub struct Base { impl Default for Base { fn default() -> Self { + let default_validator: Url = DEFAULT_LOCAL_VALIDATOR + .parse() + .expect("default local validator is malformed!"); + let mut default_announce_address = default_validator.clone(); + default_announce_address + .set_port(Some(DEFAULT_VALIDATOR_API_PORT)) + .expect("default local validator is malformed!"); Base { id: String::default(), - local_validator: DEFAULT_LOCAL_VALIDATOR - .parse() - .expect("default local validator is malformed!"), + local_validator: default_validator, + announce_address: default_announce_address, mixnet_contract_address: String::default(), mnemonic: "exact antique hybrid width raise anchor puzzle degree fee quit long crack net vague hip despair write put useless civil mechanic broom music day".to_string(), } @@ -262,19 +276,62 @@ impl Default for Rewarding { } } -#[derive(Debug, Default, Deserialize, PartialEq, Eq, Serialize)] +#[derive(Debug, Deserialize, PartialEq, Eq, Serialize)] #[serde(default)] pub struct CoconutSigner { /// Specifies whether rewarding service is enabled in this process. enabled: bool, - /// Path to the signing keypair - keypair_path: PathBuf, + /// Path to the coconut verification key. + verification_key_path: PathBuf, - /// Specifies list of all validators on the network issuing coconut credentials. - /// A special care must be taken to ensure they are in correct order. - /// The list must also contain THIS validator that is running the test - all_validator_apis: Vec, + /// Path to the coconut secret key. + secret_key_path: PathBuf, + + /// Path to the dkg dealer decryption key. + decryption_key_path: PathBuf, + + /// Path to the dkg dealer public key with proof. + public_key_with_proof_path: PathBuf, + + /// Duration of the interval for polling the dkg contract. + dkg_contract_polling_rate: Duration, +} + +impl CoconutSigner { + pub const DKG_DECRYPTION_KEY_FILE: &'static str = "dkg_decryption_key.pem"; + pub const DKG_PUBLIC_KEY_WITH_PROOF_FILE: &'static str = "dkg_public_key_with_proof.pem"; + pub const COCONUT_VERIFICATION_KEY_FILE: &'static str = "coconut_verification_key.pem"; + pub const COCONUT_SECRET_KEY_FILE: &'static str = "coconut_secret_key.pem"; + + fn default_coconut_verification_key_path() -> PathBuf { + Config::default_data_directory(None).join(Self::COCONUT_VERIFICATION_KEY_FILE) + } + + fn default_coconut_secret_key_path() -> PathBuf { + Config::default_data_directory(None).join(Self::COCONUT_SECRET_KEY_FILE) + } + + fn default_dkg_decryption_key_path() -> PathBuf { + Config::default_data_directory(None).join(Self::DKG_DECRYPTION_KEY_FILE) + } + + fn default_dkg_public_key_with_proof_path() -> PathBuf { + Config::default_data_directory(None).join(Self::DKG_PUBLIC_KEY_WITH_PROOF_FILE) + } +} + +impl Default for CoconutSigner { + fn default() -> Self { + Self { + enabled: Default::default(), + verification_key_path: CoconutSigner::default_coconut_verification_key_path(), + secret_key_path: CoconutSigner::default_coconut_secret_key_path(), + decryption_key_path: CoconutSigner::default_dkg_decryption_key_path(), + public_key_with_proof_path: CoconutSigner::default_dkg_public_key_with_proof_path(), + dkg_contract_polling_rate: DEFAULT_DKG_CONTRACT_POLLING_RATE, + } + } } impl Config { @@ -288,14 +345,17 @@ impl Config { Config::default_data_directory(Some(id)).join(NodeStatusAPI::DB_FILE); self.network_monitor.credentials_database_path = Config::default_data_directory(Some(id)).join(NetworkMonitor::DB_FILE); + self.coconut_signer.verification_key_path = Config::default_data_directory(Some(id)) + .join(CoconutSigner::COCONUT_VERIFICATION_KEY_FILE); + self.coconut_signer.secret_key_path = + Config::default_data_directory(Some(id)).join(CoconutSigner::COCONUT_SECRET_KEY_FILE); + self.coconut_signer.decryption_key_path = + Config::default_data_directory(Some(id)).join(CoconutSigner::DKG_DECRYPTION_KEY_FILE); + self.coconut_signer.public_key_with_proof_path = Config::default_data_directory(Some(id)) + .join(CoconutSigner::DKG_PUBLIC_KEY_WITH_PROOF_FILE); self } - #[cfg(feature = "coconut")] - pub fn keypair_path(&self) -> PathBuf { - self.coconut_signer.keypair_path.clone() - } - pub fn with_network_monitor_enabled(mut self, enabled: bool) -> Self { self.network_monitor.enabled = enabled; self @@ -322,6 +382,12 @@ impl Config { self } + #[cfg(feature = "coconut")] + pub fn with_announce_address(mut self, announce_address: Url) -> Self { + self.base.announce_address = announce_address; + self + } + pub fn with_custom_mixnet_contract>(mut self, mixnet_contract: S) -> Self { self.base.mixnet_contract_address = mixnet_contract.into(); self @@ -332,18 +398,6 @@ impl Config { self } - #[cfg(feature = "coconut")] - pub fn with_keypair_path(mut self, keypair_path: PathBuf) -> Self { - self.coconut_signer.keypair_path = keypair_path; - self - } - - #[cfg(feature = "coconut")] - pub fn with_custom_validator_apis(mut self, validator_api_urls: Vec) -> Self { - self.coconut_signer.all_validator_apis = validator_api_urls; - self - } - pub fn with_minimum_interval_monitor_threshold(mut self, threshold: u8) -> Self { self.rewarding.minimum_interval_monitor_threshold = threshold; self @@ -390,6 +444,11 @@ impl Config { self.base.local_validator.clone() } + #[cfg(feature = "coconut")] + pub fn get_announce_address(&self) -> Url { + self.base.announce_address.clone() + } + pub fn get_mixnet_contract_address(&self) -> String { self.base.mixnet_contract_address.clone() } @@ -450,10 +509,29 @@ impl Config { self.node_status_api.database_path.clone() } - // fix dead code warnings as this method is only ever used with coconut feature #[cfg(feature = "coconut")] - pub fn get_all_validator_api_endpoints(&self) -> Vec { - self.coconut_signer.all_validator_apis.clone() + pub fn verification_key_path(&self) -> PathBuf { + self.coconut_signer.verification_key_path.clone() + } + + #[cfg(feature = "coconut")] + pub fn secret_key_path(&self) -> PathBuf { + self.coconut_signer.secret_key_path.clone() + } + + #[cfg(feature = "coconut")] + pub fn decryption_key_path(&self) -> PathBuf { + self.coconut_signer.decryption_key_path.clone() + } + + #[cfg(feature = "coconut")] + pub fn public_key_with_proof_path(&self) -> PathBuf { + self.coconut_signer.public_key_with_proof_path.clone() + } + + #[cfg(feature = "coconut")] + pub fn get_dkg_contract_polling_rate(&self) -> Duration { + self.coconut_signer.dkg_contract_polling_rate } // TODO: Remove if still unused diff --git a/validator-api/src/config/template.rs b/validator-api/src/config/template.rs index 340b93b70a..3d745f30ed 100644 --- a/validator-api/src/config/template.rs +++ b/validator-api/src/config/template.rs @@ -16,6 +16,11 @@ id = '{{ base.id }}' # Validator server to which the API will be getting information about the network. local_validator = '{{ base.local_validator }}' +# Address announced to the directory server for the clients to connect to. +# It is useful, say, in NAT scenarios or wanting to more easily update actual IP address +# later on by using name resolvable with a DNS query, such as `nymtech.net`. +announce_address = '{{ base.announce_address }}' + # Address of the validator contract managing the network. mixnet_contract_address = '{{ base.mixnet_contract_address }}' @@ -93,17 +98,17 @@ minimum_interval_monitor_threshold = {{ rewarding.minimum_interval_monitor_thres # Specifies whether rewarding service is enabled in this process. enabled = {{ coconut_signer.enabled }} -# Path to the signing keypair -keypair_path = '{{ coconut_signer.keypair_path }}' +# Path to the coconut verification key +verification_key_path = '{{ coconut_signer.verification_key_path }}' -# Specifies list of all validators on the network issuing coconut credentials. -# A special care must be taken to ensure they are in correct order. -# The list must also contain THIS validator that is running the test -all_validator_apis = [ - {{#each coconut_signer.all_validator_apis }} - '{{this}}', - {{/each}} -] +# Path to the coconut verification key +secret_key_path = '{{ coconut_signer.secret_key_path }}' + +# Path to the dkg dealer decryption key +decryption_key_path = '{{ coconut_signer.decryption_key_path }}' + +# Path to the dkg dealer public key with proof +public_key_with_proof_path = '{{ coconut_signer.public_key_with_proof_path }}' "# } diff --git a/validator-api/src/main.rs b/validator-api/src/main.rs index ece8fab179..5116896858 100644 --- a/validator-api/src/main.rs +++ b/validator-api/src/main.rs @@ -12,8 +12,6 @@ use crate::nymd_client::Client; use crate::storage::ValidatorApiStorage; use ::config::defaults::mainnet::read_var_if_not_default; use ::config::defaults::setup_env; -#[cfg(feature = "coconut")] -use ::config::defaults::var_names::API_VALIDATOR; use ::config::defaults::var_names::{CONFIGURED, MIXNET_CONTRACT_ADDRESS, MIX_DENOM}; use ::config::NymConfig; use anyhow::Result; @@ -35,14 +33,20 @@ use std::time::Duration; use std::{fs, process}; use task::ShutdownNotifier; use tokio::sync::Notify; +#[cfg(feature = "coconut")] +use url::Url; use validator_client::nymd::SigningNymdClient; use crate::epoch_operations::RewardedSetUpdater; #[cfg(feature = "coconut")] -use coconut::{comm::QueryCommunicationChannel, InternalSignRequest}; -#[cfg(feature = "coconut")] -use coconut_interface::{Base58, KeyPair}; +use coconut::{ + comm::QueryCommunicationChannel, + dkg::controller::{init_keypair, DkgController}, + InternalSignRequest, +}; use logging::setup_logging; +#[cfg(feature = "coconut")] +use validator_client::nymd::bip32::secp256k1::elliptic_curve::rand_core::OsRng; pub(crate) mod config; pub(crate) mod contract_cache; @@ -67,9 +71,7 @@ const NYMD_VALIDATOR_ARG: &str = "nymd-validator"; const ENABLED_CREDENTIALS_MODE_ARG_NAME: &str = "enabled-credentials-mode"; #[cfg(feature = "coconut")] -const API_VALIDATORS_ARG: &str = "api-validators"; -#[cfg(feature = "coconut")] -const KEYPAIR_ARG: &str = "keypair"; +const ANNOUNCE_ADDRESS: &str = "announce-address"; #[cfg(feature = "coconut")] const COCONUT_ENABLED: &str = "enable-coconut"; @@ -189,21 +191,15 @@ fn parse_args() -> ArgMatches { #[cfg(feature = "coconut")] let base_app = base_app .arg( - Arg::with_name(KEYPAIR_ARG) - .help("Path to the secret key file") - .takes_value(true) - .long(KEYPAIR_ARG), - ) - .arg( - Arg::with_name(API_VALIDATORS_ARG) - .help("specifies list of all validators on the network issuing coconut credentials. Ensure they are properly ordered") - .long(API_VALIDATORS_ARG) - .takes_value(true) + Arg::with_name(ANNOUNCE_ADDRESS) + .help("Announced address where coconut clients will connect.") + .long(ANNOUNCE_ADDRESS) + .takes_value(true), ) .arg( Arg::with_name(COCONUT_ENABLED) .help("Flag to indicate whether coconut signer authority is enabled on this API") - .requires_all(&[KEYPAIR_ARG, MNEMONIC_ARG, API_VALIDATORS_ARG]) + .requires_all(&[MNEMONIC_ARG, ANNOUNCE_ADDRESS]) .long(COCONUT_ENABLED), ); base_app.get_matches() @@ -272,12 +268,10 @@ fn override_config(mut config: Config, matches: &ArgMatches) -> Config { } #[cfg(feature = "coconut")] - if let Some(raw_validators) = matches.value_of(API_VALIDATORS_ARG) { - config = config.with_custom_validator_apis(::config::parse_validators(raw_validators)); - } else if std::env::var(CONFIGURED).is_ok() { - if let Some(raw_validators) = read_var_if_not_default(API_VALIDATOR) { - config = config.with_custom_validator_apis(::config::parse_validators(&raw_validators)) - } + if let Some(announce_address) = matches.value_of(ANNOUNCE_ADDRESS) { + config = config.with_announce_address( + Url::parse(announce_address).expect("Could not parse announce address"), + ); } if let Some(raw_validator) = matches.value_of(NYMD_VALIDATOR_ARG) { @@ -334,11 +328,6 @@ fn override_config(mut config: Config, matches: &ArgMatches) -> Config { ) } - #[cfg(feature = "coconut")] - if let Some(keypair_path) = matches.value_of(KEYPAIR_ARG) { - config = config.with_keypair_path(keypair_path.into()) - } - if matches.is_present(ENABLED_CREDENTIALS_MODE_ARG_NAME) { config = config.with_disabled_credentials_mode(false) } @@ -381,6 +370,7 @@ fn setup_liftoff_notify(notify: Arc) -> AdHoc { fn setup_network_monitor<'a>( config: &'a Config, + _nymd_client: Client, system_version: &str, rocket: &Rocket, ) -> Option> { @@ -394,6 +384,7 @@ fn setup_network_monitor<'a>( Some(NetworkMonitorBuilder::new( config, + _nymd_client, system_version, node_status_storage, validator_cache, @@ -415,6 +406,7 @@ async fn setup_rocket( _mix_denom: String, liftoff_notify: Arc, _nymd_client: Client, + #[cfg(feature = "coconut")] coconut_keypair: coconut::keypair::KeyPair, ) -> Result> { let openapi_settings = rocket_okapi::settings::OpenApiSettings::default(); let mut rocket = rocket::build(); @@ -447,15 +439,11 @@ async fn setup_rocket( #[cfg(feature = "coconut")] let rocket = if config.get_coconut_signer_enabled() { - let keypair_bs58 = fs::read_to_string(config.keypair_path())? - .trim() - .to_string(); - let keypair = KeyPair::try_from_bs58(keypair_bs58)?; rocket.attach(InternalSignRequest::stage( - _nymd_client, + _nymd_client.clone(), _mix_denom, - keypair, - QueryCommunicationChannel::new(config.get_all_validator_api_endpoints()), + coconut_keypair, + QueryCommunicationChannel::new(_nymd_client), storage.clone().unwrap(), )) } else { @@ -506,8 +494,8 @@ async fn run_validator_api(matches: ArgMatches) -> Result<()> { // try to load config from the file, if it doesn't exist, use default values let id = matches.value_of(ID); - let config = match Config::load_from_file(id) { - Ok(cfg) => cfg, + let (config, _already_inited) = match Config::load_from_file(id) { + Ok(cfg) => (cfg, true), Err(_) => { let config_path = Config::default_config_file_path(id) .into_os_string() @@ -517,11 +505,17 @@ async fn run_validator_api(matches: ArgMatches) -> Result<()> { "Could not load the configuration file from {}. Either the file did not exist or was malformed. Using the default values instead", config_path ); - Config::new() + (Config::new(), false) } }; let config = override_config(config, &matches); + + #[cfg(feature = "coconut")] + if !_already_inited { + init_keypair(&config)?; + } + // if we just wanted to write data to the config, exit if matches.is_present(WRITE_CONFIG_ARG) { return Ok(()); @@ -534,19 +528,38 @@ async fn run_validator_api(matches: ArgMatches) -> Result<()> { // We need a bigger timeout let shutdown = ShutdownNotifier::new(10); + #[cfg(feature = "coconut")] + let coconut_keypair = coconut::keypair::KeyPair::new(); + // let's build our rocket! let rocket = setup_rocket( &config, mix_denom, Arc::clone(&liftoff_notify), signing_nymd_client.clone(), + #[cfg(feature = "coconut")] + coconut_keypair.clone(), ) .await?; - let monitor_builder = setup_network_monitor(&config, system_version, &rocket); + let monitor_builder = setup_network_monitor( + &config, + signing_nymd_client.clone(), + system_version, + &rocket, + ); let validator_cache = rocket.state::().unwrap().clone(); let node_status_cache = rocket.state::().unwrap().clone(); + #[cfg(feature = "coconut")] + { + let dkg_controller = + DkgController::new(&config, signing_nymd_client.clone(), coconut_keypair, OsRng) + .await?; + let shutdown_listener = shutdown.subscribe(); + tokio::spawn(async move { dkg_controller.run(shutdown_listener).await }); + } + // if network monitor is disabled, we're not going to be sending any rewarding hence // we're not starting signing client let validator_cache_listener = if config.get_network_monitor_enabled() { diff --git a/validator-api/src/network_monitor/mod.rs b/validator-api/src/network_monitor/mod.rs index 57e1d64e61..8b85d5fd5a 100644 --- a/validator-api/src/network_monitor/mod.rs +++ b/validator-api/src/network_monitor/mod.rs @@ -7,6 +7,7 @@ use futures::channel::mpsc; use gateway_client::bandwidth::BandwidthController; use std::sync::Arc; use task::ShutdownNotifier; +use validator_client::nymd::SigningNymdClient; use crate::config::Config; use crate::contract_cache::ValidatorCache; @@ -20,6 +21,7 @@ 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::nymd_client::Client; use crate::storage::ValidatorApiStorage; pub(crate) mod chunker; @@ -32,6 +34,7 @@ pub(crate) const ROUTE_TESTING_TEST_NONCE: u64 = 0; pub(crate) struct NetworkMonitorBuilder<'a> { config: &'a Config, + _nymd_client: Client, system_version: String, node_status_storage: ValidatorApiStorage, validator_cache: ValidatorCache, @@ -40,12 +43,14 @@ pub(crate) struct NetworkMonitorBuilder<'a> { impl<'a> NetworkMonitorBuilder<'a> { pub(crate) fn new( config: &'a Config, + _nymd_client: Client, system_version: &str, node_status_storage: ValidatorApiStorage, validator_cache: ValidatorCache, ) -> Self { NetworkMonitorBuilder { config, + _nymd_client, system_version: system_version.to_string(), node_status_storage, validator_cache, @@ -74,11 +79,18 @@ impl<'a> NetworkMonitorBuilder<'a> { ); #[cfg(feature = "coconut")] - let bandwidth_controller = BandwidthController::new( - credential_storage::initialise_storage(self.config.get_credentials_database_path()) - .await, - self.config.get_all_validator_api_endpoints(), - ); + let bandwidth_controller = { + let client = self._nymd_client.0.read().await; + let coconut_api_clients = + validator_client::CoconutApiClient::all_coconut_api_clients(&client) + .await + .expect("Could not query api clients"); + BandwidthController::new( + credential_storage::initialise_storage(self.config.get_credentials_database_path()) + .await, + coconut_api_clients, + ) + }; #[cfg(not(feature = "coconut"))] let bandwidth_controller = BandwidthController::new( credential_storage::initialise_storage(self.config.get_credentials_database_path()) diff --git a/validator-api/src/network_monitor/monitor/sender.rs b/validator-api/src/network_monitor/monitor/sender.rs index 3689a4b6db..4ec0e9ce45 100644 --- a/validator-api/src/network_monitor/monitor/sender.rs +++ b/validator-api/src/network_monitor/monitor/sender.rs @@ -90,15 +90,6 @@ struct FreshGatewayClientData { gateways_status_updater: GatewayClientUpdateSender, local_identity: Arc, gateway_response_timeout: Duration, - - // I guess in the future this struct will require aggregated verification key and.... - // ... something for obtaining actual credential - - // TODO: - // SECURITY: - // for coconut bandwidth credentials we currently have no double spending protection, just to - // get things running we're re-using the same credential for all gateways all the time. - // THIS IS VERY BAD!! bandwidth_controller: BandwidthController, disabled_credentials_mode: bool, } @@ -140,8 +131,6 @@ pub(crate) struct PacketSender { // behaviour is unlikely. active_gateway_clients: ActiveGatewayClients, - // I guess that will be required later on if credentials are got per gateway - // aggregated_verification_key: Arc, fresh_gateway_client_data: Arc, gateway_connection_timeout: Duration, max_concurrent_clients: usize, diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index 33c85634bf..0abefc0875 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -26,10 +26,22 @@ use async_trait::async_trait; #[cfg(feature = "coconut")] use coconut_bandwidth_contract_common::spend_credential::SpendCredentialResponse; #[cfg(feature = "coconut")] -use multisig_contract_common::msg::ProposalResponse; +use coconut_dkg_common::dealer::{ContractDealing, DealerDetails, DealerDetailsResponse}; +#[cfg(feature = "coconut")] +use coconut_dkg_common::types::{EncodedBTEPublicKeyWithProof, EpochState}; +#[cfg(feature = "coconut")] +use coconut_dkg_common::verification_key::{ContractVKShare, VerificationKeyShare}; +#[cfg(feature = "coconut")] +use contracts_common::dealings::ContractSafeBytes; +#[cfg(feature = "coconut")] +use cw3::ProposalResponse; #[cfg(feature = "coconut")] use validator_client::nymd::{ - traits::{CoconutBandwidthQueryClient, MultisigQueryClient, MultisigSigningClient}, + cosmwasm_client::types::ExecuteResult, + traits::{ + CoconutBandwidthQueryClient, DkgQueryClient, DkgSigningClient, MultisigQueryClient, + MultisigSigningClient, + }, AccountId, Fee, }; @@ -281,6 +293,10 @@ where Ok(self.0.read().await.nymd.get_proposal(proposal_id).await?) } + async fn list_proposals(&self) -> crate::coconut::error::Result> { + Ok(self.0.read().await.get_all_nymd_proposals().await?) + } + async fn get_spent_credential( &self, blinded_serial_number: String, @@ -294,6 +310,45 @@ where .await?) } + async fn get_current_epoch_state(&self) -> crate::coconut::error::Result { + Ok(self.0.read().await.nymd.get_current_epoch_state().await?) + } + + async fn get_self_registered_dealer_details( + &self, + ) -> crate::coconut::error::Result { + let self_address = &self.address().await; + Ok(self + .0 + .read() + .await + .nymd + .get_dealer_details(self_address) + .await?) + } + + async fn get_current_dealers(&self) -> crate::coconut::error::Result> { + Ok(self.0.read().await.get_all_nymd_current_dealers().await?) + } + + async fn get_dealings( + &self, + idx: usize, + ) -> crate::coconut::error::Result> { + Ok(self.0.read().await.get_all_nymd_epoch_dealings(idx).await?) + } + + async fn get_verification_key_shares( + &self, + ) -> crate::coconut::error::Result> { + Ok(self + .0 + .read() + .await + .get_all_nymd_verification_key_shares() + .await?) + } + async fn vote_proposal( &self, proposal_id: u64, @@ -308,4 +363,54 @@ where .await?; Ok(()) } + + async fn execute_proposal(&self, proposal_id: u64) -> crate::coconut::error::Result<()> { + self.0 + .read() + .await + .nymd + .execute_proposal(proposal_id, None) + .await?; + Ok(()) + } + + async fn register_dealer( + &self, + bte_key: EncodedBTEPublicKeyWithProof, + announce_address: String, + ) -> Result { + Ok(self + .0 + .write() + .await + .nymd + .register_dealer(bte_key, announce_address, None) + .await?) + } + + async fn submit_dealing( + &self, + dealing_bytes: ContractSafeBytes, + ) -> Result { + Ok(self + .0 + .write() + .await + .nymd + .submit_dealing_bytes(dealing_bytes, None) + .await?) + } + + async fn submit_verification_key_share( + &self, + share: VerificationKeyShare, + ) -> crate::coconut::error::Result { + Ok(self + .0 + .write() + .await + .nymd + .submit_verification_key_share(share, None) + .await?) + } }